02 GLSL 数据类型与基本语法
学完本章能做什么
你将能读懂基础 GLSL 声明,使用标量和向量组合颜色,判断哪些运算需要浮点数,并能在 Three.js 中传入自定义 uniform。
先看现象
打开本章示例。画面的红色随横向 UV 变化,绿色随纵向 UV 变化。拖动滑块只改变蓝色通道;勾选复选框交换红绿通道;选择“平均值”后,三个通道被计算成同一个灰度值。这三个控件分别对应 float、bool 和 int uniform。
1. 标量:一个值
| 类型 | 示例 | 用途 |
|---|---|---|
float |
float strength = 0.35; |
位置、时间、颜色、比例等连续数值。 |
int |
int mode = 1; |
模式编号、循环计数等整数。 |
bool |
bool enabled = true; |
条件开关;值为 true 或 false。 |
GLSL 对类型比 JavaScript 更严格。下面写法清楚地执行浮点除法:
float halfValue = 1.0 / 2.0; // 0.5
1 / 2 是整数除法,结果为整数 0;再把它放到需要 float 的位置,还可能引起类型错误。浮点常量明确写小数点,是最容易养成的习惯之一。需要转换时显式写 float(mode) 或 int(value),不要期待自动替你完成所有转换。
2. 向量:成组的值
vec2、vec3、vec4 分别装 2、3、4 个浮点数。
vec2 point = vec2( 0.2, 0.8 );
vec3 color = vec3( 1.0, 0.5, 0.0 );
vec4 rgba = vec4( color, 1.0 );
构造函数可以拼接已有的分量:vec3(point, 0.35) 把一个 vec2 和一个 float 合为 vec3;vec3(0.5) 则让三个分量都等于 0.5。构造时需要提供足够的分量,例如 vec3(0.2, 0.8) 不完整。
每个向量分量有几组常用名字:x/y/z/w 常用于位置,r/g/b/a 常用于颜色,s/t/p/q 常用于纹理坐标。它们只是同一组分量的不同叫法。
float red = color.r; // 等同于 color.x
vec2 xy = color.xy; // 取前两个分量
vec3 swapped = color.grb; // 交换红绿,保留蓝色
取多个分量叫 swizzle。读 color.grb 会产生一个新的 vec3;它不会自己改变原来的 color。本章示例通过 color = color.grb; 把新向量赋回去。
向量可以与同维度向量逐分量运算,也可以乘标量。例如 color * 0.5 会把三个通道同时减半。矩阵与向量相乘则是另一种运算,不等于逐分量相乘。
3. 矩阵与纹理采样器
mat3、mat4 是矩阵类型。第 1 章的 projectionMatrix 是 mat4,因此常与 vec4 相乘;normalMatrix 是 mat3,常与 vec3 法线相乘。乘法顺序有意义:
vec4 clip = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
sampler2D 表示二维纹理的采样器,稍后在纹理章节详细使用。它不是一张可以用普通加法直接处理的图片;要先用纹理坐标采样,得到颜色值。
4. attribute、uniform、varying 各在哪里变化
| 声明 | 谁提供或写入 | 变化频率 | 可以在哪里读 |
|---|---|---|---|
attribute |
几何体的顶点数据 | 不同顶点可以不同 | 顶点着色器 |
uniform |
Three.js 或应用代码 | 一次绘制中的所有顶点、片元共享 | 声明它的 shader 阶段 |
varying |
顶点着色器写入,光栅化器插值 | 三角形内部随位置变化 | 顶点和片元着色器均需声明 |
本章示例在 JavaScript 中定义:
const material = new THREE.ShaderMaterial( {
uniforms: {
uBlue: { value: 0.35 },
uSwap: { value: false },
uMode: { value: 0 }
},
vertexShader: vertexShaderSource,
fragmentShader: fragmentShaderSource
} );
上面只展示 uniform 的结构;完整源码给出了真实的两段 shader 字符串。片元着色器内的声明必须与传入值的用途对应:
uniform float uBlue;
uniform bool uSwap;
uniform int uMode;
varying vec2 vUv;
控件改变时,JavaScript 更新的是 material.uniforms.uBlue.value 等值。无需每次重新创建材质。
5. 从 UV 到颜色:逐行读示例
顶点着色器先将几何体自带的 uv 赋给 vUv,片元着色器再执行:
vec2 coords = vUv;
vec3 color = vec3( coords, uBlue );
if ( uSwap ) {
color = color.grb;
}
if ( uMode == 1 ) {
float average = ( color.r + color.g + color.b ) / 3.0;
color = vec3( average );
}
gl_FragColor = vec4( color, 1.0 );
#include <colorspace_fragment>
coords 是二维向量,color 是三维向量,输出颜色是四维向量。第四个分量 1.0 是完全不透明。#include <colorspace_fragment> 是 Three.js 的 shader 片段,用于把线性颜色转换到渲染器输出的色彩空间;它不是 GLSL 自带函数。
6. 函数、作用域与精度
每段 shader 都从 void main() 进入。变量写在 { ... } 块内时,只在该块及其子块内可用。可以自定义函数并复用,例如:
float average3( vec3 value ) {
return ( value.r + value.g + value.b ) / 3.0;
}
GLSL 有 highp、mediump、lowp 等精度限定。Three.js 的 ShaderMaterial 会注入精度声明;本教程的基础例子先使用默认配置。到 noise 章节再讨论精度为何可能影响不同设备上的结果。
常见问题
- **
1 / 2没有得到0.5:**两边都是整数;改为1.0 / 2.0。 - **编译器说类型不匹配:**检查
float与int、vec2与vec3,以及构造函数的分量数。 - **修改
uniform后画面不变:**确认 shader 中确实使用了它,并更新.value;变量名要与声明一致。 - **片元着色器找不到
vUv:**两段 shader 都要声明varying vec2 vUv;,且顶点阶段必须赋值。 - **颜色比数值直觉更亮:**显示经过线性到 sRGB 的转换。比较数值时先确认是在计算空间还是显示空间看颜色。
本章小结
GLSL 的类型和向量维度必须明确;attribute、uniform、varying 说明数据由谁提供、在哪个阶段变化。下一章会把这些数值放进不同坐标空间,解释为什么同一个顶点可以有几组不同的位置。