diff --git a/build.zig.zon b/build.zig.zon index f2ffc48..ce773a9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,7 +1,8 @@ .{ .name = "rayray", .version = "0.1.0", - .minimum_zig_version = "0.12.0-dev.2631+3069669bc", + .zig_version = "v0.13.0-dev.3+dddddcffd", + .minimum_zig_version = "0.13.0-dev.3+dddddcffd", .dependencies = .{ // libs folder .zmath = .{ .path = "libs/zmath" }, diff --git a/src/AABB.zig b/src/AABB.zig new file mode 100644 index 0000000..f66063a --- /dev/null +++ b/src/AABB.zig @@ -0,0 +1,84 @@ +const zm = @import("zmath"); + +const HitRecord = @import("hittable.zig").HitRecord; +const Interval = @import("interval.zig").IntervalF32; +const Ray = @import("Ray.zig"); + +const AABB = @This(); + +x: Interval = Interval.empty, +y: Interval = Interval.empty, +z: Interval = Interval.empty, + +pub fn init(x: Interval, y: Interval, z: Interval) AABB { + return AABB{ .x = x, .y = y, .z = z }; +} + +pub fn initP(a: zm.Vec, b: zm.Vec) AABB { + // Treat the two points a and b as extrema for the bounding box, so we don't require a + // particular minimum/maximum coordinate order. + return AABB{ + .x = blk: { + if (a[0] <= b[0]) break :blk Interval.init(a[0], b[0]) else break :blk Interval.init(b[0], a[0]); + }, + .y = blk: { + if (a[1] <= b[1]) break :blk Interval.init(a[1], b[1]) else break :blk Interval.init(b[1], a[1]); + }, + .z = blk: { + if (a[2] <= b[2]) break :blk Interval.init(a[2], b[2]) else break :blk Interval.init(b[2], a[2]); + }, + }; +} + +pub fn initAB(a: *const AABB, b: *const AABB) AABB { + return AABB{ + .x = Interval.initI(a.x, b.x), + .y = Interval.initI(a.y, b.y), + .z = Interval.initI(a.z, b.z), + }; +} + +pub fn axisInterval(self: *const AABB, n: i32) Interval { + if (n == 1) return self.y; + if (n == 2) return self.z; + return self.x; +} + +pub fn hit(self: *AABB, r: *Ray, ray_t: Interval) bool { + const ray_orig = r.orig; + const ray_dir = r.dir; + + var t = ray_t; + + for (0..3) |axis| { + const ax = self.axisInterval(@intCast(axis)); + const adinv = 1.0 / ray_dir[axis]; + + const t0 = (ax.min - ray_orig[axis]) * adinv; + const t1 = (ax.max - ray_orig[axis]) * adinv; + + if (t0 < t1) { + if (t0 > t.min) t.min = t0; + if (t1 < t.max) t.max = t1; + } else { + if (t1 > t.min) t.min = t1; + if (t0 < t.max) t.max = t0; + } + + if (ray_t.max <= ray_t.min) return false; + } + + return true; +} + +pub fn longestAxis(self: *AABB) i32 { + if (self.x.size() > self.y.size()) { + if (self.x.size() > self.z.size()) { + return 0; + } else return 2; + } else { + if (self.y.size() > self.z.size()) { + return 1; + } else return 2; + } +} diff --git a/src/hittable.zig b/src/hittable.zig index 88f4393..ba6d849 100644 --- a/src/hittable.zig +++ b/src/hittable.zig @@ -2,12 +2,14 @@ const std = @import("std"); const zm = @import("zmath"); +const AABB = @import("AABB.zig"); const IntervalF32 = @import("interval.zig").IntervalF32; const Material = @import("material.zig").Material; const Ray = @import("Ray.zig"); // Hittable Objects pub const Sphere = @import("hittable/Sphere.zig"); +pub const BVH = @import("hittable/BVH.zig"); pub const HitRecord = struct { p: zm.Vec, @@ -24,14 +26,33 @@ pub const HitRecord = struct { pub const Hittable = union(enum) { sphere: Sphere, + bvh_node: BVH, pub fn sphere(s: Sphere) Hittable { + // std.log.info("created sphere with mat: {}", .{s.mat}); return .{ .sphere = s }; } + pub fn bvh(b: BVH) Hittable { + return .{ .bvh_node = b }; + } + + pub fn boundingBox(self: *Hittable) AABB { + switch (self.*) { + .sphere => |*s| return s.boundingBox(), + .bvh_node => |*s| return s.boundingBox(), + } + } + pub fn hit(self: *Hittable, r: *Ray, ray_t: IntervalF32) ?HitRecord { switch (self.*) { .sphere => |*s| { + std.log.debug("try to hit Sphere: {}", .{s}); + // std.log.info("hitting sphere with mat: {}", .{s.mat}); + return s.hit(r, ray_t); + }, + .bvh_node => |*s| { + // std.log.debug("try to hit BVH", .{}); return s.hit(r, ray_t); }, } @@ -42,6 +63,7 @@ pub const Hittable = union(enum) { pub const HittableList = struct { list: std.ArrayList(Hittable), + bbox: AABB = AABB{}, pub fn init(allocator: std.mem.Allocator) HittableList { const list = std.ArrayList(Hittable).init(allocator); @@ -49,12 +71,23 @@ pub const HittableList = struct { return .{ .list = list }; } + pub fn initH(allocator: std.mem.Allocator, item: Hittable) !HittableList { + var list = std.ArrayList(Hittable).init(allocator); + try list.append(item); + return .{ .list = list, .bbox = AABB.initAB(&AABB{}, &(@constCast(&item).boundingBox())) }; + } + pub fn deinit(self: *HittableList) void { self.list.deinit(); } pub fn add(self: *HittableList, item: Hittable) !void { try self.list.append(item); + self.bbox = AABB.initAB(&self.bbox, &(@constCast(&item).boundingBox())); + } + + pub fn boundingBox(self: *HittableList) AABB { + return self.bbox; } pub fn hit(self: *HittableList, r: *Ray, ray_t: IntervalF32) ?HitRecord { diff --git a/src/hittable/BVH.zig b/src/hittable/BVH.zig new file mode 100644 index 0000000..c65aebe --- /dev/null +++ b/src/hittable/BVH.zig @@ -0,0 +1,106 @@ +const std = @import("std"); + +const AABB = @import("../AABB.zig"); +const hittable = @import("../hittable.zig"); +const Hittable = hittable.Hittable; +const HitRecord = hittable.HitRecord; +const IntervalF32 = @import("../interval.zig").IntervalF32; +const Ray = @import("../Ray.zig"); +const util = @import("../util.zig"); + +pub const BVH = @This(); + +objects: *hittable.HittableList, +left: *Hittable, +right: *Hittable, +bbox: AABB, + +pub fn initL(objects: *hittable.HittableList) BVH { + std.log.info("starting to create BVH", .{}); + return BVH.init(objects, 0, objects.list.items.len); +} + +pub fn init(objects: *hittable.HittableList, start: usize, end: usize) BVH { + const list = objects.list.items; + var bbox = AABB{}; + for (start..end) |idx| { + bbox = AABB.initAB(&bbox, &list[idx].boundingBox()); + } + + const axis = bbox.longestAxis(); + + // const comparator = blk: { + // if (axis == 0) { + // break :blk &boxXCompare; + // } else if (axis == 1) { + // break :blk &boxYCompare; + // } + // break :blk &boxZCompare; + // }; + + const object_span = end - start; + + var left = &list[start]; + var right = &list[start]; + if (object_span == 2) { + left = &list[start]; + right = &list[start + 1]; + } else if (object_span > 2) { + std.log.debug("BVH.init axis={} start={} end={}", .{axis, start, end}); + if (axis == 0) { + // break :blk&boxXCompare; + std.mem.sort(Hittable, list, .{}, boxXCompare); + } else if (axis == 1) { + // break :blk &boxYCompare; + std.mem.sort(Hittable, list, .{}, boxYCompare); + } else { + // break :blk &boxZCompare; + std.mem.sort(Hittable, list, .{}, boxZCompare); + } + // std.mem.sort(Hittable, list, null, comparator); + + const mid = start + object_span / 2; + left = @constCast(&Hittable.bvh(BVH.init(objects, start, mid))); + right = @constCast(&Hittable.bvh(BVH.init(objects, mid, end))); + } + + std.log.info("BVH created", .{}); + + return .{ + .objects = objects, + .left = left, + .right = right, + .bbox = bbox, + }; +} + +pub fn hit(self: *BVH, r: *Ray, ray_t: IntervalF32) ?HitRecord { + if (!self.bbox.hit(r, ray_t)) { + return null; + } + + if (self.left.hit(r, ray_t)) |rec| return rec; + if (self.right.hit(r, ray_t)) |rec| return rec; + return null; +} + +pub fn boundingBox(self: *BVH) AABB { + return self.bbox; +} + +fn boxCompare(a: *Hittable, b: *Hittable, axis_index: i32) bool { + const a_axis_interval = a.boundingBox().axisInterval(axis_index); + const b_axis_interval = b.boundingBox().axisInterval(axis_index); + return a_axis_interval.min < b_axis_interval.min; +} + +fn boxXCompare(_: @TypeOf(.{}), a: Hittable, b: Hittable) bool { + return boxCompare(@constCast(&a), @constCast(&b), 0); +} + +fn boxYCompare(_: @TypeOf(.{}), a: Hittable, b: Hittable) bool { + return boxCompare(@constCast(&a), @constCast(&b), 1); +} +fn boxZCompare(_: @TypeOf(.{}), a: Hittable, b: Hittable) bool { + return boxCompare(@constCast(&a), @constCast(&b), 2); +} diff --git a/src/hittable/sphere.zig b/src/hittable/sphere.zig index f9d19e1..1da5de0 100644 --- a/src/hittable/sphere.zig +++ b/src/hittable/sphere.zig @@ -1,5 +1,6 @@ const zm = @import("zmath"); +const AABB = @import("../AABB.zig"); const IntervalF32 = @import("../interval.zig").IntervalF32; const Ray = @import("../Ray.zig"); const HitRecord = @import("../hittable.zig").HitRecord; @@ -12,17 +13,33 @@ radius: f32, mat: *Material, is_moving: bool = false, center_vec: zm.Vec = zm.f32x4s(0), +bbox: ?AABB = null, -pub fn initMoving(center1: zm.Vec, center2: zm.Vec, radius: f32, mat: *Material) Sphere { - return .{ +pub fn initMoving(center1: zm.Vec, center2: zm.Vec, radius: f32, mat: Material) Sphere { + const rvec = zm.f32x4s(radius); + const box1 = AABB.initP(center1 - rvec, center1 + rvec); + const box2 = AABB.initP(center2 - rvec, center2 + rvec); + + return Sphere{ .center = center1, .radius = @max(0, radius), .mat = mat, .is_moving = true, .center_vec = center2 - center1, + .bbox = AABB.initAB(&box1, &box2), }; } +pub fn boundingBox(self: *Sphere) AABB { + if (self.bbox) |bbox| { + return bbox; + } else { + const rvec = zm.f32x4s(self.radius); + self.bbox = AABB.initP(self.center - rvec, self.center + rvec); + return self.bbox.?; + } +} + pub fn hit(self: *Sphere, r: *Ray, ray_t: IntervalF32) ?HitRecord { const center = blk: { if (self.is_moving) { diff --git a/src/interval.zig b/src/interval.zig index b80c420..5170a0f 100644 --- a/src/interval.zig +++ b/src/interval.zig @@ -82,6 +82,10 @@ fn IntInterval(comptime T: type) type { return .{ .min = min, .max = max }; } + pub fn initI(a: Self, b: Self) Self { + return Self{ .min = @min(a.min, b.min), .max = @max(a.max, b.max) }; + } + pub fn contains(self: *const Self, x: T) bool { return self.min <= x and x <= self.max; } @@ -101,6 +105,10 @@ fn IntInterval(comptime T: type) type { return .{ .min = self.min - padding, .max = self.max + padding }; } + pub fn size(self: *const Self) T { + return self.max - self.min; + } + pub fn iter(self: *const Self) Iterator { return Iterator{ .interval = self.*, @@ -121,7 +129,11 @@ fn FloatInterval(comptime T: type) type { max: T, pub fn init(min: T, max: T) Self { - return .{ .min = min, .max = max }; + return Self{ .min = min, .max = max }; + } + + pub fn initI(a: Self, b: Self) Self { + return Self{ .min = @min(a.min, b.min), .max = @max(a.max, b.max) }; } pub fn contains(self: *const Self, x: T) bool { @@ -138,9 +150,13 @@ fn FloatInterval(comptime T: type) type { return x; } - pub fn expand(self: *const Self, delta: T) Interval { + pub fn expand(self: *const Self, delta: T) Self { const padding = delta / 2; return .{ .min = self.min - padding, .max = self.max + padding }; } + + pub fn size(self: *const Self) T { + return self.max - self.min; + } }; } diff --git a/src/main.zig b/src/main.zig index 30ecf19..21c2b7c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -10,6 +10,8 @@ const HittableList = rayray.hittable.HittableList; const Material = rayray.material.Material; const Sphere = rayray.hittable.Sphere; +const scences = @import("scences.zig"); + pub const std_options = .{ .log_level = .debug, .logFn = aa.log.logFn, @@ -27,60 +29,15 @@ pub fn main() !void { defer spall.deinit_thread(); // Setting up the world - var material_ground = Material.lambertian(zm.f32x4(0.5, 0.5, 0.5, 1.0)); - - var world = HittableList.init(allocator); - try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(0, -1000, 0, 0), .radius = 1000, .mat = &material_ground })); - - var a: isize = -11; - while (a < 11) : (a += 1) { - var b: isize = -11; - while (b < 11) : (b += 1) { - const choose_mat = rayray.util.randomF32(); - const center = zm.f32x4( - @as(f32, @floatFromInt(a)) + 0.9 * rayray.util.randomF32(), - 0.2, - @as(f32, @floatFromInt(b)) + 0.9 * rayray.util.randomF32(), - 0, - ); - - if (zm.length3(center - zm.f32x4(4, 0.2, 0, 0))[0] > 0.9) { - const material = try allocator.create(Material); - - if (choose_mat < 0.8) { - // diffuse - const albedo = rayray.util.randomVec3() * rayray.util.randomVec3() + zm.f32x4(0, 0, 0, 1); - material.* = Material.lambertian(albedo); - const center2 = center + zm.f32x4(0, rayray.util.randomF32M(0, 0.5), 0, 0); - try world.add(Hittable.sphere(Sphere.initMoving(center, center2, 0.2, material))); - } else if (choose_mat < 0.95) { - // metal - const albedo = rayray.util.randomVec3M(0.5, 1) + zm.f32x4(0, 0, 0, 1); - const fuzz = rayray.util.randomF32M(0, 0.5); - material.* = Material.metal(albedo, fuzz); - try world.add(Hittable.sphere(Sphere{ .center = center, .radius = 0.2, .mat = material })); - } else { - // glass - material.* = Material.dielectric(1.5); - try world.add(Hittable.sphere(Sphere{ .center = center, .radius = 0.2, .mat = material })); - } - } - } - } - - var material1 = Material.dielectric(1.5); - try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(0, 1, 0, 0), .radius = 1, .mat = &material1 })); - - var material2 = Material.lambertian(zm.f32x4(0.4, 0.2, 0.1, 1)); - try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(-4, 1, 0, 0), .radius = 1, .mat = &material2 })); - - var material3 = Material.metal(zm.f32x4(0.7, 0.6, 0.5, 1), 0); - try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(4, 1, 0, 0), .radius = 1, .mat = &material3 })); + var scence = try scences.inOneWeekend(allocator); + defer scence.deinit(); + + std.log.info("World created", .{}); const s = spall.trace(@src(), "Raytracer", .{}); // Raytracing part - var raytracer = try rayray.Raytracer.init(allocator, world, .{ + var raytracer = try rayray.Raytracer.init(allocator, scence.world, .{ .aspect_ratio = 16.0 / 9.0, .image_width = 400, .samples_per_pixel = 100, diff --git a/src/rayray.zig b/src/rayray.zig index 7cdd5b3..4b6b5ff 100644 --- a/src/rayray.zig +++ b/src/rayray.zig @@ -136,7 +136,7 @@ pub const Raytracer = struct { }; pub fn renderThread(ctx: tracer.Context, task: *TaskTracker, id: usize) void { + defer task.done.store(true, .release); _ = id; tracer.trace(ctx); - task.done.store(true, .release); } diff --git a/src/scences.zig b/src/scences.zig new file mode 100644 index 0000000..3351105 --- /dev/null +++ b/src/scences.zig @@ -0,0 +1 @@ +pub const inOneWeekend = @import("scences/in_one_weekend.zig").scene; diff --git a/src/scences/in_one_weekend.zig b/src/scences/in_one_weekend.zig new file mode 100644 index 0000000..f391479 --- /dev/null +++ b/src/scences/in_one_weekend.zig @@ -0,0 +1,76 @@ +const std = @import("std"); + +const zm = @import("zmath"); + +const rayray = @import("rayray"); +const Hittable = rayray.hittable.Hittable; +const HittableList = rayray.hittable.HittableList; +const Material = rayray.material.Material; +const Sphere = rayray.hittable.Sphere; +const BVH = rayray.hittable.BVH; + +world: HittableList, +allocator: std.mem.Allocator, + +pub fn scene(allocator: std.mem.Allocator) !@This() { + var world = HittableList.init(allocator); + + const material_ground = try allocator.create(Material); + material_ground.* = Material.lambertian(zm.f32x4(0.5, 0.5, 0.5, 1.0)); + try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(0, -1000, 0, 0), .radius = 1000, .mat = material_ground })); + + var a: isize = -11; + while (a < 11) : (a += 1) { + var b: isize = -11; + while (b < 11) : (b += 1) { + const choose_mat = rayray.util.randomF32(); + const center = zm.f32x4( + @as(f32, @floatFromInt(a)) + 0.9 * rayray.util.randomF32(), + 0.2, + @as(f32, @floatFromInt(b)) + 0.9 * rayray.util.randomF32(), + 0, + ); + + if (zm.length3(center - zm.f32x4(4, 0.2, 0, 0))[0] > 0.9) { + const material = try allocator.create(Material); + + if (choose_mat < 0.8) { + // diffuse + const albedo = rayray.util.randomVec3() * rayray.util.randomVec3() + zm.f32x4(0, 0, 0, 1); + material.* = Material.lambertian(albedo); + try world.add(Hittable.sphere(Sphere{ .center = center, .radius = 0.2, .mat = material })); + } else if (choose_mat < 0.95) { + // metal + const albedo = rayray.util.randomVec3M(0.5, 1) + zm.f32x4(0, 0, 0, 1); + const fuzz = rayray.util.randomF32M(0, 0.5); + material.* = Material.metal(albedo, fuzz); + try world.add(Hittable.sphere(Sphere{ .center = center, .radius = 0.2, .mat = material })); + } else { + // glass + material.* = Material.dielectric(1.5); + try world.add(Hittable.sphere(Sphere{ .center = center, .radius = 0.2, .mat = material })); + } + } + } + } + + const material1 = try allocator.create(Material); + material1.* = Material.dielectric(1.5); + try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(0, 1, 0, 0), .radius = 1, .mat = material1 })); + + const material2 = try allocator.create(Material); + material2.* = Material.lambertian(zm.f32x4(0.4, 0.2, 0.1, 1)); + try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(-4, 1, 0, 0), .radius = 1, .mat = material2 })); + + const material3 = try allocator.create(Material); + material3.* = Material.metal(zm.f32x4(0.7, 0.6, 0.5, 1), 0); + try world.add(Hittable.sphere(Sphere{ .center = zm.f32x4(4, 1, 0, 0), .radius = 1, .mat = material3 })); + + var world2 = HittableList.init(allocator); + try world2.add(Hittable.bvh(BVH.initL(&world))); + return .{ .allocator = allocator, .world = world2 }; +} + +pub fn deinit(self: *@This()) void { + self.world.deinit(); +} diff --git a/src/util.zig b/src/util.zig index 5785d9e..6e95709 100644 --- a/src/util.zig +++ b/src/util.zig @@ -17,6 +17,16 @@ pub inline fn randomF32M(min: f32, max: f32) f32 { return min + (max - min) * randomF32(); } +/// Returns a random real in [0,1). +pub inline fn randomI32() i32 { + return random.float(i32); +} + +/// Returns a random real in [min,max). +pub inline fn randomI32M(min: i32, max: i32) i32 { + return min + (max - min) * randomI32(); +} + pub inline fn randomVec2() zm.Vec { return zm.f32x4(randomF32(), randomF32(), 0, 0); }