# LearnOpenGL-04高级OpenGL

# 深度测试

# 深度缓冲

为什么需要深度缓冲?

计算机屏幕是2D平面,但是我们要渲染3D场景。当多个像素在屏幕的同一坐标上重叠时,GPU需要判断,哪个像素离摄像机更近,应该显示出来;哪个更远,应该丢弃。

深度缓冲:防止被阻挡的面渲染到其它面的前面,也叫Z缓冲

Z缓冲:是一块与屏幕分辨率大小相同的显存区域,每个像素位置豆村出一个深度值。

数值范围:0.0-1.0,0.0表示离摄像头最近,1.0表示最远。

深度值在光栅化阶段计算,通常用16 位、24 位或 32 位的浮点数存储。大部分系统是24位的。

# 深度测试

被启用时,OpenGL会将一个片段的深度值与深度缓冲的内容进行对比,如果这个测试通过了的话,深度缓冲将会更新为新的深度值。如果深度测试失败了,片段将会被丢弃。

启用深度测试:

glEnable(GL_DEPTH_TEST);

清楚上次渲染迭代中写入的深度值:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

不更新深度缓冲

glDepthMask(GL_FALSE);

# 深度测试函数

OpenGL允许我们修改深度测试中使用的比较运算符。

glDepthFunc(GL_LESS);
函数 描述
GL_ALWAYS 永远通过深度测试
GL_NEVER 永远不通过深度测试
GL_LESS 在片段深度值小于缓冲的深度值时通过测试
GL_EQUAL 在片段深度值等于缓冲区的深度值时通过测试
GL_LEQUAL 在片段深度值小于等于缓冲区的深度值时通过测试
GL_GREATER 在片段深度值大于缓冲区的深度值时通过测试
GL_NOTEQUAL 在片段深度值不等于缓冲区的深度值时通过测试
GL_GEQUAL 在片段深度值大于等于缓冲区的深度值时通过测试

# 深度值精度

线性深度缓冲:

$$ \begin{equation} F_{depth} = \frac{z - near}{far - near} \end{equation} $$

z是观察空间中介于near和far之间的值,需要映射为[0,1]之间的范围。

它做的就是在z值很小的时候提供非常高的精度,而在z值很远的时候提供更少的精度。

非线性深度缓冲:

$$ \begin{equation} F_{depth} = \frac{1/z - 1/near}{1/far - 1/near} \end{equation} $$

depth_non_linear_graph

# 深度冲突

在两个平面或者三角形非常紧密地平行排列在一起时会发生,深度缓冲没有足够的精度来决定两个形状哪个在前面。结果就是这两个形状不断地在切换前后顺序,这会导致很奇怪的花纹。

# 防止深度冲突

深度冲突是深度缓冲的一个常见问题,当物体在远处时效果会更明显

  • 永远不要把多个物体摆得太靠近,以至于它们的一些三角形会重叠。
  • 尽可能将近平面设置远一些
  • 使用更高精度的深度缓冲

# 模板测试

# 模板缓冲

每个模板值(Stencil Value)是8位的,每个像素有256中模板值,根据模板值来确定是否丢弃或保留片段。

存储每个像素的整数标记值,是控制绘制区域的 “蒙版”,核心作用是标记像素、限制绘制范围。

主要用来实现阴影、镜面、镂空、轮廓描边等特效。

模板缓冲是一块与屏幕分辨率大小相同的显存区域,每个像素位置存储一个整数值(通常是 8 位,取值 0~255)。它的作用就像一张 “模板蒙版”,可以为屏幕上的每个像素打上 “标记”,后续渲染时根据这个标记决定是否绘制该像素。

# 模板测试

当片段着色器处理完一个片段后,模板测试可是执行,可能会丢弃片段,之后深度测试会执行。

stencil_buffer

模板缓冲大体步骤:

  • 启用模板缓冲的写入。
  • 渲染物体,更新模板缓冲的内容。
  • 禁用模板缓冲的写入。
  • 渲染(其它)物体,这次根据模板缓冲的内容丢弃特定的片段。

启用模板缓冲

glEnable(GL_STENCIL_TEST);

每次迭代前清除模板缓冲

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

控制模板缓冲的写入权限

glStencilMask(0xFF); // 允许写入模板缓冲(默认值
glStencilMask(0x00); // 禁止写入模板缓冲(后续操作不会修改模板值)

# 模板缓冲函数

glStencilFunc(GL_EQUAL, 1, 0xFF)

模板测试判断规则:

glStencilFunc(GLenum func, GLint ref, GLuint mask)
  • func:设置模板测试函数(Stencil Test Function)。这个测试函数将会应用到已储存的模板值上和glStencilFunc函数的ref值上。可用的选项有:GL_NEVER、GL_LESS、GL_LEQUAL、GL_GREATER、GL_GEQUAL、GL_EQUAL、GL_NOTEQUAL和GL_ALWAYS。它们的语义和深度缓冲的函数类似。

  • ref:设置了模板测试的参考值(Reference Value)。模板缓冲的内容将会与这个值进行比较。

  • mask:设置一个掩码,它将会与参考值和储存的模板值在测试比较它们之前进行与(AND)运算。初始情况下所有位都为1。

定义模板测试+深度测试后,模板缓冲值的修改规则:

glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
  • sfail:模板测试失败时采取的行为。
  • dpfail:模板测试通过,但深度测试失败时采取的行为。
  • dppass:模板测试和深度测试都通过时采取的行为。

每个选项都可以选用以下一种行为。

行为 描述
GL_KEEP 保持当前储存的模板值
GL_ZERO 将模板值设置为0
GL_REPLACE 将模板值设置为glStencilFunc函数设置的ref
GL_INCR 如果模板值小于最大值则将模板值加1
GL_INCR_WRAP 与GL_INCR一样,但如果模板值超过了最大值则归零
GL_DECR 如果模板值大于最小值则将模板值减1
GL_DECR_WRAP 与GL_DECR一样,但如果模板值小于0则将其设置为最大值
GL_INVERT 按位翻转当前的模板缓冲值

# 物体轮廓

stencil_object_outlining

为物体创建轮廓的步骤:

  1. 启用模板写入。
  2. 在绘制(需要添加轮廓的)物体之前,将模板函数设置为GL_ALWAYS,每当物体的片段被渲染时,将模板缓冲更新为1。
  3. 渲染物体。
  4. 禁用模板写入以及深度测试。
  5. 将每个物体放大一点点。
  6. 使用一个不同的片段着色器,输出一个单独的(边框)颜色。
  7. 再次绘制物体,但只在它们片段的模板值不等于1时才绘制。
  8. 再次启用模板写入和深度测试。

第一步,绘制模型

glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 1, 0xFF); // 总是通过测试,参考值设为1
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // 测试通过时,用1替换模板缓冲值
glStencilMask(0xFF); // 允许写入模板缓冲(0xFF表示全部位可写)

// 绘制原始模型:此时模型覆盖的像素的模板缓冲值会被设为1,其他像素为0
DrawModel();

glStencilMask(0x00); // 禁止写入模板缓冲(后续操作不修改模板值)

第二步,放大模型

glStencilFunc(GL_NOTEQUAL, 1, 0xFF); // 模板值不等于1时,测试通过
glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // 不修改模板缓冲值

// 绘制放大后的模型(用黑色):此时只有模板值为0的区域(模型外)会被绘制,形成轮廓
DrawScaledModel(Color(0, 0, 0));

glDisable(GL_STENCIL_TEST); // 禁用模板测试

# 混合

在片段着色器中,如果一个片段的alpha值低于某个阈值,就丢弃片段

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{             
    vec4 texColor = texture(texture1, TexCoords);
    if(texColor.a < 0.1)
        discard;
    FragColor = texColor;
}

当需要渲染多个半透明图像时,就需要混合。

$$ \begin{equation}\bar{C}{result} = \bar{\color{green}C}{source} * \color{green}F_{source} + \bar{\color{red}C}{destination} * \color{red}F{destination}\end{equation} $$ 混合颜色=源颜色向量源因子值+目标颜色向量目标因子值。

让绿色正方形绘制在红色之上 $$ \begin{equation}\bar{C}_{result} = \begin{pmatrix} \color{red}{0.0} \ \color{green}{1.0} \ \color{blue}{0.0} \ \color{purple}{0.6} \end{pmatrix} * \color{green}{0.6} + \begin{pmatrix} \color{red}{1.0} \ \color{green}{0.0} \ \color{blue}{0.0} \ \color{purple}{1.0} \end{pmatrix} * (\color{red}{1 - 0.6}) \end{equation} $$ blending_equation_mixed

glBlendFunc(GLenum sfactor, GLenum dfactor)

可选项:GL_ZERO、GL_ONE、GL_SRC_COLOR、GL_ONE_MINUS_SRC_COLOR、GL_DST_COLOR等等

# 混合函数

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

为RGB和Alpha设置不同的选项:

glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ZERO);

设置Alpha通道运算符

glBlendEquation(GLenum mode)
  • GL_FUNC_ADD:默认选项,将两个分量相加
  • GL_FUNC_SUBTRACT:将两个分量相减
  • GL_FUNC_REVERSE_SUBTRACT:将两个分量相减,但顺序相反
  • GL_MIN:取两个分量中的最小值
  • GL_MAX:取两个分量中的最大值

# 渲染半透明纹理

设定混合参数

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

不用丢弃片段了

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{             
    FragColor = texture(texture1, TexCoords);
}

深度测试和混合一起使用的话会产生一些麻烦。当写入深度缓冲时,深度缓冲不会检查片段是否是透明的,所以透明的部分会和其它值一样写入到深度缓冲中。

要想让混合在多个物体上工作,我们需要最先绘制最远的物体,最后绘制最近的物体。

当绘制一个有不透明和透明物体的场景的时候:

  1. 先绘制所有不透明的物体。
  2. 对所有透明的物体排序。
  3. 按顺序绘制所有透明的物体。

在场景中排序物体是一个很困难的技术,很大程度上由你场景的类型所决定,更别说它额外需要消耗的处理能力了。完整渲染一个包含不透明和透明物体的场景并不是那么容易。更高级的技术还有次序无关透明度

# 面剔除

空间中观察一个立方体,最多能观察到几个面?3个。

因此没有必要浪费时间绘制看不见的3个面。

# 环绕顺序

faceculling_windingorder

不同的环绕顺序

float vertices[] = {
    // 顺时针
    vertices[0], // 顶点1
    vertices[1], // 顶点2
    vertices[2], // 顶点3
    // 逆时针
    vertices[0], // 顶点1
    vertices[2], // 顶点3
    vertices[1]  // 顶点2  
};

OpenGL在渲染图元的时候将使用这个信息来决定一个三角形是一个正向三角形还是背向三角形。默认情况下,逆时针顶点所定义的三角形将会被处理为正向三角形。

观察者所面向的所有三角形顶点就是我们所指定的正确环绕顺序了,而立方体另一面的三角形顶点则是以相反的环绕顺序所渲染的。这样的结果就是,我们所面向的三角形将会是正向三角形,而背面的三角形则是背向三角形。

哇,太机智了!

faceculling_frontback

启用面剔除

glEnable(GL_CULL_FACE);

要剔除的面的类型

glCullFace(GL_FRONT);
  • GL_BACK:只剔除背向面。
  • GL_FRONT:只剔除正向面。
  • GL_FRONT_AND_BACK:剔除正向面和背向面。

将顺时针的面定义为正向面

glFrontFace(GL_CCW);

注:正向还是背向是由三角形的顶点顺序决定的,和观察者角度无关!

image-20251221143756538

# 帧缓冲

用于写入颜色值的颜色缓冲、用于写入深度信息的深度缓冲和允许我们根据一些条件丢弃特定片段的模板缓冲,这些缓冲结合起来,叫帧缓冲(Framebuffer)。

默认帧缓冲是在创建窗口时生成和配置的。通过创建自己的帧缓冲,可以获得额外的渲染目标。

# 创建帧缓冲

unsigned int fbo;
glGenFramebuffers(1, &fbo);

绑定为激活的帧缓冲

glBindFramebuffer(GL_FRAMEBUFFER, fbo);

一个完整的帧缓冲需要满足以下的条件:

  • 附加至少一个缓冲(颜色、深度或模板缓冲)。
  • 至少有一个颜色附件(Attachment)。
  • 所有的附件都必须是完整的(保留了内存)。
  • 每个缓冲都应该有相同的样本数(sample)。
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
  // 执行胜利的舞蹈

之后所有的渲染操作将会渲染到当前绑定帧缓冲的附件中。

由于我们的帧缓冲不是默认帧缓冲,渲染指令将不会对窗口的视觉输出有任何影响。出于这个原因,渲染到一个不同的帧缓冲被叫做离屏渲染。

要保证所有的渲染操作在主窗口中有视觉效果,我们需要再次激活默认帧缓冲,将它绑定到0.

glBindFramebuffer(GL_FRAMEBUFFER, 0);  // 激活默认帧缓冲,0代表默认帧缓冲ID
glDeleteFramebuffers(1, &fbo);  // 删除帧缓冲对象,删除1个

# 纹理附件

附件是一个内存位置,它能够作为帧缓冲的一个缓冲,可以将它想象为一个图像,可以选择纹理或渲染缓冲对象。

创建纹理

unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

附加到帧缓冲上

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
  • target:帧缓冲的目标(绘制、读取或者两者皆有)
  • attachment:我们想要附加的附件类型。当前我们正在附加一个颜色附件。注意最后的0意味着我们可以附加多个颜色附件。我们将在之后的教程中提到。深度缓冲GL_DEPTH_ATTACHMENT,模板缓冲GL_STENCIL_ATTACHMENT
  • textarget:你希望附加的纹理类型
  • texture:要附加的纹理本身
  • level:多级渐远纹理的级别。我们将它保留为0。

# 渲染到纹理

创建帧缓冲对象

unsigned int framebuffer;
glGenFramebuffers(1, &framebuffer);  // 生成1个帧缓冲对象
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);  // 绑定到GL_FRAMEBUFFER

创建一个纹理图像,我们将它作为一个颜色附件附加到帧缓冲上

// 生成纹理
unsigned int texColorBuffer;
glGenTextures(1, &texColorBuffer);  // 生成一个纹理
glBindTexture(GL_TEXTURE_2D, texColorBuffer);  // 绑定到当前使用的2D纹理目标
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);  // 为当前绑定的 2D 纹理分配显存并设置纹理的基本属性(尺寸、格式、数据)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );  // 纹理采样参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  
glBindTexture(GL_TEXTURE_2D, 0);  // 将当前使用的2D纹理目标,绑定到默认值0

// 将它附加到当前绑定的帧缓冲对象
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texColorBuffer, 0);  

创建渲染缓冲对象

unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo); 
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);  
glBindRenderbuffer(GL_RENDERBUFFER, 0);

将渲染缓冲对象附加到帧缓冲的深度模板附件上

glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);  // 解绑

所以,要想绘制场景到一个纹理上,我们需要采取以下的步骤:

  1. 将新的帧缓冲绑定为激活的帧缓冲,和往常一样渲染场景
  2. 绑定默认的帧缓冲
  3. 绘制一个横跨整个屏幕的四边形,将帧缓冲的颜色缓冲作为它的纹理。

# 特殊后期效果

反相

void main()
{
    FragColor = vec4(vec3(1.0 - texture(screenTexture, TexCoords)), 1.0);
}

灰度化

void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = (FragColor.r + FragColor.g + FragColor.b) / 3.0;
    FragColor = vec4(average, average, average, 1.0);
}

加权灰度

void main()
{
    FragColor = texture(screenTexture, TexCoords);
    float average = 0.2126 * FragColor.r + 0.7152 * FragColor.g + 0.0722 * FragColor.b;
    FragColor = vec4(average, average, average, 1.0);
}

# 核效果

核效果就是从纹理的其他地方采样颜色值

锐化核:玩家打了麻醉剂的效果 $$ \begin{bmatrix}2 & 2 & 2 \ 2 & -15 & 2 \ 2 & 2 & 2 \end{bmatrix} $$

模糊核:模拟玩家醉酒状态,或者没戴眼镜状态 $$ \begin{bmatrix} 1 & 2 & 1 \ 2 & 4 & 2 \ 1 & 2 & 1 \end{bmatrix} / 16 $$

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D screenTexture;

const float offset = 1.0 / 300.0;  
void main()
{
    //vec3 col = texture(screenTexture, TexCoords).rgb;
    //FragColor = vec4(col, 1.0);
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), // 左上
        vec2( 0.0f,    offset), // 正上
        vec2( offset,  offset), // 右上
        vec2(-offset,  0.0f),   // 左
        vec2( 0.0f,    0.0f),   // 中
        vec2( offset,  0.0f),   // 右
        vec2(-offset, -offset), // 左下
        vec2( 0.0f,   -offset), // 正下
        vec2( offset, -offset)  // 右下
    );

    float kernel[9] = float[](
        1.0 / 16, 2.0 / 16, 1.0 / 16,
        2.0 / 16, 4.0 / 16, 2.0 / 16,
        1.0 / 16, 2.0 / 16, 1.0 / 16  
    );

    vec3 sampleTex[9];
    for(int i = 0; i < 9; i++)
    {
        sampleTex[i] = vec3(texture(screenTexture, TexCoords.st + offsets[i]));
    }
    vec3 col = vec3(0.0);
    for(int i = 0; i < 9; i++)
        col += sampleTex[i] * kernel[i];

    FragColor = vec4(col, 1.0);
} 

边缘检测:突出边缘 $$ \begin{bmatrix} 1 & 1 & 1 \ 1 & -8 & 1 \ 1 & 1 & 1 \end{bmatrix} $$

# 后视镜效果

后视镜位置

    float quadVertices[] = { // vertex attributes for a quad that fills the entire screen in Normalized Device Coordinates. NOTE that this plane is now much smaller and at the top of the screen
        // positions   // texCoords
        -0.3f,  1.0f,  0.0f, 1.0f,
        -0.3f,  0.4f,  0.0f, 0.0f,
         0.3f,  0.4f,  1.0f, 0.0f,

        -0.3f,  1.0f,  0.0f, 1.0f,
         0.3f,  0.4f,  1.0f, 0.0f,
         0.3f,  1.0f,  1.0f, 1.0f
    };

后视镜视野视图矩阵

shader.use();
glm::mat4 model = glm::mat4(1.0f);
camera.Yaw   += 180.0f; // rotate the camera's yaw 180 degrees around
camera.ProcessMouseMovement(0, 0, false); 
glm::mat4 view = camera.GetViewMatrix();
camera.Yaw   -= 180.0f; // reset it back to its original orientation
camera.ProcessMouseMovement(0, 0, true); 
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
shader.setMat4("view", view);
shader.setMat4("projection", projection);

# 立方体贴图

立方体贴图有6个面,每个面都需需要一个纹理。

纹理目标 方位
GL_TEXTURE_CUBE_MAP_POSITIVE_X
GL_TEXTURE_CUBE_MAP_NEGATIVE_X
GL_TEXTURE_CUBE_MAP_POSITIVE_Y
GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
GL_TEXTURE_CUBE_MAP_POSITIVE_Z
GL_TEXTURE_CUBE_MAP_NEGATIVE_Z

纹理目标背后的int值是线性增加的,可以从GL_TEXTURE_CUBE_MAP_POSITIVE_X开始遍历他们。

int width, height, nrChannels;  //  纹理图片宽高,颜色通道数
unsigned char *data;   // 加载后的图片像素数据
for(unsigned int i = 0; i < textures_faces.size(); i++)
{
    data = stbi_load(textures_faces[i].c_str(), &width, &height, &nrChannels, 0);
    glTexImage2D(  // 将加载后的数据赋值给立方体贴图的第i个面
        GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 
        0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
    );
}

片段着色器中,采样器也是使用不同类型的

in vec3 textureDir; // 代表3D纹理坐标的方向向量
uniform samplerCube cubemap; // 立方体贴图的纹理采样器

void main()
{             
    FragColor = texture(cubemap, textureDir);
}

cubemaps_skybox

# 优化

目前我们是首先渲染天空盒,之后再渲染场景中的其它物体。这样子能够工作,但不是非常高效。

在提前深度测试通过的地方渲染天空盒的片段就可以了,很大程度上减少了片段着色器的调用。

将输出位置的z分量等于w分量,z分量永远等于1,经过透视除法之后,z 会变为w/w=1

void main()
{
    TexCoords = aPos;
    vec4 pos = projection * view * vec4(aPos, 1.0);
    gl_Position = pos.xyww;
}

gl_Position 是顶点着色器的内置输出变量,它存储了顶点经过模型-视图-投影矩阵变换后的齐次裁剪空间坐标

OpenGL 会自动将裁剪空间坐标转换为屏幕空间坐标,这个过程叫做透视除法

同时,需要将深度测试的函数设置为GL_LEQUAL(小于等于),而不是默认的GL_LESS(小于)。这样才能正常渲染天空盒。

# 环境映射

反射效果

顶点着色器

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;

out vec3 Normal;
out vec3 Position;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    // 将顶点的局部空间法向量转换为世界空间的法向量
    Normal = mat3(transpose(inverse(model))) * aNormal;
    // 将顶点的局部空间位置转换为世界空间位置
    Position = vec3(model * vec4(aPos, 1.0));
    // 将顶点的局部空间位置转换为裁剪空间坐标
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

片源着色器

#version 330 core
out vec4 FragColor;

in vec3 Normal;
in vec3 Position;

uniform vec3 cameraPos;
uniform samplerCube skybox;

void main()
{             
    vec3 I = normalize(Position - cameraPos); // 摄像机方向向量
    vec3 R = reflect(I, normalize(Normal));  // 计算反射方向向量
    FragColor = vec4(texture(skybox, R).rgb, 1.0);   //  从天空盒纹理采样,参数是立方体贴图和采样方向
}

当反射应用到一整个物体上(像是箱子)时,这个物体看起来就像是钢或者铬这样的高反射性材质。现实中大部分的模型都不具有完全反射性。我们可以引入反射贴图

折射效果

折射可以使用GLSL内建的refract函数实现,它需要一个法向量、一个观察方向和两个材质之间的折射率

材质 折射率
空气 1.00
1.33
1.309
玻璃 1.52
钻石 2.42

修改片源着色器

void main()
{    
    //vec3 I = normalize(Position - cameraPos);
    //vec3 R = reflect(I, normalize(Normal));
    //FragColor = vec4(texture(skybox, R).rgb, 1.0);
    float ratio = 1.00 / 1.52;
    vec3 I = normalize(Position - cameraPos);
    vec3 R = refract(I, normalize(Normal), ratio);  // 自带折射函数
    FragColor = vec4(texture(skybox, R).rgb, 1.0);
}

# 高级数据

glBufferSubData填充缓冲的特定区域

顶点数据可以分批传入缓存

float positions[] = { ... };
float normals[] = { ... };
float tex[] = { ... };
// 填充缓冲
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(positions), &positions);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions), sizeof(normals), &normals);
glBufferSubData(GL_ARRAY_BUFFER, sizeof(positions) + sizeof(normals), sizeof(tex), &tex);

将缓冲的内容复制到另一个缓冲中

void glCopyBufferSubData(GLenum readtarget, GLenum writetarget, GLintptr readoffset,
                         GLintptr writeoffset, GLsizeiptr size);

# 高级GLSL

GLSL的内建变量: gl_position gl_FragCoord

# 顶点着色器变量

gl_PointSize:设置渲染出来的点的大小

glEnable(GL_PROGRAM_POINT_SIZE);

gl_VertexID:储存了正在绘制顶点的当前ID

当(使用glDrawElements)进行索引渲染的时候,这个变量会存储正在绘制顶点的当前索引。当(使用glDrawArrays)不使用索引进行绘制的时候,这个变量会储存从渲染调用开始的已处理顶点数量。

# 片段着色器变量

gl_FragCoord:当前正在处理的片元在屏幕上的坐标和深度

void main()
{         
    // 当像素x坐标小于400时,绘制不同的颜色
    if(gl_FragCoord.x < 400)
        FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    else
        FragColor = vec4(0.0, 1.0, 0.0, 1.0);        
}

gl_FrontFacing:告诉我们当前片段是属于正向面的一部分还是背向面的一部分

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D frontTexture;
uniform sampler2D backTexture;

void main()
{             
    // 内部和外部使用不同的纹理
    if(gl_FrontFacing)
        FragColor = texture(frontTexture, TexCoords);
    else
        FragColor = texture(backTexture, TexCoords);
}

gl_FragDepth:在着色器中设置片段的深度值

自己设置深度值,OpenGL会禁用所有的提前深度测试。因为OpenGL无法在片段着色器运行之前得知片段将拥有的深度值。

从OpenGL4.2开始,在片段着色器顶部使用深度条件重新声明gl_FragDepth变量

layout (depth_<condition>) out float gl_FragDepth;

condition取值

条件 描述
any 默认值。提前深度测试是禁用的,你会损失很多性能
greater 你只能让深度值比gl_FragCoord.z更大
less 你只能让深度值比gl_FragCoord.z更小
unchanged 如果你要写入gl_FragDepth,你将只能写入gl_FragCoord.z的值

# 接口块

管理封装着色器的输入输出,使用关键字inout

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

// 块名
out VS_OUT
{
    vec2 TexCoords;
} vs_out;  // 实例名

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    vs_out.TexCoords = aTexCoords;
}  

在片段着色器中: 块名一样,实例名可以不同

#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{             
    FragColor = texture(texture, fs_in.TexCoords);   
}

# Uniform缓冲对象

UBO(Uniform Buffer Object)

#version 330 core
layout (location = 0) in vec3 aPos;

layout (std140) uniform Matrices
{
    mat4 projection;
    mat4 view;
};

uniform mat4 model;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

Uniform块中的变量可以直接访问,不需要加块名作为前缀。

# Uniform块布局

std140布局声明了每个变量的偏移量都是由一系列规则所决定的,这显式地声明了每个变量类型的内存布局。一个变量的对齐字节偏移量必须等于基准对齐量的倍数。

advanced_glsl_binding_points

多个shader可以绑定到同一个绑定点,每个绑定点又绑定某个UBO,这样shader里的uniform块就和外部的UBO数据对应上了。

// 绑定块
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);

// 绑定缓冲对象
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
// 或
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);

OpenGL4.2开始

layout(std140, binding = 2) uniform Lights { ... };

# 几何着色器

几何着色器在顶点和片段着色器之间,几何着色器的输入是一个图元(点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。

#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;

void main() {    
    gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0); 
    EmitVertex();

    gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
    EmitVertex();

    EndPrimitive();
}

调用EmitVertex,gl_Position会被添加到图元中。EndPrimitive被调用,所有发射的顶点都会合成指定的输出渲染图元。

Layout 输入布局修饰符可以接受以下任何一个图元值:

  • points:绘制GL_POINTS图元时(1)。
  • lines:绘制GL_LINES或GL_LINE_STRIP时(2)
  • lines_adjacency:GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4)
  • triangles:GL_TRIANGLES、GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3)
  • triangles_adjacency:GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)

输出布局修饰符:

  • points
  • line_strip
  • triangle_strip

几何着色器的输入是一个图元的所有顶点,用索引获取顶点。

OpenGL内建变量gl_in,内部看起来:

in gl_Vertex
{
    vec4  gl_Position;
    float gl_PointSize;
    float gl_ClipDistance[];
} gl_in[];

# 实例化

实例化:将数据一次性发送给GPU,使用一个绘制函数让OpenGL利用这些数据绘制多个物体,节省CPU到GPU的通信。

最后一个参数是需要绘制的实例数量

glDrawArraysInstanced(GL_TRIANGLES, 0, 6, 100);

内建变量gl_InstanceID:从0开始,每个实例被渲染时递增1

#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;

out vec3 fColor;

uniform vec2 offsets[100];

void main()
{
    vec2 offset = offsets[gl_InstanceID];
    gl_Position = vec4(aPos + offset, 0.0, 1.0);
    fColor = aColor;
}

使用顶点属性是,顶点着色器的每次运行都会让GLSL获取新一组适用于当前顶点的属性。

当将顶点属性定义为一个实例化数组时。顶点着色器只需要对每个实例更新顶点内容。

所有实例共享一套顶点数据。

glVertexAttribDivisor(2, 1);

它的第一个参数是需要的顶点属性,第二个参数是属性除数(Attribute Divisor)。

  • 默认情况下,属性除数是0,告诉OpenGL我们需要在顶点着色器的每次迭代时更新顶点属性。
  • 将它设置为1时,我们告诉OpenGL我们希望在渲染一个新实例的时候更新顶点属性。
  • 而设置为2时,我们希望每2个实例更新一次属性,以此类推。

# 抗锯齿

多重采样抗锯齿(Multisample Anti-aliasing, MSAA)

光栅器是位于最终处理过的顶点之后到片段着色器之前所有经过的算法与过程的总和。

多重采样中不再使用像素中心的单一采样点,而采用以特定图形排列的4个子采样点。

anti_aliasing_sample_points

工作方式:无论三角形遮盖了多少个采样点,每个像素只运行一次片段着色器,片段着色器使用插值到像素中心的顶点数据。被覆盖的子采样点数量决定了像素颜色对帧缓冲的影响程度。例如上图4个采样点有2个被遮盖了,所以三角形的颜色有一半与帧缓冲的颜色进行混合,形成一种淡蓝色。

三角形的不平滑边缘被稍浅的颜色所包围后,从远处观察时就会显得更加平滑了。

4个采样点

glfwWindowHint(GLFW_SAMPLES, 4);

# 总结

# 缓冲对象生命周期

OpenGL的缓冲对象遵循生命周期:生成-绑定-使用-销毁

# 常见的GL_XXX目标常量和 “当前状态” 的对应关系

常量标识符 对应的目标类型 OpenGL 维护的 “当前状态”
GL_FRAMEBUFFER 帧缓冲 当前绑定的帧缓冲对象 ID(默认是 0)
GL_ARRAY_BUFFER 顶点数组缓冲 当前绑定的顶点缓冲对象(VBO)ID
GL_TEXTURE_2D 2D 纹理 当前绑定的 2D 纹理对象 ID
GL_RENDERBUFFER 渲染缓冲 当前绑定的渲染缓冲对象(RBO)ID
  1. GL_FRAMEBUFFER不是变量,是 OpenGL 的枚举常量,用于指定操作的目标类型(这里是帧缓冲)。
  2. OpenGL 作为状态机,会为每个GL_XXX目标类型维护 “当前绑定的对象 ID”
  3. 后续对该类型的所有操作,都会作用于这个 “当前绑定的对象”,直到你再次调用绑定函数切换对象。

# 反射、折射

Opengl 计算反射、折射本质上需要纹理贴图和方向向量两个东西, 反射和折射计算方向向量各有一个函数,便于在纹理上采样。

Last Updated: 1/30/2026, 8:32:15 AM