04. 输入输出变量:attribute, uniform, varying
GLSL 中的变量不仅有类型(int/float),还有存储限定符,决定了数据从哪里来,到哪里去。这是理解渲染管线的关键。
⚠️ 注意:在使用 RawShaderMaterial 时,所有这些变量都需要你手动声明!
1. attribute (属性)
“每个顶点独有的数据”
- 来源:从 JavaScript (BufferGeometry) 传入。
- 作用域:仅在顶点着色器 (Vertex Shader) 中可用。
- 频率:每个顶点读取一次不同的值。
- 只读:不能修改。
常见例子:
position: 顶点的坐标 (x, y, z)normal(法线):- 定义:一个垂直于表面的向量,通常用于告诉光线“我是朝向哪里的”。
- 用途:光照计算。如果不垂直于光源,表面就会变暗。
uv(纹理坐标):- 定义:一个二维坐标 (0~1),对应图片上的位置。
- 用途:告诉 GPU 把图片的哪一部分贴到这个顶点上(像给糖果包糖纸)。
// 顶点着色器 (使用 RawShaderMaterial 时必须显式声明)
attribute vec3 position;
attribute vec2 uv;
void main() {
// 我们可以读取 position,但不能修改它
// position = vec3(0.0); // ❌ 错误
}
2. uniform (统一变量)
“全局常量”
- 来源:从 JavaScript 传入。
- 作用域:在顶点着色器和片段着色器中均可访问。
- 频率:对于一次绘制调用(Draw Call),所有顶点和所有像素看到的
uniform值都是一样的。 - 只读:不能在 Shader 中修改。
常见例子:
uTime: 当前时间(用于动画)uResolution: 屏幕分辨率uMouse: 鼠标位置modelViewMatrix: 变换矩阵 (Three.js 传入)projectionMatrix: 投影矩阵 (Three.js 传入)
// 片段着色器
precision mediump float; // 记得声明精度
uniform float uTime;
uniform vec3 uColor;
void main() {
// 所有像素使用相同的时间值
gl_FragColor = vec4(uColor, 1.0);
}
3. varying (变化变量)
“着色器之间的信使”
- 来源:由顶点着色器定义并赋值。
- 去向:传递给片段着色器读取。
- 魔法:插值 (Interpolation)。
这是 GLSL 最神奇的地方。如果你在顶点着色器中给三角形的三个顶点分别赋值红色、绿色、蓝色,那么传递到片段着色器时,三角形内部的像素会自动呈现平滑的渐变色。
关于 UV 的插值: 这正是贴图能工作的原理。
- 左上角顶点说:"我是图片的 (0,1) 位置"。
- 右上角顶点说:"我是图片的 (1,1) 位置"。
- 中间的像素会自动算出:"那我是 (0.5, 1)",于是它就去取图片正中间的颜色。
关于法线的插值: 法线也会被插值,但有一个重要问题:
- 顶点着色器中的法线可能是归一化的(长度为 1.0)
- 但插值后,中间像素的法线长度可能不再是 1.0
- ⚠️ 重要:在片段着色器中使用法线前,通常需要重新归一化 (
normalize()) - 这样才能确保法线只表示方向,不包含长度信息(用于光照计算时很重要)
// --- 顶点着色器 ---
varying vec2 vUv; // 1. 声明要传递出的变量
void main() {
vUv = uv; // 2. 赋值
gl_Position = ...;
}
// --- 片段着色器 ---
precision mediump float;
varying vec2 vUv; // 3. 声明要接收的变量(名字必须一样)
void main() {
// 4. 使用(此时 vUv 已经是插值后的结果了)
gl_FragColor = vec4(vUv, 0.0, 1.0);
}
总结表
| 限定符 | 数据来源 | 可以在哪里读取 | 变化频率 | 用途示例 |
|---|---|---|---|---|
| attribute | JS Buffer | 仅 Vertex | 每个顶点不同 | 顶点位置、法线 |
| uniform | JS | Vertex & Fragment | 每次绘制相同 (全局) | 时间、光照颜色 |
| varying | Vertex Shader | Fragment | 每个像素插值不同 | 传递 UV、法线 |
💡 记忆口诀
- attribute: 个体户(每个顶点不一样)
- uniform: 广播(所有人听到的都一样)
- varying: 快递员(从顶点送到片段,路上还顺便平滑了一下)
📝 编程作业
难度:⭐⭐ 中等 | 预计时间:45-60 分钟
任务要求
创建一个立方体,深入理解 attribute、uniform、varying 的区别和使用方法。
具体要求
- 修改顶点位置:在顶点着色器中使用
attribute vec3 position修改顶点位置(如添加sin波动) - 传递法线:使用
attribute vec3 normal读取法线,通过varying传递到片段着色器 - Uniform 控制:使用
uniform控制立方体的颜色 - Varying 插值:在片段着色器中使用插值后的法线创建视觉效果
代码框架
// 顶点着色器
precision mediump float;
precision mediump int;
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
uniform float uTime; // 用于动画
attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv;
// 传递给片段着色器
varying vec3 vNormal;
varying vec3 vPosition;
void main() {
// 任务 1:修改顶点位置(添加波动)
vec3 pos = position;
pos.y += sin(position.x * 2.0 + uTime) * 0.1;
// 任务 2:传递法线
vNormal = normal;
vPosition = pos;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
// 片段着色器
precision mediump float;
uniform vec3 uColor; // 任务 3:uniform 控制颜色
varying vec3 vNormal; // 任务 4:使用插值后的法线
varying vec3 vPosition;
void main() {
vec3 normal = normalize(vNormal);
// 简单的法线可视化(将法线映射到颜色)
vec3 color = normal * 0.5 + 0.5; // 从 [-1,1] 映射到 [0,1]
// 混合 uniform 颜色
color = mix(color, uColor, 0.5);
gl_FragColor = vec4(color, 1.0);
}
JavaScript 部分
const geometry = new THREE.BoxGeometry(1, 1, 1, 10, 10, 10); // 增加分段数
const uniforms = {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0.5, 0.8, 1.0) },
};
// 在动画循环中更新
uniforms.uTime.value = clock.getElapsedTime();
// 动态修改 uniform 颜色示例:
// uniforms.uColor.value.setRGB(1.0, 0.0, 0.0); // 改为红色
// uniforms.uColor.value.setRGB(0.0, 1.0, 0.0); // 改为绿色
检查清单
- 所有变量都正确声明(
attribute、uniform、varying) - 顶点位置有明显的变化(波动效果)
- 法线信息正确传递到片段着色器
- Uniform 颜色可以动态修改(在 JavaScript 中修改
uniforms.uColor.value,能看到颜色变化) - 法线可视化效果清晰可见(通过
mix(color, uColor, 0.5)混合)
扩展挑战
- 🌟 添加
uniform vec3 uMouse,让颜色根据鼠标位置变化 - 🌟 使用法线计算简单的光照效果
- 🌟 尝试传递多个
varying变量(如 UV、自定义数据)
参考资源
- 完整作业清单:编程作业清单.md