Skip to content

Instantly share code, notes, and snippets.

@pvande
Created October 1, 2012 20:43
Show Gist options
  • Save pvande/3814296 to your computer and use it in GitHub Desktop.
Save pvande/3814296 to your computer and use it in GitHub Desktop.
CoffeeScript Raytracer
class Vector
constructor: (@x, @y, @z) ->
@times: (k, {x,y,z}) -> new this(k * x, k * y, k * z)
@minus: (a, b) -> new this(a.x - b.x, a.y - b.y, a.z - b.z)
@plus: (a, b) -> new this(a.x + b.x, a.y + b.y, a.z + b.z)
@dot: (a, b) -> a.x * b.x + a.y * b.y + a.z * b.z
@mag: ({x,y,z}) -> Math.sqrt(x * x + y * y + z * z)
@norm: (v) -> @times(1.0 / @mag(v), v)
@cross: (a, b) -> new this(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x)
@down: new this(0.0, -1.0, 0.0)
class Color
constructor: (@r, @g, @b) ->
@scale: (k, v) -> new this(k * v.r, k * v.g, k * v.b)
@plus: (a, b) -> new this(a.r + b.r, a.g + b.g, a.b + b.b)
@times: (a, b) -> new this(a.r * b.r, a.g * b.g, a.b * b.b)
@white: new this(1.0, 1.0, 1.0)
@grey: new this(0.5, 0.5, 0.5)
@black: new this(0.0, 0.0, 0.0)
@background: @black;
@defaultColor: @black;
@toDrawingColor: ({r,g,b}) ->
r: Math.floor(Math.min(r, 1) * 255)
g: Math.floor(Math.min(g, 1) * 255)
b: Math.floor(Math.min(b, 1) * 255)
class Camera
constructor: (@pos, lookAt) ->
@forward = Vector.norm(Vector.minus(lookAt, @pos))
@right = Vector.times(1.5, Vector.norm(Vector.cross(@forward, Vector.down)))
@up = Vector.times(1.5, Vector.norm(Vector.cross(@forward, @right)))
class Sphere
constructor: (@center, radius, @surface) ->
@radius2 = radius * radius
normal: (pos) -> Vector.norm(Vector.minus(pos, @center))
intersect: (ray) ->
eo = Vector.minus(@center, ray.start)
v = Vector.dot(eo, ray.dir)
if v >= 0
if (disc = @radius2 - (Vector.dot(eo, eo) - v * v)) >= 0
{ thing: this, ray, dist: v - Math.sqrt(disc) }
class Plane
constructor: (@norm, @offset, @surface) ->
normal: -> @norm
intersect: (ray) ->
if (denom = Vector.dot(@norm, ray.dir)) <= 0
dist = (Vector.dot(@norm, ray.start) + @offset) / (-denom)
{ thing: this, ray, dist }
Surfaces =
shiny:
diffuse: -> Color.white
specular: -> Color.grey
reflect: -> 0.7
roughness: 250
checkerboard:
diffuse: (pos) ->
if (Math.floor(pos.z) + Math.floor(pos.x)) % 2 is 0 then Color.black else Color.white
specular: -> Color.white
reflect: (pos) ->
if (Math.floor(pos.z) + Math.floor(pos.x)) % 2 is 0 then 0.7 else 0.1
roughness: 150
class RayTracer
@::maxDepth = 5
intersections: (ray, scene) ->
for thing in scene.things when i = thing.intersect(ray)
closest = i if i.dist <= (closest || i).dist
closest
testRay: (ray, scene) ->
@intersections(ray, scene)?.dist
traceRay: (ray, scene, depth) ->
if i = @intersections(ray, scene) then @shade(i, scene, depth) else Color.background
shade: ({dist,ray,thing}, scene, i) ->
normal = thing.normal(pos = Vector.plus(Vector.times(dist, ray.dir), ray.start))
rDir = Vector.minus ray.dir, Vector.times(2, Vector.times(Vector.dot(normal, ray.dir), normal))
naturalColor = Color.plus(Color.background, @getNaturalColor(thing, pos, normal, rDir, scene))
reflectedColor = @getReflectionColor(thing, pos, rDir, scene, i) if i < @maxDepth
Color.plus(naturalColor, reflectedColor || Color.grey)
getReflectionColor: ({surface}, start, dir, scene, depth) ->
Color.scale(surface.reflect(start), @traceRay({ start, dir }, scene, depth + 1))
getNaturalColor: ({surface}, pos, norm, rd, scene) ->
addLight = (col, light) =>
ldis = Vector.minus(light.pos, pos)
livec = Vector.norm(ldis)
neatIsect = @testRay({ start: pos, dir: livec }, scene)
return col if (neatIsect ? 1.0 / 0.0) <= Vector.mag(ldis)
illum = Vector.dot(livec, norm)
lcolor = Color.scale(illum, light.color) if illum > 0
spec = Vector.dot(livec, Vector.norm(rd))
scolor = Color.scale(Math.pow(spec, surface.roughness), light.color) if spec > 0
lcolor = Color.times(surface.diffuse(pos), lcolor || Color.defaultColor)
scolor = Color.times(surface.specular(pos), scolor || Color.defaultColor)
Color.plus(col, Color.plus(lcolor, scolor))
scene.lights.reduce(addLight, Color.defaultColor)
render: (scene, ctx, width, height) ->
{pos, forward, right, up} = c = scene.camera
getPoint = (x, y) ->
x = +(x - (width / 2.0)) / 2.0 / width
y = -(y - (height / 2.0)) / 2.0 / height
Vector.norm(Vector.plus(forward, Vector.plus(Vector.times(x, right), Vector.times(y, up))))
for y in [0...height]
for x in [0...width]
{r,g,b} = Color.toDrawingColor(@traceRay({ start: pos, dir: getPoint(x, y) }, scene, 0))
ctx.fillStyle = "rgb(#{r}, #{g}, #{b})"
ctx.fillRect(x, y, x + 1, y + 1)
scene =
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))
canv = document.createElement("canvas")
canv.width = canv.height = 256
document.body.appendChild(canv)
ctx = canv.getContext("2d")
new RayTracer().render(scene, ctx, 256, 256)
@pvande
Copy link
Author

pvande commented Oct 1, 2012

Created for comparison with the TypeScript Raytracer (http://www.typescriptlang.org/Samples/)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment