3、着色器语言GLSL:数据类型、变量修饰符、内置变量、函数定义、精度限定
好,咱们今天来啃GLSL这块硬骨头。说实话,很多搞WebGL的朋友,一上来就被着色器语言劝退了。其实大可不必。GLSL没那么玄乎,它就是个专门伺候GPU的小语言。你把它当成C语言的一个远房亲戚,学起来就顺了。
3.1 数据类型:GPU能理解的东西
GLSL的数据类型,说白了就是告诉GPU:「我要处理什么玩意儿」。跟JavaScript那种动态类型不同,GLSL是强类型的,每个变量都得说清楚自己是什么。
基本类型:
float:浮点数。注意,GLSL里写1.0才是float,写1会被当成int。我刚开始写的时候老犯这个错,结果编译报错一脸懵。int:整数。用得不多,因为GPU天生擅长浮点运算。bool:布尔值。true/false,跟C一样。
向量类型:这才是GLSL的精髓。
vec2、vec3、vec4:分别代表2、3、4个分量的浮点向量。ivec2、ivec3、ivec4:整数向量。bvec2、bvec3、bvec4:布尔向量。
你想想看,一个颜色就是vec4(r, g, b, a),一个位置就是vec3(x, y, z)。多直观。
矩阵类型:
mat2、mat3、mat4:2x2、3x3、4x4矩阵。做变换用的。
采样器类型:
sampler2D:2D纹理采样器。这个后面讲纹理时会重点说。
核心要点:GLSL里没有指针,没有字符串,没有数组(其实有,但用起来很受限)。它只关心数学运算。你把它当成一个「数学计算器」就对了。
3.2 变量修饰符:数据从哪里来,到哪里去
变量修饰符,说白了就是给变量贴个标签,告诉GPU:「这个数据是外面传进来的」、「这个数据是临时算的」、「这个数据要输出出去」。
| 修饰符 | 含义 | 使用场景 |
|---|---|---|
attribute |
顶点属性,从CPU传入顶点着色器 | 顶点位置、颜色、法线等 |
uniform |
全局常量,从CPU传入着色器 | 变换矩阵、时间、光源位置 |
varying |
从顶点着色器传到片元着色器的插值数据 | 颜色、纹理坐标 |
const |
编译时常量 | 固定数值,比如PI |
嗯,这里要注意:在WebGL 2.0里,attribute被in取代,varying被out/in取代。但WebGL 1.0还是老一套。我个人建议,如果你是新项目,直接用WebGL 2.0,省得后面再改。
我的经验:uniform是性能杀手。每次更新uniform都要调用gl.uniformXXX(),调用次数多了会卡。我曾经在一个粒子系统里每帧更新几百个uniform,结果帧率直接掉到20。后来改成用纹理传数据,才把性能救回来。
3.3 内置变量:着色器自带的「外挂」
GLSL里有一些变量是自动存在的,你不用声明就能用。这些叫内置变量。
顶点着色器内置变量:
gl_Position:vec4类型。必须赋值。它决定了顶点在裁剪空间里的位置。你不给它赋值,GPU就不知道顶点该画在哪。gl_PointSize:float类型。点的大小。只有画点精灵时才用。
片元着色器内置变量:
gl_FragColor:vec4类型。片元的最终颜色。在WebGL 1.0里必须赋值。gl_FragCoord:vec4类型。片元在窗口中的坐标。可以用来做屏幕空间特效。gl_FrontFacing:bool类型。判断当前片元是不是正面。做双面渲染时有用。
避坑指南:我曾经在片元着色器里忘了给gl_FragColor赋值,结果整个屏幕都是黑的。找了半天才发现是这行代码被我不小心注释掉了。所以,写完着色器第一件事:检查gl_Position和gl_FragColor有没有赋值。
3.4 函数定义:把重复逻辑封装起来
GLSL支持函数,但跟C语言比,限制多一些。
// 函数定义示例
float calculateBrightness(vec3 color) {
return 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
}
void main() {
vec3 myColor = vec3(1.0, 0.5, 0.2);
float brightness = calculateBrightness(myColor);
gl_FragColor = vec4(vec3(brightness), 1.0);
}
注意几点:
- 函数必须指定返回类型。没有返回值就用
void。 - 参数传递默认是值传递,不支持引用传递(没有
&符号)。 - 函数可以重载,但参数类型必须不同。
- 递归?别想了。GLSL不支持递归调用。GPU的硬件架构决定了它没法处理递归。
说白了,GLSL的函数就是个「数学公式封装器」。别想着在里面做复杂的逻辑判断,GPU不擅长那个。
3.5 精度限定:性能与精度的取舍
精度限定,是GLSL里一个很「嵌入式」的概念。它告诉GPU:「这个计算我要多精确?」
| 精度修饰符 | 说明 | 典型场景 |
|---|---|---|
highp |
高精度,32位浮点 | 顶点位置、矩阵运算 |
mediump |
中精度,16位浮点 | 颜色、纹理坐标 |
lowp |
低精度,10位浮点 | 光照系数、简单插值 |
你想想看,移动端的GPU性能有限。如果你所有计算都用highp,功耗和发热都扛不住。但如果你用lowp算位置,那画面可能就出现「锯齿」或者「颜色断层」。
最佳实践:在片元着色器开头,一般会写一句precision mediump float;。这样所有浮点变量默认就是中精度。个别需要高精度的变量再单独声明。我习惯在顶点着色器里用precision highp float;,因为顶点位置精度不够的话,模型会「抖动」。
嗯,这里有个坑:不是所有设备都支持highp。在片元着色器里用highp之前,最好检查一下扩展GL_OES_fragment_precision_high是否可用。我曾经在某个老款安卓机上跑demo,片元着色器里用了highp,结果直接黑屏。查了半天才发现是设备不支持。
好了,GLSL的基础就这些。说白了,它就是一套「写给GPU看的数学公式」。数据类型搞清楚,修饰符用对地方,内置变量别漏掉,函数写得简洁点,精度选得合适点——你的着色器就能跑得又快又稳。
下一章,咱们开始写第一个真正的着色器程序。到时候你会发现,这些基础知识全都能用上。