@ryoppippi

ZigでRay Tracing in One Weekendをやってみた

20 Jul 2022 ・ 14 min read


はじめに

ここ数週間で​ホットな​言語、Zigを​触っている。 何か​作りたいなと​思っていたが、​以前から​やってみたかったRay Tracing in One Weekend (週末レイトレーシング)を​やってみる​ことにした。

最終画像 最終的に​生成される​画像。​うっとりする

https:-/github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig

https://raytracing.github.io/books/RayTracingInOneWeekend.html https://inzkyk.xyz/ray_tracing_in_one_weekend/

所感

本文は​C++で​書かれているが、​クラス、​継承、​演算子オーバーロード等の​概念は​Zigには​ないため適宜工夫する​必要が​あった。 また、​筆者が​普段(型の​緩めな​)Pythonや​JSを​主に​使っている​ため、​終始コンパイラに​叱られっぱなしだった​^大学で​C言語を​はじめて​学んだ​時を​思い出す

面白い​/気に​なった​点

Rustや​Go、​TS等の​近代静的型付言語では​常識なのかもしれないが、​個人的に​新鮮だった​機能や​気に​なった​点を​ここに​列挙していく。

Note

この​記事内での​Zigの​バージ​ョンは0.10.0-dev.3007+6ba2fb3dbです。​その​ためこの​記事の​内容は​後に​古くなる​可能性が​あります。

コンパイル時に​のみ​実行される​コード

zigではcomptimeと​いう​マーカを​付ける​ことで​コンパイル時に​評価される​コードを​書く​ことができる。 型情報や​定数など、​あらかじめ評価できる​情報を​用いて​コンパイラに​事前処理を​させるような​もので、​もの​すごく​クールである。 た​とえば​以下の​コードでは​受け取る​型が​intか​floatかで​呼び出す関数を​変えている​(しかも​それ以外の​型が​渡されたら​コンパイルエラーを​発生させる​ことも​できる)。

comptimeは​変数や​引数に​つける​ことができる。

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/d4e400c1e0e6d51f0ba402d3029954bcf6243128/src/rtweekend.zig#L26-L32

また、comptime_intcomptime_floatなる​ものも​存在する​^https://ziglearn.org/chapter-1/#comptime 参照。​変数が​intか​floatかを​定義時には​ざっくりと、​コンパイル時には​具体的な​型に​推論できる​特別な型である。

comptimeを​応用すると​型を​受け取って​型を​返す関数を​定義する​ことができる。​C++の​Templateの​よう。

型を受け取ってstructを返すコード
const std = @import("std");
fn Vec(
    comptime count: comptime_int,
    comptime T: type,
) type {
    return struct {
        data: [count]T,
        const Self = @This();

        fn init(data: [count]T) Self {
            return Self{ .data = data };
        }
    };
}
pub fn main() void {
    const v = Vec(3, f32).init([3]f32{ 1, 2, 3 });
    std.debug.print("{}", .{v.data.len});
}

今回の​実装では、​受け取った​型が​Vector型であるか、​また​scalarと​Vectorの​型が​一致しているかを​検証する​ために​この機能を​活用した。

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/d4e400c1e0/src/vec.zig#L141-L151

注意^筆者は​これに​約1日悩まされた。​抽象的な​型に​対する​関数を​定義する​ときは​気を​つけた方が​よさそう。

comptimeは​コンパイル時に​評価される​変数に​のみ​使用できる。 Zigではconstant valueruntime valueと​いう​2つの​用語で​区別しているようだ。 なので、​以下の​コードは​エラーに​なる。

const std = @import("std");

fn size(comptime v: anytype) comptime_int {
    const T: type = @TypeOf(v);
    return @sizeOf(T); // 定義: @sizeOf(comptime T: type) comptime_int
}

pub fn main() void {
    var v1: i32 = 1; // constでもcomptime varでもないので
    std.debug.print("{}\n", .{size(v1)}); // error: runtime value cannot be passed to comptime arg
}

この​場合、​以下のように​変更すると​実行できる。

const std = @import("std");

fn size(comptime T: type) comptime_int {
    return @sizeOf(T);
}

pub fn main() void {
    var v1: i32 = 1;
    const T = @TypeOf(v1); // 型情報は静的なので、constで定義できる
    std.debug.print("{}\n", .{size(T)}); // 4
}

switch

switchは​かなり​洗練されていて、​パターンマッチングのように​使用する​ことができる​^https://ziglearn.org/chapter-1/#switch 参照。 また、​式と​しても​利用する​ことができるので、​とても​便利^と​いうか​今回の​実装では​式としか​利用していない

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/7965b1c976/src/rtweekend.zig#L26-L32

ラベル付きBlock​(2022/07/21追記)

Zigの​Block{}に​ラベルを​与えると​式と​して​使う​ことができる。

test "struct" {
    const a = blk: {
        var x: i32 = 3;
        break :blk x + 2;
    };
    std.debug.print("{}", .{a}); // 5
}

これを​使うと、​ちょっとした​処理を​挟んで​値を​返したい​ときに​とても​便利。 わざわざ関数を​作って​渡したり、gotoを​多用したり、​また​それらを​避ける​ために​記述を​増や​したりする​必要が​なくなる。 また、​ラベルが​ついているので、​わざわざコメントを​書かなくても​わかりやすい​コードに​なる。

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/99459216213d109b7414714bd6c0c30c1d57caec/src/randomScene.zig#L41-L59

参考: ラベル付きBlockを使わないコード

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/7965b1c9765caab7168152f472fdef540a77a442/src/randomScene.zig#L41-L62

if (vec.len(center - Point3{ 4.0, 0.2, 0.0 }) > 0.9) {
        // generate a random material
    if (choose_mat < 0.8) {
        // diffuse
        const albedo = vec.randomVecInRange(rnd, Color, 0, 1) * vec.randomVecInRange(rnd,
0, 1);
        const diffuse_material = Material.lambertian(albedo);
        const diffuse_sphere = Sphere{ .center = center, .radius = 0.2,
diffuse_material };
        _ = try world.add(diffuse_sphere);
    } else if (choose_mat < 0.95) {
        // metal
        const albedo = vec.randomVecInRange(rnd, Color, 0.5, 1.0);
        const fuzz = rtw.getRandomInRange(rnd, SType, 0.0, 0.5);
        const metal_material = Material.metal(albedo, fuzz);
        const metal_sphere = Sphere{ .center = center, .radius = 0.2, .mat = metal_material };
        _ = try world.add(metal_sphere);
    } else {
        // glass
        const ir = 1.5;
        const glass_material = Material.dielectric(ir);
        const glass_sphere = Sphere{ .center = center, .radius = 0.2, .mat = glass_material };
        _ = try world.add(glass_sphere);
    }

継承の​代わりと​しての​共用体​(union

Zigにはunion型が​存在する​^https://ziglearn.org/chapter-1/#unions 参照。 C言語の​共用体と​同じく、​中身の​メンバーの​メモリは​共有される。 共用体は​ある​変数に​代入したい​型が​複数ある​ときに​用いる​ことができる。

オリジナルの​テキストでは​C++の​クラスや​継承を​多用していたが、​Zigには​それらの​機能は​ないため、​複数のstructを​まとめる​共用体を​作成した。

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/7965b1c976/src/material.zig#L17-L33

すごいのは、​Zigには​タグ付き共用体​(tagged union)なる​ものが​ある。 これが​よく​できていて、​共用体の​どの​メンバーが​使用されているかをswitchで​パターンマッチングできる。 メンバーの​種類に​合わせて​各々の​処理を​書く​ことができるのとても​便利。

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/7965b1c976/src/main.zig#L40-L50

ポインタ/定数ポインタ

ポインタ型には​定数の​もの​(*const)と​そうでない​もの​(*)が​ある。 constで​定義された​変数​(定数)からなる​ポインタは​定数ポインタ​(*const)と​いう​扱いを​受ける​そうだ。 なので、​以下の​コードは​エラーに​なる。

pub fn main() void {
    const x: u8 = 1;
    var y = &x;
    const _y = &x;
    y.* += 1; // error: cannot assign to constant
    _y.* += 1; // error: cannot assign to constant
}

varで​定義された​変数なら​通常の​ポインタと​なる。

pub fn main() void {
    var x: u8 = 1;
    var y = &x;
    const _y = &x;
    y.* += 1; // ok
    _y.* += 1; // ok
}

関数の​引数は​定数

C言語等と​違い、​Zigでは​関数の​引数は​定数である。 その​ため、​引数に​代入しようと​すると​エラーに​なる。

const std = @import("std");

fn f(a: anytype) @TypeOf(a) {
    a += 1; // error: cannot assign to constant
    return a;
}

pub fn main() void {
    var a: u8 = 1;
    std.debug.print("{}", .{f(a)});
}

仮の​変数を​経由するか、​引数を​ポインタに​する​ことで​実現できる。

const std = @import("std");

fn f(a: anytype) @TypeOf(a) {
    var tmp = a;
    tmp += 1;
    return tmp;
}

pub fn main() void {
    var a: u8 = 1;
    std.debug.print("{}", .{f(a)});
}

defer

ブロックを​抜ける​ときに​実行される​コードを​好きな​位置に​書ける。 Goには​あるようだが、​自分には​新鮮だった。 た​とえば、​メモリを​確保する​コードの​直後に​解放する​処理を​記述する​ことができる。​バグが​減りそうで​便利。

https://github.com/ryoppippi/Ray-Tracing-in-One-Weekend.zig/blob/7965b1c976/src/main.zig#L69-L72

ループ

for文が​iteratorに​しか​使えないのは​びっくりした。 じゃ​あ​添字が​ひと​つずつ​増えていく​ループを​書く​ときは​どうするかと​いえば、​なんとwhileを​使うしかないらしい

fn f() void {
    var i: u32 = 0;
    while (i <= 100) : (i += 1) {
        std.debug.print("{}\n", .{i});
    }
    std.debug.print("done!", .{});
}

一昔​前の​C言語では​添字をforの​中で​定義できなかった、と​いう​話を​思い出した。 ただ​このままだと​変数iに​対して​スコープも​何も​ないので、{}で​括ると​いい​感じに​なる。

fn f() void {
    {
        var i: u32 = 0;
        while (i <= 100) : (i += 1) {
            std.debug.print("{}\n", .{i});
        }
    }
    std.debug.print("done!", .{});
}

テスト

コード内に​テストを​直に​かけるのが​便利だった。 ただ、​最近の​他の​言語では​普通?なのかもしれない。

おわりに

コンパイラに​叱られてばかりで​めちゃくちゃ​実装が​遅くなってしまったが、​逆に​言えば​叱られた​通りに​修正していけば​動くようになるのは​とても​良い​体験だった。 また、​他言語で​実装している​人も​ちら​ほら​いらっしゃるのを​拝見したが、​今回の​実装は​だいぶ速く​実行されるように​思える​^比較の​ベンチマークを​とっていないので、​どなたか​比較してみてください

参考: 手元のM1 Mac miniでの計測結果
________________________________________________________
Executed in  879.32 secs    fish           external
   usr time  863.33 secs   35.00 micros  863.33 secs
   sys time   15.33 secs  550.00 micros   15.33 secs

今回はとにかく​書いていて​楽しかったので、​また​何か​書いてみようと​思う。

関連文献等

  • Zig Documentation - わから​なくなったらとりあえず​見る。​ただ​標準ライブラリ等は​まだ​文章化されていないので​レポジトリを​見に​行く方が​早かった。
  • Ziglearn.org - Zigの​便利な​機能が​わかりやすく​詰まっている。
  • Gamedev Guide - yet another Ziglearnと​いう​感じの​記事。

おまけ

Zigの​LSPである​ZLSは​まだまだ​発展途上である​(変数型の​情報が​抽出されない、など)。 なので​適宜コンパイルを​する​→コンパイラに​叱られる​→修正する​→…と​いう​流れで​作業していた。

この​とき、​エラーを​眺めて​デバッグするのに​Neovimの​Quickfixが​とても​便利だった。

nvim 数ヶ月前に​メインエディタを​VSCodeから​Neovimへ​移行したのだが、​今回その​ありが​たみを​存分に​実感​できた。

またvim-jpの​皆様にも​お世話に​なりました。

comment on bluesky / twitter
CC BY-NC-SA 4.0 2022-PRESENT © ryoppippi