-
-
Save dodo/1181534 to your computer and use it in GitHub Desktop.
This file contains 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
var abs = Math.abs; | |
var sqrt = Math.sqrt; | |
var floor = Math.floor; | |
var min = Math.min; | |
var V3 = function(x, y, z) { | |
this.x = x; | |
this.y = y; | |
this.z = z; | |
} | |
V3.prototype = { | |
// iop -> inplace | |
// ops -> scalar | |
add: function(v) { | |
return new V3(this.x+v.x, this.y+v.y, this.z+v.z); | |
}, | |
iadd: function(v) { | |
this.x += v.x; | |
this.y += v.y; | |
this.z += v.z; | |
}, | |
sub: function(v) { | |
return new V3(this.x-v.x, this.y-v.y, this.z-v.z); | |
}, | |
isub: function(v) { | |
this.x -= v.x; | |
this.y -= v.y; | |
this.z -= v.z; | |
}, | |
mul: function(v) { | |
return new V3(this.x*v.x, this.y*v.y, this.z*v.z); | |
}, | |
div: function(v) { | |
return new V3(this.x/v.x, this.y/v.y, this.z/v.z); | |
}, | |
muls: function(s) { | |
return new V3(this.x*s, this.y*s, this.z*s); | |
}, | |
imuls: function(s) { | |
this.x *= s; | |
this.y *= s; | |
this.z *= s; | |
}, | |
divs: function(s) { | |
return this.muls(1.0/s); | |
}, | |
dot: function(v) { | |
return this.x*v.x+this.y*v.y+this.z*v.z; | |
}, | |
normalize: function(){ | |
var s = 1.0/sqrt(this.x*this.x+this.y*this.y+this.z*this.z); | |
return new V3(this.x*s, this.y*s, this.z*s); | |
}, | |
magnitude: function(){ | |
return sqrt(this.x*this.x+this.y*this.y+this.z*this.z); | |
}, | |
magnitude2: function(){ | |
return this.x*this.x+this.y*this.y+this.z*this.z; | |
}, | |
copy: function(){ | |
return new V3(this.x, this.y, this.z); | |
} | |
}; | |
var Camera = function(origin, topleft, topright, bottomleft) { | |
this.origin = origin; | |
this.topleft = topleft; | |
this.topright = topright; | |
this.bottomleft = bottomleft; | |
this.update(); | |
} | |
Camera.prototype = { | |
update: function() { | |
this.xd = this.topright.sub(this.topleft); | |
this.yd = this.bottomleft.sub(this.topleft); | |
}, | |
getMagnitude: function(x, y) { | |
var p = this.topleft.add(this.xd.muls(x)); | |
p.iadd(this.yd.muls(y)); | |
return p.sub(this.origin).magnitude(); | |
}, | |
getRay: function(x, y) { | |
// point on screen plane | |
var p = this.topleft.add(this.xd.muls(x)); | |
p.iadd(this.yd.muls(y)); | |
p.isub(this.origin); | |
return { | |
origin: this.origin, | |
direction: p.normalize() | |
}; | |
} | |
}; | |
var Sphere = function(center, radius) { | |
this.center = center; | |
this.radius = radius; | |
this.radius2 = radius*radius; | |
}; | |
Sphere.prototype = { | |
// returns distance when ray intersects with sphere surface | |
intersect: function(ray) { | |
var distance = ray.origin.sub(this.center); | |
var b = distance.dot(ray.direction); | |
var c = b*b - distance.magnitude2() + this.radius2; | |
return c > 0.0 ? -b - sqrt(c) : -1.0; | |
}, | |
getNormal: function(point) { | |
return point.sub(this.center).normalize(); | |
} | |
}; | |
var Body = function(shape, material) { | |
this.shape = shape; | |
this.material = material; | |
} | |
var CubeMap = function(img) { | |
img = getDataFromImage(img); | |
var s = this.size = img.width/3; | |
this.width = img.width; | |
this.left = s*img.width; | |
this.front = (s+s*img.width); | |
this.right = (s*2+s*img.width); | |
this.back = (s+s*3*img.width); | |
this.up = s; | |
this.down = (s+s*2*img.width); | |
this.colors = []; | |
var i = 0; | |
var data = img.data; | |
for(var i = 0;i < data.length; i++) { | |
var color = new V3(data[i++], data[i++], data[i++]); | |
color.imuls(0.00390625); | |
// fake hdri | |
if(color.x*color.y*color.z > 0.95) { | |
color.imuls(2.5); | |
} | |
this.colors.push(color); | |
} | |
} | |
CubeMap.prototype = { | |
sample: function (ray) { | |
var d = ray.direction; | |
var ax = abs(d.x); | |
var ay = abs(d.y); | |
var az = abs(d.z); | |
var s = this.size; | |
var u = 0.0; | |
var v = 0.0; | |
var o; | |
if(ax >= ay && ax >= az){ | |
// right | |
if(d.x > 0.0){ | |
u = 1.0-(d.z/d.x+1.0)*0.5; | |
v = (d.y/d.x+1.0)*0.5; | |
o = this.right; | |
} | |
// left | |
else { | |
u = 1.0-(d.z/d.x+1.0)*0.5 | |
v = 1.0-(d.y/d.x+1.0)*0.5; | |
o = this.left; | |
} | |
} | |
else if(ay >= ax && ay >= az){ | |
// up | |
if(d.y <= 0.0) { | |
u = (d.x/d.y+1.0)*0.5; | |
v = 1.0-(d.z/d.y+1.0)*0.5; | |
o = this.up; | |
} | |
// down | |
else{ | |
u = (d.x/d.y+1.0)*0.5; | |
v = 1.0-(d.z/d.y+1.0)*0.5; | |
o = this.down; | |
} | |
} | |
else{ | |
// front | |
if(d.z>0.0) { | |
u = (d.x/d.z+1.0)*0.5; | |
v = (d.y/d.z+1.0)*0.5; | |
o = this.front; | |
} | |
// back | |
else{ | |
u = (d.x/d.z+1.0)*0.5; | |
v = (d.y/d.z+1.0)*0.5; | |
o = this.back; | |
} | |
} | |
o += floor(u*s) + floor(v*s)*this.width; | |
return this.colors[o]; | |
} | |
} | |
function getDataFromImage(img) { | |
var canvas = document.createElement('canvas'); | |
canvas.width = img.width; | |
canvas.height = img.height; | |
var ctx = canvas.getContext('2d'); | |
ctx.drawImage(img, 0 ,0); | |
return ctx.getImageData(0, 0, img.width, img.height); | |
} | |
var Renderer = function(scene, ctx) { | |
this.scene = scene; | |
this.ctx = ctx; | |
this.img = ctx.getImageData(0, 0, scene.output.width, scene.output.height); | |
this.data = this.img.data; | |
} | |
Renderer.prototype = { | |
render: function() { | |
var w = this.img.width; | |
var h = this.img.height; | |
var i = 0; | |
var data = this.data; | |
for(var y=0.0, ystep=1.0/h; y < 0.99999; y+=ystep){ | |
for(var x=0, xstep=1.0/w; x < 0.99999; x+=xstep){ | |
var ray = this.scene.camera.getRay(x, y); | |
var color = this.trace(ray, 0); | |
data[i++] = min(floor(color.x*256), 255); | |
data[i++] = min(floor(color.y*256), 255); | |
data[i++] = min(floor(color.z*256), 255); | |
data[i++] = 255; | |
} | |
} | |
this.ctx.putImageData(this.img, 0, 0); | |
}, | |
trace: function(ray, n) { | |
var mint = Infinity; | |
var hit = null; | |
for(var i = 0; i < this.scene.objects.length; i++){ | |
var o = this.scene.objects[i]; | |
var t = o.shape.intersect(ray); | |
if(t > 0.0 && t < mint) { | |
mint = t; | |
hit = o; | |
} | |
} | |
if(hit) { | |
var origin = ray.origin.add(ray.direction.muls(mint)); | |
var normal = hit.shape.getNormal(origin); | |
var direction = hit.material.bounce(ray, normal) | |
// if the ray is refracted move the intersection in a bit | |
if(direction.dot(ray.direction) > 0.0){ | |
n -= 1; | |
origin = ray.origin.add(ray.direction.muls(1.000001*mint)); | |
} | |
var newray = { | |
origin: origin, | |
direction: direction | |
} | |
if(n > 1) { | |
return this.scene.sky.sample(ray); | |
} | |
return this.trace(newray, n+1).mul(hit.material.color); | |
} | |
return this.scene.sky.sample(ray); | |
} | |
} | |
var Chrome = function(color) { | |
this.color = color; | |
} | |
Chrome.prototype = { | |
bounce: function(ray, normal) { | |
var theta1 = abs(ray.direction.dot(normal)); | |
return ray.direction.add(normal.muls(theta1*2.0)); | |
} | |
}; | |
var Glass = function(color, ior) { | |
this.color = color; | |
this.ior = ior; | |
} | |
Glass.prototype = { | |
bounce: function(ray, normal) { | |
var theta1 = abs(ray.direction.dot(normal)); | |
if(theta1 >= 0.0) { | |
var internalIndex = this.ior; | |
var externalIndex = 1.0; | |
} | |
else { | |
var internalIndex = 1.0; | |
var externalIndex = this.ior; | |
} | |
var eta = externalIndex/internalIndex; | |
var theta2 = sqrt(1.0 - (eta * eta) * (1.0 - (theta1 * theta1))); | |
// reflection | |
return ray.direction.muls(eta).sub(normal.muls(theta2-eta*theta1)); | |
} | |
} | |
var Animator = function() { | |
this.t = 0.0; | |
this.points = []; | |
} | |
Animator.prototype = { | |
add: function(point, speed) { | |
this.points.push({ | |
point: point, | |
radius: point.magnitude(), | |
alpha: Math.atan2(1, 0) - Math.atan2(point.x, point.z), | |
speed: speed | |
}); | |
}, | |
tick: function(td) { | |
this.t += td; | |
for(var i = 0; i < this.points.length; i++) { | |
var p = this.points[i]; | |
p.point.x = Math.cos(p.alpha+this.t*p.speed)*p.radius; | |
p.point.z = Math.sin(p.alpha+this.t*p.speed)*p.radius; | |
} | |
//for(var i = 0; i < objects.lenght; i++){ | |
// var shape = objects[i].shape; | |
// this.transform(shape.center); | |
//} | |
} | |
} | |
function main(canvas, img, quality, motionblur) { | |
var scene = { | |
output: {width: canvas.width*quality, height: canvas.height*quality}, | |
sky: new CubeMap(img), | |
camera: new Camera( | |
new V3(0.0, 0.0, -8.4), | |
new V3(-1.3, -1.0, -7.0), | |
new V3(1.3, -1.0, -7.0), | |
new V3(-1.3, 1.0, -7.0) | |
), | |
objects: [ | |
// glass sphere | |
// new Body(new Sphere(new V3(1.0, 2.0, 0.0), 0.5), new Glass(new V3(1.00, 1.00, 1.00), 1.5, 0.1)), | |
// chrome sphere | |
new Body(new Sphere(new V3(0.0, 0.0, 3.0), 0.5), new Glass(new V3(1.0, 1.0, 1.0), 1.5)), | |
new Body(new Sphere(new V3(0.0, 0.0, 4.5), 0.7), new Chrome(new V3(0.5, 0.5, 0.8))), | |
new Body(new Sphere(new V3(0.0, 0.0, 6.0), 0.6), new Chrome(new V3(0.5, 0.8, 0.5))), | |
new Body(new Sphere(new V3(0.0, 0.0, 7.5), 0.5), new Chrome(new V3(0.8, 0.5, 0.5))), | |
], | |
} | |
var ctx = canvas.getContext('2d'); | |
var animator = new Animator(scene); | |
animator.add(scene.camera.origin, 0.15); | |
animator.add(scene.camera.topleft, 0.15); | |
animator.add(scene.camera.topright, 0.15); | |
animator.add(scene.camera.bottomleft, 0.15); | |
for(var i = 0; i < scene.objects.length;i++) { | |
var speed = 2/(i+1); | |
if(i&1) speed = -speed; | |
animator.add(scene.objects[i].shape.center, speed); | |
} | |
var buffer = document.createElement('canvas'); | |
buffer.width = scene.output.width; | |
buffer.height = scene.output.height; | |
var renderer = new Renderer(scene, buffer.getContext('2d')); | |
var fps = new FPSCounter(ctx); | |
ctx.globalAlpha = 1.00-motionblur; | |
window.setInterval(function() { | |
animator.tick(0.03); | |
scene.camera.update(); | |
renderer.render(); | |
ctx.drawImage(buffer, 0, 0, canvas.width, canvas.height); | |
fps.draw(); | |
}, 5); | |
} | |
function init() { | |
var canvas = document.createElement('canvas'); | |
canvas.width = 320; | |
canvas.height = 240; | |
document.body.appendChild(canvas); | |
var ctx = canvas.getContext('2d'); | |
ctx.fillText('Loading textures...', 20, 30); | |
var img = document.createElement('img'); | |
var q = 0.0 + document.getElementById('quality').value; | |
document.forms[0].style.display = 'none'; | |
img.onload = function() { | |
main(canvas, img, q, 0.05); | |
} | |
img.src = 'uffizi_cross.jpeg'; | |
} | |
var FPSCounter = function(ctx) { | |
this.t = new Date().getTime()/1000.0; | |
this.n = 0; | |
this.fps = 0.0; | |
this.ctx = ctx; | |
} | |
FPSCounter.prototype = { | |
draw: function() { | |
this.n ++; | |
if(this.n == 50) { | |
this.n = 0; | |
t = new Date().getTime()/1000.0; | |
this.fps = Math.round(50.0/(t-this.t)*100)/100; | |
this.t = t; | |
} | |
this.ctx.fillText('FPS: ' + this.fps, 1, 25); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment