纯CMake中的光线追踪

2021-01-09 21:34:39

事不宜迟,我介绍一下:用100%纯CMake编写的基本的whited射线追踪器,具有多核渲染功能。如果您不关心细节,而只想查看代码,则可以在这里找到。

在这一点上,熟悉CMake的人可能会有一些疑问,因此请继续阅读以了解其工作原理。

好消息:CMake有一个数学命令。坏消息:它仅支持整数。如果您以前编写过射线追踪器,则可能是使用浮点数完成的。那么,如何从表示带符号的整数到表示类似浮点数的数字呢?一种答案是使用定点算法。

定点的基本思想很简单。我们定义一些大整数来表示数字1.0;让我们选择1000。然后我们可以将2.0表示为2000,将0.5表示为500,将-3.0表示为-3000等。当我们想将两个数字相加时,只需将其定点表示相加即可。这是CMake的外观:

这需要将两个值a和b相加并存储在变量res中。我使用PARENT_SCOPE,这样我们创建的变量实际上可以从调用函数中看到,否则CMake将在函数结束时销毁它。

若要将两个数字相乘,我们只需将其定点表示形式相乘,然后除以我们选择表示1.0的值:1.5×4.0↦1500×4000 1000 = 6000↦6.0 1.5 \ times 4.0 \ mapsto \ frac {1500 \ times 4000} {1000} = 6000 \ mapsto 6.0 1。 5×4。 0↦1 0 0 0 1 5 0 0×4 0 0 0 = 6 0 0 0↦6。 0

除法相似:1.5÷4.0×1500×1000 4000 = 375×0.375 1.5 \ div 4.0 \ mapsto \ frac {1500 \ times 1000} {4000} = 375 \ mapsto 0.375 1。 5÷4。 0 0 4 0 0 0 1 5 0 0×1 0 0 0 0 = 3 7 5 0。 3 7 5我们可以在完成除法运算后乘以1000,但是当整数除法向零舍入时,这将消除我们的所有精度(因为1500 4000×1000 = 0×1000 = 0 \ frac {1500} {4000} \乘以1000 = 0 x 1000 = 0 4 0 0 0 1 5 0 0×1 0 0 0 = 0×1 0 0 0 = 0)。只要股息不是太大(会导致溢出),首先相乘就会给我们更好的结果。

CMake的math命令仅支持基本整数运算。对于平方根之类的更复杂的操作,我们使用牛顿-拉夫森迭代。您可以在此处了解更多信息,但是基本思路是进行猜测。至于应该输出什么,然后迭代地将猜测推向答案。取决于初始猜测的质量,这仅在三到四个迭代中即可得出令人惊讶的准确结果:

函数(sqrt x res)div_by_2($ {x} guess)foreach(计数器RANGE 4)如果($ {guess}等于0)set(" $ {res}" 0 PARENT_SCOPE)return()endif ()div($ {x} $ {guess} tmp)add($ {tmp} $ {guess} tmp)div_by_2($ {tmp} guess)endforeach()set(" $ {res}&#34 ;" $ {猜测}" PARENT_SCOPE)endfunction()#sqrt(123)= 11.09072626,实际答案是11.0905365064

我还实现了一个类似的函数来分别计算1 x \ frac {1} {\ sqrt {x}} x 1,因为我发现它可以带来更好的数值稳定性,而不是像上面那样计算平方根,然后执行互惠的。当我们需要归一化向量时,这很方便。

计算机图形学中的几乎所有事物都是由矢量完成的,因此我开始实现矢量操作:vec3_add,vec3_mul,vec3_div,vec3_dot等。它们利用了CMake内置列表,这些列表非常可怕,但省去了我不得不使用三个单独的列表的麻烦变量以跟踪每个向量的各个分量。例如,这是点积的样子:

函数(vec3_dot xy res)列表(GET $ {x} 0 x_0)列表(GET $ {x} 1 x_1)列表(GET $ {x} 2 x_2)列表(GET $ {y} 0 y_0)列表(GET $ {y} 1 y_1)list(获取$ {y} 2 y_2)mul($ {x_0} $ {y_0} z_0)mul($ {x_1} $ {y_1} z_1)mul($ {x_2} $ {y_2} z_2)add($ {z_0} $ {z_1} tmp)add($ {tmp} $ {z_2} tmp)set(" $ {res}" $ {tmp} PARENT_SCOPE)endfunction()

还有其他一些细节,例如钳位和截断,这就是所需的全部算法。

如果您是射线追踪的新手,请参考Peter Shirley的精彩书籍系列。 “一个周末”中的光线跟踪,这是我的代码大致基于的。通常的直觉是将光线从相机射入场景并查看它们相交的地方。由于我们将所有场景几何图形和射线表示为数学对象,因此计算射线与几何图形之间的交点只是求解方程式的一种情况。找到交点后,我们将计算与之相交的点的颜色,该点本身可以​​通过将光线跟踪到光源或其他场景几何来计算。

为了简单起见,我使用了一个简单的场景,该场景由位于无限平面上方的球形棋盘组成。我还最终伪造了球体下面的阴影,只是绘制了一个黑色圆圈(如果您从图像中发现它,那就做得很好)。我曾经在一点上实现了弱化的光线追踪甚至路径追踪,但是对于相同的结果,它们要复杂得多,并且表现会差很多。从理论上讲,尽管没有理由我不能正确地做到这一点,但这只需要付出额外的努力和耐心。

这是主要的痕迹函数看起来像,为清楚起见,删除了一些不必要的位:

#将光线跟踪到场景中,计算沿ray函数返回的颜色(trace ray_origin ray_dir depth color)#如果($ {depth} GREATER_EQUAL 3)return()else()math(EXPR depth&#34 ; $ {depth} + 1")endif()#计算与球体和平面的交点sphere_intersect($ {ray_origin} $ {ray_dir} hit_t_1 hit_point_1 hit_normal_1)plane_intersect($ {ray_origin} $ {ray_dir} hit_t_2 hit_point_2 hit_normal_2 )#我们触球了吗? if($ {hit_t_1} GREATER $ {ray_epsilon})#计算反射光线的方向offset_origin(hit_point_1 hit_normal_1 new_origin)vec3_dot(hit_normal_1 $ {ray_dir}标量)mul_by_2($ {scalar}标量)vec3_mulf(hit_normal_1suba) ($ {ray_dir} refl_a new_dir)#递归地将新光线跟踪到场景跟踪中(new_origin new_dir $ {depth} traced_col)#计算灯光设置的贡献(col 0 0 0)light_contrib(hit_point_1 hit_normal_1 light1_pos light1_col out_col1)light_contrib(hit_point_1 hit_normal_1 light2_pos light2_col out_col2)vec3_add(col out_col1 col)vec3_add(col out_col2 col)vec3_add(col traced_col col)set(base_col $ {sphere_color})vec3_mul(base_col col col)#我们撞上飞机了吗? elseif($ {hit_t_2}更大的$ {ray_epsilon})#... snip:如果我们在范围内,则使用圆的方程式来伪造阴影#... snip:计算棋盘格,否则()#我们命中什么也不返回黑色set(col 0 0 0)endif()set(" $ {color}" $ {col} PARENT_SCOPE)endfunction()

当我开始的时候,我不确定是否可以在纯粹的CMake中进行,但是通过一些技巧,我们可以对其进行管理。

对于N N N个进程,基本计划是垂直分割图像,并让每个子进程渲染几行。我们可以使用execute_process命令调用子流程,并通过-D传递参数(例如worker索引)。然后,每个进程将其行数据吐到一个文本文件中,一旦它们全部完成,该文件将被主进程合并在一起。

一个微妙的地方是,由于我们需要所有子流程并行运行,因此我们不能简单地调用execute_process N N N次,因为它将顺序运行它们。幸运的是,我们可以在一个命令中指定多个进程同时运行(我认为这是用于将一个程序传送到下一个程序的长链),但是为了避免对NNN进行硬编码,我们必须以编程方式构造对具有CMake的EVAL CODE功能的execute_process(感谢martty的支持):

消息(状态"使用$ {num_procs}进程启动ray跟踪器,$ {image_width} x $ {image_height} image ...")set(exec_command" execute_process(\ n" )foreach(worker_index RANGE 1 $ {num_procs})set(exec_command" $ {exec_command} COMMAND cmake。-Wno-dev -Dworker_index = $ {worker_index} -Dimage_width = $ {image_width} -Dimage_height = $ {image_height} -Dnum_procs = $ {num_procs} \ n")endforeach()set(exec_command" $ {exec_command})")#开始工作进程cmake_language(EVAL CODE $ {exec_command})message(状态"光线跟踪完成,正在收集结果...")

根据“一个周末的光线跟踪”,我使用PPM图像格式。这是一种非常简单的基于文本的格式,非常适合我的目的,因为我不必费心压缩。完成渲染后,我们只需读取工作人员吐出的所有数据,编写PPM标头,然后将所有内容打印到stderr:

设置(image_contents" P3 $ {image_width} $ {image_height} \ n 255 \ n \ n")foreach(worker_index RANGE 1 $ {num_procs})文件(READ" worker-$ {worker_index } .txt" file_contents)设置(image_contents" $ {image_contents} $ {file_contents}")endforeach()消息(" $ {image_contents}")

工作进程之间的工作划分非常次优,因为朝向图像顶部的行大部分是空的,而底部的行则完全满了,这意味着某些进程完成得非常快,而其他进程则需要更长的时间。解决这个问题留给读者练习。

如果您到现在为止,请多谢阅读!随意创建问题,发送拉取请求或在GitHub上标记代码。