透视校正插值 (Perspective-Correct Interpolation)
问题的提出
在使用光栅化的图形学方法中,法线,颜色,纹理坐标这些属性通常是绑定在图元的顶点上的。在3D空间中,这些属性值在图元上应该是线性变化的。但是当3D顶点被透视投影到2D屏幕之后,如果在2D投影面上对属性值进行线性插值,其对应的属性在3D空间中却不是线性变化的。如下图所示:
图中A,B点被投影到a,b;而c是a,b的中点,从视点连接c点形成一条直线和AB相交于C,很显然C点并不是AB的中点(除非AB平行与ab)。假设A点绑定了属性值
k
=
0
k=0
k=0, B点属性值
k
=
1
k=1
k=1,那么经过透视投影后,a和b的属性值自然分别是
k
=
0
,
k
=
1
k=0,k=1
k=0,k=1,如果c的属性值通过a,b的属性值线性插值得到,且c是ab的中点,那么c的属性值为
k
=
0.5
k=0.5
k=0.5。但由于C不是AB的中点,所以C的属性值k不等于0.5。因此如果在2d投影面上对顶点属性按照2d屏幕坐标进行直接的线性插值,得到的属性值是错误的。在本图的情况下,正确的结果是c的属性值应该等于C的属性值(本图中C更靠近A,因此C的属性值应该是小于0.5),这被称为是透视正确(perspective-correct)的,而直接的线性插值结果是透视不正确的。注意我特意强调“直接”的线性插值不正确,因为我们可以通过对属性值的某个函数进行线性插值,然后再将插值结果用另一个函数转换为我们最终想要的透视正确的属性值。为了找到这个函数,我们先研究一下如何对Z值进行插值。
ps: 图片画的有点问题,c点偏下了,导致看上去C更接近B,以后有空换张图
深度值插值
- z坐标和深度值
大多数图形系统会默认投影的时候,camera处于3d空间的原点,视线方向指向 + z +z +z或 − z -z −z轴,camera上方向为 + y +y +y,右方向为 + x +x +x,这定义了camera坐标系。投影面垂直于视线,和 x y xy xy平面平行,并在视点前方距离 d d d处,如果视线方向为 + z +z +z轴,则 z = d z=d z=d;如果视线方向为 − z -z −z轴,则是 z = − d z=-d z=−d。显然在此坐标系下,z坐标就表示了深度值。 - 为什么需要z坐标/深度值
因为需要使用深度测试实现隐藏面消除,因此投影之后需要知道图元在投影面上所有像素的z值。由于只有图形的顶点(如三角形的三个点)具有z坐标,因此需要使用插值的方式从顶点z坐标计算出图元上其他像素的z坐标值。 - 问题定义
如下图所示:我们使用一个视线指向 + z +z +z轴的camera坐标系,投影面为 z = d z=d z=d。在camera空间,被投影的图元:线 A B AB AB,具有顶点 A ( X 1 , Z 1 ) A(X_1,Z_1) A(X1,Z1)和 B ( X 2 , Z 2 ) B(X_2,Z_2) B(X2,Z2)。顶点 A , B A, B A,B被投影到 z = d z=d z=d的投影面上得到点 a , b a,b a,b。在 a , b a,b a,b中间有一点 c c c,是通过 a , b a,b a,b插值得到,插值系数为 s s s,即 c = a + s ∗ ( b − a ) c = a+ s*(b-a) c=a+s∗(b−a)。连接视点和 c c c的直线和 A B AB AB相交于 C C C点 ( X t , Z t ) (X_t,Z_t) (Xt,Zt),显然 C C C点投影到 z = d z=d z=d上得到 c c c点。现在已知 A , B A,B A,B点的Z坐标 Z 1 , Z 2 Z_1,Z_2 Z1,Z2,以及 c c c点的插值系数 s s s,需要找到一个表达式求出 C C C点的Z坐标 Z t Z_t Zt。
- 推导z坐标插值关系式
Z
t
=
f
(
Z
1
,
Z
2
,
s
)
Z_t = f(Z_1,Z_2,s)
Zt=f(Z1,Z2,s)
首先,定义直线 A B AB AB为 a x + b z = c ( c 不 等 于 0 ) ax+bz=c (c不等于0) ax+bz=c(c不等于0)
对于 A B AB AB上任意一点 ( X , Z ) (X,Z) (X,Z)投影到 z = d z=d z=d上的点为 ( u , d ) (u,d) (u,d)。根据相似三角形关系有:
X u = Z d , 即 X = Z u d \frac{X}{u} = \frac{Z}{d},即 X=\frac{Zu}{d} uX=dZ,即X=dZu
将 X t = Z t u d X_t = \frac{Z_tu}{d} Xt=dZtu代入AB的方程:
a ( Z t u d ) + b Z t = c a(Z_t\frac{u}{d}) + bZ_t = c a(Ztdu)+bZt=c
Z t ( a u d + b ) = c Z_t(a\frac{u}{d} + b) = c Zt(adu+b)=c
1 Z t = a u d c + b c (1) \frac{1}{Z_t} = \frac{au}{dc} + \frac{b}{c} \tag1 Zt1=dcau+cb(1)
因为 u = u 1 + s ( u 2 − u 1 ) = u 1 ( 1 − s ) + u 2 s u = u_1 + s (u_2-u_1) = u_1(1-s)+u_2s u=u1+s(u2−u1)=u1(1−s)+u2s,代入 ( 1 ) (1) (1)
1 Z t = a u 1 ( 1 − s ) d c + a u 2 s d c + b c = a u 1 d c ( 1 − s ) + a u 2 d c s + b c ( 1 − s ) + b c s \frac{1}{Z_t} = \frac{au_1(1-s)}{dc} + \frac{au_2s}{dc} + \frac{b}{c} = \frac{au_1}{dc}(1-s) + \frac{au_2}{dc}s + \frac{b}{c}(1-s) + \frac{b}{c}s Zt1=dcau1(1−s)+dcau2s+cb=dcau1(1−s)+dcau2s+cb(1−s)+cbs
1 Z t = ( a u 1 d c + b c ) ( 1 − s ) + ( a u 2 d c + b c ) s (2) \frac{1}{Z_t} = (\frac{au_1}{dc} + \frac{b}{c})(1-s) + (\frac{au_2}{dc} + \frac{b}{c})s \tag2 Zt1=(dcau1+cb)(1−s)+(dcau2+cb)s(2)
根据 ( 1 ) (1) (1) 有
1 Z 1 = a u 1 d c + b c \frac{1}{Z_1} = \frac{au_1}{dc} + \frac{b}{c} Z11=dcau1+cb
1 Z 2 = a u 2 d c + b c \frac{1}{Z_2} = \frac{au_2}{dc} + \frac{b}{c} Z21=dcau2+cb
代入 ( 2 ) (2) (2)得
1 Z t = 1 Z 1 ( 1 − s ) + 1 Z 2 s (3) \frac{1}{Z_t} = \frac{1}{Z_1}(1-s) + \frac{1}{Z_2}s \tag3 Zt1=Z11(1−s)+Z21s(3)
从 ( 3 ) (3) (3)可知,在插值计算Z值前先计算顶点Z坐标的倒数,对倒数进行插值,然后将结果再次取倒数就可以在屏幕空间得到正确的视图空间Z值。而在实际的图形系统中,往往不需要再次取倒数得到视图空间的Z值。例如OpenGL中,会把投影后的Z值构建为 Z ′ = A Z + B Z' = \frac{A}{Z} + B Z′=ZA+B 的形式,且Z’的范围被归一化到 [ − 1 , 1 ] [-1,1] [−1,1]之间,然后再经过depth range映射为 [ 0 , 1 ] [0,1] [0,1]的范围存储到深度缓冲中,0为near plane的Z值,1为far plane的Z值,值越大离视点越远。这样处理后的Z值是和视图空间Z值倒数 1 Z \frac{1}{Z} Z1成线性关系,可以直接在光栅化时进行插值。
顶点属性的插值
正如本文一开始说的,直接对顶点属性进行线性插值得到的结果是透视不正确的。为了透视正确,顶点属性需要和z坐标成正比。上图中,A点具有属性
I
1
I_1
I1, B点具有属性
I
2
I_2
I2,我们计算C点的属性
I
t
I_t
It:
I
t
−
I
1
I
2
−
I
1
=
Z
t
−
Z
1
Z
2
−
Z
1
\frac{I_t - I_1}{I_2 - I_1} = \frac{Z_t - Z_1}{Z_2 - Z_1}
I2−I1It−I1=Z2−Z1Zt−Z1
而根据
(
3
)
(3)
(3):
Z
t
=
1
1
Z
1
(
1
−
s
)
+
1
Z
2
s
Z_t = \frac{1}{\frac{1}{Z_1}(1-s) + \frac{1}{Z_2}s}
Zt=Z11(1−s)+Z21s1
代入上式,解出
I
t
I_t
It:
I
t
−
I
1
I
2
−
I
1
=
1
1
Z
1
(
1
−
s
)
+
1
Z
2
s
−
Z
1
Z
2
−
Z
1
=
1
1
+
(
1
−
s
)
Z
2
s
Z
1
=
Z
1
s
Z
1
s
+
Z
2
(
1
−
s
)
\frac{I_t - I_1}{I_2 - I_1} = \frac{\frac{1}{\frac{1}{Z_1}(1-s) + \frac{1}{Z_2}s} - Z_1}{Z_2 - Z_1} = \frac{1}{1 + \frac{(1-s)Z_2}{sZ_1}} = \frac{Z_1s}{Z_1s+Z_2(1-s) }
I2−I1It−I1=Z2−Z1Z11(1−s)+Z21s1−Z1=1+sZ1(1−s)Z21=Z1s+Z2(1−s)Z1s
I
t
=
(
I
1
∗
Z
2
∗
(
1
−
s
)
+
I
2
∗
Z
1
∗
s
)
(
Z
1
∗
s
+
Z
2
∗
(
1
−
s
)
)
I_t = \frac{( I1*Z2*(1-s) + I2*Z1*s )}{( Z1*s + Z2*(1-s) )}
It=(Z1∗s+Z2∗(1−s))(I1∗Z2∗(1−s)+I2∗Z1∗s)
上下同除以
Z
1
Z
2
Z_1Z_2
Z1Z2得
I
t
=
(
1
−
s
)
I
1
Z
1
+
s
I
2
Z
2
(
1
−
s
)
1
Z
1
+
s
1
Z
2
It = \frac{ (1-s)\frac{I_1}{Z_1} + s\frac{I_2}{Z_2} } { (1-s)\frac{1}{Z_1} + s\frac{1}{Z_2} }
It=(1−s)Z11+sZ21(1−s)Z1I1+sZ2I2
从上式可知,在投影面上对属性插值时,先对
I
Z
\frac{I}{Z}
ZI进行插值,然后将结果除以
1
Z
\frac{1}{Z}
Z1插值的结果。这样就得到了属性的透视校正插值。