← All primitives
SplatScene
FrontierA real-time Gaussian-splat capture rendered in R3F via World Labs' Spark — photoreal 3D content (the kind AI tools like Marble export) running live in the browser.
No asset ships with this primitive — self-host a .spz/.ply/.splat under /public and pass it via the `url` prop (e.g. <SplatScene url="/splats/yours.spz" />). Needs WebGL2; render via next/dynamic ssr:false.
$
npx shadcn@latest add https://prism.icglabs.co/r/splat-scene.jsonDependencies:three@react-three/fiber@react-three/drei@sparkjsdev/spark
View raw manifest →components/prism/SplatScene.tsx
"use client";
import { Component, useEffect, useRef, useState } from "react";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { SparkRenderer, SplatMesh } from "@sparkjsdev/spark";
/**
* SplatScene — a real-time Gaussian-splat capture rendered in R3F via World Labs'
* Spark (the maintained successor to the archived Luma viewer). Photoreal 3D
* content (the kind AI tools like Marble export) running live in the browser.
*
* Built imperatively to match Spark's official vanilla-three example exactly:
* a SparkRenderer + a SplatMesh added to the scene as siblings. Self-contained
* fallback, `url` prop, honors prefers-reduced-motion.
*/
function Splats({ url, reduce }: { url: string; reduce: boolean }) {
const scene = useThree((s) => s.scene);
const gl = useThree((s) => s.gl);
const meshRef = useRef<SplatMesh | null>(null);
useEffect(() => {
const spark = new SparkRenderer({ renderer: gl });
scene.add(spark);
const mesh = new SplatMesh({ url });
// capture is recorded upside-down — quaternion (1,0,0,0) = 180° about X
mesh.quaternion.set(1, 0, 0, 0);
mesh.position.set(0, 0, 0);
scene.add(mesh);
meshRef.current = mesh;
return () => {
scene.remove(mesh);
scene.remove(spark);
meshRef.current = null;
};
}, [url, scene, gl]);
useFrame((_, dt) => {
if (meshRef.current && !reduce) meshRef.current.rotation.y += 0.35 * dt;
});
return null;
}
function SplatFallback({ label }: { label: string }) {
return (
<div
className="relative grid h-full w-full place-items-center overflow-hidden rounded-2xl border border-line"
style={{ background: "radial-gradient(120% 90% at 70% 10%, rgba(139,92,255,0.18), transparent 60%), #0c0c13" }}
>
<span className="relative z-10 text-sm text-muted">{label}</span>
</div>
);
}
/** Catches a failed splat load (e.g. missing asset) and shows the fallback. */
class CanvasBoundary extends Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ failed: boolean }
> {
state = { failed: false };
static getDerivedStateFromError() {
return { failed: true };
}
render() {
return this.state.failed ? this.props.fallback : this.props.children;
}
}
export function SplatScene({ url = "/assets/splats/butterfly.spz" }: { url?: string }) {
const [supported, setSupported] = useState<boolean | null>(null);
const [reduce, setReduce] = useState(false);
useEffect(() => {
const c = document.createElement("canvas");
setSupported(!!c.getContext("webgl2"));
setReduce(window.matchMedia("(prefers-reduced-motion: reduce)").matches);
}, []);
if (supported === false) return <SplatFallback label="Gaussian splat needs WebGL2" />;
if (supported === null) return <SplatFallback label="Loading splat…" />;
return (
<CanvasBoundary fallback={<SplatFallback label="Splat asset unavailable" />}>
<Canvas
dpr={[1, 1.5]}
gl={{ antialias: false, powerPreference: "high-performance" }}
camera={{ position: [0, 0, 3], fov: 50 }}
>
<Splats url={url} reduce={reduce} />
<OrbitControls enablePan={false} enableZoom={false} autoRotate={!reduce} autoRotateSpeed={0.5} />
</Canvas>
</CanvasBoundary>
);
}