Published
Edited
Jan 31, 2021
1 fork
Importers
29 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
createREGL = require('regl')
Insert cell
regl = createREGL({
canvas: canvas,
extensions: ['ANGLE_instanced_arrays']
})
Insert cell
Insert cell
mesh = ({
vertices: regl.buffer(bunnyVertices),
faces: regl.elements(bunnyFaces),
normals: regl.buffer(bunnyNormals),
faceCount: bunnyFaces.length,
vertexCount: bunnyVertices.length,
})
Insert cell
Insert cell
drawMesh = regl({
vert: `
precision highp float;
attribute vec3 aVertex, aNormal;
uniform mat4 uProjectionView;
varying vec3 vNormal;
void main () {
vNormal = aNormal;
gl_Position = uProjectionView * vec4(aVertex, 1);
}
`,
frag: `
precision highp float;
varying vec3 vNormal;
void main () {
gl_FragColor = vec4(0.5 + 0.5 * vNormal, 1);
}
`,
attributes: {
aVertex: regl.prop('vertices'),
aNormal: regl.prop('normals'),
},
elements: regl.prop('faces'),
});
Insert cell
Insert cell
drawNormals = {
return regl({
vert: `
precision highp float;
uniform mat4 uProjectionView, uView, uProjection;
uniform float uTailWidth, uAspect, uScale, uForeshortening;
uniform vec2 uArrowheadShape;
attribute vec3 aVertex, aNormal;
attribute vec4 aArrow;
void main () {
vec4 vp = uView * vec4(aVertex, 1);
vec4 vpn = uView * vec4(aVertex + uScale * aNormal, 1);

// Project the vertex into homogeneous coordinates
vec4 p = uProjection * vp;

// Project the vertex + normal into homogeneous coordinates
vec4 pn = uProjection * vpn;

float foreshortening = max(0.1, sqrt(1.0 - uForeshortening * abs((vp - vpn).z) / length((vp - vpn).xyz)));
// Use the y component of aArrow to select either p or pn
gl_Position = mix(p, pn, aArrow.y);

// Compute a screen-space vector parallel to the arrow.
// This step includes "perspective division" to convert homogeneous
// 4D coordinates into screen space coordinates.
// NB: it also includes an aspect ratio to scale x and y equally. This could be
// done more cleanly.
vec2 unitVector = normalize((pn.xy / pn.w - p.xy / p.w) * vec2(uAspect, 1));
// Rotate 90 degrees to get a perpendicular vector
vec2 perpUnitVector = vec2(-unitVector.y, unitVector.x);

// Perturb the point according to the aArrow instance data
gl_Position.xy += (
// Offset perpendicular to the length of the arrow:
perpUnitVector * (aArrow.x * uTailWidth + aArrow.w * uArrowheadShape.y) +

// and parallel to the length of the arrow:
+ unitVector * aArrow.z * uArrowheadShape.x * foreshortening

// This final step is just a bit tricky, but we need to pull the aspect
// ratio back out and then multiply by w to get the arrow scaled correctly
) / vec2(uAspect, 1) * gl_Position.w;

}
`,
frag: `
precision highp float;
uniform vec4 uColor;
void main () {
gl_FragColor = uColor;
}
`,
attributes: {
aVertex: {
buffer: regl.prop('vertices'),
divisor: 1, // Advance the mesh vertex once per instance
stride: 12 // each instance advances 3 floats (= 12 bytes)
},
aNormal: {
buffer: regl.prop('normals'),
divisor: 1,
stride: 12
},
// prettier-ignore
aArrow: new Float32Array([
// The per-instance triangles are defined in terms of four pieces of data which tell where
// on the arrow we are, using the mesh vertex and mesh normal as inputs. The components are:
// x: selects the position perpendicular to the length of the arrow in screen space
// y: selects either the (vertex) or (vertex + normal) in 3D space
// z: selects the arrowhead length-wise offset in screen space
// w: selects the arrowhead width-wise offset in screen space
// The first triangle of the tail:
-1, 0, 0, 0,
1, 0, 0, 0,
1, 1, -1, 0,

// The second triangle of the tail:
-1, 0, 0, 0,
1, 1, -1, 0,
-1, 1,-1, 0,

// The arrowhead:
0, 1, -1, -1,
0, 1, -1, 1,
0, 1, 0, 0
])
},
uniforms: {
// Define the screen-space line width in terms of a property but also
// scaled by the pixel ratio so that it remains constant at different
// pixel ratios. (The framebuffer height is just for proper screen-space
// scaling)
uTailWidth: (ctx, props) =>
(props.arrowTailWidth / ctx.framebufferHeight) * ctx.pixelRatio,
// Define the shape of the arrowhead. This just the scale factor
// for the ones and zeros above.
uArrowheadShape: (ctx, props) => [
(props.arrowheadLength / ctx.framebufferHeight) * ctx.pixelRatio * 2.0,
(props.arrowheadWidth / ctx.framebufferHeight) * ctx.pixelRatio
],
// The aspect ratio affects computation of offsets for the screen-space
// lines.
uAspect: ctx => ctx.framebufferWidth / ctx.framebufferHeight,
uColor: regl.prop('arrowColor'),
uForeshortening: (ctx, props) => (props.foreshortening ? 1 : 0),

// Not really necessary, but an overall scale factor for the normals
uScale: regl.prop('arrowScale')
},
primitive: 'triangles',
instances: (ctx, props) => props.vertexCount, // One instance per vertex
count: 9 // Nine vertices per instance
});
}
Insert cell
Insert cell
mutable raf = null
Insert cell
meshWithProps = {
// This triggers a redraw whenever the view properties change
camera.taint();
return Object.assign({}, mesh, {
arrowTailWidth,
arrowheadWidth,
arrowheadLength,
arrowScale,
foreshortening,
arrowColor: hexToRgb(arrowColor)
});
}
Insert cell
{
if (mutable raf !== null) {
mutable raf.cancel()
mutable raf = null;
}
// whenever we modify this, trigger a redraw:
camera.taint()
mutable raf = regl.frame(() => {
camera((state) => {
if (!state.dirty) return;
regl.clear({color: [1, 1, 1, 1]})
// This is the crux of it all! We don't need to do anything special
// with the data to render it as either a mesh or as screen-space
// normal arrows.
drawMesh(meshWithProps)
drawNormals(meshWithProps)
})
})
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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