计算机图形:高级GLSL

内建变量

OpenGL + GLSL用顶点属性、uniform和采样器,将数据从CPU传到GPU. 此外,GLSL还定义了几个gl_前缀变量,管理着色器的输入、输出.

顶点着色器变量

gl_Position

gl_Position(vec4)是顶点着色器的裁剪空间输出位置向量. 如果想在屏幕上显示任何东西,在顶点着色器中设置gl_Position即可.

gl_PointSize

gl_PointSize(float)控制渲染出来的点的大小. 如果想每个顶点的大小,那么可修改该值.

点大小功能默认禁止. 启用方式:

glEnable(GL_PROGRAM_POINT_SIZE);

e.g. 将点大小设置为裁剪空间位置z值,即顶点到相机距离. 那么,距离相机越远的地方,点越大

可设置顶点着色器:

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

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

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

gl_VertexID

gl_VertexID存储了正在绘制顶点的ID. 含义如下:

1)当使用glDrawElements索引渲染时,它存储正在绘制顶点的当前索引;
2)当使用glDrawArrays渲染时,它存储从渲染调用开始的已处理顶点数量.

不同于gl_Position和gl_PointSize都是输出变量,gl_VertexID是输入变量.

片段着色器变量

glFragCoord

glFragCoord 当前片段的窗口空间坐标,z分量对应片段的深度值,x、y分量对应片段的窗口空间(Window Space)坐标,原点是窗口左下角.

窗口(视口)由glViewport设置.

e.g. 将800x600的窗口左半部物体颜色设置为红色,右半部的设置为绿色

可编写片段着色器:

...
void main()
{
    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

gl_FrontFacing(bool)输入变量,告诉我们当前片段所属面的朝向(正面 or 反面). 前提是不启用 GL_FACE_CULL.

值含义:

  • true 当前片段是正面
  • false 当前片段是反面

简化版管线:

  1. 顶点着色器 → 顶点变换
  2. 图元组装(Primitive Assembly)
  3. 面剔除(Face Culling)
  4. 裁剪(Clipping)
  5. 透视除法 → NDC
  6. 视口变换
  7. 光栅化(Rasterization)
  8. 片段着色 → 深度 / 模板 / 混合 → 帧缓冲

OpenGL能根据顶点环绕顺序决定一个面是正面还是反面. 但在着色器中,如何判断呢?

首先,得先不使用面剔除,因为面剔除阶段发生在片段着色器之前,到片段着色器时,反面已经被剔除.

然后,可以用 gl_FrontFacing 来获取当前面的朝向.

e.g. 正面和反面使用不同纹理着色

#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

gl_FragCoord(输入变量,vec3)能获取当前片段的窗口空间坐标,包括深度值,但它是只读变量,无法修改.

gl_FragDepth(输出变量,float)支持对当前片段的深度值修改,范围0.0~1.0 .

如果想设置片段深度值,那么在片段着色器中,直接写gl_FragDepth即可:

gl_FragDepth = 0.0; // 当前片段深度值设为0.0

如果没有写gl_FragDepth,则会自动取用gl_FragCoord.z的值.

缺点: 只要在片段着色器中写gl_FragDepth,OpenGL就会禁用所有提前深度测试(Early Depth Testing). 禁用原因:OpenGL无法在片段着色器运行前知道片段将拥有的深度值,因为可能会被着色器修改.

因此,写入gl_FragDepth就需要考虑其带来的性能影响.

不过,从OpenGL 4.2开始,仍可以对两者调和,在片段着色器顶部使用深度条件(Depth Condition)重新声明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的值

如果将 condition 设置为 greater,OpenGL就能假设你只会写入比当前片段深度值更大的值. 如此,当 深度值 < 片段深度值 时,OpenGL仍能提前深度测试.

接口块

当我们想从 顶点着色器 向 片段着色器 发送数据时,可以声明对应的输入/输出变量. 像下面这样:

顶点着色器

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

out vec3 Normal; // 输出到片段着色器

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

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

片段着色器

#version 330 core

in vec3 Normal; // 从顶点着色器输入

void main()
{
    // 直接读取Normal
}

如果是大量有组织的数据呢,比如 数组、结构体?

可以使用接口块(Interface Block). 其声明类似于struct,不过需要用in/out关键字来定义(输入或输出).

顶点着色器:

#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);
}

只要2个接口块的名字("VS_OUT")一样,其对应输入、输出就会匹配起来.

Uniform

Uniform 缓冲对象

Uniform缓冲对象(Uniform Buffer Object,简称UBO)是OpenGL中用于批量管理、高效传递多个 Uniform 变量的核心工具,它解决了传统 glUniform* 逐个设置变量的性能瓶颈和代码冗余问题

如果没有UBO,那么需要逐个uniform变量设置;而有了Uniform 缓冲对象,只需要手动设置相关的uniform一次.

// 传统方式:逐个设置矩阵,代码冗余+性能差
shader.setMat4("projection", projection);
shader.setMat4("view", view);
shader.setVec3("lightPos", lightPos);
shader.setFloat("lightIntensity", 1.0f);

e.g. 我们在一个简单的着色器中,将projection,view矩阵存储到一个Uniform块(Uniform Block)中:

#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);
}

声明名为"Matrices"的Uniform块(Uniform Block),存储2个4x4块,块中变量可直接访问,无需加块名作为前缀. 接下来,在OpenGL代码中将这些矩阵值存入缓冲中,每个声明了该Uniform块的着色器都能访问这些矩阵.

其中,layout (std140) 是Uniform块的内存布局限定符,规定了GPU中Uniform块内变量的内存排布规则,作用是让CPU、GPU对Uniform块的内存布局达成“共识”,避免因编译器优化导致内存偏移不一致,进而引发错误.

Uniform 块布局

Uniform块的内容存储在一个缓冲对象上,实际是一块预留内存. 并不会保存具体存的数据类型,我们需要告诉OpenGL内存的哪一部分对应着色器的哪一个uniform变量.

GLSL默认使用共享(shared)布局,由硬件或编译器定义偏移量,多个程序共享. shared布局虽然节省了空间,但需要查询每个Uniform变量的偏移量(使用glGetUniformIndices).

而使用std140的通用性更强,可以用glBufferSubData填充缓冲.

std140布局规则

std140为不同类型变量定义了固定的 内存占用 和 对齐方式,核心规则:

变量类型 占用内存(byte) 对齐要求
float 4 4 byte对齐
vec3 16 16 byte对齐
mat4 64(4x4x4) 16 byte对齐
uniform block - 16 byte对齐

shared布局和std140布局区别:

特性 shared(默认布局) std140(标准布局)
内存对齐规则 编译器自定义(为性能优化,如缓存对齐) OpenGL 强制规定(固定、跨平台)
跨平台 / 跨 GPU 兼容性 无(NVIDIA/AMD/Intel 布局可能不同) 完全兼容(所有 GPU 都遵循同一规则)
内存偏移可预测性 不可预测(需查询 GPU 的实际偏移) 完全可预测(CPU 可提前计算)
内存效率 更高 稍低(固定规则可能引入冗余填充)
数据传递方式 需先查询 GPU 的偏移,再传递数据 直接按规则计算偏移,传递数据

使用Uniform缓冲

步骤1:创建并初始化 UBO(CPU 端)

创建一个空的 Uniform 缓冲对象,并分配内存(Matrices块,2个mat4,合计128bytes)

// 1. 生成UBO对象ID
unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);

// 2. 绑定UBO到GL_UNIFORM_BUFFER目标
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);

// 3. 为UBO分配内存(2个mat4 = 2*16*4 = 128字节,初始数据为空)
glBufferData(GL_UNIFORM_BUFFER, 128, NULL, GL_DYNAMIC_DRAW); 
// GL_DYNAMIC_DRAW:数据会频繁更新(如摄像机View矩阵)

// 4. 解绑UBO(避免后续误操作)
glBindBuffer(GL_UNIFORM_BUFFER, 0);

步骤 2:为 UBO 分配绑定点(CPU 端)

给 UBO 指定一个数字编号(绑定点),比如选0(可自定义 0~MAX 之间的任意数,建议按模块分配):

// 将uboMatrices绑定到绑定点0
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboMatrices);
// 等价写法(更灵活,可绑定一段范围):
// glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 128);
  • glBindBufferBase:将整个UBO绑定到指定绑定点;
  • glBindBufferRange:将UBO的某一段内存绑定到绑定点(适合一个 UBO 存多个统一块)

OpenGL如何知道哪个UBO对应哪个Uniform块?

OpenGL定义了一些绑定点(Binding Point),我们可将一个Uniform缓冲链接到它. 创建Uniform缓冲后,就能将其绑定到一个绑定点上,并且将着色器中的Uniform块绑定到相同绑定点,从而实现链接.

3个关键元素的对应关系:

CPU端:Uniform缓冲对象(UBO) → 绑定点(数字编号,如0/1/2) ← GPU端:着色器中的统一块(如Matrices)

如下图所示:

img

步骤 3:将着色器的统一块关联到相同绑定点(CPU 端)

// 1. 获取着色器程序中Matrices统一块的索引
unsigned int shaderProgram = shader.ID; // 你的着色器程序ID
GLuint blockIndex = glGetUniformBlockIndex(shaderProgram, "Matrices");

// 2. 将该统一块关联到绑定点0
glUniformBlockBinding(shaderProgram, blockIndex, 0);
  • glUniformBlockBinding:将Uniform块绑定到特定绑定点;
  • glGetUniformBlockIndex:获取着色器中已定义Uniform块的位置索引.

注:

  1. 需对每个着色器重复这一步.
  2. 从OpenGL 4.2起,可添加布局标识符,显式将Uniform块绑定点存储在着色器中,就不再需要调用glGetUniformBlockIndexglUniformBlockBinding.

如下:

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

步骤 4:向 UBO 写入数据(CPU 端,运行时更新)

将projection和view矩阵写入 UBO(std140布局).

// 1. 绑定UBO(可选,也可用glBufferSubData直接指定)
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);

// 2. 写入projection矩阵(偏移0字节)
glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800/600.0f, 0.1f, 100.0f);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));

// 3. 写入view矩阵(偏移64字节)
glm::mat4 view = camera.GetViewMatrix();
glBufferSubData(GL_UNIFORM_BUFFER, 64, sizeof(glm::mat4), glm::value_ptr(view));

// 4. 解绑UBO
glBindBuffer(GL_UNIFORM_BUFFER, 0);

注:常运行在main循环中.

步骤 5:着色器中声明统一块(GPU 端)

确保着色器的Uniform Block用std140布局,避免内存偏移问题:

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

// 用std140布局,关联到绑定点0(CPU端已指定)
layout (std140) uniform Matrices
{
    mat4 projection; // 0~63字节
    mat4 view;       // 64~127字节
};

uniform mat4 model;

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

如此,着色器会自动从绑定点0读取UBO中的projection和view,和普通uniform变量用法一致.

参考

高级GLSL | LearnOpenGL

posted @ 2026-03-25 16:40  明明1109  阅读(4)  评论(0)    收藏  举报