计算机图形学六:正确使用重心坐标插值(透视矫正插值(Perspective-Correct Interpolation))和图形渲染管线总结

(本篇文章同步发表于知乎专栏: https://zhuanlan.zhihu.com/p/144331875 欢迎三连关注)

正如上一文章中所提到的,我们的重心坐标往往都是在屏幕空间下所得到的,如果直接使用屏幕空间下的重心坐标进行插值会造成一定的误差,与在view space下是不一样的,那么本节内容就会具体介绍如何矫正这种误差,利用屏幕空间下的重心坐标达到正确的插值。在此之后也会对到目前为止的所有内容进行总结,即图形渲染管线。

1 透视矫正插值(Perspective-Correct Interpolation)


(为了证明的简便性,我们利用深度值Z的线性插值进行说明,重心坐标插值可以类比得到)
该问题可以很简单在上图之中表现出来,简单叙述一下,在屏幕空间进行线性插值得到点c的intensity为0.5,然而对于在view space之中正确的插值结果,可以很明显看到C的intensity绝不为0.5。这也就造成了插值的误差,应该去矫正!

首先先分别定义屏幕空间的比例为s,view space中为t,其余符号含义如下图所示:

为了简便证明,将点的坐标用2维表示,第一维为图中所示的x轴,第二维为z轴。
简而言之,我们的目标就是得出t与s的关系式,这样就可以正确的利用屏幕空间的系数s插值到正确的view space的结果,推导过程如下。

由上图所示的投影所造成的三角形相似性可以轻易得出如下几个式子:

X 1 Z 1 = u 1 d ⇒ X 1 = u 1 Z 1 d (1) \frac{X_{1}}{Z_{1}}=\frac{u_{1}}{d} \Rightarrow X_{1}=\frac{u_{1} Z_{1}}{d} \tag{1} Z1X1=du1X1=du1Z1(1)
X 2 Z 2 = u 2 d ⇒ X 2 = u 2 Z 2 d (2) \frac{X_{2}}{Z_{2}}=\frac{u_{2}}{d} \Rightarrow X_{2}=\frac{u_{2} Z_{2}}{d} \tag{2} Z2X2=du2X2=du2Z2(2)
X t Z t = u s d ⇒ Z t = d X t u s (3) \frac{X_{t}}{Z_{t}}=\frac{u_{s}}{d} \Rightarrow Z_{t}=\frac{d X_{t}}{u_{s}} \tag{3} ZtXt=dusZt=usdXt(3)

同样,分别利用screen space 以及 view space的线性插值可以得到以下几个式子:

u s = u 1 + s ( u 2 − u 1 ) (4) u_{s}=u_{1}+s\left(u_{2}-u_{1}\right)\tag{4} us=u1+s(u2u1)(4)
X t = X 1 + t ( X 2 − X 1 ) (5) X_{t}=X_{1}+t\left(X_{2}-X_{1}\right) \tag{5} Xt=X1+t(X2X1)(5)
Z t = Z 1 + t ( Z 2 − Z 1 ) (6) Z_{t}=Z_{1}+t\left(Z_{2}-Z_{1}\right) \tag{6} Zt=Z1+t(Z2Z1)(6)
将4式与5式代入3式得:
Z t = d ( X 1 + t ( X 2 − X 1 ) ) u 1 + s ( u 2 − u 1 ) (7) Z_{t}=\frac{d\left(X_{1}+t\left(X_{2}-X_{1}\right)\right)}{u_{1}+s\left(u_{2}-u_{1}\right)}\tag{7} Zt=u1+s(u2u1)d(X1+t(X2X1))(7)
再将1式与2式代入7式得:
Z t = d ( u 1 Z 1 d + t ( u 2 Z 2 d − u 1 Z 1 d ) ) u 1 + s ( u 2 − u 1 ) = u 1 Z 1 + t ( u 2 Z 2 − u 1 Z 1 ) u 1 + s ( u 2 − u 1 ) (8) \begin{aligned} Z_{t} &=\frac{d\left(\frac{u_{1} Z_{1}}{d}+t\left(\frac{u_{2} Z_{2}}{d}-\frac{u_{1} Z_{1}}{d}\right)\right)}{u_{1}+s\left(u_{2}-u_{1}\right)} \\ &=\frac{u_{1} Z_{1}+t\left(u_{2} Z_{2}-u_{1} Z_{1}\right)}{u_{1}+s\left(u_{2}-u_{1}\right)} \end{aligned} \tag{8} Zt=u1+s(u2u1)d(du1Z1+t(du2Z2du1Z1))=u1+s(u2u1)u1Z1+t(u2Z2u1Z1)(8)
最后将6式代入8式的左边得到:
Z 1 + t ( Z 2 − Z 1 ) = u 1 Z 1 + t ( u 2 Z 2 − u 1 Z 1 ) u 1 + s ( u 2 − u 1 ) (9) Z_{1}+t\left(Z_{2}-Z_{1}\right)=\frac{u_{1} Z_{1}+t\left(u_{2} Z_{2}-u_{1} Z_{1}\right)}{u_{1}+s\left(u_{2}-u_{1}\right)} \tag{9} Z1+t(Z2Z1)=u1+s(u2u1)u1Z1+t(u2Z2u1Z1)(9)
仔细观察9式,我们已经成功得出了t与s的一个关系式,其中的其它参数都是已知,因此进一步化简不在这里展开,可以得到如下t与s关系:
t = s Z 1 s Z 1 + ( 1 − s ) Z 2 (10) t=\frac{s Z_{1}}{s Z_{1}+(1-s) Z_{2}} \tag{10} t=sZ1+(1s)Z2sZ1(10)
如此就可以利用屏幕空间下的系数得到正确插值结果了,计算如下:
Z t = Z 1 + s Z 1 s Z 1 + ( 1 − s ) Z 2 ( Z 2 − Z 1 ) = 1 s Z 2 + ( 1 − s ) Z 1 (11) \begin{aligned} Z_{t}&=Z_{1}+\frac{s Z_{1}}{s Z_{1}+(1-s) Z_{2}}\left(Z_{2}-Z_{1}\right) \\ &=\frac{1}{\frac{s}{Z_2}+\frac{(1-s)}{Z_1}} \end{aligned} \tag{11} Zt=Z1+sZ1+(1s)Z2sZ1(Z2Z1)=Z2s+Z1(1s)1(11)
以上证明虽然只是针对线性插值的矫正结果,对于重心坐标插值,我们可以类似类推得出:

Z t = 1 α Z A + β Z B + γ Z C (12) Z_t=\frac{1}{\frac{\alpha}{Z_A}+\frac{\beta}{Z_B}+\frac{\gamma}{Z_C}} \tag{12} Zt=ZAα+ZBβ+ZCγ1(12)

正确得出深度的插值结果之后,再看看任意属性(法线向量,纹理坐标,view space 坐标)插值结果,依然以线性插值为例先进行推导:用 I I I代表任意属性
I t = I 1 + t ( I 2 − I 1 ) (13) I_{t}=I_{1}+t\left(I_{2}-I_{1}\right)\tag{13} It=I1+t(I2I1)(13)
I t = I 1 + s Z 1 s Z 1 + ( 1 − s ) Z 2 ( I 2 − I 1 ) (14) I_{t}=I_{1}+\frac{s Z_{1}}{s Z_{1}+(1-s) Z_{2}}\left(I_{2}-I_{1}\right)\tag{14} It=I1+sZ1+(1s)Z2sZ1(I2I1)(14)
I t = ( I 1 Z 1 + s ( I 2 Z 2 − I 1 Z 1 ) ) / 1 Z t (15) I_{t}=\left(\frac{I_{1}}{Z_{1}}+s\left(\frac{I_{2}}{Z_{2}}-\frac{I_{1}}{Z_{1}}\right)\right) / \frac{1}{Z_{t}}\tag{15} It=(Z1I1+s(Z2I2Z1I1))/Zt1(15)
(建议读者自行推导一遍上述所有过程,加深记忆)
不难看出插值的分母就是深度值的倒数,类推得出重心坐标任意属性的正确插值如下:
I t = ( α I A Z A + β I B Z B + γ I C Z C ) / 1 Z t (16) I_{t}=\left(\alpha\frac{I_{A}}{Z_{A}}+\beta\frac{I_{B}}{Z_{B}}+\gamma\frac{I_{C}}{Z_{C}}\right) / \frac{1}{Z_{t}}\tag{16} It=(αZAIA+βZBIB+γZCIC)/Zt1(16)
(tips:任意属性自然包括深度值,将深度值z代入16式可以得出与12式一样的结果)
至此,我们就可以利用16式进行所有的属性的重心坐标插值,并且保证结果正确了!

2 图形渲染管线总结

所谓图形渲染管线指的是一系列操作的流程,这个流程具体来说就是将一堆具有三维几何信息的数据点最终转换到二维屏幕空间的像素。其实也就是本系列笔记之前的所有知识连贯起来。我们以如下图作为一个总结,再具体分步骤讲解:

(整个图形管线的步骤可能有不同的分法,不一定就是上图所述的5部分,但整体流程一定是一样的)
首先来看第一个,顶点处理:

顶点处理的作用是指对所有的顶点数据进行Model,View,和Projection的变换,最终得到投影到二维平面的坐标信息(同时为了Zbuffer保留深度z值)。当然如果超出观察空间的会被剪裁掉(关于剪裁的知识因为闫老师的课并没有多提,所以本系列笔记也暂未设及到,但是会在之后进行该部分知识的补充)。
而第二步三角形处理也十分容易理解,就是将所有的顶点按照原几何信息,变成三角面,每个面由3个顶点组成。得到了许许多多个三角形之后,接下来的操作自然就是三角形光栅化了,这也是前几节笔记之中的重点内容之一:

在进行完三角形的光栅化之后,知道了哪些在三角形内的点可以被显示,那么如何确定每个像素点或者说片元(Fragement)的颜色呢?[注:片元可能比像素更小,如MSAA抗拒齿操作的进一步细分得到的采样点]。那自然就是着色了,也就是片元处理阶段应该做的。

注意这阶段顶点处理也亮起来是因为我们需要顶点信息对三角形内的点进行属性插值(tips:当然也可以直接在顶点处理阶段就算出每个顶点的颜色值,如Gouraud Shading一样)。当然这一阶段也少不了Z-Buffer来帮助确定,哪些像素点应该显示在屏幕上,哪些点被遮挡了不应该显示(这一步也有不少书籍归结在最后一步中,个人感觉差求不多,重要在于理解整个过程吧):

最后一步!Framebuffer的处理,就是将所有的像素颜色信息整合在一起,输送给显示设备加以显示。这也就完成了整个图形渲染管线了。

(tips:其实在片元处理阶段有一点还未叙述,我们也可以去做texture mapping,利用texture的信息来代替blinn-phong模型漫反射系数来当作颜色,这一部分的内容会在下一节笔记中叙述!)

在这里简单的提一下shader program,shader program分两种,分别是对顶点以及对片元的处理,程序员可以自行编程来代替原来固定的顶点处理和片元处理从而达到各种各样令人惊叹的效果!这也就是现在的可编程渲染管线。

总结

其实对于透视校正插值这一块确实是一个小坑,不少人应该在刚学习的时候都不会注意到,包括我也是,最后也是看知乎上一些大佬的回答才找了一些资料弄明白了这一块。对于图形渲染管线来说,其实就是对光栅化的一个总结,把整个流程拎出来能够有一个更加清晰的认识。

最后如果本文对你有帮助求点赞求收藏求一个大大的关注 😃 ,后序会持续更新,感谢阅读!

Reference

[1] Fundamentals of Computer Graphics 4th
[2] GAMES101-现代计算机图形学入门-闫令琪
[3] 透视矫正插值参考资料https://www.comp.nus.edu.sg/~lowkl/publications/lowk_persp_interp_techrep.pdf

  • 252
    点赞
  • 609
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
好的,以下是使用C++实现输入坐标点集x/y,使用三次多项式进行轨迹插值,输出轨迹点坐标的代码: ```c++ #include <iostream> #include <vector> #include <cmath> using namespace std; // 计算三次多项式的系数 void cubic_interpolation(vector<double>& x, vector<double>& y, vector<double>& a) { // 计算一阶差商 vector<double> delta1(y.size() - 1); for (int i = 0; i < delta1.size(); i++) { delta1[i] = (y[i+1] - y[i]) / (x[i+1] - x[i]); } // 计算二阶差商 vector<double> delta2(delta1.size() - 1); for (int i = 0; i < delta2.size(); i++) { delta2[i] = (delta1[i+1] - delta1[i]) / (x[i+2] - x[i]); } // 计算三阶差商 vector<double> delta3(delta2.size() - 1); for (int i = 0; i < delta3.size(); i++) { delta3[i] = (delta2[i+1] - delta2[i]) / (x[i+3] - x[i]); } // 计算系数a for (int i = 0; i < a.size(); i++) { if (i == 0 || i == a.size() - 1) { a[i] = 0; } else { double d1 = x[i] - x[i-1]; double d2 = x[i+1] - x[i]; a[i] = 3 * delta1[i-1] - 3 * delta1[i] + d1 * delta2[i-1] + d2 * delta2[i]; a[i] /= pow(d1, 2) * (d1 + d2); a[i] += (delta3[i-1] - delta3[i]) / (6 * (d1 + d2)); } } } // 根据系数a计算拟合函数的值 double cubic_function(double x, vector<double>& a, vector<double>& x0) { int i = 0; while (i < x0.size() && x > x0[i]) { i++; } i--; double d = x - x0[i]; double result = a[i] * pow(d, 3) + a[i+1] * pow(d, 2); result += a[i+2] * d + a[i+3]; return result; } int main() { // 输入坐标点集x/y vector<double> x = {0, 1, 2, 3, 4, 5}; vector<double> y = {1, 2, 3, 5, 8, 13}; // 计算系数a vector<double> a(y.size()); cubic_interpolation(x, y, a); // 输出轨迹点坐标 vector<double> x0; double step = 0.1; for (double i = x[0]; i <= x[x.size()-1]; i += step) { x0.push_back(i); } for (int i = 0; i < x0.size(); i++) { double y0 = cubic_function(x0[i], a, x); cout << "(" << x0[i] << ", " << y0 << ")" << endl; } return 0; } ``` 运行以上代码,即可得到轨迹点的坐标。注意,以上代码仅为示例,实际使用时需要根据具体的需要进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值