前言
前面解决了加载 3D 模型的问题,解决了在 OpenGL 场景中漫游的问题,这时我们调试程序和观察 3D 场景已经很方便了。但是前面加载的模型都还只是白茫茫一片,并不好看,这时因为我们还缺少了 3D 图形学中的两个重要精髓,那就是光照和贴图。今天,我们先来解决光照问题。
关于光照的理论知识到处都有,我就不详细展开了,简单提一下。简单的情况下,一个物体的颜色,主要由环境光、漫反射、镜面反射这几个成分组成。环境光是固定的,漫反射只和光源的方向、法线方向有关,镜面反射和光源方向、法线方向、视线方向有关。有时还要考虑光源的距离。我们只要查到相关的算法,把它写到 Shader 中即可。
今天,我就只是为了让 3D 模型看起来不是白茫茫一片,同时简单学习一下写光照的 Shader,所以我就做了一点简化,那就是只考虑了一个平行光,而且不考虑光源到物体的距离。
具体实现
- 在场景中放一个 3D 模型,设置好它的 Model Matrix 即可,还是使用前面的 Model 类来加载 obj 格式或 stl 格式的 3D 模型,这个过程非常简单。
- 对这个模型渲染需要的 Shader 进行修改,把太阳的位置、摄像机的位置作为参数传递到 Shader 中,然后编写 Shader 文件。
- 在场景中放置一个太阳,使用前面我们自己创建的 Sphere 类正好,设置好 Model Matrix 即可。
- 太阳的渲染比较简单,可以使用以前的 Shader,什么都不用改,正好让太阳显示为白茫茫一片。
经过修改后,我们的主程序大概是下面这样:
#include "../include/app.hpp"
#include "../include/shader.hpp"
#include "../include/model.hpp"
#include "../include/mesh.hpp"
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
class MyApp : public App {
private:
const GLfloat clearColor[4] = {0.2f, 0.3f, 0.3f, 1.0f};
Model buma;
Sphere sun;
Shader* simpleShaderProgram;
Shader* lightingShaderProgram;
public:
void init(){
ShaderInfo simpleShaders[] = {
{GL_VERTEX_SHADER, "simpleShader.vert"},
{GL_FRAGMENT_SHADER, "simpleShader.frag"},
{GL_NONE, ""}
};
ShaderInfo lightingShaders[] = {
{GL_VERTEX_SHADER, "lightingShader.vert"},
{GL_FRAGMENT_SHADER, "lightingShader.frag"},
{GL_NONE, ""}
};
simpleShaderProgram = new Shader(simpleShaders);
lightingShaderProgram = new Shader(lightingShaders);
buma.loadModel("buma.stl");
sun.generateMesh(60);
sun.setup();
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );
}
void display(){
glClearBufferfv(GL_COLOR, 0, clearColor);
glClear(GL_DEPTH_BUFFER_BIT);
glm::mat4 I(1.0f);
glm::vec3 X(1.0f, 0.0f, 0.0f);
glm::vec3 Y(0.0f, 1.0f, 0.0f);
glm::vec3 Z(0.0f, 0.0f, 1.0f);
glm::mat4 view_matrix = glm::lookAt(cameraPosition, cameraPosition + cameraFront, cameraUp);
glm::mat4 projection_matrix = glm::perspective(glm::radians(45.0f), aspect, 1.0f, 100.0f);
glm::mat4 buma_model_matrix = glm::translate(I, glm::vec3(0.0f, -1.5f, 0.0f)) * glm::scale(I, glm::vec3(0.03f, 0.03f, 0.03f)) * glm::rotate(I, glm::radians(0.0f), X);
glm::mat4 sun_model_matrix = glm::translate(I, glm::vec3(0.0f, 5.0f, 10.0f))
* glm::scale(I, glm::vec3(0.1f, 0.1f, 0.1f));
simpleShaderProgram->setViewMatrix(view_matrix);
simpleShaderProgram->setProjectionMatrix(projection_matrix);
simpleShaderProgram->setModelMatrix(sun_model_matrix);
simpleShaderProgram->setCurrent();
sun.render();
lightingShaderProgram->setViewMatrix(view_matrix);
lightingShaderProgram->setProjectionMatrix(projection_matrix);
lightingShaderProgram->setModelMatrix(buma_model_matrix);
glm::vec4 sun_position = glm::vec4(0.0f, 5.0f, 10.0f, 1.0f);
lightingShaderProgram->setVec4("sun_position", sun_position);
lightingShaderProgram->setVec4("eye_position", glm::vec4(cameraPosition,1.0f));
lightingShaderProgram->setCurrent();
buma.render();
}
~MyApp(){
if(lightingShaderProgram != NULL){
delete lightingShaderProgram;
}
if(simpleShaderProgram != NULL){
delete simpleShaderProgram;
}
}
};
DECLARE_MAIN(MyApp)
重点是 Shader 里面的内容,其实也不难,就是计算一下平行光的方向,然后和法线方向点乘一下即可。因为我这个例子中没有考虑镜面反射,故计算视线方向都没有太大必要。
顶点着色器lightingShader.vert内容如下:
#version 460
uniform mat4 model_matrix;
uniform mat4 projection_matrix;
uniform mat4 view_matrix;
uniform vec4 sun_position;
uniform vec4 eye_position;
layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec2 vTexCoord;
out vec4 fColor;
out vec3 fNormal;
out vec2 fTexCoord;
out vec4 fPosition;
out vec4 fSunDirection; //因为只考虑平行光,所以 Sun Direction 通过 Sun Position 和原点进行计算
out vec4 fEyeDirection; //而 Eye Direction 通过 Eye Position 和顶点进行计算
void main(void)
{
mat4 MV_matrix = view_matrix * model_matrix;
gl_Position = projection_matrix * view_matrix * model_matrix * vPosition;
fPosition = MV_matrix * vPosition;
fNormal = normalize(transpose(inverse(mat3(model_matrix))) * vNormal);
fSunDirection = normalize(sun_position - vec4(0.0, 0.0, 0.0, 0.0));
fEyeDirection = normalize(eye_position - model_matrix * vPosition);
fTexCoord = vTexCoord;
fColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
}
片段着色器lightingShader.frag内容如下:
#version 460
layout (location = 0) out vec4 color;
in vec4 fColor;
in vec3 fNormal;
in vec2 fTexCoord;
in vec4 fPosition;
in vec4 fSunDirection;
in vec4 fEyeDirection;
void main(void)
{
float diffuse = max(0.0, dot(fNormal, vec3(fSunDirection)));
color = fColor * diffuse;
}
因为只考虑了漫反射,所以代码非常简单。
使用如下命令编译、运行:
g++ -o Lighting Lighting.cpp -lGL -lglfw -lGLEW -lassimp
./Lighting
就可以看到效果了。
这时,buma 模型,是下面这样的效果:
而上一节中展示的那个坐在长椅上的女孩的模型,是下面这样的效果:
通过在场景中漫游,把太阳显露出来,是下面这样的效果:
总结
在之前的工作基础上,添加光照是比较简单的,主要是把一些重要的信息作为参数传递进 Shader,然后在编写 Shader 文件即可。至于光照涉及到的各种算法,这方面的资料应该比较好找,大家自行学习即可。
在这一节中,我还学到了一点,那就是一个场景可以使用多个 Shader Program 进行渲染,在这里,我的太阳和 3D 模型就使用了不同的 Shader。在以后的 3D 程序中,我们可以设计为每个物体关联一个 Shader。
版权申明
该随笔由京山游侠在2023年07月24日发布于博客园,引用请注明出处,转载或出版请联系博主。QQ邮箱:1841079@qq.com