07. 从顶点到片段:插值的魔法
理解“光栅化”和“插值”是理解为什么 varying 变量会渐变的关键。
1. 渲染管线简化版
顶点处理 (Vertex Shader):
- GPU 拿到三角形的三个顶点 A, B, C。
- 运行 3 次顶点着色器,计算出这三个点在屏幕上的位置。
- 同时,你可能计算了每个点的颜色:A=红, B=绿, C=蓝。
图元装配 (Primitive Assembly):
- GPU 知道这三个点组成一个三角形。
光栅化 (Rasterization):
- 这是关键步骤!
- GPU 要找出屏幕上哪些像素落在这个三角形内部。
- 比如,三角形可能覆盖了屏幕上的 1000 个像素。
片段处理 (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 $$
其中:
- $w_a + w_b + w_c = 1$
- 权重 $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 变量的插值机制。
具体要求
- 三色三角形:创建一个三角形,三个顶点分别设置为红色、绿色、蓝色,观察中间的自动渐变
- 自定义插值:创建一个平面,在顶点着色器中为不同区域设置不同的
varying值 - 高度插值:创建一个自定义的
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变量,观察它们如何独立插值 - 🌟 尝试理解重心坐标的概念(高级)
参考资源
- 完整作业清单:编程作业清单.md