图形学基础之透视校正插值

1,536 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

透视校正插值 (Perspective-Correct Interpolation)

问题的提出

在使用光栅化的图形学方法中,法线,颜色,纹理坐标这些属性通常是绑定在图元的顶点上的。在3D空间中,这些属性值在图元上应该是线性变化的。但是当3D顶点被透视投影到2D屏幕之后,如果在2D投影面上对属性值进行线性插值,其对应的属性在3D空间中却不是线性变化的。如下图所示: 透视投影校正 图中A,B点被投影到a,b;而c是a,b的中点,从视点连接c点形成一条直线和AB相交于C,很显然C点并不是AB的中点(除非AB平行与ab)。假设A点绑定了属性值k=0k=0, B点属性值k=1k=1,那么经过透视投影后,a和b的属性值自然分别是k=0k=1k=0,k=1,如果c的属性值通过a,b的属性值线性插值得到,且c是ab的中点,那么c的属性值为k=0.5k=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+zz-z轴,camera上方向为+y+y,右方向为+x+x,这定义了camera坐标系。投影面垂直于视线,和xyxy平面平行,并在视点前方距离dd处,如果视线方向为+z+z轴,则z=dz=d;如果视线方向为z-z轴,则是z=dz=-d。显然在此坐标系下,z坐标就表示了深度值。
  • 为什么需要z坐标/深度值 因为需要使用深度测试实现隐藏面消除,因此投影之后需要知道图元在投影面上所有像素的z值。由于只有图形的顶点(如三角形的三个点)具有z坐标,因此需要使用插值的方式从顶点z坐标计算出图元上其他像素的z坐标值。
  • 问题定义 如下图所示:我们使用一个视线指向+z+z轴的camera坐标系,投影面为z=dz=d。在camera空间,被投影的图元:线ABAB,具有顶点A(X1,Z1)A(X_1,Z_1)B(X2,Z2)B(X_2,Z_2)。顶点A,BA, B被投影到z=dz=d的投影面上得到点a,ba,b。在a,ba,b中间有一点cc,是通过a,ba,b插值得到,插值系数为ss,即 c=a+s(ba)c = a+ s*(b-a)。连接视点和cc的直线和ABAB相交于CC(Xt,Zt)(X_t,Z_t),显然CC点投影到z=dz=d上得到cc点。现在已知A,BA,B点的Z坐标Z1,Z2Z_1,Z_2,以及cc点的插值系数ss,需要找到一个表达式求出CC点的Z坐标ZtZ_t

透视校正插值

  • 推导z坐标插值关系式 Zt=f(Z1,Z2,s)Z_t = f(Z_1,Z_2,s) 首先,定义直线ABABax+bz=c(c不等于0ax+bz=c (c不等于0) 对于ABAB上任意一点(X,Z)(X,Z)投影到z=dz=d上的点为(u,d)(u,d)。根据相似三角形关系有:
Xu=Zd,即X=Zud\frac{X}{u} = \frac{Z}{d},即 X=\frac{Zu}{d}

Xt=ZtudX_t = \frac{Z_tu}{d}代入AB的方程:

a(Ztud)+bZt=ca(Z_t\frac{u}{d}) + bZ_t = c
Zt(aud+b)=cZ_t(a\frac{u}{d} + b) = c
1Zt=audc+bc(1)\frac{1}{Z_t} = \frac{au}{dc} + \frac{b}{c} \tag1

因为 u=u1+s(u2u1)=u1(1s)+u2su = u_1 + s (u_2-u_1) = u_1(1-s)+u_2s,代入(1)(1)

1Zt=au1(1s)dc+au2sdc+bc=au1dc(1s)+au2dcs+bc(1s)+bcs\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
1Zt=(au1dc+bc)(1s)+(au2dc+bc)s(2)\frac{1}{Z_t} = (\frac{au_1}{dc} + \frac{b}{c})(1-s) + (\frac{au_2}{dc} + \frac{b}{c})s \tag2

根据(1)(1)

1Z1=au1dc+bc\frac{1}{Z_1} = \frac{au_1}{dc} + \frac{b}{c}
1Z2=au2dc+bc\frac{1}{Z_2} = \frac{au_2}{dc} + \frac{b}{c}

代入(2)(2)

1Zt=1Z1(1s)+1Z2s(3)\frac{1}{Z_t} = \frac{1}{Z_1}(1-s) + \frac{1}{Z_2}s \tag3

(3)(3)可知,在插值计算Z值前先计算顶点Z坐标的倒数,对倒数进行插值,然后将结果再次取倒数就可以在屏幕空间得到正确的视图空间Z值。而在实际的图形系统中,往往不需要再次取倒数得到视图空间的Z值。例如OpenGL中,会把投影后的Z值构建为Z=AZ+BZ' = \frac{A}{Z} + B 的形式,且Z'的范围被归一化到[1,1][-1,1]之间,然后再经过depth range映射为[0,1][0,1]的范围存储到深度缓冲中,0为near plane的Z值,1为far plane的Z值,值越大离视点越远。这样处理后的Z值是和视图空间Z值倒数1Z\frac{1}{Z}成线性关系,可以直接在光栅化时进行插值。

顶点属性的插值

正如本文一开始说的,直接对顶点属性进行线性插值得到的结果是透视不正确的。为了透视正确,顶点属性需要和z坐标成正比。上图中,A点具有属性I1I_1, B点具有属性I2I_2,我们计算C点的属性ItI_t:

ItI1I2I1=ZtZ1Z2Z1\frac{I_t - I_1}{I_2 - I_1} = \frac{Z_t - Z_1}{Z_2 - Z_1}

而根据(3)(3):

Zt=11Z1(1s)+1Z2sZ_t = \frac{1}{\frac{1}{Z_1}(1-s) + \frac{1}{Z_2}s}

代入上式,解出ItI_t

ItI1I2I1=11Z1(1s)+1Z2sZ1Z2Z1=11+(1s)Z2sZ1=Z1sZ1s+Z2(1s)\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) }
It=(I1Z2(1s)+I2Z1s)(Z1s+Z2(1s))I_t = \frac{( I1*Z2*(1-s) + I2*Z1*s )}{( Z1*s + Z2*(1-s) )}

上下同除以Z1Z2Z_1Z_2

It=(1s)I1Z1+sI2Z2(1s)1Z1+s1Z2It = \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} }

从上式可知,在投影面上对属性插值时,先对IZ\frac{I}{Z}进行插值,然后将结果除以1Z\frac{1}{Z}插值的结果。这样就得到了属性的透视校正插值。