Last active
April 1, 2021 15:21
-
-
Save paulrobello/f86b0dcc16a21263b2f98b620f02722c to your computer and use it in GitHub Desktop.
Typescript Playground Raytracer cleaned up a bit with more display function. Requires you disable JSX in Typescript options.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | |
var mag = Vector.mag(v); | |
var 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 red = new Color(1.0, 0.0, 0.0); | |
static green = new Color(0.0, 1.0, 0.0); | |
static blue = new Color(0.0, 0.0, 1.0); | |
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) { | |
var legalize = (d:number) => 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 { | |
public forward: Vector; | |
public right: Vector; | |
public up: Vector; | |
constructor(public pos: Vector, lookAt: Vector) { | |
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))); | |
} | |
} | |
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 | null; | |
normal: (pos: Vector) => Vector; | |
surface: Surface; | |
} | |
interface Light { | |
pos: Vector; | |
color: Color; | |
} | |
interface Scene { | |
things: Thing[]; | |
lights: Light[]; | |
camera: Camera; | |
} | |
class Sphere implements Thing { | |
public 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) { | |
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 as Sphere, ray: ray, dist: dist }; | |
} | |
} | |
} | |
class Plane implements Thing { | |
public normal: (pos: Vector) =>Vector; | |
public intersect: (ray: Ray) =>Intersection | null; | |
constructor(norm: Vector, offset: number, public surface: Surface) { | |
this.normal = function(pos: Vector) { return norm; } | |
this.intersect = function(ray: Ray): Intersection | null { | |
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 as Plane, ray: ray, dist: dist }; | |
} | |
} | |
} | |
} | |
module Surfaces { | |
export var shiny: Surface = { | |
diffuse: function(pos) { return Color.white; }, | |
specular: function(pos) { return Color.grey; }, | |
reflect: function(pos) { return 0.7; }, | |
roughness: 250 | |
} | |
export var shinyRed: Surface = { | |
diffuse: function(pos) { return Color.red; }, | |
specular: function(pos) { return Color.grey; }, | |
reflect: function(pos) { return 0.7; }, | |
roughness: 250 | |
} | |
export var shinyBlue: Surface = { | |
diffuse: function(pos) { return Color.blue; }, | |
specular: function(pos) { return Color.grey; }, | |
reflect: function(pos) { return 0.7; }, | |
roughness: 250 | |
} | |
export var 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; | |
constructor(maxDepth: number = 5) { | |
this.maxDepth = maxDepth; | |
} | |
private intersections(ray: Ray, scene: Scene) { | |
let closest = +Infinity; | |
let closestInter: Intersection | null | undefined = undefined; | |
for (var i in scene.things) { | |
const 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) { | |
const isect = this.intersections(ray, scene); | |
if (isect != null) { | |
return isect.dist; | |
} else { | |
return undefined; | |
} | |
} | |
private traceRay(ray: Ray, scene: Scene, depth: number): Color { | |
const 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) { | |
const d = isect.ray.dir; | |
const pos = Vector.plus(Vector.times(isect.dist, d), isect.ray.start); | |
const normal = isect.thing.normal(pos); | |
const reflectDir = Vector.minus(d, Vector.times(2, Vector.times(Vector.dot(normal, d), normal))); | |
const naturalColor = Color.plus(Color.background, | |
this.getNaturalColor(isect.thing, pos, normal, reflectDir, scene)); | |
const 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) { | |
var addLight = (col: Color, light: Light) => { | |
const ldis = Vector.minus(light.pos, pos); | |
const livec = Vector.norm(ldis); | |
const neatIsect = this.testRay({ start: pos, dir: livec }, scene); | |
const isInShadow = (neatIsect === undefined) ? false : (neatIsect <= Vector.mag(ldis)); | |
if (isInShadow) { | |
return col; | |
} else { | |
const illum = Vector.dot(livec, norm); | |
const lcolor = (illum > 0) ? Color.scale(illum, light.color) | |
: Color.defaultColor; | |
const specular = Vector.dot(livec, Vector.norm(rd)); | |
const 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: Scene, ctx: CanvasRenderingContext2D, screenWidth: number, screenHeight: number) { | |
const getPoint = (x: number, y: number, camera: Camera) => { | |
const recenterX = (x1: number) =>(x1 - (screenWidth / 2.0)) / 2.0 / screenWidth; | |
const recenterY = (y1: number) => - (y1 - (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); | |
} | |
} | |
} | |
} | |
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.shinyBlue), | |
new Sphere(new Vector(-1.5, 0.5, 0), 0.5, Surfaces.shinyRed) | |
], | |
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:HTMLCanvasElement | null = document.getElementById("canvas") as HTMLCanvasElement; | |
if (!canv){ | |
canv = <HTMLCanvasElement>document.createElement("canvas"); | |
canv.setAttribute('id','canvas'); | |
canv.style.setProperty('position','absolute'); | |
canv.style.setProperty('position','absolute'); | |
canv.style.setProperty('top','0'); | |
canv.style.setProperty('right','0'); | |
canv.style.setProperty('z-index','1000'); | |
document.body.appendChild(canv); | |
canv.onclick=function(){ | |
canv?.parentNode?.removeChild(canv as HTMLCanvasElement); | |
}; | |
} | |
canv.width = window.innerWidth; | |
canv.height = window.innerHeight; | |
const ctx: CanvasRenderingContext2D | null = canv.getContext("2d"); | |
if (!ctx) { | |
console.error('Failed to get 2d Context'); | |
return; | |
} | |
const rayTracer = new RayTracer(5); | |
return rayTracer.render(defaultScene(), ctx, canv.width, canv.height); | |
} | |
exec(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment