| name | 3d-scene |
| description | Use when building 3D scenes for web — Three.js, React Three Fiber (R3F), Spline embeds, GLTF/GLB loading, scroll-linked 3D, camera animations. Covers scene setup, model optimization, and performance budgets. |
| arguments | [{"name":"approach","description":"Approach: r3f, threejs-vanilla, spline-embed (auto-detect if omitted)","required":false},{"name":"interaction","description":"Interaction: scroll-linked, orbit, hover, static (default: scroll-linked)","required":false}] |
3D Scene Skill
Decision Matrix — Pick Approach
| Need | Approach | Bundle Impact | Complexity |
|---|
| Product showcase with scroll | R3F + ScrollControls | ~150KB (Three.js) | Medium |
| Hero 3D scene (no-code) | Spline embed | ~0KB (iframe) | Low |
| Custom shaders + 3D | Three.js vanilla | ~150KB | High |
| Lightweight shader-only | OGL | ~8KB | Medium |
| Interactive configurator | R3F + Leva/dat.gui | ~150KB | Medium |
1. React Three Fiber (R3F) + Drei [E1]
Install:
npm i three @react-three/fiber @react-three/drei
Basic Scene
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Environment, useGLTF } from "@react-three/drei";
function Model({ url }) {
const { scene } = useGLTF(url);
return <primitive object={scene} />;
}
function Scene() {
return (
<Canvas
camera={{ position: [0, 2, 5], fov: 45 }}
style={{ width: "100%", height: "100vh" }}
>
<Environment preset="studio" />
<Model url="/models/product.glb" />
<OrbitControls enableZoom={false} />
</Canvas>
);
}
ScrollControls (Scroll-Linked 3D) [E1]
import { Canvas, useFrame } from "@react-three/fiber";
import { ScrollControls, Scroll, useScroll } from "@react-three/drei";
function AnimatedModel() {
const scroll = useScroll();
const ref = useRef();
useFrame(() => {
const offset = scroll.offset;
ref.current.rotation.y = offset * Math.PI * 2;
ref.current.position.y = offset * -5;
});
return (
<mesh ref={ref}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="royalblue" />
</mesh>
);
}
function ScrollScene() {
return (
<Canvas>
<ScrollControls
pages={5} // 5 pages of scroll (500vh)
damping={0.25} // friction (seconds to catch up)
>
{/* 3D content */}
<Scroll>
<AnimatedModel />
</Scroll>
{/* HTML content overlaid */}
<Scroll html>
<div style={{ position: "absolute", top: "100vh" }}>
<h2>Section 2</h2>
</div>
<div style={{ position: "absolute", top: "200vh" }}>
<h2>Section 3</h2>
</div>
</Scroll>
</ScrollControls>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} />
</Canvas>
);
}
useScroll Utilities
const scroll = useScroll();
useFrame(() => {
const sectionProgress = scroll.range(0.2, 0.2);
ref.current.scale.setScalar(1 + sectionProgress * 0.5);
const visible = scroll.visible(0.4, 0.2);
material.current.opacity = visible ? scroll.curve(0.4, 0.2) : 0;
});
R3F + GSAP (Alternative to ScrollControls) [E2]
When you need GSAP's pin/snap with 3D:
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
function GSAPModel() {
const mesh = useRef();
useGSAP(() => {
gsap.to(mesh.current.rotation, {
y: Math.PI * 2,
scrollTrigger: {
trigger: "#scene-container",
start: "top top",
end: "+=3000",
pin: true,
scrub: 1,
}
});
});
return <mesh ref={mesh}>...</mesh>;
}
2. Spline Embeds [E2]
What: No-code 3D design tool with web export.
Best for: Quick 3D heroes, interactive product views, landing pages.
Embed Methods
<iframe
src="https://my.spline.design/scene-abc123/"
frameBorder="0"
width="100%"
height="500px"
style={{ border: "none" }}
/>
import Spline from "@splinetool/react-spline";
<Spline scene="https://prod.spline.design/abc123/scene.splinecode" />
import { Application } from "@splinetool/runtime";
const app = new Application(canvas);
app.load("https://prod.spline.design/abc123/scene.splinecode");
Spline Capabilities
- Scroll-linked animations (native events)
- Mouse follow / hover interactions
- State changes on click
- Physics simulations
- Responsive layout
- AI text-to-3D / image-to-3D generation
Spline Limitations
- Performance: Keep <3 lights per scene
- Polygons: Smooth subdivision max 2 levels
- Loading: Scenes can be 1-5MB+ for complex objects
- Control: Less fine-grained than custom Three.js
- Offline: Requires network to load from Spline CDN (self-hosted export available)
- Scroll sync: Less precise than R3F ScrollControls or GSAP
Spline Optimization
- Delete invisible objects (inside or behind other objects)
- Use <3 lights
- Compress on export (quality vs size tradeoff)
- Reduce subdivision levels
- Optimize CAD imports: strip internal geometry, small fillets
- Keep total polygon count reasonable for target devices
3. GLTF/GLB Loading & Optimization [E1]
The Pipeline
Source (FBX/OBJ/Blender)
→ Export as GLTF/GLB
→ Optimize with gltf-transform
→ Draco/Meshopt compression
→ KTX2 texture compression
→ Mesh quantization
→ Load in Three.js/R3F
gltf-transform Optimization [E1]
npm i -g @gltf-transform/cli
gltf-transform optimize input.glb output.glb \
--compress draco \
--texture-compress webp
gltf-transform dedup input.glb deduped.glb
gltf-transform draco deduped.glb compressed.glb
gltf-transform webp compressed.glb textured.glb
gltf-transform quantize textured.glb output.glb
Real-World Size Reductions
| Stage | Example Size | Reduction |
|---|
| Raw FBX | 50 MB | baseline |
| GLTF/GLB export | 29 MB | -42% |
| Draco compression | 5 MB | -83% |
| + KTX2 textures | 2.5 MB | -91% |
| + Mesh quantization | 2 MB | -93% |
Compression Methods
| Method | Compression | Decode Speed | Three.js Version |
|---|
| Draco | Best (~90%) | Slower (Web Worker) | r100+ |
| Meshopt | Good (~85%) | Faster | r122+ |
| Quantization | Moderate (~50%) | Instant | r100+ |
Decision: Draco for smallest files, Meshopt for fastest client decode. Combine with KTX2 textures.
Loading in R3F
import { useGLTF, useTexture } from "@react-three/drei";
useGLTF.preload("/models/product.glb");
function Product() {
const { scene, nodes, materials } = useGLTF("/models/product.glb");
return (
<primitive
object={scene}
scale={0.5}
position={[0, -1, 0]}
/>
);
}
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
4. Performance Budgets [E1]
Polygon Limits
| Device Class | Max Triangles | Draw Calls | Notes |
|---|
| High-end desktop | 500K-1M | <200 | Gaming GPU |
| Average desktop | 100K-300K | <100 | Integrated GPU |
| Mobile (flagship) | 50K-150K | <50 | iPhone 14+ level |
| Mobile (budget) | 20K-50K | <30 | Older Android |
Key insight: Draw call count matters MORE than polygon count. Below 100 draw calls, most devices maintain 60fps. Above 500, even powerful GPUs struggle.
Texture Budgets
| Texture | Max Size | Format | VRAM |
|---|
| Diffuse/Albedo | 2048x2048 | KTX2/WebP | ~5MB |
| Normal map | 1024x1024 | KTX2 | ~1.3MB |
| Roughness/Metal | 512x512 | KTX2 | ~0.3MB |
| Environment | 256x256 cube | HDR/EXR | ~2MB |
Critical: A 200KB PNG on disk = 20MB+ in VRAM! KTX2 with Basis Universal stays compressed on GPU (~10x reduction).
R3F Performance Tips
import { Instances, Instance } from "@react-three/drei";
<Instances>
{positions.map((pos, i) => (
<Instance key={i} position={pos} />
))}
</Instances>
import { Detailed } from "@react-three/drei";
<Detailed distances={[0, 50, 100]}>
<HighPolyModel />
<MedPolyModel />
<LowPolyModel />
</Detailed>
<Canvas dpr={[1, 2]}> {/* max 2x, not device native */}
// 5. Use drei's Preload for loading screen
import { Preload } from "@react-three/drei";
<Canvas>
<Suspense fallback={null}>
<Scene />
</Suspense>
<Preload all />
</Canvas>
Monitor Performance
import { useFrame } from "@react-three/fiber";
import { Stats } from "@react-three/drei";
<Stats />
useFrame((state) => {
const info = state.gl.info;
});
5. Lighting & Environment [E2]
Quick Setups (drei)
<Environment preset="studio" />
<Environment files="/hdri/warehouse.hdr" />
<ambientLight intensity={0.3} />
<directionalLight position={[5, 5, 5]} intensity={1} castShadow />
<directionalLight position={[-3, 3, -5]} intensity={0.5} />
<ContactShadows
position={[0, -1, 0]}
opacity={0.5}
scale={10}
blur={2}
/>
Environment Presets (drei)
Available: apartment, city, dawn, forest, lobby, night, park, studio, sunset, warehouse
6. Common Patterns
Product Reveal on Scroll
function ProductReveal() {
return (
<Canvas>
<ScrollControls pages={4} damping={0.3}>
<Scroll>
<ProductModel /> {/* rotates with scroll */}
</Scroll>
<Scroll html>
<section style={{ top: "100vh" }}>Feature 1</section>
<section style={{ top: "200vh" }}>Feature 2</section>
<section style={{ top: "300vh" }}>CTA</section>
</Scroll>
</ScrollControls>
<Environment preset="studio" />
</Canvas>
);
}
Floating Objects (Parallax)
import { Float } from "@react-three/drei";
<Float
speed={2} // animation speed
rotationIntensity={0.5}
floatIntensity={0.5}
>
<mesh>
<torusGeometry args={[1, 0.3, 16, 32]} />
<meshStandardMaterial color="hotpink" />
</mesh>
</Float>
Text in 3D
import { Text3D, Center } from "@react-three/drei";
<Center>
<Text3D
font="/fonts/inter_bold.json"
size={0.75}
height={0.2}
curveSegments={12}
>
Hello World
<meshNormalMaterial />
</Text3D>
</Center>
Workflow
- Define the 3D need — is it product showcase, decoration, interactive?
- Pick approach — R3F for code control, Spline for no-code, OGL for shader-only
- Prepare models — export GLTF/GLB, optimize with gltf-transform
- Set up scene — Camera, lighting, environment
- Add scroll interaction — ScrollControls or GSAP integration
- Optimize — polygon budget, texture compression, instancing
- Test performance — Stats component, Chrome DevTools, test on mobile
- Add fallbacks — static image for low-end devices, loading state