[Shootout-list] Ray tracer

Jon Harrop jon@ffconsultancy.com
Fri, 29 Apr 2005 03:19:22 +0100


--Boundary-00=_qmZcC/AKsXUTR7l
Content-Type: text/plain;
  charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

On Thursday 28 April 2005 20:41, Brent Fulgham wrote:
> ...Maybe 120 lines is a better number...

Ye of little faith. ;-)

I have now ported the ray tracer to a 97-line C++. I've changed the task 
slightly to do supersampling in order to increase the amount of CPU work done 
per byte of output (to make it a better benchmark). So there is a new OCaml 
implementation too (which is slightly faster than the last).

The C++ version uses natural representations. Specifically, the OCaml used a 
variant type and pattern matched functions over this variant type:

type obj = Sphere of vec * float * float | Group of vec * float * obj array

let intersect ray scene =
  let rec of_scene ((l, _, _) as first) = function
      Sphere (center, radius, material) -> ...
    | Group (center, radius, scenes) -> ...

In contrast, the C++ version uses an inheritance hierarchy instead of the 
variant type, and implements each expression corresponding to each pattern in 
the appropriate derived class:

struct Scene { // Abstract base class representing a scene
  virtual void intersect(double &, Vec &, Ray) = 0;
  virtual void del() {};
};

struct Sphere : public Scene { // Derived class representing a sphere
  Vec center;
  double radius;
  ...
  void intersect(double &lambda, Vec &normal, Ray ray) { ... }
}

struct Group : public Scene { // Derived class representing a group of scenes
  Sphere bound;
  vector<Scene *> objs;
  void intersect(double &lambda, Vec &normal, Ray ray) { ... }
};

Note that the "intersect" function was a purely functional expression in OCaml 
but acts by way of side-effects in C++ (altering the "lambda" and "normal" 
variables passed to it by reference).

I've done some timings and the results are very wierd. On my AMD64, the C++ 
and OCaml implementations perform similarly (17.281s in C++ and 19.428s in 
OCaml). On my 32-bit Athlon I was expecting OCaml to be half the speed of the 
C++ as ocamlopt is much better at optimising floating point arithmetic for 
AMD64 than for x86. However, the OCaml is actually more than twice as fast 
(92.805s in C++ vs 44.862s in OCaml).

The C++ could be optimised in various ways but this would bloat the code and 
would be fiddly to do.

Given that this can be done in <100 lines of C++, I'd imagine it can also be 
done in Java and C#. For one thing, a few lines are dedicated to memory 
deallocation.

-- 
Dr Jon D Harrop, Flying Frog Consultancy Ltd.
Objective CAML for Scientists
http://www.ffconsultancy.com/products/ocaml_for_scientists

--Boundary-00=_qmZcC/AKsXUTR7l
Content-Type: text/x-c++src;
  charset="iso-8859-1";
  name="ray.cpp"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filename="ray.cpp"

#include <vector>
#include <iostream>
#include <limits>
#include <cmath>
using namespace std;
numeric_limits<double> dbl;
double delta = sqrt(dbl.epsilon()), infinity = dbl.infinity(), pi = M_PI;
struct Vec { // 3D vector
  double x, y, z;
  Vec(double x2, double y2, double z2) : x(x2), y(y2), z(z2) {}
};
Vec operator+(Vec a, Vec b) { return Vec(a.x + b.x, a.y + b.y, a.z + b.z); }
Vec operator-(Vec a, Vec b) { return Vec(a.x - b.x, a.y - b.y, a.z - b.z); }
Vec operator*(double a, Vec b) { return Vec(a * b.x, a * b.y, a * b.z); }
double dot(Vec a, Vec b) { return a.x*b.x + a.y*b.y + a.z*b.z; }
Vec unitise(Vec a) { return (1 / sqrt(dot(a, a))) * a; }
struct Ray { Vec orig, dir; Ray(Vec o, Vec d) : orig(o), dir(d) {} };
struct Scene { // Abstract base class representing a scene
  virtual void intersect(double &, Vec &, Ray) = 0;
  virtual void del() {};
};
struct Sphere : public Scene { // Derived class representing a sphere
  Vec center;
  double radius;
  Sphere(Vec c, double r) : center(c), radius(r) {}
  double ray_sphere(Ray ray) { // Intersection of a ray with a sphere
    Vec v = center - ray.orig;
    double b = dot(v, ray.dir), disc = b*b - dot(v, v) + radius * radius;
    if (disc < 0) return infinity;
    double d = sqrt(disc), t2 = b + d;
    if (t2 < 0) return infinity;
    double t1 = b - d;
    return (t1 > 0 ? t1 : t2);
  }
  void intersect(double &lambda, Vec &normal, Ray ray) {
    double l = ray_sphere(ray);
    if (l >= lambda) return;
    lambda = l;
    normal = unitise(ray.orig + l * ray.dir - center);
  }
};
struct Group : public Scene { // Derived class representing a group of scenes
  Sphere bound;
  vector<Scene *> objs;
  Group(Sphere b) : bound(b), objs(0) {}
  void del() {
    for (vector<Scene *>::iterator it=objs.begin(); it!=objs.end(); ++it)
      delete *it;
  }
  void intersect(double &lambda, Vec &normal, Ray ray) {
    double l = bound.ray_sphere(ray);
    if (l >= lambda) return;
    for (vector<Scene *>::iterator it=objs.begin(); it!=objs.end(); ++it)
      (*it)->intersect(lambda, normal, ray);
  }
};
Vec light = unitise(Vec(-1, -3, 2));
double ray_trace(double weight, Ray ray, Scene *scene) { // Trace a ray
  double lambda = infinity;
  Vec normal(0, 0, 0);
  scene->intersect(lambda, normal, ray);
  if (lambda == infinity) return 0;
  Vec o = ray.orig + lambda * ray.dir + delta * normal;
  double g = max(0., -dot(normal, light)), l = infinity;
  scene->intersect(l, normal, Ray(o, Vec(0, 0, 0) - light));
  return (l == infinity ? g : 0);
}
Scene *create(int level, double r, double x, double y, double z) {
  Sphere *sphere = new Sphere(Vec(x, y, z), r);
  if (level == 1) return sphere;
  Group group = Group(Sphere(Vec(x, y, z), 3*r));
  group.objs.push_back(sphere);
  double rn = 3*r/sqrt(12.);
  for (int dz=-1; dz<=1; dz+=2)
    for (int dx=-1; dx<=1; dx+=2)
      group.objs.push_back(create(level-1, r/2, x - dx*rn, y + rn, z - dz*rn));
  return (new Group(group));
}
int main(int argc, char *argv[]) {
  int level = (argc==2 ? atoi(argv[1]) : 6), w = 512, h = 512, ss = 4;
  Scene *scene=create(level, 1, 0, -1, 0); // Build the scene
  cout << "P2\n" << w << " " << h << "\n256\n";
  for (int y=h-1; y>=0; --y) {
    for (int x=0; x<w; ++x) {
      double g=0;
      for (int dx=0; dx<ss; ++dx)
	for (int dy=0; dy<ss; ++dy) {
	  Vec d(x+double(dx)/ss-w/2, y+double(dy)/ss-h/2, max(w, h));
	  g += ray_trace(1, Ray(Vec(0, 0, -4), unitise(d)), scene);
	}
      cout << int(256. * g / (ss*ss)) << " ";
    }
    cout << endl;
  }
  scene->del(); // Deallocate the scene
  return 0;
}

--Boundary-00=_qmZcC/AKsXUTR7l
Content-Type: text/x-csrc;
  charset="iso-8859-1";
  name="ray10.ml"
Content-Transfer-Encoding: 7bit
Content-Disposition: attachment;
	filename="ray10.ml"

let delta = sqrt epsilon_float and pi = 4. *. atan 1.
type vec = {x:float; y:float; z:float}
let ( *| ) s r = {x=s *. r.x; y=s *. r.y; z=s *. r.z}
let ( +| ) a b = {x=a.x +. b.x; y=a.y +. b.y; z=a.z +. b.z}
let ( -| ) a b = {x=a.x -. b.x; y=a.y -. b.y; z=a.z -. b.z}
let dot a b = a.x *. b.x +. a.y *. b.y +. a.z *. b.z
let unitise r = (1. /. sqrt (dot r r)) *| r
type sphere = { center: vec; radius: float } and ray = { orig: vec; dir: vec }
type obj = Sphere of vec * float * float | Group of vec * float * obj array
let ray_sphere ray center radius =
  let v = center -| ray.orig in
  let b = dot v ray.dir in
  let disc = b *. b -. dot v v +. radius *. radius in
  if disc < 0. then infinity else
    let disc = sqrt disc in
    (fun t2 -> if t2 < 0. then infinity else
       ((fun t1 -> if t1 > 0. then t1 else t2) (b -. disc))) (b +. disc)
let intersect ray scene =
  let rec of_scene ((l, _, _) as first) = function
      Sphere (center, radius, material) ->
	let l' = ray_sphere ray center radius in
	if l' >= l then first else (* No nearer *)
	  let normal = unitise (ray.orig +| l' *| ray.dir -| center) in
	  l', normal, material (* Replace with nearer intersection *)
    | Group (center, radius, scenes) ->
	let l' = ray_sphere ray center radius in (* Cull if possible *)
	if l' >= l then first else Array.fold_left of_scene first scenes in
  of_scene (infinity, {x=0.; y=0.; z=0.}, 0.) scene
let rec ray_trace weight light ray scene = match intersect ray scene with
    lambda, n, color ->
      if lambda = infinity then 0. else
	let o = ray.orig +| lambda *| ray.dir +| delta *| n in
	(match intersect {orig=o; dir = {x=0.; y=0.; z=0.} -| light} scene with
	   l, _, _ when l = infinity -> max 0. (-. dot n light)
	 | _ -> 0.) *. color
let () =
  let level = match Sys.argv with [| _; l |] -> int_of_string l | _ -> 6 in
  let light = unitise {x= -1.; y= -3.; z=2.} in
  let scene =
    let rec aux level r (x, y, z) =
      let obj = Sphere ({x=x; y=y; z=z}, r, 1.) in
      if level = 1 then obj else begin
	let aux l (x', y', z') =
	  aux (level-1) (0.5 *. r) (x -. x', y +. y', z +. z') :: l in
	let objs = let r' = 3. *. r /. sqrt 12. in
	Array.fold_left aux [obj]
	  [|-.r', r', -.r'; r', r', -.r'; -.r', r', r'; r', r', r'|] in
	Group ({x=x; y=y; z=z}, 3. *. r, Array.of_list objs)
      end in
    aux level 1. (0., -1., 0.) in
  let w, h = 512, 512 and ss = 4 in
  Printf.printf "P2\n%d %d\n256\n" w h;
  for y = h - 1 downto 0 do
    for x = 0 to w - 1 do
      let g = ref 0. in
      for dx = 0 to ss - 1 do
	for dy = 0 to ss - 1 do
	  let ray =
	    {orig = {x=0.; y=0.; z= -4.};
	     dir = unitise {x = float (x - w / 2) +. float dx /. float ss;
			    y = float (y - h / 2) +. float dy /. float ss;
			    z=float (max w h)} } in
	  g := !g +. ray_trace 1. light ray scene;
	done;
      done;
      Printf.printf "%d " (int_of_float (256. *. !g /. float (ss * ss)));
    done;
    Printf.printf "\n";
  done;

--Boundary-00=_qmZcC/AKsXUTR7l--