【openGLES】着色器语言(GLSL)

着色器版本规范

编译器按照声明的着色语言版本检查着色器语法,如果没有声明则默认使用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++) {
    // 循环体
}

统一变量

基本使用

  1. 定义方法
uniform mat4 modelViewMatrix;
uniform vec3 lightPosition;
uniform float specularIntensity;
  1. 赋值与传参
  • 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矩阵
  1. 自定义location
  • 可以在着色器中直接为uniform指定location,避免运行时查询:
layout(location = 2) uniform mat4 worldMat;
  • 然后在C++中直接使用该location赋值:
glUniformMatrix4fv(2, 1, GL_FALSE, &worldMat[0][0]);

统一变量块

统一变量块是一组统一变量的集合,有诸多优势:

  1. 只需设置一次,就可以在多个程序中共享
  2. 可以存储更大量的统一变量数据
  3. 缓冲区对象之间切换比单独加载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);

注意事项

  1. 顶点着色器和片段着色器共享同一组统一变量
  2. 可以为统一变量设置默认值,当C/C++客户端没有赋值时使用。
  3. 程序中可以使用的统一变量的数量受限,可通过gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors查询

顶点和片段着色器输入/输出

  • 输入输出使用in/out关键字指定。
  • 顶点着色器输入变量使用layout限定符,用于指定顶点属性的索引
  • 顶点着色器输出和片段着色器输入变量没有限定符
  • 片段着色器的输出可以使用layout限定符,用于多目标渲染任务中,将不同的输出给到不同的渲染目标。
  • 输入输出变量的数量受限,可以查询

插值限定符

  • 平滑插值:默认情况下顶点着色器输出和片段着色器输入执行平滑插值。也就是说来自顶点着色器的输出变量在图元中线性插值。
  • 平面插值:平面着色以图元的某个顶点颜色​(称为"引发顶点")作为整个图元的统一颜色,不进行颜色插值,因此渲染结果呈现均匀的色块效果。
flat out vec3 flatColor;  // 声明平面着色输出
flat in vec3 flatColor;   // 接收平面着色输入
  • 质心采样:​质心采样(Centroid Sampling)​是一种用于优化光栅化过程中插值计算的抗锯齿技术,主要解决片元(Fragment)覆盖边界时的插值异常问题。这里不做详细介绍

预处理器和指令

  1. ​条件编译指令​:如#if、#ifdef、#else、#endif,用于根据宏定义或条件判断是否包含代码块。例如
GL_ES
precision mediump float;  // 仅在OpenGL ES环境中生效
#endif
  1. #define和#undef用于定义或取消宏,支持常量或简单逻辑
  2. #error用于强制终止编译并输出错误信息,常用于调试
  3. ​#version​:必须位于着色器顶部(仅注释可前置),声明GLSL ES版本。
  4. ​#extension​:管理硬件扩展的启用或禁用,语法为:
#extension extension_name : behavior

behavior可选值:

  • require:必须支持,否则报错。
  • enable:启用扩展,不支持则警告。
  • warn:使用扩展时警告。
  • disable:禁用扩展
  1. ​GL_ES​:在OpenGL ES环境中自动定义为1,用于区分桌面版GLSL
  2. __VERSION__​:返回当前GLSL ES版本号(如100或300)
  3. ​__LINE__和__FILE__​:提供当前行号和文件名(ES 2.0中__FILE__固定为0)
  4. ​#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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值