diff --git a/TypeScript/RayTracerExample/example.js b/TypeScript/RayTracerExample/example.js new file mode 100644 index 0000000..c3ea46f --- /dev/null +++ b/TypeScript/RayTracerExample/example.js @@ -0,0 +1,252 @@ +var Vector = /** @class */ (function () { + function Vector(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + Vector.times = function (k, v) { + return new Vector(k * v.x, k * v.y, k * v.z); + }; + Vector.minus = function (v1, v2) { + return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); + }; + Vector.plus = function (v1, v2) { + return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); + }; + Vector.dot = function (v1, v2) { + return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; + }; + Vector.mag = function (v) { + return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + }; + Vector.norm = function (v) { + var mag = Vector.mag(v); + var div = (mag === 0) ? Infinity : 1.0 / mag; + return Vector.times(div, v); + }; + Vector.cross = function (v1, v2) { + 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); + }; + return Vector; +}()); +var Color = /** @class */ (function () { + function Color(r, g, b) { + this.r = r; + this.g = g; + this.b = b; + } + Color.scale = function (k, v) { + return new Color(k * v.r, k * v.g, k * v.b); + }; + Color.plus = function (v1, v2) { + return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b); + }; + Color.times = function (v1, v2) { + return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b); + }; + Color.toDrawingColor = function (c) { + var legalize = function (d) { return d > 1 ? 1 : d; }; + return { + r: Math.floor(legalize(c.r) * 255), + g: Math.floor(legalize(c.g) * 255), + b: Math.floor(legalize(c.b) * 255) + }; + }; + Color.white = new Color(1.0, 1.0, 1.0); + Color.grey = new Color(0.5, 0.5, 0.5); + Color.black = new Color(0.0, 0.0, 0.0); + Color.background = Color.black; + Color.defaultColor = Color.black; + return Color; +}()); +var Camera = /** @class */ (function () { + function Camera(pos, lookAt) { + this.pos = pos; + var down = new Vector(0.0, -1.0, 0.0); + this.forward = Vector.norm(Vector.minus(lookAt, this.pos)); + this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down))); + this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right))); + } + return Camera; +}()); +var Sphere = /** @class */ (function () { + function Sphere(center, radius, surface) { + this.center = center; + this.surface = surface; + this.radius2 = radius * radius; + } + Sphere.prototype.normal = function (pos) { + return Vector.norm(Vector.minus(pos, this.center)); + }; + Sphere.prototype.intersect = function (ray) { + var eo = Vector.minus(this.center, ray.start); + var v = Vector.dot(eo, ray.dir); + var dist = 0; + if (v >= 0) { + var disc = this.radius2 - (Vector.dot(eo, eo) - v * v); + if (disc >= 0) { + dist = v - Math.sqrt(disc); + } + } + if (dist === 0) { + return null; + } + else { + return { thing: this, ray: ray, dist: dist }; + } + }; + return Sphere; +}()); +var Plane = /** @class */ (function () { + function Plane(norm, offset, surface) { + this.surface = surface; + this.normal = function (pos) { return norm; }; + this.intersect = function (ray) { + var denom = Vector.dot(norm, ray.dir); + if (denom > 0) { + return null; + } + else { + var dist = (Vector.dot(norm, ray.start) + offset) / (-denom); + return { thing: this, ray: ray, dist: dist }; + } + }; + } + return Plane; +}()); +var Surfaces; +(function (Surfaces) { + Surfaces.shiny = { + diffuse: function (pos) { return Color.white; }, + specular: function (pos) { return Color.grey; }, + reflect: function (pos) { return 0.7; }, + roughness: 250 + }; + Surfaces.checkerboard = { + diffuse: function (pos) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return Color.white; + } + else { + return Color.black; + } + }, + specular: function (pos) { return Color.white; }, + reflect: function (pos) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return 0.1; + } + else { + return 0.7; + } + }, + roughness: 150 + }; +})(Surfaces || (Surfaces = {})); +var RayTracer = /** @class */ (function () { + function RayTracer() { + this.maxDepth = 5; + } + RayTracer.prototype.intersections = function (ray, scene) { + var closest = +Infinity; + var closestInter = undefined; + for (var i in scene.things) { + var inter = scene.things[i].intersect(ray); + if (inter != null && inter.dist < closest) { + closestInter = inter; + closest = inter.dist; + } + } + return closestInter; + }; + RayTracer.prototype.testRay = function (ray, scene) { + var isect = this.intersections(ray, scene); + if (isect != null) { + return isect.dist; + } + else { + return undefined; + } + }; + RayTracer.prototype.traceRay = function (ray, scene, depth) { + var isect = this.intersections(ray, scene); + if (isect === undefined) { + return Color.background; + } + else { + return this.shade(isect, scene, depth); + } + }; + RayTracer.prototype.shade = function (isect, scene, depth) { + var d = isect.ray.dir; + var pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); + var normal = isect.thing.normal(pos); + var reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); + var naturalColor = Color.plus(Color.background, this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); + var reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); + return Color.plus(naturalColor, reflectedColor); + }; + RayTracer.prototype.getReflectionColor = function (thing, pos, normal, rd, scene, depth) { + return Color.scale(thing.surface.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1)); + }; + RayTracer.prototype.getNaturalColor = function (thing, pos, norm, rd, scene) { + var _this = this; + var addLight = function (col, light) { + var ldis = Vector.minus(light.pos, pos); + var livec = Vector.norm(ldis); + var neatIsect = _this.testRay({ start: pos, dir: livec }, scene); + var isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); + if (isInShadow) { + return col; + } + else { + var illum = Vector.dot(livec, norm); + var lcolor = (illum > 0) ? Color.scale(illum, light.color) + : Color.defaultColor; + var specular = Vector.dot(livec, Vector.norm(rd)); + var scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.roughness), light.color) + : Color.defaultColor; + return Color.plus(col, Color.plus(Color.times(thing.surface.diffuse(pos), lcolor), Color.times(thing.surface.specular(pos), scolor))); + } + }; + return scene.lights.reduce(addLight, Color.defaultColor); + }; + RayTracer.prototype.render = function (scene, ctx, screenWidth, screenHeight) { + var getPoint = function (x, y, camera) { + var recenterX = function (x) { return (x - (screenWidth / 2.0)) / 2.0 / screenWidth; }; + var recenterY = function (y) { return -(y - (screenHeight / 2.0)) / 2.0 / screenHeight; }; + return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up)))); + }; + for (var y = 0; y < screenHeight; y++) { + for (var x = 0; x < screenWidth; x++) { + var color = this.traceRay({ start: scene.camera.pos, dir: getPoint(x, y, scene.camera) }, scene, 0); + var c = Color.toDrawingColor(color); + ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) + ", " + String(c.b) + ")"; + ctx.fillRect(x, y, x + 1, y + 1); + } + } + }; + return RayTracer; +}()); +function defaultScene() { + return { + things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), + new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), + new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)], + lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) }, + { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) }, + { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) }, + { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }], + camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0)) + }; +} +function exec() { + var canv = document.createElement("canvas"); + canv.width = 1000; + canv.height = 1000; + document.body.appendChild(canv); + var ctx = canv.getContext("2d"); + var rayTracer = new RayTracer(); + return rayTracer.render(defaultScene(), ctx, 1000, 1000); +} +exec(); diff --git a/TypeScript/RayTracerExample/example.ts b/TypeScript/RayTracerExample/example.ts new file mode 100644 index 0000000..ee03d03 --- /dev/null +++ b/TypeScript/RayTracerExample/example.ts @@ -0,0 +1,307 @@ +class Vector { + constructor(public x: number, public y: number, public z: number) { } + + static times(k: number, v: Vector) { + return new Vector(k * v.x, k * v.y, k * v.z); + } + + static minus(v1: Vector, v2: Vector) { + return new Vector(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); + } + + static plus(v1: Vector, v2: Vector) { + return new Vector(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); + } + + static dot(v1: Vector, v2: Vector) { + return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; + } + + static mag(v: Vector) { + return Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z); + } + + static norm(v: Vector) { + let mag = Vector.mag(v); + let div = (mag === 0) ? Infinity : 1.0 / mag; + return Vector.times(div, v); + } + + static cross(v1: Vector, v2: Vector) { + 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); + } +} + +class Color { + constructor(public r: number, public g: number, public b: number) { } + + static scale(k: number, v: Color) { + return new Color(k * v.r, k * v.g, k * v.b); + } + + static plus(v1: Color, v2: Color) { + return new Color(v1.r + v2.r, v1.g + v2.g, v1.b + v2.b); + } + + static times(v1: Color, v2: Color) { + return new Color(v1.r * v2.r, v1.g * v2.g, v1.b * v2.b); + } + + static white = new Color(1.0, 1.0, 1.0); + static grey = new Color(0.5, 0.5, 0.5); + static black = new Color(0.0, 0.0, 0.0); + static background = Color.black; + static defaultColor = Color.black; + + static toDrawingColor(c: Color) { + let legalize = d => d > 1 ? 1 : d; + return { + r: Math.floor(legalize(c.r) * 255), + g: Math.floor(legalize(c.g) * 255), + b: Math.floor(legalize(c.b) * 255) + } + } +} + +class Camera { + forward: Vector; + right: Vector; + up: Vector; + + constructor(public pos: Vector, lookAt: Vector) { + let down = new Vector(0.0, -1.0, 0.0); + this.forward = Vector.norm(Vector.minus(lookAt, this.pos)); + this.right = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, down))); + this.up = Vector.times(1.5, Vector.norm(Vector.cross(this.forward, this.right))); + } +} + +interface Ray { + start: Vector; + dir: Vector; +} + +interface Intersection { + thing: Thing; + ray: Ray; + dist: number; +} + +interface Surface { + diffuse: (pos: Vector) => Color; + specular: (pos: Vector) => Color; + reflect: (pos: Vector) => number; + roughness: number; +} + +interface Thing { + intersect: (ray: Ray) => Intersection; + normal: (pos: Vector) => Vector; + surface: Surface; +} + +interface Light { + pos: Vector; + color: Color; +} + +interface Scene { + things: Thing[]; + lights: Light[]; + camera: Camera; +} + +class Sphere implements Thing { + radius2: number; + + constructor(public center: Vector, radius: number, public surface: Surface) { + this.radius2 = radius * radius; + } + + normal(pos: Vector): Vector { + return Vector.norm(Vector.minus(pos, this.center)); + } + + intersect(ray: Ray) { + let eo = Vector.minus(this.center, ray.start); + let v = Vector.dot(eo, ray.dir); + let dist = 0; + if (v >= 0) { + let disc = this.radius2 - (Vector.dot(eo, eo) - v * v); + if (disc >= 0) { + dist = v - Math.sqrt(disc); + } + } + + if (dist === 0) { + return null; + } else { + return { thing: this, ray: ray, dist: dist }; + } + } +} + +class Plane implements Thing { + normal: (pos: Vector) => Vector; + + intersect: (ray: Ray) => Intersection; + + constructor(norm: Vector, offset: number, public surface: Surface) { + this.normal = function(pos: Vector) { return norm; } + this.intersect = function(ray: Ray): Intersection { + let denom = Vector.dot(norm, ray.dir); + + if (denom > 0) { + return null; + } else { + let dist = (Vector.dot(norm, ray.start) + offset) / (-denom); + return { thing: this, ray: ray, dist: dist }; + } + } + } +} + +namespace Surfaces { + export let shiny: Surface = { + diffuse: function(pos) { return Color.white; }, + specular: function(pos) { return Color.grey; }, + reflect: function(pos) { return 0.7; }, + roughness: 250 + } + + export let checkerboard: Surface = { + diffuse: function(pos) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return Color.white; + } else { + return Color.black; + } + }, + + specular: function(pos) { return Color.white; }, + reflect: function(pos) { + if ((Math.floor(pos.z) + Math.floor(pos.x)) % 2 !== 0) { + return 0.1; + } else { + return 0.7; + } + }, + roughness: 150 + } +} + +class RayTracer { + private maxDepth = 5; + + private intersections(ray: Ray, scene: Scene) { + let closest = +Infinity; + let closestInter: Intersection = undefined; + for (let i in scene.things) { + let inter = scene.things[i].intersect(ray); + if (inter != null && inter.dist < closest) { + closestInter = inter; + closest = inter.dist; + } + } + return closestInter; + } + + private testRay(ray: Ray, scene: Scene) { + let isect = this.intersections(ray, scene); + if (isect != null) { + return isect.dist; + } else { + return undefined; + } + } + + private traceRay(ray: Ray, scene: Scene, depth: number): Color { + let isect = this.intersections(ray, scene); + if (isect === undefined) { + return Color.background; + } else { + return this.shade(isect, scene, depth); + } + } + + private shade(isect: Intersection, scene: Scene, depth: number) { + let d = isect.ray.dir; + let pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); + let normal = isect.thing.normal(pos); + let reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); + let naturalColor = Color.plus(Color.background, + this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); + let reflectedColor = (depth >= this.maxDepth) ? Color.grey : this.getReflectionColor(isect.thing, pos, normal, reflectDir, scene, depth); + return Color.plus(naturalColor, reflectedColor); + } + + private getReflectionColor(thing: Thing, pos: Vector, normal: Vector, rd: Vector, scene: Scene, depth: number) { + return Color.scale(thing.surface.reflect(pos), this.traceRay({ start: pos, dir: rd }, scene, depth + 1)); + } + + private getNaturalColor(thing: Thing, pos: Vector, norm: Vector, rd: Vector, scene: Scene) { + let addLight = (col, light) => { + let ldis = Vector.minus(light.pos, pos); + let livec = Vector.norm(ldis); + let neatIsect = this.testRay({ start: pos, dir: livec }, scene); + let isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); + if (isInShadow) { + return col; + } else { + let illum = Vector.dot(livec, norm); + let lcolor = (illum > 0) ? Color.scale(illum, light.color) + : Color.defaultColor; + let specular = Vector.dot(livec, Vector.norm(rd)); + let scolor = (specular > 0) ? Color.scale(Math.pow(specular, thing.surface.roughness), light.color) + : Color.defaultColor; + return Color.plus(col, Color.plus(Color.times(thing.surface.diffuse(pos), lcolor), + Color.times(thing.surface.specular(pos), scolor))); + } + } + return scene.lights.reduce(addLight, Color.defaultColor); + } + + render(scene, ctx, screenWidth, screenHeight) { + let getPoint = (x, y, camera) => { + let recenterX = x => (x - (screenWidth / 2.0)) / 2.0 / screenWidth; + let recenterY = y => -(y - (screenHeight / 2.0)) / 2.0 / screenHeight; + return Vector.norm(Vector.plus(camera.forward, Vector.plus(Vector.times(recenterX(x), camera.right), Vector.times(recenterY(y), camera.up)))); + } + + for (let y = 0; y < screenHeight; y++) { + for (let x = 0; x < screenWidth; x++) { + let color = this.traceRay({ start: scene.camera.pos, dir: getPoint(x, y, scene.camera) }, scene, 0); + let c = Color.toDrawingColor(color); + ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) + ", " + String(c.b) + ")"; + ctx.fillRect(x, y, x + 1, y + 1); + } + } + } +} + +function defaultScene(): Scene { + return { + things: [new Plane(new Vector(0.0, 1.0, 0.0), 0.0, Surfaces.checkerboard), + new Sphere(new Vector(0.0, 1.0, -0.25), 1.0, Surfaces.shiny), + new Sphere(new Vector(-1.0, 0.5, 1.5), 0.5, Surfaces.shiny)], + lights: [{ pos: new Vector(-2.0, 2.5, 0.0), color: new Color(0.49, 0.07, 0.07) }, + { pos: new Vector(1.5, 2.5, 1.5), color: new Color(0.07, 0.07, 0.49) }, + { pos: new Vector(1.5, 2.5, -1.5), color: new Color(0.07, 0.49, 0.071) }, + { pos: new Vector(0.0, 3.5, 0.0), color: new Color(0.21, 0.21, 0.35) }], + camera: new Camera(new Vector(3.0, 2.0, 4.0), new Vector(-1.0, 0.5, 0.0)) + }; +} + +function exec() { + let canv = document.createElement("canvas"); + canv.width = 1000; + canv.height = 1000; + document.body.appendChild(canv); + let ctx = canv.getContext("2d"); + let rayTracer = new RayTracer(); + return rayTracer.render(defaultScene(), ctx, 1000, 1000); +} + +exec(); diff --git a/TypeScript/RayTracerExample/index.html b/TypeScript/RayTracerExample/index.html new file mode 100644 index 0000000..4b7facb --- /dev/null +++ b/TypeScript/RayTracerExample/index.html @@ -0,0 +1,13 @@ + + +
+ + + +