Published
Edited
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
frame % 360
Insert cell
Insert cell
modelView = {
transform({
translate: [0,0,-1.75],
scale: [0.7, 0.7, 0.7],
rotate: {
rad: 2 * (frame + 30) * Math.PI / 180,
axis: [0,1,0]
}
}, mutable mainModelView)
transform({
rotate: {
rad: 1 * (frame + 30) * Math.PI / 180,
axis: [0,0,1]
}
}, mutable mainModelView, mainModelView)
return mainModelView
}
Insert cell
geometryModelView = {
const M = mat4.identity([]);
transform({
translate: [0,0,-1.75],
scale: [0.5, 0.5, 0.5],
rotate: {
rad: 20 * Math.PI / 180,
axis: [0,1,0]
}
}, M, M);
transform({
}, M, M);
return M;
}
Insert cell
perspective = mat4.perspective([],
60 * Math.PI / 180, // fov
viewport.width / viewport.height, // aspect
0.1, // near
100 // far
)
Insert cell
light = ({
position: [2,2,3],
color: [1,1,1],
ambient: 0.3
})
Insert cell
Insert cell
uniforms = ({
u_world : (_, {modelview}) => modelview,
u_lightPosition: (_, {light}) => light.position,
u_lightColor: (_, {light}) => light.color,
u_ambientLight: (_, {light}) => light.ambient,
u_projection: perspective
})
Insert cell
fragmentShader = `
precision mediump float;
varying vec3 v_color;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;
uniform vec3 u_lightColor;
uniform float u_ambientLight;
void main() {
float maxLightComponent = max(max(u_lightColor.r, u_lightColor.g), u_lightColor.b);
vec3 color = (u_lightColor / maxLightComponent) * v_color;
float pointLight = 1.0 - u_ambientLight;
vec3 a = u_ambientLight * color;
vec3 p = pointLight * clamp(color * dot(v_normal, v_surfaceToLight), 0.0, 1.0);
gl_FragColor = vec4(a + p, 1);
}
`
Insert cell
drawSphere = regl({
attributes: {
a_position: sphere.pos,
a_color: regl.prop('color'),
a_normal: sphere.normal
},
frag: fragmentShader,
vert: `
uniform mat4 u_world;
uniform mat4 u_projection;
uniform vec3 u_lightPosition;

attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec3 a_color;

varying vec3 v_color;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;

void main() {
gl_Position = u_projection * u_world * vec4(a_position, 1.0);
v_color = a_color;
v_normal = (mat3(u_world) * a_normal).xyz;
v_surfaceToLight = vec3(vec4(u_lightPosition, 1.0) - gl_Position);
}`,
uniforms: {
...uniforms
},
elements: function(_, {wireframe}) {
return wireframe ? sphere.edges : sphere.faces;
},
depth: {
enable: true,
func: '<',
mask: true,
},
})

Insert cell
drawWireframe = regl({
attributes: {
a_position: sphere.edgePos,
a_color: regl.prop('color'),
a_normal: sphere.edgeNormals
},
frag: fragmentShader,
vert: `
uniform mat4 u_world;
uniform mat4 u_projection;
uniform vec3 u_lightPosition;
uniform float u_lineWidth;

attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec3 a_color;

varying vec3 v_color;
varying vec3 v_normal;
varying vec3 v_surfaceToLight;

void main() {
gl_Position = u_projection * u_world * vec4(a_position + u_lineWidth * a_normal, 1.0);
v_color = a_color;
v_normal = (mat3(u_world) * a_normal).xyz;
v_surfaceToLight = vec3(vec4(u_lightPosition, 1.0) - gl_Position);
}
`,
uniforms: {
...uniforms,
u_lineWidth: regl.prop('lineWidth')
},
elements: sphere.edgeFaces,
depth: {
enable: true,
func: '<',
mask: true,
},
})
Insert cell
Insert cell
Insert cell
{
let i = 0;
const frameRate = detail > 30 ? 1 : 30;
while (playing) {
mutable frame = i;
await Promises.delay(1000 / frameRate);
yield ++i;
}
}
Insert cell
DRAW = {
regl.clear({
color: [0, 0, 0, 1]
})

function color([x,y,z]) {
const r = (y + 1) / 2;
const g = 1 - (y + 1) / 2;
const b = (y + 1) / 2;
return vec3.scale([], [r,g,b], 1/Math.max(r,g,b))
}

function drawVertices(vertexSize, baseModelView, vertexColor, light) {
for (const P of sphere.pos) {
const mv = transform({translate: P, scale: [vertexSize, vertexSize, vertexSize]});
mat4.multiply(mv, baseModelView, mv);
const c = vertexColor(P)
drawSphere({
modelview: mv,
color: sphere.pos.map(_ => c),
wireframe: false,
light
})
}
}

switch (mode) {
case "object":
drawSphere({
modelview: modelView,
color: sphere.pos.map(color),
wireframe: false,
light
})
break;
case "object with triangles":
drawSphere({
modelview: modelView,
color: sphere.pos.map(color),
wireframe: false,
light
})
drawWireframe({
modelview: modelView,
color: sphere.edgeLookup.map(i => [1,1,1]),
lineWidth: 0.02,
light
});
break;
case "triangles":
drawWireframe({
modelview: modelView,
color: sphere.edgeLookup.map(i => color(sphere.pos[i])),
lineWidth: 0.02,
light: {
...light,
ambient: 0.7
}
});
drawVertices(0.03, mainModelView, color, {
...light,
ambient: 0.7
});
break;
case "vertices":
drawVertices(0.02, mainModelView, color, {
...light,
ambient: 0.7
});
break;
case "geometry":
drawVertices(0.02, geometryModelView, () => [1,1,1], {
...light,
ambient: 0.7
});
break;
default:
throw new Error("Unknown mode " + mode);
}
}
Insert cell
Insert cell
transform = ({translate, scale, rotate}, out = [], from = mat4.identity([])) => {
if (translate) mat4.translate(out, from, translate);
if (rotate) mat4.rotate(out, out, rotate.rad, rotate.axis);
if (scale) mat4.scale(out, out, scale);
return out;
}
Insert cell
Insert cell
Insert cell
sphere = convertGeometry(new THREE.SphereGeometry(1, detail, detail))
Insert cell
convertGeometry = function(geom) {
function pointToVector({x,y,z}) { return [x,y,z] }
const pos = geom.vertices.map(pointToVector)
const normal = geom.vertices.map(_ => [0,0,0]);
const edgeLookup = [];
const edgePos = [];
const edgeFaces = [];
const edgeNormals = [];
function normals(v, otherEndpoint, normal) {
const edge = vec3.sub([], otherEndpoint, v);
const n1 = vec3.cross([], normal, edge)
const n2 = vec3.negate([], n1);
return [n1, n2];
}
function addEdge(ia, ib) {
/*
a1 b2
| |
a---------------b
| |
a2 b1
*/
const a = pos[ia];
const b = pos[ib];
const [na1, na2] = normals(a, b, normal[ia]);
const [nb1, nb2] = normals(b, a, normal[ib]);
const a1 = edgePos.length;
const b1 = a1 + 1;
const a2 = a1 + 2;
const b2 = a1 + 3;
edgeLookup.push(ia,ib,ia,ib);
edgePos.push(a,b,a,b);
edgeNormals.push(na1, nb1, na2, nb2);
edgeFaces.push(
[a1, a2, b2],
[b1, b2, a2]
);
}

// this does unnecessary work, but whatever
for (const f of geom.faces) {
normal[f.a] = pointToVector(f.vertexNormals[0]);
normal[f.b] = pointToVector(f.vertexNormals[1]);
normal[f.c] = pointToVector(f.vertexNormals[2]);
// edges.push([f.a, f.b])
// edges.push([f.b, f.c])
// edges.push([f.a, f.c])
addEdge(f.a, f.b);
addEdge(f.b, f.c);
addEdge(f.c, f.a);
}
return {
pos,
normal,
faces: geom.faces.map(({a,b,c}) => [a,b,c]),
edgePos,
edgeFaces,
edgeNormals,
edgeLookup,
geom
}
}
Insert cell
Insert cell
regl = createRegl(canvas)
Insert cell
createRegl = require('regl@1.4.2/dist/regl.js')
Insert cell
THREE = await require("three@0.99.0/build/three.min.js");
Insert cell
glMatrix = await require("https://unpkg.com/gl-matrix@3/gl-matrix-min.js")
Insert cell
mat4 = glMatrix.mat4
Insert cell
vec4 = glMatrix.vec4
Insert cell
vec3 = glMatrix.vec3
Insert cell
import {select, slider} from "@jashkenas/inputs"
Insert cell
import {Button} from "@observablehq/inputs"
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