1. 顶点着色器 vs 片段(片元)着色器

1.1. 核心比喻

想象一下制作一个 3D 粘土模型(网格) 然后 给它拍照

1.2. 对比表格

顶点着色器vs片元着色器.png

1.3. 详细职责与工作流

顶点着色器:几何变形专家

主要工作是把 3D 模型从本地坐标一步步转换到屏幕上的 2D 坐标。

glsl

// Three.js 中最简单的顶点着色器示例
// 展示了标准坐标变换流水线
void main() {
    // 1. 将顶点从模型本地空间转换到世界空间
    // 2. 再转换到摄像机观察空间
    // 3. 最后通过投影矩阵转换到裁剪空间
    // 这三步在 Three.js 中通常被合并为:
    // modelViewMatrix = viewMatrix × modelMatrix
    // projectionMatrix × modelViewMatrix
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

顶点着色器能做什么?

  // 正弦波效果
  float wave = sin(position.x * 10.0 + time) * 0.2;
  vec3 newPosition = position;
  newPosition.y += wave;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(newPosition, 1.0);
  varying vec2 vUv;
  varying vec3 vWorldPosition;
  varying vec3 vNormal;

  void main() {
      vUv = uv;  // 传递UV坐标
      vNormal = normalMatrix * normal;  // 变换法线
      vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }

片段着色器:视觉表现大师

主要工作是为屏幕上每个像素计算最终颜色。

glsl

// 最简单的片段着色器 - 纯色
void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 红色
}

// 使用顶点着色器传递的UV坐标采样纹理
uniform sampler2D map;
varying vec2 vUv;

void main() {
    vec4 texColor = texture2D(map, vUv);
    gl_FragColor = texColor;
}

片段着色器能做什么?

  // 简单的漫反射光照
  varying vec3 vNormal;
  uniform vec3 lightDirection;
  uniform vec3 diffuseColor;

  void main() {
      float diffuse = max(dot(normalize(vNormal), normalize(-lightDirection)), 0.0);
      vec3 color = diffuseColor * diffuse;
      gl_FragColor = vec4(color, 1.0);
  }

1.4. 数据传递:管道连接

顶点着色器和片段着色器之间通过 varying 变量(或 GLSL 300 es 中的 in/out 通信:

text

顶点着色器 → [插值器] → 片段着色器
    ↓                     ↓
每个顶点计算         每个像素接收插值后的数据

关键点:片段着色器接收的是插值后的数据!

glsl

// 顶点着色器
attribute vec3 color;
varying vec3 vColor;

void main() {
    vColor = color;  // 每个顶点有自己的颜色
    gl_Position = ...;
}

// 片段着色器
varying vec3 vColor;

void main() {
    // vColor 在这里是三个顶点颜色的插值混合!
    gl_FragColor = vec4(vColor, 1.0);
}

1.5. 性能考虑

  1. 顶点着色器执行次数 ≈ 顶点数量

  2. 片段着色器执行次数 ≈ 屏幕像素数 × 模型覆盖面积(有深度测试和提前终止优化)

优化原则


1.6. 实际案例对比

场景:创建一个波浪平面

方案A:只在顶点着色器做波浪

glsl

// 顶点着色器 - 计算波浪位移
uniform float time;
varying float vHeight;

void main() {
    float height = sin(position.x + time) * 0.5;
    vHeight = height;
    vec3 pos = vec3(position.x, position.y + height, position.z);
    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

// 片段着色器 - 基于高度着色
varying float vHeight;
void main() {
    vec3 color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 1.0, 0.0), vHeight + 0.5);
    gl_FragColor = vec4(color, 1.0);
}
方案B:在片段着色器计算波浪

glsl

// 顶点着色器 - 只传递世界位置
varying vec3 vWorldPos;
void main() {
    vWorldPos = (modelMatrix * vec4(position, 1.0)).xyz;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}

// 片段着色器 - 计算波浪和颜色
uniform float time;
varying vec3 vWorldPos;
void main() {
    // 问题:每个像素都计算sin,开销大!
    float height = sin(vWorldPos.x + time) * 0.5;
    vec3 color = mix(vec3(0.0, 0.0, 1.0), vec3(1.0, 1.0, 0.0), height + 0.5);
    gl_FragColor = vec4(color, 1.0);
}

结果对比


1.7. 黄金法则

  1. 顶点着色器决定 "形状和位置"

  2. 片段着色器决定 "外观和颜色"

  3. 能用顶点着色器预计算的,就不要在片段着色器重复计算

  4. 需要逐像素精确计算的(如纹理、光照细节),必须在片段着色器

理解这个区别是掌握Three.js高级渲染技术的基石。通常,一个效果需要两者协作:顶点着色器准备数据、片段着色器实现最终视觉效果。

1.8. 图形学拓展阅读

1.8.1. 在 Three.js(以及大多数图形学框架)中:

text

modelViewMatrix = viewMatrix × modelMatrix

注意:矩阵乘法是从右往左应用的,所以:

glsl

// 在顶点着色器中,这相当于:
vec4 modelViewPosition = viewMatrix * modelMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * modelViewPosition;

为什么 Three.js 提供 modelViewMatrix?

为了优化!Three.js 在 CPU 端预先计算了这个乘法,避免在 GPU 的每个顶点上重复计算:

javascript

// Three.js 内部大概是这样做的:
material.uniforms.modelViewMatrix.value = camera.matrixWorldInverse.multiply(object.matrixWorld);

完整的变换流水线

让我们看看从模型局部坐标到屏幕坐标的完整过程:

text

模型局部空间 → 世界空间 → 观察空间 → 裁剪空间 → 屏幕空间
     ↓           ↓          ↓           ↓          ↓
  position   modelMatrix  viewMatrix  projectionMatrix  viewport变换
                     \        /                /
                  modelViewMatrix      gl_Position

在顶点着色器中的三种写法(效果相同):
position 是顶点在模型局部空间(Model Local Space / Object Space)中的坐标。

它描述的是网格在自身坐标系中的原始几何形状,不考虑物体在场景中的位置、旋转或缩放。
关键点:position 永远不变!

glsl

// 写法1:使用预计算的 modelViewMatrix(最常用、最优化)
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

// 写法2:显式分解(更清晰理解过程)
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vec4 viewPosition = viewMatrix * worldPosition;
gl_Position = projectionMatrix * viewPosition;

// 写法3:直接使用 modelViewProjectionMatrix(如果Three.js提供了的话)
// gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

视觉化理解

想象一个在 (1, 0, 0) 位置的立方体和一个在 (0, 0, 5) 的摄像机:

javascript

// JavaScript 设置
cube.position.set(1, 0, 0);
camera.position.set(0, 0, 5);
camera.lookAt(0, 0, 0);

// 在顶点着色器中的变换过程:
// 1. modelMatrix:将立方体从局部中心移到世界空间的 (1, 0, 0)
// 2. viewMatrix:将所有物体反向移动,使摄像机在原点看向 -Z
//    (相当于把世界移动,让摄像机在原点)
// 3. projectionMatrix:应用透视/正交投影

相关矩阵速查表

相关矩阵速查表.png

实际代码示例

javascript

// 创建着色器材质
const material = new THREE.ShaderMaterial({
    vertexShader: `
        uniform mat4 modelMatrix;
        uniform mat4 viewMatrix;
        uniform mat4 projectionMatrix;
        uniform mat4 modelViewMatrix;

        // 这些是等价的!
        void main() {
            // 方式1:直接使用预计算的 modelViewMatrix
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);

            // 方式2:自己组合(验证它们相等)
            vec4 test1 = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            vec4 test2 = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
            // test1 应该完全等于 test2(可能有浮点误差)
        }
    `,
    fragmentShader: `...`
});

常见误区澄清

误区1: modelViewMatrix = modelMatrix × viewMatrix

误区2: modelViewMatrix 包含了投影

误区3: 可以随意修改这些矩阵

什么时候需要自己计算?

大多数时候用 modelViewMatrix 就行,但有些特殊情况需要分解:

glsl

// 例子:需要在世界空间中计算光照
varying vec3 vWorldPosition;
varying vec3 vViewPosition;

void main() {
    // 世界空间位置(用于世界空间的光照计算)
    vec4 worldPos = modelMatrix * vec4(position, 1.0);
    vWorldPosition = worldPos.xyz;

    // 观察空间位置(也可以从 modelViewMatrix 获得)
    vec4 viewPos = modelViewMatrix * vec4(position, 1.0);
    vViewPosition = viewPos.xyz;

    // 裁剪空间位置(最终输出)
    gl_Position = projectionMatrix * viewPos;
}

总结