网站Logo Ran's blog

空间变换细节与插值透视矫正

ranranranqaq
6
2025-07-11

各个空间与对应的变换

  • 每个3D模型都有自己的局部空间(Local Space)。空间拥有自己的局部坐标系,定义了自己的原点位置。

  • 经过模型变换(Model Transformation)后,应用model矩阵变为世界空间(World Space)。具体的变换过程取决于我们想把模型放在世界的哪个位置

  • 经过观察变换(View Transformation)后,应用view矩阵变为视图空间(View Space)。主要为了实现视角变换(摄像机位置的改变)

  • 经过投影(Projection)后,应用projection矩阵变为裁剪空间(Clip Space)。这一步将视锥进行线性变换,裁剪视锥外的区域,存储压缩信息,便于后续变换到NDC

  • 经过齐次除法后,变为NDC(Normalized Device Coordinates)。这一步变换到一个范围为[-1,1]的正方体中

  • 经过视口变换(Viewport Transformation)后,变为屏幕空间(Screen Space)将NDC坐标映射到[0, 屏幕宽度] 和 [0, 屏幕高度]

World Space与View Space

这两个空间的区别仅在于摄像机位置不同,导致两个坐标系不同。在GAMES101的作业3中大量使用了View Space坐标来计算各种插值、Blinn-Phong模型等,这是因为作业3中固定了摄像机视角,View Space坐标更方便。

而在视角可能变化的情况中,上述计算更多会使用世界坐标。

在作业3的draw函数中:

//仅应用view和model矩阵,因此mm中是view space下的坐标
std::array<Eigen::Vector4f, 3> mm {
            (view * model * t->v[0]),
            (view * model * t->v[1]),
            (view * model * t->v[2])
        };

std::array<Eigen::Vector3f, 3> viewspace_pos;

//截断得到三维的view space顶点坐标
std::transform(mm.begin(), mm.end(), viewspace_pos.begin(), [](auto& v) {
  return v.template head<3>();
});

//传入光栅化
rasterize_triangle(newtri, viewspace_pos);

截取的这一段展示了draw()如何得到View Space坐标。如果仅应用model矩阵,就可以得到World Space坐标。

由于光照计算需要模拟三维空间的物理规律,插值计算也需要符合三维下的几何特征,这些计算都需要用这两个坐标中的一个进行。

Clip Space(透视投影矩阵)

先看这个视频https://www.bilibili.com/video/BV1LS4y1b7xZ

看完后就知道透视投影矩阵是怎么来的了,以及能够知道齐次除法如何通过矩阵实现,w为什么是view space中的-z。这个推导过程不难

【这里等待补充文字讲解】

经过透视投影矩阵变换后,空间就变为了clip space:

这样就会得到w=-z的clip space坐标,等待进行透视除法。这一步需要保存下来,因为我们还需要view space中的z值来帮助我们插值,转换到NDC之后就丢失这个信息了。

透视投影还有个非常重要的点:投影后,远近平面会反过来。透视除法后,近平面会被映射到NDC的-1,远平面被映射到NDC的1。也就是说,标准右手系中,投影前是z越小越远,投影后是z越小越近(这里的z是投影后的z,不是view space中的z)

这一点在上面的视频中有动画演示,因此深度测试需要找出z更小的像素渲染,来反转这个关系。

NDC和Screen Space

经过齐次除法后,变换结果被限制在 {[-1,1]}^{3} 的范围内,这个结果就是NDC。

再将NDC映射到屏幕的长宽上(简单的缩放,对每个本来在[-1,1]中的点乘一下缩放系数就行),即可得到Screen Space。

【重要】rasterize_triangle实现

这个函数用于光栅化单个三角形,它在draw()中被调用。尽管不同作业中的draw()实现方式不一样,但都是通过某种方式传入了一堆三角形,进行一些预处理(作业3分析)后逐个调用rasterize_triangle()进行光栅化。

它的实现大致步骤如下:

  1. 计算获取三角形的bouding box,遍历其中的每个像素,获取每个像素的中心坐标(+0.5),据此判断是否位于三角形内部

  2. 若不在三角形内部,则跳过该像素;若在内部,则通过后续步骤光栅化。

  3. 通过重心坐标,插值计算该像素的z-buffer近似值。