From 14009f869e8ab9db1a372e73a877b504867117cb Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 02:31:33 +0800 Subject: [PATCH] fix: keep world splat loading responsive --- components/canvas/world3d/World3DViewer.tsx | 44 +++++++++++++++------ 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/components/canvas/world3d/World3DViewer.tsx b/components/canvas/world3d/World3DViewer.tsx index b5be59204..8ef0f6c75 100644 --- a/components/canvas/world3d/World3DViewer.tsx +++ b/components/canvas/world3d/World3DViewer.tsx @@ -157,7 +157,7 @@ function lerpSparkRevealProfile( } const SPLAT_REVEAL_DURATION_MS = 2600; -const SPLAT_PARTICLE_SAMPLE_LIMIT = 5200; +const SPLAT_PARTICLE_SAMPLE_LIMIT = 1800; const KEY_TO_MOVEMENT: Partial> = { KeyW: "forward", @@ -217,10 +217,12 @@ function buildSplatParticleCloudData( splatMesh: SparkSplatMesh, sampleLimit = SPLAT_PARTICLE_SAMPLE_LIMIT ): SplatParticleCloudData | null { - const totalSplats = Math.max(0, splatMesh.packedSplats?.numSplats || 0); + const packedSplats = splatMesh.packedSplats; + const totalSplats = Math.max(0, packedSplats?.numSplats || 0); if (!totalSplats) return null; - const sampleStep = Math.max(1, Math.floor(totalSplats / sampleLimit)); + const targetCount = Math.min(sampleLimit, totalSplats); + const sampleStep = Math.max(totalSplats / targetCount, 1); const points: Array<{ center: THREE.Vector3; color: THREE.Color; @@ -228,17 +230,17 @@ function buildSplatParticleCloudData( }> = []; const warmWhite = new THREE.Color("#fff1dc"); - splatMesh.forEachSplat((index, center, _scales, _quaternion, opacity, color) => { - if (points.length >= sampleLimit) return; - if (index % sampleStep !== 0) return; - if (opacity < 0.035) return; + for (let sampleIndex = 0; sampleIndex < targetCount; sampleIndex += 1) { + const index = Math.min(Math.floor(sampleIndex * sampleStep), totalSplats - 1); + const { center, opacity, color } = packedSplats.getSplat(index); + if (opacity < 0.035) continue; points.push({ - center: center.clone(), - color: color.clone(), + center, + color, opacity, }); - }); + } if (!points.length) return null; @@ -758,12 +760,13 @@ function WorldSplatScene({ applySparkRevealProfile(sparkRenderer, splatMesh, SPLAT_LOADING_PROFILE); let cancelled = false; + let idleCallbackId: number | null = null; + let timeoutId: ReturnType | null = null; splatMesh.initialized .then(() => { if (cancelled) return; window.requestAnimationFrame(() => { if (cancelled || hasAppliedInitialViewRef.current) return; - setParticleCloudData(buildSplatParticleCloudData(splatMesh)); positionCameraInsideWorld({ camera, controls: orbitControlsRef.current, @@ -772,6 +775,19 @@ function WorldSplatScene({ }); hasAppliedInitialViewRef.current = true; onLoadStateChange?.("ready"); + + const buildParticleCloud = () => { + if (cancelled) return; + setParticleCloudData(buildSplatParticleCloudData(splatMesh)); + }; + + if ("requestIdleCallback" in window) { + idleCallbackId = window.requestIdleCallback(buildParticleCloud, { + timeout: 900, + }); + } else { + timeoutId = globalThis.setTimeout(buildParticleCloud, 120); + } }); }) .catch(() => { @@ -779,6 +795,12 @@ function WorldSplatScene({ }); return () => { cancelled = true; + if (idleCallbackId !== null && "cancelIdleCallback" in window) { + window.cancelIdleCallback(idleCallbackId); + } + if (timeoutId !== null) { + globalThis.clearTimeout(timeoutId); + } splatMesh.dispose(); }; }, [camera, groundPlaneOffset, onLoadStateChange, sparkRenderer, splatMesh]);