blob: a6cb37fffdfa229c36b75c39074cdfa3ae57b752 [file] [log] [blame] [edit]
// 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 public 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;
}
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;
}
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;
}
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;
}
}
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;
}
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;
}
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);
}
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;
}
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();
}
}