Feat/polish-mission1 #12
@@ -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<readonly [number, number]> = [
|
||||||
|
[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 (
|
||||||
|
<lineSegments frustumCulled={false} renderOrder={999}>
|
||||||
|
<primitive object={geometry} attach="geometry" />
|
||||||
|
<lineBasicMaterial
|
||||||
|
vertexColors
|
||||||
|
depthTest={false}
|
||||||
|
depthWrite={false}
|
||||||
|
transparent
|
||||||
|
opacity={0.85}
|
||||||
|
/>
|
||||||
|
</lineSegments>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<THREE.Group>(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 (
|
||||||
|
<group ref={groupRef} frustumCulled={false}>
|
||||||
|
<primitive object={model} />
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
useGLTF.preload(MODEL_PATH);
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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<DebugVisualsStore>((set) => ({
|
||||||
|
showPlayerModel: false,
|
||||||
|
setShowPlayerModel: (showPlayerModel) => set({ showPlayerModel }),
|
||||||
|
showOctree: false,
|
||||||
|
setShowOctree: (showOctree) => set({ showOctree }),
|
||||||
|
octreeMaxDepth: 6,
|
||||||
|
setOctreeMaxDepth: (octreeMaxDepth) => set({ octreeMaxDepth }),
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user