☰
GLSL变量与类型全解析:从向量矩阵到精度与std140对齐
2026/10/20 6:37:24 网站建设 项目流程

GLSL的规范文档里,《Variables and Types》这章读起来很枯燥——没有纹理采样那种画面感,也没有光照模型那样能让脑子里立刻浮现出漂亮的渲染结果。但凡是真正在GPU上写过几个shader、调过几个深夜bug的人,最后都会回到这一章里找答案。比如切线空间算错,排查半天发现是mat3的构造参数顺序记反了;比如移动端画面远处出现色阶断层,查了一圈发现是法线变量被精度限定符偷偷降成了半精度;再比如uniform buffer里的数据对不上,仔细一对才发现std140的对齐规则和CPU端的结构体完全不是一回事。诸如此类,根子都出在变量声明和类型规则上。

这篇就从GLSL的变量与类型系统入手,把规范里那套规则掰开揉碎讲一遍。重点不光是“有什么类型”,更会讲“为什么是这个规则”“实际项目中踩过哪些坑”,以及哪些细节是规范里写了但日常文档很少强调的。适合刚接触GLSL的图形学初学者,也适合写了好几年shader但没系统读过规范的人用来查漏补缺。

1. 为什么说变量和类型是shader与GPU之间的契约

1.1 GLSL是严格强类型语言,这点和C/C++差别很大

GLSL在设计上继承了C语言的语法骨架,但类型系统远比C严格。C语言里int和float互相转换非常随意,写错类型通常只是警告,甚至编译器会默默帮你做隐式转换。GLSL不同,它在类型检查上几乎是“零容忍”的:类型不匹配就是编译错误,没有讨论余地。

为什么这么设计?因为shader是跑在GPU上的,GPU的每个执行单元都对应特定的数据通路。顶点属性进来是vec4,纹理坐标是vec2,变换矩阵是mat4——这些类型直接决定了硬件要准备多少个寄存器、怎么解析二进制流、怎么组织并行运算。如果类型可以随随便便转换,硬件层的资源调度就没法做了。所以GLSL用强类型来保证每一个变量在编译期就有明确的大小和语义,编译器才能把它映射到对应的物理资源上。

比如一个最简单的顶点着色器:

#version 330 core in vec3 position; in vec3 normal; uniform mat4 modelViewProjection; out vec3 vNormal; void main() { vNormal = normal; gl_Position = modelViewProjection * vec4(position, 1.0); }

这里in vec3 position声明了顶点属性是一个三通道向量,GPU就知道要从顶点缓冲里按3个float为一组去读数据;uniform mat4 modelViewProjection声明了一个4x4矩阵,驱动层就知道要用glUniformMatrix4fv上传16个float。整个渲染管线的数据流,就是靠这些变量声明串起来的。

1.2 类型系统决定了shader能做什么、不能做什么

GLSL没有指针,没有动态内存分配,没有字符串,没有类。这些都被刻意砍掉了,因为GPU的编程模型就是数据并行:成千上万个线程同时运行同一段代码,处理不同的数据。指针和动态分配在这种模型下是灾难——每个线程各自管理内存,分布式内存分配的开销无法承受。

所以GLSL的类型列表非常精简,基本围绕“数值计算”这个核心场景展开:

  • 标量:bool、int、uint、float、double
  • 向量:由标量组成的2/3/4维向量
  • 矩阵:2x2到4x4的方阵和非方阵
  • 数组:固定大小的同类元素集合
  • 结构体:把不同类型打包成一个自定义类型
  • 不透明类型:采样器sampler*、图像image*、原子计数器atomic_uint

这套类型系统没有“字符串”“链表”这类高级抽象,但足以覆盖实时渲染里绝大多数的数学运算。理解了这个前提,再看规范里的每一项规则,逻辑就顺了。

2. 标量、向量与矩阵:从维度数学到硬件寄存器

2.1 标量类型各自的定位

五个标量类型里,最常用的是float,GPU的ALU天生就是为浮点运算设计的。int和uint主要在索引、循环控制、位运算、整数纹理采样这些场景使用。bool用于分支判断,注意在大多数GPU上bool实际占用一个32位寄存器,不像CPU那样压缩到1位。double在桌面端GL 4.0以后才引入,移动端几乎不可用,而且双精度运算在GPU上非常慢(消费级显卡的双精度单元数量远少于单精度),除非做科学计算,否则实时渲染项目里基本不用。

用一个表格来对比会更直观:

类型大小典型用途注意事项
bool实现相关,通常32位分支条件、逻辑标志不要在GPU上依赖位宽
int32位有符号循环、索引、整数运算与uint之间无隐式转换
uint32位无符号位操作、原子计数器、索引负数赋值会回卷
float32位IEEE754几乎所有渲染计算精度和范围要看具体GPU
double64位IEEE754高精度计算桌面高端卡才靠谱,速度慢

2.2 向量类型:分量访问与swizzle操作

向量是GLSL里使用频率最高的类型,vec2、vec3、vec4分别对应二维、三维、齐次坐标。除了vec,同一套维度还配套了布尔向量bvec*、整数向量ivec*、无符号整数向量uvec*、双精度向量dvec*。这套“同构家族”设计的意义在于:数学向量运算是冗余的,只要给一个浮点向量实现了加法,其他类型的向量也能复用同一套语法。

向量的分量有三种命名空间:

  • 位置坐标:x、y、z、w
  • 颜色分量:r、g、b、a
  • 纹理坐标:s、t、p、q

三套名字可以互相访问同一个分量,v.r和v.x是同一个东西。但规则有一条硬性限制:一个swizzle表达式里不能混用不同命名空间的字母,比如v.xy合法,v.xr就是编译错误。

swizzle操作最大的价值是可以一次性重排分量,这在处理法线、切线、UV坐标时非常实用:

vec2 uv01 = texCoord.xy; vec3 normalFromTangent = vec3(bitangent * tangent.xyw); // 重排并升维 vec4 packedColor = albedo.rgba;

有一点必须强调:当swizzle出现在赋值号左边时,不能有重复分量,v.xx = vec2(1.0)是非法的;但作为右值,重复分量完全没问题,vec2 duplicated = v.xx可以正常编译。这是因为GPU并行写同一个寄存器会出现竞争,规范从设计层面禁止了这种不确定性。

2.3 矩阵类型:构造方式与列主序存储

矩阵类型包括mat2、mat3、mat4,以及非方阵mat2x3、mat3x2、mat2x4等。第一个数字是列数,第二个是行数,例如mat3x2表示3列2行。

GLSL的矩阵存储是列主序的,这个细节影响深远。构造矩阵时,参数是按“列”来填充的:

// 三列分别对应: // col0 = (1.0, 0.0, 0.0) // col1 = (0.0, 1.0, 0.0) // col2 = (0.0, 0.0, 1.0) mat3 m = mat3(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);

用单个标量构造:mat3 m = mat3(1.0);会生成对角矩阵,对角线上的分量都是1.0,其他位置都是0.0。这是最常用的初始化方式,相当于创建单位矩阵。

从低维矩阵升维,比如mat3 m = mat3(mat2);会保留左上角的2x2部分,其余位置按单位矩阵补全。从高维矩阵降维,mat2 m = mat2(mat4);则取左上角2x2。这套构造规则在处理法线矩阵、切空间旋转时经常用到,写的时候要心里有数。

矩阵乘法和向量的顺序不能记错。mat4 * vec4把矩阵作用到向量上,是常规的变换操作;vec4 * mat4相当于对矩阵做转置后再作用,结果完全不同。实际项目里,模型变换矩阵、视图矩阵、透视矩阵的链式乘法,M * V * P这种顺序要始终一致,否则会出现模型扭曲、方向翻转这类很难定位的问题。

3. 复合类型:结构体、数组与接口块

3.1 结构体:把零散的变量打包成有意义的数据单元

结构体在GLSL里的作用和C语言类似,把相关的变量聚合成一个逻辑单元:

struct Light { vec3 direction; vec3 color; float intensity; }; uniform Light u_light; void main() { vec3 diffuse = u_light.color * u_light.intensity * max(dot(normal, -u_light.direction), 0.0); }

结构体可以嵌套,成员类型可以是标量、向量、矩阵、数组,甚至另一个结构体。两个结构体变量可以直接赋值,前提是类型完全一致;结构体之间支持==和!=比较,会逐一比较所有成员,但前提是成员类型也要完全相同。

需要注意,不透明类型(采样器、图像、原子计数器)不能作为结构体成员,也不能作为数组元素。这点和很多图形API的处理方式不同,本质原因是这些类型无法被拷贝,而结构体和数组的赋值语义要求成员可以被整体拷贝。

3.2 数组:大小、索引与length()

数组的声明有两种等价写法:

float arr[5]; float[5] arr2; // 4.0以后支持的类型名在前写法,主要用于函数返回值和参数

数组大小必须是编译期常量,或者通过初始化列表推断:

float values[] = { 1.0, 2.0, 3.0 }; // 大小推断为3

数组访问用[]运算符,越界访问在GLSL里是未定义行为——有的驱动会报错,有的驱动会静默返回垃圾值,这是极其隐蔽的bug来源。数组的成员数量可以通过.length()获取,距离值类型是int,在循环里做边界判断非常方便:

for (int i = 0; i < values.length(); ++i) { total += values[i]; }

数组之间不允许整体赋值、比较、拼接。数组作为函数参数时按引用传递(省去拷贝开销),但形参必须声明数组类型,可以用空大小float[]表示“任意长度的数组”,也可以指定固定大小。

我见过不少人在for循环里用float i做索引,然后总是不功——因为GLSL里数组索引必须用整数类型,浮点索引直接编译报错。这个问题在移动端低精度优化时特别容易犯,后面精度部分会再展开。

3.3 接口块:跨阶段数据传递的正规渠道

接口块(interface block)是GLSL中非常重要但容易被初学者忽略的复合类型。它本质上是一个带存储限定符的结构体,用uniform或buffer限定,用于批量管理uniform数据和着色器存储缓冲区(SSBO)。

// uniform块声明在shader里 layout(std140) uniform TransformBlock { mat4 viewProjection; mat4 model; vec3 cameraPosition; float ambientFactor; };

接口块和普通结构体最大的区别在于:块内的成员实际存储在GPU缓冲区中,CPU端通过glUniformBlockBinding关联一个缓冲区对象,然后往里填充数据。这就牵出著名的std140布局规则,它规定了GPU缓冲区里每个成员的字节偏移,CPU端结构体必须按同样规则排列才能对上号。

std140的规则归纳起来大致是:

类型对齐规则典型偏移
float/int/bool4字节从4字节边界开始
vec2/ivec28字节从8字节边界开始
vec3/vec4及更大16字节从16字节边界开始
数组元素按自身对齐要求排列,整体对齐取其最大成员对齐的倍数vec3 arr[2]每元素占16字节
嵌套结构体按内部最大成员对齐,且整体对齐到其最大成员对齐的倍数内部成员各自对齐后再对外对齐

这里最反直觉的一点:vec3变量。表面上是12字节,但std140下它的对齐是16字节,也就是说三个vec3成员连续声明,内存里会是48字节而不是36字节。如果CPU端的C++结构体直接用float pos[3]、float normal[3]这样子排,缓冲区就会错位,渲染出来的模型会奇形怪状,或者干脆什么都不显示。

排查这类问题有个好方法:用glGetActiveUniformsiv查询每个uniform的偏移量,和CPU端结构体做对比。偏移量对不上,十有八九是std140对齐没处理好。

4. 存储限定符:数据从哪来、到哪去

4.1 in/out/const/inout:函数参数与阶段间通信

GLSL的存储限定符定义了变量的生命周期和可见范围。先看最基础的几类:

  • const:编译期常量,值不可修改,shader内联优化的重要依据。
  • in:函数参数时表示输入参数;在着色器入口(main之外)用于声明从上一个阶段接收的数据,比如顶点着色器里的顶点属性。
  • out:函数参数时表示输出参数,实际上是通过指针/引用语义写回;在着色器入口用于声明传递给下一个阶段的数据。
  • inout:函数参数里既读又写,效果相当于引用传递。

函数参数默认是in,可以省略不写。inout在GLSL里不像C++有引用语法那么直观,但它就是“修改实参本身”的意思:

void applyTransform(inout vec3 pos, mat4 m) { pos = (m * vec4(pos, 1.0)).xyz; }

调用后,外面的pos变量会被更新。这在封装光照、雾效、坐标变换这类逻辑时能少写很多返回值拼接。

在旧版GLSL(1.20之前)里,跨阶段变量用attribute和varying这两个关键字,attribute在顶点着色器里声明顶点属性,varying在顶点和片段着色器之间传数据。从GLSL 1.30开始统一替换为in/out。现在写新代码一律用in/out,但看老项目代码时遇到attribute可别懵。

4.2 uniform、buffer与shared:三种不同粒度的共享数据

  • uniform:着色器全局只读数据,在一次绘制调用中不变。所有可编程阶段都能访问。它适合放模型矩阵、材质参数、光照参数这些高频变更但每个绘制批次只设置一次的数据。现代GL更推荐把多个uniform打包进uniform块,减少状态切换的开销。
  • buffer:声明着色器存储缓冲区(SSBO),支持读写。计算着色器里做通用计算、粒子系统更新、可见性查询都会用。buffer块可以用std430布局,比std140更紧凑,没有vec3那种16字节对齐的额外膨胀。
  • shared:只在计算着色器里使用,用于同一个工作组内线程之间的数据共享。共享内存的大小有硬件上限(通常几十KB),访问要比全局内快很多,适合做分块归约、局部缓存这类优化。

4.3 插值限定符:flat、smooth与noperspective

当变量从顶点着色器输出到片段着色器时,GPU会在片元之间做插值。默认是smooth,就是透视正确插值;flat则不做插值,所有片元都取触发这个图元的顶点值;noperspective表示线性插值,不用透视校正。

这三个限定符里,flat的实用价值最大。典型场景是顶点色ID、图元索引这类数据——如果这些值被插值,边缘会糊成一片,像素级拾取就会出错,flat可以完美避开:

flat out int primitiveID;

插值限定符错误导致的问题很典型:模型表面出现奇怪的颜色渐变,或者边缘出现细线。有人会把问题归到光栅化设置上,其实查一下out变量的插值限定符,很快就能定位。

5. 精度限定符:移动端性能的隐形开关

5.1 highp、mediump与lowp到底改了什么

GLSL的精度限定符是一个容易被桌面开发者忽视、但移动端必须重视的特性。三种精度:

精度浮点范围(典型)典型尾数位适用场景
highp32位IEEE75423位位置坐标、矩阵、深度
mediump范围约±6550410位颜色、UV、法线方向
lowp范围约±26位环境光系数、高光强度

注意上面给的是相对常见的实现,实际精度由GPU决定,着色器里声明的精度只是“最低要求”。桌面GPU通常把所有精度都实现为32位浮点,所以桌面环境下mediump和highp性能可能没有差别;移动端就不同了——很多移动GPU把mediump实现为半精度浮点(fp16),运算更快、带宽更低,但范围和精度都有限。

highp、mediump、lowp可以修饰float和int,不能修饰bool和矩阵(矩阵的精度由其元素类型决定)。用法有两种:

// 变量级别 mediump vec2 texCoord; // 全局默认精度声明 precision mediump float; precision highp int;

5.2 默认精度:片段着色器里的float没有默认值

规范对默认精度做了很严格的规定:

  • 顶点着色器中,float和int默认是highp,所以不写精度也能正常工作。
  • 片段着色器中,int默认是highp,但float没有默认精度——如果不显式声明,会直接编译报错,除非所有浮点变量都显式写了精度。

所以几乎每个片段着色器文件顶部都会有一行:

precision highp float;

这句已经成为移动端GLSL的“惯例咒语”了,但很多人不知道它真正解决的是“没有默认精度”这个硬性规则。

5.3 移动端实战:mediump优化要谨慎,踩过的坑很真实

我自己印象最深的一个教训,是帮人排查一个移动端项目。现象是法线贴图在角色近景时边缘出现明显色带,远看以为是贴图压缩问题。后来把片元着色器里的法线变量从mediump vec3改成highp vec3,画面立刻正常。原因很简单:法线贴图采样之后要跟TBN矩阵做乘法,矩阵可能是highp的,两边精度不一致时中间结果被截断到半精度,误差被放大成可见的色阶。

另一个容易翻车的点:在循环里用mediump int做索引。mediump int在部分移动GPU上只有16位,值超过32767就会溢出,循环会变成死循环或者提前退出。这类问题很难肉眼发现,定位时可以把循环变量单独提到highp int验证。

所以我的建议很直接:移动端做精度优化时,优先把UV、颜色、光照系数这类“数值范围小、对误差不敏感”的量降成mediump;凡是要做矩阵乘法、要参与深度计算、要做循环索引的变量,一律保持highp。性能收益虽然存在,但为了一点性能换来画面撕裂和数值错乱的调试成本,性价比太低了。

5.4 invariant与precise:解决同一个表达式在不同shader里计算结果不一致的问题

这部分在《Variables and Types》里不算显眼,但遇到特定问题时极其关键。invariant保证同一个表达式在不同着色器里的计算结果严格一致——GPU编译器可以重新关联浮点表达式(类似CPU的-ffast-math),导致两个shader里写法完全相同的表达式生成了不同的指令顺序,最终数值有微小差异。

典型的场景是深度预渲染:主渲染pass用一套texture矩阵算深度,后处理pass里再用同一套矩阵算深度做测试,两边结果差了一点点,就会出现像素抖动。解决办法是在深度输出变量前加invariant:

invariant out vec3 vDepthPosition;

precise是更新的限定符,要求某个变量的计算不能做会改变其值的优化,但允许具体的指令顺序有差异。它和invariant的区别在于:precise影响的是该变量相关的运算,invariant保证的是同一个表达式在不同编译单元里的位精确一致性。平时用不上,但如果做大型渲染管线且涉及到多pass复用相同数值,最好记住有这两个保命关键字。

6. 类型转换与构造:规则严谨,处处是坑

6.1 隐式转换:只做安全的加宽

GLSL并非完全禁止隐式类型转换,但规则非常保守。规范允许的隐式转换大致是“从窄到宽”的方向:

  • int可以隐式转换为float和double
  • uint可以隐式转换为float和double
  • float可以隐式转换为double

注意有两类经常被误以为可以但实际不行的:

  • int和uint之间没有隐式转换。写uint a = intValue;如果intValue是int类型,编译直接报错。
  • bool不参与任何数值隐式转换。不能把int当bool用,也不能把bool当float用。

隐式转换会发生在表达式内部,比如int + float会自动把int转成float再运算,结果是float。矩阵和向量的混合运算也有类似的隐式升级规则,但维度不匹配依然直接报错。

一个很容易让人困惑的实际例子:uint参与运算时,如果混入int,在某些编译器上会报错,在另一些编译器上会得到更宽的转换。最稳妥的做法是显式构造,不依赖编译器的“仁慈”。

6.2 构造函数与显式转换:一切都可以手动做

GLSL的构造函数同时承担了类型转换的职责,任何标量、向量、矩阵都可以通过显式的构造函数互相转换:

float f = float(intVal); int i = int(floatVal); // 截断,不是四舍五入 uint u = uint(intVal); // 负数会按模运算回卷 vec3 v = vec3(1.0, 2.0, 3.0); ivec3 iv = ivec3(v); // 每个分量独立截断 vec4 v4 = vec4(v3, 1.0); // 追加一个分量 vec3 v3 = vec3(v4); // 丢弃多余分量

浮点转整数是截断行为,和C语言一致,不是四舍五入,也不会报溢出错误。对负数来说,转成uint会按2^32取模回卷:

uint u = uint(-1); // 结果是4294967295

6.3 运算符重载与矩阵乘法陷阱

GLSL为向量和矩阵重载了几乎所有的算术运算符。向量之间按分量逐一运算;向量和标量混合运算,会把标量广播到每个分量。这些都是很方便的语法糖,但有两个陷阱常踩。

第一个是标量乘矩阵和矩阵乘向量时,维数对齐问题。vec3 * mat3和mat3 * vec3都允许,但前者相当于对矩阵做转置后再作用。

第二个是运算结果类型随操作数升级。int * vec3会先把int隐式转成float再乘,结果是vec3;但uint * vec3呢?uint转float也是允许的,所以也能编译。这种“能编译”不代表“语义正确”,尤其在做精确整数计算时,转成浮点再回来会丢精度。需要固定整数语义时,务必把两个操作数都声明成同样的整数类型。

6.4 数组和结构体不能隐式转换

类型转换规则对复合类型不适用。两个结构体即使成员类型完全一样,只要声明类型不同(哪怕仅仅是名字不同?在GLSL里结构体相同定义就是相同类型?实际上结构体类型是按成员类型和顺序区分的,相同定义的结构体是同一个类型。),规范不允许结构体之间隐式转换,也不允许数组和相同元素类型的数组相互转换。所有复合类型的赋值必须是严格类型匹配的。写代码时如果感觉“这个vec3数组和那个vec3数组好像可以互相赋一下”——别想,编译不过。

7. 实操排查:常见编译错误与运行期异常速查

7.1 编译错误汇总表

错误信息关键词原因处理办法
cannot convert from 'float' to 'vec3'类型不匹配的赋值或函数实参用构造函数显式转换,检查维度
no matching function for call to 'vec4(float, int)'构造函数的实参类型不匹配把int实参显式转为float
implicit conversion from 'int' to 'uint' is not allowed有符号整型与无符号整型混用显式做uint()或int()转换
swizzle ... can't ...swizzle表达式混用命名空间或左值重复分量统一用xyzw或rgba,左值不重复
array index must be an integer用浮点或非整数类型做数组索引索引变量改用int或uint
'precision' must be ...片段着色器的float没有默认精度文件顶部加precision highp float;
undeclared identifier变量名拼写或作用域问题,或者#version不匹配导致关键字不存在检查关键字与GLSL版本是否匹配

7.2 一个典型的“类型不匹配”实战案例

有位开发者在计算着色器里写了一个通用卷积函数,声明如下:

shared float cache[256]; void convolve(float[] data, int width, int idx) { ... }

调用时传入了shared内存数组。结果编译报错:数组参数无法与shared数组匹配,因为共享变量和普通全局变量在存储空间上不兼容。最后把convolve改成直接操作shared数组的专用版本,或者用buffer块把数据搬出去处理,问题才解决。这类问题在GLSL里很常见:语法的“看起来能传”和规范层面的“存储空间不匹配”是两码事。

7.3 std140对齐错位的排查流程

uniform buffer数据错位的典型特征:同一份代码在桌面端正常,移动端或者某个特定驱动上表现异常,或者改了CPU端结构体之后立刻花屏。排查步骤建议如下:

  1. CPU端用glGetUniformBlockIndex拿到块索引。
  2. 调用glGetActiveUniformsiv查询每个uniform的GL_UNIFORM_OFFSET。
  3. 在shader里分别打印/调试每个成员的实际生效值,和CPU写入值做对比。
  4. 如果偏移不一致,重点检查vec3、数组、嵌套结构体的对齐规则。

实际项目中我见过有人为了对齐,在C++结构体里手动插入float padding[1]来对齐16字节,结果shader端一改字段顺序,C++端忘了同步,又花了一晚上排查。这件事的教训是:不要依赖手动padding,统一的办法是C++端定义一个宏封装std140对齐规则,或者直接把所有vec3字段手动补成vec4。

提示:凡是跨CPU/GPU边界传递的结构体数据,不要在两边各写一份“看起来一样”的定义,一定要让CPU端和shader端共享同一个布局规则文档或头文件,布局变更时同步才不容易漏。

7.4 矩阵构造顺序错误导致的模型扭曲

某次做骨骼动画,顶点着色器里要先用蒙皮矩阵变换位置,再用模型矩阵变换到世界空间。代码写成了:

mat4 skinTransform = mat4(0.0); mat4 finalTransform = model * skinTransform; // 顺序写反成 skinTransform * model

表现是模型每隔一段时间就“跳一下”,不是完全扭曲,但动画明显不对。后来发现不是动画数据问题,而是构造矩阵时第一个参数按行传了,mat4的列主序构造导致变换顺序全乱。GLSL里写矩阵构造时,记住三列是连续的,不能按行的习惯写,否则“旋转”“平移”效果会串位。

排查路径也分享一下:先在CPU端把同一个矩阵用数学库算好,再上传到uniform里做对比,如果CPU算出来的结果和shader计算的不一致,基本可以确定是构造顺序或者列主序/行主序搞反了。

8. 一些项目里的使用建议和最终经验

从实际项目角度,补充几条关于变量与类型使用的长期经验,这可能比单纯背规则更有用。

第一,写shader时先声明所有的in、out、uniform,再写函数和逻辑。这样编译器能更早发现类型问题,GPU编译器的优化也更有依据。很多人习惯把逻辑和变量声明混在一起写,最后变量多了,自己都忘了哪个是哪个。

第二,尽量使用新版布局限定符。现在的GLSL版本都支持location,尽量不用旧式的layout缺省绑定方式,显式指定location可以避免跨阶段接口匹配错位。

第三,计算着色器里索引变量统一用uint,避免和gl_GlobalInvocationID的类型做不必要的转换。顶点属性索引、纹理数组索引等也优先用int,不要随手写成float。

第四,类型转换要显示做,即使编译器允许隐式转换,代码里也应该优先写构造函数。这样在团队协作时,别人看代码能一眼理解你的意图,不用猜这个转换是“有意的”还是“手滑的”。

最后再提一个经常被忽略的点:GLSL的规范文档本身读起来虽然枯燥,但很多驱动编译器的行为是以规范为基准实现的。遇到“这个shader在A卡上正常、在B卡上花屏”这类问题,翻一翻规范里对应的章节,往往比在网上搜半天案例更有效。变量与类型这一章更是在底子上决定了shader能不能编译、编译后跑得对不对。把这里的规则吃透,后面学高级光照、几何着色器、计算着色器,都会顺畅很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询