blob: e536a57b4cc2c40115da3f8657d36538429d5e15 [file] [log] [blame]
// The ray tracer code in this file is written by Adam Burmister.
// It is available in its original form from:
//
// http://labs.flog.nz.co/raytracer/
//
// It has been modified slightly by Google to work as a standalone benchmark,
// but all the computational code remains untouched.
// For JetStream3, this code was rewritten using ES6 classes and private fields,
// dropping namespaces and Prototype.js class system, as well as slightly refactored.
// All the computational code still remains untouched.
class Color {
#red = 0;
#green = 0;
#blue = 0;
constructor(red, green, blue) {
this.#red = red;
this.#green = green;
this.#blue = blue;
}
static add(c1, c2) {
return new Color(c1.#red + c2.#red, c1.#green + c2.#green, c1.#blue + c2.#blue);
}
static addScalar(c1, s) {
return new Color(c1.#red + s, c1.#green + s, c1.#blue + s).limit();
}
static multiply(c1, c2) {
return new Color(c1.#red * c2.#red, c1.#green * c2.#green, c1.#blue * c2.#blue);
}
static multiplyScalar(c1, f) {
return new Color(c1.#red * f, c1.#green * f, c1.#blue * f);
}
static blend(c1, c2, w) {
return Color.add(
Color.multiplyScalar(c1, 1 - w),
Color.multiplyScalar(c2, w),
);
}
limit() {
this.#red = this.#red > 0 ? (this.#red > 1 ? 1 : this.#red) : 0;
this.#green = this.#green > 0 ? (this.#green > 1 ? 1 : this.#green) : 0;
this.#blue = this.#blue > 0 ? (this.#blue > 1 ? 1 : this.#blue) : 0;
return this;
}
brightness() {
const r = Math.floor(this.#red * 255);
const g = Math.floor(this.#green * 255);
const b = Math.floor(this.#blue * 255);
return (r * 77 + g * 150 + b * 29) >> 8;
}
toString() {
const r = Math.floor(this.#red * 255);
const g = Math.floor(this.#green * 255);
const b = Math.floor(this.#blue * 255);
return `rgb(${r},${g},${b})`;
}
}
class Light {
static #defaultColor = new Color(0, 0, 0);
#position = null;
#color = Light.#defaultColor;
constructor(position, color) {
this.#position = position;
this.#color = color;
}
get position() { return this.#position; }
get color() { return this.#color; }
toString() {
return `Light [${this.position}]`;
}
}
class Vector {
#x = 0;
#y = 0;
#z = 0;
static add(v, w) {
return new Vector(w.#x + v.#x, w.#y + v.#y, w.#z + v.#z);
}
static subtract(v, w) {
return new Vector(v.#x - w.#x, v.#y - w.#y, v.#z - w.#z);
}
static multiplyScalar(v, w) {
return new Vector(v.#x * w, v.#y * w, v.#z * w);
}
constructor(x, y, z) {
this.#x = x;
this.#y = y;
this.#z = z;
}
get x() { return this.#x; }
get y() { return this.#y; }
get z() { return this.#z; }
normalize() {
const m = this.#magnitude();
return new Vector(this.#x / m, this.#y / m, this.#z / m);
}
negateY() {
this.#y *= -1;
}
#magnitude() {
return Math.sqrt((this.#x * this.#x) + (this.#y * this.#y) + (this.#z * this.#z));
}
cross(w) {
return new Vector(
-this.#z * w.#y + this.#y * w.#z,
this.#z * w.#x - this.#x * w.#z,
-this.#y * w.#x + this.#x * w.#y,
);
}
dot(w) {
return this.#x * w.#x + this.#y * w.#y + this.#z * w.#z;
}
toString() {
return `Vector [${this.#x},${this.#y},${this.#z}]`;
}
}
class Ray {
#position = null;
#direction = null;
constructor(position, direction) {
this.#position = position;
this.#direction = direction;
}
get position() { return this.#position; }
get direction() { return this.#direction; }
toString() {
return `Ray [${this.position},${this.direction}]`;
}
}
class Scene {
#camera = null;
#background = null;
#shapes = [];
#lights = [];
constructor(camera, background, shapes, lights) {
this.#camera = camera;
this.#background = background;
this.#shapes = shapes;
this.#lights = lights;
}
get camera() { return this.#camera; }
get background() { return this.#background; }
get shapes() { return this.#shapes; }
get lights() { return this.#lights; }
}
class Material {
#reflection = 0;
#transparency = 0;
#gloss = 0;
#hasTexture = false;
constructor(reflection, transparency, gloss, hasTexture) {
this.#reflection = reflection;
this.#transparency = transparency;
this.#gloss = gloss;
this.#hasTexture = hasTexture;
}
get reflection() { return this.#reflection; }
get transparency() { return this.#transparency; }
get gloss() { return this.#gloss; }
get hasTexture() { return this.#hasTexture; }
getColor() {
throw new Error("getColor() isn't implemented");
}
toString() {
return `Material [gloss=${this.gloss}, transparency=${this.transparency}, hasTexture=${this.hasTexture}]`;
}
}
class SolidMaterial extends Material {
static #defaultColor = new Color(0, 0, 0);
#color = SolidMaterial.#defaultColor;
constructor(color, reflection, transparency, gloss) {
super(reflection, transparency, gloss, true);
this.#color = color;
}
getColor() {
return this.#color;
}
toString() {
return `SolidMaterial [gloss=${this.gloss}, transparency=${this.transparency}, hasTexture=${this.hasTexture}]`;
}
}
class ChessboardMaterial extends Material {
static #defaultColorEven = new Color(0, 0, 0);
static #defaultColorOdd = new Color(0, 0, 0);
#colorEven = ChessboardMaterial.#defaultColorEven;
#colorOdd = ChessboardMaterial.#defaultColorOdd;
#density = 1;
constructor(colorEven, colorOdd, reflection, transparency, gloss, density) {
super(reflection, transparency, gloss, true);
this.#colorEven = colorEven;
this.#colorOdd = colorOdd;
this.#density = density;
}
#wrapUp(t) {
t %= 2;
if (t < -1) t += 2;
if (t >= 1) t -= 2;
return t;
}
getColor(u, v) {
const t = this.#wrapUp(u * this.#density) * this.#wrapUp(v * this.#density);
return t < 0 ? this.#colorEven : this.#colorOdd;
}
toString() {
return `ChessMaterial [gloss=${this.gloss}, transparency=${this.transparency}, hasTexture=${this.hasTexture}]`;
}
}
class Shape {
#position = null;
#material = null;
constructor(position, material) {
this.#position = position;
this.#material = material;
}
get position() { return this.#position; }
get material() { return this.#material; }
intersect(ray) {
throw new Error("intersect() isn't implemented");
}
}
class Sphere extends Shape {
#radius = 0;
constructor(position, material, radius) {
super(position, material);
this.#radius = radius;
}
intersect(ray) {
const info = new IntersectionInfo();
info.shape = this;
const dst = Vector.subtract(ray.position, this.position);
const B = dst.dot(ray.direction);
const C = dst.dot(dst) - (this.#radius * this.#radius);
const D = (B * B) - C;
if (D > 0) { // intersection!
info.isHit = true;
info.distance = (-B) - Math.sqrt(D);
info.position = Vector.add(ray.position, Vector.multiplyScalar(ray.direction, info.distance));
info.normal = Vector.subtract(info.position, this.position).normalize();
info.color = this.material.getColor(0, 0);
} else {
info.isHit = false;
}
return info;
}
toString() {
return `Sphere [position=${this.position}, radius=${this.#radius}]`;
}
}
class Plane extends Shape {
#d = 0;
constructor(position, material, d) {
super(position, material);
this.#d = d;
}
intersect(ray) {
const info = new IntersectionInfo();
info.shape = this;
const Vd = this.position.dot(ray.direction);
if (Vd === 0) return info; // no intersection
const t = -(this.position.dot(ray.position) + this.#d) / Vd;
if (t <= 0) return info;
info.isHit = true;
info.position = Vector.add(ray.position, Vector.multiplyScalar(ray.direction, t));
info.normal = this.position;
info.distance = t;
if (this.material.hasTexture) {
const vU = new Vector(this.position.y, this.position.z, -this.position.x);
const vV = vU.cross(this.position);
const u = info.position.dot(vU);
const v = info.position.dot(vV);
info.color = this.material.getColor(u, v);
} else {
info.color = this.material.getColor(0, 0);
}
return info;
}
toString() {
return `Plane [${this.position}, d=${this.#d}]`;
}
}
class IntersectionInfo {
static #defaultColor = new Color(0, 0, 0);
isHit = false;
hitCount = 0;
shape = null;
position = null;
normal = null;
color = IntersectionInfo.#defaultColor;
distance = null;
toString() {
return `Intersection [${this.position}]`;
}
}
class Camera {
#position = null;
#lookAt = null;
#up = null;
#equator = null;
#screen = null;
constructor(position, lookAt, up) {
this.#position = position;
this.#lookAt = lookAt;
this.#up = up;
this.#equator = this.#lookAt.normalize().cross(this.#up);
this.#screen = Vector.add(this.#position, this.#lookAt);
}
get position() { return this.#position; }
getRay(vx, vy) {
const pos = Vector.subtract(
this.#screen,
Vector.subtract(
Vector.multiplyScalar(this.#equator, vx),
Vector.multiplyScalar(this.#up, vy),
),
);
pos.negateY();
const dir = Vector.subtract(pos, this.#position);
return new Ray(pos, dir.normalize());
}
toString() {
return `Camera [${this.position}]`;
}
}
class Background {
static #defaultColor = new Color(0, 0, 0);
#color = Background.#defaultColor;
#ambience = 0;
constructor(color, ambience) {
this.#color = color;
this.#ambience = ambience;
}
get color() { return this.#color; }
get ambience() { return this.#ambience; }
toString() {
return `Background [${this.color}]`;
}
}
class Engine {
// Variable used to hold a number that can be used to verify that
// the scene was ray traced correctly.
#checkNumber = 0;
#options = {};
constructor(options) {
this.#options = {
canvasHeight: 100,
canvasWidth: 100,
pixelWidth: 2,
pixelHeight: 2,
renderDiffuse: false,
renderShadows: false,
renderHighlights: false,
renderReflections: false,
rayDepth: 2,
...options,
};
this.#options.canvasHeight /= this.#options.pixelHeight;
this.#options.canvasWidth /= this.#options.pixelWidth;
}
renderScene(scene) {
for (let x = 0; x < this.#options.canvasWidth; x++) {
for (let y = 0; y < this.#options.canvasHeight; y++) {
const xp = x * 1 / this.#options.canvasWidth * 2 - 1;
const yp = y * 1 / this.#options.canvasHeight * 2 - 1;
const ray = scene.camera.getRay(xp, yp);
const color = this.#getPixelColor(ray, scene);
this.#setPixel(x, y, color);
}
}
if (this.#checkNumber !== 2321)
throw new Error("Scene rendered incorrectly");
}
#getPixelColor(ray, scene) {
const info = this.#testIntersection(ray, scene, null);
if (info.isHit)
return this.#rayTrace(info, ray, scene, 0);
return scene.background.color;
}
#setPixel(x, y, color) {
if (x === y)
this.#checkNumber += color.brightness();
}
#testIntersection(ray, scene, exclude) {
let hitCount = 0;
let best = new IntersectionInfo();
best.distance = 2000;
for (let i = 0; i < scene.shapes.length; i++) {
const shape = scene.shapes[i];
if (shape !== exclude) {
const info = shape.intersect(ray);
if (info.isHit && info.distance >= 0 && info.distance < best.distance) {
best = info;
hitCount++;
}
}
}
best.hitCount = hitCount;
return best;
}
#getReflectionRay(P, N, V) {
const c1 = -N.dot(V);
const R1 = Vector.add(Vector.multiplyScalar(N, 2 * c1), V);
return new Ray(P, R1);
}
#rayTrace(info, ray, scene, depth) {
// Calc ambient
let color = Color.multiplyScalar(info.color, scene.background.ambience);
const shininess = 10 ** (info.shape.material.gloss + 1);
for (let i = 0; i < scene.lights.length; i++) {
const light = scene.lights[i];
// Calc diffuse lighting
const v = Vector.subtract(light.position, info.position).normalize();
if (this.#options.renderDiffuse) {
const L = v.dot(info.normal);
if (L > 0) {
color = Color.add(
color,
Color.multiply(
info.color,
Color.multiplyScalar(light.color, L),
),
);
}
}
// The greater the depth the more accurate the colours, but
// this is exponentially (!) expensive
if (depth <= this.#options.rayDepth) {
// calculate reflection ray
if (this.#options.renderReflections && info.shape.material.reflection > 0) {
const reflectionRay = this.#getReflectionRay(info.position, info.normal, ray.direction);
const refl = this.#testIntersection(reflectionRay, scene, info.shape);
if (refl.isHit && refl.distance > 0) {
refl.color = this.#rayTrace(refl, reflectionRay, scene, depth + 1);
} else {
refl.color = scene.background.color;
}
color = Color.blend(
color,
refl.color,
info.shape.material.reflection,
);
}
}
// Render shadows and highlights
let shadowInfo = new IntersectionInfo();
if (this.#options.renderShadows) {
const shadowRay = new Ray(info.position, v);
shadowInfo = this.#testIntersection(shadowRay, scene, info.shape);
if (shadowInfo.isHit && shadowInfo.shape !== info.shape) {
const vA = Color.multiplyScalar(color, 0.5);
const dB = 0.5 * (shadowInfo.shape.material.transparency ** 0.5);
color = Color.addScalar(vA, dB);
}
}
// Phong specular highlights
if (this.#options.renderHighlights && !shadowInfo.isHit && info.shape.material.gloss > 0) {
const Lv = Vector.subtract(info.shape.position, light.position).normalize();
const E = Vector.subtract(scene.camera.position, info.shape.position).normalize();
const H = Vector.subtract(E, Lv).normalize();
const glossWeight = Math.max(info.normal.dot(H), 0) ** shininess;
color = Color.add(Color.multiplyScalar(light.color, glossWeight), color);
}
}
return color.limit();
}
}
function renderScene() {
const camera = new Camera(
new Vector(0, 0, -15),
new Vector(-0.2, 0, 5),
new Vector(0, 1, 0),
);
const background = new Background(new Color(0.5, 0.5, 0.5), 0.4);
const shapes = [
new Sphere(
new Vector(-1.5, 1.5, 2),
new SolidMaterial(new Color(0, 0.5, 0.5), 0.3, 0, 2),
1.5,
),
new Sphere(
new Vector(1, 0.25, 1),
new SolidMaterial(new Color(0.9, 0.9, 0.9), 0.1, 0, 1.5),
0.5,
),
new Plane(
new Vector(0.1, 0.9, -0.5).normalize(),
new ChessboardMaterial(
new Color(1, 1, 1),
new Color(0, 0, 0),
0.2, 0, 1, 0.7,
),
1.2,
),
];
const lights = [
new Light(
new Vector(5, 10, -1),
new Color(0.8, 0.8, 0.8),
),
new Light(
new Vector(-3, 5, -15),
new Color(0.8, 0.8, 0.8),
),
];
const scene = new Scene(camera, background, shapes, lights);
const raytracer = new Engine({
canvasWidth: 100,
canvasHeight: 100,
pixelWidth: 5,
pixelHeight: 5,
renderDiffuse: true,
renderHighlights: true,
renderShadows: true,
renderReflections: true,
rayDepth: 2,
});
raytracer.renderScene(scene);
}
class Benchmark {
runIteration() {
for (let i = 0; i < 15; ++i)
renderScene();
}
}