Skip to content

Instantly share code, notes, and snippets.

@yvan-sraka
Last active April 9, 2018 21:35
Show Gist options
  • Save yvan-sraka/4bb885ec72b954efe899e885010313c5 to your computer and use it in GitHub Desktop.
Save yvan-sraka/4bb885ec72b954efe899e885010313c5 to your computer and use it in GitHub Desktop.

jsrt README

jsrt is a simple to understand Javascript implementation of ray tracing that was designed mainly to be easy to understand and modify. It only supports very basic features (spheres and planes are the only objects you can render) but is fast enough to render small animations in real time.

If you want to see it in action, there is a demo here.

<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment