├── .gitignore ├── README.md ├── dummy.dom.js ├── package.json ├── raytrace.css ├── raytracer.html ├── raytracer.js ├── raytracer.js.map ├── raytracer.ts ├── rt.vanilla.html ├── rt.vanilla.js ├── trace.ts.js ├── trace.vanilla.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # raytrace 2 | 3 | A comparison between [original raytrace.ts](https://github.com/Microsoft/TypeScriptSamples/tree/master/raytracer) performance VS a hand-written [JS port](./rt.vanilla.js). 4 | 5 | **Live Test** - both pages show render time in console 6 | 7 | * original [**TypeScript**](https://webreflection.github.io/raytrace/raytracer.html) targeting *ES2022* code 8 | * hand-written [**JavaScript**](https://webreflection.github.io/raytrace/rt.vanilla.html) 9 | 10 | ## Licence ## 11 | 12 | The [original TypeScript version](https://github.com/Microsoft/TypeScriptSamples/issues/143) and this JS translation are licenced under the Apache-2.0 licence. 13 | 14 | ### How to test locally 15 | 16 | * fork or clone this repo 17 | * enter `raytrace` directory (`cd raytrace`) 18 | * `npm i` 19 | * `npm run build` 20 | * run any local server to test `raytracer.html` or `rt.vanilla.html` on your localhost 21 | 22 | ### Could the vanilla version have TS too? 23 | 24 | Check the [JSDoc TS](https://github.com/WebReflection/raytrace/blob/jsdoc-ts/rt.vanilla.js) annotated file which is fully compliant with TS and it still performs as good as the not-annotated file. 25 | -------------------------------------------------------------------------------- /dummy.dom.js: -------------------------------------------------------------------------------- 1 | // minimum dependencies to make the code run on node 2 | globalThis.requestAnimationFrame = queueMicrotask; 3 | globalThis.document = { 4 | body: {appendChild(){}}, 5 | documentElement: { 6 | classList: {add(){}} 7 | }, 8 | createElement: () => ({ 9 | width: 0, 10 | height: 0, 11 | getContext: () => ({ 12 | fillStyle: '', 13 | fillRect() {} 14 | }) 15 | }) 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "tsc -p .", 5 | "dexnode:vanilla": "npx dexnode --out v8.js.log --redirect-code-traces-to=./v8.vanilla.log trace.vanilla.js", 6 | "dexnode:ts": "npx dexnode --out v8.ts.log --redirect-code-traces-to=./v8.ts.log trace.ts.js" 7 | }, 8 | "license": "ISC", 9 | "devDependencies": { 10 | "typescript": "^5.0.4" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /raytrace.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: black; 3 | } 4 | 5 | html.done canvas { 6 | animation: blur 1s; 7 | } 8 | 9 | @keyframes blur { 10 | from { 11 | filter: blur(5px); 12 | } 13 | to { 14 | filter: blur(0); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /raytracer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Raytracer TS 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /raytracer.js: -------------------------------------------------------------------------------- 1 | const { floor, min, sqrt } = Math; 2 | class Vector { 3 | constructor(x, y, z) { 4 | this.x = x; 5 | this.y = y; 6 | this.z = z; 7 | } 8 | static times(k, v) { return new Vector(k * v.x, k * v.y, k * v.z); } 9 | static minus(v1, v2) { return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } 10 | static plus(v1, v2) { return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } 11 | static dot(v1, v2) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } 12 | static mag(v) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } 13 | static norm(v) { 14 | var mag = Vector.mag(v); 15 | var div = (mag === 0) ? Infinity : 1.0 / mag; 16 | return Vector.times(div, v); 17 | } 18 | static cross(v1, v2) { 19 | 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); 20 | } 21 | } 22 | class Color { 23 | constructor(r, g, b) { 24 | this.r = r; 25 | this.g = g; 26 | this.b = b; 27 | } 28 | static scale(k, v) { return new Color(k * v.r, k * v.g, k * v.b); } 29 | static plus(v1, v2) { return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b); } 30 | static times(v1, v2) { return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b); } 31 | static { this.white = new Color(1.0, 1.0, 1.0); } 32 | static { this.grey = new Color(0.5, 0.5, 0.5); } 33 | static { this.black = new Color(0.0, 0.0, 0.0); } 34 | static { this.background = Color.black; } 35 | static { this.defaultColor = Color.black; } 36 | static toDrawingColor(c) { 37 | return { 38 | r: floor(min(c.r, 1) * 255), 39 | g: floor(min(c.g, 1) * 255), 40 | b: floor(min(c.b, 1) * 255) 41 | }; 42 | } 43 | } 44 | class Camera { 45 | constructor(pos, lookAt) { 46 | this.pos = pos; 47 | var down = new Vector(0.0, -1.0, 0.0); 48 | this.forward = Vector.norm(Vector.minus(lookAt, this.pos)); 49 | this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down))); 50 | this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right))); 51 | } 52 | } 53 | class Sphere { 54 | constructor(center, radius, surface) { 55 | this.center = center; 56 | this.surface = surface; 57 | this.radius2 = radius * radius; 58 | } 59 | normal(pos) { return Vector.norm(Vector.minus(pos, this.center)); } 60 | intersect(ray) { 61 | var eo = Vector.minus(this.center, ray.start); 62 | var v = Vector.dot(eo, ray.dir); 63 | var dist = 0; 64 | if (v >= 0) { 65 | var disc = this.radius2 - (Vector.dot(eo, eo) - v * v); 66 | if (disc >= 0) { 67 | dist = v - Math.sqrt(disc); 68 | } 69 | } 70 | if (dist === 0) { 71 | return null; 72 | } 73 | else { 74 | return { thing: this, ray: ray, dist: dist }; 75 | } 76 | } 77 | } 78 | class Plane { 79 | constructor(norm, offset, surface) { 80 | this.norm = norm; 81 | this.offset = offset; 82 | this.surface = surface; 83 | } 84 | normal(_pos) { return this.norm; } 85 | intersect(ray) { 86 | var denom = Vector.dot(this.norm, ray.dir); 87 | if (denom > 0) { 88 | return null; 89 | } 90 | else { 91 | var dist = (Vector.dot(this.norm, ray.start) + this.offset) / (-denom); 92 | return { thing: this, ray: ray, dist: dist }; 93 | } 94 | } 95 | } 96 | class Surfaces { 97 | static { this.shiny = { 98 | diffuse: function (pos) { return Color.white; }, 99 | specular: function (pos) { return Color.grey; }, 100 | reflect: function (pos) { return 0.7; }, 101 | roughness: 250 102 | }; } 103 | static { this.checkerboard = { 104 | diffuse: function (pos) { 105 | if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { 106 | return Color.white; 107 | } 108 | else { 109 | return Color.black; 110 | } 111 | }, 112 | specular: function (pos) { return Color.white; }, 113 | reflect: function (pos) { 114 | if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { 115 | return 0.1; 116 | } 117 | else { 118 | return 0.7; 119 | } 120 | }, 121 | roughness: 150 122 | }; } 123 | } 124 | class RayTracer { 125 | constructor() { 126 | this.maxDepth = 5; 127 | } 128 | intersections(ray, scene) { 129 | var closest = +Infinity; 130 | var closestInter = undefined; 131 | for (const thing of scene.things) { 132 | var inter = thing.intersect(ray); 133 | if (inter != null && inter.dist < closest) { 134 | closestInter = inter; 135 | closest = inter.dist; 136 | } 137 | } 138 | return closestInter; 139 | } 140 | testRay(ray, scene) { 141 | var isect = this.intersections(ray, scene); 142 | if (isect != null) { 143 | return isect.dist; 144 | } 145 | else { 146 | return undefined; 147 | } 148 | } 149 | traceRay(ray, scene, depth) { 150 | var isect = this.intersections(ray, scene); 151 | if (isect == null) { 152 | return Color.background; 153 | } 154 | else { 155 | return this.shade(isect, scene, depth); 156 | } 157 | } 158 | shade(isect, scene, depth) { 159 | var d = isect.ray.dir; 160 | var pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); 161 | var normal = isect.thing.normal(pos); 162 | var reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); 163 | var naturalColor = Color.plus(Color.background, this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); 164 | var reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); 165 | return Color.plus(naturalColor, reflectedColor); 166 | } 167 | getReflectionColor(thing, pos, normal, rd, scene, depth) { 168 | return Color.scale(thing.surface.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1)); 169 | } 170 | getNaturalColor(thing, pos, norm, rd, scene) { 171 | var addLight = (col, light) => { 172 | var ldis = Vector.minus(light.pos, pos); 173 | var livec = Vector.norm(ldis); 174 | var neatIsect = this.testRay({ start: pos, dir: livec }, scene); 175 | var isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); 176 | if (isInShadow) { 177 | return col; 178 | } 179 | else { 180 | var illum = Vector.dot(livec, norm); 181 | var lcolor = (illum > 0) ? Color.scale(illum, light.color) 182 | : Color.defaultColor; 183 | var specular = Vector.dot(livec, Vector.norm(rd)); 184 | var scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.roughness), light.color) 185 | : Color.defaultColor; 186 | return Color.plus(col, Color.plus(Color.times(thing.surface.diffuse(pos), lcolor), Color.times(thing.surface.specular(pos), scolor))); 187 | } 188 | }; 189 | return scene.lights.reduce(addLight, Color.defaultColor); 190 | } 191 | render(scene, ctx, screenWidth, screenHeight) { 192 | var getPoint = (x, y, camera) => { 193 | var recenterX = x => (x - (screenWidth / 2.0)) / 2.0 / screenWidth; 194 | var recenterY = y => -(y - (screenHeight / 2.0)) / 2.0 / screenHeight; 195 | return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up)))); 196 | }; 197 | const { camera } = scene; 198 | for (var y = 0; y < screenHeight; y++) { 199 | for (var x = 0; x < screenWidth; x++) { 200 | var color = this.traceRay({ start: camera.pos, dir: getPoint(x, y, camera) }, scene, 0); 201 | const { r, g, b } = Color.toDrawingColor(color); 202 | ctx.fillStyle = `rgb(${r},${g},${b})`; 203 | ctx.fillRect(x, y, 1, 1); 204 | } 205 | } 206 | } 207 | } 208 | function defaultScene() { 209 | return { 210 | things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), 211 | new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), 212 | new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)], 213 | lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) }, 214 | { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) }, 215 | { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) }, 216 | { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }], 217 | camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0)) 218 | }; 219 | } 220 | const exec = (width, height) => { 221 | const canv = document.createElement("canvas"); 222 | canv.width = width; 223 | canv.height = height; 224 | (new RayTracer).render(defaultScene(), canv.getContext("2d"), width, height); 225 | document.body.appendChild(canv); 226 | }; 227 | const size = 256; 228 | const render = () => new Promise(resolve => { 229 | requestAnimationFrame(() => { 230 | console.time('raytrace TS'); 231 | exec(size, size); 232 | console.timeEnd('raytrace TS'); 233 | resolve(); 234 | }); 235 | }); 236 | render() 237 | .then(render) 238 | .then(render) 239 | .then(render) 240 | .then(render) 241 | .then(render) 242 | .then(() => { 243 | document.documentElement.classList.add('done'); 244 | }); 245 | //# sourceMappingURL=raytracer.js.map -------------------------------------------------------------------------------- /raytracer.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"raytracer.js","sourceRoot":"","sources":["raytracer.ts"],"names":[],"mappings":"AAAA,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;AAElC,MAAM,MAAM;IACR,YAAmB,CAAS,EACT,CAAS,EACT,CAAS;QAFT,MAAC,GAAD,CAAC,CAAQ;QACT,MAAC,GAAD,CAAC,CAAQ;QACT,MAAC,GAAD,CAAC,CAAQ;IAC5B,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,CAAS,EAAE,CAAS,IAAI,OAAO,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACpF,MAAM,CAAC,KAAK,CAAC,EAAU,EAAE,EAAU,IAAI,OAAO,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClG,MAAM,CAAC,IAAI,CAAC,EAAU,EAAE,EAAU,IAAI,OAAO,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjG,MAAM,CAAC,GAAG,CAAC,EAAU,EAAE,EAAU,IAAI,OAAO,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACtF,MAAM,CAAC,GAAG,CAAC,CAAS,IAAI,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACzE,MAAM,CAAC,IAAI,CAAC,CAAS;QACjB,IAAI,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC;QAC7C,OAAO,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,EAAU,EAAE,EAAU;QAC/B,OAAO,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EACzB,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EACzB,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;CACJ;AAED,MAAM,KAAK;IACP,YAAmB,CAAS,EACT,CAAS,EACT,CAAS;QAFT,MAAC,GAAD,CAAC,CAAQ;QACT,MAAC,GAAD,CAAC,CAAQ;QACT,MAAC,GAAD,CAAC,CAAQ;IAC5B,CAAC;IACD,MAAM,CAAC,KAAK,CAAC,CAAS,EAAE,CAAQ,IAAI,OAAO,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,CAAC,IAAI,CAAC,EAAS,EAAE,EAAS,IAAI,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9F,MAAM,CAAC,KAAK,CAAC,EAAS,EAAE,EAAS,IAAI,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;aACxF,UAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;aACjC,SAAI,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;aAChC,UAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;aACjC,eAAU,GAAG,KAAK,CAAC,KAAK,CAAC;aACzB,iBAAY,GAAG,KAAK,CAAC,KAAK,CAAC;IAClC,MAAM,CAAC,cAAc,CAAC,CAAQ;QAC1B,OAAO;YACH,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC;YAC3B,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC;YAC3B,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC;SAC9B,CAAA;IACL,CAAC;;AAGL,MAAM,MAAM;IAKR,YAAmB,GAAW,EAAE,MAAc;QAA3B,QAAG,GAAH,GAAG,CAAQ;QAC1B,IAAI,IAAI,GAAG,IAAI,MAAM,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;QAC9E,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACrF,CAAC;CACJ;AAqCD,MAAM,MAAM;IAGR,YAAmB,MAAc,EAAE,MAAc,EAAS,OAAgB;QAAvD,WAAM,GAAN,MAAM,CAAQ;QAAyB,YAAO,GAAP,OAAO,CAAS;QACtE,IAAI,CAAC,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;IACnC,CAAC;IACD,MAAM,CAAC,GAAW,IAAY,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,SAAS,CAAC,GAAQ;QACd,IAAI,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAC9C,IAAI,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACT,IAAI,IAAI,GAAG,IAAI,CAAC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;YACvD,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;gBACZ,IAAI,GAAG,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/B,CAAC;QACL,CAAC;QACD,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;YACb,OAAO,IAAI,CAAC;QAChB,CAAC;aAAM,CAAC;YACJ,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACjD,CAAC;IACL,CAAC;CACJ;AAED,MAAM,KAAK;IACP,YAAmB,IAAY,EAAS,MAAc,EAAS,OAAgB;QAA5D,SAAI,GAAJ,IAAI,CAAQ;QAAS,WAAM,GAAN,MAAM,CAAQ;QAAS,YAAO,GAAP,OAAO,CAAS;IAAG,CAAC;IACnF,MAAM,CAAC,IAAY,IAAI,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,SAAS,CAAC,GAAQ;QACd,IAAI,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;YACZ,OAAO,IAAI,CAAC;QAChB,CAAC;aAAM,CAAC;YACJ,IAAI,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;YACvE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACjD,CAAC;IACL,CAAC;CACJ;AAED,MAAM,QAAQ;aACH,UAAK,GAAY;QACpB,OAAO,EAAE,UAAS,GAAG,IAAI,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9C,QAAQ,EAAE,UAAS,GAAG,IAAI,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QAC9C,OAAO,EAAE,UAAS,GAAG,IAAI,OAAO,GAAG,CAAC,CAAC,CAAC;QACtC,SAAS,EAAE,GAAG;KACjB,CAAA;aACM,iBAAY,GAAY;QAC3B,OAAO,EAAE,UAAS,GAAG;YACjB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpD,OAAO,KAAK,CAAC,KAAK,CAAC;YACvB,CAAC;iBAAM,CAAC;gBACJ,OAAO,KAAK,CAAC,KAAK,CAAC;YACvB,CAAC;QACL,CAAC;QACD,QAAQ,EAAE,UAAS,GAAG,IAAI,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,OAAO,EAAE,UAAS,GAAG;YACjB,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpD,OAAO,GAAG,CAAC;YACf,CAAC;iBAAM,CAAC;gBACJ,OAAO,GAAG,CAAC;YACf,CAAC;QACL,CAAC;QACD,SAAS,EAAE,GAAG;KACjB,CAAA;;AAIL,MAAM,SAAS;IAAf;QACY,aAAQ,GAAG,CAAC,CAAC;IAsFzB,CAAC;IApFW,aAAa,CAAC,GAAQ,EAAE,KAAY;QACxC,IAAI,OAAO,GAAG,CAAC,QAAQ,CAAC;QACxB,IAAI,YAAY,GAAiB,SAAS,CAAC;QAC3C,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YAC/B,IAAI,KAAK,GAAG,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,CAAC,IAAI,GAAG,OAAO,EAAE,CAAC;gBACxC,YAAY,GAAG,KAAK,CAAC;gBACrB,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC;YACzB,CAAC;QACL,CAAC;QACD,OAAO,YAAY,CAAC;IACxB,CAAC;IAEO,OAAO,CAAC,GAAQ,EAAE,KAAY;QAClC,IAAI,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3C,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC,IAAI,CAAC;QACtB,CAAC;aAAM,CAAC;YACJ,OAAO,SAAS,CAAC;QACrB,CAAC;IACL,CAAC;IAEO,QAAQ,CAAC,GAAQ,EAAE,KAAY,EAAE,KAAa;QAClD,IAAI,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC3C,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;YAChB,OAAO,KAAK,CAAC,UAAU,CAAC;QAC5B,CAAC;aAAM,CAAC;YACJ,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,KAAmB,EAAE,KAAY,EAAE,KAAa;QAC1D,IAAI,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;QACtB,IAAI,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACpE,IAAI,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;QAC/F,IAAI,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,EAChB,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;QACjG,IAAI,cAAc,GAAG,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QACzI,OAAO,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,cAAc,CAAC,CAAC;IACpD,CAAC;IAEO,kBAAkB,CAAC,KAAY,EAAE,GAAW,EAAE,MAAc,EAAE,EAAU,EAAE,KAAY,EAAE,KAAa;QACzG,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IAC7G,CAAC;IAEO,eAAe,CAAC,KAAY,EAAE,GAAW,EAAE,IAAY,EAAE,EAAU,EAAE,KAAY;QACrF,IAAI,QAAQ,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAC1B,IAAI,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACxC,IAAI,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC9B,IAAI,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,KAAK,CAAC,CAAC;YAChE,IAAI,UAAU,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;YACrF,IAAI,UAAU,EAAE,CAAC;gBACb,OAAO,GAAG,CAAC;YACf,CAAC;iBAAM,CAAC;gBACJ,IAAI,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;gBACpC,IAAI,MAAM,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;oBAChC,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC;gBAC/C,IAAI,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;gBAClD,IAAI,MAAM,GAAG,CAAC,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC;oBACzE,CAAC,CAAC,KAAK,CAAC,YAAY,CAAC;gBAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,EAC/C,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;YACzF,CAAC;QACL,CAAC,CAAA;QACD,OAAO,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,CAAC,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,YAAY;QACxC,IAAI,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE;YAC5B,IAAI,SAAS,GAAG,CAAC,CAAC,EAAE,CAAA,CAAC,CAAC,GAAG,CAAC,WAAW,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,WAAW,CAAC;YAClE,IAAI,SAAS,GAAG,CAAC,CAAC,EAAE,CAAC,CAAE,CAAC,CAAC,GAAG,CAAC,YAAY,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,YAAY,CAAC;YACvE,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAClJ,CAAC,CAAA;QACD,MAAM,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;QACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;YACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC,EAAE,EAAE,CAAC;gBACnC,IAAI,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;gBACxF,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;gBAChD,GAAG,CAAC,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;gBACtC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAC7B,CAAC;QACL,CAAC;IACL,CAAC;CACJ;AAGD,SAAS,YAAY;IACjB,OAAO;QACH,MAAM,EAAE,CAAC,IAAI,KAAK,CAAC,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,YAAY,CAAC;YAChE,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC;YAC5D,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;QACrE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,IAAI,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;YACvE,EAAE,GAAG,EAAE,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE;YACtE,EAAE,GAAG,EAAE,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,EAAE;YACxE,EAAE,GAAG,EAAE,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;QAChF,MAAM,EAAE,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,IAAI,MAAM,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;KAC5E,CAAC;AACN,CAAC;AAGD,MAAM,IAAI,GAAG,CAAC,KAAa,EAAE,MAAc,EAAE,EAAE;IAC3C,MAAM,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACnB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACrB,CAAC,IAAI,SAAS,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IAC7E,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC,CAAC;AAGF,MAAM,IAAI,GAAG,GAAG,CAAC;AACjB,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,IAAI,OAAO,CAAO,OAAO,CAAC,EAAE;IAC7C,qBAAqB,CAAC,GAAG,EAAE;QACvB,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACjB,OAAO,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QAC/B,OAAO,EAAE,CAAC;IACd,CAAC,CAAC,CAAC;AACP,CAAC,CAAC,CAAC;AAEH,MAAM,EAAE;KACH,IAAI,CAAC,MAAM,CAAC;KACZ,IAAI,CAAC,MAAM,CAAC;KACZ,IAAI,CAAC,MAAM,CAAC;KACZ,IAAI,CAAC,MAAM,CAAC;KACZ,IAAI,CAAC,MAAM,CAAC;KACZ,IAAI,CAAC,GAAG,EAAE;IACP,QAAQ,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACnD,CAAC,CAAC,CAAC"} -------------------------------------------------------------------------------- /raytracer.ts: -------------------------------------------------------------------------------- 1 | const { floor, min, sqrt } = Math; 2 | 3 | class Vector { 4 | constructor(public x: number, 5 | public y: number, 6 | public z: number) { 7 | } 8 | static times(k: number, v: Vector) { return new Vector(k * v.x, k * v.y, k * v.z); } 9 | static minus(v1: Vector, v2: Vector) { return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } 10 | static plus(v1: Vector, v2: Vector) { return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } 11 | static dot(v1: Vector, v2: Vector) { return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; } 12 | static mag(v: Vector) { return sqrt(v.x * v.x + v.y * v.y + v.z * v.z); } 13 | static norm(v: Vector) { 14 | var mag = Vector.mag(v); 15 | var div = (mag === 0) ? Infinity : 1.0 / mag; 16 | return Vector.times(div, v); 17 | } 18 | static cross(v1: Vector, v2: Vector) { 19 | return new Vector(v1.y * v2.z - v1.z * v2.y, 20 | v1.z * v2.x - v1.x * v2.z, 21 | v1.x * v2.y - v1.y * v2.x); 22 | } 23 | } 24 | 25 | class Color { 26 | constructor(public r: number, 27 | public g: number, 28 | public b: number) { 29 | } 30 | static scale(k: number, v: Color) { return new Color(k * v.r, k * v.g, k * v.b); } 31 | static plus(v1: Color, v2: Color) { return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b); } 32 | static times(v1: Color, v2: Color) { return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b); } 33 | static white = new Color(1.0, 1.0, 1.0); 34 | static grey = new Color(0.5, 0.5, 0.5); 35 | static black = new Color(0.0, 0.0, 0.0); 36 | static background = Color.black; 37 | static defaultColor = Color.black; 38 | static toDrawingColor(c: Color) { 39 | return { 40 | r: floor(min(c.r, 1) * 255), 41 | g: floor(min(c.g, 1) * 255), 42 | b: floor(min(c.b, 1) * 255) 43 | } 44 | } 45 | } 46 | 47 | class Camera { 48 | public forward: Vector; 49 | public right: Vector; 50 | public up: Vector; 51 | 52 | constructor(public pos: Vector, lookAt: Vector) { 53 | var down = new Vector(0.0, -1.0, 0.0); 54 | this.forward = Vector.norm(Vector.minus(lookAt, this.pos)); 55 | this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down))); 56 | this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right))); 57 | } 58 | } 59 | 60 | interface Ray { 61 | start: Vector; 62 | dir: Vector; 63 | } 64 | 65 | interface Intersection { 66 | thing: Thing; 67 | ray: Ray; 68 | dist: number; 69 | } 70 | 71 | interface Surface { 72 | diffuse: (pos: Vector) => Color; 73 | specular: (pos: Vector) => Color; 74 | reflect: (pos: Vector) => number; 75 | roughness: number; 76 | } 77 | 78 | interface Thing { 79 | intersect: (ray: Ray) => Intersection; 80 | normal: (pos: Vector) => Vector; 81 | surface: Surface; 82 | } 83 | 84 | interface Light { 85 | pos: Vector; 86 | color: Color; 87 | } 88 | 89 | interface Scene { 90 | things: Thing[]; 91 | lights: Light[]; 92 | camera: Camera; 93 | } 94 | 95 | class Sphere implements Thing { 96 | public radius2: number; 97 | 98 | constructor(public center: Vector, radius: number, public surface: Surface) { 99 | this.radius2 = radius * radius; 100 | } 101 | normal(pos: Vector): Vector { return Vector.norm(Vector.minus(pos, this.center)); } 102 | intersect(ray: Ray) { 103 | var eo = Vector.minus(this.center, ray.start); 104 | var v = Vector.dot(eo, ray.dir); 105 | var dist = 0; 106 | if (v >= 0) { 107 | var disc = this.radius2 - (Vector.dot(eo, eo) - v * v); 108 | if (disc >= 0) { 109 | dist = v - Math.sqrt(disc); 110 | } 111 | } 112 | if (dist === 0) { 113 | return null; 114 | } else { 115 | return { thing: this, ray: ray, dist: dist }; 116 | } 117 | } 118 | } 119 | 120 | class Plane implements Thing { 121 | constructor(public norm: Vector, public offset: number, public surface: Surface) {} 122 | normal(_pos: Vector) { return this.norm; } 123 | intersect(ray: Ray): Intersection { 124 | var denom = Vector.dot(this.norm, ray.dir); 125 | if (denom > 0) { 126 | return null; 127 | } else { 128 | var dist = (Vector.dot(this.norm, ray.start) + this.offset) / (-denom); 129 | return { thing: this, ray: ray, dist: dist }; 130 | } 131 | } 132 | } 133 | 134 | class Surfaces { 135 | static shiny: Surface = { 136 | diffuse: function(pos) { return Color.white; }, 137 | specular: function(pos) { return Color.grey; }, 138 | reflect: function(pos) { return 0.7; }, 139 | roughness: 250 140 | } 141 | static checkerboard: Surface = { 142 | diffuse: function(pos) { 143 | if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { 144 | return Color.white; 145 | } else { 146 | return Color.black; 147 | } 148 | }, 149 | specular: function(pos) { return Color.white; }, 150 | reflect: function(pos) { 151 | if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { 152 | return 0.1; 153 | } else { 154 | return 0.7; 155 | } 156 | }, 157 | roughness: 150 158 | } 159 | } 160 | 161 | 162 | class RayTracer { 163 | private maxDepth = 5; 164 | 165 | private intersections(ray: Ray, scene: Scene) { 166 | var closest = +Infinity; 167 | var closestInter: Intersection = undefined; 168 | for (const thing of scene.things) { 169 | var inter = thing.intersect(ray); 170 | if (inter != null && inter.dist < closest) { 171 | closestInter = inter; 172 | closest = inter.dist; 173 | } 174 | } 175 | return closestInter; 176 | } 177 | 178 | private testRay(ray: Ray, scene: Scene) { 179 | var isect = this.intersections(ray, scene); 180 | if (isect != null) { 181 | return isect.dist; 182 | } else { 183 | return undefined; 184 | } 185 | } 186 | 187 | private traceRay(ray: Ray, scene: Scene, depth: number): Color { 188 | var isect = this.intersections(ray, scene); 189 | if (isect == null) { 190 | return Color.background; 191 | } else { 192 | return this.shade(isect, scene, depth); 193 | } 194 | } 195 | 196 | private shade(isect: Intersection, scene: Scene, depth: number) { 197 | var d = isect.ray.dir; 198 | var pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); 199 | var normal = isect.thing.normal(pos); 200 | var reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); 201 | var naturalColor = Color.plus(Color.background, 202 | this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); 203 | var reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); 204 | return Color.plus(naturalColor, reflectedColor); 205 | } 206 | 207 | private getReflectionColor(thing: Thing, pos: Vector, normal: Vector, rd: Vector, scene: Scene, depth: number) { 208 | return Color.scale(thing.surface.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1)); 209 | } 210 | 211 | private getNaturalColor(thing: Thing, pos: Vector, norm: Vector, rd: Vector, scene: Scene) { 212 | var addLight = (col, light) => { 213 | var ldis = Vector.minus(light.pos, pos); 214 | var livec = Vector.norm(ldis); 215 | var neatIsect = this.testRay({ start: pos, dir: livec }, scene); 216 | var isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); 217 | if (isInShadow) { 218 | return col; 219 | } else { 220 | var illum = Vector.dot(livec, norm); 221 | var lcolor = (illum > 0) ? Color.scale(illum, light.color) 222 | : Color.defaultColor; 223 | var specular = Vector.dot(livec, Vector.norm(rd)); 224 | var scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.roughness), light.color) 225 | : Color.defaultColor; 226 | return Color.plus(col, Color.plus(Color.times(thing.surface.diffuse(pos), lcolor), 227 | Color.times(thing.surface.specular(pos), scolor))); 228 | } 229 | } 230 | return scene.lights.reduce(addLight, Color.defaultColor); 231 | } 232 | 233 | render(scene, ctx, screenWidth, screenHeight) { 234 | var getPoint = (x, y, camera) => { 235 | var recenterX = x =>(x - (screenWidth / 2.0)) / 2.0 / screenWidth; 236 | var recenterY = y => - (y - (screenHeight / 2.0)) / 2.0 / screenHeight; 237 | return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up)))); 238 | } 239 | const { camera } = scene; 240 | for (var y = 0; y < screenHeight; y++) { 241 | for (var x = 0; x < screenWidth; x++) { 242 | var color = this.traceRay({ start: camera.pos, dir: getPoint(x, y, camera) }, scene, 0); 243 | const { r, g, b } = Color.toDrawingColor(color); 244 | ctx.fillStyle = `rgb(${r},${g},${b})`; 245 | ctx.fillRect(x, y, 1, 1); 246 | } 247 | } 248 | } 249 | } 250 | 251 | 252 | function defaultScene(): Scene { 253 | return { 254 | things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), 255 | new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), 256 | new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)], 257 | lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) }, 258 | { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) }, 259 | { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) }, 260 | { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }], 261 | camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0)) 262 | }; 263 | } 264 | 265 | 266 | const exec = (width: number, height: number) => { 267 | const canv = document.createElement("canvas"); 268 | canv.width = width; 269 | canv.height = height; 270 | (new RayTracer).render(defaultScene(), canv.getContext("2d"), width, height); 271 | document.body.appendChild(canv); 272 | }; 273 | 274 | 275 | const size = 256; 276 | const render = () => new Promise(resolve => { 277 | requestAnimationFrame(() => { 278 | console.time('raytrace TS'); 279 | exec(size, size); 280 | console.timeEnd('raytrace TS'); 281 | resolve(); 282 | }); 283 | }); 284 | 285 | render() 286 | .then(render) 287 | .then(render) 288 | .then(render) 289 | .then(render) 290 | .then(render) 291 | .then(() => { 292 | document.documentElement.classList.add('done'); 293 | }); 294 | -------------------------------------------------------------------------------- /rt.vanilla.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Raytracer JS 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /rt.vanilla.js: -------------------------------------------------------------------------------- 1 | const { floor, min, sqrt } = Math; 2 | 3 | const vector = (x, y, z) => ({ x, y, z }); 4 | const Vector = { 5 | times: (k, { x, y, z }) => vector(k * x, k * y, k * z), 6 | minus: (v1, v2) => vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z), 7 | plus: (v1, v2) => vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z), 8 | dot: (v1, v2) => v1.x * v2.x + v1.y * v2.y + v1.z * v2.z, 9 | mag: ({ x, y, z }) => sqrt(x * x + y * y + z * z), 10 | norm: v => { 11 | const mag = Vector.mag(v); 12 | const div = (mag === 0) ? Infinity : 1.0 / mag; 13 | return Vector.times(div, v); 14 | }, 15 | cross: (v1, v2) => vector( 16 | v1.y * v2.z - v1.z * v2.y, 17 | v1.z * v2.x - v1.x * v2.z, 18 | v1.x * v2.y - v1.y * v2.x 19 | ) 20 | }; 21 | 22 | const color = (r, g, b) => ({ r, g, b }); 23 | const black = color(0.0, 0.0, 0.0); 24 | const Color = { 25 | black, 26 | background: black, 27 | defaultColor: black, 28 | white: color(1.0, 1.0, 1.0), 29 | grey: color(0.5, 0.5, 0.5), 30 | scale: (k, { r, g, b }) => color(k * r, k * g, k * b), 31 | plus: (v1, v2) => color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b), 32 | times: (v1, v2) => color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b), 33 | toDrawingColor: ({ r, g, b }) => color( 34 | floor(min(r, 1) * 255), 35 | floor(min(g, 1) * 255), 36 | floor(min(b, 1) * 255) 37 | ) 38 | }; 39 | 40 | const camera = (pos, lookAt) => { 41 | const forward = Vector.norm(Vector.minus(lookAt, pos)); 42 | const right = Vector.times(1.5, Vector.norm(Vector.cross(forward, vector(0.0, -1.0, 0.0)))); 43 | return { pos, forward, right, up: Vector.times(1.5, Vector.norm(Vector.cross(forward, right))) }; 44 | }; 45 | 46 | const geo = (thing, ray, dist) => ({ thing, ray, dist }); 47 | class Sphere { 48 | constructor(center, radius, surface) { 49 | this.center = center; 50 | this.surface = surface; 51 | this.radius2 = radius * radius; 52 | } 53 | normal(pos) { 54 | return Vector.norm(Vector.minus(pos, this.center)); 55 | } 56 | intersect(r) { 57 | const eo = Vector.minus(this.center, r.start); 58 | const v = Vector.dot(eo, r.dir); 59 | let dist = 0; 60 | if (v >= 0) { 61 | const disc = this.radius2 - (Vector.dot(eo, eo) - v * v); 62 | if (disc >= 0) 63 | dist = v - sqrt(disc); 64 | } 65 | return dist === 0 ? null : geo(this, r, dist); 66 | } 67 | } 68 | 69 | class Plane { 70 | constructor(norm, offset, surface) { 71 | this.norm = norm; 72 | this.offset = offset; 73 | this.surface = surface; 74 | } 75 | normal(_) { 76 | return this.norm; 77 | } 78 | intersect(r) { 79 | const { norm, offset } = this; 80 | const denom = Vector.dot(norm, r.dir); 81 | return denom > 0 ? null : geo(this, r, (Vector.dot(norm, r.start) + offset) / (-denom)); 82 | } 83 | } 84 | 85 | const Surfaces = { 86 | shiny: { 87 | diffuse: (_) => Color.white, 88 | specular: (_) => Color.grey, 89 | reflect: (_) => 0.7, 90 | roughness: 250 91 | }, 92 | checkerboard: { 93 | diffuse: ({ x, z }) => (floor(z) + floor(x)) % 2 !== 0 ? Color.white : Color.black, 94 | specular: (_) => Color.white, 95 | reflect: ({ x, z }) => (floor(z) + floor(x)) % 2 !== 0 ? 0.1 : 0.7, 96 | roughness: 150 97 | } 98 | }; 99 | 100 | const ray = (start, dir) => ({ start, dir }); 101 | class RayTracer { 102 | constructor() { 103 | this.maxDepth = 5; 104 | } 105 | intersections(r, scene) { 106 | let closest = +Infinity; 107 | let closestInter = null; 108 | for (const thing of scene.things) { 109 | const inter = thing.intersect(r); 110 | if (inter != null && inter.dist < closest) { 111 | closestInter = inter; 112 | closest = inter.dist; 113 | } 114 | } 115 | return closestInter; 116 | } 117 | testRay(r, scene) { 118 | return this.intersections(r, scene)?.dist; 119 | } 120 | traceRay(r, scene, depth) { 121 | const isect = this.intersections(r, scene); 122 | return isect == null ? Color.background : this.shade(isect, scene, depth); 123 | } 124 | shade({ thing, dist, ray: { start, dir } }, scene, depth) { 125 | const pos = Vector.plus(Vector.times(dist, dir), start); 126 | const normal = thing.normal(pos); 127 | const reflectDir = Vector.minus(dir, Vector.times(2, Vector.times(Vector.dot(normal, dir), normal))); 128 | return Color.plus( 129 | Color.plus(Color.background, this.getNaturalColor(thing, pos, normal, reflectDir, scene)), 130 | (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(thing, pos, normal, reflectDir, scene, depth) 131 | ); 132 | } 133 | getReflectionColor({ surface }, pos, _, rd, scene, depth) { 134 | return Color.scale(surface.reflect(pos), this.traceRay(ray(pos, rd), scene, depth + 1)); 135 | } 136 | getNaturalColor({ surface }, pos, norm, rd, scene) { 137 | let col = Color.defaultColor; 138 | for (const light of scene.lights) { 139 | const ldis = Vector.minus(light.pos, pos); 140 | const livec = Vector.norm(ldis); 141 | const neatIsect = this.testRay(ray(pos, livec), scene); 142 | if (neatIsect != null && neatIsect <= Vector.mag(ldis)) 143 | continue; 144 | const illum = Vector.dot(livec, norm); 145 | const lcolor = illum > 0 ? Color.scale(illum, light.color) : Color.defaultColor; 146 | const specular = Vector.dot(livec, Vector.norm(rd)); 147 | const scolor = specular > 0 ? 148 | Color.scale(Math.pow(specular, surface.roughness), light.color) : 149 | Color.defaultColor; 150 | col = Color.plus(col, Color.plus(Color.times(surface.diffuse(pos), lcolor), Color.times(surface.specular(pos), scolor))); 151 | } 152 | return col; 153 | } 154 | render(scene, ctx, screenWidth, screenHeight) { 155 | const recenterX = x => (x - (screenWidth / 2.0)) / 2.0 / screenWidth; 156 | const recenterY = y => -(y - (screenHeight / 2.0)) / 2.0 / screenHeight; 157 | const getPoint = (x, y, camera) => Vector.norm( 158 | Vector.plus(camera.forward, Vector.plus( 159 | Vector.times(recenterX(x), camera.right), 160 | Vector.times(recenterY(y), camera.up) 161 | )) 162 | ); 163 | const { camera } = scene; 164 | for (let y = 0; y < screenHeight; y++) { 165 | for (let x = 0; x < screenWidth; x++) { 166 | const { r, g, b } = Color.toDrawingColor( 167 | this.traceRay(ray(camera.pos, getPoint(x, y, camera)), scene, 0) 168 | ); 169 | ctx.fillStyle = `rgb(${r},${g},${b})`; 170 | ctx.fillRect(x, y, 1, 1); 171 | } 172 | } 173 | } 174 | } 175 | 176 | const light = (pos, color) => ({ pos, color }); 177 | const defaultScene = () => ({ 178 | things: [ 179 | new Plane(vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), 180 | new Sphere(vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), 181 | new Sphere(vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny) 182 | ], 183 | lights: [ 184 | light(vector(-2.0, 2.5, 0.0), color(0.49, 0.07, 0.07)), 185 | light(vector(1.5, 2.5, 1.5), color(0.07, 0.07, 0.49)), 186 | light(vector(1.5, 2.5, -1.5), color(0.07, 0.49, 0.071)), 187 | light(vector(0.0, 3.5, 0.0), color(0.21, 0.21, 0.35)) 188 | ], 189 | camera: camera(vector(3.0, 2.0, 4.0), vector(-1.0, 0.5, 0.0)) 190 | }); 191 | 192 | const exec = (width, height) => { 193 | const canv = document.createElement("canvas"); 194 | canv.width = width; 195 | canv.height = height; 196 | (new RayTracer).render(defaultScene(), canv.getContext("2d"), width, height); 197 | document.body.appendChild(canv); 198 | }; 199 | 200 | const size = 256; 201 | const render = () => new Promise(resolve => { 202 | requestAnimationFrame(() => { 203 | console.time('raytrace JS'); 204 | exec(size, size); 205 | console.timeEnd('raytrace JS'); 206 | resolve(); 207 | }); 208 | }); 209 | 210 | render() 211 | .then(render) 212 | .then(render) 213 | .then(render) 214 | .then(render) 215 | .then(render) 216 | .then(() => { 217 | document.documentElement.classList.add('done'); 218 | }); 219 | -------------------------------------------------------------------------------- /trace.ts.js: -------------------------------------------------------------------------------- 1 | import('./dummy.dom.js').then(() => import('./raytracer.js')); 2 | -------------------------------------------------------------------------------- /trace.vanilla.js: -------------------------------------------------------------------------------- 1 | import('./dummy.dom.js').then(() => import('./rt.vanilla.js')); 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "ES2022"], 4 | "module": "ES2022", 5 | "target": "ES2022", 6 | "useDefineForClassFields": false, 7 | "sourceMap": true 8 | } 9 | } 10 | --------------------------------------------------------------------------------