diff --git a/src/parser/parser.zig b/src/parser/parser.zig index 531f2e7..488c506 100644 --- a/src/parser/parser.zig +++ b/src/parser/parser.zig @@ -74,6 +74,11 @@ fn ObjectConfig(comptime T: type) type { min: T = -std.math.inf(T), max: T = std.math.inf(T), closed: bool = false, + }, + cone: *struct { + min: T = -std.math.inf(T), + max: T = std.math.inf(T), + closed: bool = false, } }, transform: ?TransformConfig(T) = null, @@ -226,6 +231,13 @@ fn parseObject(comptime T: type, allocator: Allocator, object: ObjectConfig(T)) c.variant.cylinder.closed = cyl.closed; break :blk c; }, + .cone => |cyl| blk: { + var c = Shape(T).cone(); + c.variant.cone.min = cyl.min; + c.variant.cone.max = cyl.max; + c.variant.cone.closed = cyl.closed; + break :blk c; + }, }; shape.casts_shadow = object.@"casts-shadow"; diff --git a/src/raytracer/shapes/cone.zig b/src/raytracer/shapes/cone.zig new file mode 100644 index 0000000..1040efc --- /dev/null +++ b/src/raytracer/shapes/cone.zig @@ -0,0 +1,230 @@ +const std = @import("std"); +const testing = std.testing; +const Allocator = std.mem.Allocator; +const inf = std.math.inf; + +const Tuple = @import("../tuple.zig").Tuple; +const Matrix = @import("../matrix.zig").Matrix; +const Ray = @import("../ray.zig").Ray; + +const shape = @import("shape.zig"); +const Intersection = shape.Intersection; +const Intersections = shape.Intersections; +const sortIntersections = shape.sortIntersections; +const Shape = shape.Shape; + +/// A cone object, backed by floats of type `T`. +/// +/// All cones are axis-aligned and centered at the origin in their +/// own object space. To move them, rotate them, resize them, etc. +/// in world space, use Shape.setTransform. +pub fn Cone(comptime T: type) type { + return struct { + const Self = @This(); + const tolerance: T = 1e-4; + + min: T = -inf(T), + max: T = inf(T), + closed: bool = false, + + fn check_cap(ray: Ray(T), t: T, radius: T) bool { + const x = ray.origin.x + t * ray.direction.x; + const z = ray.origin.z + t * ray.direction.z; + + return x * x + z * z <= radius * radius; + } + + fn intersect_caps(cone: *const Shape(T), ray: Ray(T), xs: *Intersections(T)) !void { + if (!cone.variant.cone.closed or @fabs(ray.direction.y) < Self.tolerance) { + return; + } + + var t = (cone.variant.cone.min - ray.origin.y) / ray.direction.y; + if (check_cap(ray, t, cone.variant.cone.min)) { + try xs.append(Intersection(T).new(t, cone)); + } + + t = (cone.variant.cone.max - ray.origin.y) / ray.direction.y; + if (check_cap(ray, t, cone.variant.cone.max)) { + try xs.append(Intersection(T).new(t, cone)); + } + } + + pub fn localIntersect( + self: Self, allocator: Allocator, super: *const Shape(T), ray: Ray(T) + ) !Intersections(T) { + const a = ray.direction.x * ray.direction.x + - ray.direction.y * ray.direction.y + + ray.direction.z * ray.direction.z; + + var xs = Intersections(T).init(allocator); + + const b = 2.0 * ray.origin.x * ray.direction.x + - 2.0 * ray.origin.y * ray.direction.y + + 2.0 * ray.origin.z * ray.direction.z; + + if (@fabs(a) < Self.tolerance and @fabs(b) < Self.tolerance) { + // Ray misses + try Self.intersect_caps(super, ray, &xs); + return xs; + } + + const c = ray.origin.x * ray.origin.x + - ray.origin.y * ray.origin.y + + ray.origin.z * ray.origin.z; + + if (@fabs(a) < Self.tolerance) { + // This parallel ray intersects once with the surface... + try xs.append(Intersection(T).new(-c / (2.0 * b), super)); + + // ...but might hit a cap on the way out! + try Self.intersect_caps(super, ray, &xs); + return xs; + } + + const discriminant = b * b - 4.0 * a * c; + + if (discriminant < 0.0) { + // Ray does not intersect + return xs; + } + + var t0 = (-b - @sqrt(discriminant)) / (2.0 * a); + var t1 = (-b + @sqrt(discriminant)) / (2.0 * a); + + if (t0 > t1) { + const save = t0; + t0 = t1; + t1 = save; + } + + const y0 = ray.origin.y + t0 * ray.direction.y; + if (self.min < y0 and y0 < self.max) { + try xs.append(Intersection(T).new(t0, super)); + } + + const y1 = ray.origin.y + t1 * ray.direction.y; + if (self.min < y1 and y1 < self.max) { + try xs.append(Intersection(T).new(t1, super)); + } + + try Self.intersect_caps(super, ray, &xs); + return xs; + } + + pub fn localNormalAt(self: Self, super: Shape(T), point: Tuple(T)) Tuple(T) { + _ = super; + + const dist = point.x * point.x + point.z * point.z; + + if (dist < self.max * self.max and point.y >= self.max - Self.tolerance) { + return Tuple(T).vec3(0.0, 1.0, 0.0); + } else if (dist < self.min * self.min and point.y <= self.min + Self.tolerance) { + return Tuple(T).vec3(0.0, -1.0, 0.0); + } else { + const y = -std.math.sign(point.y) * @sqrt(point.x * point.x + point.z * point.z); + return Tuple(T).vec3(point.x, y, point.z); + } + } + }; +} + +fn testRayIntersectsCone( + comptime T: type, allocator: Allocator, origin: Tuple(T), direction: Tuple(T), t0: T, t1: T +) !void { + var cone = Shape(T).cone(); + const r = Ray(T).new(origin, direction.normalized()); + + var xs = try cone.intersect(allocator, r); + defer xs.deinit(); + + try testing.expect(xs.items.len == 2); + try testing.expectApproxEqAbs(xs.items[0].t, t0, Cone(T).tolerance); + try testing.expectApproxEqAbs(xs.items[1].t, t1, Cone(T).tolerance); +} + +test "Intersecting a cone with a ray" { + const allocator = testing.allocator; + + // Needs f64 precision + + try testRayIntersectsCone( + f64, allocator, Tuple(f64).point(0.0, 0.0, -5.0), Tuple(f64).vec3(0.0, 0.0, 1.0), 5.0, 5.0 + ); + + try testRayIntersectsCone( + f64, allocator, Tuple(f64).point(0.0, 0.0, -5.0), Tuple(f64).vec3(1.0, 1.0, 1.0), 8.66025, 8.66025 + ); + + try testRayIntersectsCone( + f64, allocator, Tuple(f64).point(1.0, 1.0, -5.0), Tuple(f64).vec3(-0.5, -1.0, 1.0), 4.55006, 49.44994 + ); +} + +test "Intersecting a cone with a ray parallel to one of its halves" { + const allocator = testing.allocator; + + var s = Shape(f32).cone(); + const direction = Tuple(f32).vec3(0.0, 1.0, 1.0).normalized(); + const ray = Ray(f32).new(Tuple(f32).point(0.0, 0.0, -1.0), direction); + + const xs = try s.intersect(allocator, ray); + defer xs.deinit(); + + try testing.expectEqual(xs.items.len, 1); + try testing.expectApproxEqAbs(xs.items[0].t, 0.35355, Cone(f32).tolerance); +} + +fn testRayIntersectsClosedCone( + comptime T: type, allocator: Allocator, origin: Tuple(T), direction: Tuple(T), count: usize +) !void { + var cone = Shape(T).cone(); + + cone.variant.cone.min = -0.5; + cone.variant.cone.max = 0.5; + cone.variant.cone.closed = true; + + const r = Ray(T).new(origin, direction.normalized()); + + const xs = try cone.intersect(allocator, r); + defer xs.deinit(); + + try testing.expectEqual(xs.items.len, count); +} + +test "Intersecting a cone's end caps" { + const allocator = testing.allocator; + + try testRayIntersectsClosedCone( + f32, allocator, Tuple(f32).point(0.0, 0.0, -5.0), Tuple(f32).vec3(0.0, 1.0, 0.0), 0 + ); + + try testRayIntersectsClosedCone( + f32, allocator, Tuple(f32).point(0.0, 0.0, -0.25), Tuple(f32).vec3(0.0, 1.0, 1.0), 2 + ); + + try testRayIntersectsClosedCone( + f32, allocator, Tuple(f32).point(0.0, 0.0, -0.25), Tuple(f32).vec3(0.0, 1.0, 0.0), 4 + ); +} + + +fn testNormalOnCone(comptime T: type, point: Tuple(T), normal: Tuple(T)) !void { + var cone = Shape(T).cone(); + + try testing.expect(cone.normalAt(point).approxEqual(normal)); +} + +test "Computing the normal vector on a cone" { + try testNormalOnCone( + f32, Tuple(f32).point(0.0, 0.0, 0.0), Tuple(f32).vec3(0.0, 0.0, 0.0) + ); + + try testNormalOnCone( + f32, Tuple(f32).point(1.0, 1.0, 1.0), Tuple(f32).vec3(1.0, -@sqrt(2.0), 1.0).normalized() + ); + + try testNormalOnCone( + f32, Tuple(f32).point(-1.0, -1.0, 0.0), Tuple(f32).vec3(-1.0, 1.0, 0.0).normalized() + ); +} diff --git a/src/raytracer/shapes/cylinder.zig b/src/raytracer/shapes/cylinder.zig index a17e40c..b3023ae 100644 --- a/src/raytracer/shapes/cylinder.zig +++ b/src/raytracer/shapes/cylinder.zig @@ -144,7 +144,7 @@ test "A ray misses a cylinder" { fn testRayIntersectsCylinder( comptime T: type, allocator: Allocator, origin: Tuple(T), direction: Tuple(T), t0: T, t1: T ) !void { - var cyl = Shape(f32).cylinder(); + var cyl = Shape(T).cylinder(); const r = Ray(T).new(origin, direction.normalized()); var xs = try cyl.intersect(allocator, r); @@ -287,7 +287,7 @@ test "Intersecting the caps of a closed cylinder" { ); } -fn testNormalOnClosedCylinder(comptime T: type, point: Tuple(f32), normal: Tuple(f32)) !void { +fn testNormalOnClosedCylinder(comptime T: type, point: Tuple(T), normal: Tuple(T)) !void { var cyl = Shape(T).cylinder(); cyl.variant.cylinder.min = 1.0; diff --git a/src/raytracer/shapes/shape.zig b/src/raytracer/shapes/shape.zig index 3b5bd8b..bda9bdd 100644 --- a/src/raytracer/shapes/shape.zig +++ b/src/raytracer/shapes/shape.zig @@ -10,6 +10,7 @@ const Ray = @import("../ray.zig").Ray; const Sphere = @import("sphere.zig").Sphere; const Cube = @import("cube.zig").Cube; const Cylinder = @import("cylinder.zig").Cylinder; +const Cone = @import("cone.zig").Cone; const Plane = @import("plane.zig").Plane; const PreComputations = @import("../world.zig").PreComputations; @@ -78,6 +79,7 @@ pub fn Shape(comptime T: type) type { sphere: Sphere(T), cube: Cube(T), cylinder: Cylinder(T), + cone: Cone(T), plane: Plane(T), }; @@ -130,6 +132,11 @@ pub fn Shape(comptime T: type) type { return Self.new(Self.Variant { .cylinder = Cylinder(T) {} }); } + /// Creates a new cone. + pub fn cone() Self { + return Self.new(Self.Variant { .cone = Cone(T) {} }); + } + /// Creates a new plane. pub fn plane() Self { return Self.new(Self.Variant { .plane = Plane(T) {} }); diff --git a/src/raytracer/tuple.zig b/src/raytracer/tuple.zig index 6fe5aee..f3ff849 100644 --- a/src/raytracer/tuple.zig +++ b/src/raytracer/tuple.zig @@ -107,7 +107,12 @@ pub fn Tuple(comptime T: type) type { /// /// Assumes `self` is a vector. pub inline fn normalized(self: Self) Self { - return self.div(self.magnitude()); + const mag = self.magnitude(); + if (mag == 0.0) { + return self; + } else { + return self.div(mag); + } } /// Computes the dot product. diff --git a/src/tests.zig b/src/tests.zig index c1d8db3..6b66ad5 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -12,6 +12,7 @@ comptime { _ = @import("raytracer/shapes/sphere.zig"); _ = @import("raytracer/shapes/cube.zig"); _ = @import("raytracer/shapes/cylinder.zig"); + _ = @import("raytracer/shapes/cone.zig"); _ = @import("raytracer/shapes/plane.zig"); _ = @import("raytracer/patterns/pattern.zig"); _ = @import("raytracer/patterns/solid.zig");