aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/rtw
diff options
context:
space:
mode:
Diffstat (limited to 'src/rtw')
-rw-r--r--src/rtw/aabb.zig61
-rw-r--r--src/rtw/hit_record.zig21
-rw-r--r--src/rtw/hittable.zig335
-rw-r--r--src/rtw/material.zig100
-rw-r--r--src/rtw/rand.zig40
-rw-r--r--src/rtw/ray.zig13
-rw-r--r--src/rtw/texture.zig72
-rw-r--r--src/rtw/vec.zig109
8 files changed, 751 insertions, 0 deletions
diff --git a/src/rtw/aabb.zig b/src/rtw/aabb.zig
new file mode 100644
index 0000000..2c283c0
--- /dev/null
+++ b/src/rtw/aabb.zig
@@ -0,0 +1,61 @@
+const Ray = @import("ray.zig").Ray;
+const Point3 = @import("vec.zig").Point3;
+
+pub const Aabb = struct {
+ min: Point3,
+ max: Point3,
+
+ pub fn hit(aabb: Aabb, r: Ray, tMin: f64, tMax: f64) bool {
+ var tMin_ = tMin;
+ var tMax_ = tMax;
+ {
+ const s0 = (aabb.min.x - r.origin.x) / r.dir.x;
+ const s1 = (aabb.max.x - r.origin.x) / r.dir.x;
+ const t0 = @min(s0, s1);
+ const t1 = @max(s0, s1);
+ tMin_ = @max(t0, tMin_);
+ tMax_ = @min(t1, tMax_);
+ if (tMax_ <= tMin_) {
+ return false;
+ }
+ }
+ {
+ const s0 = (aabb.min.y - r.origin.y) / r.dir.y;
+ const s1 = (aabb.max.y - r.origin.y) / r.dir.y;
+ const t0 = @min(s0, s1);
+ const t1 = @max(s0, s1);
+ tMin_ = @max(t0, tMin_);
+ tMax_ = @min(t1, tMax_);
+ if (tMax_ <= tMin_) {
+ return false;
+ }
+ }
+ {
+ const s0 = (aabb.min.z - r.origin.z) / r.dir.z;
+ const s1 = (aabb.max.z - r.origin.z) / r.dir.z;
+ const t0 = @min(s0, s1);
+ const t1 = @max(s0, s1);
+ tMin_ = @max(t0, tMin_);
+ tMax_ = @min(t1, tMax_);
+ if (tMax_ <= tMin_) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ pub fn surroundingBox(box0: Aabb, box1: Aabb) Aabb {
+ return .{
+ .min = .{
+ .x = @min(box0.min.x, box1.min.x),
+ .y = @min(box0.min.y, box1.min.y),
+ .z = @min(box0.min.z, box1.min.z),
+ },
+ .max = .{
+ .x = @max(box0.max.x, box1.max.x),
+ .y = @max(box0.max.y, box1.max.y),
+ .z = @max(box0.max.z, box1.max.z),
+ },
+ };
+ }
+};
diff --git a/src/rtw/hit_record.zig b/src/rtw/hit_record.zig
new file mode 100644
index 0000000..f71904e
--- /dev/null
+++ b/src/rtw/hit_record.zig
@@ -0,0 +1,21 @@
+const vec = @import("vec.zig");
+const material = @import("material.zig");
+const Vec3 = vec.Vec3;
+const Point3 = vec.Point3;
+const Material = material.Material;
+
+pub const HitRecord = struct {
+ // The point where the ray and the hittable hits.
+ p: Point3,
+ // The normal of the hittable at p.
+ normal: Vec3,
+ // The material at p.
+ material: *const Material,
+ // p = ray.at(t)
+ t: f64,
+ // The coordinate of the surface where the ray intersects.
+ u: f64,
+ v: f64,
+ // True if the ray hits the hittable from the front face, i.e., outside of it.
+ front_face: bool,
+};
diff --git a/src/rtw/hittable.zig b/src/rtw/hittable.zig
new file mode 100644
index 0000000..c84b3d0
--- /dev/null
+++ b/src/rtw/hittable.zig
@@ -0,0 +1,335 @@
+const std = @import("std");
+const debug = std.debug;
+const math = std.math;
+const pi = math.pi;
+const ArrayList = std.ArrayList;
+
+const Ray = @import("ray.zig").Ray;
+const Vec3 = @import("vec.zig").Vec3;
+const Point3 = @import("vec.zig").Point3;
+const Color = @import("vec.zig").Color;
+const rgb = @import("vec.zig").rgb;
+const randomPointInUnitSphere = @import("rand.zig").randomPointInUnitSphere;
+const randomPointInUnitDisk = @import("rand.zig").randomPointInUnitDisk;
+const randomUnitVector = @import("rand.zig").randomUnitVector;
+const randomInt = @import("rand.zig").randomInt;
+const randomReal01 = @import("rand.zig").randomReal01;
+const randomReal = @import("rand.zig").randomReal;
+
+const Material = @import("material.zig").Material;
+const DiffuseMaterial = @import("material.zig").DiffuseMaterial;
+const MetalMaterial = @import("material.zig").MetalMaterial;
+const DielectricMaterial = @import("material.zig").DielectricMaterial;
+
+const Texture = @import("texture.zig").Texture;
+const SolidTexture = @import("texture.zig").SolidTexture;
+const CheckerTexture = @import("texture.zig").CheckerTexture;
+
+const HitRecord = @import("hit_record.zig").HitRecord;
+const Aabb = @import("aabb.zig").Aabb;
+
+const HittableTag = enum {
+ sphere,
+ movingSphere,
+ list,
+ bvhNode,
+};
+
+pub const Hittable = union(HittableTag) {
+ sphere: Sphere,
+ movingSphere: MovingSphere,
+ list: HittableList,
+ bvhNode: BvhNode,
+
+ pub fn hit(h: Hittable, r: Ray, tMin: f64, tMax: f64, record: *HitRecord) bool {
+ return switch (h) {
+ HittableTag.sphere => |sphere| sphere.hit(r, tMin, tMax, record),
+ HittableTag.movingSphere => |movingSphere| movingSphere.hit(r, tMin, tMax, record),
+ HittableTag.list => |list| list.hit(r, tMin, tMax, record),
+ HittableTag.bvhNode => |node| node.hit(r, tMin, tMax, record),
+ };
+ }
+
+ fn boudingBox(h: Hittable, time0: f64, time1: f64, outputBox: *Aabb) bool {
+ return switch (h.*) {
+ HittableTag.sphere => |sphere| sphere.boudingBox(time0, time1, outputBox),
+ HittableTag.movingSphere => |movingSphere| movingSphere.boudingBox(time0, time1, outputBox),
+ HittableTag.list => |list| list.boudingBox(time0, time1, outputBox),
+ HittableTag.bvhNode => |node| node.boudingBox(time0, time1, outputBox),
+ };
+ }
+
+ pub fn deinit(h: *const Hittable, allocator: anytype) void {
+ return switch (h.*) {
+ HittableTag.sphere => |sphere| sphere.deinit(allocator),
+ HittableTag.movingSphere => |movingSphere| movingSphere.deinit(allocator),
+ HittableTag.list => |list| list.deinit(allocator),
+ HittableTag.bvhNode => |node| node.deinit(allocator),
+ };
+ }
+};
+
+const Sphere = struct {
+ center: Point3,
+ radius: f64,
+ material: *const Material,
+
+ fn hit(sphere: Sphere, r: Ray, tMin: f64, tMax: f64, record: *HitRecord) bool {
+ const oc = r.origin.sub(sphere.center);
+ const a = r.dir.normSquared();
+ const half_b = Vec3.dot(oc, r.dir);
+ const c = oc.normSquared() - sphere.radius * sphere.radius;
+
+ const discriminant = half_b * half_b - a * c;
+ if (discriminant < 0.0) {
+ // r does not intersect the sphere.
+ return false;
+ }
+ const sqrtd = @sqrt(discriminant);
+
+ // Find the nearest root that lies in the acceptable range.
+ var root = (-half_b - sqrtd) / a;
+ if (root < tMin or tMax < root) {
+ root = (-half_b + sqrtd) / a;
+ if (root < tMin or tMax < root) {
+ // out of range
+ return false;
+ }
+ }
+
+ record.t = root;
+ record.p = r.at(root);
+ const outward_normal = (record.p.sub(sphere.center)).div(sphere.radius);
+ record.front_face = Vec3.dot(outward_normal, r.dir) < 0.0;
+ if (record.front_face) {
+ record.normal = outward_normal;
+ } else {
+ record.normal = outward_normal.mul(-1.0);
+ }
+ Sphere.getSphereUv(outward_normal, &record.u, &record.v);
+ record.material = sphere.material;
+
+ return true;
+ }
+
+ fn boudingBox(sphere: Sphere, time0: f64, time1: f64, outputBox: *Aabb) bool {
+ _ = time0;
+ _ = time1;
+ const o = sphere.center;
+ const r = sphere.radius;
+ outputBox.* = .{
+ .min = o.sub(.{ .x = r, .y = r, .z = r }),
+ .max = o.add(.{ .x = r, .y = r, .z = r }),
+ };
+ return true;
+ }
+
+ fn getSphereUv(p: Point3, u: *f64, v: *f64) void {
+ const phi = math.atan2(f64, -p.z, p.x) + pi;
+ const theta = math.acos(-p.y);
+ u.* = phi / (2.0 * pi);
+ v.* = theta / pi;
+ }
+
+ fn deinit(sphere: *const Sphere, allocator: anytype) void {
+ allocator.destroy(sphere.material);
+ }
+};
+
+const MovingSphere = struct {
+ center0: Point3,
+ center1: Point3,
+ time0: f64,
+ time1: f64,
+ radius: f64,
+ material: *const Material,
+
+ fn hit(sphere: MovingSphere, r: Ray, tMin: f64, tMax: f64, record: *HitRecord) bool {
+ const center_ = sphere.center(r.time);
+ const oc = r.origin.sub(center_);
+ const a = r.dir.normSquared();
+ const half_b = Vec3.dot(oc, r.dir);
+ const c = oc.normSquared() - sphere.radius * sphere.radius;
+
+ const discriminant = half_b * half_b - a * c;
+ if (discriminant < 0.0) {
+ // r does not intersect the sphere.
+ return false;
+ }
+ const sqrtd = @sqrt(discriminant);
+
+ // Find the nearest root that lies in the acceptable range.
+ var root = (-half_b - sqrtd) / a;
+ if (root < tMin or tMax < root) {
+ root = (-half_b + sqrtd) / a;
+ if (root < tMin or tMax < root) {
+ // out of range
+ return false;
+ }
+ }
+
+ record.t = root;
+ record.p = r.at(root);
+ const outward_normal = (record.p.sub(center_)).div(sphere.radius);
+ record.front_face = Vec3.dot(outward_normal, r.dir) < 0.0;
+ if (record.front_face) {
+ record.normal = outward_normal;
+ } else {
+ record.normal = outward_normal.mul(-1.0);
+ }
+ record.material = sphere.material;
+
+ return true;
+ }
+
+ fn boudingBox(sphere: MovingSphere, time0: f64, time1: f64, outputBox: *Aabb) bool {
+ const o0 = sphere.center(time0);
+ const o1 = sphere.center(time1);
+ const r = sphere.radius;
+ const box0 = .{
+ .min = o0.sub(.{ .x = r, .y = r, .z = r }),
+ .max = o0.add(.{ .x = r, .y = r, .z = r }),
+ };
+ const box1 = .{
+ .min = o1.sub(.{ .x = r, .y = r, .z = r }),
+ .max = o1.add(.{ .x = r, .y = r, .z = r }),
+ };
+ outputBox.* = Aabb.surroundingBox(box0, box1);
+ return true;
+ }
+
+ fn center(sphere: MovingSphere, t: f64) Point3 {
+ return sphere.center0.add(sphere.center1.sub(sphere.center0).mul((t - sphere.time0) / (sphere.time1 - sphere.time0)));
+ }
+
+ fn deinit(sphere: *const MovingSphere, allocator: anytype) void {
+ allocator.destroy(sphere.material);
+ }
+};
+
+const HittableList = struct {
+ objects: ArrayList(Hittable),
+
+ fn hit(list: HittableList, r: Ray, tMin: f64, tMax: f64, record: *HitRecord) bool {
+ var hit_anything = false;
+ var closest_so_far = tMax;
+
+ for (list.objects.items) |object| {
+ var rec: HitRecord = undefined;
+ if (object.hit(r, tMin, closest_so_far, &rec)) {
+ hit_anything = true;
+ closest_so_far = rec.t;
+ record.* = rec;
+ }
+ }
+ return hit_anything;
+ }
+
+ fn boudingBox(list: HittableList, time0: f64, time1: f64, outputBox: *Aabb) bool {
+ if (list.objects.items.len == 0) {
+ return false;
+ }
+ var firstBox = true;
+ var tmpBox: Aabb = undefined;
+ for (list.objects.items) |object| {
+ if (!object.boudingBox(time0, time1, tmpBox)) {
+ return false;
+ }
+ outputBox = if (firstBox) tmpBox else Aabb.surroundingBox(outputBox, tmpBox);
+ firstBox = false;
+ }
+ return true;
+ }
+
+ fn deinit(list: *const HittableList, allocator: anytype) void {
+ for (list.objects.items) |object| {
+ object.deinit(allocator);
+ }
+ list.objects.deinit();
+ }
+};
+
+const BvhNode = struct {
+ left: *Hittable, // TODO
+ right: *Hittable, // TODO
+ box: Aabb,
+
+ fn init(
+ objects: ArrayList(*Hittable),
+ start: usize,
+ end: usize,
+ time0: f64,
+ time1: f64,
+ ) BvhNode {
+ var left: *Hittable = undefined;
+ var right: *Hittable = undefined;
+
+ var objects_ = objects;
+ const axis = randomInt(u8, 0, 3);
+
+ const objectSpan = end - start;
+ if (objectSpan == 1) {
+ left = objects.items[start];
+ right = objects.items[start];
+ } else if (objectSpan == 2) {
+ if (BvhNode.boxCompare(axis, objects[start], objects[start + 1])) {
+ left = objects[start];
+ right = objects[start + 1];
+ } else {
+ left = objects[start + 1];
+ right = objects[start];
+ }
+ } else {
+ std.sort.sort(*Hittable, objects_, axis, BvhNode.boxCompare);
+ const mid = start + objectSpan / 2;
+ left.* = .{ .bvhNode = BvhNode.init(objects, start, mid, time0, time1) };
+ right.* = .{ .bvhNode = BvhNode.init(objects, mid, end, time0, time1) };
+ }
+
+ var boxLeft: Aabb = undefined;
+ var boxRight: Aabb = undefined;
+
+ if (!left.boudingBox(time0, time1, boxLeft) || !right.boudingBox(time0, time1, boxRight)) {
+ // ERROR
+ }
+
+ return .{
+ .left = left,
+ .right = right,
+ .box = Aabb.surroundingBox(boxLeft, boxRight),
+ };
+ }
+
+ fn boxCompare(axis: u8, a: *const Hittable, b: *const Hittable) bool {
+ if (axis == 0) {
+ return a.x < b.x;
+ } else if (axis == 1) {
+ return a.y < b.y;
+ } else {
+ return a.z < b.z;
+ }
+ }
+
+ fn hit(node: BvhNode, r: Ray, tMin: f64, tMax: f64, record: *HitRecord) bool {
+ if (!node.box.hit(r, tMin, tMax)) {
+ return false;
+ }
+
+ const hitLeft = node.left.hit(r, tMin, tMax, record);
+ const hitRight = node.right.hit(r, tMin, if (hitLeft) record.t else tMax, record);
+
+ return hitLeft or hitRight;
+ }
+
+ fn boudingBox(node: BvhNode, time0: f64, time1: f64, outputBox: *Aabb) bool {
+ _ = time0;
+ _ = time1;
+ outputBox.* = node.box;
+ return true;
+ }
+
+ fn deinit(node: *const BvhNode, allocator: anytype) void {
+ _ = node;
+ _ = allocator;
+ }
+};
diff --git a/src/rtw/material.zig b/src/rtw/material.zig
new file mode 100644
index 0000000..87244ab
--- /dev/null
+++ b/src/rtw/material.zig
@@ -0,0 +1,100 @@
+const std = @import("std");
+const debug = std.debug;
+const math = std.math;
+
+const Ray = @import("ray.zig").Ray;
+const vec = @import("vec.zig");
+const Vec3 = vec.Vec3;
+const rgb = vec.rgb;
+const Color = vec.Color;
+const HitRecord = @import("hit_record.zig").HitRecord;
+const Texture = @import("texture.zig").Texture;
+const rand = @import("rand.zig");
+const Random = rand.Random;
+const randomPointInUnitSphere = rand.randomPointInUnitSphere;
+const randomUnitVector = rand.randomUnitVector;
+const randomReal01 = rand.randomReal01;
+
+const MaterialTag = enum {
+ diffuse,
+ metal,
+ dielectric,
+};
+
+pub const Material = union(MaterialTag) {
+ diffuse: DiffuseMaterial,
+ metal: MetalMaterial,
+ dielectric: DielectricMaterial,
+
+ pub fn scatter(mat: Material, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rng: Random) bool {
+ return switch (mat) {
+ MaterialTag.diffuse => |diffuse_mat| diffuse_mat.scatter(r_in, record, attenuation, scattered, rng),
+ MaterialTag.metal => |metal_mat| metal_mat.scatter(r_in, record, attenuation, scattered, rng),
+ MaterialTag.dielectric => |dielectric_mat| dielectric_mat.scatter(r_in, record, attenuation, scattered, rng),
+ };
+ }
+};
+
+pub const DiffuseMaterial = struct {
+ albedo: Texture,
+
+ fn scatter(mat: DiffuseMaterial, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rng: Random) bool {
+ var scatter_direction = record.normal.add(randomUnitVector(rng));
+ if (scatter_direction.near_zero()) {
+ scatter_direction = record.normal;
+ }
+ scattered.* = .{ .origin = record.p, .dir = scatter_direction, .time = r_in.time };
+ attenuation.* = mat.albedo.value(record.u, record.v, record.p);
+ return true;
+ }
+};
+
+pub const MetalMaterial = struct {
+ albedo: Color,
+ fuzz: f64,
+
+ fn scatter(mat: MetalMaterial, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rng: Random) bool {
+ debug.assert(mat.fuzz <= 1.0);
+ const reflected = reflect(r_in.dir.normalized(), record.normal);
+ scattered.* = .{ .origin = record.p, .dir = reflected.add(randomPointInUnitSphere(rng).mul(mat.fuzz)), .time = r_in.time };
+ attenuation.* = mat.albedo;
+ return reflected.dot(record.normal) > 0.0;
+ }
+};
+
+pub const DielectricMaterial = struct {
+ // index of refraction.
+ ir: f64,
+
+ fn scatter(mat: DielectricMaterial, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rng: Random) bool {
+ const refraction_ratio = if (record.front_face) 1.0 / mat.ir else mat.ir;
+ const unit_dir = r_in.dir.normalized();
+
+ const cos_theta = @min(unit_dir.mul(-1.0).dot(record.normal), 1.0);
+ const sin_theta = @sqrt(1.0 - cos_theta * cos_theta);
+ // sin(theta') = refraction_ratio * sin(theta) <= 1
+ const can_refract = refraction_ratio * sin_theta <= 1.0;
+
+ const dir = if (can_refract and reflectance(cos_theta, refraction_ratio) < randomReal01(rng)) refract(unit_dir, record.normal, refraction_ratio) else reflect(unit_dir, record.normal);
+ scattered.* = .{ .origin = record.p, .dir = dir, .time = r_in.time };
+ attenuation.* = rgb(1.0, 1.0, 1.0);
+ return true;
+ }
+
+ fn reflectance(cos: f64, refraction_idx: f64) f64 {
+ const r0 = (1.0 - refraction_idx) / (1.0 + refraction_idx);
+ const r1 = r0 * r0;
+ return r1 + (1.0 - r1) * math.pow(f64, 1.0 - cos, 5.0);
+ }
+};
+
+fn reflect(v: Vec3, n: Vec3) Vec3 {
+ return v.sub(n.mul(2 * v.dot(n)));
+}
+
+fn refract(uv: Vec3, n: Vec3, etai_over_etat: f64) Vec3 {
+ const cos_theta = @min(uv.mul(-1.0).dot(n), 1.0);
+ const r_out_perpendicular = uv.add(n.mul(cos_theta)).mul(etai_over_etat);
+ const r_out_parallel = n.mul(-@sqrt(@fabs(1.0 - r_out_perpendicular.normSquared())));
+ return r_out_perpendicular.add(r_out_parallel);
+}
diff --git a/src/rtw/rand.zig b/src/rtw/rand.zig
new file mode 100644
index 0000000..47a8d61
--- /dev/null
+++ b/src/rtw/rand.zig
@@ -0,0 +1,40 @@
+const std = @import("std");
+
+const Vec3 = @import("vec.zig").Vec3;
+
+pub const Random = std.rand.Random;
+
+// [min, max)
+pub fn randomInt(comptime T: type, rand: Random, min: T, max: T) T {
+ return rand.intRangeLessThan(T, min, max);
+}
+
+// [0, 1)
+pub fn randomReal01(rand: Random) f64 {
+ return rand.float(f64);
+}
+
+// [min, max)
+pub fn randomReal(rand: Random, min: f64, max: f64) f64 {
+ return min + randomReal01(rand) * (max - min);
+}
+
+pub fn randomPointInUnitSphere(rand: Random) Vec3 {
+ while (true) {
+ const p = Vec3.random(rand, -1.0, 1.0);
+ if (p.norm() >= 1) continue;
+ return p;
+ }
+}
+
+pub fn randomPointInUnitDisk(rand: Random) Vec3 {
+ while (true) {
+ const p = Vec3{ .x = randomReal(rand, -1.0, 1.0), .y = randomReal(rand, -1.0, 1.0), .z = 0.0 };
+ if (p.norm() >= 1) continue;
+ return p;
+ }
+}
+
+pub fn randomUnitVector(rand: Random) Vec3 {
+ return randomPointInUnitSphere(rand).normalized();
+}
diff --git a/src/rtw/ray.zig b/src/rtw/ray.zig
new file mode 100644
index 0000000..e46198f
--- /dev/null
+++ b/src/rtw/ray.zig
@@ -0,0 +1,13 @@
+const vec = @import("vec.zig");
+const Vec3 = vec.Vec3;
+const Point3 = vec.Point3;
+
+pub const Ray = struct {
+ origin: Vec3,
+ dir: Vec3,
+ time: f64,
+
+ pub fn at(r: Ray, t: f64) Point3 {
+ return r.origin.add(r.dir.mul(t));
+ }
+};
diff --git a/src/rtw/texture.zig b/src/rtw/texture.zig
new file mode 100644
index 0000000..47c3c4f
--- /dev/null
+++ b/src/rtw/texture.zig
@@ -0,0 +1,72 @@
+const std = @import("std");
+
+const Color = @import("vec.zig").Color;
+const Vec3 = @import("vec.zig").Vec3;
+
+const TextureTag = enum {
+ solid,
+ checker,
+};
+
+pub const Texture = union(TextureTag) {
+ solid: SolidTexture,
+ checker: CheckerTexture,
+
+ pub fn makeSolid(color: Color) Texture {
+ return .{ .solid = .{ .color = color } };
+ }
+
+ pub fn makeChecker(allocator: std.mem.Allocator, odd: Color, even: Color) !Texture {
+ return .{ .checker = try CheckerTexture.init(
+ allocator,
+ Texture.makeSolid(odd),
+ Texture.makeSolid(even),
+ ) };
+ }
+
+ pub fn value(tx: Texture, u: f64, v: f64, p: Vec3) Color {
+ return switch (tx) {
+ TextureTag.solid => |solidTx| solidTx.value(u, v, p),
+ TextureTag.checker => |checkerTx| checkerTx.value(u, v, p),
+ };
+ }
+};
+
+pub const SolidTexture = struct {
+ color: Color,
+
+ fn value(tx: SolidTexture, u: f64, v: f64, p: Vec3) Color {
+ _ = u;
+ _ = v;
+ _ = p;
+ return tx.color;
+ }
+};
+
+pub const CheckerTexture = struct {
+ allocator: std.mem.Allocator,
+ odd: *const Texture,
+ even: *const Texture,
+
+ fn init(allocator: std.mem.Allocator, odd: Texture, even: Texture) !CheckerTexture {
+ var odd_ = try allocator.create(Texture);
+ var even_ = try allocator.create(Texture);
+ odd_.* = odd;
+ even_.* = even;
+ return .{
+ .allocator = allocator,
+ .odd = odd_,
+ .even = even_,
+ };
+ }
+
+ fn deinit(tx: CheckerTexture) void {
+ tx.allocator.destroy(tx.even);
+ tx.allocator.destroy(tx.odd);
+ }
+
+ fn value(tx: CheckerTexture, u: f64, v: f64, p: Vec3) Color {
+ const sines = @sin(10 * p.x) * @sin(10 * p.y) * @sin(10 * p.z);
+ return if (sines < 0) tx.odd.value(u, v, p) else tx.even.value(u, v, p);
+ }
+};
diff --git a/src/rtw/vec.zig b/src/rtw/vec.zig
new file mode 100644
index 0000000..9b2c3f0
--- /dev/null
+++ b/src/rtw/vec.zig
@@ -0,0 +1,109 @@
+const std = @import("std");
+
+const rand = @import("rand.zig");
+const Random = rand.Random;
+const randomReal = rand.randomReal;
+const randomReal01 = rand.randomReal01;
+
+pub const Vec3 = struct {
+ x: f64,
+ y: f64,
+ z: f64,
+
+ pub fn norm(v: Vec3) f64 {
+ return @sqrt(v.normSquared());
+ }
+
+ pub fn normSquared(v: Vec3) f64 {
+ return v.x * v.x + v.y * v.y + v.z * v.z;
+ }
+
+ pub fn dot(u: Vec3, v: Vec3) f64 {
+ return u.x * v.x + u.y * v.y + u.z * v.z;
+ }
+
+ pub fn cross(u: Vec3, v: Vec3) Vec3 {
+ return Vec3{
+ .x = u.y * v.z - u.z * v.y,
+ .y = u.z * v.x - u.x * v.z,
+ .z = u.x * v.y - u.y * v.x,
+ };
+ }
+
+ pub fn normalized(v: Vec3) Vec3 {
+ const n = v.norm();
+ if (n == 0.0) {
+ return v;
+ } else {
+ return v.div(n);
+ }
+ }
+
+ pub fn add(u: Vec3, v: Vec3) Vec3 {
+ return Vec3{
+ .x = u.x + v.x,
+ .y = u.y + v.y,
+ .z = u.z + v.z,
+ };
+ }
+
+ pub fn sub(u: Vec3, v: Vec3) Vec3 {
+ return Vec3{
+ .x = u.x - v.x,
+ .y = u.y - v.y,
+ .z = u.z - v.z,
+ };
+ }
+
+ pub fn mul(v: Vec3, t: f64) Vec3 {
+ return Vec3{
+ .x = v.x * t,
+ .y = v.y * t,
+ .z = v.z * t,
+ };
+ }
+
+ pub fn mulV(u: Vec3, v: Vec3) Vec3 {
+ return Vec3{
+ .x = u.x * v.x,
+ .y = u.y * v.y,
+ .z = u.z * v.z,
+ };
+ }
+
+ pub fn div(v: Vec3, t: f64) Vec3 {
+ return Vec3{
+ .x = v.x / t,
+ .y = v.y / t,
+ .z = v.z / t,
+ };
+ }
+
+ pub fn random01(rng: Random) Vec3 {
+ return Vec3{
+ .x = randomReal01(rng),
+ .y = randomReal01(rng),
+ .z = randomReal01(rng),
+ };
+ }
+
+ pub fn random(rng: Random, min: f64, max: f64) Vec3 {
+ return Vec3{
+ .x = randomReal(rng, min, max),
+ .y = randomReal(rng, min, max),
+ .z = randomReal(rng, min, max),
+ };
+ }
+
+ pub fn near_zero(v: Vec3) bool {
+ const epsilon = 1e-8;
+ return @fabs(v.x) < epsilon and @fabs(v.y) < epsilon and @fabs(v.z) < epsilon;
+ }
+};
+
+pub const Point3 = Vec3;
+pub const Color = Vec3;
+
+pub fn rgb(r: f64, g: f64, b: f64) Color {
+ return .{ .x = r, .y = g, .z = b };
+}