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