Linux 下的 OpenGL 之路(七):光照

发布时间 2023-07-24 11:27:12作者: 京山游侠

前言

前面解决了加载 3D 模型的问题,解决了在 OpenGL 场景中漫游的问题,这时我们调试程序和观察 3D 场景已经很方便了。但是前面加载的模型都还只是白茫茫一片,并不好看,这时因为我们还缺少了 3D 图形学中的两个重要精髓,那就是光照和贴图。今天,我们先来解决光照问题。

关于光照的理论知识到处都有,我就不详细展开了,简单提一下。简单的情况下,一个物体的颜色,主要由环境光、漫反射、镜面反射这几个成分组成。环境光是固定的,漫反射只和光源的方向、法线方向有关,镜面反射和光源方向、法线方向、视线方向有关。有时还要考虑光源的距离。我们只要查到相关的算法,把它写到 Shader 中即可。

今天,我就只是为了让 3D 模型看起来不是白茫茫一片,同时简单学习一下写光照的 Shader,所以我就做了一点简化,那就是只考虑了一个平行光,而且不考虑光源到物体的距离。

具体实现

  1. 在场景中放一个 3D 模型,设置好它的 Model Matrix 即可,还是使用前面的 Model 类来加载 obj 格式或 stl 格式的 3D 模型,这个过程非常简单。
  2. 对这个模型渲染需要的 Shader 进行修改,把太阳的位置、摄像机的位置作为参数传递到 Shader 中,然后编写 Shader 文件。
  3. 在场景中放置一个太阳,使用前面我们自己创建的 Sphere 类正好,设置好 Model Matrix 即可。
  4. 太阳的渲染比较简单,可以使用以前的 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