Skip to main content

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 的插值: 这正是贴图能工作的原理。

  1. 左上角顶点说:"我是图片的 (0,1) 位置"。
  2. 右上角顶点说:"我是图片的 (1,1) 位置"。
  3. 中间的像素会自动算出:"那我是 (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);
}

总结表

限定符数据来源可以在哪里读取变化频率用途示例
attributeJS Buffer仅 Vertex每个顶点不同顶点位置、法线
uniformJSVertex & Fragment每次绘制相同 (全局)时间、光照颜色
varyingVertex ShaderFragment每个像素插值不同传递 UV、法线

💡 记忆口诀

  • attribute: 个体户(每个顶点不一样)
  • uniform: 广播(所有人听到的都一样)
  • varying: 快递员(从顶点送到片段,路上还顺便平滑了一下)

📝 编程作业

难度:⭐⭐ 中等 | 预计时间:45-60 分钟

任务要求

创建一个立方体,深入理解 attributeuniformvarying 的区别和使用方法。

具体要求

  1. 修改顶点位置:在顶点着色器中使用 attribute vec3 position 修改顶点位置(如添加 sin 波动)
  2. 传递法线:使用 attribute vec3 normal 读取法线,通过 varying 传递到片段着色器
  3. Uniform 控制:使用 uniform 控制立方体的颜色
  4. 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); // 改为绿色

检查清单

  • 所有变量都正确声明(attributeuniformvarying
  • 顶点位置有明显的变化(波动效果)
  • 法线信息正确传递到片段着色器
  • Uniform 颜色可以动态修改(在 JavaScript 中修改 uniforms.uColor.value,能看到颜色变化)
  • 法线可视化效果清晰可见(通过 mix(color, uColor, 0.5) 混合)

扩展挑战

  • 🌟 添加 uniform vec3 uMouse,让颜色根据鼠标位置变化
  • 🌟 使用法线计算简单的光照效果
  • 🌟 尝试传递多个 varying 变量(如 UV、自定义数据)

参考资源