perf: upgrade world viewer to spark 2
This commit is contained in:
parent
147f9394c8
commit
1c935bf7ae
@ -170,6 +170,13 @@ const SPLAT_REVEAL_DURATION_MS = 900;
|
|||||||
const SPLAT_PARTICLE_SAMPLE_LIMIT = 720;
|
const SPLAT_PARTICLE_SAMPLE_LIMIT = 720;
|
||||||
const SPLAT_WORLD_BOUNDS_SAMPLE_LIMIT = 1200;
|
const SPLAT_WORLD_BOUNDS_SAMPLE_LIMIT = 1200;
|
||||||
const SPLAT_FAST_LOAD_SKIP_REVEAL_MS = 1200;
|
const SPLAT_FAST_LOAD_SKIP_REVEAL_MS = 1200;
|
||||||
|
const SPARK_INLINE_LOD_BUILD_THRESHOLD = 180_000;
|
||||||
|
const SPARK_INLINE_LOD_SPLAT_SCALE = 0.78;
|
||||||
|
const SPARK_INLINE_LOD_RENDER_SCALE = 1.8;
|
||||||
|
const SPARK_MINIMAL_FRAME_LOD_SPLAT_SCALE = 0.6;
|
||||||
|
const SPARK_MINIMAL_FRAME_LOD_RENDER_SCALE = 2.4;
|
||||||
|
const SPARK_MIN_SORT_INTERVAL_MS = 24;
|
||||||
|
const SPARK_MINIMAL_FRAME_SORT_INTERVAL_MS = 40;
|
||||||
|
|
||||||
const KEY_TO_MOVEMENT: Partial<Record<string, MovementCommand>> = {
|
const KEY_TO_MOVEMENT: Partial<Record<string, MovementCommand>> = {
|
||||||
KeyW: "forward",
|
KeyW: "forward",
|
||||||
@ -230,6 +237,7 @@ function buildSplatParticleCloudData(
|
|||||||
sampleLimit = SPLAT_PARTICLE_SAMPLE_LIMIT
|
sampleLimit = SPLAT_PARTICLE_SAMPLE_LIMIT
|
||||||
): SplatParticleCloudData | null {
|
): SplatParticleCloudData | null {
|
||||||
const packedSplats = splatMesh.packedSplats;
|
const packedSplats = splatMesh.packedSplats;
|
||||||
|
if (!packedSplats) return null;
|
||||||
const totalSplats = Math.max(0, packedSplats?.numSplats || 0);
|
const totalSplats = Math.max(0, packedSplats?.numSplats || 0);
|
||||||
if (!totalSplats) return null;
|
if (!totalSplats) return null;
|
||||||
|
|
||||||
@ -303,6 +311,9 @@ function getSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
|||||||
|
|
||||||
function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
||||||
const packedSplats = splatMesh.packedSplats;
|
const packedSplats = splatMesh.packedSplats;
|
||||||
|
if (!packedSplats) {
|
||||||
|
return getSplatWorldBounds(splatMesh);
|
||||||
|
}
|
||||||
const totalSplats = Math.max(0, packedSplats?.numSplats || 0);
|
const totalSplats = Math.max(0, packedSplats?.numSplats || 0);
|
||||||
if (totalSplats < 32) {
|
if (totalSplats < 32) {
|
||||||
return getSplatWorldBounds(splatMesh);
|
return getSplatWorldBounds(splatMesh);
|
||||||
@ -679,15 +690,44 @@ function WorldSplatLoadingOverlay({
|
|||||||
phase: Exclude<SplatLoadingPhase, "hidden">;
|
phase: Exclude<SplatLoadingPhase, "hidden">;
|
||||||
}) {
|
}) {
|
||||||
const isRevealing = phase === "revealing";
|
const isRevealing = phase === "revealing";
|
||||||
|
const isLoading = phase === "loading";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{phase === "loading" ? (
|
{previewImageUrl ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"pointer-events-none absolute inset-0 overflow-hidden transition-opacity duration-500",
|
||||||
|
isLoading ? "opacity-100" : "opacity-30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="世界预览"
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 h-full w-full object-cover transition-transform duration-700",
|
||||||
|
isLoading ? "scale-[1.01]" : "scale-100"
|
||||||
|
)}
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="high"
|
||||||
|
src={previewImageUrl}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute inset-0 transition-opacity duration-500",
|
||||||
|
isLoading
|
||||||
|
? "bg-gradient-to-b from-black/10 via-black/12 to-black/30"
|
||||||
|
: "bg-gradient-to-b from-black/0 via-black/5 to-black/18"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : phase === "loading" ? (
|
||||||
<div className="pointer-events-none absolute inset-0 overflow-hidden transition-opacity duration-300">
|
<div className="pointer-events-none absolute inset-0 overflow-hidden transition-opacity duration-300">
|
||||||
{previewImageUrl ? (
|
{previewImageUrl ? (
|
||||||
<img
|
<img
|
||||||
alt="世界预览"
|
alt="世界预览"
|
||||||
className="absolute inset-0 h-full w-full object-cover opacity-[0.1] blur-[10px] saturate-[0.72]"
|
className="absolute inset-0 h-full w-full object-cover opacity-[0.1] blur-[10px] saturate-[0.72]"
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority="high"
|
||||||
src={previewImageUrl}
|
src={previewImageUrl}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@ -703,11 +743,17 @@ function WorldSplatLoadingOverlay({
|
|||||||
<div className="rounded-2xl border border-white/10 bg-black/45 px-3 py-2 shadow-[0_20px_60px_rgba(0,0,0,0.38)] backdrop-blur-xl">
|
<div className="rounded-2xl border border-white/10 bg-black/45 px-3 py-2 shadow-[0_20px_60px_rgba(0,0,0,0.38)] backdrop-blur-xl">
|
||||||
<div className="flex items-center gap-2 text-[11px] text-zinc-100">
|
<div className="flex items-center gap-2 text-[11px] text-zinc-100">
|
||||||
<Sparkles className="h-3.5 w-3.5 text-cyan-200" />
|
<Sparkles className="h-3.5 w-3.5 text-cyan-200" />
|
||||||
<span>{isRevealing ? "世界数据正在聚合" : "正在载入世界数据"}</span>
|
<span>{isRevealing ? "高清世界正在接管预览" : "正在载入世界数据"}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-zinc-400">
|
<div className="mt-1 flex items-center gap-2 text-[10px] text-zinc-400">
|
||||||
<Loader2 className="h-3 w-3 animate-spin" />
|
<Loader2 className="h-3 w-3 animate-spin" />
|
||||||
<span>{isRevealing ? "当前世界正在从颗粒态恢复细节" : "空间数据准备中"}</span>
|
<span>
|
||||||
|
{isRevealing
|
||||||
|
? "预览图已显示,世界细节正在补齐"
|
||||||
|
: previewImageUrl
|
||||||
|
? "先展示世界预览图,空间数据后台继续加载"
|
||||||
|
: "空间数据准备中"}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -951,6 +997,7 @@ function WorldSplatScene({
|
|||||||
splatUrl,
|
splatUrl,
|
||||||
revealPhase = "loading",
|
revealPhase = "loading",
|
||||||
firstPerson = false,
|
firstPerson = false,
|
||||||
|
minimalFrame = false,
|
||||||
hotspots = [],
|
hotspots = [],
|
||||||
interactionMode = "navigate",
|
interactionMode = "navigate",
|
||||||
onPick,
|
onPick,
|
||||||
@ -964,6 +1011,7 @@ function WorldSplatScene({
|
|||||||
splatUrl: string;
|
splatUrl: string;
|
||||||
revealPhase?: SplatLoadingPhase;
|
revealPhase?: SplatLoadingPhase;
|
||||||
firstPerson?: boolean;
|
firstPerson?: boolean;
|
||||||
|
minimalFrame?: boolean;
|
||||||
hotspots?: World3DHotspot[];
|
hotspots?: World3DHotspot[];
|
||||||
interactionMode?: "navigate" | "hotspot";
|
interactionMode?: "navigate" | "hotspot";
|
||||||
onPick?: (pick: World3DPickResult) => void;
|
onPick?: (pick: World3DPickResult) => void;
|
||||||
@ -996,17 +1044,36 @@ function WorldSplatScene({
|
|||||||
renderer,
|
renderer,
|
||||||
blurAmount: 0.18,
|
blurAmount: 0.18,
|
||||||
maxStdDev: Math.sqrt(7),
|
maxStdDev: Math.sqrt(7),
|
||||||
originDistance: 0.85,
|
sortRadial: true,
|
||||||
view: {
|
minSortIntervalMs: minimalFrame
|
||||||
sortRadial: true,
|
? SPARK_MINIMAL_FRAME_SORT_INTERVAL_MS
|
||||||
sortDistance: 0.04,
|
: SPARK_MIN_SORT_INTERVAL_MS,
|
||||||
},
|
enableLod: true,
|
||||||
|
enableLodFetching: false,
|
||||||
|
lodSplatScale: minimalFrame
|
||||||
|
? SPARK_MINIMAL_FRAME_LOD_SPLAT_SCALE
|
||||||
|
: SPARK_INLINE_LOD_SPLAT_SCALE,
|
||||||
|
lodRenderScale: minimalFrame
|
||||||
|
? SPARK_MINIMAL_FRAME_LOD_RENDER_SCALE
|
||||||
|
: SPARK_INLINE_LOD_RENDER_SCALE,
|
||||||
|
numLodFetchers: 2,
|
||||||
}),
|
}),
|
||||||
[renderer]
|
[minimalFrame, renderer]
|
||||||
);
|
);
|
||||||
const splatMesh = useMemo(
|
const splatMesh = useMemo(
|
||||||
() => new SparkSplatMesh({ fileType: SplatFileType.SPZ, url: splatUrl }),
|
() =>
|
||||||
[splatUrl]
|
new SparkSplatMesh({
|
||||||
|
fileType: SplatFileType.SPZ,
|
||||||
|
url: splatUrl,
|
||||||
|
lod: true,
|
||||||
|
nonLod: true,
|
||||||
|
lodAbove: SPARK_INLINE_LOD_BUILD_THRESHOLD,
|
||||||
|
enableLod: true,
|
||||||
|
lodScale: minimalFrame
|
||||||
|
? SPARK_MINIMAL_FRAME_LOD_SPLAT_SCALE
|
||||||
|
: SPARK_INLINE_LOD_SPLAT_SCALE,
|
||||||
|
}),
|
||||||
|
[minimalFrame, splatUrl]
|
||||||
);
|
);
|
||||||
const groundPlaneOffset =
|
const groundPlaneOffset =
|
||||||
typeof semanticsMetadata?.ground_plane_offset === "number"
|
typeof semanticsMetadata?.ground_plane_offset === "number"
|
||||||
@ -1023,6 +1090,7 @@ function WorldSplatScene({
|
|||||||
applySparkRevealProfile(sparkRenderer, splatMesh, SPLAT_NORMAL_PROFILE);
|
applySparkRevealProfile(sparkRenderer, splatMesh, SPLAT_NORMAL_PROFILE);
|
||||||
return () => {
|
return () => {
|
||||||
sparkRenderer.removeFromParent();
|
sparkRenderer.removeFromParent();
|
||||||
|
sparkRenderer.dispose();
|
||||||
};
|
};
|
||||||
}, [sparkRenderer, splatMesh]);
|
}, [sparkRenderer, splatMesh]);
|
||||||
|
|
||||||
@ -1182,7 +1250,7 @@ function WorldSplatScene({
|
|||||||
applySparkRevealProfile(sparkRenderer, splatMesh, SPLAT_NORMAL_PROFILE);
|
applySparkRevealProfile(sparkRenderer, splatMesh, SPLAT_NORMAL_PROFILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
sparkRenderer.needsUpdate = true;
|
sparkRenderer.dirty = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handlePointerDown = (event: ThreeEvent<PointerEvent>) => {
|
const handlePointerDown = (event: ThreeEvent<PointerEvent>) => {
|
||||||
@ -1428,12 +1496,12 @@ function WorldSplatViewer({
|
|||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [0, 1.6, 4], fov: 55 }}
|
camera={{ position: [0, 1.6, 4], fov: 55 }}
|
||||||
className="h-full w-full bg-[radial-gradient(circle_at_top,_rgba(15,23,42,0.95),_rgba(1,1,1,1)_68%)]"
|
className="h-full w-full bg-[radial-gradient(circle_at_top,_rgba(15,23,42,0.95),_rgba(1,1,1,1)_68%)]"
|
||||||
dpr={[1, 1.25]}
|
dpr={minimalFrame ? [0.85, 1] : [1, 1]}
|
||||||
frameloop="always"
|
frameloop="always"
|
||||||
gl={{
|
gl={{
|
||||||
antialias: false,
|
antialias: false,
|
||||||
powerPreference: "high-performance",
|
powerPreference: "high-performance",
|
||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: Boolean(onCaptureReady),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WorldSplatCaptureBridge onCaptureReady={onCaptureReady} />
|
<WorldSplatCaptureBridge onCaptureReady={onCaptureReady} />
|
||||||
@ -1441,6 +1509,7 @@ function WorldSplatViewer({
|
|||||||
firstPerson={firstPerson}
|
firstPerson={firstPerson}
|
||||||
hotspots={hotspots}
|
hotspots={hotspots}
|
||||||
interactionMode={interactionMode}
|
interactionMode={interactionMode}
|
||||||
|
minimalFrame={minimalFrame}
|
||||||
onHotspotSelect={onHotspotSelect}
|
onHotspotSelect={onHotspotSelect}
|
||||||
onLoadStateChange={setLoadState}
|
onLoadStateChange={setLoadState}
|
||||||
onPick={onPick}
|
onPick={onPick}
|
||||||
|
|||||||
@ -59,7 +59,10 @@ export function pickBestWorldLabsSplatUrl(
|
|||||||
return pickBestWorldLabsSplatEntry(spzUrls)?.[1] || null;
|
return pickBestWorldLabsSplatEntry(spzUrls)?.[1] || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INLINE_WORLDLABS_SPLAT_TARGET_SCORE = 1_500_000;
|
// World Labs exports multiple SPZ tiers (for example 100k / 500k / full).
|
||||||
|
// For embedded previews we bias hard toward the lightweight tier so users see
|
||||||
|
// the world quickly, then can switch to imagery / mesh for detail if needed.
|
||||||
|
const INLINE_WORLDLABS_SPLAT_TARGET_SCORE = 100_000;
|
||||||
|
|
||||||
function pickClosestResolutionEntry(
|
function pickClosestResolutionEntry(
|
||||||
entries: Array<[string, string]>,
|
entries: Array<[string, string]>,
|
||||||
|
|||||||
@ -60,7 +60,7 @@
|
|||||||
"@radix-ui/react-visually-hidden": "^1.1.0",
|
"@radix-ui/react-visually-hidden": "^1.1.0",
|
||||||
"@react-three/drei": "^10.7.7",
|
"@react-three/drei": "^10.7.7",
|
||||||
"@react-three/fiber": "^9.5.0",
|
"@react-three/fiber": "^9.5.0",
|
||||||
"@sparkjsdev/spark": "^0.1.10",
|
"@sparkjsdev/spark": "^2.0.0",
|
||||||
"@tanstack/react-virtual": "^3.13.24",
|
"@tanstack/react-virtual": "^3.13.24",
|
||||||
"@types/form-data": "^2.5.2",
|
"@types/form-data": "^2.5.2",
|
||||||
"@vercel/analytics": "^1.3.1",
|
"@vercel/analytics": "^1.3.1",
|
||||||
|
|||||||
13
pnpm-lock.yaml
generated
13
pnpm-lock.yaml
generated
@ -129,8 +129,8 @@ importers:
|
|||||||
specifier: ^9.5.0
|
specifier: ^9.5.0
|
||||||
version: 9.5.0(@types/react@18.3.18)(immer@11.1.3)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0)
|
version: 9.5.0(@types/react@18.3.18)(immer@11.1.3)(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)(three@0.182.0)
|
||||||
'@sparkjsdev/spark':
|
'@sparkjsdev/spark':
|
||||||
specifier: ^0.1.10
|
specifier: ^2.0.0
|
||||||
version: 0.1.10
|
version: 2.0.0(three@0.182.0)
|
||||||
'@tanstack/react-virtual':
|
'@tanstack/react-virtual':
|
||||||
specifier: ^3.13.24
|
specifier: ^3.13.24
|
||||||
version: 3.13.24(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)
|
version: 3.13.24(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)
|
||||||
@ -2977,8 +2977,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
|
resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@sparkjsdev/spark@0.1.10':
|
'@sparkjsdev/spark@2.0.0':
|
||||||
resolution: {integrity: sha512-CiijdZQuj7KPDUqIZPiEqyUkJCYo1JqR05vq/V+ElxMwqR7L70ZuZDyIKcasjZHSiPB8pGRMH8HZGqUKO9aRPQ==}
|
resolution: {integrity: sha512-W1p7NM4xXcUAVVcVus2mIFwcAC3VckyE0l4wXJuSmhcOgb6hHpqhQ/Ks9MK9ts7j23K+UwbCBwe+aTc/zgGygw==}
|
||||||
|
peerDependencies:
|
||||||
|
three: ^0.180.0
|
||||||
|
|
||||||
'@standard-schema/spec@1.0.0':
|
'@standard-schema/spec@1.0.0':
|
||||||
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
|
||||||
@ -9424,9 +9426,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
'@sparkjsdev/spark@0.1.10':
|
'@sparkjsdev/spark@2.0.0(three@0.182.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
fflate: 0.8.2
|
fflate: 0.8.2
|
||||||
|
three: 0.182.0
|
||||||
|
|
||||||
'@standard-schema/spec@1.0.0': {}
|
'@standard-schema/spec@1.0.0': {}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user