---
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:**
```bash
npm i three @react-three/fiber @react-three/drei
```
### Basic Scene
```jsx
import { Canvas } from "@react-three/fiber";
import { OrbitControls, Environment, useGLTF } from "@react-three/drei";
function Model({ url }) {
const { scene } = useGLTF(url);
return ;
}
function Scene() {
return (
);
}
```
### ScrollControls (Scroll-Linked 3D) [E1]
```jsx
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; // 0 to 1
// Rotate model based on scroll
ref.current.rotation.y = offset * Math.PI * 2;
// Move camera/model along a path
ref.current.position.y = offset * -5;
});
return (
);
}
function ScrollScene() {
return (
Section 3
);
}
```
### useScroll Utilities
```jsx
const scroll = useScroll();
// scroll.offset — 0 to 1 (overall progress)
// scroll.delta — scroll speed
// scroll.range(from, distance) — 0-1 within range
// scroll.curve(from, distance) — bell curve within range
// scroll.visible(from, distance) — boolean visibility
useFrame(() => {
// Animate only in section 2 (20%-40% of scroll)
const sectionProgress = scroll.range(0.2, 0.2);
ref.current.scale.setScalar(1 + sectionProgress * 0.5);
// Fade in section 3
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:
```jsx
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 ...;
}
```
---
## 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
```jsx
// Method 1: iframe (simplest, no bundle impact)
// Method 2: React component (more control)
import Spline from "@splinetool/react-spline";
// Method 3: Vanilla JS
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]
```bash
# Install
npm i -g @gltf-transform/cli
# Full optimization pipeline
gltf-transform optimize input.glb output.glb \
--compress draco \
--texture-compress webp
# Or step by step:
gltf-transform dedup input.glb deduped.glb # remove duplicate data
gltf-transform draco deduped.glb compressed.glb # geometry compression
gltf-transform webp compressed.glb textured.glb # texture to WebP
gltf-transform quantize textured.glb output.glb # mesh quantization
```
### 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
```jsx
import { useGLTF, useTexture } from "@react-three/drei";
// Preload for instant display
useGLTF.preload("/models/product.glb");
function Product() {
const { scene, nodes, materials } = useGLTF("/models/product.glb");
return (
);
}
// With Draco decoder
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader";
// In Canvas setup or loader config:
// DRACOLoader points to decoder files (usually from CDN)
```
---
## 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
```jsx
// 1. Use instancing for repeated objects
import { Instances, Instance } from "@react-three/drei";
{positions.map((pos, i) => (
))}
// 2. Frustum culling (enabled by default in Three.js)
// 3. LOD (Level of Detail)
import { Detailed } from "@react-three/drei";
// 4. Limit pixel ratio on mobile