feat: improve real splat world reveal
This commit is contained in:
parent
62b5b75ec9
commit
9281df46fe
@ -42,7 +42,13 @@ type MovementCommand =
|
||||
| "down";
|
||||
|
||||
type MovementState = Record<MovementCommand, boolean>;
|
||||
type SplatLoadingPhase = "loading" | "fading" | "hidden";
|
||||
type SplatLoadingPhase = "loading" | "revealing" | "hidden";
|
||||
type SplatRevealCloudData = {
|
||||
count: number;
|
||||
colors: Float32Array;
|
||||
startPositions: Float32Array;
|
||||
targetPositions: Float32Array;
|
||||
};
|
||||
|
||||
type World3DViewerProps = {
|
||||
mode: World3DViewMode;
|
||||
@ -168,81 +174,143 @@ function WorldEmptyState({
|
||||
);
|
||||
}
|
||||
|
||||
const SPLAT_LOADING_PARTICLES = [
|
||||
{ x: 8, y: 18, size: 5, dx: 18, dy: -22, delay: 0.1, duration: 4.6 },
|
||||
{ x: 18, y: 64, size: 7, dx: 26, dy: -18, delay: 0.4, duration: 5.2 },
|
||||
{ x: 26, y: 34, size: 4, dx: -12, dy: -28, delay: 0.7, duration: 4.8 },
|
||||
{ x: 34, y: 78, size: 6, dx: 14, dy: -24, delay: 1.1, duration: 5.6 },
|
||||
{ x: 44, y: 16, size: 8, dx: -18, dy: 18, delay: 0.3, duration: 5.4 },
|
||||
{ x: 52, y: 56, size: 5, dx: 20, dy: -26, delay: 0.9, duration: 4.9 },
|
||||
{ x: 61, y: 28, size: 6, dx: -22, dy: -16, delay: 1.4, duration: 5.8 },
|
||||
{ x: 68, y: 72, size: 4, dx: 16, dy: -20, delay: 0.2, duration: 4.7 },
|
||||
{ x: 76, y: 42, size: 8, dx: -14, dy: 22, delay: 1.2, duration: 5.1 },
|
||||
{ x: 84, y: 62, size: 5, dx: 22, dy: -14, delay: 0.6, duration: 5.7 },
|
||||
{ x: 91, y: 24, size: 6, dx: -16, dy: 18, delay: 1.5, duration: 4.5 },
|
||||
{ x: 12, y: 44, size: 4, dx: 12, dy: 14, delay: 1.8, duration: 5.3 },
|
||||
] as const;
|
||||
|
||||
const SPLAT_ASSEMBLY_POINT_COUNT = 220;
|
||||
const SPLAT_ASSEMBLY_COLORS = ["#67e8f9", "#93c5fd", "#f8fafc", "#22d3ee"] as const;
|
||||
const REAL_SPLAT_REVEAL_SAMPLE_LIMIT = 3600;
|
||||
|
||||
function seededUnit(index: number, salt: number) {
|
||||
const value = Math.sin(index * 12.9898 + salt * 78.233) * 43758.5453;
|
||||
return value - Math.floor(value);
|
||||
}
|
||||
|
||||
function SplatAssemblyCloud({
|
||||
function buildSplatRevealCloudData(
|
||||
splatMesh: SparkSplatMesh,
|
||||
sampleLimit = REAL_SPLAT_REVEAL_SAMPLE_LIMIT
|
||||
): SplatRevealCloudData | null {
|
||||
const totalSplats = Math.max(0, splatMesh.packedSplats?.numSplats || 0);
|
||||
if (!totalSplats) return null;
|
||||
|
||||
const sampleStep = Math.max(1, Math.floor(totalSplats / sampleLimit));
|
||||
const sampled: Array<{
|
||||
center: THREE.Vector3;
|
||||
color: THREE.Color;
|
||||
opacity: number;
|
||||
}> = [];
|
||||
const bounds = new THREE.Box3();
|
||||
|
||||
splatMesh.forEachSplat((index, center, _scales, _quaternion, opacity, color) => {
|
||||
if (sampled.length >= sampleLimit) return;
|
||||
if (opacity < 0.04) return;
|
||||
if (index % sampleStep !== 0) return;
|
||||
|
||||
const target = center.clone();
|
||||
bounds.expandByPoint(target);
|
||||
sampled.push({
|
||||
center: target,
|
||||
color: color.clone(),
|
||||
opacity,
|
||||
});
|
||||
});
|
||||
|
||||
if (!sampled.length) return null;
|
||||
|
||||
const sceneCenter = bounds.getCenter(new THREE.Vector3());
|
||||
const sceneBoundsSize = bounds.getSize(new THREE.Vector3());
|
||||
const sceneSize = sceneBoundsSize.length();
|
||||
const sceneRadius = Math.max(sceneSize * 0.42, 1.6);
|
||||
const white = new THREE.Color("#fff4de");
|
||||
const colors = new Float32Array(sampled.length * 3);
|
||||
const startPositions = new Float32Array(sampled.length * 3);
|
||||
const targetPositions = new Float32Array(sampled.length * 3);
|
||||
|
||||
sampled.forEach((sample, index) => {
|
||||
const stride = index * 3;
|
||||
const direction = sample.center.clone().sub(sceneCenter);
|
||||
if (direction.lengthSq() < 1e-4) {
|
||||
direction.set(
|
||||
seededUnit(index, 10) - 0.5,
|
||||
seededUnit(index, 11) - 0.35,
|
||||
seededUnit(index, 12) - 0.5
|
||||
);
|
||||
}
|
||||
|
||||
direction.normalize();
|
||||
const swirlAxis = new THREE.Vector3(
|
||||
seededUnit(index, 13) - 0.5,
|
||||
seededUnit(index, 14) - 0.5,
|
||||
seededUnit(index, 15) - 0.5
|
||||
);
|
||||
if (swirlAxis.lengthSq() < 1e-4) {
|
||||
swirlAxis.set(0.3, 1, 0.2);
|
||||
}
|
||||
swirlAxis.normalize();
|
||||
|
||||
const tangent = new THREE.Vector3().crossVectors(direction, swirlAxis);
|
||||
if (tangent.lengthSq() < 1e-4) {
|
||||
tangent.set(1, 0, 0);
|
||||
} else {
|
||||
tangent.normalize();
|
||||
}
|
||||
|
||||
const start = sample.center
|
||||
.clone()
|
||||
.sub(sceneCenter)
|
||||
.multiplyScalar(1.08 + seededUnit(index, 16) * 0.38)
|
||||
.add(sceneCenter)
|
||||
.addScaledVector(
|
||||
tangent,
|
||||
sceneRadius * (0.018 + seededUnit(index, 17) * 0.085)
|
||||
)
|
||||
.addScaledVector(
|
||||
direction,
|
||||
sceneRadius * (0.035 + seededUnit(index, 18) * 0.12)
|
||||
)
|
||||
.add(
|
||||
new THREE.Vector3(
|
||||
0,
|
||||
sceneBoundsSize.y * (seededUnit(index, 19) - 0.5) * 0.16,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
const displayColor = sample.color
|
||||
.clone()
|
||||
.lerp(white, 0.42)
|
||||
.multiplyScalar(0.88 + Math.min(sample.opacity, 1) * 0.24);
|
||||
|
||||
targetPositions[stride] = sample.center.x;
|
||||
targetPositions[stride + 1] = sample.center.y;
|
||||
targetPositions[stride + 2] = sample.center.z;
|
||||
startPositions[stride] = start.x;
|
||||
startPositions[stride + 1] = start.y;
|
||||
startPositions[stride + 2] = start.z;
|
||||
colors[stride] = displayColor.r;
|
||||
colors[stride + 1] = displayColor.g;
|
||||
colors[stride + 2] = displayColor.b;
|
||||
});
|
||||
|
||||
return {
|
||||
count: sampled.length,
|
||||
colors,
|
||||
startPositions,
|
||||
targetPositions,
|
||||
};
|
||||
}
|
||||
|
||||
function SplatRevealCloud({
|
||||
data,
|
||||
phase,
|
||||
}: {
|
||||
data: SplatRevealCloudData;
|
||||
phase: Exclude<SplatLoadingPhase, "hidden">;
|
||||
}) {
|
||||
const pointsRef = useRef<THREE.Points>(null);
|
||||
const materialRef = useRef<THREE.PointsMaterial>(null);
|
||||
const positionBuffer = useMemo(
|
||||
() => data.startPositions.slice(),
|
||||
[data.startPositions]
|
||||
);
|
||||
const geometryRef = useRef<THREE.BufferGeometry>(null);
|
||||
const phaseRef = useRef(phase);
|
||||
const phaseStartedAtRef = useRef(0);
|
||||
|
||||
const seeds = useMemo(
|
||||
() =>
|
||||
Array.from({ length: SPLAT_ASSEMBLY_POINT_COUNT }, (_, index) => {
|
||||
const spread = seededUnit(index, 1);
|
||||
const tilt = seededUnit(index, 2);
|
||||
const orbit = seededUnit(index, 3);
|
||||
const colorIndex = Math.floor(
|
||||
seededUnit(index, 4) * SPLAT_ASSEMBLY_COLORS.length
|
||||
);
|
||||
|
||||
return {
|
||||
target: new THREE.Vector3(
|
||||
(spread - 0.5) * 4.8,
|
||||
0.45 + tilt * 2.6,
|
||||
(orbit - 0.5) * 4.4
|
||||
),
|
||||
orbitRadius: 0.4 + seededUnit(index, 5) * 2.2,
|
||||
orbitSpeed: 0.2 + seededUnit(index, 6) * 0.8,
|
||||
angle: seededUnit(index, 7) * Math.PI * 2,
|
||||
verticalAmplitude: 0.08 + seededUnit(index, 8) * 0.32,
|
||||
drift: 0.65 + seededUnit(index, 9) * 0.75,
|
||||
color: new THREE.Color(
|
||||
SPLAT_ASSEMBLY_COLORS[colorIndex] || SPLAT_ASSEMBLY_COLORS[0]
|
||||
),
|
||||
};
|
||||
}),
|
||||
[]
|
||||
);
|
||||
const positions = useMemo(
|
||||
() => new Float32Array(SPLAT_ASSEMBLY_POINT_COUNT * 3),
|
||||
[]
|
||||
);
|
||||
const colors = useMemo(() => {
|
||||
const result = new Float32Array(SPLAT_ASSEMBLY_POINT_COUNT * 3);
|
||||
seeds.forEach((seed, index) => {
|
||||
result[index * 3] = seed.color.r;
|
||||
result[index * 3 + 1] = seed.color.g;
|
||||
result[index * 3 + 2] = seed.color.b;
|
||||
});
|
||||
return result;
|
||||
}, [seeds]);
|
||||
const phaseStartedAtRef = useRef(performance.now());
|
||||
|
||||
useEffect(() => {
|
||||
if (phaseRef.current !== phase) {
|
||||
@ -263,69 +331,70 @@ function SplatAssemblyCloud({
|
||||
if (!geometry || !material) return;
|
||||
|
||||
const elapsed = state.clock.getElapsedTime();
|
||||
const fadeElapsed =
|
||||
phaseRef.current === "fading"
|
||||
? Math.min((performance.now() - phaseStartedAtRef.current) / 850, 1)
|
||||
: 0;
|
||||
const baseOpacity =
|
||||
phaseRef.current === "loading"
|
||||
? 0.78
|
||||
: THREE.MathUtils.lerp(0.78, 0, fadeElapsed);
|
||||
material.opacity = baseOpacity;
|
||||
material.size = THREE.MathUtils.lerp(0.09, 0.02, fadeElapsed);
|
||||
const rawProgress = Math.min(
|
||||
(performance.now() - phaseStartedAtRef.current) / 1450,
|
||||
1
|
||||
);
|
||||
const easedProgress = 1 - Math.pow(1 - rawProgress, 3);
|
||||
const positionAttribute = geometry.getAttribute("position");
|
||||
const positions = positionAttribute.array as Float32Array;
|
||||
|
||||
seeds.forEach((seed, index) => {
|
||||
for (let index = 0; index < data.count; index += 1) {
|
||||
const stride = index * 3;
|
||||
const orbitAngle = elapsed * seed.orbitSpeed + seed.angle;
|
||||
const phaseMix =
|
||||
phaseRef.current === "loading"
|
||||
? 0.22 + 0.2 * Math.sin(elapsed * 0.65 + seed.angle)
|
||||
: THREE.MathUtils.lerp(0.72, 1.02, fadeElapsed);
|
||||
const orbitRadius =
|
||||
seed.orbitRadius *
|
||||
(phaseRef.current === "loading"
|
||||
? 0.95 + 0.08 * Math.sin(elapsed * 1.3 + seed.angle)
|
||||
: Math.max(0, 0.45 * (1 - fadeElapsed)));
|
||||
const verticalWave =
|
||||
Math.sin(elapsed * seed.drift + seed.angle * 1.4) *
|
||||
seed.verticalAmplitude *
|
||||
(phaseRef.current === "loading" ? 1 : 1 - fadeElapsed);
|
||||
|
||||
const swirl =
|
||||
Math.sin(elapsed * 2.4 + index * 0.17) * (1 - easedProgress) * 0.018;
|
||||
positions[stride] =
|
||||
seed.target.x * phaseMix + Math.cos(orbitAngle) * orbitRadius;
|
||||
THREE.MathUtils.lerp(
|
||||
data.startPositions[stride],
|
||||
data.targetPositions[stride],
|
||||
easedProgress
|
||||
) +
|
||||
swirl;
|
||||
positions[stride + 1] =
|
||||
seed.target.y * phaseMix +
|
||||
verticalWave +
|
||||
Math.sin(orbitAngle * 0.7) * orbitRadius * 0.18;
|
||||
THREE.MathUtils.lerp(
|
||||
data.startPositions[stride + 1],
|
||||
data.targetPositions[stride + 1],
|
||||
easedProgress
|
||||
) +
|
||||
swirl * 0.45;
|
||||
positions[stride + 2] =
|
||||
seed.target.z * phaseMix + Math.sin(orbitAngle) * orbitRadius;
|
||||
});
|
||||
THREE.MathUtils.lerp(
|
||||
data.startPositions[stride + 2],
|
||||
data.targetPositions[stride + 2],
|
||||
easedProgress
|
||||
) -
|
||||
swirl * 0.65;
|
||||
}
|
||||
|
||||
geometry.attributes.position.needsUpdate = true;
|
||||
positionAttribute.needsUpdate = true;
|
||||
material.opacity = THREE.MathUtils.lerp(0.9, 0, easedProgress);
|
||||
material.size = THREE.MathUtils.lerp(0.04, 0.009, easedProgress);
|
||||
});
|
||||
|
||||
return (
|
||||
<points frustumCulled={false} ref={pointsRef} renderOrder={2}>
|
||||
<bufferGeometry ref={geometryRef}>
|
||||
<bufferAttribute
|
||||
attach="attributes-position"
|
||||
args={[positions, 3]}
|
||||
<group rotation={[Math.PI, 0, 0]}>
|
||||
<points
|
||||
frustumCulled={false}
|
||||
ref={pointsRef}
|
||||
renderOrder={2}
|
||||
>
|
||||
<bufferGeometry ref={geometryRef}>
|
||||
<bufferAttribute attach="attributes-position" args={[positionBuffer, 3]} />
|
||||
<bufferAttribute attach="attributes-color" args={[data.colors, 3]} />
|
||||
</bufferGeometry>
|
||||
<pointsMaterial
|
||||
blending={THREE.AdditiveBlending}
|
||||
depthTest={false}
|
||||
depthWrite={false}
|
||||
ref={materialRef}
|
||||
size={0.04}
|
||||
sizeAttenuation
|
||||
toneMapped={false}
|
||||
transparent
|
||||
vertexColors
|
||||
/>
|
||||
<bufferAttribute
|
||||
attach="attributes-color"
|
||||
args={[colors, 3]}
|
||||
/>
|
||||
</bufferGeometry>
|
||||
<pointsMaterial
|
||||
blending={THREE.AdditiveBlending}
|
||||
depthWrite={false}
|
||||
ref={materialRef}
|
||||
size={0.09}
|
||||
sizeAttenuation
|
||||
transparent
|
||||
vertexColors
|
||||
/>
|
||||
</points>
|
||||
</points>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
@ -336,99 +405,48 @@ function WorldSplatLoadingOverlay({
|
||||
previewImageUrl?: string | null;
|
||||
phase: Exclude<SplatLoadingPhase, "hidden">;
|
||||
}) {
|
||||
const isFading = phase === "fading";
|
||||
const isRevealing = phase === "revealing";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute inset-0 overflow-hidden transition-all duration-700",
|
||||
isFading ? "scale-[1.02] opacity-0 blur-[1px]" : "opacity-100"
|
||||
"pointer-events-none absolute inset-0 overflow-hidden transition-all duration-500",
|
||||
isRevealing ? "opacity-0" : "opacity-100"
|
||||
)}
|
||||
>
|
||||
{previewImageUrl ? (
|
||||
<img
|
||||
alt="世界预览"
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-35 blur-[2px] saturate-[0.85]"
|
||||
className="absolute inset-0 h-full w-full object-cover opacity-[0.16] blur-[8px] saturate-[0.7]"
|
||||
src={previewImageUrl}
|
||||
/>
|
||||
) : null}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(14,165,233,0.16),_rgba(1,1,1,0)_34%),linear-gradient(180deg,rgba(2,6,23,0.72)_0%,rgba(2,6,23,0.9)_100%)]" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(120deg,transparent_0%,rgba(255,255,255,0.05)_44%,transparent_58%)] [background-size:220%_100%] animate-[worldlabs-scan_3.6s_ease-in-out_infinite]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_rgba(255,245,220,0.08)_0%,_rgba(11,15,25,0.18)_18%,_rgba(2,6,23,0.72)_54%,_rgba(1,2,6,0.95)_100%)]" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(5,10,20,0.16)_0%,rgba(5,10,20,0.78)_100%)]" />
|
||||
<div className="absolute inset-0 bg-[linear-gradient(90deg,transparent_0%,rgba(255,248,220,0.045)_48%,transparent_58%)] [background-size:220%_100%] animate-[worldlabs-scan_3.1s_ease-in-out_infinite]" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_50%_52%,transparent_0%,transparent_28%,rgba(1,2,6,0.12)_58%,rgba(1,2,6,0.68)_100%)]" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-48 bg-[linear-gradient(180deg,rgba(2,6,23,0)_0%,rgba(2,6,23,0.92)_100%)]" />
|
||||
|
||||
<div className="absolute inset-0">
|
||||
{SPLAT_LOADING_PARTICLES.map((particle, index) => (
|
||||
<span
|
||||
className="absolute rounded-full bg-cyan-300/70 shadow-[0_0_24px_rgba(103,232,249,0.45)]"
|
||||
key={`${particle.x}-${particle.y}-${index}`}
|
||||
style={
|
||||
{
|
||||
left: `${particle.x}%`,
|
||||
top: `${particle.y}%`,
|
||||
width: `${particle.size}px`,
|
||||
height: `${particle.size}px`,
|
||||
"--worldlabs-dx": `${particle.dx}px`,
|
||||
"--worldlabs-dy": `${particle.dy}px`,
|
||||
animation: `worldlabs-particle ${particle.duration}s ease-in-out ${particle.delay}s infinite`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="absolute inset-x-0 bottom-0 h-40 bg-[linear-gradient(180deg,rgba(2,6,23,0)_0%,rgba(2,6,23,0.88)_100%)]" />
|
||||
|
||||
<div className="absolute inset-0 flex items-center justify-center p-6">
|
||||
<div className="w-full max-w-lg rounded-3xl border border-white/10 bg-black/35 px-6 py-5 text-center shadow-[0_24px_100px_rgba(0,0,0,0.45)] backdrop-blur-xl">
|
||||
<div className="mb-3 inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-cyan-400/20 bg-cyan-400/10">
|
||||
<Sparkles className="h-6 w-6 text-cyan-200" />
|
||||
<div className="absolute bottom-5 left-5 flex items-end gap-3">
|
||||
<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">
|
||||
<Sparkles className="h-3.5 w-3.5 text-cyan-200" />
|
||||
<span>{isRevealing ? "真实点云正在成形" : "正在载入真实 Gaussian Splats"}</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-lg font-semibold text-white">
|
||||
{isFading ? "正在显露 3D 世界细节" : "正在构建 3D 世界体积"}
|
||||
</p>
|
||||
<p className="mx-auto max-w-md text-sm leading-6 text-zinc-300">
|
||||
{isFading
|
||||
? "Gaussian Splat 已加载完成,正在把真实细节从过渡层中释放出来。"
|
||||
: "SPZ 会先以粗糙体积快速占位,再逐步补齐细节。先用粒子过渡能让加载观感更顺。"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-center gap-2 text-[11px] text-cyan-100/85">
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
<span>{isFading ? "即将完成" : "初始化 Splat 渲染器"}</span>
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-zinc-400">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span>{isRevealing ? "点云聚合中" : "SPZ 数据准备中"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes worldlabs-particle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 0, 0) scale(0.4);
|
||||
}
|
||||
20% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
55% {
|
||||
opacity: 0.8;
|
||||
transform: translate3d(var(--worldlabs-dx), var(--worldlabs-dy), 0) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate3d(
|
||||
calc(var(--worldlabs-dx) * 1.45),
|
||||
calc(var(--worldlabs-dy) * 1.45),
|
||||
0
|
||||
)
|
||||
scale(1.35);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes worldlabs-scan {
|
||||
0% {
|
||||
background-position: 115% 0;
|
||||
background-position: 120% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -15% 0;
|
||||
background-position: -20% 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
@ -653,7 +671,7 @@ function HotspotBillboard({
|
||||
|
||||
function WorldSplatScene({
|
||||
splatUrl,
|
||||
assemblyPhase = "loading",
|
||||
revealPhase = "loading",
|
||||
firstPerson = false,
|
||||
hotspots = [],
|
||||
interactionMode = "navigate",
|
||||
@ -663,7 +681,7 @@ function WorldSplatScene({
|
||||
onLoadStateChange,
|
||||
}: {
|
||||
splatUrl: string;
|
||||
assemblyPhase?: SplatLoadingPhase;
|
||||
revealPhase?: SplatLoadingPhase;
|
||||
firstPerson?: boolean;
|
||||
hotspots?: World3DHotspot[];
|
||||
interactionMode?: "navigate" | "hotspot";
|
||||
@ -676,15 +694,19 @@ function WorldSplatScene({
|
||||
const orbitControlsRef = useRef<any>(null);
|
||||
const [virtualMovement, setVirtualMovement] =
|
||||
useState<MovementState>(createMovementState);
|
||||
const [revealCloudData, setRevealCloudData] =
|
||||
useState<SplatRevealCloudData | null>(null);
|
||||
const semanticFacts = useMemo(
|
||||
() => getSemanticFacts(semanticsMetadata),
|
||||
[semanticsMetadata]
|
||||
);
|
||||
const revealPhaseRef = useRef<SplatLoadingPhase>(revealPhase);
|
||||
const revealStartedAtRef = useRef(performance.now());
|
||||
const sparkRenderer = useMemo(
|
||||
() =>
|
||||
new SparkRenderer({
|
||||
renderer,
|
||||
blurAmount: 0.2,
|
||||
blurAmount: 0.18,
|
||||
maxStdDev: Math.sqrt(7),
|
||||
}),
|
||||
[renderer]
|
||||
@ -694,18 +716,36 @@ function WorldSplatScene({
|
||||
useEffect(() => {
|
||||
sparkRenderer.frustumCulled = false;
|
||||
sparkRenderer.renderOrder = 10;
|
||||
sparkRenderer.minAlpha = 0.02;
|
||||
sparkRenderer.preBlurAmount = 0.04;
|
||||
return () => {
|
||||
sparkRenderer.removeFromParent();
|
||||
};
|
||||
}, [sparkRenderer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (revealPhaseRef.current !== revealPhase) {
|
||||
revealPhaseRef.current = revealPhase;
|
||||
revealStartedAtRef.current = performance.now();
|
||||
}
|
||||
}, [revealPhase]);
|
||||
|
||||
useEffect(() => {
|
||||
splatMesh.frustumCulled = false;
|
||||
splatMesh.rotation.x = Math.PI;
|
||||
splatMesh.opacity = 0;
|
||||
sparkRenderer.blurAmount = 0.95;
|
||||
sparkRenderer.preBlurAmount = 0.28;
|
||||
sparkRenderer.minAlpha = 0.08;
|
||||
sparkRenderer.maxStdDev = Math.sqrt(10);
|
||||
setRevealCloudData(null);
|
||||
|
||||
let cancelled = false;
|
||||
splatMesh.initialized
|
||||
.then(() => {
|
||||
if (!cancelled) onLoadStateChange?.("ready");
|
||||
if (cancelled) return;
|
||||
setRevealCloudData(buildSplatRevealCloudData(splatMesh));
|
||||
onLoadStateChange?.("ready");
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) onLoadStateChange?.("error");
|
||||
@ -714,9 +754,40 @@ function WorldSplatScene({
|
||||
cancelled = true;
|
||||
splatMesh.dispose();
|
||||
};
|
||||
}, [onLoadStateChange, splatMesh]);
|
||||
}, [onLoadStateChange, sparkRenderer, splatMesh]);
|
||||
|
||||
useFrame(() => {
|
||||
const phase = revealPhaseRef.current;
|
||||
const revealElapsed =
|
||||
phase === "revealing"
|
||||
? Math.min((performance.now() - revealStartedAtRef.current) / 1250, 1)
|
||||
: 0;
|
||||
const easedReveal = 1 - Math.pow(1 - revealElapsed, 3);
|
||||
|
||||
if (phase === "loading") {
|
||||
splatMesh.opacity = 0;
|
||||
sparkRenderer.blurAmount = 0.95;
|
||||
sparkRenderer.preBlurAmount = 0.28;
|
||||
sparkRenderer.minAlpha = 0.08;
|
||||
sparkRenderer.maxStdDev = Math.sqrt(10);
|
||||
} else if (phase === "revealing") {
|
||||
splatMesh.opacity = THREE.MathUtils.lerp(0.02, 1, easedReveal);
|
||||
sparkRenderer.blurAmount = THREE.MathUtils.lerp(0.95, 0.16, easedReveal);
|
||||
sparkRenderer.preBlurAmount = THREE.MathUtils.lerp(0.28, 0.04, easedReveal);
|
||||
sparkRenderer.minAlpha = THREE.MathUtils.lerp(0.08, 0.02, easedReveal);
|
||||
sparkRenderer.maxStdDev = THREE.MathUtils.lerp(
|
||||
Math.sqrt(10),
|
||||
Math.sqrt(7),
|
||||
easedReveal
|
||||
);
|
||||
} else {
|
||||
splatMesh.opacity = 1;
|
||||
sparkRenderer.blurAmount = 0.16;
|
||||
sparkRenderer.preBlurAmount = 0.04;
|
||||
sparkRenderer.minAlpha = 0.02;
|
||||
sparkRenderer.maxStdDev = Math.sqrt(7);
|
||||
}
|
||||
|
||||
sparkRenderer.needsUpdate = true;
|
||||
});
|
||||
|
||||
@ -755,8 +826,8 @@ function WorldSplatScene({
|
||||
position={[0, groundPlaneOffset, 0]}
|
||||
/>
|
||||
) : null}
|
||||
{assemblyPhase !== "hidden" ? (
|
||||
<SplatAssemblyCloud phase={assemblyPhase} />
|
||||
{revealPhase !== "hidden" && revealCloudData ? (
|
||||
<SplatRevealCloud data={revealCloudData} phase={revealPhase} />
|
||||
) : null}
|
||||
<Suspense fallback={null}>
|
||||
<primitive object={sparkRenderer}>
|
||||
@ -870,10 +941,10 @@ function WorldSplatViewer({
|
||||
|
||||
useEffect(() => {
|
||||
if (loadState === "ready") {
|
||||
setOverlayPhase("fading");
|
||||
setOverlayPhase("revealing");
|
||||
const timer = window.setTimeout(() => {
|
||||
setOverlayPhase("hidden");
|
||||
}, 720);
|
||||
}, 1250);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
|
||||
@ -917,13 +988,13 @@ function WorldSplatViewer({
|
||||
gl={{ antialias: false }}
|
||||
>
|
||||
<WorldSplatScene
|
||||
assemblyPhase={overlayPhase}
|
||||
firstPerson={firstPerson}
|
||||
hotspots={hotspots}
|
||||
interactionMode={interactionMode}
|
||||
onHotspotSelect={onHotspotSelect}
|
||||
onLoadStateChange={setLoadState}
|
||||
onPick={onPick}
|
||||
revealPhase={overlayPhase}
|
||||
semanticsMetadata={semanticsMetadata}
|
||||
splatUrl={splatUrl}
|
||||
/>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user