BoxLines.ts
examples/mpm/BoxLines.ts · raw · open the example · all examples · site home
Exports: BoxLines
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
// The simulation box as fat lines of a constant pixel width: the 12 edges
// and the 12 face midlines (together the three mid-plane rectangles). One
// unit cube, placed and scaled onto the walls every frame. The lines are at
// full opacity while the walls move (or `touch` is called), then ease down
// to `settings.idle`.
//
// Drawn in the same pass as the splats, after them (transparent,
// renderOrder 1, above the splat renderer's), blended and without depth
// writes: every line shows over the water, back edges included.
import * as THREE from "three";
import { Line2NodeMaterial } from "three/webgpu";
import { LineSegments2 } from "three/examples/jsm/lines/webgpu/LineSegments2.js";
import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
import { TSL } from "@sparkjsdev/radix";
const { abs, clamp, float, length, materialLineWidth, max, uniform, uv, vec2 } = TSL;
const EDGES = 12;
// A transparent Line2NodeMaterial composites itself over a copy of the
// opaque pass (which has no splats) instead of blending: build its shader as
// if it were opaque, and keep it transparent for the render list
class BlendedLineMaterial extends Line2NodeMaterial {
setupDiffuseColor(builder: Parameters<Line2NodeMaterial["setupDiffuseColor"]>[0]) {
this.transparent = false;
super.setupDiffuseColor(builder);
this.transparent = true;
}
}
// Segments along each axis a at the four (b, c) corners of the other two
// axes, then the four face midlines along a
function unitBoxSegments() {
const edges: number[] = [];
const midlines: number[] = [];
for (let a = 0; a < 3; a++) {
const b = (a + 1) % 3;
const c = (a + 2) % 3;
const segment = (list: number[], pb: number, pc: number) => {
const start = [0, 0, 0];
start[b] = pb;
start[c] = pc;
const end = [...start];
end[a] = 1;
list.push(...start, ...end);
};
for (const [pb, pc] of [[0, 0], [1, 0], [0, 1], [1, 1]]) {
segment(edges, pb, pc);
}
for (const [pb, pc] of [[0.5, 0], [0.5, 1], [0, 0.5], [1, 0.5]]) {
segment(midlines, pb, pc);
}
}
return [...edges, ...midlines];
}
export class BoxLines extends LineSegments2 {
/** Pixel width, the opacity when idle, seconds at full opacity after the last change, then seconds to fade */
readonly settings = { width: 2, idle: 0.15, hold: 0.3, fade: 1.0, midlines: true };
readonly opacity = uniform(1);
private lastActive = -Infinity;
private readonly size = new THREE.Vector3();
constructor(color: THREE.ColorRepresentation = 0xffffff) {
const geometry = new LineSegmentsGeometry().setPositions(unitBoxSegments());
// Normal blending spelled out as custom: the shader build would force
// alpha to 1 for NormalBlending while `transparent` is off
const material = new BlendedLineMaterial({
color,
transparent: true,
blending: THREE.CustomBlending,
blendSrc: THREE.SrcAlphaFactor,
blendDst: THREE.OneMinusSrcAlphaFactor,
depthWrite: false,
});
super(geometry, material);
this.renderOrder = 1;
// Without MSAA the material only cuts the round caps: fade the outer
// pixel of the line (drawn one pixel wider) by the distance from its axis
const { x, y } = uv();
const r = length(vec2(x, max(abs(y).sub(1), 0)));
material.opacityNode = this.opacity.mul(clamp(float(1).sub(r).mul(materialLineWidth).mul(0.5), 0, 1));
}
/** Show the lines at full opacity */
touch(now = performance.now()) {
this.lastActive = now;
}
/** Fit the walls and fade; a wall change shows the lines at full opacity */
update(walls: THREE.Box3, now = performance.now()) {
const { settings } = this;
walls.getSize(this.size);
if (!walls.min.equals(this.position) || !this.size.equals(this.scale)) {
this.touch(now);
}
this.position.copy(walls.min);
this.scale.copy(this.size);
this.material.linewidth = settings.width + 1;
this.geometry.instanceCount = settings.midlines ? 2 * EDGES : EDGES;
const idle = (now - this.lastActive) / 1000 - settings.hold;
const t = settings.fade > 0 ? THREE.MathUtils.smoothstep(idle, 0, settings.fade) : idle > 0 ? 1 : 0;
this.opacity.value = settings.idle + (1 - settings.idle) * (1 - t);
}
}