计算机图形:高级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 当前片段是反面
简化版管线:
- 顶点着色器 → 顶点变换
- 图元组装(Primitive Assembly)
- 面剔除(Face Culling)
- 裁剪(Clipping)
- 透视除法 → NDC
- 视口变换
- 光栅化(Rasterization)
- 片段着色 → 深度 / 模板 / 混合 → 帧缓冲
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)
如下图所示:

步骤 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块的位置索引.
注:
- 需对每个着色器重复这一步.
- 从OpenGL 4.2起,可添加布局标识符,显式将Uniform块绑定点存储在着色器中,就不再需要调用
glGetUniformBlockIndex,glUniformBlockBinding.
如下:
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变量用法一致.

浙公网安备 33010602011771号