From 7cf04511f9b6f906acb55770e4861522ec7f7c46 Mon Sep 17 00:00:00 2001 From: Robert Sheehy Date: Fri, 17 Oct 2025 10:20:09 -0500 Subject: [PATCH] Add fog and camera controls --- src/geometry/geometry.zig | 12 +++++-- src/geometry/repeat.zig | 18 ++++++++++ src/geometry/rotate.zig | 51 +++++++++++++++++++++----- src/geometry/transform.zig | 13 +++++++ src/hit.zig | 1 + src/main.zig | 73 ++++++++++++++++++++++++++++++-------- src/scene.zig | 25 ++++++++++--- src/shading.zig | 37 +++++++++++++------ 8 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 src/geometry/repeat.zig create mode 100644 src/geometry/transform.zig diff --git a/src/geometry/geometry.zig b/src/geometry/geometry.zig index ec1c7b1..4c42254 100644 --- a/src/geometry/geometry.zig +++ b/src/geometry/geometry.zig @@ -3,11 +3,15 @@ const math = @import("zlm").as(f64); const Box = @import("box.zig").Box; const Intersection = @import("intersection.zig").Intersection; const Octahedron = @import("octahedron.zig").Octahedron; -const Rotate = @import("rotate.zig").Rotate; +const Repeat = @import("repeat.zig").Repeat; +const RotateX = @import("rotate.zig").RotateX; +const RotateY = @import("rotate.zig").RotateY; +const RotateZ = @import("rotate.zig").RotateZ; const Scale = @import("scale.zig").Scale; const Sphere = @import("sphere.zig").Sphere; const Subtraction = @import("subtraction.zig").Subtraction; const Torus = @import("torus.zig").Torus; +const Transform = @import("transform.zig").Transform; const UnionSmooth = @import("union_smooth.zig").UnionSmooth; const UnionExact = @import("union_exact.zig").UnionExact; @@ -15,11 +19,15 @@ pub const Geometry = union(enum) { box: Box, intersection: Intersection, octahedron: Octahedron, - rotate: Rotate, + repeat: Repeat, + rotatex: RotateX, + rotatey: RotateY, + rotatez: RotateZ, scale: Scale, sphere: Sphere, subtraction: Subtraction, torus: Torus, + transform: Transform, union_smooth: UnionSmooth, union_exact: UnionExact, diff --git a/src/geometry/repeat.zig b/src/geometry/repeat.zig new file mode 100644 index 0000000..c46832b --- /dev/null +++ b/src/geometry/repeat.zig @@ -0,0 +1,18 @@ +const math = @import("zlm").as(f64); +const Geometry = @import("geometry.zig").Geometry; + +pub const Repeat = struct { + geometry: *const Geometry, + spacing: f64, + + const Self = @This(); + + pub fn distance(self: Self, point: math.Vec3) f64 { + const q = math.vec3( + @mod(point.x, self.spacing) - self.spacing / 2.0, + @mod(point.y, self.spacing) - self.spacing / 2.0, + @mod(point.z, self.spacing) - self.spacing / 2.0, + ); + return self.geometry.distance(q); + } +}; diff --git a/src/geometry/rotate.zig b/src/geometry/rotate.zig index 5527fc2..8708d26 100644 --- a/src/geometry/rotate.zig +++ b/src/geometry/rotate.zig @@ -1,20 +1,55 @@ const math = @import("zlm").as(f64); const Geometry = @import("geometry.zig").Geometry; -pub const Rotate = struct { - transformation: math.Mat4, +fn rotate_2d(angle: f64) math.Mat2 { + const s = @sin(angle); + const c = @cos(angle); + return math.Mat2{ + .fields = [2][2]f64{ + [2]f64{ c, -s }, + [2]f64{ s, c }, + }, + }; +} + +pub const RotateX = struct { + angle: f64, geometry: *const Geometry, const Self = @This(); - pub fn new(geometry: *const Geometry, axis: math.Vec3, angle: f64) Self { - return Self{ - .geometry = geometry, - .transformation = math.Mat4.createAngleAxis(axis, angle), - }; + pub fn distance(self: Self, point: math.Vec3) f64 { + const transformation = point.swizzle("yz").transform(rotate_2d(self.angle)); + return self.geometry.distance( + math.vec3(point.x, transformation.x, transformation.y), + ); } +}; + +pub const RotateY = struct { + angle: f64, + geometry: *const Geometry, + + const Self = @This(); + + pub fn distance(self: Self, point: math.Vec3) f64 { + const transformation = point.swizzle("xz").transform(rotate_2d(self.angle)); + return self.geometry.distance( + math.vec3(transformation.x, point.y, transformation.y), + ); + } +}; + +pub const RotateZ = struct { + angle: f64, + geometry: *const Geometry, + + const Self = @This(); pub fn distance(self: Self, point: math.Vec3) f64 { - return self.geometry.distance(point.transformPosition(self.transformation)); + const transformation = point.swizzle("xy").transform(rotate_2d(self.angle)); + return self.geometry.distance( + math.vec3(transformation.x, transformation.y, point.z), + ); } }; diff --git a/src/geometry/transform.zig b/src/geometry/transform.zig new file mode 100644 index 0000000..113594b --- /dev/null +++ b/src/geometry/transform.zig @@ -0,0 +1,13 @@ +const math = @import("zlm").as(f64); +const Geometry = @import("geometry.zig").Geometry; + +pub const Transform = struct { + matrix: math.Mat4, + geometry: *const Geometry, + + const Self = @This(); + + pub fn distance(self: Self, point: math.Vec3) f64 { + return self.geometry.distance(point.transformPosition(self.matrix)); + } +}; diff --git a/src/hit.zig b/src/hit.zig index 7f284a6..d4b0791 100644 --- a/src/hit.zig +++ b/src/hit.zig @@ -2,5 +2,6 @@ const math = @import("zlm").as(f64); pub const HitRecord = struct { normal: math.Vec3, + distance: f64, hit: bool, }; diff --git a/src/main.zig b/src/main.zig index 30cf5ad..71dcf06 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,14 +4,7 @@ const math = @import("zlm").as(f64); const math_usize = @import("zlm").as(usize); const Shading = @import("./shading.zig").Shading; - const Scene = @import("./scene.zig").Scene; -const Rotate = @import("./geometry/rotate.zig").Rotate; -const Torus = @import("./geometry/torus.zig").Torus; -const UnionSmooth = @import("./geometry/union_smooth.zig").UnionSmooth; -const Sphere = @import("./geometry/sphere.zig").Sphere; -const Scale = @import("./geometry/scale.zig").Scale; - const Geometry = @import("./geometry/geometry.zig").Geometry; const Event = union(enum) { @@ -46,17 +39,33 @@ pub fn main() !void { const gamma: f32 = 2.4; const light_position = math.vec3(1.0, -1.0, -1.0).normalize(); - const t1 = Geometry{ .torus = .{ .inner = 0.2, .outer = 1.0 } }; - const rt1 = Geometry{ .rotate = Rotate.new(&t1, math.vec3(0.0, 0.0, 1.0), 90.0) }; - const t2 = Geometry{ .torus = .{ .inner = 0.2, .outer = 1.0 } }; - const t3 = Geometry{ .union_smooth = .{ .a = &rt1, .b = &t2, .smooth = 0.3 } }; - const st3 = Geometry{ .scale = .{ .geometry = &t3, .amount = 1.2 } }; + const donut = Geometry{ .torus = .{ + .inner = 0.2, + .outer = 1.0, + } }; + const donut2 = Geometry{ .rotatez = .{ .geometry = &donut, .angle = 45.0 } }; + + const box = Geometry{ .box = .{ .dimensions = math.vec3(0.5, 0.5, 0.5) } }; + _ = box; + + const repeat = Geometry{ + .repeat = .{ .geometry = &donut, .spacing = 3.0 }, + }; + _ = repeat; + + const g = donut2; const GeometryScene = Scene(Geometry); const shading = Shading.new(light_position, gamma); var some_scene: ?GeometryScene = null; + const cam_right = math.vec3(1.0, 0.0, 0.0); + const cam_up = math.vec3(0.0, 1.0, 0.0); + var t: f64 = 0.0; + var roty: f64 = 0.0; + var rotx: f64 = 0.0; + while (true) { while (loop.tryEvent()) |event| { switch (event) { @@ -65,17 +74,30 @@ pub fn main() !void { return; } else if (key.matches('q', .{})) { return; + } else if (key.matches('a', .{})) { + roty = roty + 0.05; + } else if (key.matches('d', .{})) { + roty = roty - 0.05; + } else if (key.matches('w', .{})) { + rotx = rotx + 0.05; + } else if (key.matches('s', .{})) { + rotx = rotx - 0.05; } }, .winsize => |ws| { try vx.resize(allocator, tty.writer(), ws); - some_scene = GeometryScene.new(math_usize.vec2(ws.cols - 2, ws.rows - 2), shading); + some_scene = GeometryScene.new( + math_usize.vec2(ws.cols - 2, ws.rows - 2), + shading, + ); }, else => {}, } } + t = t + 0.1; + const win = vx.window(); win.clear(); @@ -93,9 +115,30 @@ pub fn main() !void { }); screen_buffer.clearRetainingCapacity(); + if (some_scene) |scene| { - try scene.render(0.0, st3, &screen_buffer.writer); - _ = child.printSegment(.{ .text = screen_buffer.written() }, .{ .wrap = .grapheme }); + const rot_y = math.Mat4.createAngleAxis(cam_up, roty); + const rot_x = math.Mat4.createAngleAxis(cam_right, rotx); + const rot_cam_rel = rot_y.mul(rot_x); + + // Apply rotation to geometry + const rotated = Geometry{ + .transform = .{ + .geometry = &g, + .matrix = rot_cam_rel.transpose(), + }, + }; + + try scene.render( + t, + rotated, + &screen_buffer.writer, + ); + + _ = child.printSegment( + .{ .text = screen_buffer.written() }, + .{ .wrap = .grapheme }, + ); } try vx.render(tty.writer()); diff --git a/src/scene.zig b/src/scene.zig index 2c3ab58..4e5efa2 100644 --- a/src/scene.zig +++ b/src/scene.zig @@ -55,7 +55,12 @@ pub fn Scene(comptime T: type) type { math.vec2(self.dimensionsf.x, self.dimensionsf.y), ).div(math.vec2(self.dimensionsf.y, self.dimensionsf.y)); - const ray_direction = get_ray_direction(uv, ray_origin, math.vec3(0.0, 0.0, 0.0), 1.0); + const ray_direction = get_ray_direction( + uv, + ray_origin, + math.vec3(0.0, 0.0, 0.0), + 1.0, + ); var total_distance: f64 = 0.0; for (0..80) |i| { _ = i; @@ -64,14 +69,26 @@ pub fn Scene(comptime T: type) type { const distance: f64 = @abs(map(time, point, geometry)); total_distance = total_distance + distance; if (distance < self.surface_distance) { - return HitRecord{ .hit = true, .normal = get_normal(time, point, geometry) }; + return HitRecord{ + .hit = true, + .normal = get_normal(time, point, geometry), + .distance = total_distance, + }; } if (total_distance > self.max_distance) { - return HitRecord{ .hit = false, .normal = math.Vec3.zero }; + return HitRecord{ + .hit = false, + .normal = math.Vec3.zero, + .distance = 0.0, + }; } } - return HitRecord{ .hit = false, .normal = math.Vec3.zero }; + return HitRecord{ + .hit = false, + .normal = math.Vec3.zero, + .distance = 0.0, + }; } pub fn get_normal(time: f64, point: math.Vec3, geometry: T) math.Vec3 { diff --git a/src/shading.zig b/src/shading.zig index 15258fc..2b58e2f 100644 --- a/src/shading.zig +++ b/src/shading.zig @@ -18,37 +18,54 @@ pub const Shading = struct { gamma: f64, lut: []const u8, + fog_density: f64, + fog_brightness: f64, + pub fn new(light: math.Vec3, gamma: f64) Self { return Self{ .light = light, .gamma = gamma, .lut = ".,-~:;=!*#$@", + .fog_brightness = 0.00, + .fog_density = 0.05, }; } + fn fogFactor(self: Self, dist: f64) f64 { + if (self.fog_density <= 0.0) { + return 0.0; + } + const d = @max(dist, 0.0); + // Standard exponential fog. For denser close-range fog, use: exp(-density * d * d) + + return 1.0 - @exp(-self.fog_density * d); + } + pub fn calculate(self: Self, pixel: math_usize.Vec2, hit_record: HitRecord) u8 { if (!hit_record.hit) { return ' '; } - - const brightness: f64 = @max(0.0, hit_record.normal.normalize().dot(self.light)); const levels_f: f32 = @floatFromInt(self.lut.len); + // --- Surface shading in linear space --- + const n_dot_l: f64 = @max(0.0, hit_record.normal.normalize().dot(self.light)); + var b_lin = std.math.clamp(n_dot_l, 0.0, 1.0); - // 1) Clamp + gamma (linear -> perceptual) - const b: f64 = std.math.clamp(brightness, 0.0, 1.0); - const g: f64 = if (self.gamma <= 0.0) 1.0 else self.gamma; - const b_perc = std.math.pow(f64, b, 1.0 / g); + // --- Fog blend in linear space --- + const dist: f64 = hit_record.distance; + const f = self.fogFactor(dist); + b_lin = b_lin * (1.0 - f) + self.fog_brightness * f; - // 2) Ordered dithering bias from Bayer (normalize to [0,1)) - // Add a *tiny* offset proportional to matrix cell; scale by level count. + // --- Gamma to perceptual --- + const b_perc = std.math.pow(f64, b_lin, 1.0 / self.gamma); + + // --- Ordered dithering bias (same as before) --- const b4f: f32 = @floatFromInt(BAYER_INDEX[pixel.y & 3][pixel.x & 3]); const t: f32 = (b4f + 0.5) / 16.0; // [0,1) const bias: f32 = (t - 0.5) / levels_f; // small symmetric nudge - // 3) Quantize to nearest glyph index (rounded) + // --- Quantize to glyph --- const v: f64 = std.math.clamp(b_perc + bias, 0.0, 1.0); const idx: u8 = @intFromFloat(v * (levels_f - 1.0) + 0.5); - return self.lut[@min(idx, self.lut.len - 1)]; } };