Stanford Bunny

The Stanford bunny is a 3D mesh of triangles commonly used as a benchmark for computer graphics applications.

This web page presents a 85-line OCaml program that uses OpenGL to render the Stanford bunny in real time:

The mesh is represented as a vertex array and an index array. The vertex array is a sequence of 3D vertex coordinates. The index array is a sequence of triples of indices into the vertex array referring to the three vertices of each triangle.

Walkthrough

Preface

The program uses OpenGL via lablGL, the Str module and the Unix module, all of which must be loaded:

# #directory "+lablGL";;
# #load "lablgl.cma";;
# #load "lablglut.cma";;
# #load "str.cma";;
# #load "unix.cma";;

Loading the data

The mesh data can be loaded from file using a relatively small amount of code.

We begin with some abbreviated aliases to convert between types:

# let fos = float_of_string;;
val fos : string -> float = <fun>
# let ios = int_of_string;;
val ios : string -> int = <fun>

The following function splits a string into a list of space-separated strings:

# let split = Str.split (Str.regexp_string " ");;
val split : string -> string list = <fun>

This read function reads lines of input, accumulating vertex positions in vs and triples of indices in is:

# let rec read vs is ch =
    match try Some(split (input_line ch)) with _ -> None with
    | None -> vs, is
    | Some[x;y;z;_;_] -> read ((fos x, fos y, fos z) :: vs) is ch
    | Some["3";i;j;k] -> read vs ((ios i, ios j, ios k) :: is) ch
    | Some s -> read vs is ch;;
val read :
  (float * float * float) list ->
  (int * int * int) list ->
  in_channel -> (float * float * float) list * (int * int * int) list = <fun>

The mesh data is loaded using the read function:

# let vertices, indices =
    let ch = open_in "data/bun_zipper.ply" in
    let vs, is = read [] [] ch in
    close_in ch;
    Printf.printf "%d vertices, %d triangles\n%!" (List.length vs) (List.length is);
    Array.of_list (List.rev vs), is;;
35947 vertices, 69451 triangles
val vertices : (float * float * float) array =
  [|(-0.0378297, 0.12794, 0.00447467); ...|]
val indices : (int * int * int) list =
  [(17277, 17346, 17345); ...]

The vertex positions vs will be accessed randomly, so they are converted into an array. The indices is are only iterated over sequentially, so they are left as a list.

Vector arithmetic

To improve fidelity, we compute a normal for each vertex by averaging the normals of all triangles that have that vertex. We begin by defining some vector operators that act on 3-tuples of floats.

Addition, subtraction and scaling:

# let ( +| ) (x0, y0, z0) (x1, y1, z1) = x0 +. x1, y0 +. y1, z0 +. z1;;
val ( +| ) :
  float * float * float -> float * float * float -> float * float * float =
  <fun>
# let ( -| ) (x0, y0, z0) (x1, y1, z1) = x0 -. x1, y0 -. y1, z0 -. z1;;
val ( -| ) :
  float * float * float -> float * float * float -> float * float * float =
  <fun>
# let ( *| ) s (x, y, z) = s *. x, s *. y, s *. z;;
val ( *| ) : float -> float * float * float -> float * float * float = <fun>

Dot product, normalisation and cross product:

# let dot (x0, y0, z0) (x1, y1, z1) = x0 *. x1 +. y0 *. y1 +. z0 *. z1;;
val dot : float * float * float -> float * float * float -> float = <fun>
# let norm r = 1. /. sqrt(dot r r) *| r;;
val norm : float * float * float -> float * float * float = <fun>
# let cross (x0, y0, z0) (x1, y1, z1) =
  z0 *. y1 -. z1 *. y0, x0 *. z1 -. x1 *. z0, y0 *. x1 -. y1 *. x0;;
val cross :
  float * float * float -> float * float * float -> float * float * float =
  <fun>

Computing surface normals

The normals are computed by creating an initial normal array containing zero vectors. The index array is then iterated over and, for each triangle, the normal is computed using the cross product and the entry in the normal array for each of the tree vertices of the triangle is incremented by the normal vector for that triangle. Finally, the normal vectors are normalised so have unit length:

# let normals =
  let vs = vertices in
  let ns = Array.make (Array.length vs) (0., 0., 0.) in
  let aux (i, j, k) =
    let n = norm(cross ((cross (vs.(j) -| vs.(i)) (vs.(k) -| vs.(i))) in
    List.iter (fun i -> ns.(i) <- ns.(i) +| n) [i;j;k] in
  List.iter aux indices;
  Array.map norm ns;;
val normals : (float * float * float) array =
  [|(-0.196148468580245, -0.972595481176564, 0.124835124338272); ...|]

Using a separate normal vector for each vertex (rather than for each triangle) allows OpenGL to interpolate the colours across the triangles, giving the illusion of a smooth surface rather than a triangulated one.

Timer

The scene is animated as a function of time. The following time function returns the time in seconds since the time function was defined:

# let time =
    let start = Unix.gettimeofday () in
    fun () -> Unix.gettimeofday () -. start;;
val time : unit -> float = <fun>

Resizing

Glut requires a reshape function to handle resizing of the visualisation:

# let width = ref 1 and height = ref 1;;
val width : int ref = {contents = 1}
val height : int ref = {contents = 1}
# let reshape ~w ~h =
    width := max 1 w;
    height := max 1 h;
    GlDraw.viewport 0 0 w h;;
val reshape : w:int -> h:int -> unit = <fun>

The width and height of the display will be used to compute the aspect ratio, required to compute the perspective transformation.

Rendering

A triangle with indices i, j and k can be rendered using the vertex coordinates from the vertices array and the normal vector from the normal array:

# let draw_triangle (i, j, k) =
    List.iter (fun i -> GlDraw.normal3 normals.(i);
                 GlDraw.vertex3 vertices.(i)) [i;j;k];;
val draw_triangle : int * int * int -> unit = <fun>

The whole bunny can be drawn by iterating draw_triangle over the indices array:

# let draw_bunny() =
    GlDraw.begins `triangles;
    List.iter draw_triangle indices;
    GlDraw.ends();;
val draw_bunny : unit -> unit = <fun>

Performance can be greatly increased by memoizing this process in an OpenGL display list. The task of memoizing a function in a display list can be factored out into a higher-order memoize function in OCaml:

# let memoize f =
    let display_list = ref None in
    fun () -> match !display_list with
    | Some list -> GlList.call list
    | None ->
        display_list := Some (GlList.create `compile);
        f();
        GlList.ends ();;
val memoize : (unit -> 'a) -> unit -> unit = <fun>

This memoize function can be used to memoize any given function in a display list. In this case, we supercede the draw_bunny function with a memoized version:

# let draw_bunny = memoize draw_bunny;;
val draw_bunny : unit -> unit = <fun>

The complete render function clear the display, initialises a perspective projection of the scene, sets a light, rotates the scene and draws the bunny:

# let render() =
    GlClear.clear [`color; `depth];
    Gl.enable `depth_test;
    GlMat.mode `projection;
    GlMat.load_identity ();
    let aspect = float !width /. float !height in
    GluMat.perspective ~fovy:45.0 ~aspect ~z:(0.1, 1.);
    GluMat.look_at ~eye:(0., 0.12, -0.25) ~center:(0., 0.1, 0.) ~up:(0., 1., 0.);
    GlMat.mode `modelview;
    GlMat.load_identity ();
    Gl.enable `lighting;
    Gl.enable `light0;
    GlLight.light ~num:0 (`position ((`position (1., -1., 1., 1.));
    GlMat.rotate3 (15. *. time()) (0., 1., 0.);
    GlLight.material `both (`(`shininess 100.);
    Gl.enable `color_material;
    GlLight.color_material `both `specular;
    GlLight.color_material `both `ambient_and_diffuse;
    GlDraw.color (0.6, 0.5, 0.5);
    draw_bunny();
    Gl.flush ();
    Glut.swapBuffers ();;
val render : unit -> unit = <fun>

The main program creates a glut window, registers the reshape and render callback functions, an idle function to repeatedly redraw the scene and starts the glut mainloop:

# let _ = Glut.init S  ys.argv in
    Glut.initDisplayMode ~depth:true ~double_buffer:true ();
    let _ = Glut.createWindow ~title:"Stanford bunny" in
    Glut.reshapeFunc ~cb:reshape;
    Glut.displayFunc ~cb:render;
    Glut.idleFunc ~cb:(Som(Some Glut.postRedisplay);
    Glut.keyboardFunc ~cb:(f(fun ~key ~x ~y -> if key=27 then exit 0);
    Glut.mainLoop ();;

Even when compiled to interpreted OCaml bytecode, this program can render a rotating bunny at hundreds of frames per second.

Downloads

The source code can be compiled with:

ocamlc -I +lablGL lablgl.cma lablglut.cma str.cma unix.cma bunny.ml -o bunny