From 37a3aba67277de8efa901027bd93bce3cf457426 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Fri, 29 May 2026 13:34:51 -0400 Subject: [PATCH] viewer: center shadow frustum on focus so it tracks the view (#347) The directional shadow light was placed at focus + theme offset (only ~17-32 units), while the ortho shadow camera ran near=1/far=100. That left the focus near the front of a long frustum whose far end swung around as the look-at moved, so shadows appeared not to follow the view. Park the light at a fixed distance along its (preserved) direction and bracket near/far around that distance so the focus stays centered in the frustum depth. Co-authored-by: Claude Opus 4.7 --- .../viewer/src/components/viewer/lights.tsx | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/viewer/src/components/viewer/lights.tsx b/packages/viewer/src/components/viewer/lights.tsx index dcecb89f..79847356 100644 --- a/packages/viewer/src/components/viewer/lights.tsx +++ b/packages/viewer/src/components/viewer/lights.tsx @@ -26,6 +26,16 @@ const SHADOWS_DISABLED = // deliberate middle ground — present, but not the heavy contact shadow there. const MAX_SHADOW_INTENSITY = 0.55 +// Shadow frustum framing. The directional light is parked at a fixed distance +// along its (theme-defined) direction from the focus point, and the ortho +// shadow camera's depth is centred on that focus. Theme offsets are only +// ~17–32 units long, so without a fixed distance the focus sits near the front +// of a long frustum and the far end swings around as the focus moves. Keeping +// the light far away and the focus centred keeps the frustum hugging the view. +const SHADOW_DISTANCE = 120 +const SHADOW_NEAR = 20 +const SHADOW_FAR = 220 + export function Lights() { const sceneTheme = useViewer((state) => state.sceneTheme) const theme = getSceneTheme(sceneTheme) @@ -39,6 +49,8 @@ export function Lights() { // ortho shadow camera only covers ±shadowCameraSize around the light target, // so it has to track the view or anything far from origin gets no shadows. const shadowFocus = useRef(new THREE.Vector3()) + // Scratch vector for the per-light direction, reused to avoid per-frame allocs. + const shadowDir = useRef(new THREE.Vector3()) const hemiRef = useRef(null) const ambientRef = useRef(null) @@ -77,7 +89,13 @@ export function Lights() { const light = lightRefs.current[index] if (!(config?.castShadow && light)) continue const [ox, oy, oz] = config.position - light.position.set(focus.x + ox, focus.y + oy, focus.z + oz) + // Preserve the theme's light DIRECTION but park the light at a fixed + // distance, so the ortho frustum (depth centred via SHADOW_NEAR/FAR) + // stays centred on the focus instead of dangling far past it. + const dir = shadowDir.current.set(ox, oy, oz) + if (dir.lengthSq() === 0) dir.set(0, 1, 0) + dir.normalize().multiplyScalar(SHADOW_DISTANCE) + light.position.set(focus.x + dir.x, focus.y + dir.y, focus.z + dir.z) light.target.position.copy(focus) light.target.updateMatrixWorld() } @@ -175,9 +193,9 @@ export function Lights() {