diff --git a/src/hit.zig b/src/hit.zig new file mode 100644 index 0000000..7f284a6 --- /dev/null +++ b/src/hit.zig @@ -0,0 +1,6 @@ +const math = @import("zlm").as(f64); + +pub const HitRecord = struct { + normal: math.Vec3, + hit: bool, +}; diff --git a/src/main.zig b/src/main.zig index f92d6d1..faebe8c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,9 @@ const std = @import("std"); const vaxis = @import("vaxis"); 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; @@ -48,7 +51,10 @@ pub fn main() !void { const st3 = Scale(UnionSmooth(Rotate(Torus), Torus)).new(t3, 1.2); const ThisScene = Scene(Scale(UnionSmooth(Rotate(Torus), Torus))); - var scene = ThisScene.new(200.0, 200.0, light_position, gamma); + + const shading = Shading.new(light_position, gamma); + + var scene = ThisScene.new(math_usize.vec2(200, 200), shading); while (true) { while (loop.tryEvent()) |event| { @@ -63,8 +69,7 @@ pub fn main() !void { .winsize => |ws| { try vx.resize(allocator, tty.writer(), ws); - - scene = ThisScene.new(ws.cols - 2, ws.rows - 2, light_position, gamma); + scene = ThisScene.new(math_usize.vec2(ws.cols - 2, ws.rows - 2), shading); }, else => {}, } diff --git a/src/scene.zig b/src/scene.zig index e9a9a42..8fa1e8b 100644 --- a/src/scene.zig +++ b/src/scene.zig @@ -1,45 +1,9 @@ const std = @import("std"); const math = @import("zlm").as(f64); +const math_usize = @import("zlm").as(usize); -const LUMINANCE = ".,-~:;=!*#$@"; - -fn bayer4(x: usize, y: usize) u8 { - const M = [_][4]u8{ - .{ 0, 8, 2, 10 }, - .{ 12, 4, 14, 6 }, - .{ 3, 11, 1, 9 }, - .{ 15, 7, 13, 5 }, - }; - return M[y & 3][x & 3]; -} - -fn brightnessToCharDither(x: usize, y: usize, b_in: f64, gamma: f64) u8 { - const lut = LUMINANCE; - const levels_f: f32 = @floatFromInt(lut.len); - - // 1) Clamp + gamma (linear -> perceptual) - const b = std.math.clamp(b_in, 0.0, 1.0); - const g = if (gamma <= 0.0) 1.0 else gamma; - const b_perc = std.math.pow(f64, b, 1.0 / g); - - // 2) Ordered dithering bias from Bayer (normalize to [0,1)) - // Add a *tiny* offset proportional to matrix cell; scale by level count. - const b4f: f64 = @floatFromInt(bayer4(x, y)); - const t = (b4f + 0.5) / 16.0; // [0,1) - const bias = (t - 0.5) / levels_f; // small symmetric nudge - - // 3) Quantize to nearest glyph index (rounded) - const v = std.math.clamp(b_perc + bias, 0.0, 1.0); - var idx: u8 = @intFromFloat(v * (levels_f - 1.0) + 0.5); - if (idx >= lut.len) idx = lut.len - 1; - - return lut[idx]; -} - -const HitRecord = struct { - normal: math.Vec3, - hit: bool, -}; +const HitRecord = @import("./hit.zig").HitRecord; +const Shading = @import("./shading.zig").Shading; pub fn get_ray_direction(uv: math.Vec2, point: math.Vec3, lookat: math.Vec3, z: f64) math.Vec3 { const f = lookat.sub(point).normalize(); @@ -54,38 +18,26 @@ pub fn Scene(comptime T: type) type { return struct { const Self = @This(); - light: math.Vec3, - gamma: f64, - - width: usize, - height: usize, + shading: Shading, - widthf: f64, - heightf: f64, + dimensions: math_usize.Vec2, + dimensionsf: math.Vec2, - pub fn new(width: usize, height: usize, light: math.Vec3, gamma: f64) Self { + pub fn new(dimensions: math_usize.Vec2, shading: Shading) Self { return Self{ - .width = width, - .height = height, - .light = light, - .widthf = @floatFromInt(width), - .heightf = @floatFromInt(height), - .gamma = gamma, + .dimensions = dimensions, + .shading = shading, + .dimensionsf = math.vec2(@floatFromInt(dimensions.x), @floatFromInt(dimensions.y)), }; } pub fn render(self: Self, time: f64, geometry: T, writer: *std.Io.Writer) !void { - for (0..self.height) |y| { - for (0..self.width) |x| { + for (0..self.dimensions.y) |y| { + for (0..self.dimensions.x) |x| { const xf: f64 = @floatFromInt(x); const yf: f64 = @floatFromInt(y); const hit_record = self.march(time, geometry, xf, yf); - if (hit_record.hit) { - const brightness = @max(0.0, hit_record.normal.normalize().dot(self.light)); - try writer.writeByte(brightnessToCharDither(x, y, brightness, self.gamma)); - } else { - try writer.writeByte(' '); - } + try writer.writeByte(self.shading.calculate(math_usize.vec2(x, y), hit_record)); } } } @@ -94,7 +46,9 @@ pub fn Scene(comptime T: type) type { const ray_origin = math.vec3(0.0, 0.0, -2.0); const frag_coord = math.vec4(x + 0.5, y + 0.5, 0.0, 1.0); - const uv = frag_coord.scale(2.0).swizzle("xy").sub(math.vec2(self.widthf, self.heightf)).div(math.vec2(self.heightf, self.heightf)); + const uv = frag_coord.scale(2.0).swizzle("xy").sub( + 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); var total_distance: f64 = 0.0; diff --git a/src/shading.zig b/src/shading.zig new file mode 100644 index 0000000..15258fc --- /dev/null +++ b/src/shading.zig @@ -0,0 +1,54 @@ +const std = @import("std"); +const math = @import("zlm").as(f64); +const math_usize = @import("zlm").as(usize); + +const HitRecord = @import("./hit.zig").HitRecord; + +const BAYER_INDEX = [4][4]u8{ + .{ 0, 8, 2, 10 }, + .{ 12, 4, 14, 6 }, + .{ 3, 11, 1, 9 }, + .{ 15, 7, 13, 5 }, +}; + +pub const Shading = struct { + const Self = @This(); + + light: math.Vec3, + gamma: f64, + lut: []const u8, + + pub fn new(light: math.Vec3, gamma: f64) Self { + return Self{ + .light = light, + .gamma = gamma, + .lut = ".,-~:;=!*#$@", + }; + } + + 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); + + // 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); + + // 2) Ordered dithering bias from Bayer (normalize to [0,1)) + // Add a *tiny* offset proportional to matrix cell; scale by level count. + 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) + 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)]; + } +};