我们绘制了180行裸C ++的卡通爆炸图

一周前,我发表计算机图形学课程的 另一章 ; 今天,我们再次回到光线追踪,但是这次我们将比渲染琐碎的球体走得更远。 我不需要照片写实;出于卡通目的,在我看来, 这样的爆炸会下降。

一如往常,我们只有一个裸露的编译器可供使用,不能使用任何第三方库。 我不想打扰窗口管理器,鼠标/键盘处理等。 我们程序的结果将是一张保存到磁盘的简单图片。 我根本不追求速度/优化,我的目标是展示基本原理。

总体而言,在这样的条件下,如何用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); // note that the color is "hot", ie has components >1 const Vec3f orange(1.0, 0.6, 0.0); const Vec3f red(1.0, 0.0, 0.0); const Vec3f darkgray(0.2, 0.2, 0.2); const Vec3f gray(0.4, 0.4, 0.4); float x = std::max(0.f, std::min(1.f, d)); if (x<.25f) return lerp(gray, darkgray, x*4.f); else if (x<.5f) return lerp(darkgray, red, x*4.f-1.f); else if (x<.75f) return lerp(red, orange, x*4.f-2.f); return lerp(orange, yellow, x*4.f-3.f); } 

这是五个关键颜色之间的简单线性渐变。 好,这是图片!



结论


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

Source: https://habr.com/ru/post/zh-CN437714/


All Articles