Skip to main content

07. 从顶点到片段:插值的魔法

理解“光栅化”和“插值”是理解为什么 varying 变量会渐变的关键。

1. 渲染管线简化版

  1. 顶点处理 (Vertex Shader):

    • GPU 拿到三角形的三个顶点 A, B, C。
    • 运行 3 次顶点着色器,计算出这三个点在屏幕上的位置。
    • 同时,你可能计算了每个点的颜色:A=红, B=绿, C=蓝。
  2. 图元装配 (Primitive Assembly):

    • GPU 知道这三个点组成一个三角形。
  3. 光栅化 (Rasterization):

    • 这是关键步骤!
    • GPU 要找出屏幕上哪些像素落在这个三角形内部。
    • 比如,三角形可能覆盖了屏幕上的 1000 个像素。
  4. 片段处理 (Fragment Shader):

    • 对这 1000 个像素中的每一个,运行一次片段着色器。
    • 问题:这 1000 个像素并不在顶点 A、B 或 C 上,它们在三角形中间。它们的颜色应该是多少?
    • 答案线性插值 (Linear Interpolation)

2. 什么是插值?

2.1 直观图解

假设你画了一条线段,左端点是红色,右端点是蓝色。中间的颜色会怎么变?GPU 会自动帮你生成从红到紫再到蓝的渐变。

顶点 A (value=0.0)                         顶点 B (value=1.0)
● --------------------------------------- ●
^ ^ ^ ^
像素1 像素2 像素3 像素4
(val=0.2) (val=0.4) (val=0.6) (val=0.8)

在 3D 三角形中,这个过程是二维的:

       顶点 A (红色)
/ \
/ \
/ p \ <-- 对于三角形内部的任意像素 P
/ \
顶点 B ------- 顶点 C
(绿色) (蓝色)

对于像素 p,GPU 会计算它离 A、B、C 分别有多近,然后混合这三种颜色。 如果 p 正好在三角形中心,它的颜色就是 (红 + 绿 + 蓝) / 3

2.2 为什么 varying 变量会渐变?

varying 这个词本身的意思就是“变化的”。 当你把一个变量声明为 varying 并从 Vertex Shader 传给 Fragment Shader 时,你告诉 GPU: "请在光栅化过程中,帮我计算出每个像素对应的中间值。"

  • 顶点 A 的 vColor 是红色 (1, 0, 0)
  • 顶点 B 的 vColor 是绿色 (0, 1, 0)
  • 它们中间的像素,GPU 会自动计算出 (0.5, 0.5, 0),也就是黄色!

3. 硬核原理:重心坐标 (Barycentric Coordinates)

(进阶选读,不影响写代码,但有助于理解)

GPU 是如何精确算出插值比例的?答案是重心坐标

对于三角形内的任意点 P,都可以用三个权重值 $(w_a, w_b, w_c)$ 来表示: $$ P = w_a \cdot A + w_b \cdot B + w_c \cdot C $$

其中:

  1. $w_a + w_b + w_c = 1$
  2. 权重 $w$ 代表了点 P "偏向" 哪个顶点的程度。
    • 如果 P 靠近 A,$w_a$ 就接近 1。
    • 如果 P 在 BC 边上,$w_a$ 就是 0。

GPU 在光栅化时,会为每个像素计算这组重心坐标,然后用它来混合所有varying 变量(颜色、UV、法线等)。


4. UV 坐标的奥秘

纹理坐标 (UV) 是最常见的 varying 变量。

  • 左下角顶点 UV = (0, 0)
  • 右上角顶点 UV = (1, 1)

当这些值传递到片段着色器时,对于屏幕上的每个像素,vUv 变量就代表了该像素在图片上的相对位置。我们拿着这个位置去查找纹理颜色(texture2D),就实现了贴图。

// 顶点着色器
varying vec2 vUv;
attribute vec2 uv;
void main() {
vUv = uv; // 将顶点 UV 传递出去
// ...
}

// 片段着色器
varying vec2 vUv; // 接收插值后的 UV
uniform sampler2D map;
void main() {
// 根据插值后的坐标去取色
gl_FragColor = texture2D(map, vUv);
}

4. 总结

varying 变量就像是连接骨架(顶点)的蒙皮。你在骨架关节处定义了属性,蒙皮会自动平滑过渡连接这些属性。这就是为什么我们能看到平滑的光照和颜色,而不是一个个孤立的色块。


📝 编程作业

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

任务要求

通过实际编程深入理解 varying 变量的插值机制。

具体要求

  1. 三色三角形:创建一个三角形,三个顶点分别设置为红色、绿色、蓝色,观察中间的自动渐变
  2. 自定义插值:创建一个平面,在顶点着色器中为不同区域设置不同的 varying
  3. 高度插值:创建一个自定义的 varying float vElevation,在顶点着色器中计算高度,在片段着色器中使用

代码框架

// 顶点着色器
precision mediump float;
precision mediump int;

uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;

attribute vec3 position;
attribute vec2 uv;

// 任务 1 & 2:传递颜色
varying vec3 vColor;

// 任务 3:传递高度
varying float vElevation;

void main() {
// 任务 1:为不同顶点设置不同颜色
// 可以根据 position 或 uv 来设置
if (uv.x < 0.33) {
vColor = vec3(1.0, 0.0, 0.0); // 红色
} else if (uv.x < 0.66) {
vColor = vec3(0.0, 1.0, 0.0); // 绿色
} else {
vColor = vec3(0.0, 0.0, 1.0); // 蓝色
}

// 任务 3:计算高度
vec3 pos = position;
vElevation = sin(position.x * 5.0) * 0.1;
pos.y += vElevation;

gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}

// 片段着色器
precision mediump float;

varying vec3 vColor;
varying float vElevation;

void main() {
// 观察插值后的颜色
vec3 color = vColor;

// 使用高度值调整亮度
color += vElevation * 2.0;

gl_FragColor = vec4(color, 1.0);
}

检查清单

  • 可以看到三个顶点颜色的平滑过渡
  • 理解了为什么中间区域会自动渐变
  • 高度值正确传递并影响颜色
  • 尝试修改顶点的颜色值,观察插值变化

扩展挑战

  • 🌟 创建一个更复杂的几何体(如球体),观察法线的插值
  • 🌟 使用多个 varying 变量,观察它们如何独立插值
  • 🌟 尝试理解重心坐标的概念(高级)

参考资源