Published
Edited
Jun 3, 2022
3 forks
3 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
//
// Helper function to create a new vector and set its coordinates
//
Vector3 = (x=0,y=0,z=0) => vec3.fromValues(x,y,z)
Insert cell
//
// Sets 'out' to vector v reflected with respect to vector n (surface normal).
// Both v and n are assumed to be normalized.
// Returns false if dot(n,v) <= 0, otherwise returns out.
//
function reflection (out,v,n) {
let vdotn = vec3.dot(v,n);
if (vdotn < 0) return false;
return vec3.sub(out,vec3.scale(vec3.create(),n,2*vdotn),v);
}
Insert cell
//
// A ray in 3d
//
class Ray {
// o: the origin point (from where the ray is emitted)
// v: a unit vector indicating the ray propagation direction
constructor (o, v) {
[this.o, this.v] = [o,v];
}
// Point at distance t from the origin
at (t) {
let p = vec3.clone(this.o);
return vec3.scaleAndAdd(p,p,this.v,t)
}
}
Insert cell
//
// A sphere in 3D
//
class Sphere {
// c : the center point
// r : the radius
constructor (c, r) {
[this.c,this.r] = [c,r]
}
// Ray intersection
intersect (ray) {
// Translate ray to sphere's local coordinate system
let s = vec3.sub(vec3.create(),this.c,ray.o);
// Coeficients of quadratic equation
let a = vec3.dot(ray.v,ray.v);
let b = -2.0 * vec3.dot(s,ray.v);
let c = vec3.dot(s,s) - this.r*this.r;
// Discriminant
let d = b*b - 4*a*c;
if (d > 0.0) {
let sign = Math.sign(c);
return (-b - sign*Math.sqrt(d))/(2*a);
}
// No hit - return infinity
return Number.MAX_VALUE;
}
// Returns normal vector at p
normal (p) {
let n = vec3.sub(vec3.create(),p,this.c);
return vec3.normalize(n,n)
}
}
Insert cell
//
// A Plane in 3D
//
class Plane {
// p : a point on the plane
// n : plane normal
constructor (p,n) {
[this.p,this.n] = [p,n];
}
// Returns distance along ray to object
intersect (ray) {
// ray must not be parallel to plane
let vdotn = vec3.dot(this.n,ray.v);
if (vdotn == 0) return Number.MAX_VALUE;
// vector from the ray origin to p
let s = vec3.sub(vec3.create(),this.p,ray.o);
return vec3.dot(s,this.n) / vdotn;
}
// Returns the normal at p (constant for a plane)
normal(p) {
return this.n
}
}
Insert cell
Insert cell
//
// A point light in 3d
//
class PointLight {
// p : light position
// l : color (a vec3 with r,g,b values between 0 and 1)
constructor (p,l) {
[this.p,this.l] = [p,l];
}
}
Insert cell
//
// A simple phong material with ambient, diffuse, and specular components
//
class Material {
// c : overall color of the object (a vec3)
// a : ambient coefficient
// d : diffuse coefficient (a fraction between 0 and 1)
// s : specular coefficient (a fraction between 0 and 1)
// e : specularity exponent (a positive value)
constructor (c,a,d,s,e) {
[this.c,this.a,this.d,this.s,this.e] = [c,a,d,s,e]
}
}
Insert cell
//
// A Scene object (geometry+material)
//
class Obj {
// g : geometry
// m : material
constructor (g,m) {
[this.g,this.m] = [g,m]
}
}
Insert cell
//
// A Camera is defined by its position, which should be placed somewhere on the
// positive z halfspace; and the dimensions of the projection plane defined on
// the z=0 plane, given by xmin,ymin (the lower left corner) and xsize,ysize
// (width and height of the projection rectangle)
class Camera {
constructor (pos, xmin=-1,ymin=-1,xsize=2,ysize=2) {
[this.pos, this.xmin, this.ymin, this.xsize, this.ysize] = [pos, xmin,ymin,xsize,ysize]
}
}
Insert cell
//
// A Viewport defines what part of the target canvas will be painted with
// the rendered image.
//
class Viewport {
// x,y : the coordinates of the upper left corner
// width/height: the width and height of the viewport in pixels
constructor (x = 0, y = 0, width = 512, height=512) {
[this.x,this.y,this.width,this.height] = [x,y,width,height]
}
}
Insert cell
Insert cell
scene = {
// Geometries
let s1 = new Sphere(Vector3(0.5,0.0,-1.0),0.5);
let s2 = new Sphere(Vector3(-0.6,-1.0,-3.5),0.5);
let p1 = new Plane(Vector3(-1.5,0,0),Vector3(1,0,0));
let p2 = new Plane(Vector3(1.5,0,0),Vector3(-1,0,0));
let p3 = new Plane(Vector3(0,-1.5,0),Vector3(0,1,0));
let p4 = new Plane(Vector3(0,1.5,0),Vector3(0,-1,0));
let p5 = new Plane(Vector3(0,0,-5),Vector3(0,0,1));
let p6 = new Plane(Vector3(0,0,5),Vector3(0,0,-1));
// Materials
let matteWhite = new Material(Vector3(1,1,1),0.3,0.6,0,0);
let shinyBlue = new Material(Vector3(0.2,0.2,1),0.3,0.5,0.5,30);
let shinyYellow = new Material(Vector3(1,1,0),0.3,0.5,0.5,20);
// Objects
let objects = [new Obj(s1,shinyBlue),
new Obj(s2,shinyYellow),
new Obj(p1,matteWhite),
new Obj(p2,matteWhite),
new Obj(p3,matteWhite),
new Obj(p4,matteWhite),
new Obj(p5,matteWhite),
new Obj(p6,matteWhite)
];
// Lights
let lights = [new PointLight(Vector3(0,1,0), Vector3(1,1,1))];
// Camera
let camera = new Camera(Vector3 (0,0,4),-1,-1,2,2);
// Viewport
let viewport = new Viewport(0,0,512,512);
return {
objects,
lights,
camera,
viewport
}
}
Insert cell
Insert cell
pixelColor = {
// Obtain scene components
let { objects, lights, camera, viewport } = scene;
// Returns a ray that starts at the camera position and
// passes through image pixel at px,py
function pixelRay (px,py) {
let pointOnProjectionPlane = Vector3(camera.xsize*px/viewport.width+camera.xmin,
camera.ymin + camera.ysize -camera.ysize*py/viewport.height);
return new Ray (camera.pos, vec3.sub(vec3.create(),pointOnProjectionPlane,camera.pos))
}
// Returns the distance along the ray and the scene object first hit by 'ray'.
// If no object is hit, returns false
function firstHit (ray) {
let intersections = objects.map(obj=>[obj.g.intersect(ray),obj])
intersections = intersections.filter(([dist,obj])=> dist > 0.001 && dist < Number.MAX_VALUE);
if (intersections.length == 0) return false;
return intersections.reduce ((a,b) => a[0]<b[0] ? a : b);
}
// The color black
let black = Vector3 (0,0,0);
// Recursive ray tracing.
// Returns the color seen by ray, following up to 'level' specular reflections.
function raytrace (ray, level = 1) {
// See if we hit anything
let hit = firstHit(ray);
if (!hit) return black;
let [dist,obj] = hit;
// Material properties
let {c,a,d,s,e} = obj.m;
// The final color starts with the ambient color
let rgb = vec3.scale (vec3.create(),c,a);
// The hit point
let q = ray.at(dist);
// The hit normal
let n = obj.g.normal(q);
// Vector from surface point to viewer
let qv = vec3.negate (vec3.create(), ray.v);
vec3.normalize(qv,qv);
// Take into account each light
for (let {p,l} of lights) {
// Vector from surface point to light position
let qp = vec3.sub (vec3.create(), p, q);
let lightDist = vec3.length(qp);
vec3.normalize(qp,qp);
// Cast shadow ray
let shadowRay = new Ray(q,qp);
let shadowHit = firstHit (shadowRay);
if (!shadowHit || shadowHit[0] >= lightDist) {
// Compute diffuse color
vec3.scaleAndAdd (rgb, rgb, c, d * Math.max(0, vec3.dot (qp, n)));
}
// Non-recursive specular computation if term non-zero
if (level <= 0 && s > 0) {
let ref = reflection(vec3.create(),qp,n);
if (ref) {
vec3.scaleAndAdd (rgb, rgb, l, s * Math.pow (vec3.dot (ref,qv), e));
}
}
}
// Recursive specular computation if term non-zero
if (level > 0 && s > 0) {
// Reflected vector
let ref = reflection(vec3.create(),qv,n);
if (ref) {
let color = raytrace(new Ray(q,ref), level-1);
vec3.scaleAndAdd (rgb,rgb, color, s)
}
}
// Clamp to 1
vec3.min (rgb, rgb, Vector3(1,1,1))
return rgb
}
return function (px,py) {
let ray = pixelRay (px,py);
let color = raytrace(ray,1);
vec3.scale(color,color,255);
return color
}
}
Insert cell
Insert cell
{
let ctx = canvas.getContext("2d");
yield* pdisplay(
ctx,
scene.viewport.width,
scene.viewport.height,
pixelColor,
scene.viewport.x,
scene.viewport.y
);
}
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more