![]() |
![]() |
| OCaml tutorials and examples |
Stanford BunnyThe 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. WalkthroughPrefaceThe program uses OpenGL via lablGL, the # #directory "+lablGL";; # #load "lablgl.cma";; # #load "lablglut.cma";; # #load "str.cma";; # #load "unix.cma";; Loading the dataThe 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
# 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
# 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 Vector arithmeticTo 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 normalsThe 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. TimerThe scene is animated as a function of time. The following
# let time =
let start = Unix.gettimeofday () in
fun () -> Unix.gettimeofday () -. start;;
val time : unit -> float = <fun>
ResizingGlut requires a
# 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. RenderingA triangle with indices
# 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
# 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
# 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 # let draw_bunny = memoize draw_bunny;; val draw_bunny : unit -> unit = <fun> The complete
# 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
# 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. DownloadsThe source code can be compiled with: ocamlc -I +lablGL lablgl.cma lablglut.cma str.cma unix.cma bunny.ml -o bunny
|
| © Flying Frog Consultancy Ltd., 2007 | Contact the webmaster |