Published
Edited
Mar 25, 2022
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
isoSurfaceCanvas = htl.html`<canvas width=800 height=600>`
Insert cell
Insert cell
rbf = RBF(data.points, data.values, kernel)
Insert cell
interpolant = rbf.interpolant
Insert cell
//
// Returns a scalar field sampling function f(p) that speeds up the computation of func(p).
// The idea is to presample the field within the rectangular region between min and max
// with a n x n x n grid. If p is in a cell that likely does not contain a zero, an
// approximation is returned.
//
function adaptiveSampler(min, max, n, func) {
const dn = d3.zip(min, max).map(([xmin, xmax]) => (xmax - xmin) / n);

// Sample the coarse grid
const grid = [];
for (let i = 0; i <= n; i++) {
const x = i * dn[0] + min[0];
const a = [];
grid.push(a);
for (let j = 0; j <= n; j++) {
const y = j * dn[1] + min[1];
const b = [];
a.push(b);
for (let k = 0; k <= n; k++) {
const z = k * dn[2] + min[2];
b.push(func([x, y, z])[0]);
}
}
}

// Collect cell addresses straddling the zero values of func
const hash = (i, j, k) => (i * (n + 1) + j) * (n + 1) + k;
const zeroCells = new Set();
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
for (let k = 0; k < n; k++) {
let vmin, vmax;
vmin = vmax = grid[i][j][k];
for (let [di, dj, dk] of [
[0, 0, 1],
[0, 1, 0],
[0, 1, 1],
[1, 0, 0],
[1, 0, 1],
[1, 1, 0],
[1, 1, 1]
]) {
let v = grid[i + di][j + dj][k + dk];
vmin = Math.min(vmin, v);
vmax = Math.max(vmax, v);
}

if (vmin < 0 && vmax > 0) {
zeroCells.add(hash(i, j, k));
}
}
}
}

return (p) => {
const index = [],
rem = [];
for (let i of [0, 1, 2]) {
index[i] = ~~((p[i] - min[i]) / dn[i]);
rem[i] = (p[i] - min[i]) % dn[i];
}
const [i, j, k] = index;
if (Math.max(...rem) < 0.01 || !zeroCells.has(hash(i, j, k)))
return [grid[i][j][k]];
return func(p);
};
}
Insert cell
Insert cell
Insert cell
isoObject = {
const [min, max] = [
[-2, -2, -2],
[2, 2, 2]
];
const func = useAdaptive
? adaptiveSampler(min, max, gridSize / 8, interpolant)
: interpolant;
return iso.surfaceNets(
[gridSize, gridSize, gridSize],
(x, y, z) => func([x, y, z])[0],
[min, max]
);
}
Insert cell
mesh = new Mesh(isoObject.positions, isoObject.cells)
Insert cell
ballMesh = {
let ball = subdivide(subdivide(cube));
return new Mesh(ball.positions, [...ball.triangleFaces()]);
}
Insert cell
isoSurfaceApp = {
const frag = `#version 300 es
precision highp float;
in vec3 vertex_normal;
out vec4 fragColor;
uniform vec4 color;
uniform vec3 light_dir;


void main () {
fragColor = vec4(color.xyz *(0.2+0.8*(max(0.,dot(vertex_normal,light_dir)))),1.0);
}`;

const vert = `#version 300 es

layout(location=0) in vec4 position;
layout(location=1) in vec3 normal;

out vec3 vertex_normal;

uniform mat4 modelview;
uniform mat4 projection;
uniform float scaleFactor;

void main () {
vec4 worldpos = modelview*(scaleFactor*position);
gl_Position = projection*worldpos;
vertex_normal = normalize((modelview*vec4(normal,0.0)).xyz);
}`;

const canvas = isoSurfaceCanvas;
const { width: w, height: h } = canvas;
const app = picogl
.createApp(canvas)
.clearColor(0.5, 0.5, 0.5, 1)
.enable(picogl.DEPTH_TEST);

function vtxArray(mesh) {
const positions = new Float32Array(mesh.positions.flat());
const normals = new Float32Array(mesh.vertexNormals.flat());
const indices = new Uint16Array(mesh.faces.flat());

const positionsBuffer = app.createVertexBuffer(picogl.FLOAT, 3, positions);
const normalsBuffer = app.createVertexBuffer(picogl.FLOAT, 3, normals);
const indexBuffer = app.createIndexBuffer(picogl.UNSIGNED_SHORT, indices);
return app
.createVertexArray()
.vertexAttributeBuffer(0, positionsBuffer)
.vertexAttributeBuffer(1, normalsBuffer)
.indexBuffer(indexBuffer);
}

const program = app.createProgram(vert, frag);
const drawCallSurface = app.createDrawCall(program, vtxArray(mesh));
const drawCallBall = app.createDrawCall(program, vtxArray(ballMesh));
return { app, drawCallSurface, drawCallBall };
}
Insert cell
renderIsoSurface = {
const canvas = isoSurfaceCanvas;
const { app, drawCallSurface, drawCallBall } = isoSurfaceApp;
const arcball = new Arcball(canvas);

const refresh = () => {
app.clear();
const modelview = mat4.mul(
[],
mat4.fromTranslation([], [0, 0, -5]),
arcball.transform
);

const projection = mat4.perspective(
[],
(45 * Math.PI) / 180,
canvas.width / canvas.height,
0.01,
100
);
drawCallSurface
.uniform("modelview", modelview)
.uniform("light_dir", vec3.normalize([], [1, 1, 1]))
.uniform("color", [1.0, 0.0, 0.0, 1.0])
.uniform("scaleFactor", 1.0)
.uniform("projection", projection)
.draw();
data.points.forEach((p, i) => {
drawCallBall
.uniform(
"modelview",
mat4.multiply(
[],
modelview,
mat4.multiply(
[],
mat4.fromTranslation([], p),
mat4.fromScaling([], [0.05, 0.05, 0.05])
)
)
)
.uniform("light_dir", vec3.normalize([], [1, 1, 1]))
.uniform("color", i < 8 ? [0, 0, 1, 1] : [1.0, 1.0, 0.0, 1.0])
.uniform("scaleFactor", 1.0)
.uniform("projection", projection)
.draw();
});
};
canvas.onmousedown = (e) =>
arcball.mousedown(e.offsetX, canvas.height - e.offsetY);
canvas.onmousemove = (e) => {
if (e.buttons) {
arcball.mousemove(e.offsetX, canvas.height - e.offsetY);
refresh();
}
};
canvas.onmouseup = (e) => arcball.mouseup();
refresh();
}
Insert cell
Insert cell
Insert cell
Insert cell
picogl = require("picogl@0.17.8/build/picogl.min.js")
Insert cell
glmatrix = require("https://bundle.run/gl-matrix@3.3.0")
Insert cell
mat4 = glmatrix.mat4
Insert cell
vec3 = glmatrix.vec3
Insert cell
vec2 = glmatrix.vec2
Insert cell
class Arcball {
constructor(canvas) {
this.canvas = canvas;
this.center = [canvas.width / 2, canvas.height / 2];
this.radius = (Math.min(canvas.width, canvas.height) / 2) * 0.9;
this.prev = [];
this.transform = mat4.create();
this.mouseIsDown = false;
}
ballPos(p) {
let u = vec2.sub([], p, this.center);
if (vec2.len(u) >= this.radius) return vec3.normalize([], [...u, 0]);
let z = Math.sqrt(this.radius ** 2 - u[0] ** 2 - u[1] ** 2);
return vec3.normalize([], [...u, z]);
}
mousedown(x, y) {
this.mouseIsDown = true;
this.prev = [x, y];
}
mousemove(x, y) {
if (!this.mouseIsDown) return this.transform;
let mouse = [x, y];
let u = this.ballPos(this.prev);
let v = this.ballPos(mouse);
this.prev = mouse;
let axis = vec3.cross([], u, v);
let ang = Math.asin(vec3.len(axis));
let rot = mat4.fromRotation([], ang, vec3.normalize([], axis));
[this.u, this.v, this.ang, this.axis] = [u, v, ang, axis];
if (rot) mat4.mul(this.transform, rot, this.transform);
return this.transform;
}
mouseup(x, y) {
this.mouseIsDown = false;
}
}
Insert cell
iso = require("https://bundle.run/isosurface@1.0.0")
Insert cell
import { Mesh, cube, subdivide } from "@esperanc/platonic-solids-playground"
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