2017-01-01 计算机图形学 光线追踪 rust

从 2017 年初开始研究光线追踪进行图像合成,准备用 Rust 实现一个光线追踪渲染器。 应该会很好玩,但也是一个挺大的坑,就当作是 2017 年的新玩具吧。

光线追踪 (Ray tracing (Wikipedia)) 是一种渲染方法, 能够渲染出相对真实的图像,但同时也需要很大的运算量。

Rust 是一门较新的语言。 目标是作为一种快速的,能够保证内存安全,线程安全的系统级编程语言,拥有很多函数式语言的特性,没有垃圾回收。

在看图形学的资料时,常常会感受到缺少一些数学、物理学、统计学的知识,所以在前期很多时间都是用来补这些知识,也算一个学以致用的过程。

记录

有一个比较困扰的问题是,在 debug 的时候,虽然能从 log 里或得到一些信息, 比如点的位置、光线的位置方向,但这些都是以一串数字给出的,并不算直观。 想到了一个方法,把 log 里的数据导入到 three.js, 这样就可以在浏览器里借用成熟的平台来查看我的场景并 debug 了。


被一个 bug 卡了有两三天。是这样的,用了上面的方法之后,非常直观地发现,生成的光线方向很诡异。 于是顺着代码一行一行走,找到了好几个问题。

1. 在构造 PerspectiveCamera 的代码中,因为手滑写错了几个字母 ,比如 xy 写反了,minmax 写混了。

2. Transform::look_at() 应该返回一个 camera-to-world 变换,但是实际上返回的是 world-to-camera 变换。

3. 生成的光线的方向向量没有被归一化 (normalize)。

这三个问题花了一些时间找到,但在修复了这些问题之后,生成的光线只朝向相机 (PerspectiveCamera) 视锥左下角的四分之一。 而且,通读了这些代码之后,再也找不到有什么问题了。 然后,基本确定一定是相机的从屏幕坐标到世界坐标的转换有错。 然而,这里只有不长的一些代码,仔细理了理逻辑也没有问题。 于是只好反反复复看,看到相机的构造函数里有用到变换矩阵的逆,想了想是不是求逆矩阵写错了。 输出之后看到,变换 (Transform) 里的逆矩阵并不是原矩阵的逆矩阵(...啊咧?)。 遂测试求逆矩阵的函数,却发现并没有错,输出非常正常,非常正确,很奇怪。 最后发现变换重载的乘法运算符函数里是这样的:

fn mul(self, t: Transform) -> Transform {
    Transform {
        m: self.m * t.m,
        m_inv: self.m_inv * t.m_inv,
    }
}
然而,正确的做法是这样:
fn mul(self, t: Transform) -> Transform {
    Transform {
        m: self.m * t.m,
        m_inv: t.m_inv * self.m_inv, // 注意这行
    }
}
简单的数学问题...


写好了关于表面反射的部分,测试了一下。 发现在一个不透明的物体表面,有一些光线会进入物体内部, 但物体表面应该反射所有的光线,没有折射,而且这种情况没有什么规律。 因为在程序运行过程中会用到一些随机数值,所以问题很有可能是取到特定的随机数时发生。 接下来花了挺长时间发现,生成光线的时候用到的一个随机数导致了问题。 于是在这里设置了一个固定值,这样每一次都可以复现这个问题了。 接下来要研究为什么取这些数值的时候会导致错误。 确定是浮点数的误差导致的问题。当光线生成之后,就要计算光线与物体的交点了。 但是,因为浮点数计算的累积误差以及浮点数的精度有限,交点的位置可能并不刚好在平面上。 有的时候,交点就出现在物体内部了。然后,当从这个交点在生成一条光线的时候, 这条光线的方向是正确的,但是起始点是错的。光线会立即从里面与物体表面相交, 然后就有可能导致接下来的光线走向物体内部。

所以,解决方案就是,计算好可能的误差范围,然后在生成光线的时候, 光线的起始点是交点的位置再沿着法线加上一个小的偏移值。

参考资料

  • Matt Pharr, Greg Humphreys, Wenzel Jakob. Physically Based Rendering: From Theory to Implementation
    用了 2016 年末发布的第三版。书中展示了一个完整的管线追踪渲染器 pbrt-v3。
  • Fletcher Dunn, Ian Parberry. 3D Math Primer for Graphics and Game
  • Scratchapixel
  • pt.go