UNPKG

jsx

Version:

a faster, safer, easier JavaScript

853 lines (682 loc) 24.7 kB
// -*- mode: jsx; jsx-indent-level: 4; indent-tabs-mode: nil; -*- // 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 the all the computational code remains // untouched. This file also contains a copy of parts of the Prototype // JavaScript framework which is used by the ray tracer. import "./base.jsx"; import "js/web.jsx"; class RayTrace { function constructor() { var rayTrace = new BenchmarkSuite('RayTrace', 739989, [ new Benchmark('RayTrace', function() { Main.renderScene(); }) ]); } } // ------------------------------------------------------------------------ // ------------------------------------------------------------------------ // The rest of this file is the actual ray tracer written by Adam // Burmister. It's a concatenation of the following files: // // flog/color.js // flog/light.js // flog/vector.js // flog/ray.js // flog/scene.js // flog/material/basematerial.js // flog/material/solid.js // flog/material/chessboard.js // flog/shape/baseshape.js // flog/shape/sphere.js // flog/shape/plane.js // flog/intersectioninfo.js // flog/camera.js // flog/background.js // flog/engine.js // This class used to be in Flog.RayTracer namespace class Color { var red = 0.0; var green = 0.0; var blue = 0.0; function constructor() { this(0.0, 0.0, 0.0); } function constructor(r : number, g : number, b : number) { this.red = r; this.green = g; this.blue = b; } static function add(c1 : Color, c2 : Color) : Color { var result = new Color(0,0,0); result.red = c1.red + c2.red; result.green = c1.green + c2.green; result.blue = c1.blue + c2.blue; return result; } static function addScalar(c1 : Color, s : number) : Color { var result = new Color(0,0,0); result.red = c1.red + s; result.green = c1.green + s; result.blue = c1.blue + s; result.limit(); return result; } static function subtract(c1 : Color, c2 : Color) : Color { var result = new Color(0,0,0); result.red = c1.red - c2.red; result.green = c1.green - c2.green; result.blue = c1.blue - c2.blue; return result; } static function multiply(c1 : Color, c2 : Color) : Color { var result = new Color(0,0,0); result.red = c1.red * c2.red; result.green = c1.green * c2.green; result.blue = c1.blue * c2.blue; return result; } static function multiplyScalar(c1 : Color, f : number) : Color { var result = new Color(0,0,0); result.red = c1.red * f; result.green = c1.green * f; result.blue = c1.blue * f; return result; } static function divideFactor(c1 : Color, f : number) : Color { var result = new Color(0,0,0); result.red = c1.red / f; result.green = c1.green / f; result.blue = c1.blue / f; return result; } function limit() : void { this.red = (this.red > 0.0) ? ( (this.red > 1.0) ? 1.0 : this.red ) : 0.0; this.green = (this.green > 0.0) ? ( (this.green > 1.0) ? 1.0 : this.green ) : 0.0; this.blue = (this.blue > 0.0) ? ( (this.blue > 1.0) ? 1.0 : this.blue ) : 0.0; } function distance(color : Color) : number { var d = Math.abs(this.red - color.red) + Math.abs(this.green - color.green) + Math.abs(this.blue - color.blue); return d; } static function blend(c1 : Color, c2 : Color, w : number) : Color { var result = new Color(0,0,0); result = Color.add( Color.multiplyScalar(c1, 1 - w), Color.multiplyScalar(c2, w) ); return result; } function brightness() : number { var r = Math.floor(this.red*255); var g = Math.floor(this.green*255); var b = Math.floor(this.blue*255); return (r * 77 + g * 150 + b * 29) >> 8; } override function toString() : string { var r = Math.floor(this.red*255); var g = Math.floor(this.green*255); var b = Math.floor(this.blue*255); return "rgb("+ r as string +","+ g as string +","+ b as string +")"; } } class Light { var position = null : Vector; var color = null : Color; var intensity = 10.0; function constructor(pos : Vector, color : Color) { this(pos, color, 10.0); } function constructor(pos : Vector, color : Color, intensity : number) { this.position = pos; this.color = color; this.intensity = intensity; } override function toString() : string { return 'Light [' + this.position.x as string + ',' + this.position.y as string + ',' + this.position.z as string + ']'; } } class Vector { var x = 0.0; var y = 0.0; var z = 0.0; function constructor() { this(0,0,0); } function constructor(x : number, y : number, z : number) { this.x = x; this.y = y; this.z = z; } function copy(vector : Vector) : void { this.x = vector.x; this.y = vector.y; this.z = vector.z; } function normalize() : Vector { var m = this.magnitude(); return new Vector(this.x / m, this.y / m, this.z / m); } function magnitude() : number { return Math.sqrt((this.x * this.x) + (this.y * this.y) + (this.z * this.z)); } function cross(w : Vector) : Vector{ 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); } function dot(w : Vector) : number { return this.x * w.x + this.y * w.y + this.z * w.z; } static function add(v : Vector, w : Vector) : Vector { return new Vector(w.x + v.x, w.y + v.y, w.z + v.z); } static function subtract(v : Vector, w : Vector) : Vector { if(!w || !v) throw 'Vectors must be defined [' + v.toString() + ',' + w.toString() + ']'; return new Vector(v.x - w.x, v.y - w.y, v.z - w.z); } static function multiplyVector(v : Vector, w : Vector) : Vector { return new Vector(v.x * w.x, v.y * w.y, v.z * w.z); } static function multiplyScalar(v : Vector, w : number) : Vector { return new Vector(v.x * w, v.y * w, v.z * w); } override function toString() : string { return 'Vector [' + this.x as string + ',' + this.y as string + ',' + this.z as string + ']'; } } class Ray { var position = null : Vector; var direction = null : Vector; function constructor(pos : Vector, dir : Vector) { this.position = pos; this.direction = dir; } override function toString() : string { return 'Ray [' + this.position.toString() + ',' + this.direction.toString() + ']'; } } /* Fake a Flog.* namespace */ class Scene { var camera : Camera; var shapes : Shape[]; var lights : Light[]; var background : Background; function constructor() { this.camera = new Camera( new Vector(0,0,-5), new Vector(0,0,1), new Vector(0,1,0) ); this.shapes = new Array.<Shape>(); this.lights = new Array.<Light>(); this.background = new Background(new Color(0,0,0.5), 0.2); } } // NOTE // BaseMaterial, Solid, and Chessboard classes used to be in Material namespace abstract class BaseMaterial { var gloss = 2.0; // [0...infinity] 0 = matt var transparency = 0.0; // 0=opaque var reflection = 0.0; // [0...infinity] 0 = no reflection var refraction = 0.50; var hasTexture = false; function constructor() { } abstract function getColor(u : number, v : number) : Color; function wrapUp(t : number) : number { t = t % 2.0; if(t < -1) t += 2.0; if(t >= 1) t -= 2.0; return t; } override function toString() : string { return 'Material [gloss=' + this.gloss as string + ', transparency=' + this.transparency as string + ', hasTexture=' + this.hasTexture as string +']'; } } class Solid extends BaseMaterial { var color : Color; function constructor(color : Color, reflection : number, refraction : number, transparency : number, gloss : number) { this.color = color; this.reflection = reflection; this.transparency = transparency; this.gloss = gloss; this.hasTexture = false; } override function getColor(u : number, v : number) : Color { return this.color; } override function toString() : string { return 'SolidMaterial [gloss=' + this.gloss as string + ', transparency=' + this.transparency as string + ', hasTexture=' + this.hasTexture as string +']'; } } /* Fake a Flog.* namespace */ class Chessboard extends BaseMaterial { var colorEven = null : Color; var colorOdd = null : Color; var density = 0.5; function constructor(colorEven : Color, colorOdd : Color, reflection : number, transparency : number, gloss : number, density : number) { this.colorEven = colorEven; this.colorOdd = colorOdd; this.reflection = reflection; this.transparency = transparency; this.gloss = gloss; this.density = density; this.hasTexture = true; } override function getColor(u : number, v : number) : Color { var t = this.wrapUp(u * this.density) * this.wrapUp(v * this.density); if(t < 0.0) return this.colorEven; else return this.colorOdd; } override function toString() : string { return 'ChessMaterial [gloss=' + this.gloss as string + ', transparency=' + this.transparency as string + ', hasTexture=' + this.hasTexture as string +']'; } } // NOTE // Sphere and Plane classes used to be in Shape namespace abstract class Shape { var position : Vector; var material : BaseMaterial; abstract function intersect(ray : Ray) : IntersectionInfo; } class Sphere extends Shape { var radius : number; function constructor(pos : Vector, radius : number, material : BaseMaterial) { this.radius = radius; this.position = pos; this.material = material; } override function intersect(ray : Ray) : IntersectionInfo { var info = new IntersectionInfo(); info.shape = this; var dst = Vector.subtract(ray.position, this.position); var B = dst.dot(ray.direction); var C = dst.dot(dst) - (this.radius * this.radius); var 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; } override function toString() : string { return 'Sphere [position=' + this.position.toString() + ', radius=' + this.radius as string + ']'; } } class Plane extends Shape { var d = 0.0; function constructor(pos : Vector, d : number, material : BaseMaterial) { this.position = pos; this.d = d; this.material = material; } override function intersect(ray : Ray) : IntersectionInfo { var info = new IntersectionInfo(); var Vd = this.position.dot(ray.direction); if(Vd == 0) return info; // no intersection var t = -(this.position.dot(ray.position) + this.d) / Vd; if(t <= 0) return info; info.shape = this; 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){ var vU = new Vector(this.position.y, this.position.z, -this.position.x); var vV = vU.cross(this.position); var u = info.position.dot(vU); var v = info.position.dot(vV); info.color = this.material.getColor(u,v); } else { info.color = this.material.getColor(0,0); } return info; } override function toString() : string { return 'Plane [' + this.position.toString() + ', d=' + this.d as string + ']'; } } class IntersectionInfo { var isHit = false; var hitCount = 0; var shape = null : Shape; var position = null : Vector; var normal = null : Vector; var color = null : Color; var distance = null : Nullable.<number>; function constructor() { this.color = new Color(0,0,0); } override function toString() : string { return 'Intersection [' + this.position.toString() + ']'; } } class Camera { var position = null : Vector; var lookAt = null : Vector; var equator = null : Vector; var up = null : Vector; var screen = null : Vector; function constructor(pos : Vector, lookAt : Vector, up : Vector) { this.position = pos; this.lookAt = lookAt; this.up = up; this.equator = lookAt.normalize().cross(this.up); this.screen = Vector.add(this.position, this.lookAt); } function getRay(vx : number, vy : number) : Ray { var pos = Vector.subtract( this.screen, Vector.subtract( Vector.multiplyScalar(this.equator, vx), Vector.multiplyScalar(this.up, vy) ) ); pos.y = pos.y * -1; var dir = Vector.subtract( pos, this.position ); var ray = new Ray(pos, dir.normalize()); return ray; } override function toString() : string { return 'Ray []'; } } /* Fake a Flog.* namespace */ class Background { var color = null : Color; var ambience = 0.0; function constructor(color : Color, ambience : number) { this.color = color; this.ambience = ambience; } } class Options { var canvasHeight = 100; var canvasWidth = 100; var pixelWidth = 2; var pixelHeight = 2; var renderDiffuse = false; var renderShadows = false; var renderHighlights = false; var renderReflections = false; var rayDepth = 2; } class Engine { var canvas = null : CanvasRenderingContext2D; /* 2d context we can render to */ var options = null : Options; function constructor(options : Options) { this.options = options; this.options.canvasHeight /= this.options.pixelHeight; this.options.canvasWidth /= this.options.pixelWidth; /* TODO: dynamically include other scripts */ } function setPixel(x : number, y : number, color : Color) : void { var pxW = this.options.pixelWidth; var pxH = this.options.pixelHeight; if (this.canvas) { this.canvas.fillStyle = color.toString(); this.canvas.fillRect (x * pxW, y * pxH, pxW, pxH); } else { if (x == y) { Main.checkNumber += color.brightness(); } // print(x * pxW, y * pxH, pxW, pxH); } } function renderScene(scene : Scene, canvas : HTMLCanvasElement) : void { Main.checkNumber = 0; /* Get canvas */ if (canvas) { this.canvas = canvas.getContext("2d") as CanvasRenderingContext2D; } else { this.canvas = null; } var canvasHeight = this.options.canvasHeight; var canvasWidth = this.options.canvasWidth; for(var y=0; y < canvasHeight; y++){ for(var x=0; x < canvasWidth; x++){ var yp = y * 1.0 / canvasHeight * 2 - 1; var xp = x * 1.0 / canvasWidth * 2 - 1; var ray = scene.camera.getRay(xp, yp); var color = this.getPixelColor(ray, scene); this.setPixel(x, y, color); } } if (Main.checkNumber != 2321) { throw new Error("Scene rendered incorrectly"); } } function getPixelColor(ray : Ray, scene : Scene) : Color { var info = this.testIntersection(ray, scene, null); if(info.isHit){ var color = this.rayTrace(info, ray, scene, 0); return color; } return scene.background.color; } function testIntersection(ray : Ray, scene : Scene, exclude : Shape) : IntersectionInfo { var hits = 0; var best = new IntersectionInfo(); best.distance = 2000; for(var i=0; i<scene.shapes.length; i++){ var shape = scene.shapes[i]; if(shape != exclude){ var info = shape.intersect(ray); if(info.isHit && info.distance >= 0 && info.distance < best.distance){ best = info; hits++; } } } best.hitCount = hits; return best; } function getReflectionRay(P : Vector ,N : Vector,V : Vector) : Ray { var c1 = -N.dot(V); var R1 = Vector.add( Vector.multiplyScalar(N, 2*c1), V ); return new Ray(P, R1); } function rayTrace(info : IntersectionInfo, ray : Ray, scene : Scene, depth : number) : Color { // Calc ambient var color = Color.multiplyScalar(info.color, scene.background.ambience); var oldColor = color; var shininess = Math.pow(10, info.shape.material.gloss + 1); for(var i=0; i<scene.lights.length; i++){ var light = scene.lights[i]; // Calc diffuse lighting var v = Vector.subtract( light.position, info.position ).normalize(); if(this.options.renderDiffuse){ var L = v.dot(info.normal); if(L > 0.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) { var reflectionRay = this.getReflectionRay(info.position, info.normal, ray.direction); var 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 ); } // Refraction /* TODO */ } /* Render shadows and highlights */ var shadowInfo = new IntersectionInfo(); if(this.options.renderShadows){ var shadowRay = new Ray(info.position, v); shadowInfo = this.testIntersection(shadowRay, scene, info.shape); if(shadowInfo.isHit && shadowInfo.shape != info.shape /*&& shadowInfo.shape.type != 'PLANE'*/){ var vA = Color.multiplyScalar(color, 0.5); var dB = (0.5 * Math.pow(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){ var Lv = Vector.subtract( info.shape.position, light.position ).normalize(); var E = Vector.subtract( scene.camera.position, info.shape.position ).normalize(); var H = Vector.subtract( E, Lv ).normalize(); var glossWeight = Math.pow(Math.max(info.normal.dot(H), 0), shininess); color = Color.add( Color.multiplyScalar(light.color, glossWeight), color ); } } color.limit(); return color; } } class Main { // Variable used to hold a number that can be used to verify that // the scene was ray traced correctly. static var checkNumber : number; static function renderScene() : void { var scene = new Scene(); scene.camera = new Camera( new Vector(0, 0, -15), new Vector(-0.2, 0, 5), new Vector(0, 1, 0) ); scene.background = new Background( new Color(0.5, 0.5, 0.5), 0.4 ); var sphere = new Sphere( new Vector(-1.5, 1.5, 2), 1.5, new Solid( new Color(0,0.5,0.5), 0.3, 0.0, 0.0, 2.0 ) ); var sphere1 = new Sphere( new Vector(1, 0.25, 1), 0.5, new Solid( new Color(0.9,0.9,0.9), 0.1, 0.0, 0.0, 1.5 ) ); var plane = new Plane( new Vector(0.1, 0.9, -0.5).normalize(), 1.2, new Chessboard( new Color(1,1,1), new Color(0,0,0), 0.2, 0.0, 1.0, 0.7 ) ); scene.shapes.push(plane); scene.shapes.push(sphere); scene.shapes.push(sphere1); var light = new Light( new Vector(5, 10, -1), new Color(0.8, 0.8, 0.8) ); var light1 = new Light( new Vector(-3, 5, -15), new Color(0.8, 0.8, 0.8), 100 ); scene.lights.push(light); scene.lights.push(light1); var imageWidth = 100; // $F('imageWidth'); var imageHeight = 100; // $F('imageHeight'); var pixelSize = "5,5".split(','); // $F('pixelSize').split(','); var renderDiffuse = true; // $F('renderDiffuse'); var renderShadows = true; // $F('renderShadows'); var renderHighlights = true; // $F('renderHighlights'); var renderReflections = true; // $F('renderReflections'); var rayDepth = 2;//$F('rayDepth'); var options = new Options(); options.canvasWidth = imageWidth; options.canvasHeight = imageHeight; options.pixelWidth = pixelSize[0] as number; options.pixelHeight = pixelSize[1] as number; options.renderDiffuse = renderDiffuse; options.renderHighlights = renderHighlights; options.renderShadows = renderShadows; options.renderReflections = renderReflections; options.rayDepth = rayDepth; var raytracer = new Engine(options); raytracer.renderScene(scene, null); } } // vim: set expandtab: