🎨 Ray Tracer
A from-scratch ray tracer with reflections, shadows, and multi-sample anti-aliasing
Overview
This module implements a recursive ray tracer in pure OCaml — no external graphics libraries. It renders 3D scenes by simulating how light rays bounce off objects, producing PPM images with realistic reflections, shadows, and diffuse/specular shading.
The architecture separates geometry (vector math, intersection), shading (materials, lighting), and rendering (camera, anti-aliasing) into clean modules.
Core Modules
module Vec3 = struct
type t = { x: float; y: float; z: float }
let add a b = { x = a.x +. b.x; y = a.y +. b.y; z = a.z +. b.z }
let sub a b = ...
let scale t s = { x = t.x *. s; y = t.y *. s; z = t.z *. s }
let dot a b = a.x *. b.x +. a.y *. b.y +. a.z *. b.z
let cross a b = ...
let length v = sqrt (dot v v)
let normalize v = scale v (1.0 /. length v)
end
module Ray = struct
type t = { origin: Vec3.t; direction: Vec3.t }
let at ray t = Vec3.add ray.origin (Vec3.scale ray.direction t)
end
Materials and Shapes
type material = {
color : Color.t; (* base color *)
ambient : float; (* ambient coefficient 0–1 *)
diffuse : float; (* Lambertian diffuse coefficient *)
specular : float; (* specular highlight coefficient *)
shininess : float; (* Phong exponent *)
reflective: float; (* mirror reflectivity 0–1 *)
}
type shape =
| Sphere of Vec3.t * float * material (* center, radius, material *)
| Plane of Vec3.t * Vec3.t * material (* point, normal, material *)
(* Intersection returns the closest hit point, normal, and material *)
type hit = {
t : float; (* ray parameter at intersection *)
point : Vec3.t; (* world-space hit point *)
normal : Vec3.t; (* surface normal at hit *)
material : material; (* material of the hit surface *)
}
Ray-Object Intersection
(* Sphere intersection via quadratic formula *)
let intersect_sphere ray center radius =
let oc = Vec3.sub ray.origin center in
let a = Vec3.dot ray.direction ray.direction in
let b = 2.0 *. Vec3.dot oc ray.direction in
let c = Vec3.dot oc oc -. radius *. radius in
let discriminant = b *. b -. 4.0 *. a *. c in
if discriminant < 0.0 then None
else
let t1 = (-.b -. sqrt discriminant) /. (2.0 *. a) in
let t2 = (-.b +. sqrt discriminant) /. (2.0 *. a) in
(* Return closest positive t *)
...
(* Plane intersection: t = dot(point - origin, normal) / dot(direction, normal) *)
let intersect_plane ray point normal = ...
Shading Model
The renderer implements Phong shading with shadow rays and recursive reflections:
(* For each light source: *)
(* 1. Shadow test — cast ray from hit point toward light *)
let is_shadowed scene light_pos hit_point = ...
(* 2. Diffuse — Lambertian: max(0, dot(N, L)) *)
(* 3. Specular — Phong: max(0, dot(R, V))^shininess *)
(* Recursive reflection *)
let rec trace scene ray depth =
if depth <= 0 then Color.black
else match closest_hit ray scene.shapes with
| None -> scene.background
| Some hit ->
let local = shade scene ray hit in
if hit.material.reflective > 0.0 then
let reflect_dir = Vec3.reflect ray.direction hit.normal in
let reflect_ray = { origin = ...; direction = reflect_dir } in
let reflected = trace scene reflect_ray (depth - 1) in
Color.lerp local reflected hit.material.reflective
else local
Camera and Anti-Aliasing
module Camera = struct
type t = {
origin : Vec3.t;
lower_left: Vec3.t;
horizontal: Vec3.t;
vertical : Vec3.t;
}
(* Constructs a camera from position, look-at, up vector, and FOV *)
end
(* Multi-sample anti-aliasing: shoot N random rays per pixel *)
let render ~width ~height ~samples scene camera =
Array.init height (fun j ->
Array.init width (fun i ->
let color = ref Color.black in
for _ = 1 to samples do
let u = (float i +. Random.float 1.0) /. float width in
let v = (float j +. Random.float 1.0) /. float height in
let ray = Camera.ray_at camera u v in
color := Color.add !color (trace scene ray 5)
done;
Color.scale !color (1.0 /. float samples)
)
)
Output
(* Renders to PPM (Portable Pixmap) — universal, no dependencies *)
let write_ppm filename pixels =
let oc = open_out filename in
let h = Array.length pixels in
let w = Array.length pixels.(0) in
Printf.fprintf oc "P3\n%d %d\n255\n" w h;
(* Write RGB triplets, gamma-corrected *)
...
(* Demo scene: reflective spheres on a checkerboard floor *)
let demo_scene () = {
shapes = [
Sphere((0, 1, 0), 1.0, mirror_material);
Sphere((-2, 0.5, 1), 0.5, red_matte);
Sphere((2, 0.5, -1), 0.5, blue_shiny);
Plane((0, 0, 0), (0, 1, 0), checkerboard);
];
lights = [ PointLight((-5, 5, -5), white) ];
background = sky_gradient;
}
OCaml Concepts Demonstrated
- Modules as namespaces —
Vec3,Color,Ray,Cameraeach encapsulate a domain - Record types — materials, hits, camera parameters all use named fields
- Recursive functions with depth —
tracerecurses with decreasing depth for reflections - Option types for intersections —
intersect_*returnsSome hitorNone - Array.init for image buffer — 2D array generation using higher-order functions
- Float arithmetic — extensive use of OCaml's
+.,*.,sqrtfor vector math - Ref cells for accumulation —
colorref in anti-aliasing loop
Running It
# Compile and run
ocamlfind ocamlopt raytracer.ml -o raytracer
./raytracer
# Produces output.ppm — open with any image viewer
# Also runs built-in test suite verifying vector math and intersections