一周前,我
发表了
计算机图形学课程的 另一章 ; 今天,我们再次回到光线追踪,但是这次我们将比渲染琐碎的球体走得更远。 我不需要照片写实;出于卡通目的,在我看来,
这样的爆炸会下降。
一如往常,我们只有一个裸露的编译器可供使用,不能使用任何第三方库。 我不想打扰窗口管理器,鼠标/键盘处理等。 我们程序的结果将是一张保存到磁盘的简单图片。 我根本不追求速度/优化,我的目标是展示基本原理。
总体而言,在这样的条件下,如何用180行代码绘制这样的图片?

让我什至插入一个动画gif(六米):

现在,我们将整个任务分为几个阶段:
第一阶段:阅读上一篇文章
是的,完全正确。 首先要做的是阅读
上一章 ,该
章讨论了光线跟踪的基础知识。 它很短,原则上不能读取所有反射折射,但至少要等到漫射光下才建议阅读。 代码很简单,人们甚至在微控制器上运行它:

第二阶段:画一个球
让我们绘制一个球体,而不必担心材料或照明。 为简单起见,此球体将位于坐标中心。 关于这张照片,我想得到:

请参阅
此处的代码,但让我直接在本文中为您提供主要代码:
#define _USE_MATH_DEFINES #include <cmath> #include <algorithm> #include <limits> #include <iostream> #include <fstream> #include <vector> #include "geometry.h" const float sphere_radius = 1.5; float signed_distance(const Vec3f &p) { return p.norm() - sphere_radius; } bool sphere_trace(const Vec3f &orig, const Vec3f &dir, Vec3f &pos) { pos = orig; for (size_t i=0; i<128; i++) { float d = signed_distance(pos); if (d < 0) return true; pos = pos + dir*std::max(d*0.1f, .01f); } return false; } int main() { const int width = 640; const int height = 480; const float fov = M_PI/3.; std::vector<Vec3f> framebuffer(width*height); #pragma omp parallel for for (size_t j = 0; j<height; j++) { // actual rendering loop for (size_t i = 0; i<width; i++) { float dir_x = (i + 0.5) - width/2.; float dir_y = -(j + 0.5) + height/2.; // this flips the image at the same time float dir_z = -height/(2.*tan(fov/2.)); Vec3f hit; if (sphere_trace(Vec3f(0, 0, 3), Vec3f(dir_x, dir_y, dir_z).normalize(), hit)) { // the camera is placed to (0,0,3) and it looks along the -z axis framebuffer[i+j*width] = Vec3f(1, 1, 1); } else { framebuffer[i+j*width] = Vec3f(0.2, 0.7, 0.8); // background color } } } std::ofstream ofs("./out.ppm", std::ios::binary); // save the framebuffer to file ofs << "P6\n" << width << " " << height << "\n255\n"; for (size_t i = 0; i < height*width; ++i) { for (size_t j = 0; j<3; j++) { ofs << (char)(std::max(0, std::min(255, static_cast<int>(255*framebuffer[i][j])))); } } ofs.close(); return 0; }
向量的类别存在于geometry.h文件中,这里不再赘述:首先,那里的一切都很琐碎,对二维和三维向量进行简单的操作(加,减,赋值,乘以标量,标量积),其次,
gbg作为计算机图形学课程的一部分已经对其进行了
详细描述 。
我将图片保存为
ppm格式 ; 尽管并非总是最方便的进一步查看方式,但这是保存图像的最简单方法。
因此,在main()函数中,我有两个循环:第二个循环只是将图像保存到磁盘上,第一个循环穿过图像的所有像素,通过该像素从相机发出光线,并查看该光线是否与我们的球体相交。
请注意,本文的主要思想是:如果在上一篇文章中我们分析性地考虑了射线与球体的交点,那么现在我将其数字化。 这个想法很简单:球体的方程形式为x ^ 2 + y ^ 2 + z ^ 2-r ^ 2 = 0; 但通常,函数f(x,y,z)= x ^ 2 + y ^ 2 + z ^ 2-r ^ 2定义在整个空间中。 在球体内部,函数f(x,y,z)将具有负值,在球体外部,其将为正。 也就是说,函数f(x,y,z)设置点(x,y,z)到我们球体的距离(带符号!)。 因此,我们只是沿光束滑动,直到我们感到无聊或函数f(x,y,z)变为负值。 sphere_trace()函数就是这样做的。
第三阶段:原始照明
让我们编写最简单的漫射照明代码,我想在输出端得到这样的图片:

与上一篇文章一样,为了易于阅读,我做了一个步骤=一个提交。 变化可以在
这里看到 。
对于漫射照明,仅仅计算光束与表面的交点还不够,我们需要知道该点处与表面的法线向量。 我通过到表面的距离的函数中的简单
有限差分得到了该法向矢量:
Vec3f distance_field_normal(const Vec3f &pos) { const float eps = 0.1; float d = signed_distance(pos); float nx = signed_distance(pos + Vec3f(eps, 0, 0)) - d; float ny = signed_distance(pos + Vec3f(0, eps, 0)) - d; float nz = signed_distance(pos + Vec3f(0, 0, eps)) - d; return Vec3f(nx, ny, nz).normalize(); }
原则上,当然,由于我们正在绘制一个球体,因此可以更容易地获得法线,但是我为将来做准备。
第四阶段:让我们在球体上画出图案
让我们在我们的区域中绘制某种模式,例如:

为此,在前面的代码中,我
仅更改
了两行!我是怎么做到的? 当然,我没有纹理。 我只是把函数g(x,y,z)= sin(x)* sin(y)* sin(z); 它再次在整个空间中定义。 当我的射线在某个点处穿过球体时,函数g(x,y,z)的值将为我设置像素的颜色。
顺便说一下,请注意球体周围的同心圆-这些是我的相交数值计算的伪像。
第五步:位移映射
我为什么要画这种图案? 他会帮助我画出这样的刺猬:

在我的图案为黑色的地方,我想在我们的球体上打一个洞,而在白色的地方,相反,将驼峰拉长。
为此,只需
更改代码中的
三行即可 :
float signed_distance(const Vec3f &p) { Vec3f s = Vec3f(p).normalize(sphere_radius); float displacement = sin(16*sx)*sin(16*sy)*sin(16*sz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); }
也就是说,我更改了到我们表面的距离的计算,将其定义为x ^ 2 + y ^ 2 + z ^ 2-r ^ 2-sin(x)* sin(y)* sin(z)。 实际上,我们定义了一个
隐式函数 。
第六步:另一个隐式函数
为什么我只对球体表面上的点评估正弦乘积呢? 让我们重新定义隐式函数,如下所示:
float signed_distance(const Vec3f &p) { float displacement = sin(16*px)*sin(16*py)*sin(16*pz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); }
与之前的代码的区别很小,最好
查看diff 。 结果如下:

因此,我们可以在对象中定义断开连接的组件!
第七步:伪随机噪声
上一张图片已经开始有点像爆炸了,但是罪恶的产物具有太规则的规律。 我们需要更多的“撕裂”的功能,更多的“随机”功能...
Perlin的噪音将对我们有所帮助。 这样的事情比罪恶的产物更适合我们:

如何产生这样的噪音有些不合时宜,但这是主要思想:您需要产生具有不同分辨率的随机图像,将其平滑以得到如下所示的图像:

然后总结一下:
在这里和
这里阅读更多。
让我们
添加一些代码来产生这种噪声并得到这张图片:

请注意,在渲染代码中,我什么都没有改变,只是“使”球体“起皱”的功能已经改变。
第八阶段,决赛:增加色彩
我
在提交中所做的唯一
更改是,我应用的颜色不是均匀的白色,而是线性地取决于所应用的噪声量:
Vec3f palette_fire(const float d) { const Vec3f yellow(1.7, 1.3, 1.0);
这是五个关键颜色之间的简单线性渐变。 好,这是图片!

结论
这种射线追踪技术称为射线行进。 作业很简单:将爆炸与二十一点和反射一起穿过以前的光线跟踪器,然后爆炸将照亮周围的一切! 顺便说一下,这种爆炸缺乏透明度。