From c21982d2b3891671ec523c99a3f442bc96e83d0d Mon Sep 17 00:00:00 2001 From: Nathan Bourgeois Date: Sat, 18 May 2024 13:38:46 -0400 Subject: [PATCH] Raytracer Demo (#65) --- showcase/build.zig | 1 + showcase/build.zig.zon | 1 + showcase/carts/raytracer/build.zig | 19 +++ showcase/carts/raytracer/build.zig.zon | 12 ++ showcase/carts/raytracer/src/camera.zig | 153 +++++++++++++++++++ showcase/carts/raytracer/src/hit.zig | 116 ++++++++++++++ showcase/carts/raytracer/src/interval.zig | 32 ++++ showcase/carts/raytracer/src/main.zig | 96 ++++++++++++ showcase/carts/raytracer/src/material.zig | 130 ++++++++++++++++ showcase/carts/raytracer/src/ppm.zig | 52 +++++++ showcase/carts/raytracer/src/ray.zig | 18 +++ showcase/carts/raytracer/src/vec.zig | 176 ++++++++++++++++++++++ 12 files changed, 806 insertions(+) create mode 100644 showcase/carts/raytracer/build.zig create mode 100644 showcase/carts/raytracer/build.zig.zon create mode 100644 showcase/carts/raytracer/src/camera.zig create mode 100644 showcase/carts/raytracer/src/hit.zig create mode 100644 showcase/carts/raytracer/src/interval.zig create mode 100644 showcase/carts/raytracer/src/main.zig create mode 100644 showcase/carts/raytracer/src/material.zig create mode 100644 showcase/carts/raytracer/src/ppm.zig create mode 100644 showcase/carts/raytracer/src/ray.zig create mode 100644 showcase/carts/raytracer/src/vec.zig diff --git a/showcase/build.zig b/showcase/build.zig index 0748f39..9c95f92 100644 --- a/showcase/build.zig +++ b/showcase/build.zig @@ -8,6 +8,7 @@ const carts = .{ .{ "blobs", @import("blobs") }, .{ "plasma", @import("plasma") }, .{ "metalgear_timer", @import("metalgear_timer") }, + .{ "raytracer", @import("raytracer") }, .{ "neopixelpuzzle", @import("neopixelpuzzle") }, }; diff --git a/showcase/build.zig.zon b/showcase/build.zig.zon index 77c4e8b..e6d9d0c 100644 --- a/showcase/build.zig.zon +++ b/showcase/build.zig.zon @@ -14,6 +14,7 @@ .blobs = .{ .path = "carts/blobs" }, .plasma = .{ .path = "carts/plasma" }, .metalgear_timer = .{ .path = "carts/metalgear-timer" }, + .raytracer = .{ .path = "carts/raytracer" }, .neopixelpuzzle = .{ .path = "carts/neopixelpuzzle" }, }, .paths = .{ diff --git a/showcase/carts/raytracer/build.zig b/showcase/carts/raytracer/build.zig new file mode 100644 index 0000000..4a0fca0 --- /dev/null +++ b/showcase/carts/raytracer/build.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const sycl_badge = @import("sycl_badge"); + +pub const author_name = "Nathan Bourgeois"; +pub const author_handle = "iridescentrose"; +pub const cart_title = "Raytracer"; +pub const description = "Raytracing in One Weekend raytracer on badge!"; + +pub fn build(b: *std.Build) void { + const optimize = b.standardOptimizeOption(.{}); + const sycl_badge_dep = b.dependency("sycl_badge", .{}); + + const cart = sycl_badge.add_cart(sycl_badge_dep, b, .{ + .name = "raytracer", + .optimize = optimize, + .root_source_file = b.path("src/main.zig"), + }); + cart.install(b); +} diff --git a/showcase/carts/raytracer/build.zig.zon b/showcase/carts/raytracer/build.zig.zon new file mode 100644 index 0000000..4da583c --- /dev/null +++ b/showcase/carts/raytracer/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = "raytracer", + .version = "0.0.0", + .dependencies = .{ + .sycl_badge = .{ .path = "../../.." }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/showcase/carts/raytracer/src/camera.zig b/showcase/carts/raytracer/src/camera.zig new file mode 100644 index 0000000..fd8a967 --- /dev/null +++ b/showcase/carts/raytracer/src/camera.zig @@ -0,0 +1,153 @@ +const Camera = @This(); +const vec = @import("vec.zig"); +const Point3 = vec.Point3; +const Vec3 = vec.Vec3; +const Color3 = vec.Color3; +const std = @import("std"); +const Ray = @import("ray.zig"); +const hit = @import("hit.zig"); +const HittableList = hit.HittableList; +const HitRecord = hit.HitRecord; +const Interval = @import("interval.zig"); +const cart = @import("cart-api"); + +const aspect_ratio = 5.0 / 4.0; +const image_width: usize = 160; +const image_height: usize = @intFromFloat(@as(f32, @floatFromInt(image_width)) / aspect_ratio); + +const theta: f32 = vfov / 180.0 * std.math.pi; +const h: f32 = std.math.tan(theta / 2.0); +const viewport_height: f32 = 2 * h * focus_distance; +const viewport_width: f32 = viewport_height * @as(f32, @floatFromInt(image_width)) / @as(f32, @floatFromInt(image_height)); +const vfov: f32 = 20; +const lookat = Point3.init(0, 0, 0); +const vup = Vec3.init(0, 1, 0); +const defocus_angle: f32 = 0.6; +const focus_distance: f32 = 10.0; + +lookfrom: Vec3, +pixel00_location: Vec3, +pixel_delta_u: Vec3, +pixel_delta_v: Vec3, +camera_center: Vec3, +samples: usize, +max_children: usize, +rand_engine: std.rand.DefaultPrng, +random: std.rand.Random, +u: Vec3, +v: Vec3, +w: Vec3, +defocus_disk_u: Vec3, +defocus_disk_v: Vec3, + +pub fn init(position: Vec3) Camera { + var self: Camera = undefined; + self.lookfrom = position; + self.camera_center = self.lookfrom; + + self.w = self.lookfrom.sub(lookat).unit_vector(); + self.u = vup.cross(self.w).unit_vector(); + self.v = self.w.cross(self.u); + + var viewport_u = self.u.mul_scalar(viewport_width); + var viewport_v = self.v.mul_scalar(-viewport_height); + + self.pixel_delta_u = viewport_u.div_scalar(@floatFromInt(image_width)); + self.pixel_delta_v = viewport_v.div_scalar(@floatFromInt(image_height)); + + self.rand_engine = std.rand.DefaultPrng.init(0); + self.random = self.rand_engine.random(); + self.samples = 25; + self.max_children = 7; + + var viewport_upper_left = self.camera_center.sub(self.w.mul_scalar(focus_distance)).sub(viewport_u.div_scalar(2.0)).sub(viewport_v.div_scalar(2.0)); + self.pixel00_location = viewport_upper_left.add(self.pixel_delta_u.add(self.pixel_delta_v).mul_scalar(0.5)); + + const defocus_radians = (defocus_angle / 2.0) / 180.0 * std.math.pi; + const defocus_radius = focus_distance * @tan(defocus_radians); + + self.defocus_disk_u = self.u.mul_scalar(defocus_radius); + self.defocus_disk_v = self.v.mul_scalar(defocus_radius); + + return self; +} + +fn pixel_sample_square(self: *Camera) Vec3 { + const x = -0.5 + self.random.float(f32); + const y = -0.5 + self.random.float(f32); + + return self.pixel_delta_u.mul_scalar(x).add(self.pixel_delta_v.mul_scalar(y)); +} + +fn defocus_disk_sample(self: *Camera) Vec3 { + const p = Vec3.random_in_unit_disk(self.random); + return self.camera_center.add(self.defocus_disk_u.mul_scalar(p.data[0])).add(self.defocus_disk_v.mul_scalar(p.data[1])); +} + +pub fn get_ray(self: *Camera, i: u32, j: u32) Ray { + var pixel_center = self.pixel00_location.add(self.pixel_delta_u.mul_scalar(@floatFromInt(i))).add(self.pixel_delta_v.mul_scalar(@floatFromInt(j))); + var pixel_sample = pixel_center.add(self.pixel_sample_square()); + + const ray_origin = if (defocus_angle <= 0) self.camera_center else self.defocus_disk_sample(); + const ray_direction = pixel_sample.sub(ray_origin); + + return Ray.init(ray_origin, ray_direction); +} + +pub fn render(self: *Camera, world: *HittableList) !void { + var j: u32 = 0; + while (j < image_height) : (j += 1) { + //std.debug.print("Scanlines Remaining: {}\n", .{image_height - j}); + + var i: u32 = 0; + while (i < image_width) : (i += 1) { + var col = Color3.zero(); + + for (0..self.samples) |_| { + const ray = self.get_ray(i, j); + _ = col.addEq(self.ray_color(ray, self.max_children, world)); + } + + const samples: f32 = @floatFromInt(self.samples); + col = col.div_scalar(samples); + + const index: usize = i + j * image_width; + const color = vec.color3_to_color(col); + cart.framebuffer[index] = .{ + .r = @truncate(color.rgb.r >> 3), + .g = @truncate(color.rgb.g >> 2), + .b = @truncate(color.rgb.b >> 3), + }; + } + } + //std.debug.print("Done!\n", .{}); +} + +fn ray_color(self: *Camera, ray: Ray, depth: usize, world: *HittableList) Color3 { + var hit_record: HitRecord = undefined; + + if (depth == 0) { + return Color3.zero(); + } + + if (world.hit(ray, Interval{ .min = 0.001, .max = std.math.inf(f32) }, &hit_record)) { + var scattered: Ray = undefined; + var attenuation: Color3 = undefined; + + const mat = hit_record.mat; + if (mat.vtable.scatter(mat.ptr, self.random, ray, hit_record, &attenuation, &scattered)) { + var col = self.ray_color(scattered, depth - 1, world); + return col.mul(attenuation); + } + + return Color3.zero(); + } + + var dir = ray.direction.unit_vector(); + const a = 0.5 * (dir.y() + 1.0); + + const white = Color3.init(1.0, 1.0, 1.0); + const blue = Color3.init(0.5, 0.7, 1.0); + + return white.mul_scalar(1.0 - a).add(blue.mul_scalar(a)); +} diff --git a/showcase/carts/raytracer/src/hit.zig b/showcase/carts/raytracer/src/hit.zig new file mode 100644 index 0000000..a134402 --- /dev/null +++ b/showcase/carts/raytracer/src/hit.zig @@ -0,0 +1,116 @@ +const vec = @import("vec.zig"); +const Vec3 = vec.Vec3; +const Point3 = vec.Point3; +const Interval = @import("interval.zig"); +const Ray = @import("ray.zig"); +const std = @import("std"); +const material = @import("material.zig"); +const Material = material.Material; + +pub const HitRecord = struct { + point: Point3, + normal: Vec3, + t: f32, + front_face: bool, + mat: Material, + + pub fn set_face_normal(self: *HitRecord, ray: Ray, outward_normal: Vec3) void { + self.front_face = ray.direction.dot(outward_normal) < 0; + self.normal = if (self.front_face) outward_normal else outward_normal.negate(); + } +}; + +pub const Hittable = struct { + ptr: *anyopaque, + vtable: *const VTable, + + pub const VTable = struct { + hit: *const fn (ctx: *anyopaque, ray: Ray, ray_t: Interval, hit_record: *HitRecord, ret_addr: usize) bool, + }; + + pub fn rawHit(self: Hittable, ray: Ray, ray_t: Interval, hit_record: *HitRecord, ret_addr: usize) bool { + return self.vtable.hit(self.ptr, ray, ray_t, hit_record, ret_addr); + } +}; + +pub const Sphere = struct { + center: Point3, + radius: f32, + mat: Material, + + pub fn init(center: Point3, radius: f32, mat: Material) Sphere { + return Sphere{ .center = center, .radius = radius, .mat = mat }; + } + + pub fn hit(ctx: *anyopaque, ray: Ray, ray_t: Interval, hit_record: *HitRecord, ret_addr: usize) bool { + _ = ret_addr; + const self: *Sphere = @ptrCast(@alignCast(ctx)); + + const oc = ray.origin.sub(self.center); + const a = ray.direction.length_squared(); + const half_b = oc.dot(ray.direction); + const c = oc.length_squared() - self.radius * self.radius; + + const discriminant = half_b * half_b - a * c; + if (discriminant < 0) + return false; + + const sqrtd = std.math.sqrt(discriminant); + + var root = (-half_b - sqrtd) / a; + + if (!ray_t.surrounds(root)) { + root = (-half_b + sqrtd) / a; + if (!ray_t.surrounds(root)) + return false; + } + + hit_record.t = root; + hit_record.point = ray.at(root); + const outward_normal = hit_record.point.sub(self.center).div_scalar(self.radius); + hit_record.set_face_normal(ray, outward_normal); + hit_record.mat = self.mat; + + return true; + } + + pub fn hittable(self: *Sphere) Hittable { + return .{ + .ptr = self, + .vtable = &.{ + .hit = hit, + }, + }; + } +}; + +pub const HittableList = struct { + objects: std.ArrayList(Hittable), + + pub fn init(allocator: std.mem.Allocator) !HittableList { + var self: HittableList = undefined; + self.objects = std.ArrayList(Hittable).init(allocator); + return self; + } + + pub fn hit(self: *HittableList, ray: Ray, ray_t: Interval, hit_record: *HitRecord) bool { + var record: HitRecord = undefined; + var hit_anything: bool = false; + var closest_so_far = ray_t.max; + + for (self.objects.items) |item| { + const is_hit = item.vtable.hit(item.ptr, ray, ray_t, &record, @returnAddress()); + + if (is_hit) { + // Make sure record is only set if it's closer + if (record.t < closest_so_far) { + hit_anything = true; + closest_so_far = record.t; + hit_record.* = record; + } + } + } + + return hit_anything; + } +}; diff --git a/showcase/carts/raytracer/src/interval.zig b/showcase/carts/raytracer/src/interval.zig new file mode 100644 index 0000000..76fa8c7 --- /dev/null +++ b/showcase/carts/raytracer/src/interval.zig @@ -0,0 +1,32 @@ +const std = @import("std"); + +const Interval = @This(); + +max: f32, +min: f32, + +pub fn empty() Interval { + return .{ .min = std.math.inf(f32), .max = -std.math.inf(f32) }; +} + +pub fn universe() Interval { + return .{ .min = -std.math.inf(f32), .max = std.math.inf(f32) }; +} + +pub fn contains(self: *const Interval, x: f32) bool { + return self.min <= x and x <= self.max; +} + +pub fn surrounds(self: *const Interval, x: f32) bool { + return self.min < x and x < self.max; +} + +pub fn clamp(self: *const Interval, x: f32) f32 { + if (x < self.min) { + return self.min; + } else if (x > self.max) { + return self.max; + } + + return x; +} diff --git a/showcase/carts/raytracer/src/main.zig b/showcase/carts/raytracer/src/main.zig new file mode 100644 index 0000000..13313b8 --- /dev/null +++ b/showcase/carts/raytracer/src/main.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const hittable = @import("hit.zig"); +const vec = @import("vec.zig"); +const Vec3 = vec.Vec3; +const HittableList = hittable.HittableList; +const Sphere = hittable.Sphere; +const Camera = @import("camera.zig"); +const material = @import("material.zig"); +const LambertianMaterial = material.LambertianMaterial; +const MetalMaterial = material.MetalMaterial; +const DialectricMaterial = material.DialectricMaterial; +const Material = material.Material; +const Interval = @import("interval.zig"); + +var angle: f32 = 0; + +export fn start() void { + angle = 0; +} + +export fn update() void { + angle += 15.0; + main_program() catch return; +} + +var buffer = [_]u8{0} ** 16384; +pub fn main_program() !void { + // Allocator + + var gpa = std.heap.FixedBufferAllocator.init(&buffer); + const allocator = gpa.allocator(); + + var world = try HittableList.init(allocator); + var ground_mat = LambertianMaterial.init(Vec3.init(0.5, 0.5, 0.5)); + var ground = Sphere.init(Vec3.init(0, -1000, 0), 1000, ground_mat.material()); + try world.objects.append(ground.hittable()); + + // Camera + const radius = 10.0; + const angle_rad = angle / 180.0 * std.math.pi; + const x = radius * std.math.sin(angle_rad); + const y = radius * std.math.cos(angle_rad); + + var camera = Camera.init(Vec3.init(x, 2, y)); + + var a: i32 = -5; + while (a < 5) : (a += 1) { + var b: i32 = -5; + while (b < 5) : (b += 1) { + const mat_choose: f32 = camera.random.float(f32); + + var center = Vec3.init(@as(f32, @floatFromInt(a)) + 0.9 * camera.random.float(f32), 0.2, @as(f32, @floatFromInt(b)) + 0.9 * camera.random.float(f32)); + + if (center.sub(Vec3.init(4, 0.2, 0)).length() > 0.9) { + var materials: Material = undefined; + + if (mat_choose < 0.8) { + const color = Vec3.random(camera.random); + var mat = try allocator.create(LambertianMaterial); + mat.* = LambertianMaterial.init(color); + materials = mat.material(); + } else if (mat_choose < 0.95) { + const color = Vec3.random_interval(camera.random, Interval{ .min = 0.5, .max = 1.0 }); + const fuzz = camera.random.float(f32) * 0.5; + + var mat = try allocator.create(MetalMaterial); + mat.* = MetalMaterial.init(color, fuzz); + materials = mat.material(); + } else { + var mat = try allocator.create(DialectricMaterial); + mat.* = DialectricMaterial.init(1.5); + materials = mat.material(); + } + + var sphere = try allocator.create(Sphere); + sphere.* = Sphere.init(center, 0.2, materials); + + try world.objects.append(sphere.hittable()); + } + } + } + + var material1 = DialectricMaterial.init(1.5); + var material2 = LambertianMaterial.init(Vec3.init(0.4, 0.2, 0.1)); + var material3 = MetalMaterial.init(Vec3.init(0.7, 0.6, 0.5), 0.0); + + var sphere1 = Sphere.init(Vec3.init(0, 0.75, 0), 0.75, material1.material()); + var sphere2 = Sphere.init(Vec3.init(-2.5, 0.75, 0), 0.75, material2.material()); + var sphere3 = Sphere.init(Vec3.init(2.5, 0.75, 0), 0.75, material3.material()); + + try world.objects.append(sphere1.hittable()); + try world.objects.append(sphere2.hittable()); + try world.objects.append(sphere3.hittable()); + + try camera.render(&world); +} diff --git a/showcase/carts/raytracer/src/material.zig b/showcase/carts/raytracer/src/material.zig new file mode 100644 index 0000000..4badd54 --- /dev/null +++ b/showcase/carts/raytracer/src/material.zig @@ -0,0 +1,130 @@ +const std = @import("std"); +const Ray = @import("ray.zig"); +const hit = @import("hit.zig"); +const HitRecord = hit.HitRecord; +const vec = @import("vec.zig"); +const Color3 = vec.Color3; +const Vec3 = vec.Vec3; + +pub const Material = struct { + ptr: *anyopaque, + vtable: VTable, + const VTable = struct { + scatter: *const fn (ctx: *anyopaque, random_engine: std.rand.Random, ray_in: Ray, record: HitRecord, attenuation: *Color3, scattered: *Ray) bool, + }; +}; + +pub const LambertianMaterial = struct { + albedo: Color3, + + pub fn init(col: Color3) LambertianMaterial { + return LambertianMaterial{ + .albedo = col, + }; + } + + pub fn scatter(ctx: *anyopaque, random_engine: std.rand.Random, ray_in: Ray, record: HitRecord, attenuation: *Color3, ray_out: *Ray) bool { + _ = ray_in; + + const self: *LambertianMaterial = @ptrCast(@alignCast(ctx)); + + var scatter_dir = record.normal.add(Vec3.random_in_unit_sphere(random_engine)); + + if (scatter_dir.near_zero()) + scatter_dir = record.normal; + + ray_out.* = Ray{ .origin = record.point, .direction = scatter_dir }; + attenuation.* = self.albedo; + + return true; + } + + pub fn material(self: *LambertianMaterial) Material { + return .{ + .ptr = self, + .vtable = .{ + .scatter = scatter, + }, + }; + } +}; + +pub const MetalMaterial = struct { + albedo: Color3, + fuzz: f32, + + pub fn init(col: Color3, f: f32) MetalMaterial { + return MetalMaterial{ + .albedo = col, + .fuzz = if (f < 1) f else 1.0, + }; + } + + pub fn scatter(ctx: *anyopaque, random_engine: std.rand.Random, ray_in: Ray, record: HitRecord, attenuation: *Color3, ray_out: *Ray) bool { + const self: *MetalMaterial = @ptrCast(@alignCast(ctx)); + + var reflected = ray_in.direction.unit_vector().reflect(record.normal); + ray_out.* = Ray{ .origin = record.point, .direction = reflected.add(Vec3.random_in_unit_sphere(random_engine).mul_scalar(self.fuzz)) }; + attenuation.* = self.albedo; + + return true; + } + + pub fn material(self: *MetalMaterial) Material { + return .{ + .ptr = self, + .vtable = .{ + .scatter = scatter, + }, + }; + } +}; + +pub fn reflectance(cosine: f32, ref_idx: f32) f32 { + var r0 = (1 - ref_idx) / (1 + ref_idx); + r0 = r0 * r0; + + return r0 + (1 - r0) * std.math.pow(f32, (1 - cosine), 5); +} + +pub const DialectricMaterial = struct { + ir: f32, + + pub fn init(refraction: f32) DialectricMaterial { + return DialectricMaterial{ .ir = refraction }; + } + + pub fn scatter(ctx: *anyopaque, random_engine: std.rand.Random, ray_in: Ray, record: HitRecord, attenuation: *Color3, scattered: *Ray) bool { + const self: *DialectricMaterial = @ptrCast(@alignCast(ctx)); + + attenuation.* = Color3.init(1.0, 1.0, 1.0); + const refraction_ratio = if (record.front_face) 1.0 / self.ir else self.ir; + + const unit_direciton = ray_in.direction.unit_vector(); + + const cos_theta = @min(unit_direciton.negate().dot(record.normal), 1.0); + const sin_theta = std.math.sqrt(1.0 - cos_theta * cos_theta); + + const cannot_refract = refraction_ratio * sin_theta > 1.0; + var direction: Vec3 = undefined; + + if (cannot_refract or reflectance(cos_theta, refraction_ratio) > random_engine.float(f32)) { + direction = unit_direciton.reflect(record.normal); + } else { + direction = unit_direciton.refract(record.normal, refraction_ratio); + } + + scattered.* = Ray{ .origin = record.point, .direction = direction }; + + return true; + } + + pub fn material(self: *DialectricMaterial) Material { + return .{ + .ptr = self, + .vtable = .{ + .scatter = scatter, + }, + }; + } +}; diff --git a/showcase/carts/raytracer/src/ppm.zig b/showcase/carts/raytracer/src/ppm.zig new file mode 100644 index 0000000..77ad492 --- /dev/null +++ b/showcase/carts/raytracer/src/ppm.zig @@ -0,0 +1,52 @@ +const std = @import("std"); + +pub const RGB = packed struct { + r: u8, + g: u8, + b: u8, +}; + +pub const Color = packed union { + value: u24, + rgb: RGB, +}; + +// pub const PPM = struct { +// width: usize, +// height: usize, +// data: []Color, +// allocator: std.mem.Allocator, + +// pub fn init(allocator: std.mem.Allocator, width: usize, height: usize) !PPM { +// const self = PPM{ +// .width = width, +// .height = height, +// .data = try allocator.alloc(Color, width * height), +// .allocator = allocator, +// }; + +// return self; +// } + +// pub fn deinit(self: *PPM) void { +// self.allocator.free(self.data); +// } + +// // pub fn save_to_file(self: *PPM, filename: []const u8) !void { +// // var file = try std.fs.cwd().createFile(filename, .{}); +// // defer file.close(); +// // errdefer file.close(); + +// // const fwriter = file.writer(); +// // var bufferedWriter = std.io.bufferedWriter(fwriter); +// // var bwriter = bufferedWriter.writer(); + +// // try bwriter.print("P3\n{} {}\n255\n", .{ self.width, self.height }); + +// // for (self.data) |pixel| { +// // try bwriter.print("{} {} {}\n", .{ pixel.rgb.r, pixel.rgb.g, pixel.rgb.b }); +// // } + +// // try bufferedWriter.flush(); +// // } +// }; diff --git a/showcase/carts/raytracer/src/ray.zig b/showcase/carts/raytracer/src/ray.zig new file mode 100644 index 0000000..4adacaf --- /dev/null +++ b/showcase/carts/raytracer/src/ray.zig @@ -0,0 +1,18 @@ +const vec = @import("vec.zig"); +const Vec3 = vec.Vec3; + +const Ray = @This(); + +origin: Vec3, +direction: Vec3, + +pub fn init(origin: Vec3, direction: Vec3) Ray { + return Ray{ + .origin = origin, + .direction = direction, + }; +} + +pub fn at(self: *const Ray, t: f32) Vec3 { + return self.origin.add(self.direction.mul_scalar(t)); +} diff --git a/showcase/carts/raytracer/src/vec.zig b/showcase/carts/raytracer/src/vec.zig new file mode 100644 index 0000000..63d7bbf --- /dev/null +++ b/showcase/carts/raytracer/src/vec.zig @@ -0,0 +1,176 @@ +const std = @import("std"); +const ppm = @import("ppm.zig"); +const Color = ppm.Color; +const RGB = ppm.RGB; +const Interval = @import("interval.zig"); + +pub const Vec3 = struct { + data: @Vector(3, f32), + + pub fn init(xd: f32, yd: f32, zd: f32) Vec3 { + var self = Vec3{ .data = @Vector(3, f32){ 0.0, 0.0, 0.0 } }; + + self.data[0] = xd; + self.data[1] = yd; + self.data[2] = zd; + + return self; + } + + pub fn zero() Vec3 { + return Vec3{ .data = @Vector(3, f32){ 0.0, 0.0, 0.0 } }; + } + + pub fn x(self: *const Vec3) f32 { + return self.data[0]; + } + + pub fn y(self: *const Vec3) f32 { + return self.data[1]; + } + + pub fn z(self: *const Vec3) f32 { + return self.data[2]; + } + + pub fn negate(self: *const Vec3) Vec3 { + return Vec3{ .data = -self.data }; + } + + pub fn addEq(self: *Vec3, v: Vec3) *Vec3 { + self.data += v.data; + return self; + } + + pub fn mulEq(self: *Vec3, scalar: f32) *Vec3 { + self.data *= @splat(scalar); + return self; + } + + pub fn divEq(self: *Vec3, scalar: f32) *Vec3 { + self.data /= @splat(scalar); + return self; + } + + pub fn length(self: *const Vec3) f32 { + return std.math.sqrt(self.length_squared()); + } + + pub fn length_squared(self: *const Vec3) f32 { + return self.data[0] * self.data[0] + self.data[1] * self.data[1] + self.data[2] * self.data[2]; + } + + pub fn add(u: *const Vec3, v: Vec3) Vec3 { + return Vec3{ .data = u.data + v.data }; + } + + pub fn sub(u: *const Vec3, v: Vec3) Vec3 { + return Vec3{ .data = u.data - v.data }; + } + + pub fn mul(u: *const Vec3, v: Vec3) Vec3 { + return Vec3{ .data = u.data * v.data }; + } + + pub fn mul_scalar(u: *const Vec3, v: f32) Vec3 { + return Vec3{ .data = u.data * @as(@Vector(3, f32), @splat(v)) }; + } + + pub fn div_scalar(u: *const Vec3, v: f32) Vec3 { + return Vec3{ .data = u.data / @as(@Vector(3, f32), @splat(v)) }; + } + + pub fn dot(u: *const Vec3, v: Vec3) f32 { + const res: f32 = @reduce(.Add, u.data * v.data); + return res; + } + + pub fn cross(u: *const Vec3, v: Vec3) Vec3 { + const xd: f32 = u.data[1] * v.data[2] - u.data[2] * v.data[1]; + const yd: f32 = u.data[2] * v.data[0] - u.data[0] * v.data[2]; + const zd: f32 = u.data[0] * v.data[1] - u.data[1] * v.data[0]; + + return init(xd, yd, zd); + } + + pub fn unit_vector(u: *const Vec3) Vec3 { + return u.div_scalar(u.length()); + } + + pub fn random(random_engine: std.rand.Random) Vec3 { + return Vec3.init(random_engine.float(f32), random_engine.float(f32), random_engine.float(f32)); + } + + pub fn random_interval(random_engine: std.rand.Random, interval: Interval) Vec3 { + var vec = Vec3.zero(); + vec.data[0] = interval.min + (interval.max - interval.min) * random_engine.float(f32); + vec.data[1] = interval.min + (interval.max - interval.min) * random_engine.float(f32); + vec.data[2] = interval.min + (interval.max - interval.min) * random_engine.float(f32); + + return vec; + } + + pub fn random_in_sphere(random_engine: std.rand.Random) Vec3 { + while (true) { + var p = Vec3.random_interval(random_engine, .{ .min = -1, .max = 1 }); + + if (p.length_squared() < 1) + return p; + } + } + + pub fn random_in_unit_sphere(random_engine: std.rand.Random) Vec3 { + return Vec3.random_in_sphere(random_engine).unit_vector(); + } + + pub fn random_on_hemisphere(random_engine: std.rand.Random, normal: Vec3) Vec3 { + var on_unit_sphere = Vec3.random_in_unit_sphere(random_engine); + + if (on_unit_sphere.dot(normal) > 0.0) { + return on_unit_sphere; + } else { + return on_unit_sphere.negate(); + } + } + + pub fn near_zero(v: *const Vec3) bool { + const s = 1e-8; + + return (@abs(v.data[0]) < s) and (@abs(v.data[1]) < s) and (@abs(v.data[2]) < s); + } + + pub fn reflect(v: *const Vec3, n: Vec3) Vec3 { + const d = v.dot(n) * 2.0; + return v.sub(n.mul_scalar(d)); + } + + pub fn refract(uv: *const Vec3, n: Vec3, etai_over_etat: f32) Vec3 { + const cos_theta = @min(uv.negate().dot(n), 1.0); + + const r_out_perp = (uv.add(n.mul_scalar(cos_theta))).mul_scalar(etai_over_etat); + const r_out_parallel = n.mul_scalar(-std.math.sqrt(@abs(1.0 - r_out_perp.length_squared()))); + + return r_out_perp.add(r_out_parallel); + } + + pub fn random_in_unit_disk(random_engine: std.rand.Random) Vec3 { + while (true) { + var p = Vec3.zero(); + p.data[0] = -1 + 2.0 * random_engine.float(f32); + p.data[1] = -1 + 2.0 * random_engine.float(f32); + + if (p.length_squared() < 1) + return p; + } + } +}; +pub const Point3 = Vec3; +pub const Color3 = Vec3; + +pub fn color3_to_color(c: Color3) Color { + return Color{ .rgb = RGB{ + .r = @intFromFloat(std.math.sqrt(c.x()) * 255.99), + .g = @intFromFloat(std.math.sqrt(c.y()) * 255.99), + .b = @intFromFloat(std.math.sqrt(c.z()) * 255.99), + } }; +}