🎨 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

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