第三课:深入着色器语言GLSL——数据类型、变量修饰符、内置变量与函数
好,咱们接着聊。上一章我们把OpenGL ES的渲染管线捋了一遍,也写了一个能跑起来的三角形。但说实话,那个三角形的颜色是写死的,顶点位置也是硬编码的。这哪叫「从入门到精通」?顶多算「从入门到放弃的边缘」。
真正让GPU听你话的,是着色器语言——GLSL。这一章,我们就深入进去,看看GLSL到底有哪些数据类型、变量修饰符,以及那些好用的内置变量和函数。最后,我会带你手写第一个真正意义上的自定义着色器。
核心知识点一览
GLSL数据类型 → 变量修饰符 → 内置变量/函数 → 自定义着色器实战
GLSL的数据类型——比你想象的要丰富
GLSL是强类型语言。什么意思?就是你不能像写JavaScript那样,一个变量今天存整数明天存字符串。每个变量在声明时就必须确定类型。
我刚开始学的时候,觉得这很烦。但后来发现,强类型恰恰是GPU高效运行的基础——GPU是并行计算单元,它需要知道每个数据占多少字节,才能批量处理。
基本数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
| float | 单精度浮点数 | 1.0, -3.14, 2.5e-3 |
| int | 有符号整数 | 42, -7, 0 |
| bool | 布尔值 | true, false |
注意:GLSL里写浮点数一定要带小数点。写float a = 1;会报错,必须写成float a = 1.0;。这个坑我踩过,编译半天过不去,最后发现是少了个点。
向量类型——GPU的拿手好戏
向量是GLSL的灵魂。GPU天生就是为向量运算设计的,一条指令能同时处理多个分量。
| 类型 | 分量 | 访问方式 |
|---|---|---|
| vec2 | 2个float | .xy, .rg, .st |
| vec3 | 3个float | .xyz, .rgb, .stp |
| vec4 | 4个float | .xyzw, .rgba, .stpq |
我个人习惯用.xyzw表示位置坐标,用.rgba表示颜色。这样代码可读性更好。你想想看,看到color.rgb一眼就知道是颜色,看到position.xy就知道是坐标。
小技巧:GLSL支持swizzle操作,也就是可以任意重组分量。比如vec4(1.0, 2.0, 3.0, 4.0).zyxw会得到(3.0, 2.0, 1.0, 4.0)。这在做颜色通道交换时特别方便。
矩阵类型
矩阵在3D变换中无处不在。GLSL提供了mat2、mat3、mat4,分别对应2x2、3x3、4x4矩阵。
// 声明一个4x4单位矩阵
mat4 identity = mat4(1.0);
// 矩阵乘法
vec4 position = projection * view * model * vec4(inputPos, 1.0);
矩阵乘法顺序很重要。我记得有一次调试一个模型死活显示不对,最后发现是把model * view写成了view * model。矩阵乘法不满足交换律,这个一定要记住。
采样器类型
纹理采样器sampler2D和samplerCube是专门用来从纹理中取颜色的。它们只能声明为uniform,不能随意赋值。
uniform sampler2D uTexture;
// 在片元着色器中采样
vec4 texColor = texture2D(uTexture, vTexCoord);
变量修饰符——数据从哪里来,到哪里去
GLSL的变量修饰符,说白了就是告诉GPU:这个数据是CPU传进来的,还是顶点着色器算好要传给片元着色器的,还是每个顶点都不同的。
| 修饰符 | 作用域 | 说明 |
|---|---|---|
| attribute | 顶点着色器 | 每个顶点不同的输入数据(位置、法线、颜色等) |
| uniform | 全局 | 所有顶点/片元共享的常量(变换矩阵、光源位置等) |
| varying | 顶点→片元 | 从顶点着色器传递到片元着色器的插值数据 |
| const | 局部 | 编译期常量,不可修改 |
这里有个容易混淆的点:attribute只能在顶点着色器中使用,varying是连接顶点和片元着色器的桥梁。我在项目中见过有人把varying写在片元着色器里当普通变量用,结果编译报错——其实varying需要在两端都声明,类型和名字必须一致。
注意:在OpenGL ES 3.0中,attribute和varying被in/out取代了。但2.0版本仍然广泛使用,而且很多教材还是用老写法。我这里讲的是ES 2.0的语法,兼容性更好。
内置变量与函数——GPU给你的免费工具
GLSL提供了一些内置变量,你不用声明就能直接用。这些变量是GPU运行时自动填充的。
顶点着色器内置变量
gl_Position:vec4类型,必须赋值。表示顶点在裁剪空间中的位置。gl_PointSize:float类型,控制点精灵的大小。
片元着色器内置变量
gl_FragColor:vec4类型,输出片元的最终颜色。gl_FragCoord:vec4类型,片元在窗口中的坐标。gl_FrontFacing:bool类型,判断当前片元是否属于正面。
常用内置函数
| 分类 | 函数 | 说明 |
|---|---|---|
| 数学 | sin, cos, tan, abs, floor, ceil, mod | 基础数学运算 |
| 插值 | mix(a, b, t) | 线性插值,返回 a*(1-t) + b*t |
| 限制 | clamp(x, min, max) | 将x限制在[min, max]区间 |
| 向量 | length, normalize, dot, cross | 向量长度、归一化、点积、叉积 |
| 纹理 | texture2D(sampler, coord) | 从纹理中采样颜色 |
mix函数我几乎每个项目都会用。做颜色渐变、透明度混合、动画过渡,它都是首选。比你自己写a*(1-t) + b*t要快,因为GPU对mix有硬件优化。
实战:第一个自定义着色器
好了,理论说够了。我们来写一个真正能跑的自定义着色器。这个着色器会让一个三角形从中心向外渐变颜色,并且随时间变化。
顶点着色器
// simple_vertex.glsl
attribute vec4 aPosition;
attribute vec2 aTexCoord;
uniform mat4 uMVPMatrix;
varying vec2 vTexCoord;
void main() {
gl_Position = uMVPMatrix * aPosition;
vTexCoord = aTexCoord;
}
片元着色器
// simple_fragment.glsl
precision mediump float;
varying vec2 vTexCoord;
uniform float uTime;
void main() {
// 计算片元到中心的距离
vec2 center = vec2(0.5, 0.5);
float dist = distance(vTexCoord, center);
// 根据距离和时间混合颜色
vec3 color1 = vec3(1.0, 0.2, 0.2); // 红色
vec3 color2 = vec3(0.2, 0.2, 1.0); // 蓝色
// sin(uTime) 在 -1 到 1 之间变化,映射到 0 到 1
float mixFactor = 0.5 + 0.5 * sin(uTime + dist * 3.0);
vec3 finalColor = mix(color1, color2, mixFactor);
gl_FragColor = vec4(finalColor, 1.0);
}
这段代码做了什么?
- 顶点着色器把纹理坐标传给片元着色器。
- 片元着色器计算每个片元到纹理中心(0.5, 0.5)的距离。
- 用
sin函数让颜色随时间波动,距离越远波动相位越不同。 - 最终效果:一个从红到蓝渐变、并且像呼吸一样脉动的三角形。
我的经验:写片元着色器时,precision mediump float;这行一定要加。不加的话,有些GPU会默认用低精度,导致颜色出现条带。我曾经在一个项目里排查了半天,最后发现是精度声明的问题。
Java端如何传递uniform
着色器写好了,还得从Java端把uTime传进去。代码大概是这样的:
// 获取uniform位置
int timeHandle = GLES20.glGetUniformLocation(program, "uTime");
// 在渲染循环中更新
float time = System.currentTimeMillis() / 1000.0f;
GLES20.glUniform1f(timeHandle, time);
这里要注意:glUniform1f要在glUseProgram之后调用,否则传不进去。我刚开始经常忘记这个顺序,结果uniform值一直是默认的0。
避坑指南
最后,分享几个我踩过的坑:
- 变量命名冲突:GLSL的变量名不能以
gl_开头,那是内置变量的保留前缀。 - 精度声明:片元着色器必须声明默认精度,否则在某些设备上会编译失败。
- 循环次数:GLSL中的循环必须是常数次,不能用变量控制循环次数。因为GPU需要提前知道指令数量。
- 条件分支:尽量少用if-else,GPU是并行执行的,分支会导致部分单元等待。能用
mix、clamp、step解决的,就别用if。
嗯,这一章的内容就到这里。GLSL其实不难,关键是动手写。你把上面那个着色器跑起来,改改颜色值、调调sin的频率,很快就能找到感觉。
公众号:蓝海资料掘金营,微信deep3321