Ray Tracing in One Weekend on WebAssembly

A partial Ray Tracing in One Weekend running on the browser with WebAssembly, built by Zig.

Fat pointer interfaces in Zig

It is possible to implement the Go style interface values in Zig with the help of c_void pointers. Implementing interface pattern is wordier than in other languages (Go, Python, Rust, ...), but it is refreshing to see the mechanism that is hidden in other languages being trasparent when writing in Zig.

Following is an example of different material types implementing the Material interface type:

// Interface type

const Material = struct {
    ptr: *const c_void,
    vtable: *const VTable,

    const VTable = struct {
        scatter: fn (*const c_void, *Vec3, *Ray, Ray, HitRecord) bool,
    };

    fn scatter(self: @This(), out_attenuation: *Vec3, out_scattered: *Ray, in_ray: Ray, rec: HitRecord) bool {
        return self.vtable.scatter(self.ptr, out_attenuation, out_scattered, in_ray, rec);
    }
};

// Implementations

const Lambertian = struct {
    albedo: Vec3,

    const vtable = Material.VTable{
        .scatter = scatter,
    };

    fn material(self: *const @This()) Material {
        return Material{
            .ptr = self,
            .vtable = &vtable,
        };
    }

    fn scatter(
        ptr: *const c_void,
        out_attenuation: *Vec3,
        out_scattered: *Ray,
        in_ray: Ray,
        rec: HitRecord,
    ) bool {
        _ = in_ray;

        const self = @ptrCast(*const Lambertian, @alignCast(@alignOf(Lambertian), ptr));

        var scatter_direction = vec3Add(rec.surface_normal, randomOnUnitSphere());
        if (vec3NearZero(scatter_direction)) {
            scatter_direction = rec.surface_normal;
        }

        out_scattered.* = Ray{ .origin = rec.p, .dir = scatter_direction };
        out_attenuation.* = self.albedo;
        return true;
    }
};

const Metal = struct {
    albedo: Vec3,

    const vtable = Material.VTable{
        .scatter = scatter,
    };

    fn material(self: *const @This()) Material {
        return Material{
            .ptr = self,
            .vtable = &vtable,
        };
    }

    fn scatter(
        ptr: *const c_void,
        out_attenuation: *Vec3,
        out_scattered: *Ray,
        in_ray: Ray,
        rec: HitRecord,
    ) bool {
        const self = @ptrCast(*const Metal, @alignCast(@alignOf(Metal), ptr));

        const reflected = vec3Reflect(vec3Normalize(in_ray.dir), rec.surface_normal);
        out_scattered.* = Ray{ .origin = rec.p, .dir = reflected };
        out_attenuation.* = self.albedo;
        return vec3Dot(out_scattered.dir, rec.surface_normal) > 0;
    }
};

// Instantiate

const material_ground = Lambertian{ .albedo = vec3(0.8, 0.8, 0.0) };

var ground = Sphere{
    .center = vec3(0, -100.5, -1),
    .radius = 100.0,
    .material = material_ground.material(),
};

// ...