|
import { Camera } from './Camera' |
|
import { Color } from './Color' |
|
import { Intersection } from './Intersection' |
|
import { Light } from './Light' |
|
import { Ray } from './Ray' |
|
import { Scene } from './Scene' |
|
import { Thing } from './Thing' |
|
import { Vector } from './Vector' |
|
|
|
/** |
|
* Computes the closest intersection of a ray with all things in the scene. |
|
* Returns undefined if the ray does not intersect. |
|
*/ |
|
function intersections(ray: Ray, scene: Scene): Intersection | undefined { |
|
let closest = +Infinity |
|
let closestInter: Intersection | undefined = undefined |
|
for (const thing of scene.things) { |
|
const inter = thing.intersect(ray) |
|
if (inter !== null && inter.dist < closest) { |
|
closestInter = inter |
|
closest = inter.dist |
|
} |
|
} |
|
return closestInter |
|
} |
|
|
|
/** |
|
* Returns the distance to the nearest intersecting thing, or undefined |
|
* if the ray does not intersect an object in the scene. |
|
*/ |
|
function testRay(ray: Ray, scene: Scene): number | undefined { |
|
const isect = intersections(ray, scene) |
|
if (typeof isect !== 'undefined') { |
|
return isect.dist |
|
} |
|
else { |
|
return undefined |
|
} |
|
} |
|
|
|
export class RayTracer { |
|
|
|
private maxDepth = 5 |
|
|
|
private traceRay(ray: Ray, scene: Scene, depth: number): Color { |
|
const isect = 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) { |
|
normal = normal |
|
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) { |
|
const addLight = (col: Color, light: Light) => { |
|
const ldis = Vector.minus(light.pos, pos) |
|
const livec = Vector.norm(ldis) |
|
const neatIsect = 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) |
|
} |
|
|
|
/** |
|
* Renders the scene to the HTML5 canvas 2D context. |
|
*/ |
|
render(scene: Scene, ctx: CanvasRenderingContext2D, screenWidth: number, screenHeight: number) { |
|
/** |
|
* Maybe not very well named. I think this is computing the unit vector direction of the ray? |
|
*/ |
|
const getPoint = (x: number, y: number, camera: Camera): Vector => { |
|
const recenterX = (xInner: number) => (xInner - (screenWidth / 2.0)) / 2.0 / screenWidth |
|
const recenterY = (yInner: number) => -(yInner - (screenHeight / 2.0)) / 2.0 / screenHeight |
|
const right = Vector.times(recenterX(x), camera.right) |
|
const up = Vector.times(recenterY(y), camera.up) |
|
return Vector.norm(Vector.plus(camera.forward, Vector.plus(right, up))) |
|
} |
|
// Compute the color of every pixel by tracing a ray backwards |
|
for (let y = 0; y < screenHeight; y++) { |
|
for (let x = 0; x < screenWidth; x++) { |
|
const color = this.traceRay({ start: scene.camera.pos, dir: getPoint(x, y, scene.camera) }, scene, 0) |
|
const c = Color.toDrawingColor(color) |
|
ctx.fillStyle = "rgb(" + String(c.r) + ", " + String(c.g) + ", " + String(c.b) + ")" |
|
ctx.fillRect(x, y, x + 1, y + 1) |
|
} |
|
} |
|
} |
|
|
|
} |