4 Commits

Author SHA1 Message Date
Tom Boullay 153833deec Update favicon.ico
🔍 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
2026-06-01 11:32:31 +02:00
Tom Boullay b617885aa2 chore(ui): clean loading overlay logo styling
Show the loading logo as a raw, contained image without rounded corners
or drop shadow, slightly larger to balance the empty space.
2026-06-01 11:28:15 +02:00
Tom Boullay 5d2e7e2aab fix(world): allow walking through la fabrik door
Strip the 'porte' mesh from the cloned scene used to build the la fabrik
collision octree. The wall geometry already has a doorway cutout, so
removing the door slab leaves the opening passable. The visual model is
rendered separately by MergedStaticMapModel and is unaffected.

Drops the stop-gap LA_FABRIK_COLLISION_Y_OFFSET added during debugging.
2026-06-01 11:28:15 +02:00
Tom Boullay de77f76d48 fix(world): restore shadow auto-update
Reverts the manual shadow refresh throttle introduced in 6d58b90 which
prevented shadows from rendering. Renderer and sun shadow now use
autoUpdate=true and a per-frame needsUpdate=true pulse, matching the
behaviour that produced visible shadows before that commit.
2026-06-01 11:28:07 +02:00
6 changed files with 27 additions and 72 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+3 -5
View File
@@ -942,11 +942,9 @@ canvas {
.scene-loading-overlay__logo {
position: relative;
z-index: 1;
width: clamp(180px, 28vw, 320px);
max-height: min(38vh, 320px);
border-radius: 16px;
object-fit: cover;
box-shadow: 0 28px 80px rgba(0, 0, 0, 0.28);
width: clamp(207px, 32.2vw, 368px);
max-height: min(43.7vh, 368px);
object-fit: contain;
}
.scene-loading-overlay__footer {
+2 -2
View File
@@ -130,7 +130,7 @@ export function HomePage(): React.JSX.Element | null {
gl.shadowMap.enabled = true;
gl.shadowMap.type = THREE.PCFShadowMap;
gl.shadowMap.autoUpdate = false;
gl.shadowMap.autoUpdate = true;
gl.shadowMap.needsUpdate = true;
// The browser hands us a WEBGL_lose_context extension we can use to
@@ -148,7 +148,7 @@ export function HomePage(): React.JSX.Element | null {
const handleContextRestored = () => {
gl.shadowMap.enabled = true;
gl.shadowMap.type = THREE.PCFShadowMap;
gl.shadowMap.autoUpdate = false;
gl.shadowMap.autoUpdate = true;
gl.shadowMap.needsUpdate = true;
logger.info("WebGL", "Context restored");
};
+16
View File
@@ -223,6 +223,22 @@ function CollisionModelInstance({
scale: normalizedScale,
});
const sceneInstance = useClonedObject(scene);
useEffect(() => {
// Strip the door slab 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.
if (node.name !== "lafabrik") return;
const removed: THREE.Object3D[] = [];
sceneInstance.traverse((child) => {
if (child.name === "porte") {
removed.push(child);
}
});
for (const child of removed) {
child.removeFromParent();
}
}, [node.name, sceneInstance]);
const collisionPosition = useMemo(() => {
if (node.name === "terrain") return position;
+6 -59
View File
@@ -1,11 +1,8 @@
import { useEffect, useRef } from "react";
import type { MutableRefObject } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import {
PCFShadowMap,
Vector3,
type AmbientLight,
type Camera,
type DirectionalLight,
type Object3D,
type WebGLRenderer,
@@ -35,21 +32,17 @@ const SHADOW_MAP_SIZE = 2048;
const SHADOW_CAMERA_SIZE = 95;
const SHADOW_CAMERA_NEAR = 0.5;
const SHADOW_CAMERA_FAR = 300;
const SHADOW_REFRESH_INTERVAL_MS = 180;
const SHADOW_REFRESH_DISTANCE = 0.75;
const SHADOW_REFRESH_DISTANCE_SQUARED =
SHADOW_REFRESH_DISTANCE * SHADOW_REFRESH_DISTANCE;
function configureManualRendererShadows(gl: WebGLRenderer): void {
function configureRendererShadows(gl: WebGLRenderer): void {
gl.shadowMap.enabled = true;
gl.shadowMap.type = PCFShadowMap;
gl.shadowMap.autoUpdate = false;
gl.shadowMap.autoUpdate = true;
gl.shadowMap.needsUpdate = true;
}
function configureSunShadow(sun: DirectionalLight, sunTarget: Object3D): void {
sun.target = sunTarget;
sun.shadow.autoUpdate = false;
sun.shadow.autoUpdate = true;
sun.shadow.needsUpdate = true;
sun.shadow.mapSize.width = SHADOW_MAP_SIZE;
sun.shadow.mapSize.height = SHADOW_MAP_SIZE;
@@ -62,56 +55,18 @@ function configureSunShadow(sun: DirectionalLight, sunTarget: Object3D): void {
sun.shadow.camera.updateProjectionMatrix();
}
function requestSunShadowRefresh({
camera,
elapsedMs,
gl,
lastCameraPosition,
lastRefreshMs,
shadowHasInitialPosition,
sun,
}: {
camera: Camera;
elapsedMs: number;
gl: WebGLRenderer;
lastCameraPosition: Vector3;
lastRefreshMs: MutableRefObject<number>;
shadowHasInitialPosition: MutableRefObject<boolean>;
sun: DirectionalLight;
}): void {
if (elapsedMs - lastRefreshMs.current < SHADOW_REFRESH_INTERVAL_MS) {
return;
}
const cameraMovedEnough =
!shadowHasInitialPosition.current ||
lastCameraPosition.distanceToSquared(camera.position) >=
SHADOW_REFRESH_DISTANCE_SQUARED;
if (!cameraMovedEnough) return;
configureManualRendererShadows(gl);
sun.shadow.needsUpdate = true;
lastCameraPosition.copy(camera.position);
lastRefreshMs.current = elapsedMs;
shadowHasInitialPosition.current = true;
}
export function Lighting(): React.JSX.Element {
const camera = useThree((state) => state.camera);
const gl = useThree((state) => state.gl);
const ambient = useRef<AmbientLight>(null);
const sun = useRef<DirectionalLight>(null);
const sunTarget = useRef<Object3D>(null);
const lastShadowRefreshMs = useRef(-SHADOW_REFRESH_INTERVAL_MS);
const lastShadowCameraPosition = useRef(new Vector3());
const shadowHasInitialPosition = useRef(false);
useEffect(() => {
if (!sun.current || !sunTarget.current) return;
configureSunShadow(sun.current, sunTarget.current);
configureManualRendererShadows(gl);
configureRendererShadows(gl);
}, [gl]);
useDebugFolder("Lighting", (folder) => {
@@ -146,7 +101,7 @@ export function Lighting(): React.JSX.Element {
.name("Sun Z");
});
useFrame(({ clock }) => {
useFrame(() => {
if (ambient.current) {
ambient.current.color.set(LIGHTING_STATE.ambientColor);
ambient.current.intensity = LIGHTING_STATE.ambientIntensity;
@@ -163,15 +118,7 @@ export function Lighting(): React.JSX.Element {
sun.current.color.set(LIGHTING_STATE.sunColor);
sun.current.intensity = LIGHTING_STATE.sunIntensity;
sun.current.updateMatrixWorld();
requestSunShadowRefresh({
camera,
elapsedMs: clock.elapsedTime * 1000,
gl,
lastCameraPosition: lastShadowCameraPosition.current,
lastRefreshMs: lastShadowRefreshMs,
shadowHasInitialPosition,
sun: sun.current,
});
sun.current.shadow.needsUpdate = true;
}
});
-6
View File
@@ -45,11 +45,6 @@ function forceSceneShadowPass(
});
}
function restoreManualShadowUpdates(gl: THREE.WebGLRenderer): void {
gl.shadowMap.autoUpdate = false;
gl.shadowMap.needsUpdate = true;
}
export function SceneShadowWarmup({
active,
onReady,
@@ -82,7 +77,6 @@ export function SceneShadowWarmup({
secondFrame = window.requestAnimationFrame(() => {
forceSceneShadowPass(gl, scene);
restoreManualShadowUpdates(gl);
invalidate();
onReady();
});