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
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ This project was implemented as a learning experience using Anthropic's Claude a
This ray tracer currently supports:

### Core Rendering
- **Ray-object intersection** for spheres, planes, and cubes
- **Ray-object intersection** for spheres, planes, cubes, cylinders, and cones
- **Quadric surface support** with shared geometry handling for cylinders and cones
- **Phong lighting model** with ambient, diffuse, and specular components
- **Shadows** with accurate shadow ray calculations
- **Camera system** with configurable field of view and transforms
Expand All @@ -28,6 +29,8 @@ This ray tracer currently supports:
- **Total internal reflection** for accurate light behavior in transparent objects

### Geometry & Transformations
- **Primitive shapes**: spheres, planes, cubes, cylinders (with caps), and cones (with caps)
- **Constrained primitives**: cylinders and cones with configurable height limits
- **3D transformations**: translation, scaling, rotation, shearing
- **View transformations** for camera positioning
- **Matrix operations** with 4x4 homogeneous coordinates
Expand All @@ -37,7 +40,7 @@ This ray tracer currently supports:

The raytracer can render complex scenes with multiple objects, transparency, refraction, reflections, and realistic lighting:

![Sample raytraced scene with cube, spheres, transparency and reflections](docs/cube.png)
![Sample raytraced scene with cylinders, cones, cubes, spheres, transparency and reflections](docs/cylinder.png)

## Getting Started

Expand Down Expand Up @@ -75,8 +78,9 @@ sbt test
## Project Structure

- `src/main/scala/` - Core raytracer implementation
- `Main.scala` - Demo scene with spheres, planes, cubes, and lighting
- `Ray.scala`, `Sphere.scala`, `Plane.scala`, `Cube.scala` - Geometric primitives
- `Main.scala` - Demo scene with spheres, planes, cubes, cylinders, cones, and lighting
- `Ray.scala`, `Sphere.scala`, `Plane.scala`, `Cube.scala`, `Cylinder.scala`, `Cone.scala` - Geometric primitives
- `QuadricShape.scala` - Base class for quadric surfaces (cylinders and cones)
- `Material.scala`, `Pattern.scala` - Surface properties and patterns
- `Camera.scala`, `World.scala` - Scene and camera management
- `Color.scala`, `Canvas.scala` - Color handling and image generation
Expand All @@ -93,6 +97,8 @@ The default scene (in `Main.scala`) renders:
- **Right sphere**: Highly reflective metallic surface
- **Left sphere**: Matte yellow-orange surface
- A red cube positioned in the background with rotation
- A blue cylinder with closed caps positioned on the left side
- An orange cone with closed caps positioned in the foreground
- Point light source positioned above and to the left

You can modify the scene by editing `Main.scala` to experiment with different:
Expand Down
Binary file added docs/cylinder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
96 changes: 96 additions & 0 deletions src/main/scala/Cone.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.samuelcantrell.raytracer.cone

import com.samuelcantrell.raytracer.ray
import com.samuelcantrell.raytracer.tuple
import com.samuelcantrell.raytracer.intersection
import com.samuelcantrell.raytracer.matrix
import com.samuelcantrell.raytracer.material
import com.samuelcantrell.raytracer.shape
import com.samuelcantrell.raytracer.equality
import com.samuelcantrell.raytracer.quadric.QuadricShape
import java.util.UUID

case class Cone(
override val id: String = UUID.randomUUID().toString,
override val transform: matrix.Matrix = matrix.Matrix.identity(),
override val objectMaterial: material.Material = material.material(),
override val minimum: Double = Double.NegativeInfinity,
override val maximum: Double = Double.PositiveInfinity,
override val closed: Boolean = false
) extends QuadricShape(id, transform, objectMaterial, minimum, maximum, closed) {

// Cone-specific implementations
protected def surfaceIntersectionCoefficients(localRay: ray.Ray): (Double, Double, Double) = {
// Cone equation: x² + z² = y²
val a = localRay.direction.x * localRay.direction.x +
localRay.direction.z * localRay.direction.z -
localRay.direction.y * localRay.direction.y
val b = 2 * localRay.origin.x * localRay.direction.x +
2 * localRay.origin.z * localRay.direction.z -
2 * localRay.origin.y * localRay.direction.y
val c = localRay.origin.x * localRay.origin.x +
localRay.origin.z * localRay.origin.z -
localRay.origin.y * localRay.origin.y
(a, b, c)
}

protected def handleParallelRay(localRay: ray.Ray, b: Double, c: Double): Option[Double] = {
// Ray is parallel to one of the cone's halves
if (math.abs(b) >= equality.EPSILON) {
Some(-c / (2 * b))
} else {
None
}
}

protected def capRadius(y: Double): Double = math.abs(y)

protected def surfaceNormal(localPoint: tuple.Tuple): tuple.Tuple = {
val dist = localPoint.x * localPoint.x + localPoint.z * localPoint.z
val y = if (localPoint.y > 0) -math.sqrt(dist) else math.sqrt(dist)
tuple.makeVector(localPoint.x, y, localPoint.z)
}

protected def isOnCap(localPoint: tuple.Tuple, y: Double): Boolean = {
val dist = localPoint.x * localPoint.x + localPoint.z * localPoint.z
dist < math.abs(y) && math.abs(localPoint.y - y) < equality.EPSILON
}

def withTransform(newTransform: matrix.Matrix): Cone = {
this.copy(transform = newTransform)
}

def withMaterial(newMaterial: material.Material): Cone = {
this.copy(objectMaterial = newMaterial)
}
}

def cone(): Cone = {
Cone()
}

// Convenience functions for backward compatibility
def setTransform(c: Cone, t: matrix.Matrix): Cone = {
shape.setTransform(c, t)
}

def setMaterial(c: Cone, m: material.Material): Cone = {
shape.setMaterial(c, m)
}

def intersect(c: Cone, r: ray.Ray): intersection.Intersections = {
shape.intersect(c, r)
}

def normalAt(c: Cone, worldPoint: tuple.Tuple): tuple.Tuple = {
shape.normalAt(c, worldPoint)
}

// Direct access to local methods for testing
def localIntersect(c: Cone, localRay: ray.Ray): intersection.Intersections = {
c.localIntersect(localRay)
}

def localNormalAt(c: Cone, localPoint: tuple.Tuple): tuple.Tuple = {
c.localNormalAt(localPoint)
}
86 changes: 86 additions & 0 deletions src/main/scala/Cylinder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.samuelcantrell.raytracer.cylinder

import com.samuelcantrell.raytracer.ray
import com.samuelcantrell.raytracer.tuple
import com.samuelcantrell.raytracer.intersection
import com.samuelcantrell.raytracer.matrix
import com.samuelcantrell.raytracer.material
import com.samuelcantrell.raytracer.shape
import com.samuelcantrell.raytracer.equality
import com.samuelcantrell.raytracer.quadric.QuadricShape
import java.util.UUID

case class Cylinder(
override val id: String = UUID.randomUUID().toString,
override val transform: matrix.Matrix = matrix.Matrix.identity(),
override val objectMaterial: material.Material = material.material(),
override val minimum: Double = Double.NegativeInfinity,
override val maximum: Double = Double.PositiveInfinity,
override val closed: Boolean = false
) extends QuadricShape(id, transform, objectMaterial, minimum, maximum, closed) {

// Cylinder-specific implementations
protected def surfaceIntersectionCoefficients(localRay: ray.Ray): (Double, Double, Double) = {
val a = localRay.direction.x * localRay.direction.x +
localRay.direction.z * localRay.direction.z
val b = 2 * localRay.origin.x * localRay.direction.x +
2 * localRay.origin.z * localRay.direction.z
val c = localRay.origin.x * localRay.origin.x +
localRay.origin.z * localRay.origin.z - 1
(a, b, c)
}

protected def handleParallelRay(localRay: ray.Ray, b: Double, c: Double): Option[Double] = {
// Cylinders don't have parallel ray intersections (when a ≈ 0, no intersection)
None
}

protected def capRadius(y: Double): Double = 1.0

protected def surfaceNormal(localPoint: tuple.Tuple): tuple.Tuple = {
tuple.makeVector(localPoint.x, 0, localPoint.z)
}

protected def isOnCap(localPoint: tuple.Tuple, y: Double): Boolean = {
val dist = localPoint.x * localPoint.x + localPoint.z * localPoint.z
dist < 1 && math.abs(localPoint.y - y) < equality.EPSILON
}

def withTransform(newTransform: matrix.Matrix): Cylinder = {
this.copy(transform = newTransform)
}

def withMaterial(newMaterial: material.Material): Cylinder = {
this.copy(objectMaterial = newMaterial)
}
}

def cylinder(): Cylinder = {
Cylinder()
}

// Convenience functions for backward compatibility
def setTransform(c: Cylinder, t: matrix.Matrix): Cylinder = {
shape.setTransform(c, t)
}

def setMaterial(c: Cylinder, m: material.Material): Cylinder = {
shape.setMaterial(c, m)
}

def intersect(c: Cylinder, r: ray.Ray): intersection.Intersections = {
shape.intersect(c, r)
}

def normalAt(c: Cylinder, worldPoint: tuple.Tuple): tuple.Tuple = {
shape.normalAt(c, worldPoint)
}

// Direct access to local methods for testing
def localIntersect(c: Cylinder, localRay: ray.Ray): intersection.Intersections = {
c.localIntersect(localRay)
}

def localNormalAt(c: Cylinder, localPoint: tuple.Tuple): tuple.Tuple = {
c.localNormalAt(localPoint)
}
36 changes: 35 additions & 1 deletion src/main/scala/Main.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import com.samuelcantrell.raytracer.sphere
import com.samuelcantrell.raytracer.plane
import com.samuelcantrell.raytracer.cube
import com.samuelcantrell.raytracer.cylinder
import com.samuelcantrell.raytracer.cone
import com.samuelcantrell.raytracer.transformation
import com.samuelcantrell.raytracer.material
import com.samuelcantrell.raytracer.color
Expand Down Expand Up @@ -92,6 +94,36 @@ import com.samuelcantrell.raytracer.pattern
)
val cubeWithMaterial = cube.setMaterial(cubeTransformed, cubeMaterial)

// Create a cylinder
val cylinderShape = cylinder.cylinder().copy(minimum = 0, maximum = 2, closed = true)
val cylinderTransform = transformation.translation(-2, 0, 1) *
transformation.scaling(0.5, 1, 0.5)
val cylinderTransformed = cylinder.setTransform(cylinderShape, cylinderTransform)
val cylinderMaterial = material
.material()
.copy(
materialColor = color.Color(0.2, 0.6, 0.9),
diffuse = 0.8,
specular = 0.3,
reflective = 0.1
)
val cylinderWithMaterial = cylinder.setMaterial(cylinderTransformed, cylinderMaterial)

// Create a cone
val coneShape = cone.cone().copy(minimum = -1, maximum = 0, closed = true)
val coneTransform = transformation.translation(0.5, 1, -2) *
transformation.scaling(0.8, 1, 0.8)
val coneTransformed = cone.setTransform(coneShape, coneTransform)
val coneMaterial = material
.material()
.copy(
materialColor = color.Color(0.9, 0.4, 0.1),
diffuse = 0.7,
specular = 0.4,
reflective = 0.05
)
val coneWithMaterial = cone.setMaterial(coneTransformed, coneMaterial)

// Create the world with all objects
val lightSource = light.pointLight(
tuple.makePoint(-10, 10, -10),
Expand All @@ -104,7 +136,9 @@ import com.samuelcantrell.raytracer.pattern
middleWithMaterial,
rightWithMaterial,
leftWithMaterial,
cubeWithMaterial
cubeWithMaterial,
cylinderWithMaterial,
coneWithMaterial
),
lightSource = Some(lightSource)
)
Expand Down
Loading