feat(debug): add filters to octree visualization

Default visualization was unreadable because every node from depth 0 to
maxDepth was rendered with rainbow-coloured edges. Add three filters
exposed in the Debug folder:

- Octree Leaves Only (default true): skip internal nodes
- Octree Min Depth (default 4): hide the largest enclosing boxes
- Octree Opacity (default 0.35): tone down line density

Also skip nodes without triangles, drop the per-depth HSL palette in
favour of a uniform cyan, and bump default Octree Max Depth to 8.
This commit is contained in:
Tom Boullay
2026-06-01 16:50:08 +02:00
parent 5faf4b4197
commit da7d66e1fd
3 changed files with 76 additions and 33 deletions
@@ -1,5 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { Box3, BufferAttribute, BufferGeometry, Color } from "three"; import { Box3, BufferAttribute, BufferGeometry } from "three";
import type { Octree } from "three-stdlib"; import type { Octree } from "three-stdlib";
import { useDebugVisualsStore } from "@/managers/stores/useDebugVisualsStore"; import { useDebugVisualsStore } from "@/managers/stores/useDebugVisualsStore";
@@ -11,6 +11,13 @@ interface OctreeNodeBox {
box: Box3; box: Box3;
depth: number; depth: number;
triangleCount: number; triangleCount: number;
isLeaf: boolean;
}
interface CollectOptions {
minDepth: number;
maxDepth: number;
leavesOnly: boolean;
} }
const BOX_VERTEX_INDEX_PAIRS: ReadonlyArray<readonly [number, number]> = [ const BOX_VERTEX_INDEX_PAIRS: ReadonlyArray<readonly [number, number]> = [
@@ -30,20 +37,28 @@ const BOX_VERTEX_INDEX_PAIRS: ReadonlyArray<readonly [number, number]> = [
function collectOctreeBoxes( function collectOctreeBoxes(
node: Octree, node: Octree,
maxDepth: number, options: CollectOptions,
depth = 0, depth = 0,
acc: OctreeNodeBox[] = [], acc: OctreeNodeBox[] = [],
): OctreeNodeBox[] { ): OctreeNodeBox[] {
if (depth > maxDepth) return acc; if (depth > options.maxDepth) return acc;
const isLeaf = node.subTrees.length === 0;
const passesDepth = depth >= options.minDepth;
const passesLeafFilter = !options.leavesOnly || isLeaf;
const hasTriangles = node.triangles.length > 0;
if (passesDepth && passesLeafFilter && hasTriangles) {
acc.push({ acc.push({
box: node.box, box: node.box,
depth, depth,
triangleCount: node.triangles.length, triangleCount: node.triangles.length,
isLeaf,
}); });
}
for (const sub of node.subTrees) { for (const sub of node.subTrees) {
collectOctreeBoxes(sub, maxDepth, depth + 1, acc); collectOctreeBoxes(sub, options, depth + 1, acc);
} }
return acc; return acc;
@@ -55,17 +70,12 @@ function buildOctreeLineGeometry(
const positionsBuffer = new Float32Array( const positionsBuffer = new Float32Array(
nodes.length * BOX_VERTEX_INDEX_PAIRS.length * 2 * 3, nodes.length * BOX_VERTEX_INDEX_PAIRS.length * 2 * 3,
); );
const colorsBuffer = new Float32Array(
nodes.length * BOX_VERTEX_INDEX_PAIRS.length * 2 * 3,
);
const corners: [number, number, number][] = Array.from({ length: 8 }, () => [ const corners: [number, number, number][] = Array.from({ length: 8 }, () => [
0, 0, 0, 0, 0, 0,
]); ]);
let positionsOffset = 0; let positionsOffset = 0;
let colorsOffset = 0;
const colorHelper = new Color();
for (const node of nodes) { for (const node of nodes) {
const { min, max } = node.box; const { min, max } = node.box;
@@ -79,9 +89,6 @@ function buildOctreeLineGeometry(
corners[6] = [min.x, max.y, max.z]; corners[6] = [min.x, max.y, max.z];
corners[7] = [max.x, max.y, max.z]; corners[7] = [max.x, max.y, max.z];
const hue = (node.depth * 0.13) % 1;
colorHelper.setHSL(hue, 0.85, 0.55);
for (const [a, b] of BOX_VERTEX_INDEX_PAIRS) { for (const [a, b] of BOX_VERTEX_INDEX_PAIRS) {
const ca = corners[a]!; const ca = corners[a]!;
const cb = corners[b]!; const cb = corners[b]!;
@@ -91,19 +98,11 @@ function buildOctreeLineGeometry(
positionsBuffer[positionsOffset++] = cb[0]; positionsBuffer[positionsOffset++] = cb[0];
positionsBuffer[positionsOffset++] = cb[1]; positionsBuffer[positionsOffset++] = cb[1];
positionsBuffer[positionsOffset++] = cb[2]; positionsBuffer[positionsOffset++] = cb[2];
colorsBuffer[colorsOffset++] = colorHelper.r;
colorsBuffer[colorsOffset++] = colorHelper.g;
colorsBuffer[colorsOffset++] = colorHelper.b;
colorsBuffer[colorsOffset++] = colorHelper.r;
colorsBuffer[colorsOffset++] = colorHelper.g;
colorsBuffer[colorsOffset++] = colorHelper.b;
} }
} }
const geometry = new BufferGeometry(); const geometry = new BufferGeometry();
geometry.setAttribute("position", new BufferAttribute(positionsBuffer, 3)); geometry.setAttribute("position", new BufferAttribute(positionsBuffer, 3));
geometry.setAttribute("color", new BufferAttribute(colorsBuffer, 3));
return geometry; return geometry;
} }
@@ -111,14 +110,21 @@ export function DebugOctreeVisualization({
octree, octree,
}: DebugOctreeVisualizationProps): React.JSX.Element | null { }: DebugOctreeVisualizationProps): React.JSX.Element | null {
const showOctree = useDebugVisualsStore((state) => state.showOctree); const showOctree = useDebugVisualsStore((state) => state.showOctree);
const minDepth = useDebugVisualsStore((state) => state.octreeMinDepth);
const maxDepth = useDebugVisualsStore((state) => state.octreeMaxDepth); const maxDepth = useDebugVisualsStore((state) => state.octreeMaxDepth);
const leavesOnly = useDebugVisualsStore((state) => state.octreeLeavesOnly);
const opacity = useDebugVisualsStore((state) => state.octreeOpacity);
const geometry = useMemo(() => { const geometry = useMemo(() => {
if (!octree || !showOctree) return null; if (!octree || !showOctree) return null;
const boxes = collectOctreeBoxes(octree, maxDepth); const boxes = collectOctreeBoxes(octree, {
minDepth,
maxDepth,
leavesOnly,
});
if (boxes.length === 0) return null; if (boxes.length === 0) return null;
return buildOctreeLineGeometry(boxes); return buildOctreeLineGeometry(boxes);
}, [maxDepth, octree, showOctree]); }, [leavesOnly, maxDepth, minDepth, octree, showOctree]);
if (!geometry) return null; if (!geometry) return null;
@@ -126,11 +132,11 @@ export function DebugOctreeVisualization({
<lineSegments frustumCulled={false} renderOrder={999}> <lineSegments frustumCulled={false} renderOrder={999}>
<primitive object={geometry} attach="geometry" /> <primitive object={geometry} attach="geometry" />
<lineBasicMaterial <lineBasicMaterial
vertexColors color="#22d3ee"
depthTest={false} depthTest={false}
depthWrite={false} depthWrite={false}
transparent transparent
opacity={0.85} opacity={opacity}
/> />
</lineSegments> </lineSegments>
); );
+28 -3
View File
@@ -3,10 +3,14 @@ import { useDebugVisualsStore } from "@/managers/stores/useDebugVisualsStore";
export function useDebugVisualsDebug(): void { export function useDebugVisualsDebug(): void {
useDebugFolder("Debug", (folder) => { useDebugFolder("Debug", (folder) => {
const state = useDebugVisualsStore.getState();
const controls = { const controls = {
showPlayerModel: useDebugVisualsStore.getState().showPlayerModel, showPlayerModel: state.showPlayerModel,
showOctree: useDebugVisualsStore.getState().showOctree, showOctree: state.showOctree,
octreeMaxDepth: useDebugVisualsStore.getState().octreeMaxDepth, octreeMinDepth: state.octreeMinDepth,
octreeMaxDepth: state.octreeMaxDepth,
octreeLeavesOnly: state.octreeLeavesOnly,
octreeOpacity: state.octreeOpacity,
}; };
folder folder
@@ -23,11 +27,32 @@ export function useDebugVisualsDebug(): void {
useDebugVisualsStore.getState().setShowOctree(value); useDebugVisualsStore.getState().setShowOctree(value);
}); });
folder
.add(controls, "octreeLeavesOnly")
.name("Octree Leaves Only")
.onChange((value: boolean) => {
useDebugVisualsStore.getState().setOctreeLeavesOnly(value);
});
folder
.add(controls, "octreeMinDepth", 0, 10, 1)
.name("Octree Min Depth")
.onChange((value: number) => {
useDebugVisualsStore.getState().setOctreeMinDepth(value);
});
folder folder
.add(controls, "octreeMaxDepth", 0, 10, 1) .add(controls, "octreeMaxDepth", 0, 10, 1)
.name("Octree Max Depth") .name("Octree Max Depth")
.onChange((value: number) => { .onChange((value: number) => {
useDebugVisualsStore.getState().setOctreeMaxDepth(value); useDebugVisualsStore.getState().setOctreeMaxDepth(value);
}); });
folder
.add(controls, "octreeOpacity", 0.05, 1, 0.05)
.name("Octree Opacity")
.onChange((value: number) => {
useDebugVisualsStore.getState().setOctreeOpacity(value);
});
}); });
} }
+13 -1
View File
@@ -7,6 +7,12 @@ interface DebugVisualsStore {
setShowOctree: (value: boolean) => void; setShowOctree: (value: boolean) => void;
octreeMaxDepth: number; octreeMaxDepth: number;
setOctreeMaxDepth: (value: number) => void; setOctreeMaxDepth: (value: number) => void;
octreeMinDepth: number;
setOctreeMinDepth: (value: number) => void;
octreeLeavesOnly: boolean;
setOctreeLeavesOnly: (value: boolean) => void;
octreeOpacity: number;
setOctreeOpacity: (value: number) => void;
} }
export const useDebugVisualsStore = create<DebugVisualsStore>((set) => ({ export const useDebugVisualsStore = create<DebugVisualsStore>((set) => ({
@@ -14,6 +20,12 @@ export const useDebugVisualsStore = create<DebugVisualsStore>((set) => ({
setShowPlayerModel: (showPlayerModel) => set({ showPlayerModel }), setShowPlayerModel: (showPlayerModel) => set({ showPlayerModel }),
showOctree: false, showOctree: false,
setShowOctree: (showOctree) => set({ showOctree }), setShowOctree: (showOctree) => set({ showOctree }),
octreeMaxDepth: 6, octreeMaxDepth: 8,
setOctreeMaxDepth: (octreeMaxDepth) => set({ octreeMaxDepth }), setOctreeMaxDepth: (octreeMaxDepth) => set({ octreeMaxDepth }),
octreeMinDepth: 4,
setOctreeMinDepth: (octreeMinDepth) => set({ octreeMinDepth }),
octreeLeavesOnly: true,
setOctreeLeavesOnly: (octreeLeavesOnly) => set({ octreeLeavesOnly }),
octreeOpacity: 0.35,
setOctreeOpacity: (octreeOpacity) => set({ octreeOpacity }),
})); }));