透视校正插值
需要预先学习:
1. 屏幕空间插值的问题
纹理映射 介绍过属性插值:光栅化阶段用重心坐标(barycentric coordinates)把顶点属性(颜色、法线、UV)推算到三角形内部每个片元。纹理映射 的注里留了一个悬念:屏幕空间的重心坐标不能直接用于属性插值,透视投影下会产生扭曲。
先看症状。一块斜着放的地板(或墙),用均匀的棋盘格纹理铺满:
正确(透视校正) 错误(屏幕空间直接插值)
┌────┬──┬──┬────┐ ┌──┬──┬──┬──┬──┐
│ │ │ │ │ │ │ │ │ │ │ ← 远处:格子应越来越密
│ │ │ │ │ │ │ │ │ │ │ 却被均匀压缩
├────┼──┼──┼────┤ ├──┼──┼──┼──┼──┤
│ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
└────┴──┴──┴────┘ └──┴──┴──┴──┴──┘
近处格子大 格子大小沿深度没有
远处格子小(近大远小) 正确的透视递减
屏幕空间直接插值时,远处与近处的格子被"均匀拉平",地板看起来像被向前压扁,透视感消失。颜色与法线的插值同理出错,只是没有纹理这么直观。
问题出在哪:透视除法(除以 )对 、 是非线性的,而线性插值与非线性变换不可交换。这一篇把这个坑的机理讲清楚,并给出修正公式。
2. 推导
2.1 记号约定
设三角形三个顶点 、、(它们已位于视图空间,见 坐标空间总览)。对某个属性 (UV、颜色、法线分量……),顶点处取值 、、。
屏幕上一点 在三角形内,屏幕空间重心坐标为 ,满足 (面积比定义见 纹理映射)。
错误做法(屏幕空间直接插值):
这个式子在世界空间里插值是对的,但 、、 是在屏幕空间里量出来的——两个空间的重心坐标不相等。
2.2 关键事实:屏幕坐标是视图坐标除以深度
透视投影下(见 坐标空间总览),裁剪空间 ,透视除法后屏幕坐标为:
( 取正深度,符号约定不影响推导。)也就是说:屏幕坐标是视图坐标的线性函数除以深度。
同样地,屏幕空间的重心坐标 是由屏幕坐标定义的线性关系:
把每个屏幕坐标按 展开:
对 、(NDC 的 同样是 的分式线性函数)各有一个同样的方程。联立可解出一个关键结论——屏幕空间重心坐标换算到视图空间,需要按 重新加权:
即片元深度的倒数是顶点深度倒数的线性插值。
2.3 属性的正确插值
视图空间里,属性随位置线性变化(线性插值在本空间内正确):
其中 是视图空间的重心坐标。它与屏幕空间重心坐标的关系(由上面的换算推出):
代入整理,得到透视校正插值(perspective-correct interpolation)公式:
用裁剪空间的 (,仅差符号)写就是教科书常见形式:
结论:可插值的不是属性本身,而是"属性除以 "——分子分母各做一次屏幕空间线性插值,再相除。因为除以 抵消了透视除法的非线性,两次线性插值都在"除以 后的世界"里成立。
注:这类"先除、插值、再除回"形式的量称为双曲线插值(hyperbolic interpolation),因为插值结果对屏幕坐标是分式(双曲线)函数,而不是线性函数——远处属性变化被自然压密,正是透视该有的样子。
3. 数值例子
用两个顶点的线段(退化成"一维三角形")验证。设线段两端:
- :视图空间 ,属性 ,即屏幕
- :视图空间 ,属性 ,即屏幕
取屏幕中点 ,对应线性权重 。
错误做法:
透视校正:
验证 是否正确:屏幕 的点,视图空间应满足 且落在线段上(,,):。属性真实值正是 ——校正公式正确,屏幕中点对应的属性偏向了近处顶点(近处顶点在屏幕上"占据"了更多区间,属性变化也应更慢)。
4. 硬件如何实现
透视校正插值不需要你在着色器里手写——光栅化硬件对 in/out 变量的插值默认就是透视校正的。硬件的实现策略:
- 顶点着色器输出裁剪空间坐标与属性后,硬件对每个可插值属性预乘 (同时单独插值 本身);
- 光栅化时对 与 分别做普通的屏幕空间线性插值;
- 片元着色器读取前,用 还原出 。
除法与乘法分摊到每个片元,代价由硬件摊薄。正交投影下 恒为 1,公式退化为普通线性插值——两种投影共用同一套硬件逻辑。
4.1 noperspective:故意关掉校正
GLSL 提供插值限定符关闭这个行为(见 glsl语法 的插值限定符):
// 顶点着色器
noperspective out vec2 screenUV;
// 片元着色器
noperspective in vec2 screenUV; // 按屏幕空间线性插值,不做透视校正
适用场景:属性本来就定义在屏幕空间上——全屏后处理的坐标、某些 UI 参数。它们不需要(也不应该)被透视校正。
注意:
flat(不插值,取 provoking vertex 的值)与noperspective(线性但不校正)是两回事:前者用于实例 ID、整数属性这类"图元内恒定"的数据,后者用于"屏幕空间意义上线性"的数据。
5. 与深度的关系
有一个容易混淆的相邻问题:片元的深度(NDC 的 )本身也是插值出来的,但它不是线性插值。
由本篇第二个关键事实, 在屏幕空间线性插值,等价地:NDC 的 在屏幕空间按双曲线插值(先插 再取倒数)。这正是 视图(摄像机)与投影 注与 深度缓冲 深度精度一节的来源:
- 视图空间的 被 的形式非线性压缩:近处一小段深度占满 的大区间,远处一大段深度挤在尾部;
- 两个工程对策也由此而来:控制 near/far 比值(压缩程度由它决定)、reverse-z(反转映射方向,利用浮点数在 0 附近精度高的特性,见 深度缓冲)。
而硬件存进深度缓冲、参与深度测试的正是这个非线性 ——深度测试比较的是"非线性深度",结果只受相对远近影响,不受非线性影响;非线性只在"精度分布"上体现为远处更容易 z-fighting。
| 量 | 屏幕空间插值方式 | 说明 |
|---|---|---|
| 颜色、UV、法线等顶点属性 | 透视校正(除以 插值再还原) | 硬件默认行为 |
| (深度倒数) | 线性插值 | 硬件内部使用 |
| NDC 深度 | 双曲线(非均匀) | 由 线性间接得到,精度近密远疏 |
noperspective 标记的属性 |
线性插值 | 显式关闭校正,用于屏幕空间属性 |
一句话总结:透视除法是非线性的,一切"跨越透视除法还想保持线性"的插值都必须按 加权——属性插值如此(本篇),深度精度分布如此(深度缓冲),ShadowMap与PCF、PCSS 在光源空间比较深度时同样如此。


