diff options
| author | nsfisis <nsfisis@gmail.com> | 2022-12-07 22:37:14 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2022-12-07 22:54:47 +0900 |
| commit | 86914985b77ddf5d6d3a3dd6c13deed9d906a471 (patch) | |
| tree | a523883d6524d55aee713bfd6400f044f8715d52 /src | |
| parent | e264078ed9d94b5091a6d78f8128496ac49fbde4 (diff) | |
| download | RayTracingInOneWeekend.zig-86914985b77ddf5d6d3a3dd6c13deed9d906a471.tar.gz RayTracingInOneWeekend.zig-86914985b77ddf5d6d3a3dd6c13deed9d906a471.tar.zst RayTracingInOneWeekend.zig-86914985b77ddf5d6d3a3dd6c13deed9d906a471.zip | |
refactor: separate single main.zig
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.zig | 765 | ||||
| -rw-r--r-- | src/rtw.zig | 8 | ||||
| -rw-r--r-- | src/rtw/aabb.zig | 61 | ||||
| -rw-r--r-- | src/rtw/hit_record.zig | 21 | ||||
| -rw-r--r-- | src/rtw/hittable.zig | 335 | ||||
| -rw-r--r-- | src/rtw/material.zig | 100 | ||||
| -rw-r--r-- | src/rtw/rand.zig | 40 | ||||
| -rw-r--r-- | src/rtw/ray.zig | 13 | ||||
| -rw-r--r-- | src/rtw/texture.zig | 72 | ||||
| -rw-r--r-- | src/rtw/vec.zig | 109 |
10 files changed, 815 insertions, 709 deletions
diff --git a/src/main.zig b/src/main.zig index eb537a8..e0e5702 100644 --- a/src/main.zig +++ b/src/main.zig @@ -3,422 +3,38 @@ const debug = std.debug; const math = std.math; const ArrayList = std.ArrayList; -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(rand: std.rand.Random) Vec3 { - return Vec3{ - .x = randomReal01(rand), - .y = randomReal01(rand), - .z = randomReal01(rand), - }; - } - - pub fn random(rand: std.rand.Random, min: f64, max: f64) Vec3 { - return Vec3{ - .x = randomReal(rand, min, max), - .y = randomReal(rand, min, max), - .z = randomReal(rand, min, max), - }; - } - - // for debugging - pub fn pp(v: Vec3) void { - debug.print("{} {} {}\n", .{ v.x, v.y, v.z }); - } - - 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; - } -}; - -fn randomPointInUnitSphere(rand: std.rand.Random) Vec3 { - while (true) { - const p = Vec3.random(rand, -1.0, 1.0); - if (p.norm() >= 1) continue; - return p; - } -} - -fn randomPointInUnitDisk(rand: std.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; - } -} - -fn randomUnitVector(rand: std.rand.Random) Vec3 { - return randomPointInUnitSphere(rand).normalized(); -} - -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); -} - -const Point3 = Vec3; -const Color = Vec3; - -fn rgb(r: f64, g: f64, b: f64) Color { - return .{ .x = r, .y = g, .z = b }; -} - -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)); - } -}; - -const MaterialTag = enum { - diffuse, - metal, - dielectric, -}; - -const Material = union(MaterialTag) { - diffuse: DiffuseMaterial, - metal: MetalMaterial, - dielectric: DielectricMaterial, - - fn scatter(mat: Material, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rand: std.rand.Random) bool { - return switch (mat) { - MaterialTag.diffuse => |diffuse_mat| diffuse_mat.scatter(r_in, record, attenuation, scattered, rand), - MaterialTag.metal => |metal_mat| metal_mat.scatter(r_in, record, attenuation, scattered, rand), - MaterialTag.dielectric => |dielectric_mat| dielectric_mat.scatter(r_in, record, attenuation, scattered, rand), - }; - } -}; - -const DiffuseMaterial = struct { - albedo: Texture, - - fn scatter(mat: DiffuseMaterial, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rand: std.rand.Random) bool { - var scatter_direction = record.normal.add(randomUnitVector(rand)); - 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; - } -}; - -const MetalMaterial = struct { - albedo: Color, - fuzz: f64, - - fn scatter(mat: MetalMaterial, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rand: std.rand.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(rand).mul(mat.fuzz)), .time = r_in.time }; - attenuation.* = mat.albedo; - return reflected.dot(record.normal) > 0.0; - } -}; - -const DielectricMaterial = struct { - // index of refraction. - ir: f64, - - fn scatter(mat: DielectricMaterial, r_in: Ray, record: HitRecord, attenuation: *Color, scattered: *Ray, rand: std.rand.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(rand)) 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); - } -}; - -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, -}; - -const HittableTag = enum { - sphere, - movingSphere, - list, - bvhNode, -}; - -const Hittable = union(HittableTag) { - sphere: Sphere, - movingSphere: MovingSphere, - list: HittableList, - bvhNode: BvhNode, - - 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), - }; - } - - 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.* = 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 rtw = @import("rtw.zig"); + +const Ray = rtw.ray.Ray; +const Vec3 = rtw.vec.Vec3; +const Point3 = rtw.vec.Point3; +const Color = rtw.vec.Color; +const rgb = rtw.vec.rgb; +const Random = rtw.rand.Random; +const randomPointInUnitSphere = rtw.rand.randomPointInUnitSphere; +const randomPointInUnitDisk = rtw.rand.randomPointInUnitDisk; +const randomUnitVector = rtw.rand.randomUnitVector; +const randomInt = rtw.rand.randomInt; +const randomReal01 = rtw.rand.randomReal01; +const randomReal = rtw.rand.randomReal; + +const Material = rtw.material.Material; +const DiffuseMaterial = rtw.material.DiffuseMaterial; +const MetalMaterial = rtw.material.MetalMaterial; +const DielectricMaterial = rtw.material.DielectricMaterial; + +const Texture = rtw.texture.Texture; +const SolidTexture = rtw.texture.SolidTexture; +const CheckerTexture = rtw.texture.CheckerTexture; + +const Hittable = rtw.hittable.Hittable; +const Sphere = rtw.hittable.Sphere; +const MovingSphere = rtw.hittable.MovingSphere; +const HittableList = rtw.hittable.HittableList; +const BvhNode = rtw.hittable.BvhNode; + +const Aabb = rtw.aabb.Aabb; +const HitRecord = rtw.hit_record.HitRecord; fn makeSphere(center: Point3, radius: f64, material: *const Material) Hittable { return .{ @@ -430,48 +46,6 @@ fn makeSphere(center: Point3, radius: f64, material: *const Material) Hittable { }; } -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 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 inf = math.inf(f64); const pi = math.pi; @@ -479,233 +53,6 @@ fn deg2rad(degree: f64) f64 { return degree * pi / 180.0; } -// [min, max) -fn randomInt(comptime T: type, rand: std.rand.Random, min: T, max: T) T { - return rand.intRangeLessThan(T, min, max); -} - -// [0, 1) -fn randomReal01(rand: std.rand.Random) f64 { - return rand.float(f64); -} - -// [min, max) -fn randomReal(rand: std.rand.Random, min: f64, max: f64) f64 { - return min + randomReal01(rand) * (max - min); -} - -const Aabb = struct { - min: Point3, - max: Point3, - - 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; - } -}; - -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), - }, - }; -} - -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 = 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; - } -}; - -const TextureTag = enum { - solid, - checker, -}; - -const Texture = union(TextureTag) { - solid: SolidTexture, - checker: CheckerTexture, - - fn makeSolid(color: Color) Texture { - return .{ .solid = .{ .color = color } }; - } - - fn makeChecker(allocator: std.mem.Allocator, odd: Color, even: Color) !Texture { - return .{ .checker = try CheckerTexture.init( - allocator, - Texture.makeSolid(odd), - Texture.makeSolid(even), - ) }; - } - - 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), - }; - } -}; - -const SolidTexture = struct { - color: Color, - - fn value(tx: SolidTexture, u: f64, v: f64, p: Vec3) Color { - _ = u; - _ = v; - _ = p; - return tx.color; - } -}; - -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); - } -}; - const Camera = struct { origin: Point3, horizontal: Vec3, @@ -757,19 +104,19 @@ const Camera = struct { }; } - fn getRay(camera: Camera, rand: std.rand.Random, s: f64, t: f64) Ray { - const rd = randomPointInUnitDisk(rand).mul(camera.lens_radius); + fn getRay(camera: Camera, rng: Random, s: f64, t: f64) Ray { + const rd = randomPointInUnitDisk(rng).mul(camera.lens_radius); const offset = camera.u.mul(rd.x).add(camera.v.mul(rd.y)); const dir = camera.lower_left_corner.add(camera.horizontal.mul(s)).add(camera.vertical.mul(t)).sub(camera.origin).sub(offset); return .{ .origin = camera.origin.add(offset), .dir = dir, - .time = randomReal(rand, camera.time0, camera.time1), + .time = randomReal(rng, camera.time0, camera.time1), }; } }; -fn rayColor(r: Ray, world: Hittable, rand: std.rand.Random, depth: u32) Color { +fn rayColor(r: Ray, world: Hittable, rng: Random, depth: u32) Color { var rec: HitRecord = undefined; if (depth == 0) { // If we've exceeded the ray bounce limit, no more ligth is gathered. @@ -778,8 +125,8 @@ fn rayColor(r: Ray, world: Hittable, rand: std.rand.Random, depth: u32) Color { if (world.hit(r, 0.001, inf, &rec)) { var scattered: Ray = undefined; var attenuation: Color = undefined; - if (rec.material.scatter(r, rec, &attenuation, &scattered, rand)) { - return attenuation.mulV(rayColor(scattered, world, rand, depth - 1)); + if (rec.material.scatter(r, rec, &attenuation, &scattered, rng)) { + return attenuation.mulV(rayColor(scattered, world, rng, depth - 1)); } else { return rgb(0.0, 0.0, 0.0); } @@ -798,8 +145,8 @@ fn writeColor(out: anytype, c: Color, samples_per_pixel: u32) !void { }); } -fn generateTwoSpheres(rand: std.rand.Random, allocator: anytype) !Hittable { - _ = rand; +fn generateTwoSpheres(rng: Random, allocator: anytype) !Hittable { + _ = rng; var hittable_objects = ArrayList(Hittable).init(allocator); const checker = try Texture.makeChecker(allocator, rgb(0.2, 0.3, 0.1), rgb(0.9, 0.9, 0.9)); @@ -815,7 +162,7 @@ fn generateTwoSpheres(rand: std.rand.Random, allocator: anytype) !Hittable { return .{ .list = .{ .objects = hittable_objects } }; } -fn generateRandomScene(rand: std.rand.Random, allocator: anytype) !Hittable { +fn generateRandomScene(rng: Random, allocator: anytype) !Hittable { var hittable_objects = ArrayList(Hittable).init(allocator); var mat_ground = try allocator.create(Material); @@ -839,11 +186,11 @@ fn generateRandomScene(rand: std.rand.Random, allocator: anytype) !Hittable { while (a < 3) : (a += 1) { var b: i32 = -3; while (b < 3) : (b += 1) { - const choose_mat = randomReal01(rand); + const choose_mat = randomReal01(rng); const center = Point3{ - .x = @intToFloat(f64, a) + 0.9 * randomReal01(rand), + .x = @intToFloat(f64, a) + 0.9 * randomReal01(rng), .y = 0.2, - .z = @intToFloat(f64, b) + 0.9 * randomReal01(rand), + .z = @intToFloat(f64, b) + 0.9 * randomReal01(rng), }; if (center.sub(.{ .x = 4, .y = 0.2, .z = 0 }).norm() <= 0.9) { @@ -853,9 +200,9 @@ fn generateRandomScene(rand: std.rand.Random, allocator: anytype) !Hittable { var mat_sphere = try allocator.create(Material); if (choose_mat < 0.8) { // diffuse - const albedo = Color.random01(rand).mulV(Color.random01(rand)); + const albedo = Color.random01(rng).mulV(Color.random01(rng)); mat_sphere.* = .{ .diffuse = .{ .albedo = .{ .solid = .{ .color = albedo } } } }; - const center1 = center.add(.{ .x = 0, .y = randomReal(rand, 0, 0.5), .z = 0 }); + const center1 = center.add(.{ .x = 0, .y = randomReal(rng, 0, 0.5), .z = 0 }); try hittable_objects.append(.{ .movingSphere = .{ .center0 = center, .center1 = center1, @@ -866,8 +213,8 @@ fn generateRandomScene(rand: std.rand.Random, allocator: anytype) !Hittable { } }); } else if (choose_mat < 0.95) { // metal - const albedo = Color.random(rand, 0.5, 1); - const fuzz = randomReal(rand, 0, 0.5); + const albedo = Color.random(rng, 0.5, 1); + const fuzz = randomReal(rng, 0, 0.5); mat_sphere.* = .{ .metal = .{ .albedo = albedo, .fuzz = fuzz } }; try hittable_objects.append(makeSphere(center, 0.2, mat_sphere)); } else { @@ -886,8 +233,8 @@ pub fn main() !void { const allocator = gpa.allocator(); defer debug.assert(!gpa.deinit()); - var rng = std.rand.DefaultPrng.init(42); - var rand = rng.random(); + var rng_ = std.rand.DefaultPrng.init(42); + var rng = rng_.random(); // Image const aspect_ratio = 3.0 / 2.0; @@ -906,13 +253,13 @@ pub fn main() !void { var aperture: f64 = 0; if (scene == 1) { - world = try generateRandomScene(rand, allocator); + world = try generateRandomScene(rng, allocator); lookFrom = .{ .x = 13, .y = 2, .z = 3 }; lookAt = .{ .x = 0, .y = 0, .z = 0 }; vFov = 20.0; aperture = 0.1; } else if (scene == 2) { - world = try generateTwoSpheres(rand, allocator); + world = try generateTwoSpheres(rng, allocator); lookFrom = .{ .x = 13, .y = 2, .z = 3 }; lookAt = .{ .x = 0, .y = 0, .z = 0 }; vFov = 20.0; @@ -947,10 +294,10 @@ pub fn main() !void { var s: u32 = 0; var pixelColor = rgb(0.0, 0.0, 0.0); while (s < samples_per_pixel) : (s += 1) { - const u = (@intToFloat(f64, i) + randomReal01(rand)) / (image_width - 1); - const v = (@intToFloat(f64, j) + randomReal01(rand)) / (image_height - 1); - const r = camera.getRay(rand, u, v); - pixelColor = pixelColor.add(rayColor(r, world, rand, max_depth)); + const u = (@intToFloat(f64, i) + randomReal01(rng)) / (image_width - 1); + const v = (@intToFloat(f64, j) + randomReal01(rng)) / (image_height - 1); + const r = camera.getRay(rng, u, v); + pixelColor = pixelColor.add(rayColor(r, world, rng, max_depth)); } try writeColor(stdout, pixelColor, samples_per_pixel); } diff --git a/src/rtw.zig b/src/rtw.zig new file mode 100644 index 0000000..7dd9c93 --- /dev/null +++ b/src/rtw.zig @@ -0,0 +1,8 @@ +pub const aabb = @import("rtw/aabb.zig"); +pub const hit_record = @import("rtw/hit_record.zig"); +pub const hittable = @import("rtw/hittable.zig"); +pub const material = @import("rtw/material.zig"); +pub const rand = @import("rtw/rand.zig"); +pub const ray = @import("rtw/ray.zig"); +pub const texture = @import("rtw/texture.zig"); +pub const vec = @import("rtw/vec.zig"); 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 }; +} |
