https://github.com/xalanq/cg_tracing
整个项目用 Rust 编写,使用 Rayon 库的多线程加速(除了速度比 C/C++ 差点外,Rust 比 C/C++ 好用太多了)。
- cargo 1.35.0 (6f3e9c367 2019-04-04)
- rustc 1.35.0 (3c235d560 2019-05-20)
- stable-x86_64-pc-windows-msvc
assets/ 资源文件
example/ 某些配置文件和样例
result/ 成果以及相应的配置文件
src/
-- macros.rs 一些宏
-- main.rs 可执行程序入口代码,默认读入 ./example/test.json 作为配置文件
-- prelude.rs 引用后便能调用项目大部分功能
-- lib.rs
-- geo/ 包含物体渲染所需要的结构
---- mod.rs 定义了物体所需要实现的 trait,即要实现的接口
---- texture.rs 材质
---- collection/ 各种物体的具体实现
------ bezier.rs Bezier 曲线/曲面
------ mesh.rs 三角网格
------ mod.rs
------ plane.rs 平面
------ sphere.rs 球
-------ds/ 物体所用到的数据结构
---------bbox.rs 包围盒
---------bsptree.rs BSP-Tree
---------kdtree.rs KD-Tree
---------mod.rs
-- linalg/ 线性代数数学工具
---- mat.rs 4x4 矩阵以及一些矩阵变换
---- mod.rs
---- ray.rs 光线(射线)
---- transform.rs 存储物体的一系列矩阵变换及逆变换
---- vct.rs 三维向量
-- scene/ 场景
---- camera.rs 摄像机
---- mod.rs
---- sppm.rs 渐进光子映射所需要的数据结构
---- world.rs 实现了路径追踪和渐进光子映射算法
-- utils/ 一些常用工具
---- images.rs 存储材质图片的结构
---- mod.rs 随机数生成器、常用函数等
所有图形渲染的参数都能用 JSON 格式的配置文件来设置,整体代码结构也尽量保持了清晰易懂、尽量降低了不同文件之间的耦合性、尽量减少了一些常数开销。
- PT/SPPM
- 网格化求交 / Bezier 参数曲面求交
- 算法型加速:Bezier 线性求多项式系数、下山牛顿迭代、KD-Tree/BSP-Tree、快速三角面求交、随机数生成器
- 景深、软阴影、抗锯齿、贴图
配置文件见 result_6.json (注意,白光在正上方,视野外左边有红墙,右边有紫墙,所以有泛红和泛紫效果,同时焦平面在龙那里,所以红球模糊)。
代码部分参考了 small-pt 这个项目。
原理是从摄像机成像面的某个像素点不断发出方向随机的射线,射线在经过不同材质表面的物体反射、折射后,最终抵达光源,然后回溯求出色彩信息。
可以发现射线抵达光源是一个小概率事件,因此该算法需要大量采样才能达到令人满意的效果。一般来说,一个像素点采样 8192 次就差不多了。同时由于反射、折射可能无限进行下去,因此还需要限制一下深度,简单场景设置 5 就差不多了。
由于 PT 是超采样的,所以自然就达到了软阴影的效果,同时也有了抗锯齿的效果。
具体实现见 src/scene/world.rs 第 183 行以后。
见论文 Progressive Photon Mapping
原理是先像路径追踪那样从摄像机发送射线,不同的是,PPM 在第一次漫反射的地方停止,并记录交点。然后再从光源随机发出光子,若光子打到了之前记录的某个交点的一个半径为 R 的球内,则统计该光子对这个交点的贡献。同时根据统计次数的增多,这个半径 R 应当适当减小。当光子足够多时,则这些贡献累加起来则会趋向于真实值。
配置文件见 result_7.json (这是在检查之后写的,由于实在没时间跑了,就跑了个低分辨率的图,然后光子数比较少、迭代次数比较少,所以还有点噪)。
见论文 Stochastic Progressive Photon Mapping
与 PPM 不同的是,统计的不再是对某个交点的贡献,而是对一个区域的贡献(比如说成像面的像素点),其余的和 PPM 大致相似。
交点我用 KD-Tree 来维护,然后每个像素点的信息用以下公式维护,$\alpha = 0.7$:
"renderer": {
"type": "sppm",
"view_point_sample": 4, // 每个像素点 HitPoint 采样数
"photon_sample": 300000, // 光子数目
"radius": 1, // 初始半径
"radius_decay": 0.95, // 半径衰减值
"rounds": 100, // 迭代轮数
"light_pos": { "x": 50, "y": 81.599999, "z": 81.6 }, // 光源位置
"light_r": 15 // 光源半径
}
具体实现见 src/scene/world.rs 第 402 行以后。
"camera": {
"origin": { "x": 50.0, "y": 50.0, "z": 300.0 }, // 位置
"direct": { "x": 0.0, "y": -0.082612, "z": -1.0 }, // 方向
"view_angle_scale": 0.5135, // 成像平面的视角比例
"plane_distance": 140.0, // 成像平面与摄像机的距离
"focal_distance": 100.0, // 修正后的焦距,成像平面到焦平面的距离
"aperture": 0.7 // 光圈半径
},
配置文件 result_4.json(在最前面的紫球聚焦)。
配置文件 result_5.json(在中间的紫球聚焦)。
参考的是这篇文章,大概就是将摄像头看成一个圆而不是一个点,然后在这个圆上随机生成一个点作为摄像头,再根据焦距来确定焦平面,即: $$ o = o_{camera} + r, d = nomalize(focal_distance \cdot nomalize(d_{camera}) - r) $$
其中
具体实现见 src/scene/world.rs 第 236 行(这里还综合了与成像平面的混合操作)。
配置文件 result_1.json(中间是反射的球,材质有色部分漫反射,无色部分反射;右边是透明的球,材质有色部分不透明,无色部分透明)。
以下均约定射线
为了优化一点常数,我的代码里物体求交分为两步:光线求交检测中,先对所有物体只求出
设球圆心为
贴图的话我的做法比较蠢,先将球看成单位球,然后将半球的
具体实现见 src/geo/collection/sphere.rs。
平面用点-法式描述,点为
贴图的话比较简单,设定
具体实现见 src/geo/collection/plane.rs。
包围盒用
在这里两个向量的除法定义为各个分量作除法后得到的新向量。两个向量取
但该算法会有较多的比较运算,而且很多无解的情况下根本不需要这么多比较。可以发现上述操作基本都是对每个分量来做的,因此可以将各个分量单独来做并判断。具体实现见 src/geo/collection/ds/bbox.rs 第 30 行以后。
贴图的话,就 6 个面当平面分别贴一下就好(但我没写)。
长方体可以由一个包围盒通过矩阵变换得到,因此不做讨论(也没写)。
配置文件 result_3.json(注意左边反射物体的顶端是光源,底部黑区域是影子,右边折射物体的影子上的光斑是光线折射得到的)。
一个二维
显然这个
但这个系数是可以优美的解出来的,具体看 n+e 同学写的解法,再根据解法里的式子稍微简化后可以得到
let mut a = vec![];
let mut t = 1.0;
for i in 0..=n {
a.push((x[0] * t, y[0] * t));
t = t * (n - i) as f64 / (i + 1) as f64;
for j in 0..n - i {
x[j] = x[j + 1] - x[j];
y[j] = y[j + 1] - y[j];
}
}
接下来考虑旋转曲面求交,只考虑 Bezier 曲线绕
光线与曲面相交当且仅当交点到
平方后两边乘
则令
其中
则
那么就可以用牛顿迭代求出
由于可能有多解,某些特殊情况两个解甚至挨得特别近,那么一个初始值迭代得到的解往往不是最优解(使得
这个下山牛顿迭代具体是这样,在迭代
牛顿迭代求出
然后是求法向量。若
则法向量为
若
具体实现见 src/geo/collection/bezier.rs。
配置文件 result_2.json。
只实现了三角面片的网格物体。用 KD-Tree 或 BSP-Tree 来维护所有三角面片。以下仅说明 BSP-Tree(因为KD-Tree 是 BSP-Tree 的特例,划分用的是平行坐标轴的平面)。
BSP-Tree 如何选取划分的平面是一个难题,首先选所有 KD-Tree 会选的面(即平行坐标轴且左右点集到平面距离方差最大的那个),然后再考虑每个三角形面片的面,以及这个面绕着三条边旋转 45°、-45°得到的面。得到面之后再划分,跨越平面的三角面两边都会加入进去。当面片个数小于设定阈值便直接划分为叶子。
这样预处理的复杂度是平方级的,特别慢,但目前我没有找到一种好的办法(而 KD-Tree 的预处理理想情况下是
查询的话就比较普通了,根据射线与划分平面的位置关系,确定左右孩子的先后递归顺序,然后在递归的同时根据目前最优答案再剪枝一下。实际测试下来速度还是很快的(我用的是手工栈实现的递归)。
三角面与光线求交我用的不是 Möller–Trumbore intersection algorithm 而是 Fast Ray-Triangle Intersections by Coordinate Transformation,可以通过预处理得到一些变换矩阵,之后只需要最多进行 15 次乘法、15 次加法、1 次除法(浮点数)便能求出解(也并不是直接矩阵乘法),具体见代码第 171 行以后。
BSP-Tree 和 KD-Tree 在我的测试中,查询速度差别并不大,面比较少时 BSP-Tree 会慢一些(因为查询时 BSP-Tree 要进行两次点积,而 KD-Tree 只需要一个乘法,所以深度优势便体现不出来了,更何况深度也不会比 KD-Tree 浅到哪里去)。同时由于预处理实在太慢,所以我最终渲染用的数据结构均为 KD-Tree 而非 BSP-Tree。
具体实现见 src/geo/collection/mesh.rs。
生成一个
U32 x = seed
function rand()
x ^= x << 13
x ^= x >> 17
x ^= x << 5
return x / MAX_U32