collisionOverlay.ts
examples/lcc/collisionOverlay.ts · raw · open the example · all examples · site home
Exports: OverlaySettingsCollisionOverlay
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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
// The collision debug overlay: under the scene's `model` (so in the file's
// frame), a wireframe of every loaded collision chunk in a color of its own,
// each chunk's box (bright once loaded), optionally the BVH's boxes at one
// depth (leaves above it included) for the chunk nearest the camera, and the
// walker's capsule and last ground hit. Lines are drawn after the splats,
// without a depth test by default (an x-ray view).
import * as THREE from "three";
import { BVH_NODE_BYTES, isLeaf, rightChild, type CollisionChunk, type Vec3 } from "./collision.js";
import type { CollisionStream } from "./CollisionStream.js";
export interface OverlaySettings {
mesh: boolean;
boxes: boolean;
leaves: boolean;
// BVH depth of the boxes shown (0: the root)
leafDepth: number;
depthTest: boolean;
opacity: number;
}
const PALETTE = ["#ff595e", "#ffca3a", "#8ac926", "#1982c4", "#6a4c93", "#ff924c", "#52a675", "#c77dff", "#4cc9f0", "#f15bb5"];
// The 12 edges of each box as line segment pairs
function boxLines(boxes: number[][]): Float32Array {
const out = new Float32Array(boxes.length * 72);
let at = 0;
const corner = (b: number[], i: number) => [i & 1 ? b[3] : b[0], i & 2 ? b[4] : b[1], i & 4 ? b[5] : b[2]];
const edges = [[0, 1], [2, 3], [4, 5], [6, 7], [0, 2], [1, 3], [4, 6], [5, 7], [0, 4], [1, 5], [2, 6], [3, 7]];
for (const b of boxes) {
for (const [i, j] of edges) {
out.set(corner(b, i), at);
out.set(corner(b, j), at + 3);
at += 6;
}
}
return out;
}
// The BVH's boxes at `depth`, and leaves above it
function bvhBoxes(chunk: CollisionChunk, depth: number): number[][] {
const { bvh } = chunk;
const out: number[][] = [];
const stack: [number, number][] = [[0, 0]];
while (stack.length > 0 && out.length < 100_000) {
const [at, d] = stack.pop()!;
if (at + BVH_NODE_BYTES > bvh.byteLength) {
continue;
}
if (d === depth || isLeaf(bvh, at)) {
out.push(Array.from({ length: 6 }, (_, k) => bvh.getFloat32(at + k * 4, true)));
continue;
}
stack.push([rightChild(bvh, at), d + 1], [at + BVH_NODE_BYTES, d + 1]);
}
return out;
}
export class CollisionOverlay {
readonly group = new THREE.Group();
private readonly meshes = new Map<string, THREE.LineSegments>();
private readonly boxes = new THREE.LineSegments(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: "#ffffff", transparent: true }));
private readonly leaves = new THREE.LineSegments(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: "#00e5ff", transparent: true }));
private readonly capsule = new THREE.LineSegments(new THREE.BufferGeometry(), new THREE.LineBasicMaterial({ color: "#7CFC00", transparent: true }));
private stream?: CollisionStream;
private boxesKey = "";
private leavesKey = "";
constructor() {
this.group.name = "collision overlay";
this.group.add(this.boxes, this.leaves, this.capsule);
for (const lines of [this.boxes, this.leaves, this.capsule]) {
lines.renderOrder = 10;
lines.frustumCulled = false;
}
}
attach(stream: CollisionStream | undefined) {
this.clear();
this.stream = stream;
}
private clear() {
for (const lines of this.meshes.values()) {
lines.geometry.dispose();
(lines.material as THREE.Material).dispose();
this.group.remove(lines);
}
this.meshes.clear();
this.boxesKey = this.leavesKey = "";
this.setLines(this.boxes, new Float32Array(0));
this.setLines(this.leaves, new Float32Array(0));
this.setLines(this.capsule, new Float32Array(0));
}
private setLines(lines: THREE.LineSegments, positions: Float32Array) {
lines.geometry.dispose();
lines.geometry = new THREE.BufferGeometry();
lines.geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
}
// Once per frame: the eye (file frame) picks the chunk whose BVH boxes show;
// the walker's feet and capsule, when walking
update(settings: OverlaySettings, eye: Vec3, walker?: { feet: Vec3; up: Vec3; radius: number; height: number; hit?: Vec3 }) {
const stream = this.stream;
this.group.visible = stream !== undefined && (settings.mesh || settings.boxes || settings.leaves || walker !== undefined);
if (!stream || !this.group.visible) {
return;
}
const chunks = stream.set.chunks;
// Wireframes of the loaded chunks
for (const [id, lines] of this.meshes) {
if (!chunks.has(id)) {
lines.geometry.dispose();
(lines.material as THREE.Material).dispose();
this.group.remove(lines);
this.meshes.delete(id);
}
}
if (settings.mesh) {
stream.entries.forEach((entry, i) => {
const chunk = chunks.get(entry.id);
if (chunk && !this.meshes.has(entry.id)) {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(chunk.vertices, 3));
geometry.setIndex(new THREE.BufferAttribute(chunk.faces, 1));
const lines = new THREE.LineSegments(new THREE.WireframeGeometry(geometry), new THREE.LineBasicMaterial({ color: PALETTE[i % PALETTE.length], transparent: true }));
geometry.dispose();
lines.renderOrder = 10;
lines.frustumCulled = false;
this.meshes.set(entry.id, lines);
this.group.add(lines);
}
});
}
for (const lines of this.meshes.values()) {
lines.visible = settings.mesh;
}
// Chunk boxes, rebuilt when what is loaded changes
const key = settings.boxes ? stream.entries.map((entry) => entry.state).join() : "";
if (key !== this.boxesKey) {
this.boxesKey = key;
this.setLines(this.boxes, settings.boxes ? boxLines(stream.entries.map((entry) => Array.from(entry.box))) : new Float32Array(0));
}
this.boxes.visible = settings.boxes;
// BVH boxes of the nearest loaded chunk
let nearest: CollisionChunk | undefined;
let best = Infinity;
if (settings.leaves) {
for (const chunk of chunks.values()) {
const b = chunk.box;
const d = Math.hypot(Math.max(b[0] - eye.x, 0, eye.x - b[3]), Math.max(b[1] - eye.y, 0, eye.y - b[4]), Math.max(b[2] - eye.z, 0, eye.z - b[5]));
if (d < best) {
best = d;
nearest = chunk;
}
}
}
const leavesKey = nearest ? `${nearest.id}|${settings.leafDepth}` : "";
if (leavesKey !== this.leavesKey) {
this.leavesKey = leavesKey;
this.setLines(this.leaves, nearest ? boxLines(bvhBoxes(nearest, Math.max(0, Math.round(settings.leafDepth)))) : new Float32Array(0));
}
this.leaves.visible = settings.leaves;
// The walker: two rings and four lines of its capsule, a cross at the hit
if (walker) {
this.setLines(this.capsule, capsuleLines(walker));
}
this.capsule.visible = walker !== undefined;
for (const lines of [...this.meshes.values(), this.boxes, this.leaves, this.capsule]) {
const material = lines.material as THREE.LineBasicMaterial;
material.opacity = settings.opacity;
material.depthTest = settings.depthTest;
material.depthWrite = false;
}
}
dispose() {
this.clear();
}
}
function capsuleLines({ feet, up, radius, height, hit }: { feet: Vec3; up: Vec3; radius: number; height: number; hit?: Vec3 }): Float32Array {
const u = new THREE.Vector3(up.x, up.y, up.z).normalize();
const a = new THREE.Vector3(1, 0, 0);
if (Math.abs(a.dot(u)) > 0.9) {
a.set(0, 1, 0);
}
const s = a.clone().cross(u).normalize();
const t = u.clone().cross(s).normalize();
const base = new THREE.Vector3(feet.x, feet.y, feet.z);
const out: number[] = [];
const point = (h: number, angle: number, r = radius) => base.clone().addScaledVector(u, h).addScaledVector(s, Math.cos(angle) * r).addScaledVector(t, Math.sin(angle) * r);
const segments = 24;
for (const h of [radius, height - radius]) {
for (let i = 0; i < segments; i += 1) {
out.push(...point(h, (i / segments) * Math.PI * 2).toArray(), ...point(h, ((i + 1) / segments) * Math.PI * 2).toArray());
}
}
for (let i = 0; i < 4; i += 1) {
out.push(...point(radius, (i / 4) * Math.PI * 2).toArray(), ...point(height - radius, (i / 4) * Math.PI * 2).toArray());
}
if (hit) {
const c = new THREE.Vector3(hit.x, hit.y, hit.z);
const r = radius * 0.6;
out.push(...c.clone().addScaledVector(s, -r).toArray(), ...c.clone().addScaledVector(s, r).toArray());
out.push(...c.clone().addScaledVector(t, -r).toArray(), ...c.clone().addScaledVector(t, r).toArray());
}
return new Float32Array(out);
}