之字形路径追踪器

2021-08-09 22:02:38

如果您已经阅读了此博客一段时间,您可能知道当我尝试新的编程语言时,我喜欢编写一个简单的路径跟踪器,以获得更好的“感觉”。它非常基于 smallpt(C++ 中的 99 行路径跟踪器),但我不想让它尽可能短。我更感兴趣的是我可以轻松运行它以及我可以使用哪些语言“功能” .公平地说,并非所有语言都考虑到路径跟踪器,因此其中一些语言比其他语言更容易。过去,我曾尝试过 Go(2013 年)和 Rust(2014 年)。Go 可能是不是 100% 适合此类问题的语言示例,但我还是玩得很开心。这次我决定尝试 Zig。我不得不承认我完全“盲目”了,除了在 Twitter 上发表的一些随口评论外,对语言知之甚少。我有点期待另一个 Rust,所以最初对“Spartan” Zig 的表现有点反感。在我调整并开始接受它的本质之后——一个现代的“C+”替代方案——它实际上变成了一个非常有趣的实验。我喜欢的一件事是——类似于 Go——通常只有一种做事方式,所以你不要不要花太多时间考虑它,已经为你做出决定......有时可能会有点令人惊讶(“你是什么意思使用索引进行迭代的唯一方法是'while'循环,'for'可以'不做吗?”),但最终我认为这是一件好事。 Zig 是一种仍在快速发展的年轻语言,因此版本之间存在差异,我使用的是每晚构建,更具体地说是 0.9.0-dev.473+9979741bf(它不会在 0.8.0 上编译) . Github 项目可以在这里找到。像往常一样,让我分享一堆随机观察。像往常一样,请记住,这些都是从对语言设计不太了解并且只是闲逛的人的角度编写的。听起来我主要是在抱怨,但实际上我已经非常喜欢 Zig。如前所述,Zig 有点简约(在这方面让我想起了一点 Go)。用他们自己的话说,这是一种“小而简单的语言”。他们绝对不喜欢创建太多关键字,所以希望经常使用“const”:)。 // 一个实际的常量值 const RESOLUTION: usize = 512; // 导入模块/使用 const std = @import( "std"); // 类型定义 const rfloat = f64; const Vec4 = Vector(4, rfloat); // 枚举定义 const MaterialType = enum { DIFFUSE, GLOSSY, MIRROR }; // 结构定义 const Axes = struct { a: Vec4, b: Vec4 }; // ... 等等。 “可变性”是明确的,你可以使用 var(对于变量)或 const(对于常量)(所以没有像 Rust 那样“隐含的”const,但转换相当简单,让 -> const,让 mut -> var) Zig支持“虚拟”指针,但它们不能为空,除非明确标记为“可选”

“可选”支持不限于指针,我用它来表示我们在光线投射时是否碰到任何对象: const IntersectResult = struct { objectIndex: ?usize = undefined, t: rfloat = std.math.f64_max }; const shadow_result = scene.intersect(shadow_ray); // 测试对象索引是否为“已定义”。 |shadow_index|将包含一个实际值 // 可以使用(我们必须从可选类型中“提取”它)。 if (shadow_result.objectIndex) |shadow_index | { if (shadow_index == light_index) { 内置 SIMD 向量类型。我主要用它来解决运算符的问题(它们带有所有基本操作,但没有像点积这样的东西),但是立即使用它真的很好。即使我只需要 xyz,我也使用了 4D 向量,因为它对我来说更“自然”(映射到 m128),但似乎 3D 向量也能正常工作(基于我在编译器资源管理器中的有限测试。函数参数可能即使没有显式请求,也可以通过引用传递(好吧,如果显式,将是指针)。文档声称“当这些类型作为参数传递时,Zig 可以选择复制和按值传递,或按引用传递,无论 Zig 决定的方式更快”。一开始我有点怀疑,但在随机的几个地方验证后,我不再担心,只是相信编译器会做正确的事情。上面提到的优化“部分成为可能,部分原因在于事实那个参数是不可变的”。一开始我有点恼火,但这是一件很小的事情。for 循环只能用于迭代数组的元素,如果你只想执行一个块 N 次,你需要 while... var i: usize = 0; while (i < SPP) : (i += 1) { // 循环for (0..SPP) 这个有点奇怪,但如果有办法让它作为单行(即。 fold 计数器定义和 while) 我可能会被 Rust 宠坏,它有惊人的错误消息,但编译器消息有点简洁,有时会产生误导。我的一些参数最初被称为 u1/u2,它隐藏了 Zig 类型(1 位/2 位整数),但每晚的错误消息是“未使用的函数参数”。直到我使用编译器资源管理器来验证我是否真的没有触及该参数并注意到 0.8.0 提供了更好的信息之前,我无法弄清楚。(在此处报告了问题)。还有一些其他的案例引起了片刻的挠头。

我觉得编译器也可以使用一些更严格的警告......这个是在我身上,但我觉得我可以使用一些帮助:) 我的第一个版本的 intersect 函数将返回一个对象指针(如果命中)。在里面,我按值迭代所有对象。有关详细信息,请参阅此简化的 CE 代码段。我的初始版本是 intersect1 + bar 。查找 LBB0_8 以查看循环的行为,正如您可能注意到的,它是一个基于索引的循环,它实际上从未将对象指针存储在任何地方,因此从该函数返回的值将是伪造的。这是公平的,我想可能会导致有时在更快的代码中(不确定这种特殊情况),但是,理想情况下,我们至少会收到关于获取指向临时/优化数据的指针的警告。要进行比较,请参阅 intersect2 + foo,迭代指针,对应的循环片段是 LBB1_8。随机的轻微个人烦恼,“这不是你是我”的领域:默认情况下,花车以科学格式打印。要获得十进制,您必须使用 {d},这很好,但如果您想以这种方式打印向量,则更烦人,必须提供自己的格式化程序,例如: fn format_vector( ns: Vec4, comptime fmt: [] const u8 , 选项: std.fmt.FormatOptions, writer: anytype) !无效 { _ = 选项; _ = fmt;返回 std.fmt.format(writer, "[{d},{d},{d},{d}]", .{ ns[ 0], ns[ 1], ns[ 2], ns[ 3] });我希望有一个选项可以在全局范围内更改它,但似乎并非如此。 Zig 浮点文字的类型为 comptime_float,它本质上是最大的类型,即。 f128。要使用不同的类型,您必须先转换它,我不喜欢这种语法: // tan 没有为 comptime_float 实现(55.​​0 本身是一个 comptime_float) // 所以我们必须显式地转换自己。 const fov_scale = std.math.tan( @as(rfloat, 55.0 * std.math.pi / 180.0 * 0.5));我不介意 C 做 55.0f (float) vs 55.0 (double) 等等的方式,但不可否认的是不确定如何处理 f32/f64 和 f128,必须想出一个额外的后缀。它也有点不一致,因为某些函数(如 pow)将采用类型参数并允许使用 comptime 参数:Zig 源代码比 Go 和 Rust 都短(Zig 大约 670 行 - Go 和 Rust 约 770 行),我是猜测主要是因为内置的 Vector 类型。生成的代码也令人印象深刻。二进制文件很小——大约 120k,Rust 大约 200k,Go(!) 大约 1.6MB。就性能而言,Zig 几乎与 Rust 相当,或者可能更快一点。因为它是在我的笔记本电脑上测量过的,所以请带上一粒盐,但我确保它已正确预热并且没有节流。 Zig 和 Rust 都需要大约 400 秒来渲染 512x512 图像(每像素 16*16 个样本,最多 8 次反弹(最少 4 个),4xAA)。多线程,两个版本都需要大约 100 秒。如果我用力眯眼,看起来 Zig 的速度可能始终快 2-3%,但这并不是完全一致的,因为我们使用了 2 个不同的线程系统。除了声明 Zig 之外,我不会得出任何深远的结论至少在同一个球场上并且非常有竞争力。