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 runtime = "nodejs";
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
const THREE_ASSET_BROWSER_CACHE_CONTROL =
|
||||||
|
"private, max-age=86400, stale-while-revalidate=604800";
|
||||||
|
|
||||||
function isAllowedModelHost(hostname: string) {
|
function isAllowedModelHost(hostname: string) {
|
||||||
const normalized = hostname.toLowerCase();
|
const normalized = hostname.toLowerCase();
|
||||||
@ -144,7 +146,13 @@ export async function GET(request: Request) {
|
|||||||
const acceptRanges = upstream.headers.get("accept-ranges");
|
const acceptRanges = upstream.headers.get("accept-ranges");
|
||||||
if (acceptRanges) responseHeaders.set("accept-ranges", acceptRanges);
|
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) {
|
if (shouldRewriteGltf) {
|
||||||
const rewrittenPayload = rewriteGltfPayload(await upstream.text(), targetUrl);
|
const rewrittenPayload = rewriteGltfPayload(await upstream.text(), targetUrl);
|
||||||
|
|||||||
@ -91,6 +91,7 @@ import { cn, fetcher } from "@/lib/utils";
|
|||||||
import {
|
import {
|
||||||
getSortedWorldLabsSplatEntries,
|
getSortedWorldLabsSplatEntries,
|
||||||
pickBestArchivedWorldLabsSplatUrl,
|
pickBestArchivedWorldLabsSplatUrl,
|
||||||
|
pickPreferredInlineWorldLabsSplatUrl,
|
||||||
} from "@/lib/worldlabs/splat-utils";
|
} from "@/lib/worldlabs/splat-utils";
|
||||||
import {
|
import {
|
||||||
DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG,
|
DEFAULT_PUBLIC_THREED_MODEL_ALIAS_CONFIG,
|
||||||
@ -2605,7 +2606,10 @@ export default function CanvasModel3DPage() {
|
|||||||
? buildWorldShareUrl(currentTask.shareToken)
|
? buildWorldShareUrl(currentTask.shareToken)
|
||||||
: null;
|
: null;
|
||||||
const currentSpzEntries = getSortedWorldLabsSplatEntries(currentWorldAsset?.spzUrls);
|
const currentSpzEntries = getSortedWorldLabsSplatEntries(currentWorldAsset?.spzUrls);
|
||||||
const currentPrimarySpzUrl = currentSpzEntries[0]?.[1] || null;
|
const currentPrimarySpzUrl =
|
||||||
|
pickPreferredInlineWorldLabsSplatUrl(currentWorldAsset?.spzUrls) ||
|
||||||
|
currentSpzEntries[0]?.[1] ||
|
||||||
|
null;
|
||||||
const currentWorldImageUrl =
|
const currentWorldImageUrl =
|
||||||
currentWorldAsset?.panoUrl ||
|
currentWorldAsset?.panoUrl ||
|
||||||
currentWorldAsset?.thumbnailUrl ||
|
currentWorldAsset?.thumbnailUrl ||
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { notFound } from "next/navigation";
|
|||||||
import { WorldSharePage } from "@/components/canvas/world3d/WorldSharePage";
|
import { WorldSharePage } from "@/components/canvas/world3d/WorldSharePage";
|
||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import { model3dAsset } from "@/lib/db/schema";
|
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";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ export default async function SharedWorldPage({
|
|||||||
previewImageUrl: asset.previewImageUrl || assetPayload?.thumbnailUrl || null,
|
previewImageUrl: asset.previewImageUrl || assetPayload?.thumbnailUrl || null,
|
||||||
prompt: asset.prompt || null,
|
prompt: asset.prompt || null,
|
||||||
semanticsMetadata: assetPayload?.semanticsMetadata || null,
|
semanticsMetadata: assetPayload?.semanticsMetadata || null,
|
||||||
splatUrl: pickBestArchivedWorldLabsSplatUrl(assetPayload?.spzUrls),
|
splatUrl: pickPreferredInlineWorldLabsSplatUrl(assetPayload?.spzUrls),
|
||||||
title: assetPayload?.displayName || asset.prompt || `World ${asset.taskId.slice(0, 8)}`,
|
title: assetPayload?.displayName || asset.prompt || `World ${asset.taskId.slice(0, 8)}`,
|
||||||
worldUrl: assetPayload?.worldUrl || null,
|
worldUrl: assetPayload?.worldUrl || null,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
isArchivedWorldLabsAssetUrl,
|
isArchivedWorldLabsAssetUrl,
|
||||||
pickBestArchivedWorldLabsSplatUrl,
|
pickBestArchivedWorldLabsSplatUrl,
|
||||||
|
pickPreferredInlineWorldLabsSplatUrl,
|
||||||
} from "@/lib/worldlabs/splat-utils";
|
} from "@/lib/worldlabs/splat-utils";
|
||||||
import { getNodeContentScale } from "@/lib/canvas/node-content-scale";
|
import { getNodeContentScale } from "@/lib/canvas/node-content-scale";
|
||||||
import {
|
import {
|
||||||
@ -201,9 +202,17 @@ export function World3DNode({
|
|||||||
|
|
||||||
const activeView = useMemo(() => resolveActiveView(data), [data]);
|
const activeView = useMemo(() => resolveActiveView(data), [data]);
|
||||||
const availableViews = useMemo(() => resolveAvailableViews(data), [data]);
|
const availableViews = useMemo(() => resolveAvailableViews(data), [data]);
|
||||||
const renderableSplatUrl = isArchivedWorldLabsAssetUrl(data.splatUrl)
|
const renderableSplatUrl = useMemo(() => {
|
||||||
? data.splatUrl
|
const assetPayloadSplatUrl = pickPreferredInlineWorldLabsSplatUrl(
|
||||||
: undefined;
|
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)
|
const renderableMeshUrl = isArchivedWorldLabsAssetUrl(data.meshUrl)
|
||||||
? data.meshUrl
|
? data.meshUrl
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@ -166,8 +166,10 @@ function lerpSparkRevealProfile(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPLAT_REVEAL_DURATION_MS = 2600;
|
const SPLAT_REVEAL_DURATION_MS = 900;
|
||||||
const SPLAT_PARTICLE_SAMPLE_LIMIT = 1800;
|
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>> = {
|
const KEY_TO_MOVEMENT: Partial<Record<string, MovementCommand>> = {
|
||||||
KeyW: "forward",
|
KeyW: "forward",
|
||||||
@ -307,7 +309,7 @@ function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
splatMesh.updateMatrixWorld(true);
|
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 sampleStep = Math.max(Math.floor(totalSplats / targetCount), 1);
|
||||||
const point = new THREE.Vector3();
|
const point = new THREE.Vector3();
|
||||||
const xs: number[] = [];
|
const xs: number[] = [];
|
||||||
@ -1063,16 +1065,16 @@ function WorldSplatScene({
|
|||||||
onLoadStateChange?.("ready");
|
onLoadStateChange?.("ready");
|
||||||
|
|
||||||
const buildParticleCloud = () => {
|
const buildParticleCloud = () => {
|
||||||
if (cancelled) return;
|
if (cancelled || revealPhaseRef.current === "hidden") return;
|
||||||
setParticleCloudData(buildSplatParticleCloudData(splatMesh));
|
setParticleCloudData(buildSplatParticleCloudData(splatMesh));
|
||||||
};
|
};
|
||||||
|
|
||||||
if ("requestIdleCallback" in window) {
|
if ("requestIdleCallback" in window) {
|
||||||
idleCallbackId = window.requestIdleCallback(buildParticleCloud, {
|
idleCallbackId = window.requestIdleCallback(buildParticleCloud, {
|
||||||
timeout: 900,
|
timeout: 250,
|
||||||
});
|
});
|
||||||
} else {
|
} 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 [loadState, setLoadState] = useState<"loading" | "ready" | "error">("loading");
|
||||||
const [overlayPhase, setOverlayPhase] = useState<SplatLoadingPhase>("loading");
|
const [overlayPhase, setOverlayPhase] = useState<SplatLoadingPhase>("loading");
|
||||||
const resolvedSplatUrl = useMemo(() => buildThreeAssetUrl(splatUrl), [splatUrl]);
|
const resolvedSplatUrl = useMemo(() => buildThreeAssetUrl(splatUrl), [splatUrl]);
|
||||||
|
const loadStartedAtRef = useRef(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
loadStartedAtRef.current = performance.now();
|
||||||
setLoadState("loading");
|
setLoadState("loading");
|
||||||
setOverlayPhase("loading");
|
setOverlayPhase("loading");
|
||||||
}, [resolvedSplatUrl]);
|
}, [resolvedSplatUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loadState === "ready") {
|
if (loadState === "ready") {
|
||||||
setOverlayPhase("revealing");
|
const elapsed = performance.now() - loadStartedAtRef.current;
|
||||||
|
setOverlayPhase(
|
||||||
|
elapsed <= SPLAT_FAST_LOAD_SKIP_REVEAL_MS ? "hidden" : "revealing"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadState === "loading") {
|
if (loadState === "loading") {
|
||||||
@ -1421,8 +1428,13 @@ 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]}
|
||||||
frameloop="always"
|
frameloop="always"
|
||||||
gl={{ antialias: false, preserveDrawingBuffer: true }}
|
gl={{
|
||||||
|
antialias: false,
|
||||||
|
powerPreference: "high-performance",
|
||||||
|
preserveDrawingBuffer: true,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<WorldSplatCaptureBridge onCaptureReady={onCaptureReady} />
|
<WorldSplatCaptureBridge onCaptureReady={onCaptureReady} />
|
||||||
<WorldSplatScene
|
<WorldSplatScene
|
||||||
|
|||||||
@ -59,6 +59,40 @@ export function pickBestWorldLabsSplatUrl(
|
|||||||
return pickBestWorldLabsSplatEntry(spzUrls)?.[1] || null;
|
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 = [
|
const KNOWN_ARCHIVED_ASSET_HOSTS = [
|
||||||
"blob.vercel-storage.com",
|
"blob.vercel-storage.com",
|
||||||
"s3.1080p.hk",
|
"s3.1080p.hk",
|
||||||
@ -129,3 +163,26 @@ export function pickBestArchivedWorldLabsSplatUrl(
|
|||||||
) {
|
) {
|
||||||
return getArchivedWorldLabsSplatEntries(spzUrls)[0]?.[1] || null;
|
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