14 · Uniform 类型、数组与纹理
GLSL 是静态类型语言。uniform 的类型在编译、链接时确定,JavaScript 更新 .value 不会把 float 变成 vec3。Three.js r185 的 WebGLUniforms 根据 GPU 返回的活跃 uniform 类型选择上传函数,而不是根据某一帧 JavaScript 值重新设计着色器。
常用类型映射
| GLSL 声明 | 推荐 JavaScript 值 | 更新示例 |
|---|---|---|
float uDemoTime |
Number | .value = seconds |
int uDemoCount |
范围合法的整数 Number | .value = Math.floor(count) |
bool uDemoEnabled |
Boolean | .value = true |
vec2 uDemoOffset |
THREE.Vector2 | .value.set(x, y) |
vec3 uDemoDirection |
THREE.Vector3 | .value.copy(direction) |
vec3 uDemoTint |
THREE.Color | .value.set('#e8795f') |
vec4 uDemoBounds |
THREE.Vector4 | .value.set(x, y, z, w) |
mat3 uDemoUvMatrix |
THREE.Matrix3 | .value.setUvTransform(...) |
mat4 uDemoTransform |
THREE.Matrix4 | .value.copy(matrix) |
sampler2D uDemoMask |
THREE.Texture | .value = loadedTexture |
samplerCube uDemoEnvironment |
THREE.CubeTexture | .value = cubeTexture |
此表采用便于表达语义的类型。上传器也支持一些普通数组或 typed array 形式,但业务代码不必为了“底层”而放弃 Vector 和 Matrix 的几何方法。矩阵应交给 Three.js 对象管理,不要因为 set() 的参数看起来按行排列,就再手动转置其 elements。
GLSL 中写浮点常量推荐 1.0、0.5。整数用于索引与计数,参与浮点计算时显式转换:float(uDemoCount)。JavaScript 允许小数并不意味着 GLSL int 参数也适合存放小数;先在业务层限定范围,避免隐式截断和溢出。
更新对象:set、copy 和替换 value
const uniforms = {
uDemoDirection: { value: new THREE.Vector3(0, 1, 0) },
uDemoUvMatrix: { value: new THREE.Matrix3() }
};
uniforms.uDemoDirection.value.copy(targetDirection).normalize();
uniforms.uDemoUvMatrix.value.setUvTransform(0, 0, 2, 2, 0.3, 0.5, 0.5);
这样可以复用对象,减少渲染循环的内存分配。.value = anotherVector 也可以上传,但同时改变了共享关系:之后修改 anotherVector 就会改变该参数。无论采用哪一种,保留外围 { value } 容器,见第 13 章。
归一化是几何要求,不是上传要求。Three.js 不会自动归一化自定义方向 uniform。若 Shader 公式要求单位方向,输入零向量时还需选择明确的退化策略。
数组的容量与有效数量
例如一面墙受到最多四个圆形热源影响:
uniform vec3 uDemoCenters[4];
uniform float uDemoRadii[4];
uniform int uDemoCount;
const heatUniforms = {
uDemoCenters: {
value: Array.from({ length: 4 }, () => new THREE.Vector3())
},
uDemoRadii: { value: new Float32Array([1, 1, 1, 1]) },
uDemoCount: { value: 0 }
};
heatUniforms.uDemoCenters.value[0].set(1, 2, 0);
heatUniforms.uDemoRadii.value[0] = 0.6;
heatUniforms.uDemoCount.value = 1;
GLSL 的数组容量是 4;当前使用一个热源时,只改 count,不把数组缩为长度 1。不能写 uniform vec3 centers[uDemoCount];,因为 uniform 不是编译期常量。要把容量提高到 8,需要改源码或 define,并重新编译。数量很大时考虑 DataTexture、实例属性等表示方式;uniform 有硬件容量限制,不能把任意规模的数据塞进去。
多个向量对象数组会由上传器展平成数值数组;普通浮点数组也可以用 Float32Array。不要用 Array(4).fill(new THREE.Vector3()) 创建中心数组,否则四个元素引用的是同一向量。
sampler 存的是纹理对象,不是纹理单元编号
const mask = new THREE.TextureLoader().load('/pictures/mask.png');
mask.colorSpace = THREE.NoColorSpace;
const uniform = { value: mask };
这里的路径是示意,使用项目中实际存在的资源。Three.js 分配纹理单元并绑定对象,不需要手动往 .value 中写 0 或底层 WebGLTexture。
颜色贴图通常标记 SRGBColorSpace;遮罩、粗糙度、金属度、法线等数据贴图通常使用 NoColorSpace。传给 sampler2D 不代表会自动获得内置 map 的整套 UV 和解码逻辑。自定义采样应确认纹理实际颜色空间和上传格式;对常见 sRGB 图像,在正确标记及对应纹理格式下采样会得到线性值,不要再盲目手写一次 gamma 解码。使用视频、特殊浮点数据或自定义格式时要单独核对路径。
用 new THREE.Color('#...') 得到的颜色在默认颜色管理下进入线性工作空间。new THREE.Vector3(r, g, b) 只是三个数字,不会把显示空间颜色自动转换为线性颜色。
三种不同的更新
- 换一张同类型纹理:更新
uniform.value,通常无需重编译;新纹理自身负责上传。 - 修改已存在 DataTexture 的像素:修改
.image.data,再设置texture.needsUpdate = true。 - 改 GLSL sampler 类型、添加编译条件或改变内置贴图功能开关:这是程序结构变化,按第 16 章处理。
纹理对象与材质是独立资源。material.dispose() 不会代替 texture.dispose()。仅在确定没有其他对象使用时释放共享纹理;克隆 uniform 容器不应隐式复制或销毁共享纹理。
源码核对
阅读 node_modules/three/src/renderers/webgl/WebGLUniforms.js 中 getSingularSetter()、getPureArraySetter()、flatten() 与各纹理上传 setter,可看到上述类型的分派方式。活跃数组长度可能受编译优化影响;编写业务接口时仍以声明容量和统一布局为准,不依赖这种优化细节。
练习(暂不提供答案)
- 用两个 Vector3 uniform 表示线段端点,在片元阶段计算距离时,还需要明确什么坐标空间?
- 建立容量为 8 的热源参数,支持 0~8 个有效源,列出哪些更新需要重编译。
- 用同一张灰度图分别作为颜色贴图和数据遮罩,比较颜色空间设置导致的阈值差异。
- 设计纹理替换和销毁接口,使两个材质共享纹理时不会提前释放资源。