加载中...
返回

【图形学学习笔记】【OpenGL】光照和着色

reference: 《计算机图形学编程(使用OpenGL和C++)》

1 光照模型

1.1 光源

书中介绍的ADS光照模型:

A——环境光反射(Ambient Reflection):模拟低级光照,影响场景中所有物体。

D——漫反射(Diffuse Reflection):根据光线的入射角度调整物体亮度。

S——镜面反射(Specular Reflection):展示物体的光泽,通过在物体表面上,光线直接地反射到我们的眼睛的位置,策略性地放置适当大小的高光来实现。

一个光源对物体的影响,可以体现在这三个反射。例如要描述一道红光,它的声明可以是:

// RGBA
std::array<float, 4> redLightAmbient { 0.1f, .0f, .0f, 1.0f }; 
std::array<float, 4> redLightDiffuse { 1.0f, .0f, .0f, 1.0f }; 
std::array<float, 4> redLightSpecular { 1.0f, .0f, .0f, 1.0f }; 

这表明这道光对于环境光有微小的贡献( 0.1 );而当它照到物体上时,为漫反射和镜面反射分别提供RGBA为 { 1.0f, .0f, .0f, 1.0f } 的光强,用于最终的颜色计算。

反射与光的入射角有关。这里只先考虑一个 位置光 ,它是位于3D场景下某个坐标的点光源,对于一个物体上的不同顶点,这个点光源发出的光入射到这些顶点的角度不同,因此产生不同的反射效果。位置 (5, 2, -3) 处的红色点光源,描述如下:

// RGBA
std::array<float, 4> redLightAmbient { 0.1f, .0f, .0f, 1.0f };
std::array<float, 4> redLightDiffuse { 1.0f, .0f, .0f, 1.0f };
std::array<float, 4> redLightSpecular { 1.0f, .0f, .0f, 1.0f };
// position
std::array<float, 3> redLightPosition { 5.0f, 2.0f, -3.0f };

1.2 材质

一个物体最终的光照着色效果,由光源和物体本身的材质决定。光源主要描述光的颜色和位置,而材质描述物体接受光照后反射的效果。

在ADS模型下,物体的材质也围绕这三种反射来描述。

光源加在物体上的环境光、漫反射光、镜面光分别体现出物体材质上的三种不同反射,它们的建模策略如下:

  • 环境光:直接用环境光加在物体上的光强乘以材质本身对环境光的反射参数即可。
  • 漫反射光:与光源提供的漫反射光强、材质的漫反射参数、入射角(反射角)有关。随着入射角变大,反射出来的光会减弱,当入射角的余弦值变为负(大于90°或小于-90°),反射出来的光是 0
  • 镜面光:与光源提供的镜面光强、材质的镜面反射参数、入射角(反射角)、观察方向有关。镜面光虽然不会随着入射角变大而变弱,但是会随着相机与反射光的夹角变大而变弱。

对于镜面反射,不同物体的 光泽度 又不同,随着光泽度的增加,镜面反射的现象应该变显著,反射的光会较少地逸散,当相机与反射光的夹角变大时,光强的衰减会变得明显。使用余弦的指数来作为镜面反射衰减的建模:

以黄金为例,它的材质描述为:

// ADS反射参数
std::array<float, 4> glodMaterialAmbient { 0.24725f, 0.1995f, 0.0745f, 1.0f }; 
std::array<float, 4> glodMaterialDiffuse { 0.75164f, 0.60648f, 0.22648f, 1.0f }; 
std::array<float, 4> glodMaterialSpecular { 0.628281f, 0.555802f, 0.366065f, 1.0f }; 
// 光泽,即反射光衰减的余弦的指数
std::array<float, 1> glodMaterialShininess { 51.2f }; 

2 Gouraud着色

把我们第1节理解到的光照模型作用在物体的 顶点 上,就是Gouraud着色的基本思路:

  1. 确定每个顶点的颜色
  2. 栅格化过程中插入像素时对颜色(即光照)也进行插值

首先定义材质,这是很简单的:

namespace utils
{
    // Gold material properties
    namespace Gold
    {
        std::array<float, 4> getAmbient()
        {
            return {0.24725f, 0.1995f, 0.0745f, 1.0f};
        }

        std::array<float, 4> getDiffuse()
        {
            return {0.75164f, 0.60648f, 0.22648f, 1.0f};
        }

        std::array<float, 4> getSpecular()
        {
            return {0.628281f, 0.555802f, 0.366065f, 1.0f};
        }

        float getShininess()
        {
            return 51.2f;
        }
    }
}

随后是光源:

// ===== light =====
glm::vec3 gInitialLightLoc = glm::vec3(5.0f, 2.0f, 2.0f);
std::array<float, 4> gGlobalAmbient { 0.7f, 0.7f, 0.7f, 1.0f };
std::array<float, 4> gLightAmbient { .0f, .0f, .0f, 1.0f };
std::array<float, 4> gLightDiffuse { 1.0f, 1.0f, 1.0f, 1.0f };
std::array<float, 4> gLightSpecular { 1.0f, 1.0f, 1.0f, 1.0f };

由于每个顶点的位置不同,没法在C++程序中计算光照,必须把这些信息传入顶点着色器中计算,因此定义一个函数传输这些信息:

// 全局变量
GLuint globalAmbLoc, lAmbLoc, lDiffLoc, lSpecLoc, posLoc, mAmbLoc, mDiffLoc, mSpecLoc, mShiLoc;

void installLights()
{
    globalAmbLoc = glGetUniformLocation(renderingProgram, "globalAmbient");
    lAmbLoc = glGetUniformLocation(renderingProgram, "light.ambient");
    lDiffLoc = glGetUniformLocation(renderingProgram, "light.diffuse");
    lSpecLoc = glGetUniformLocation(renderingProgram, "light.specular");
    posLoc = glGetUniformLocation(renderingProgram, "light.position");
    
    mAmbLoc = glGetUniformLocation(renderingProgram, "material.ambient");
    mDiffLoc = glGetUniformLocation(renderingProgram, "material.diffuse");
    mSpecLoc = glGetUniformLocation(renderingProgram, "material.specular");
    mShiLoc = glGetUniformLocation(renderingProgram, "material.shininess");

    glProgramUniform3fv(renderingProgram, posLoc, 1, glm::value_ptr(gCurrentLightPos));

    glProgramUniform4fv(renderingProgram, globalAmbLoc, 1, gGlobalAmbient.data());
    glProgramUniform4fv(renderingProgram, lAmbLoc, 1, gLightAmbient.data());
    glProgramUniform4fv(renderingProgram, lDiffLoc, 1, gLightDiffuse.data());
    glProgramUniform4fv(renderingProgram, lSpecLoc, 1, gLightSpecular.data());
    
    glProgramUniform4fv(renderingProgram, mAmbLoc, 1, gMaterialAmbient.data());
    glProgramUniform4fv(renderingProgram, mDiffLoc, 1, gMaterialDiffuse.data());
    glProgramUniform4fv(renderingProgram, mSpecLoc, 1, gMaterialSpecular.data());
    glProgramUniform1f(renderingProgram, mShiLoc, gMaterialShininess);
}

display 函数中,还有最后一个特殊的计算是mv矩阵的逆转置矩阵,用于计算每个顶点完成mv变换后的法向量。

void display(GLFWwindow *window, double currentTime)
{
    glUseProgram(renderingProgram);

    // initialization
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glEnable(GL_CULL_FACE);
    glFrontFace(GL_CCW);
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);

    // 启用混合以支持透明度
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    // M 矩阵在GLSL内构建,V矩阵在cpp构建
    projLoc = glGetUniformLocation(renderingProgram, "proj_matrix");
    mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix");
    normLoc = glGetUniformLocation(renderingProgram, "norm_matrix");

    vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
    mMat = glm::translate(glm::mat4(1.0f), glm::vec3(pyramidX, pyramidY, pyramidZ));
    // 透视矩阵先赋值
    glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat));
    // 稍微沿X轴旋转一些,观察模型底部
    glm::mat4 rotxMat = glm::rotate(glm::mat4(1.0f), (float)currentTime, glm::vec3(1.0f, 1.0f, 0.0f));
    glm::mat4 scalMat = glm::scale(glm::mat4(1.0f), glm::vec3(2.f, 2.f, 2.f));
    mMat = rotxMat * scalMat * mMat;
    mvMat = vMat * mMat;
    glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat));

    // 逆转置矩阵
    invTrMat = glm::transpose(glm::inverse(mvMat));
    glUniformMatrix4fv(normLoc, 1, GL_FALSE, glm::value_ptr(invTrMat));

    // ===== 其余无关代码略 =====
}

现在,顶点着色器拿到了光照计算所需的一切,

#version 410

struct PosotionLight
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    vec3 position;
};

struct Material
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float shininess;
};

layout(location = 0) in vec3 position;
layout(location = 1) in vec2 textureCoord;
layout(location = 2) in vec3 normal;

uniform mat4 proj_matrix;
uniform mat4 v_matrix;
uniform mat4 m_matrix;
uniform mat4 mv_matrix;
uniform mat4 norm_matrix;

uniform Material material;
uniform PosotionLight light;
uniform vec4 globalAmbient;

out vec4 varying_color;

// vertex.glsl
void main()
{
    // 顶点位置
    vec4 P = mv_matrix * vec4(position, 1.0);
    // 法向量转换到视觉空间(不能直接用mv,要用mv的逆转置矩阵)
    vec3 N = normalize((norm_matrix * vec4(normal, 1.0)).xyz);
    // 视觉空间光照向量(顶点到光源)
    vec3 L = normalize(light.position - P.xyz);
    // 视觉向量(顶点发出的光传入相机)
    vec3 V = normalize(-P.xyz);
    // 镜面反射向量
    vec3 R = normalize(reflect(-L, N));

    // 环境光
    vec3 ambient = (globalAmbient * material.ambient + light.ambient * material.ambient).xyz;
    // 漫反射
    vec3 diffuse = (light.diffuse.xyz * material.diffuse.xyz * max(dot(L, N), 0.0));
    // 镜面反射
    vec3 specular = (light.specular.xyz * material.specular.xyz * pow(max(dot(R, V), 0.0), material.shininess));

    varying_color = vec4((ambient + diffuse + specular), material.diffuse.a);
    gl_Position = proj_matrix * mv_matrix * vec4(position, 1.0);
}

而片段着色器只要承接颜色就好:

#version 410

in vec4 varying_color;

out vec4 color;

// fragment.glsl
void main()
{
    color = varying_color;
}

导出成gif后画质炸了,实际还是比较平滑的

3 Phong着色

在顶点着色器中进行ADS光照计算带来的问题是镜面高光有片面感。原因是光照计算只发生在顶点,三角形的面积范围内的像素只是进行了插值。

Phong将ADS计算放到片段着色器中,在顶点着色器中计算法向量和光照向量,藉由插值机制使得片段着色器内每个像素都能进行ADS计算,即使得光照计算按像素而非按顶点完成。

实现这个算法只需要对Gouraud着色的实现进行微小调整,顶点着色器只计算N和L:

#version 410

struct PosotionLight
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    vec3 position;
};

struct Material
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float shininess;
};

layout(location = 0) in vec3 position;
layout(location = 1) in vec2 textureCoord;
layout(location = 2) in vec3 normal;

uniform mat4 proj_matrix;
uniform mat4 v_matrix;
uniform mat4 m_matrix;
uniform mat4 mv_matrix;
uniform mat4 norm_matrix;

uniform Material material;
uniform PosotionLight light;
uniform vec4 globalAmbient;

out vec3 varyingNormal;
out vec3 varyingLightDir;
out vec3 varyingVertPos;

// vertex.glsl
void main()
{
    varyingVertPos = (mv_matrix * vec4(position, 1.0)).xyz;
    varyingLightDir = normalize(light.position - varyingVertPos);
    varyingNormal = (norm_matrix * vec4(normal, 1.0)).xyz;

    gl_Position = proj_matrix * mv_matrix * vec4(position, 1.0);
}

片段着色器中完成剩余的ADS计算:

#version 410


struct PosotionLight
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    vec3 position;
};

struct Material
{
    vec4 ambient;
    vec4 diffuse;
    vec4 specular;
    float shininess;
};

layout(location = 0) in vec3 position;
layout(location = 1) in vec2 textureCoord;
layout(location = 2) in vec3 normal;

uniform mat4 proj_matrix;
uniform mat4 v_matrix;
uniform mat4 m_matrix;
uniform mat4 mv_matrix;
uniform mat4 norm_matrix;

uniform Material material;
uniform PosotionLight light;
uniform vec4 globalAmbient;

in vec3 varyingNormal;
in vec3 varyingLightDir;
in vec3 varyingVertPos;

out vec4 color;

void main()
{
    vec3 N = normalize(varyingNormal);
    vec3 L =  normalize(-varyingLightDir);
    // 视觉向量(顶点发出的光传入相机)
    vec3 V = normalize(-varyingVertPos.xyz);
    // 镜面反射向量
    vec3 R = normalize(reflect(-varyingLightDir, varyingNormal));

    // 环境光
    vec3 ambient = (globalAmbient * material.ambient + light.ambient * material.ambient).xyz;
    // 漫反射
    vec3 diffuse = (light.diffuse.xyz * material.diffuse.xyz * max(dot(L, N), 0.0));
    // 镜面反射
    vec3 specular = (light.specular.xyz * material.specular.xyz * pow(max(dot(R, V), 0.0), material.shininess));

    color = vec4((ambient + diffuse + specular), material.diffuse.a);
}

效果对比(左Gouraud着色,右Phong着色):

4 Blinn-Phong着色

todo…

有朋自远方来,不亦说乎?