fix(world): reallocate shadow map after Suspense + clear LaFabrik doorway
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled
Shadows occasionally failed to render on initial load and the Fabrik doorway sometimes blocked the player. Both issues are tracked down to geometry that mounts after Lighting: - Shadows: GLTFs and the merged static map mount imperatively after Lighting, so materials get compiled against a renderer state that pre-dates the final scene and bake a 'no shadow map' permutation, silently dropping shadows. A WebGL context-restore cycle fixes it, but is too invasive. New 'useShadowMapWarmup' hook replays it cheaply: once the scene mesh count has been stable for ~1s, it disposes the directional shadow map (three.js reallocates it on the next render) and marks every material 'needsUpdate' so shaders rebind to the freshly created shadow sampler. - Doorway: the door slab + its Solidify-modifier frame (children of the 'Thicken' parent in the LaFabrik GLTF) sat inside the doorway AABB and prevented the player from walking through. Stripped from the collision octree alongside the existing 'porte' slab; visual rendering is unaffected. Also: extract sun-relative-to-camera placement into a small helper, remove the temporary diagnostic logs, and document the shadow warmup in three-debugging.md.
This commit is contained in:
@@ -44,30 +44,39 @@ through opaque geometry.
|
|||||||
## Shadow rendering intermittence
|
## Shadow rendering intermittence
|
||||||
|
|
||||||
Shadows occasionally failed to render on initial load and could disappear
|
Shadows occasionally failed to render on initial load and could disappear
|
||||||
mid-session even though the `Lighting` configuration ran to completion.
|
mid-session even though the `Lighting` configuration ran to completion. The
|
||||||
|
fix has two layers:
|
||||||
|
|
||||||
Root cause: the sun follows the camera (its world matrix is dirty every frame
|
### Per-frame refresh (steady state)
|
||||||
via `updateMatrixWorld()` inside `Lighting.useFrame`). With `shadow.autoUpdate`
|
|
||||||
alone, three.js can skip the shadow map re-render on a frame where the matrix
|
|
||||||
update has happened but the renderer's internal dirty tracking does not pick
|
|
||||||
it up, leaving the shadow map stale or unrendered.
|
|
||||||
|
|
||||||
Fix in `src/world/Lighting.tsx`: explicit `sun.shadow.needsUpdate = true` in
|
The sun follows the camera, so its world matrix is dirty every frame. With
|
||||||
two places, restoring the belt-and-suspenders pattern from `develop`:
|
`shadow.autoUpdate` alone, three.js can skip the shadow map re-render on a
|
||||||
|
frame where the matrix update has happened but the renderer's internal dirty
|
||||||
|
tracking does not pick it up. To prevent that, `Lighting.useFrame` sets
|
||||||
|
`sun.shadow.needsUpdate = true` after the per-frame matrix updates. Shadow
|
||||||
|
config is centralized in `src/data/world/lightingConfig.ts` (`bias=0`,
|
||||||
|
`normalBias=0`, `cameraSize=95`).
|
||||||
|
|
||||||
- After `configureSunShadow(...)` in the mount `useEffect`.
|
### Mount-time shadow map reallocation (`useShadowMapWarmup`)
|
||||||
- At the end of the `useFrame` block, right after `sun.updateMatrixWorld()`.
|
|
||||||
|
|
||||||
Mitigations also in place:
|
The merged static map and other GLTFs mount imperatively after `Lighting`,
|
||||||
|
so the shadow render target ends up linked to a renderer state that pre-dates
|
||||||
|
the final scene. Materials compiled at that point bake a "no shadow map"
|
||||||
|
permutation into their shader program and silently fail to render shadows
|
||||||
|
until a WebGL context-restore cycle (the kind triggered by Chrome DevTools
|
||||||
|
in `?debug` runs) reallocates everything.
|
||||||
|
|
||||||
- Shadow config centralized in `src/data/world/lightingConfig.ts`
|
`src/hooks/three/useShadowMapWarmup.ts` replays that cycle programmatically
|
||||||
(`bias=0`, `normalBias=0`, `cameraSize=95`).
|
without the cost of a full context loss. It runs a `useFrame` watchdog that
|
||||||
- Late-suspension Suspense boundaries in `World.tsx` to prevent global scene
|
samples the scene mesh count every 6 frames; once the count has been stable
|
||||||
remounts that would re-run shadow setup mid-load.
|
for ~1 s (or after a 5 s safety cap), it:
|
||||||
- `gl.shadowMap.needsUpdate = true` on `onCreated` and on
|
|
||||||
`webglcontextrestored` in `src/pages/page.tsx`.
|
|
||||||
|
|
||||||
If the issue reproduces, capture `[diag]`-style logs from `useOctreeGraphNode`,
|
1. Disposes the directional light shadow map and nulls it. three.js
|
||||||
`Lighting`, and `GameMapCollision` to confirm there is no extra configuration
|
reallocates the render target on the next render at the configured
|
||||||
pass (which would indicate a remaining suspending hook outside the existing
|
`mapSize`.
|
||||||
Suspense boundaries).
|
2. Marks every material's `needsUpdate = true`, forcing a shader recompile
|
||||||
|
that rebinds every program to the freshly created shadow sampler.
|
||||||
|
3. Forces a single shadow pass and invalidates the renderer.
|
||||||
|
|
||||||
|
The watchdog runs once per mount and adds a single traversal every 6 frames
|
||||||
|
during the warmup window, after which it self-terminates.
|
||||||
|
|||||||
@@ -1,29 +1,9 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { RefObject } from "react";
|
import type { RefObject } from "react";
|
||||||
import { Mesh, type Object3D } from "three";
|
import { type Object3D } from "three";
|
||||||
import { Octree } from "three-stdlib";
|
import { Octree } from "three-stdlib";
|
||||||
import type { OctreeReadyHandler } from "@/types/three/three";
|
import type { OctreeReadyHandler } from "@/types/three/three";
|
||||||
|
|
||||||
// [diag] temporary — count meshes/triangles captured in the octree graph node
|
|
||||||
function snapshotGraphNode(node: Object3D): {
|
|
||||||
meshCount: number;
|
|
||||||
triCount: number;
|
|
||||||
} {
|
|
||||||
let meshCount = 0;
|
|
||||||
let triCount = 0;
|
|
||||||
node.traverse((obj) => {
|
|
||||||
if (obj instanceof Mesh) {
|
|
||||||
meshCount += 1;
|
|
||||||
const geom = obj.geometry;
|
|
||||||
const idx = geom.index;
|
|
||||||
triCount += idx
|
|
||||||
? idx.count / 3
|
|
||||||
: (geom.attributes.position?.count ?? 0) / 3;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { meshCount, triCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useOctreeGraphNode(
|
export function useOctreeGraphNode(
|
||||||
graphNodeRef: RefObject<Object3D | null>,
|
graphNodeRef: RefObject<Object3D | null>,
|
||||||
onOctreeReady: OctreeReadyHandler,
|
onOctreeReady: OctreeReadyHandler,
|
||||||
@@ -48,15 +28,6 @@ export function useOctreeGraphNode(
|
|||||||
const octree = new Octree();
|
const octree = new Octree();
|
||||||
octree.fromGraphNode(graphNode);
|
octree.fromGraphNode(graphNode);
|
||||||
|
|
||||||
// [diag] temporary — log octree contents to detect partial builds
|
|
||||||
const snapshot = snapshotGraphNode(graphNode);
|
|
||||||
console.log("[octree:build]", {
|
|
||||||
rebuildKey,
|
|
||||||
meshCount: snapshot.meshCount,
|
|
||||||
triCount: Math.round(snapshot.triCount),
|
|
||||||
timestamp: performance.now().toFixed(0),
|
|
||||||
});
|
|
||||||
|
|
||||||
onOctreeReady(octree);
|
onOctreeReady(octree);
|
||||||
}, [enabled, graphNodeRef, onOctreeReady, rebuildKey]);
|
}, [enabled, graphNodeRef, onOctreeReady, rebuildKey]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import {
|
||||||
|
Material,
|
||||||
|
Mesh,
|
||||||
|
type DirectionalLight,
|
||||||
|
type Scene,
|
||||||
|
type WebGLRenderer,
|
||||||
|
} from "three";
|
||||||
|
|
||||||
|
interface UseShadowMapWarmupOptions {
|
||||||
|
/** Light whose shadow map should be reallocated once the scene stabilizes. */
|
||||||
|
light: React.RefObject<DirectionalLight | null>;
|
||||||
|
scene: Scene;
|
||||||
|
gl: WebGLRenderer;
|
||||||
|
invalidate: () => void;
|
||||||
|
/** Frames the mesh count must remain unchanged to consider the scene stable. */
|
||||||
|
stableFramesThreshold?: number;
|
||||||
|
/** Hard cap on how long we keep watching, in frames (~5s @60fps). */
|
||||||
|
safetyCapFrames?: number;
|
||||||
|
/** Sample mesh count every N frames to keep the traversal cost negligible. */
|
||||||
|
sampleEveryFrames?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShadowMapWarmup({
|
||||||
|
light,
|
||||||
|
scene,
|
||||||
|
gl,
|
||||||
|
invalidate,
|
||||||
|
stableFramesThreshold = 60,
|
||||||
|
safetyCapFrames = 300,
|
||||||
|
sampleEveryFrames = 6,
|
||||||
|
}: UseShadowMapWarmupOptions): void {
|
||||||
|
const meshCountRef = useRef(0);
|
||||||
|
const stableFramesRef = useRef(0);
|
||||||
|
const watchFramesRef = useRef(0);
|
||||||
|
const doneRef = useRef(false);
|
||||||
|
|
||||||
|
useFrame(() => {
|
||||||
|
if (doneRef.current || !light.current) return;
|
||||||
|
|
||||||
|
watchFramesRef.current += 1;
|
||||||
|
|
||||||
|
if (watchFramesRef.current % sampleEveryFrames === 0) {
|
||||||
|
let meshCount = 0;
|
||||||
|
scene.traverse((object) => {
|
||||||
|
if (object instanceof Mesh) meshCount += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (meshCount !== meshCountRef.current) {
|
||||||
|
meshCountRef.current = meshCount;
|
||||||
|
stableFramesRef.current = 0;
|
||||||
|
} else {
|
||||||
|
stableFramesRef.current += sampleEveryFrames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stableEnough = stableFramesRef.current >= stableFramesThreshold;
|
||||||
|
const safetyCapReached = watchFramesRef.current >= safetyCapFrames;
|
||||||
|
if (!stableEnough && !safetyCapReached) return;
|
||||||
|
|
||||||
|
doneRef.current = true;
|
||||||
|
reallocateShadowMap(light.current);
|
||||||
|
invalidateAllMaterials(scene);
|
||||||
|
forceShadowPass(gl, scene, light.current);
|
||||||
|
invalidate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reallocateShadowMap(light: DirectionalLight): void {
|
||||||
|
const shadowMap = light.shadow.map;
|
||||||
|
if (!shadowMap) return;
|
||||||
|
|
||||||
|
shadowMap.dispose();
|
||||||
|
light.shadow.map = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidateAllMaterials(scene: Scene): void {
|
||||||
|
const seen = new Set<Material>();
|
||||||
|
scene.traverse((object) => {
|
||||||
|
if (!(object instanceof Mesh)) return;
|
||||||
|
const materials = Array.isArray(object.material)
|
||||||
|
? object.material
|
||||||
|
: [object.material];
|
||||||
|
for (const material of materials) {
|
||||||
|
if (!material || seen.has(material)) continue;
|
||||||
|
seen.add(material);
|
||||||
|
material.needsUpdate = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function forceShadowPass(
|
||||||
|
gl: WebGLRenderer,
|
||||||
|
scene: Scene,
|
||||||
|
light: DirectionalLight,
|
||||||
|
): void {
|
||||||
|
scene.updateMatrixWorld(true);
|
||||||
|
light.target.updateMatrixWorld(true);
|
||||||
|
light.updateMatrixWorld(true);
|
||||||
|
light.shadow.camera.updateMatrixWorld(true);
|
||||||
|
light.shadow.camera.updateProjectionMatrix();
|
||||||
|
light.shadow.needsUpdate = true;
|
||||||
|
gl.shadowMap.needsUpdate = true;
|
||||||
|
}
|
||||||
@@ -272,37 +272,30 @@ function CollisionModelInstance({
|
|||||||
});
|
});
|
||||||
const sceneInstance = useClonedObject(scene);
|
const sceneInstance = useClonedObject(scene);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Strip the door slab from the la fabrik collision octree so the player
|
// Strip the door slab AND its Solidify-modifier frame from the la fabrik
|
||||||
// can walk through the doorway. The visual model is rendered separately
|
// collision octree so the player can walk through the doorway. The visual
|
||||||
// by MergedStaticMapModel and is unaffected.
|
// model is rendered separately by `MergedStaticMapModel` and is unaffected.
|
||||||
|
//
|
||||||
|
// - `porte` (+ Blender suffixes `porte.001` / `porte_001`): the door slab
|
||||||
|
// itself. We exclude unrelated names like `porte stock` (a shelf of
|
||||||
|
// stocked doors) by requiring an exact match or a numeric suffix only.
|
||||||
|
// - Children of a `Thicken` parent: the doorway frame produced by
|
||||||
|
// Blender's Solidify modifier. Its world AABBs sit right inside the
|
||||||
|
// doorway and otherwise prevent the player from entering / exiting.
|
||||||
if (node.name !== "lafabrik") return;
|
if (node.name !== "lafabrik") return;
|
||||||
|
|
||||||
// Strip the door slab (and any Blender-suffixed variant like `porte.001`,
|
|
||||||
// `porte_001`) from the la fabrik collision octree so the player can walk
|
|
||||||
// through the doorway. The visual model is rendered separately by
|
|
||||||
// MergedStaticMapModel and is unaffected. We exclude unrelated names like
|
|
||||||
// `porte stock` (a shelf of stocked doors) by requiring an exact match or
|
|
||||||
// a numeric suffix only.
|
|
||||||
const isDoorSlab = (name: string): boolean =>
|
const isDoorSlab = (name: string): boolean =>
|
||||||
name === "porte" || /^porte[._]\d+$/i.test(name);
|
name === "porte" || /^porte[._]\d+$/i.test(name);
|
||||||
|
const isDoorFrameThickenChild = (child: THREE.Object3D): boolean =>
|
||||||
|
child.parent?.name === "Thicken";
|
||||||
|
|
||||||
// [diag] temporary — collect all door-like candidate names to debug stripping
|
const doorMeshes: THREE.Object3D[] = [];
|
||||||
const candidates: string[] = [];
|
|
||||||
const removed: THREE.Object3D[] = [];
|
|
||||||
sceneInstance.traverse((child) => {
|
sceneInstance.traverse((child) => {
|
||||||
if (/porte/i.test(child.name)) {
|
if (isDoorSlab(child.name) || isDoorFrameThickenChild(child)) {
|
||||||
candidates.push(child.name);
|
doorMeshes.push(child);
|
||||||
}
|
|
||||||
if (isDoorSlab(child.name)) {
|
|
||||||
removed.push(child);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
console.log("[lafabrik:porte-strip]", {
|
for (const child of doorMeshes) {
|
||||||
candidates,
|
|
||||||
strippedCount: removed.length,
|
|
||||||
strippedNames: removed.map((c) => c.name),
|
|
||||||
});
|
|
||||||
for (const child of removed) {
|
|
||||||
child.removeFromParent();
|
child.removeFromParent();
|
||||||
}
|
}
|
||||||
}, [node.name, sceneInstance]);
|
}, [node.name, sceneInstance]);
|
||||||
|
|||||||
+26
-116
@@ -1,12 +1,10 @@
|
|||||||
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 {
|
||||||
@@ -29,6 +27,7 @@ import {
|
|||||||
} from "@/data/world/lightingConfig";
|
} from "@/data/world/lightingConfig";
|
||||||
import { LA_FABRIK_INTERIOR_LIGHT_POSITION } from "@/data/world/laFabrikConfig";
|
import { LA_FABRIK_INTERIOR_LIGHT_POSITION } from "@/data/world/laFabrikConfig";
|
||||||
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
import { useDebugFolder } from "@/hooks/debug/useDebugFolder";
|
||||||
|
import { useShadowMapWarmup } from "@/hooks/three/useShadowMapWarmup";
|
||||||
import { LIGHTING_STATE } from "@/world/lightingState";
|
import { LIGHTING_STATE } from "@/world/lightingState";
|
||||||
|
|
||||||
function configureRendererShadows(gl: WebGLRenderer): void {
|
function configureRendererShadows(gl: WebGLRenderer): void {
|
||||||
@@ -53,35 +52,17 @@ function configureSunShadow(sun: DirectionalLight, sunTarget: Object3D): void {
|
|||||||
sun.shadow.camera.updateProjectionMatrix();
|
sun.shadow.camera.updateProjectionMatrix();
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceShadowPass(
|
function placeSunRelativeToCamera(
|
||||||
gl: WebGLRenderer,
|
|
||||||
scene: Scene,
|
|
||||||
sun: DirectionalLight,
|
sun: DirectionalLight,
|
||||||
|
sunTarget: Object3D,
|
||||||
|
cameraPosition: { x: number; z: number },
|
||||||
): void {
|
): void {
|
||||||
scene.updateMatrixWorld(true);
|
sunTarget.position.set(cameraPosition.x, 0, cameraPosition.z);
|
||||||
sun.updateMatrixWorld(true);
|
sun.position.set(
|
||||||
sun.shadow.camera.updateProjectionMatrix();
|
cameraPosition.x + LIGHTING_STATE.sunX,
|
||||||
sun.shadow.needsUpdate = true;
|
LIGHTING_STATE.sunY,
|
||||||
gl.shadowMap.needsUpdate = true;
|
cameraPosition.z + LIGHTING_STATE.sunZ,
|
||||||
}
|
);
|
||||||
|
|
||||||
// [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 {
|
||||||
@@ -92,68 +73,20 @@ export function Lighting(): React.JSX.Element {
|
|||||||
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;
|
||||||
|
|
||||||
configureSunShadow(sun.current, sunTarget.current);
|
|
||||||
configureRendererShadows(gl);
|
configureRendererShadows(gl);
|
||||||
|
configureSunShadow(sun.current, sunTarget.current);
|
||||||
|
// Prime the sun + target onto the camera before the first shadow pass so
|
||||||
|
// the initial shadow frustum already covers the visible scene; without
|
||||||
|
// this, the first frame is rendered with the default (origin-centered)
|
||||||
|
// frustum and shadows can appear absent until the player moves.
|
||||||
|
placeSunRelativeToCamera(sun.current, sunTarget.current, camera.position);
|
||||||
|
}, [camera, gl]);
|
||||||
|
|
||||||
// Multi-frame shadow warmup: forces the shadow pass over 3 consecutive
|
useShadowMapWarmup({ light: sun, scene, gl, invalidate });
|
||||||
// frames so newly mounted meshes (loaded asynchronously by Suspense) get
|
|
||||||
// their world matrices and shadow map properly allocated. Without this,
|
|
||||||
// shadows can fail to render after a Physics Suspense remount.
|
|
||||||
let raf1 = 0;
|
|
||||||
let raf2 = 0;
|
|
||||||
forceShadowPass(gl, scene, sun.current);
|
|
||||||
invalidate();
|
|
||||||
raf1 = window.requestAnimationFrame(() => {
|
|
||||||
if (!sun.current) return;
|
|
||||||
forceShadowPass(gl, scene, sun.current);
|
|
||||||
invalidate();
|
|
||||||
raf2 = window.requestAnimationFrame(() => {
|
|
||||||
if (!sun.current) return;
|
|
||||||
forceShadowPass(gl, scene, sun.current);
|
|
||||||
invalidate();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// [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,
|
|
||||||
});
|
|
||||||
|
|
||||||
// [diag] temporary — track WebGL context loss/restore to correlate with shadow drops
|
|
||||||
const canvas = gl.domElement;
|
|
||||||
const handleContextLost = (event: Event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
console.log("[ctx:lost]", { timestamp: performance.now().toFixed(0) });
|
|
||||||
};
|
|
||||||
const handleContextRestored = () => {
|
|
||||||
console.log("[ctx:restored]", {
|
|
||||||
timestamp: performance.now().toFixed(0),
|
|
||||||
});
|
|
||||||
if (sun.current) {
|
|
||||||
forceShadowPass(gl, scene, sun.current);
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
canvas.addEventListener("webglcontextlost", handleContextLost);
|
|
||||||
canvas.addEventListener("webglcontextrestored", handleContextRestored);
|
|
||||||
return () => {
|
|
||||||
window.cancelAnimationFrame(raf1);
|
|
||||||
window.cancelAnimationFrame(raf2);
|
|
||||||
canvas.removeEventListener("webglcontextlost", handleContextLost);
|
|
||||||
canvas.removeEventListener("webglcontextrestored", handleContextRestored);
|
|
||||||
};
|
|
||||||
}, [gl, invalidate, scene]);
|
|
||||||
|
|
||||||
useDebugFolder("Lighting", (folder) => {
|
useDebugFolder("Lighting", (folder) => {
|
||||||
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");
|
folder.addColor(LIGHTING_STATE, "ambientColor").name("Ambient Color");
|
||||||
@@ -187,43 +120,20 @@ export function Lighting(): React.JSX.Element {
|
|||||||
.name("Sun Z");
|
.name("Sun Z");
|
||||||
});
|
});
|
||||||
|
|
||||||
useFrame(({ clock }) => {
|
useFrame(() => {
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sun.current && sunTarget.current) {
|
if (!sun.current || !sunTarget.current) return;
|
||||||
sunTarget.current.position.set(camera.position.x, 0, camera.position.z);
|
|
||||||
sunTarget.current.updateMatrixWorld();
|
|
||||||
sun.current.position.set(
|
|
||||||
camera.position.x + LIGHTING_STATE.sunX,
|
|
||||||
LIGHTING_STATE.sunY,
|
|
||||||
camera.position.z + LIGHTING_STATE.sunZ,
|
|
||||||
);
|
|
||||||
sun.current.color.set(LIGHTING_STATE.sunColor);
|
|
||||||
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
|
||||||
sun.current.updateMatrixWorld();
|
|
||||||
sun.current.shadow.needsUpdate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// [diag] periodic shadow pipeline check (every 2s)
|
placeSunRelativeToCamera(sun.current, sunTarget.current, camera.position);
|
||||||
const now = clock.getElapsedTime();
|
sunTarget.current.updateMatrixWorld();
|
||||||
if (now - lastDiagAtRef.current > 2 && sun.current) {
|
sun.current.color.set(LIGHTING_STATE.sunColor);
|
||||||
lastDiagAtRef.current = now;
|
sun.current.intensity = LIGHTING_STATE.sunIntensity;
|
||||||
console.log("[shadow:tick]", {
|
sun.current.updateMatrixWorld();
|
||||||
shadowMapEnabled: gl.shadowMap.enabled,
|
sun.current.shadow.needsUpdate = true;
|
||||||
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