Skip to content

Commit

Permalink
Add cones
Browse files Browse the repository at this point in the history
  • Loading branch information
SinclaM committed Jul 15, 2023
1 parent 4bc6e61 commit cf4252f
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/parser/parser.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand Down
230 changes: 230 additions & 0 deletions src/raytracer/shapes/cone.zig
Original file line number Diff line number Diff line change
@@ -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()
);
}
4 changes: 2 additions & 2 deletions src/raytracer/shapes/cylinder.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions src/raytracer/shapes/shape.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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),
};

Expand Down Expand Up @@ -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) {} });
Expand Down
7 changes: 6 additions & 1 deletion src/raytracer/tuple.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions src/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit cf4252f

Please sign in to comment.