perf: speed up world viewer loading
This commit is contained in:
parent
89ec01cfb8
commit
dee8019efd
@ -4,6 +4,8 @@ import { buildThreeAssetUrl } from "@/lib/canvas/three-asset-url";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const maxDuration = 60;
|
||||
const THREE_ASSET_BROWSER_CACHE_CONTROL =
|
||||
"private, max-age=86400, stale-while-revalidate=604800";
|
||||
|
||||
function isAllowedModelHost(hostname: string) {
|
||||
const normalized = hostname.toLowerCase();
|
||||
@ -144,7 +146,13 @@ export async function GET(request: Request) {
|
||||
const acceptRanges = upstream.headers.get("accept-ranges");
|
||||
if (acceptRanges) responseHeaders.set("accept-ranges", acceptRanges);
|
||||
|
||||
responseHeaders.set("cache-control", "private, no-store");
|
||||
const etag = upstream.headers.get("etag");
|
||||
if (etag) responseHeaders.set("etag", etag);
|
||||
|
||||
const lastModified = upstream.headers.get("last-modified");
|
||||
if (lastModified) responseHeaders.set("last-modified", lastModified);
|
||||
|
||||
responseHeaders.set("cache-control", THREE_ASSET_BROWSER_CACHE_CONTROL);
|
||||
|
||||
if (shouldRewriteGltf) {
|
||||
const rewrittenPayload = rewriteGltfPayload(await upstream.text(), targetUrl);
|
||||
|
||||
@ -91,6 +91,7 @@ import { cn, fetcher } from "@/lib/utils";
|
||||
import {
|
||||
getSortedWorldLabsSplatEntries,
|
||||
pickBestArchivedWorldLabsSplatUrl,
|
||||
pickPreferredInlineWorldLabsSplatUrl,
|
||||
} from "@/lib/worldlabs/splat-utils";
|
||||
import {
|
||||
DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG,
|
||||
@ -2605,7 +2606,10 @@ export default function CanvasModel3DPage() {
|
||||
? buildWorldShareUrl(currentTask.shareToken)
|
||||
: null;
|
||||
const currentSpzEntries = getSortedWorldLabsSplatEntries(currentWorldAsset?.spzUrls);
|
||||
const currentPrimarySpzUrl = currentSpzEntries[0]?.[1] || null;
|
||||
const currentPrimarySpzUrl =
|
||||
pickPreferredInlineWorldLabsSplatUrl(currentWorldAsset?.spzUrls) ||
|
||||
currentSpzEntries[0]?.[1] ||
|
||||
null;
|
||||
const currentWorldImageUrl =
|
||||
currentWorldAsset?.panoUrl ||
|
||||
currentWorldAsset?.thumbnailUrl ||
|
||||
|
||||
@ -3,7 +3,7 @@ import { notFound } from "next/navigation";
|
||||
import { WorldSharePage } from "@/components/canvas/world3d/WorldSharePage";
|
||||
import { db } from "@/lib/db";
|
||||
import { model3dAsset } from "@/lib/db/schema";
|
||||
import { pickBestArchivedWorldLabsSplatUrl } from "@/lib/worldlabs/splat-utils";
|
||||
import { pickPreferredInlineWorldLabsSplatUrl } from "@/lib/worldlabs/splat-utils";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@ -86,7 +86,7 @@ export default async function SharedWorldPage({
|
||||
previewImageUrl: asset.previewImageUrl || assetPayload?.thumbnailUrl || null,
|
||||
prompt: asset.prompt || null,
|
||||
semanticsMetadata: assetPayload?.semanticsMetadata || null,
|
||||
splatUrl: pickBestArchivedWorldLabsSplatUrl(assetPayload?.spzUrls),
|
||||
splatUrl: pickPreferredInlineWorldLabsSplatUrl(assetPayload?.spzUrls),
|
||||
title: assetPayload?.displayName || asset.prompt || `World ${asset.taskId.slice(0, 8)}`,
|
||||
worldUrl: assetPayload?.worldUrl || null,
|
||||
};
|
||||
|
||||
@ -35,6 +35,7 @@ import {
|
||||
import {
|
||||
isArchivedWorldLabsAssetUrl,
|
||||
pickBestArchivedWorldLabsSplatUrl,
|
||||
pickPreferredInlineWorldLabsSplatUrl,
|
||||
} from "@/lib/worldlabs/splat-utils";
|
||||
import { getNodeContentScale } from "@/lib/canvas/node-content-scale";
|
||||
import {
|
||||
@ -201,9 +202,17 @@ export function World3DNode({
|
||||
|
||||
const activeView = useMemo(() => resolveActiveView(data), [data]);
|
||||
const availableViews = useMemo(() => resolveAvailableViews(data), [data]);
|
||||
const renderableSplatUrl = isArchivedWorldLabsAssetUrl(data.splatUrl)
|
||||
? data.splatUrl
|
||||
: undefined;
|
||||
const renderableSplatUrl = useMemo(() => {
|
||||
const assetPayloadSplatUrl = pickPreferredInlineWorldLabsSplatUrl(
|
||||
metadataAssetPayload.spzUrls as Record<string, string> | null | undefined
|
||||
);
|
||||
|
||||
if (isArchivedWorldLabsAssetUrl(assetPayloadSplatUrl)) {
|
||||
return assetPayloadSplatUrl;
|
||||
}
|
||||
|
||||
return isArchivedWorldLabsAssetUrl(data.splatUrl) ? data.splatUrl : undefined;
|
||||
}, [data.splatUrl, metadataAssetPayload.spzUrls]);
|
||||
const renderableMeshUrl = isArchivedWorldLabsAssetUrl(data.meshUrl)
|
||||
? data.meshUrl
|
||||
: undefined;
|
||||
|
||||
@ -166,8 +166,10 @@ function lerpSparkRevealProfile(
|
||||
};
|
||||
}
|
||||
|
||||
const SPLAT_REVEAL_DURATION_MS = 2600;
|
||||
const SPLAT_PARTICLE_SAMPLE_LIMIT = 1800;
|
||||
const SPLAT_REVEAL_DURATION_MS = 900;
|
||||
const SPLAT_PARTICLE_SAMPLE_LIMIT = 720;
|
||||
const SPLAT_WORLD_BOUNDS_SAMPLE_LIMIT = 1200;
|
||||
const SPLAT_FAST_LOAD_SKIP_REVEAL_MS = 1200;
|
||||
|
||||
const KEY_TO_MOVEMENT: Partial<Record<string, MovementCommand>> = {
|
||||
KeyW: "forward",
|
||||
@ -307,7 +309,7 @@ function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
||||
}
|
||||
|
||||
splatMesh.updateMatrixWorld(true);
|
||||
const targetCount = Math.min(totalSplats, 3200);
|
||||
const targetCount = Math.min(totalSplats, SPLAT_WORLD_BOUNDS_SAMPLE_LIMIT);
|
||||
const sampleStep = Math.max(Math.floor(totalSplats / targetCount), 1);
|
||||
const point = new THREE.Vector3();
|
||||
const xs: number[] = [];
|
||||
@ -1063,16 +1065,16 @@ function WorldSplatScene({
|
||||
onLoadStateChange?.("ready");
|
||||
|
||||
const buildParticleCloud = () => {
|
||||
if (cancelled) return;
|
||||
if (cancelled || revealPhaseRef.current === "hidden") return;
|
||||
setParticleCloudData(buildSplatParticleCloudData(splatMesh));
|
||||
};
|
||||
|
||||
if ("requestIdleCallback" in window) {
|
||||
idleCallbackId = window.requestIdleCallback(buildParticleCloud, {
|
||||
timeout: 900,
|
||||
timeout: 250,
|
||||
});
|
||||
} else {
|
||||
timeoutId = globalThis.setTimeout(buildParticleCloud, 120);
|
||||
timeoutId = globalThis.setTimeout(buildParticleCloud, 160);
|
||||
}
|
||||
});
|
||||
})
|
||||
@ -1364,15 +1366,20 @@ function WorldSplatViewer({
|
||||
const [loadState, setLoadState] = useState<"loading" | "ready" | "error">("loading");
|
||||
const [overlayPhase, setOverlayPhase] = useState<SplatLoadingPhase>("loading");
|
||||
const resolvedSplatUrl = useMemo(() => buildThreeAssetUrl(splatUrl), [splatUrl]);
|
||||
const loadStartedAtRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
loadStartedAtRef.current = performance.now();
|
||||
setLoadState("loading");
|
||||
setOverlayPhase("loading");
|
||||
}, [resolvedSplatUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (loadState === "ready") {
|
||||
setOverlayPhase("revealing");
|
||||
const elapsed = performance.now() - loadStartedAtRef.current;
|
||||
setOverlayPhase(
|
||||
elapsed <= SPLAT_FAST_LOAD_SKIP_REVEAL_MS ? "hidden" : "revealing"
|
||||
);
|
||||
}
|
||||
|
||||
if (loadState === "loading") {
|
||||
@ -1421,8 +1428,13 @@ function WorldSplatViewer({
|
||||
<Canvas
|
||||
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%)]"
|
||||
dpr={[1, 1.25]}
|
||||
frameloop="always"
|
||||
gl={{ antialias: false, preserveDrawingBuffer: true }}
|
||||
gl={{
|
||||
antialias: false,
|
||||
powerPreference: "high-performance",
|
||||
preserveDrawingBuffer: true,
|
||||
}}
|
||||
>
|
||||
<WorldSplatCaptureBridge onCaptureReady={onCaptureReady} />
|
||||
<WorldSplatScene
|
||||
|
||||
@ -59,6 +59,40 @@ export function pickBestWorldLabsSplatUrl(
|
||||
return pickBestWorldLabsSplatEntry(spzUrls)?.[1] || null;
|
||||
}
|
||||
|
||||
const INLINE_WORLDLABS_SPLAT_TARGET_SCORE = 1_500_000;
|
||||
|
||||
function pickClosestResolutionEntry(
|
||||
entries: Array<[string, string]>,
|
||||
targetScore: number
|
||||
) {
|
||||
if (!entries.length) return null;
|
||||
|
||||
const scoredEntries = entries
|
||||
.map((entry) => ({
|
||||
entry,
|
||||
score: parseResolutionScore(entry[0]),
|
||||
}))
|
||||
.filter(
|
||||
(item) =>
|
||||
Number.isFinite(item.score) &&
|
||||
item.score > 0 &&
|
||||
item.score !== Number.MAX_SAFE_INTEGER
|
||||
);
|
||||
|
||||
if (!scoredEntries.length) {
|
||||
return entries[0] || null;
|
||||
}
|
||||
|
||||
return scoredEntries
|
||||
.slice()
|
||||
.sort((left, right) => {
|
||||
const distanceDelta =
|
||||
Math.abs(left.score - targetScore) - Math.abs(right.score - targetScore);
|
||||
if (distanceDelta !== 0) return distanceDelta;
|
||||
return left.score - right.score;
|
||||
})[0]?.entry || null;
|
||||
}
|
||||
|
||||
const KNOWN_ARCHIVED_ASSET_HOSTS = [
|
||||
"blob.vercel-storage.com",
|
||||
"s3.1080p.hk",
|
||||
@ -129,3 +163,26 @@ export function pickBestArchivedWorldLabsSplatUrl(
|
||||
) {
|
||||
return getArchivedWorldLabsSplatEntries(spzUrls)[0]?.[1] || null;
|
||||
}
|
||||
|
||||
export function pickPreferredInlineWorldLabsSplatEntry(
|
||||
spzUrls?: Record<string, string> | null
|
||||
) {
|
||||
const archivedEntries = getArchivedWorldLabsSplatEntries(spzUrls);
|
||||
if (archivedEntries.length > 0) {
|
||||
return pickClosestResolutionEntry(
|
||||
archivedEntries,
|
||||
INLINE_WORLDLABS_SPLAT_TARGET_SCORE
|
||||
);
|
||||
}
|
||||
|
||||
return pickClosestResolutionEntry(
|
||||
getSortedWorldLabsSplatEntries(spzUrls),
|
||||
INLINE_WORLDLABS_SPLAT_TARGET_SCORE
|
||||
);
|
||||
}
|
||||
|
||||
export function pickPreferredInlineWorldLabsSplatUrl(
|
||||
spzUrls?: Record<string, string> | null
|
||||
) {
|
||||
return pickPreferredInlineWorldLabsSplatEntry(spzUrls)?.[1] || null;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user