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.
// 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();
/// 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();
});
}