文章目录
着色器版本规范
编译器按照声明的着色语言版本检查着色器语法,如果没有声明则默认使用1.0版本
#version 310 es
变量和变量类型
变量分类 | 类型 | 描述 |
---|---|---|
标量 | float, int, uint, bool | 用于浮点、整数、无符号整数和布尔值的基于标量的数据类型 |
浮点向量 | float, vec2, vec3, vec4 | 有1、2、3、4个分量的基于浮点的向量类型 |
整数向量 | int, ivec2, ivec3, ivec4 | 有1、2、3、4个分量的基于整数的向量类型 |
无符号整数向量 | uint, uvec2, uvec3, uvec4 | 有1、2、3、4个分量的基于无符号整数的向量类型 |
布尔向量 | bool, bvec2, bvec3, bvec4 | 有1、2、3、4个分量的基于布尔的向量类型 |
矩阵 | mat2(或mat2x2), mat2x3, mat2x4, mat3x2, mat3(或mat3x3), mat3x4, mat4x2, mat4x3, mat4(或mat4x4) | 2x2, 2x3, 2x4, 3x2, 3x3, 3x4, 4x2, 4x3或4x4的基于浮点的矩阵 |
变量构造器
- 不允许隐式类型转换
- 隐式截断:向量参数超出目标矩阵列大小时,多余分量被忽略
- 列优先存储:矩阵构造时参数按列填充,与内存布局一致。
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0} 标量值会填充向量的所有分量
vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5} 从左到右依次填充分量,支持标量和向量混合参数
vec3 temp = vec3(myVec3); // temp = myVec3 从左到右依次填充分量,支持标量和向量混合参数
vec2 myVec2 = vec2(myVec3); // myVec2 = {myVec3.x, myVec3.y} 从左到右依次填充分量,支持标量和向量混合参数
myVec4 = vec4(myVec2, temp); // myVec4 = {myVec2.x, myVec2.y, temp.x, temp.y} 参数总数需匹配目标向量分量数
mat4(1.0); // 创建4x4单位矩阵,对角线为1.0
mat2(vec2(1.0, 0.0), vec2(0.0, 1.0)); // 构造2x2矩阵
mat3 myMat3 = mat3(
1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 1.0, 1.0 // 第三列
);
向量和矩阵分量
向量分量访问
- 两种访问方式:
.
运算符:myVec3.xyz
- 数组下标:
myVec3[0]
- 命名约定:
- 坐标:
{x,y,z,w}
- 颜色:
{r,g,b,a}
- 纹理:
{s,t,p,q}
- 禁止混合使用(如
.xgr
非法)
- 坐标:
vec3 myVec3 = vec3(0.0, 1.0, 2.0);
vec3 temp;
temp = myVec3.xyz; // {0.0, 1.0, 2.0}
temp = myVec3.xxx; // {0.0, 0.0, 0.0}
temp = myVec3.zyx; // {2.0, 1.0, 0.0}
矩阵访问
- 矩阵视为向量集合(如
mat2
=2个vec2
) - 访问方式:
- 取列向量:
myMat4[0]
- 取元素:
myMat4[1][1]
或myMat4[2].z
- 取列向量:
mat4 myMat4 = mat4(1.0); // 单位矩阵
vec4 col0 = myMat4[0]; // 获取第0列
float m1_1 = myMat4[1][1]; // 获取(1,1)元素
float m2_2 = myMat4[2].z; // 获取(2,2)元素
常量
const float zero = 0.0;
const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);
- 适用于所有基本数据类型(float、vec、mat等)
- 行为与C/C++中的const一致
- 用于定义着色器中不变的值
结构
- 支持将相关变量聚合为逻辑单元
- 提高代码可读性和可维护性
- 可用于uniform、varying等所有变量类型
- 结构体名称作为新的用户定义类型
struct fogStruct {
vec4 color;
float start;
float end;
} fogVar;
fogVar = fogStruct(
vec4(0.0, 1.0, 0.0, 0.0), // color
0.5, // start
2.0 // end
);
vec4 color = fogVar.color; // 访问color成员
float start = fogVar.start; // 访问start成员
float end = fogVar.end; // 访问end成员
数组
float floatArray[4]; // 声明包含4个float的数组
vec4 vecArray[2]; // 声明包含2个vec4的数组
// 隐式大小声明
float a[4] = float[](1.0, 2.0, 3.0, 4.0);
// 显式大小声明
float b[4] = float[4](1.0, 2.0, 3.0, 4.0);
// 复杂类型初始化
vec2 c[2] = vec2[2](vec2(1.0), vec2(1.0));
函数
限定符 | 描述 |
---|---|
in | (默认限定符)参数按值传送,函数内部不能修改原变量 |
inout | 变量按照引用传入函数,如果该值被修改,它将在函数退出后变化 |
out | 该变量的值不被传入函数,但是在函数返回时将被修改 |
禁止递归:函数不能递归调用
实现原因:某些GPU通过内嵌函数代码实现函数调用,没有堆栈支持
设计考量:语言设计允许内嵌式实现,以兼容无堆栈的GPU架构
// 函数声明示例
vec4 myFunc(inout float myFloat, // inout参数
out vec4 myVec4, // out参数
mat4 myMat4); // in参数(默认)
// 函数定义示例:计算漫反射光
vec4 diffuse(vec3 normal, vec3 light, vec4 baseColor)
{
return baseColor * dot(normal, light);
}
控制流语句
并行执行特性:GPU以批次方式并行执行顶点/片段
分支执行影响:批次中所有顶点/片段通常需要执行所有分支路径
迭代扩散限制:应限制跨顶点/片段的流控或循环迭代扩散
架构差异性:不同GPU的批次大小不同,需要具体分析性能影响
// 条件语句示例
if(color.a < 0.25) {
color *= color.a;
} else {
color = vec4(0.0);
}
// 循环结构(ES 3.0+完全支持)
for(int i = 0; i < 10; i++) {
// 循环体
}
统一变量
基本使用
- 定义方法
uniform mat4 modelViewMatrix;
uniform vec3 lightPosition;
uniform float specularIntensity;
- 赋值与传参
- cpu端
GLint loc = glGetUniformLocation(program, "variableName");
glUniform1i(loc, value); // 整型
glUniform1f(loc, value); // 浮点型
glUniform3f(loc, x, y, z); // 三维向量
glUniformMatrix4fv(loc, 1, GL_FALSE, &matrix[0][0]); // 4x4矩阵
- 自定义location
- 可以在着色器中直接为uniform指定location,避免运行时查询:
layout(location = 2) uniform mat4 worldMat;
- 然后在C++中直接使用该location赋值:
glUniformMatrix4fv(2, 1, GL_FALSE, &worldMat[0][0]);
统一变量块
统一变量块是一组统一变量的集合,有诸多优势:
- 只需设置一次,就可以在多个程序中共享
- 可以存储更大量的统一变量数据
- 缓冲区对象之间切换比单独加载uniform更高效
layout(std140, binding = 0) uniform MATS {
mat4 mvMat;
vec3 aPos;
};
cpu端
struct ABlock {
glm::mat4 aMatrix;
glm::vec3 pos;
};
glGenBuffers(1, &aMapBuffer);
glBindBuffer(GL_UNIFORM_BUFFER, aMapBuffer);
// 为绑定的缓冲分配内存空间。
glBufferData(GL_UNIFORM_BUFFER, sizeof(ABlock), NULL, GL_DYNAMIC_DRAW);
// 将缓冲绑定到绑定点索引0,使着色器可以通过此索引访问UBO数据。
glBindBufferBase(GL_UNIFORM_BUFFER, 0, aMapBuffer);
// 映射缓冲对象的内存到客户端地址空间,返回指向内存的指针mapBlock,允许直接修改数据。
ABlock* mapBlock = (ABlock*)glMapBufferRange(...);
// 解除映射,确保修改后的数据同步到GPU端
glUnmapBuffer(GL_UNIFORM_BUFFER);
uniform block的布局方式:
1. 共享布局shared
- OpenGL编译器决定每个成员在内存中的确切位置
- 允许不同着色器程序共享相同的Uniform Block定义
- 需要查询每个成员的具体偏移量才能正确填充数据
- 优点:内存使用效率高,适合硬件实现
- 缺点:需要额外的API调用来查询布局信息
- 适用场景:需要在多个着色器程序间共享Uniform Block数据的情况
uniform TransformBlock {
float scale;
vec3 translation;
float rotation[3];
mat4 projection_matrix;
} transform;
CPU端的使用方法
// 获取Uniform Block索引
GLuint blockIndex = glGetUniformBlockIndex(program, "BlockName");
// 获取成员索引和偏移量
const GLchar* names[] = {"scale", "translation", "rotation"};
GLuint indices[3];
glGetUniformIndices(program, 3, names, indices);
GLint offsets[3];
glGetActiveUniformsiv(program, 3, indices, GL_UNIFORM_OFFSET, offsets);
// 获取Uniform Block大小
GLint blockSize;
glGetActiveUniformBlockiv(shaderProgram, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);
// 创建UBO
unsigned int uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, blockSize, NULL, GL_DYNAMIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// 将UBO绑定到绑定点0
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboMatrices);
// 将着色器的Uniform Block绑定到相同的绑定点
glUniformBlockBinding(shaderProgram, blockIndex, 0);
// 将矩阵数据填充到UBO中
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
memcpy(blockBuffer + offset[0], glm::value_ptr(model), sizeof(glm::mat4));
memcpy(blockBuffer + offset[1], glm::value_ptr(view), sizeof(glm::mat4));
memcpy(blockBuffer + offset[2], glm::value_ptr(projection), sizeof(glm::mat4));
glBufferSubData(GL_UNIFORM_BUFFER, 0, blockSize, blockBuffer);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
这个流程的基本思路是:首先创建一个blockSize尺寸的UBO和cpu上的buffer。接着根据获取到的偏置量将结构体中的数据复制到cpu的buffer中准备好。最后使用glBufferSubData将cpu数据上传到gpu中
2. 打包布局packed
优点:内存使用效率最高
缺点:可移植性差,不同硬件可能有不同布局
适用场景:内存受限的应用程序
3. 标准布局std140
std140是一种标准化的显式布局方式,它规定了严格的内存对齐规则:
- 标量类型(如int、float、bool):按4字节对齐
- 向量类型:vec2:8字节对齐
- vec3和vec4:16字节对齐
- 数组:每个元素按16字节对齐
- 矩阵:按列存储,每列视为一个vec4(16字节对齐)
优点:布局规则明确,无需查询偏移量
缺点:内存利用率较低(有填充字节)
适用场景:需要简单明确的内存布局,不频繁更新的Uniform Block
layout(std140) uniform ExampleBlock {
float scale; // 偏移0,占用4字节
vec3 translation; // 偏移16(跳过12字节),占用12字节
float rotation[3]; // 偏移32(跳过4字节),每个元素16字节,共48字节
mat4 projection; // 偏移80(跳过0字节),每列16字节,共64字节
};
在std140布局中,可以使用offset限定符自定义成员偏移量, 自定义偏移量仍需遵守基本对齐规则
layout(std140) uniform ManuallyLaidOutBlock {
layout(offset = 32) vec4 foo; // 偏移32字节
layout(offset = 8) vec2 bar; // 偏移8字节
layout(offset = 48) vec3 baz; // 偏移48字节
};
CPU端使用方法:类似于shared布局方式。不同之处在于由于std140布局方式不需要获取offset,因此可以直接把cpu上的数据使用 glBufferSubData进行上传,而无需手动获取偏置并重新申请一块buffer用于排列内存。
#include <glm/glm.hpp>
#include <glm/gtc/type_ptr.hpp>
// 使用GLM的std140布局适配器
namespace glm {
struct ExampleBlock {
mat4 projection;
mat4 view;
vec3 lightPos;
float lightIntensity;
vec3 lightColor;
bool isEnabled;
};
}
// 使用GLM的std140内存布局
using ExampleBlock_Std140 = glm::detail::storage<glm::ExampleBlock, glm::precision::highp, glm::detail::is_aligned<true>::value>;
// 创建UBO
GLuint ubo;
glGenBuffers(1, &ubo);
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferData(GL_UNIFORM_BUFFER, sizeof(ExampleBlock_Std140), NULL, GL_DYNAMIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// 绑定到绑定点0
glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo);
// 填充数据
ExampleBlock_Std140 blockData;
blockData.projection = glm::perspective(...);
blockData.view = glm::lookAt(...);
blockData.lightPos = glm::vec3(1.0f, 1.0f, 1.0f);
blockData.lightIntensity = 1.5f;
blockData.lightColor = glm::vec3(1.0f, 1.0f, 1.0f);
blockData.isEnabled = true;
// 更新UBO
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(ExampleBlock_Std140), glm::value_ptr(blockData));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// 在着色器程序中绑定Uniform Block到相同绑定点
GLuint blockIndex = glGetUniformBlockIndex(shaderProgram, "ExampleBlock");
glUniformBlockBinding(shaderProgram, blockIndex, 0);
注意事项
- 顶点着色器和片段着色器共享同一组统一变量
- 可以为统一变量设置默认值,当C/C++客户端没有赋值时使用。
- 程序中可以使用的统一变量的数量受限,可通过gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors查询
顶点和片段着色器输入/输出
- 输入输出使用in/out关键字指定。
- 顶点着色器输入变量使用layout限定符,用于指定顶点属性的索引
- 顶点着色器输出和片段着色器输入变量没有限定符
- 片段着色器的输出可以使用layout限定符,用于多目标渲染任务中,将不同的输出给到不同的渲染目标。
- 输入输出变量的数量受限,可以查询
插值限定符
- 平滑插值:默认情况下顶点着色器输出和片段着色器输入执行平滑插值。也就是说来自顶点着色器的输出变量在图元中线性插值。
- 平面插值:平面着色以图元的某个顶点颜色(称为"引发顶点")作为整个图元的统一颜色,不进行颜色插值,因此渲染结果呈现均匀的色块效果。
flat out vec3 flatColor; // 声明平面着色输出
flat in vec3 flatColor; // 接收平面着色输入
- 质心采样:质心采样(Centroid Sampling)是一种用于优化光栅化过程中插值计算的抗锯齿技术,主要解决片元(Fragment)覆盖边界时的插值异常问题。这里不做详细介绍
预处理器和指令
- 条件编译指令:如#if、#ifdef、#else、#endif,用于根据宏定义或条件判断是否包含代码块。例如
GL_ES
precision mediump float; // 仅在OpenGL ES环境中生效
#endif
- #define和#undef用于定义或取消宏,支持常量或简单逻辑
- #error用于强制终止编译并输出错误信息,常用于调试
- #version:必须位于着色器顶部(仅注释可前置),声明GLSL ES版本。
- #extension:管理硬件扩展的启用或禁用,语法为:
#extension extension_name : behavior
behavior可选值:
- require:必须支持,否则报错。
- enable:启用扩展,不支持则警告。
- warn:使用扩展时警告。
- disable:禁用扩展
- GL_ES:在OpenGL ES环境中自动定义为1,用于区分桌面版GLSL
- __VERSION__:返回当前GLSL ES版本号(如100或300)
- __LINE__和__FILE__:提供当前行号和文件名(ES 2.0中__FILE__固定为0)
- #pragma:控制编译器行为
#pragma optimize(on) // 开启优化(默认)
#pragma debug(off) // 关闭调试信息(默认关闭)
精度限定符
- 在顶点着色器中,int和float默认精度都是highp
- 在片段着色器中,浮点值没有默认的精度值。
- 指定默认精度
precision highp float;
precision mediump int;
不变性
假设有两个独立的顶点着色器(用于多通道渲染),均计算相同顶点位置:
// 顶点着色器A
uniform mat4 u_MVP;
attribute vec3 a_Pos;
void main() {
gl_Position = u_MVP * vec4(a_Pos, 1.0); // 表达式相同
}
// 顶点着色器B(逻辑与A完全相同)
uniform mat4 u_MVP;
attribute vec3 a_Pos;
void main() {
gl_Position = u_MVP * vec4(a_Pos, 1.0); // 相同表达式
}
尽管代码逻辑一致,但实际运行时可能出现:
- 指令重排序:着色器A可能优化为(u_MVP[0] * a_Pos.x) + (u_MVP[1] * a_Pos.y) + …,而着色器B保持原乘法顺序。
- 寄存器分配差异:中间结果存储的寄存器位宽不同(如A用fp32,B用fp16),导致舍入误差
不一致的后果
当这两个着色器分别用于阴影生成和主渲染通道时:顶点在阴影通道的gl_Position可能为(0.5001, 0.4999, 0.0, 同一顶点在主通道的gl_Position可能为(0.4999, 0.5001, 0.0, 1.0)。结果:深度测试时本应重合的像素出现Z-fighting(闪烁的黑色斑点)
解决方案:invariant限定符
// 修改后的顶点着色器A和B
invariant gl_Position; // 声明为不可变
uniform mat4 u_MVP;
attribute vec3 a_Pos;
void main() {
gl_Position = u_MVP * vec4(a_Pos, 1.0);
}