Compare commits
3 Commits
bee0c7f223
...
a2a491bd5c
| Author | SHA1 | Date | |
|---|---|---|---|
| a2a491bd5c | |||
| da7d66e1fd | |||
| 5faf4b4197 |
@@ -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;
|
||||||
|
|
||||||
acc.push({
|
const isLeaf = node.subTrees.length === 0;
|
||||||
box: node.box,
|
const passesDepth = depth >= options.minDepth;
|
||||||
depth,
|
const passesLeafFilter = !options.leavesOnly || isLeaf;
|
||||||
triangleCount: node.triangles.length,
|
const hasTriangles = node.triangles.length > 0;
|
||||||
});
|
|
||||||
|
if (passesDepth && passesLeafFilter && hasTriangles) {
|
||||||
|
acc.push({
|
||||||
|
box: node.box,
|
||||||
|
depth,
|
||||||
|
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ export function TalkieDialogueOverlay(): React.JSX.Element | null {
|
|||||||
mainState !== "intro" || TALKIE_REVEAL_STEPS.has(introStep);
|
mainState !== "intro" || TALKIE_REVEAL_STEPS.has(introStep);
|
||||||
const isNarratorDialogue = activeSubtitle?.speaker === "Narrateur";
|
const isNarratorDialogue = activeSubtitle?.speaker === "Narrateur";
|
||||||
|
|
||||||
if (!isAfterReveal || !isNarratorDialogue) return null;
|
if (!isAfterReveal) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside
|
<aside
|
||||||
className="talkie-dialogue-overlay talkie-dialogue-overlay--raised"
|
className={`talkie-dialogue-overlay${isNarratorDialogue ? " talkie-dialogue-overlay--raised" : ""}`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<TalkieSignalLines />
|
{isNarratorDialogue ? <TalkieSignalLines /> : null}
|
||||||
<div className="talkie-dialogue-overlay__model-frame">
|
<div className="talkie-dialogue-overlay__model-frame">
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [0, 0, 4.2], zoom: 78 }}
|
camera={{ position: [0, 0, 4.2], zoom: 78 }}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
+54
-2
@@ -1,10 +1,12 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useFrame, useThree } from "@react-three/fiber";
|
import { useFrame, useThree } from "@react-three/fiber";
|
||||||
import {
|
import {
|
||||||
|
Mesh,
|
||||||
PCFShadowMap,
|
PCFShadowMap,
|
||||||
type AmbientLight,
|
type AmbientLight,
|
||||||
type DirectionalLight,
|
type DirectionalLight,
|
||||||
type Object3D,
|
type Object3D,
|
||||||
|
type Scene,
|
||||||
type WebGLRenderer,
|
type WebGLRenderer,
|
||||||
} from "three";
|
} from "three";
|
||||||
import {
|
import {
|
||||||
@@ -51,12 +53,33 @@ function configureSunShadow(sun: DirectionalLight, sunTarget: Object3D): void {
|
|||||||
sun.shadow.camera.updateProjectionMatrix();
|
sun.shadow.camera.updateProjectionMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [diag] temporary helper: count shadow-casting/receiving meshes in the scene
|
||||||
|
function snapshotShadowMeshes(scene: Scene): {
|
||||||
|
meshCount: number;
|
||||||
|
castShadowCount: number;
|
||||||
|
receiveShadowCount: number;
|
||||||
|
} {
|
||||||
|
let meshCount = 0;
|
||||||
|
let castShadowCount = 0;
|
||||||
|
let receiveShadowCount = 0;
|
||||||
|
scene.traverse((obj) => {
|
||||||
|
if (obj instanceof Mesh) {
|
||||||
|
meshCount += 1;
|
||||||
|
if (obj.castShadow) castShadowCount += 1;
|
||||||
|
if (obj.receiveShadow) receiveShadowCount += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { meshCount, castShadowCount, receiveShadowCount };
|
||||||
|
}
|
||||||
|
|
||||||
export function Lighting(): React.JSX.Element {
|
export function Lighting(): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const gl = useThree((state) => state.gl);
|
const gl = useThree((state) => state.gl);
|
||||||
|
const scene = useThree((state) => state.scene);
|
||||||
const ambient = useRef<AmbientLight>(null);
|
const ambient = useRef<AmbientLight>(null);
|
||||||
const sun = useRef<DirectionalLight>(null);
|
const sun = useRef<DirectionalLight>(null);
|
||||||
const sunTarget = useRef<Object3D>(null);
|
const sunTarget = useRef<Object3D>(null);
|
||||||
|
const lastDiagAtRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!sun.current || !sunTarget.current) return;
|
if (!sun.current || !sunTarget.current) return;
|
||||||
@@ -64,7 +87,18 @@ export function Lighting(): React.JSX.Element {
|
|||||||
configureSunShadow(sun.current, sunTarget.current);
|
configureSunShadow(sun.current, sunTarget.current);
|
||||||
configureRendererShadows(gl);
|
configureRendererShadows(gl);
|
||||||
sun.current.shadow.needsUpdate = true;
|
sun.current.shadow.needsUpdate = true;
|
||||||
}, [gl]);
|
|
||||||
|
// [diag] one-shot scene snapshot to count shadow casters/receivers
|
||||||
|
const counts = snapshotShadowMeshes(scene);
|
||||||
|
console.log("[shadow:mount]", {
|
||||||
|
shadowMapEnabled: gl.shadowMap.enabled,
|
||||||
|
shadowMapType: gl.shadowMap.type,
|
||||||
|
shadowAutoUpdate: gl.shadowMap.autoUpdate,
|
||||||
|
sunCastShadow: sun.current.castShadow,
|
||||||
|
hasShadowMap: !!sun.current.shadow.map,
|
||||||
|
...counts,
|
||||||
|
});
|
||||||
|
}, [gl, scene]);
|
||||||
|
|
||||||
useDebugFolder("Lighting", (folder) => {
|
useDebugFolder("Lighting", (folder) => {
|
||||||
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");
|
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");
|
||||||
@@ -98,7 +132,7 @@ export function Lighting(): React.JSX.Element {
|
|||||||
.name("Sun Z");
|
.name("Sun Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
useFrame(() => {
|
useFrame(({ clock }) => {
|
||||||
if (ambient.current) {
|
if (ambient.current) {
|
||||||
ambient.current.color.set(LIGHTING_STATE.ambientColor);
|
ambient.current.color.set(LIGHTING_STATE.ambientColor);
|
||||||
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
|
||||||
@@ -117,6 +151,24 @@ export function Lighting(): React.JSX.Element {
|
|||||||
sun.current.updateMatrixWorld();
|
sun.current.updateMatrixWorld();
|
||||||
sun.current.shadow.needsUpdate = true;
|
sun.current.shadow.needsUpdate = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [diag] periodic shadow pipeline check (every 2s)
|
||||||
|
const now = clock.getElapsedTime();
|
||||||
|
if (now - lastDiagAtRef.current > 2 && sun.current) {
|
||||||
|
lastDiagAtRef.current = now;
|
||||||
|
console.log("[shadow:tick]", {
|
||||||
|
shadowMapEnabled: gl.shadowMap.enabled,
|
||||||
|
shadowAutoUpdate: gl.shadowMap.autoUpdate,
|
||||||
|
sunCastShadow: sun.current.castShadow,
|
||||||
|
sunIntensity: sun.current.intensity,
|
||||||
|
hasShadowMapTexture: !!sun.current.shadow.map?.texture,
|
||||||
|
sunPos: sun.current.position.toArray().map((n) => Number(n.toFixed(2))),
|
||||||
|
targetPos: sunTarget.current?.position
|
||||||
|
.toArray()
|
||||||
|
.map((n) => Number(n.toFixed(2))),
|
||||||
|
renderCalls: gl.info.render.calls,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user