|
<!DOCTYPE html> |
|
<html> |
|
<head><title>Javascript RT</title></head> |
|
|
|
<body> |
|
<canvas id="framebuffer" width="800" height="600"></canvas> |
|
<script type="text/javascript"> |
|
/* Javascript minimal ray tracing engine |
|
* Copyright (C) 2012 Salvatore Sanfilippo <[email protected]> |
|
* |
|
* This softare is released under the terms of the BSD two-clause license. */ |
|
|
|
var ctx; |
|
var pixels; |
|
var screen_width = 320; |
|
var screen_height = 200; |
|
var frame = 0; |
|
|
|
function init() { |
|
ctx = document.getElementById('framebuffer').getContext('2d'); |
|
pixels = ctx.createImageData(screen_width, screen_height); |
|
setInterval(draw, 1000 / 25); |
|
}; |
|
|
|
function draw() { |
|
if (frame > 160) return; |
|
|
|
var start = new Date().getTime(); |
|
computeScene(); |
|
ctx.putImageData(pixels, 0, 0); |
|
console.debug("Frame "+frame+" ms: "+((new Date().getTime())-start)); |
|
frame++; |
|
}; |
|
|
|
function computeScene() { |
|
var scene = new Scene(); |
|
|
|
var sphere1 = scene.addObject(new Obj("Sphere 1",new Sphere())); |
|
var sphere2 = scene.addObject(new Obj("Sphere 2",new Sphere())); |
|
var plane = scene.addObject(new Obj("Ground",new Plane(0,.5,-2,0,.5,-4,2,.5,-2))); |
|
var light1 = scene.addLight(new Obj("Light 1",new Light())); |
|
var light2 = scene.addLight(new Obj("Light 2",new Light())); |
|
var light3 = scene.addLight(new Obj("Light 3",new Light())); |
|
|
|
sphere1.o.center.x = Math.cos(frame/10); |
|
sphere1.o.center.z = Math.sin(frame/10); |
|
sphere1.o.radius = 0.5; |
|
|
|
sphere2.o.center.x = 0; |
|
sphere2.o.radius = 0.5; |
|
|
|
light1.o.center.set(4,-1,-2); |
|
light2.o.center.set(-1,-1,-2); |
|
light3.o.center.set(1,-6,-2); |
|
|
|
light1.color.r = .5; |
|
light1.color.g = .5; |
|
light1.color.b = .5; |
|
|
|
light2.color.r = .3; |
|
light2.color.g = .3; |
|
light2.color.b = .3; |
|
|
|
light3.color.r = .4; |
|
light3.color.g = .4; |
|
light3.color.b = .4; |
|
|
|
sphere1.color.r = 1; |
|
sphere1.color.g = .3; |
|
sphere1.color.b = .3; |
|
sphere1.specularity = .5; |
|
sphere1.reflection = .1; |
|
|
|
sphere2.color.r = .3; |
|
sphere2.color.g = 1; |
|
sphere2.color.b = .3; |
|
sphere2.specularity = .5; |
|
sphere2.reflection = .1; |
|
|
|
plane.color.r = .3; |
|
plane.color.g = .3; |
|
plane.color.b = .3; |
|
|
|
scene.traceScene(); |
|
} |
|
|
|
// Vector implementation |
|
|
|
function Vector(x,y,z) { |
|
if (arguments.length == 0) { |
|
x = y = z = 0; |
|
} |
|
this.x = x; |
|
this.y = y; |
|
this.z = z; |
|
} |
|
|
|
Vector.prototype = { |
|
// Set x, y, z |
|
set: function(x,y,z) { |
|
this.x = x; |
|
this.y = y; |
|
this.z = z; |
|
}, |
|
|
|
// Vector magnitude |
|
magnitude: function() { |
|
return Math.sqrt(this.x*this.x + this.y*this.y + this.z*this.z); |
|
}, |
|
|
|
// Vector normalization (modify the vector so that that it has |
|
// magnitude 1 but with the same orientation as the original vector). |
|
normalize: function() { |
|
var m = this.magnitude(); |
|
|
|
if (m == 0) m = 1; |
|
this.x /= m; |
|
this.y /= m; |
|
this.z /= m; |
|
return this; |
|
} |
|
} |
|
|
|
// Ray implementation |
|
|
|
function Ray() { |
|
this.origin = new Vector(); |
|
this.direction = new Vector(); |
|
} |
|
|
|
// Sphere implementation |
|
|
|
function Sphere() { |
|
this.type = "sphere"; |
|
this.center = new Vector(); |
|
this.radius = 1.0; |
|
} |
|
|
|
Sphere.prototype = { |
|
/* Normal on a point of the sphere, normalized. |
|
We don't use the Vector object for speed issues. */ |
|
normalToPoint: function(x,y,z) { |
|
x -= this.center.x; |
|
y -= this.center.y; |
|
z -= this.center.z; |
|
/* we already know the magnitudo that is just the radius of the sphere |
|
so we can easily normalize the vector just dividing for it. */ |
|
x /= this.radius; |
|
y /= this.radius; |
|
z /= this.radius; |
|
return {x: x, y: y, z: z}; |
|
}, |
|
|
|
/* Intersect a ray with this sphere. Returns an object |
|
with two fields: 'type' and 'dist'. |
|
|
|
Type can be: |
|
|
|
0: no intersection |
|
1: internal intersection (the ray origin is inside the sphere) |
|
2: external intersection (the ray origin is outside the sphere) |
|
|
|
The distance is simply the distance between the ray origin and |
|
the intersection point. |
|
|
|
NOTE: This function needs ray.direction to be normalized. */ |
|
intersect: function(ray) { |
|
var x, y, z, distance = +Infinity; |
|
|
|
/* set x,y,z to the sphere center minus the ray origin. */ |
|
x = this.center.x - ray.origin.x; |
|
y = this.center.y - ray.origin.y; |
|
z = this.center.z - ray.origin.z; |
|
/* compute x,y,z dot product with itself. */ |
|
var xyz_dot = (x*x)+(y*y)+(z*z); |
|
/* compute dot product between x,y,z and ray direction. */ |
|
var b = (x*ray.direction.x)+(y*ray.direction.y)+(z*ray.direction.z); |
|
|
|
/* We can now compute the discriminant and check for intersections. */ |
|
var disc = b*b - xyz_dot + this.radius*this.radius; |
|
var type = 0; |
|
|
|
if (disc > 0) { |
|
var d = Math.sqrt(disc); |
|
var root1 = b-d; |
|
var root2 = b+d; |
|
|
|
if (root2 > 0) { |
|
if (root1 < 0) { |
|
if (root2 < distance) { distance = root2; type = -1; } |
|
} else { |
|
if (root1 < distance) { distance = root1; type = 1; } |
|
} |
|
} |
|
} |
|
return {type: type, dist: distance}; |
|
} |
|
} |
|
|
|
// Plane |
|
|
|
function Plane(x1,y1,z1,x2,y2,z2,x3,y3,z3) { |
|
/* Create a plane passing from the three points. We take as input |
|
the three points but internally represent the plane as the normal |
|
to the plane and a point, in the form: |
|
|
|
ax + by + cz + d = 0 |
|
|
|
Note: a,b,c is the vector representing the normal. We need to compute |
|
a,b,c and d. |
|
|
|
To compute the normal vector a,b,c we use the fact that given three |
|
points we can compute v1 and v2, two vectors that are parallel to the |
|
plan. */ |
|
var v1x = x1-x2, v1y = y1-y2, v1z = z1-z2; |
|
var v2x = x1-x3, v2y = y1-y3, v2z = z1-z3; |
|
|
|
/* Now the vectorial product between v1 and v2 is a vector that |
|
is normal to the plane. */ |
|
var nx = (v1y*v2z)-(v1z*v2y); |
|
var ny = (v1z*v2x)-(v1x*v2z); |
|
var nz = (v1x*v2y)-(v1y*v2x); |
|
|
|
/* We are still missing d, but since we know that x1,y1,z1 is a point |
|
on the plane, we know that: |
|
|
|
ax1 + by1 + cz1 + d = 0 |
|
|
|
so |
|
|
|
d = -(ax1 + by1 + cz1) |
|
*/ |
|
this.normal = new Vector(nx,ny,nz); |
|
this.d = -(nx*x1 + ny*y1 + nz*z1); |
|
} |
|
|
|
Plane.prototype = { |
|
normalToPoint: function(x,y,z) { |
|
return this.normal; |
|
}, |
|
|
|
intersect: function(ray) { |
|
var type = 0; |
|
var distance = +Infinity; |
|
|
|
/* First check if there is an intersection between the ray and the |
|
plane. If the ray is parallel to the plane there is not for sure. |
|
|
|
Compute the dot product between the ray direction and the plan |
|
normal (that is, the cosine of the angle between the two vectors). |
|
If it is zero we can not have a collision. */ |
|
var ndotrd = (this.normal.x * ray.direction.x) + |
|
(this.normal.y * ray.direction.y) + |
|
(this.normal.z * ray.direction.z); |
|
if (ndotrd) { |
|
/* Compute the intersection distance. */ |
|
var ndoro = (this.normal.x * ray.origin.x) + |
|
(this.normal.y * ray.origin.y) + |
|
(this.normal.z * ray.origin.z); |
|
distance = - (ndoro + this.d)/ndotrd; |
|
/* Now there is an intersection only if the distance is positive, |
|
otherwise the intersection is not in the ray direction |
|
(but is backward). */ |
|
if (distance > 0) type = 1; |
|
} |
|
return {type: type, dist: distance}; |
|
} |
|
} |
|
|
|
// Light |
|
|
|
function Light() { |
|
this.type = "light"; |
|
this.center = new Vector(); |
|
} |
|
|
|
// Generic scene object implementation |
|
|
|
function Obj(name,o) { |
|
this.name = name; |
|
this.o = o; |
|
this.color = {r: 1, g: 1, b: 1}; |
|
this.specularity = 0; |
|
this.reflection = 0; |
|
} |
|
|
|
// Scene object |
|
|
|
function Scene() { |
|
this.objects = []; |
|
this.lights = []; |
|
} |
|
|
|
Scene.prototype = { |
|
addObject: function(o) { |
|
this.objects.push(o); |
|
return o; |
|
}, |
|
|
|
addLight: function(o) { |
|
this.lights.push(o); |
|
return o; |
|
}, |
|
|
|
traceRay: function (ray, depth) { |
|
var obj = null; |
|
var color = {r: 0, g: 0, b: 0}; |
|
var distance = +Infinity; |
|
|
|
for (var j = 0; j < this.objects.length; j++) { |
|
var test_obj = this.objects[j]; |
|
var res = test_obj.o.intersect(ray); |
|
if (res.type) { |
|
if (obj == null || res.dist < distance) { |
|
obj = test_obj; |
|
distance = res.dist; |
|
} |
|
} |
|
} |
|
|
|
/* Determine the color if we hit sometihng */ |
|
if (obj) { |
|
/* We have the distance, so we can compute the intersection |
|
point simply as: origin + (direction * distance) */ |
|
var x = ray.origin.x + ray.direction.x*distance, |
|
y = ray.origin.y + ray.direction.y*distance, |
|
z = ray.origin.z + ray.direction.z*distance; |
|
/* Now get the normal to the intersection point of the object */ |
|
var normal = obj.o.normalToPoint(x,y,z); |
|
|
|
for (var j = 0; j < this.lights.length; j++) { |
|
var light = this.lights[j]; |
|
|
|
/* COMPUTE RAY INTERSECTION POINT. |
|
Compute the vector that looks from the intersection |
|
point to the light. We do this simply subtracting the |
|
intersection point from the light position. */ |
|
var lx = light.o.center.x - x; |
|
var ly = light.o.center.y - y; |
|
var lz = light.o.center.z - z; |
|
/* Normalize */ |
|
var len = Math.sqrt(lx*lx + ly*ly + lz*lz); |
|
if (len == 0) len = 1; |
|
lx /= len; |
|
ly /= len; |
|
lz /= len; |
|
|
|
/* HANDLE SHADOWS. |
|
Is this light obscured by some other object? |
|
We simply trace a ray between the ray-object intersection |
|
point (x,y,z variables) and the light center. |
|
|
|
If this ray intersects some object with a distance smaller |
|
than the light itself, this point is in shadow from the |
|
point of view of this light. */ |
|
/* Compute the distance between the point and the light. */ |
|
var pldistance = Math.sqrt( |
|
(x-light.o.center.x)*(x-light.o.center.x)+ |
|
(y-light.o.center.y)*(y-light.o.center.y)+ |
|
(z-light.o.center.z)*(z-light.o.center.z)); |
|
var sray = new Ray(); |
|
sray.origin.set(x,y,z); |
|
/* We need to add a small number towards the light to the |
|
origin vector, to make sure it will not intersect with the |
|
current object itself. */ |
|
sray.origin.x += lx/10000; |
|
sray.origin.y += ly/10000; |
|
sray.origin.z += lz/10000; |
|
/* To set the direction we can use lx, ly, lz variables that |
|
are already set to a normalized vector pointing from the |
|
intersection point to the light. */ |
|
sray.direction.set(lx, ly, lz); |
|
var shadow = false; |
|
for (var i = 0; i < this.objects.length; i++) { |
|
var test_obj = this.objects[i]; |
|
var res = test_obj.o.intersect(sray); |
|
if (res.type && res.dist < pldistance) { |
|
shadow = true; |
|
break; |
|
} |
|
} |
|
if (shadow) continue; /* try next light */ |
|
|
|
/* DIFFUSE SHADING. |
|
Now the dot product between the normal and the point |
|
towards the light is the cosine between the angle |
|
formed by the vectors, that is, our brightness. */ |
|
var cosine = normal.x*lx+normal.y*ly+normal.z*lz; |
|
if (cosine < 0) cosine = 0; |
|
color.r += cosine * obj.color.r * light.color.r; |
|
color.g += cosine * obj.color.g * light.color.g; |
|
color.b += cosine * obj.color.b * light.color.b; |
|
|
|
/* SPECULAR SHADING. |
|
Add point of view dependent specular light using |
|
Phone shading. */ |
|
if (obj.specularity > 0) { |
|
var vrx = lx - normal.x * cosine * 2, |
|
vry = ly - normal.y * cosine * 2, |
|
vrz = lz - normal.z * cosine * 2; |
|
var cosSigma = (ray.direction.x*vrx)+ |
|
(ray.direction.y*vry)+ |
|
(ray.direction.z*vrz); |
|
if (cosSigma > 0) { |
|
var specularity = obj.specularity; |
|
color.r += light.color.r * specularity * Math.pow(cosSigma,64); |
|
color.g += light.color.g * specularity * Math.pow(cosSigma,64); |
|
color.b += light.color.b * specularity * Math.pow(cosSigma,64); |
|
} |
|
} |
|
|
|
/* REFLECTION. |
|
This is easy, we compute the reflection ray, that |
|
is given by: |
|
|
|
Rr = R - 2* N * (R dot N) |
|
|
|
Where R is the ray that we want reflected. |
|
N is the normal of the surface. |
|
|
|
Having the ray we simply call this function recursively |
|
to check the color of the object hit by this reflected |
|
ray. */ |
|
if (obj.reflection > 0 && depth < 3) { |
|
var rr = new Ray(); |
|
var dotnr = (ray.direction.x * normal.x) + |
|
(ray.direction.y * normal.y) + |
|
(ray.direction.z * normal.z); |
|
rr.origin.set(x,y,z); |
|
rr.direction.set(ray.direction.x - 2 * normal.x * dotnr, |
|
ray.direction.y - 2 * normal.y * dotnr, |
|
ray.direction.z - 2 * normal.z * dotnr); |
|
rr.origin.x += rr.direction.x / 10000; |
|
rr.origin.y += rr.direction.y / 10000; |
|
rr.origin.z += rr.direction.z / 10000; |
|
var rcolor = this.traceRay(rr,depth+1); |
|
color.r *= 1-obj.reflection; |
|
color.g *= 1-obj.reflection; |
|
color.b *= 1-obj.reflection; |
|
color.r += rcolor.color.r * obj.reflection; |
|
color.g += rcolor.color.g * obj.reflection; |
|
color.b += rcolor.color.b * obj.reflection; |
|
} |
|
} |
|
if (color.r > 1) color.r = 1; |
|
if (color.g > 1) color.g = 1; |
|
if (color.b > 1) color.b = 1; |
|
} |
|
return {obj: obj, color: color} |
|
}, |
|
|
|
traceScene: function () { |
|
var pov = new Vector(0,0,-4); |
|
var ray = new Ray(); |
|
|
|
ray.origin = pov |
|
for (var x = 0; x < screen_width; x++) { |
|
for (var y = 0; y < screen_height; y++) { |
|
ray.direction.set((x-screen_width/2)/100, |
|
(y-screen_height/2)/100, |
|
4); |
|
ray.direction.normalize(); |
|
var trace = this.traceRay(ray,0); |
|
var offset = x*4+y*4*screen_width; |
|
|
|
pixels.data[offset+3] = 255; |
|
pixels.data[offset+0] = trace.color.r*255; |
|
pixels.data[offset+1] = trace.color.g*255; |
|
pixels.data[offset+2] = trace.color.b*255; |
|
} |
|
} |
|
} |
|
} |
|
|
|
init(); |
|
|
|
</script> |
|
</body> |
|
</html> |