本文章是在Games101闫大神的《计算机图形学入门》课程的基础上编写的,讨论三角形光栅化时透视矫正插值的问题。
在三角形光栅化时,三角形内部的点的属性是通过三角形三个顶点的属性插值而来,比如某个像素的深度可通过三个顶点在经过视口变换后,将映射到屏幕上的深度重心坐标插值而来,但是实际上这种方法是不准确的,和实际情况有偏差。
一、透视
比如上面两张图,左图是相机完全平行于桌面得到的;右图是将手机抬起一定角度后拍摄的。
在两张照片中过Q点添加了一条水平红线,然后将BC分为了绿,蓝两部分。很明显可以看到通过透视家用投影后,Q点的水平位置不再处于BC中点。
因此,透视实际上是一种近大远小的视觉现象,而右图中DC边离我们近,AB边离我们远,所以造成了正方形的上半部分会被缩放得更加厉害!
也就是说,我们代码中的重心坐标插值的方法是在经过了透视家用投影变换,用屏幕空间下的坐标插值的,但是这种情况下得到的alpha、beta、gamma和view space下(也就是透视家用投影变换之前的空间)实际的alpha、beta、gamma有偏差,因此需要在屏幕空间下的alpha、beta、gamma做校正。
二、透视矫正插值(Perspective-Correct Interpolation)
(为了证明的简便性,我们利用深度值Z的线性插值进行说明,重心坐标插值可以类比得到) 该问题可以很简单在上图之中表现出来,简单叙述一下,在屏幕空间进行线性插值得到点c的intensity为0.5,然而对于在view space之中正确的插值结果,可以很明显看到C的intensity绝不为0.5。这也就造成了插值的误差,应该去矫正!
为了简便证明,将点的坐标用2维表示,第一维为图中所示的x轴,第二维为z轴。 简而言之,我们的目标就是得出t与s的关系式,这样就可以正确的利用屏幕空间的系数s插值到正确的view space的结果,推导过程省略,直接得出公式。
正确得出深度的插值结果之后,再看看任意属性(法线向量,纹理坐标,view space 坐标)插值结果
用I代表任意属性
类推得出重心坐标任意属性的正确插值如下:
三、Games101作业3的透视校正说明
作业3加载了一台小牛模型,并实现了各种类型的纹理映射。按照代码注释,是进行了透视校正插值的
auto [alpha, beta, gamma] = computeBarycentric2D(x, y, t.v);
// TODO: Inside your rasterization loop:
// * v.w() is the vertex view space depth value z.
// * Z is interpolated view space depth for the current pixel
// * zp is depth between zNear and zFar, used for z-buffer
// float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
// float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
// zp *= Z;
//透视校正插值
float Z = 1.0 / (alpha / v[0].w() + beta / v[1].w() + gamma / v[2].w());
float zp = alpha * v[0].z() / v[0].w() + beta * v[1].z() / v[1].w() + gamma * v[2].z() / v[2].w();
zp *= Z;如上代码所示,v[0].w()、v[1].w()、v[2].w() 这3个值里如果存的是view space下的Z值,那么就是真正的透视校正插值,而且通过透视家用投影矩阵的最后一行我们也能发现计算出来的v[].w()的值就会是view space(右手坐标系)下的Z值
但是代码框架中并没有这么做,而是将v[].w()的值直接置为1,也就是没有校正,直接用的屏幕空间下的插值
auto v = t.toVector4();
std::array<Vector4f, 3> Triangle::toVector4() const
{
std::array<Vector4f, 3> res;
std::transform(std::begin(v), std::end(v), res.begin(), [](auto& vec) { return Vector4f(vec.x(), vec.y(), vec.z(), 1.f); });
return res;
}论坛中有人讨论,这种模型下,是否作校正,得到的结果都不会有太大偏差,具体有多少偏差这里也无法说明。
但是如果不作校正,在重心坐标插值时可引入一台weight(权值)变量来人为弥补这种偏差(虽然在作业中weight仍默认为1)
static Eigen::Vector2f interpolate(float alpha, float beta, float gamma, const Eigen::Vector2f& vert1,
const Eigen::Vector2f& vert2, const Eigen::Vector2f& vert3, float weight)
{
auto u = (alpha * vert1[0] + beta * vert2[0] + gamma * vert3[0]);
auto v = (alpha * vert1[1] + beta * vert2[1] + gamma * vert3[1]);
u /= weight;
v /= weight;
return Eigen::Vector2f(u, v);
}
Reference
[1] GAMES101-现代计算机图形学入门-闫令琪
[2] 图形学 - 关于透视矫正插值那些事
[3] 计算机图形学六:透视矫正插值和图形渲染管线总结 |