Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions src/geometry/geometry.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,31 @@ 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;

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,

Expand Down
18 changes: 18 additions & 0 deletions src/geometry/repeat.zig
Original file line number Diff line number Diff line change
@@ -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);
}
};
51 changes: 43 additions & 8 deletions src/geometry/rotate.zig
Original file line number Diff line number Diff line change
@@ -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),
);
}
};
13 changes: 13 additions & 0 deletions src/geometry/transform.zig
Original file line number Diff line number Diff line change
@@ -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));
}
};
1 change: 1 addition & 0 deletions src/hit.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ const math = @import("zlm").as(f64);

pub const HitRecord = struct {
normal: math.Vec3,
distance: f64,
hit: bool,
};
73 changes: 58 additions & 15 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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();

Expand All @@ -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());
Expand Down
25 changes: 21 additions & 4 deletions src/scene.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down
37 changes: 27 additions & 10 deletions src/shading.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
}
};