diff --git a/src/components/debug/DebugOctreeVisualization.tsx b/src/components/debug/DebugOctreeVisualization.tsx new file mode 100644 index 0000000..9d37403 --- /dev/null +++ b/src/components/debug/DebugOctreeVisualization.tsx @@ -0,0 +1,137 @@ +import { useMemo } from "react"; +import { Box3, BufferAttribute, BufferGeometry, Color } from "three"; +import type { Octree } from "three-stdlib"; +import { useDebugVisualsStore } from "@/managers/stores/useDebugVisualsStore"; + +interface DebugOctreeVisualizationProps { + octree: Octree | null; +} + +interface OctreeNodeBox { + box: Box3; + depth: number; + triangleCount: number; +} + +const BOX_VERTEX_INDEX_PAIRS: ReadonlyArray = [ + [0, 1], + [1, 3], + [3, 2], + [2, 0], + [4, 5], + [5, 7], + [7, 6], + [6, 4], + [0, 4], + [1, 5], + [2, 6], + [3, 7], +]; + +function collectOctreeBoxes( + node: Octree, + maxDepth: number, + depth = 0, + acc: OctreeNodeBox[] = [], +): OctreeNodeBox[] { + if (depth > maxDepth) return acc; + + acc.push({ + box: node.box, + depth, + triangleCount: node.triangles.length, + }); + + for (const sub of node.subTrees) { + collectOctreeBoxes(sub, maxDepth, depth + 1, acc); + } + + return acc; +} + +function buildOctreeLineGeometry( + nodes: readonly OctreeNodeBox[], +): BufferGeometry { + const positionsBuffer = new Float32Array( + 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 }, () => [ + 0, 0, 0, + ]); + + let positionsOffset = 0; + let colorsOffset = 0; + const colorHelper = new Color(); + + for (const node of nodes) { + const { min, max } = node.box; + + corners[0] = [min.x, min.y, min.z]; + corners[1] = [max.x, min.y, min.z]; + corners[2] = [min.x, max.y, min.z]; + corners[3] = [max.x, max.y, min.z]; + corners[4] = [min.x, min.y, max.z]; + corners[5] = [max.x, min.y, max.z]; + corners[6] = [min.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) { + const ca = corners[a]!; + const cb = corners[b]!; + positionsBuffer[positionsOffset++] = ca[0]; + positionsBuffer[positionsOffset++] = ca[1]; + positionsBuffer[positionsOffset++] = ca[2]; + positionsBuffer[positionsOffset++] = cb[0]; + positionsBuffer[positionsOffset++] = cb[1]; + 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(); + geometry.setAttribute("position", new BufferAttribute(positionsBuffer, 3)); + geometry.setAttribute("color", new BufferAttribute(colorsBuffer, 3)); + return geometry; +} + +export function DebugOctreeVisualization({ + octree, +}: DebugOctreeVisualizationProps): React.JSX.Element | null { + const showOctree = useDebugVisualsStore((state) => state.showOctree); + const maxDepth = useDebugVisualsStore((state) => state.octreeMaxDepth); + + const geometry = useMemo(() => { + if (!octree || !showOctree) return null; + const boxes = collectOctreeBoxes(octree, maxDepth); + if (boxes.length === 0) return null; + return buildOctreeLineGeometry(boxes); + }, [maxDepth, octree, showOctree]); + + if (!geometry) return null; + + return ( + + + + + ); +} diff --git a/src/components/debug/DebugPlayerModel.tsx b/src/components/debug/DebugPlayerModel.tsx new file mode 100644 index 0000000..fd4d6a3 --- /dev/null +++ b/src/components/debug/DebugPlayerModel.tsx @@ -0,0 +1,58 @@ +import { useEffect, useMemo, useRef } from "react"; +import * as THREE from "three"; +import { useFrame } from "@react-three/fiber"; +import { useGLTF } from "@react-three/drei"; + +const MODEL_PATH = "/models/persoprincipal/model.gltf"; +// Offset expressed in the camera's local space: +// - x: horizontal (0 = centered) +// - y: vertical relative to camera eye (negative = below) +// - z: forward (negative = in front of the camera) +const LOCAL_OFFSET = new THREE.Vector3(0, -1, -2.5); + +const eulerHelper = new THREE.Euler(); + +export function DebugPlayerModel(): React.JSX.Element { + const groupRef = useRef(null); + const { scene } = useGLTF(MODEL_PATH); + + const model = useMemo(() => { + const cloned = scene.clone(true); + cloned.traverse((child) => { + if (child instanceof THREE.Mesh) { + child.castShadow = true; + child.receiveShadow = true; + child.frustumCulled = false; + } + }); + return cloned; + }, [scene]); + + useEffect( + () => () => { + model.clear(); + }, + [model], + ); + + useFrame(({ camera }) => { + const group = groupRef.current; + if (!group) return; + + // Place the model in front of the camera using its local space so it stays + // visible regardless of the camera pitch (top-down ebike view, etc.). + group.position.copy(LOCAL_OFFSET).applyMatrix4(camera.matrixWorld); + + // Keep the model upright and aligned with the camera yaw only. + eulerHelper.setFromQuaternion(camera.quaternion, "YXZ"); + group.rotation.set(0, eulerHelper.y, 0); + }); + + return ( + + + + ); +} + +useGLTF.preload(MODEL_PATH); diff --git a/src/hooks/debug/useDebugVisualsDebug.ts b/src/hooks/debug/useDebugVisualsDebug.ts new file mode 100644 index 0000000..ba7af7f --- /dev/null +++ b/src/hooks/debug/useDebugVisualsDebug.ts @@ -0,0 +1,33 @@ +import { useDebugFolder } from "@/hooks/debug/useDebugFolder"; +import { useDebugVisualsStore } from "@/managers/stores/useDebugVisualsStore"; + +export function useDebugVisualsDebug(): void { + useDebugFolder("Debug", (folder) => { + const controls = { + showPlayerModel: useDebugVisualsStore.getState().showPlayerModel, + showOctree: useDebugVisualsStore.getState().showOctree, + octreeMaxDepth: useDebugVisualsStore.getState().octreeMaxDepth, + }; + + folder + .add(controls, "showPlayerModel") + .name("Show Player Model") + .onChange((value: boolean) => { + useDebugVisualsStore.getState().setShowPlayerModel(value); + }); + + folder + .add(controls, "showOctree") + .name("Show Octree") + .onChange((value: boolean) => { + useDebugVisualsStore.getState().setShowOctree(value); + }); + + folder + .add(controls, "octreeMaxDepth", 0, 10, 1) + .name("Octree Max Depth") + .onChange((value: number) => { + useDebugVisualsStore.getState().setOctreeMaxDepth(value); + }); + }); +} diff --git a/src/managers/stores/useDebugVisualsStore.ts b/src/managers/stores/useDebugVisualsStore.ts new file mode 100644 index 0000000..86c09ec --- /dev/null +++ b/src/managers/stores/useDebugVisualsStore.ts @@ -0,0 +1,19 @@ +import { create } from "zustand"; + +interface DebugVisualsStore { + showPlayerModel: boolean; + setShowPlayerModel: (value: boolean) => void; + showOctree: boolean; + setShowOctree: (value: boolean) => void; + octreeMaxDepth: number; + setOctreeMaxDepth: (value: number) => void; +} + +export const useDebugVisualsStore = create((set) => ({ + showPlayerModel: false, + setShowPlayerModel: (showPlayerModel) => set({ showPlayerModel }), + showOctree: false, + setShowOctree: (showOctree) => set({ showOctree }), + octreeMaxDepth: 6, + setOctreeMaxDepth: (octreeMaxDepth) => set({ octreeMaxDepth }), +}));