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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more