Published
Edited
Dec 30, 2018
12 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function* main(w, h, samplesCount = 4) {
const position = new Vec(-22, 5, 25);
const goal = new Vec(-3, 4, 0).sub(position).norm();
const left = new Vec(goal.z, 0, -goal.x).norm().mul(1 / w);

// Cross-product to get the up vector
const up = new Vec(
goal.y * left.z - goal.z * left.y,
goal.z * left.x - goal.x * left.z,
goal.x * left.y - goal.y * left.x
);

const imgData = new ImageData(w, h);
for(let x = w; x--;) {
for(let y = h; y--;) {
let color = new Vec;
for(let p = samplesCount; p--;) {
color = color.add(trace(
position,
goal
.add(left.mul(x - w / 2 + rng()))
.add(up.mul(y - h / 2 + rng()))
.norm()
));
}
// Reinhard tone mapping
color = color.mul(1 / samplesCount);
imgData.data.set([
color.x / (color.x + 1) * 255,
color.y / (color.y + 1) * 255,
color.z / (color.z + 1) * 255,
255
], imgData.data.length - (1 + x + y * w) * 4);
}
yield imgData;
}
}
Insert cell
function rng() {
return Math.random();
}
Insert cell
function mod(a, b) {
return a - Math.floor(a / b) * b;
}
Insert cell
class Vec {
constructor(a = 0, b = a, c = a) {
this.x = a;
this.y = b;
this.z = arguments.length === 2 ? 0 : c;
}
add(v) {
return new Vec(this.x + v.x, this.y + v.y, this.z + v.z);
}
sub(v) {
return new Vec(this.x - v.x, this.y - v.y, this.z - v.z);
}
mul(s) {
return new Vec(this.x * s, this.y * s, this.z * s);
}
dot(v) {
return this.x * v.x + this.y * v.y + this.z * v.z;
}
norm() {
return this.mul(1 / Math.sqrt(this.dot(this)));
}
cross(v) {
return new Vec(
this.y * v.z - this.z * v.y,
this.z * v.x - this.x * v.z,
this.x * v.y - this.y * v.x
);
}
// Cast object to vec (required for workers).
static cast(o) {
return new Vec(o.x, o.y, o.z);
}
}
Insert cell
/**
* Rectangle CSG equation. Returns minimum signed distance from space
* carved by lowerLeft vertex and opposite rectangle vertex upperRight.
*
* @param {Vec} position
* @param {Vec} lowerLeft
* @param {Vec} upperRight
* @return {number}
*/
function boxTest(position, lowerLeft, upperRight) {
lowerLeft = position.sub(lowerLeft);
upperRight = upperRight.sub(position);
return -Math.min(
lowerLeft.x, upperRight.x,
lowerLeft.y, upperRight.y,
lowerLeft.z, upperRight.z
);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
letters = {
// // 15 two points lines
const letters = ''
+ '5O5_' + '5W9W' + '5_9_' // P (without curve)
+ 'AOEO' + 'COC_' + 'A_E_' // I
+ 'IOQ_' + 'I_QO' // X
+ 'UOY_' + 'Y_]O' + 'WW[W' // A
+ 'aOa_' + 'aWeW' + 'a_e_' + 'cWiO'; // R (without curve)
const lines = [];
for(let i = 0; i < letters.length; i+= 4) {
const begin = (new Vec(letters.charCodeAt(i) - 79, letters.charCodeAt(i + 1) - 79)).mul(.5);
const e = (new Vec(letters.charCodeAt(i + 2) - 79, letters.charCodeAt(i + 3) - 79)).mul(.5).sub(begin);
lines.push(begin, e);
}
return lines;
}
Insert cell
/**
* Sample the world using Signed Distance Fields.
*
* @param {Vec} position
* @return {object} - distance, hitType
*/
function queryDatabase(position) {
let distance = 1e9, hitType;
const f = new Vec(position.x, position.y, 0);
for(let i = 0; i < letters.length; i+= 2) {
const begin = Vec.cast(letters[i]);
const e = Vec.cast(letters[i+1]);

const o = f.add(begin.add(
e.mul(Math.min(-Math.min(begin.sub(f).dot(e) / e.dot(e), 0), 1))
).mul(-1));
distance = Math.min(distance, o.dot(o)); // compare squared distance.
}
distance = Math.sqrt(distance); // Get real distance, not square distance.

// Two curves (for P and R in PixaR) with hard-coded locations.
const curves = [new Vec(-11, 6), new Vec(11, 6)];
for(let i = 2; i--;) {
const o = f.add(curves[i].mul(-1));
distance = Math.min(
distance,
o.x > 0 ? Math.abs(Math.sqrt(o.dot(o)) - 2)
: (o.y += o.y > 0 ? -2 : 2, Math.sqrt(o.dot(o)))
);
}
distance = Math.pow(Math.pow(distance, 8) + Math.pow(position.z, 8), .125) - .5;
hitType = HIT_LETTER;
const roomDist = Math.min(// min(A,B) = Union with Constructive solid geometry
//-min carves an empty space
-Math.min(
// Lower room
boxTest(position, new Vec(-30, -.5, -30), new Vec(30, 18, 30)),
// Upper room
boxTest(position, new Vec(-25, 17, -25), new Vec(25, 20, 25))
),
boxTest( // Ceiling "planks" spaced 8 units apart.
new Vec(mod(Math.abs(position.x), 8), position.y, position.z),
new Vec(1.5, 18.5, -25),
new Vec(6.5, 20, 25)
)
);

if(roomDist < distance) {
distance = roomDist;
hitType = HIT_WALL;
}
const sun = 19.9 - position.y; // Everything above 19.9 is light source.
if(sun < distance) {
distance = sun;
hitType = HIT_SUN;
}
return {distance, hitType};
}
Insert cell
/**
* Perform signed sphere marching
* Returns hitType 0, 1, 2, or 3 and update hit position/normal
*
* @param {Vec} origin
* @param {Vec} direction
* @return {object} - hitType, hitPosition, hitNormal
*/
function rayMarching(origin, direction) {
let hitType, hitPosition, hitNormal;
let noHitCount = 0;
let d; // distance from closest object in world.
// Signed distance marching
for(let total_d = 0; total_d < 100; total_d += d) {
hitPosition = origin.add(direction.mul(total_d));
({distance: d, hitType} = queryDatabase(hitPosition)); // distance from closest object in world.
if (d < .01 || ++noHitCount > 99) {
hitNormal = new Vec(
queryDatabase( hitPosition.add(new Vec(.01, 0, 0)) ).distance - d,
queryDatabase( hitPosition.add(new Vec(0, .01, 0)) ).distance - d,
queryDatabase( hitPosition.add(new Vec(0, 0, .01)) ).distance - d
).norm();
return {hitType, hitPosition, hitNormal};
}
}
return {hitType: HIT_NONE};
}
Insert cell
/**
* @param {Vec} origin
* @param {Vec} direction
* @return {Vec} - color
*/
function trace(origin, direction) {
let color = new Vec, attenuation = 1;
const attFactor = .2;
const lightDirection = new Vec(.6, .6, 1).norm(); // Directional light

for(let bounceCount = 3; bounceCount--;) {
const { hitType, hitPosition, hitNormal, distance } = rayMarching(origin, direction);

switch(hitType) {
case HIT_NONE:
// No hit. This is over, return color.
return color;
case HIT_LETTER:
// Specular bounce on a letter. No color acc.
direction = direction.add( hitNormal.mul( hitNormal.dot(direction) * -2) );
origin = hitPosition.add(direction.mul(.1));
attenuation *= attFactor; // Attenuation via distance traveled.
break;
case HIT_WALL:
const incidence = hitNormal.dot(lightDirection);
const p = 6.283185 * rng();
const c = rng();
const s = Math.sqrt(1 - c);
const g = hitNormal.z < 0 ? -1 : 1;
const u = -1 / (g + hitNormal.z);
const v = hitNormal.x * hitNormal.y * u;
direction = new Vec(v, g + hitNormal.y * hitNormal.y * u, -hitNormal.y)
.mul(Math.cos(p) * s)
.add(
new Vec(1 + g * hitNormal.x * hitNormal.x * u, g * v, -g * hitNormal.x)
.mul(Math.sin(p) * s)
)
.add(hitNormal.mul(Math.sqrt(c)));
origin = hitPosition.add(direction.mul(.1));
attenuation *= attFactor;
if(incidence > 0 && rayMarching(hitPosition.add(hitNormal.mul(.1)), lightDirection).hitType === HIT_SUN) {
color = color.add( new Vec(500, 400, 100).mul(attenuation).mul(incidence) );
}
break;
case HIT_SUN:
return color.add(new Vec(50, 80, 100).mul(attenuation));
}
}
return color;
}
Insert cell
Insert cell
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