Raytracer: TypeScript & Dart

Both programs were compiled to JavaScript. There's also a fullscreen version of the Dart raytracer here. Tip: Widen the browser window to see code side-by-side.

TypeScript code (from official repo)


// Reformated to fit in 80 character width. Added start button functionality
// and basic performance testing in the exec() method.

class Vector {
  constructor(public x: number,
        public y: number,
        public z: number) {
  }
  static times(k: number, v: Vector) {
    return new Vector(k * v.x, k * v.y, k * v.z);
  }
  static minus(v1: Vector, v2: Vector) {
    return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);
  }
  static plus(v1: Vector, v2: Vector) {
    return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z);
  }
  static dot(v1: Vector, v2: Vector) {
    return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
  }
  static mag(v: Vector) {
    return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
  }
  static norm(v: Vector) {
    var mag = Vector.mag(v);
    var div = (mag === 0) ? Infinity : 1.0 / mag;
    return Vector.times(div, v);
  }
  static cross(v1: Vector, v2: Vector) {
    return new Vector(v1.y * v2.z - v1.z * v2.y, v1.z * v2.x - v1.x * v2.z,
        v1.x * v2.y - v1.y * v2.x);
  }
}

class Color {
  constructor(public r: number,
        public g: number,
        public b: number) {
  }
  static scale(k: number, v: Color) {
    return new Color(k * v.r, k * v.g, k * v.b);
  }
  static plus(v1: Color, v2: Color) {
    return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b);
  }
  static times(v1: Color, v2: Color) {
    return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b);
  }
  static white = new Color(1.0, 1.0, 1.0);
  static grey = new Color(0.5, 0.5, 0.5);
  static black = new Color(0.0, 0.0, 0.0);
  static background = Color.black;
  static defaultColor = Color.black;
  static toDrawingColor(c: Color) {
    var legalize = d => d > 1 ? 1 : d;
    return {
      r: Math.floor(legalize(c.r) * 255),
      g: Math.floor(legalize(c.g) * 255),
      b: Math.floor(legalize(c.b) * 255)
    }
  }
}

class Camera {
  public forward: Vector;
  public right: Vector;
  public up: Vector;

  constructor(public pos: Vector, lookAt: Vector) {
    var down = new Vector(0.0, -1.0, 0.0);
    this.forward = Vector.norm(Vector.minus(lookAt, this.pos));
    this.right = Vector.times(1.5,
      Vector.norm(Vector.cross(this.forward, down)));
    this.up = Vector.times(1.5,
      Vector.norm(Vector.cross(this.forward, this.right)));
  }
}

interface Ray {
  start: Vector;
  dir: Vector;
}

interface Intersection {
  thing: Thing;
  ray: Ray;
  dist: number;
}


interface Surface {
  diffuse: (pos: Vector) => Color;
  specular: (pos: Vector) => Color;
  reflect: (pos: Vector) => number;
  roughness: number;
}

interface Thing {
  intersect: (ray: Ray) => Intersection;
  normal: (pos: Vector) => Vector;
  surface: Surface;
}

interface Light {
  pos: Vector;
  color: Color;
}


interface Scene {
  things: Thing[];
  lights: Light[];
  camera: Camera;
}


class Sphere implements Thing {
  public radius2: number;

  constructor(public center: Vector, radius: number,
        public surface: Surface) {
    this.radius2 = radius * radius;
  }
  normal(pos: Vector): Vector {
    return Vector.norm(Vector.minus(pos, this.center));
  }
  intersect(ray: Ray) {
    var eo = Vector.minus(this.center, ray.start);
    var v = Vector.dot(eo, ray.dir);
    var dist = 0;
    if (v >= 0) {
      var disc = this.radius2 - (Vector.dot(eo, eo) - v * v);
      if (disc >= 0) {
        dist = v - Math.sqrt(disc);
      }
    }
    if (dist === 0) {
      return null;
    } else {
      return { thing: this, ray: ray, dist: dist };
    }
  }
}

class Plane implements Thing {
  public normal: (pos: Vector) =>Vector;
  public intersect: (ray: Ray) =>Intersection;
  constructor(norm: Vector, offset: number, public surface: Surface) {
    this.normal = function(pos: Vector) { return norm; }
    this.intersect = function(ray: Ray): Intersection {
      var denom = Vector.dot(norm, ray.dir);
      if (denom > 0) {
        return null;
      } else {
        var dist = (Vector.dot(norm, ray.start) + offset) / (-denom);
        return { thing: this, ray: ray, dist: dist };
      }
    }
  }
}



module Surfaces {
  export var shiny: Surface = {
    diffuse: function(pos) { return Color.white; },
    specular: function(pos) { return Color.grey; },
    reflect: function(pos) { return 0.7; },
    roughness: 250
  }
  export var checkerboard: Surface = {
    diffuse: function(pos) {
      if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) {
        return Color.white;
      } else {
        return Color.black;
      }
    },
    specular: function(pos) { return Color.white; },
    reflect: function(pos) {
      if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) {
        return 0.1;
      } else {
        return 0.7;
      }
    },
    roughness: 150
  }
}








class RayTracer {
  private maxDepth = 5;

  private intersections(ray: Ray, scene: Scene) {
    var closest = +Infinity;
    var closestInter: Intersection = undefined;
    for (var i in scene.things) {
      var inter = scene.things[i].intersect(ray);
      if (inter != null && inter.dist < closest) {
        closestInter = inter;
        closest = inter.dist;
      }
    }
    return closestInter;
  }

  private testRay(ray: Ray, scene: Scene) {
    var isect = this.intersections(ray, scene);
    if (isect != null) {
      return isect.dist;
    } else {
      return undefined;
    }
  }

  private traceRay(ray: Ray, scene: Scene, depth: number): Color {
    var isect = this.intersections(ray, scene);
    if (isect === undefined) {
      return Color.background;
    } else {
      return this.shade(isect, scene, depth);
    }
  }

  private shade(isect: Intersection, scene: Scene, depth: number) {
    var d = isect.ray.dir;
    var pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start);
    var normal = isect.thing.normal(pos);
    var reflectDir = Vector.minus(d,
      Vector.times(2, Vector.times(Vector.dot(normal, d), normal)));
    var naturalColor = Color.plus(
      Color.background,
      this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene));
    var reflectedColor = (depth >= this.maxDepth)
      ? Color.grey
      : this.getReflectionColor(
        isect.thing, pos, normal, reflectDir, scene, depth);
    return Color.plus(naturalColor, reflectedColor);
  }

  private getReflectionColor(thing: Thing, pos: Vector, normal: Vector,
                 rd: Vector, scene: Scene, depth: number) {
    return Color.scale(thing.surface.reflect(pos),
      this.traceRay({ start: pos, dir: rd }, scene, depth + 1));
  }


  private getNaturalColor(thing: Thing, pos: Vector, norm: Vector,
              rd: Vector, scene: Scene) {
    var addLight = (col, light) => {
      var ldis = Vector.minus(light.pos, pos);
      var livec = Vector.norm(ldis);
      var neatIsect = this.testRay({ start: pos, dir: livec }, scene);
      var isInShadow = (neatIsect === undefined)
        ? false : (neatIsect <= Vector.mag(ldis));
      if (isInShadow) {
        return col;
      } else {
        var illum = Vector.dot(livec, norm);
        var lcolor = (illum > 0) ? Color.scale(illum, light.color)
                      : Color.defaultColor;
        var specular = Vector.dot(livec, Vector.norm(rd));
        var scolor = (specular > 0)
          ? Color.scale(Math.pow(specular, thing.surface.roughness), light.color)
          : Color.defaultColor;
        return Color.plus(col,
          Color.plus(Color.times(thing.surface.diffuse(pos), lcolor),
            Color.times(thing.surface.specular(pos), scolor)));
      }
    }
    return scene.lights.reduce(addLight, Color.defaultColor);
  }

  render(scene, ctx, screenWidth, screenHeight) {
    var getPoint = (x, y, camera) => {
      var recenterX = x =>(x - (screenWidth / 2.0)) / 2.0 / screenWidth;
      var recenterY =
        y => - (y - (screenHeight / 2.0)) / 2.0 / screenHeight;
      return Vector.norm(Vector.plus(camera.forward,
        Vector.plus(Vector.times(recenterX(x), camera.right),
          Vector.times(recenterY(y), camera.up))));
    }
    for (var y = 0; y < screenHeight; y++) {
      for (var x = 0; x < screenWidth; x++) {
        var color = this.traceRay(
          { start: scene.camera.pos,
            dir: getPoint(x, y, scene.camera) }, scene, 0);
        var c = Color.toDrawingColor(color);
        ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) +
          ", " + String(c.b) + ")";
        ctx.fillRect(x, y, x + 1, y + 1);
      }
    }
  }
}

function defaultScene(): Scene {
  return {
    things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard),
         new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny),
         new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)],
    lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) },
         { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) },
         { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) },
         { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }],
    camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0))
  };
}

function exec() {
  let width = 256;
  let height = 256;

  let canvas : HTMLCanvasElement =
      <HTMLCanvasElement>document.querySelector("#ts-canvas");
  let info : Element = document.querySelector("#ts-info");
  let button : HTMLButtonElement =
      <HTMLButtonElement>document.querySelector("#ts-button");

  let i = 0;
  let rendersPerBatch = 1;  // Change to run several raytraces on first click.
  let times = [];

  button.onclick = (e) => {
    info.textContent = "Rendering...";
    let ctx = canvas.getContext("2d");
    ctx.clearRect(0, 0, width, height);
    // Take the time to show the above to user.
    window.setTimeout((_) => {
      let start = window.performance.now();
      let rayTracer = new RayTracer();
      rayTracer.render(defaultScene(), ctx, width, height);
      var time = window.performance.now() - start;
      info.textContent = `Rendered in ${Math.round(time)} ms.`;
      times.push(time);
      console.log(times);
      i++;
      if (i < rendersPerBatch) button.click();
    }, 100);
  };
}

exec();


            

Dart code (from here)


/// Line-for-line port of the TypeScript raytracer to idiomatic Dart. Type
/// information added. This code is null safe.

library ts_raytracer;

import 'dart:async';
import 'dart:html';
import 'dart:math';

class Vector {
  final double x;
  final double y;
  final double z;

  const Vector(this.x, this.y, this.z);

  Vector operator *(num k) => Vector(k * x, k * y, k * z);
  Vector operator -(Vector o) => Vector(x - o.x, y - o.y, z - o.z);
  Vector operator +(Vector o) => Vector(x + o.x, y + o.y, z + o.z);
  double dot(Vector o) => x * o.x + y * o.y + z * o.z;
  double mag() => sqrt(x * x + y * y + z * z);
  Vector norm() {
    var mag = this.mag();
    var div = (mag == 0) ? double.infinity : 1.0 / mag;
    return this * div;
  }

  Vector cross(Vector o) =>
      Vector(y * o.z - z * o.y, z * o.x - x * o.z, x * o.y - y * o.x);
}

class Color {
  final double r;
  final double g;
  final double b;
  const Color(this.r, this.g, this.b);

  Color scale(double k) => Color(k * r, k * g, k * b);
  Color operator +(Color v) => Color(r + v.r, g + v.g, b + v.b);
  Color operator *(Color v) => Color(r * v.r, g * v.g, b * v.b);

  static var white = Color(1.0, 1.0, 1.0);
  static var grey = Color(0.5, 0.5, 0.5);
  static var black = Color(0.0, 0.0, 0.0);
  static var background = Color.black;
  static var defaultColor = Color.black;

  DrawingColor toDrawingColor() {
    var legalize = (num d) => d > 1 ? 1 : d;
    return DrawingColor(
        r: (legalize(r) * 255).toInt(),
        g: (legalize(g) * 255).toInt(),
        b: (legalize(b) * 255).toInt());
  }
}

class DrawingColor {
  final int r;
  final int g;
  final int b;
  const DrawingColor({required this.r, required this.g, required this.b});
}

class Camera {
  Vector pos;
  final Vector forward;
  final Vector right;
  final Vector up;

  factory Camera(Vector pos, Vector lookAt) {
    var down = Vector(0.0, -1.0, 0.0);
    var forward = (lookAt - pos).norm();
    var right = forward.cross(down).norm() * 1.5;
    var up = forward.cross(right).norm() * 1.5;
    return Camera._(pos, forward, right, up);
  }

  Camera._(this.pos, this.forward, this.right, this.up);
}

class Ray {
  final Vector start;
  final Vector dir;

  const Ray(this.start, this.dir);
}

class Intersection {
  final Thing thing;
  final Ray ray;
  final double dist;

  const Intersection(this.thing, this.ray, this.dist);
}

abstract class Surface {
  Color diffuse(Vector pos);
  Color specular(Vector pos);
  double reflect(Vector pos);
  int get roughness;
}

abstract class Thing {
  Intersection? intersect(Ray ray);
  Vector normal(Vector pos);
  Surface get surface;
}

class Light {
  final Vector pos;
  final Color color;

  const Light(this.pos, this.color);
}

class Scene {
  final List<Thing> things;
  final List<Light> lights;
  final Camera camera;

  const Scene(this.things, this.lights, this.camera);
}

class Sphere implements Thing {
  final double radius;
  final double radius2;
  final Vector center;
  final Surface surface;

  const Sphere(this.center, double radius, this.surface)
      : radius = radius,
        radius2 = radius * radius;

  Vector normal(Vector pos) => (pos - center).norm();
  Intersection? intersect(Ray ray) {
    var eo = center - ray.start;
    var v = eo.dot(ray.dir);
    var dist = 0.0;
    if (v >= 0) {
      var disc = radius2 - (eo.dot(eo) - v * v);
      if (disc >= 0) {
        dist = v - sqrt(disc);
      }
    }
    if (dist == 0) {
      return null;
    } else {
      return Intersection(this, ray, dist);
    }
  }
}

class Plane implements Thing {
  final Vector norm;
  final double offset;
  final Surface surface;

  const Plane(this.norm, this.offset, this.surface);

  Intersection? intersect(Ray ray) {
    var denom = norm.dot(ray.dir);
    if (denom > 0) {
      return null;
    } else {
      var dist = (norm.dot(ray.start) + offset) / (-denom);
      return Intersection(this, ray, dist);
    }
  }

  Vector normal(Vector pos) => norm;
}

// Ugh. We're trying to emulate 'module' here, without a separate file.
// Dart programs don't normally use classes in this way.
class Surfaces {
  static final Surface shiny =
      CustomSurface(((_) => Color.white), ((_) => Color.grey), (_) => 0.7, 250);
  static final Surface checkerboard = CustomSurface(
      (Vector pos) {
        if ((pos.z.floor() + pos.x.floor()) % 2 != 0) {
          return Color.white;
        } else {
          return Color.black;
        }
      },
      ((_) => Color.white),
      (pos) {
        if ((pos.z.floor() + pos.x.floor()) % 2 != 0) {
          return 0.1;
        } else {
          return 0.7;
        }
      },
      150);
}

class CustomSurface implements Surface {
  final Color Function(Vector) _diffuse, _specular;
  final double Function(Vector) _reflect;
  final int roughness;

  const CustomSurface(
      this._diffuse, this._specular, this._reflect, this.roughness);

  Color diffuse(Vector pos) => _diffuse(pos);
  Color specular(Vector pos) => _specular(pos);
  double reflect(Vector pos) => _reflect(pos);
}

class RayTracer {
  static const int _maxDepth = 5;

  Intersection? _intersections(Ray ray, Scene scene) {
    double closest = double.infinity;
    Intersection? closestInter;
    for (Thing thing in scene.things) {
      Intersection? inter = thing.intersect(ray);
      if (inter != null && inter.dist < closest) {
        closestInter = inter;
        closest = inter.dist;
      }
    }
    return closestInter;
  }

  double? _testRay(Ray ray, Scene scene) {
    var isect = _intersections(ray, scene);
    if (isect != null) {
      return isect.dist;
    } else {
      return null;
    }
  }

  Color _traceRay(Ray ray, Scene scene, int depth) {
    var isect = _intersections(ray, scene);
    if (isect == null) {
      return Color.background;
    } else {
      return _shade(isect, scene, depth);
    }
  }

  Color _shade(Intersection isect, Scene scene, int depth) {
    var d = isect.ray.dir;
    var pos = d * isect.dist + isect.ray.start;
    var normal = isect.thing.normal(pos);
    var reflectDir = d - normal * normal.dot(d) * 2.0;
    var naturalColor = Color.background +
        _getNaturalColor(isect.thing, pos, normal, reflectDir, scene);
    var reflectedColor = (depth >= _maxDepth)
        ? Color.grey
        : _getReflectionColor(
            isect.thing, pos, normal, reflectDir, scene, depth);
    return naturalColor + reflectedColor;
  }

  Color _getReflectionColor(Thing thing, Vector pos, Vector normal, Vector rd,
      Scene scene, int depth) {
    var color = _traceRay(Ray(pos, rd), scene, depth + 1);
    var scale = thing.surface.reflect(pos);
    return color.scale(scale);
  }

  Color _getNaturalColor(
      Thing thing, Vector pos, Vector norm, Vector rd, Scene scene) {
    Color addLight(Color col, Light light) {
      var ldis = light.pos - pos;
      var livec = ldis.norm();
      var neatIsect = _testRay(Ray(pos, livec), scene);
      var isInShadow = (neatIsect == null) ? false : (neatIsect <= ldis.mag());
      if (isInShadow) {
        return col;
      } else {
        var illum = livec.dot(norm);
        var lcolor =
            (illum > 0) ? light.color.scale(illum) : Color.defaultColor;
        var specular = livec.dot(rd.norm());
        var scolor = (specular > 0)
            ? light.color
                .scale(pow(specular, thing.surface.roughness) as double)
            : Color.defaultColor;
        return col +
            (thing.surface.diffuse(pos) * lcolor) +
            (thing.surface.specular(pos) * scolor);
      }
    }

    ;
    return scene.lights.fold(Color.defaultColor, addLight);
  }

  void render(Scene scene, CanvasRenderingContext2D ctx, int screenWidth,
      int screenHeight) {
    Vector getPoint(int x, int y, Camera camera) {
      var recenterX = (x) => (x - (screenWidth / 2.0)) / 2.0 / screenWidth;
      var recenterY = (y) => -(y - (screenHeight / 2.0)) / 2.0 / screenHeight;
      return (camera.forward +
              camera.right * recenterX(x) +
              camera.up * recenterY(y))
          .norm();
    }

    for (var y = 0; y < screenHeight; y++) {
      for (var x = 0; x < screenWidth; x++) {
        var color = _traceRay(
            Ray(scene.camera.pos, getPoint(x, y, scene.camera)), scene, 0);
        var c = color.toDrawingColor();
        ctx.fillStyle = "rgb(${c.r}, ${c.g}, ${c.b})";
        ctx.fillRect(x, y, x + 1, y + 1);
      }
    }
  }
}

Scene defaultScene() => Scene([
      Plane(Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard),
      Sphere(Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny),
      Sphere(Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)
    ], [
      Light(Vector(-2.0, 2.5, 0.0), Color(0.49, 0.07, 0.07)),
      Light(Vector(1.5, 2.5, 1.5), Color(0.07, 0.07, 0.49)),
      Light(Vector(1.5, 2.5, -1.5), Color(0.07, 0.49, 0.071)),
      Light(Vector(0.0, 3.5, 0.0), Color(0.21, 0.21, 0.35))
    ], Camera(Vector(3.0, 2.0, 4.0), Vector(-1.0, 0.5, 0.0)));

void main() {
  int width = 256;
  int height = 256;

  CanvasElement canvas = querySelector("#dart-canvas") as CanvasElement;
  Element info = querySelector("#dart-info")!;
  ButtonElement button = querySelector("#dart-button") as ButtonElement;

  int i = 0;
  int rendersPerBatch = 1; // Change to run several raytraces on first click.
  var times = [];

  button.onClick.listen((_) async {
    info.text = "Rendering...";
    var ctx = canvas.context2D;
    ctx.clearRect(0, 0, width, height);
    // Take the time to show the above to user.
    await Future.delayed(Duration(milliseconds: 100));
    var start = window.performance.now();
    var rayTracer = RayTracer();
    rayTracer.render(defaultScene(), ctx, width, height);
    var time = window.performance.now() - start;
    info.text = "Rendered in ${time.round()} ms.";
    times.add(time);
    print(times);
    i++;
    if (i < rendersPerBatch) button.click();
  });
}