Web 3d react three fiber
Skill agents-inc/skills/dist/plugins/web-3d-react-three-fiber/skills/web-3d-react-three-fiber
React Three Fiber (R3F) 3D rendering — Canvas, meshes, materials, lights, cameras, animations, events, physics, post-processing, performanceFrom its SKILL.md
npx -y skills add agents-inc/skills --skill web-3d-react-three-fiberAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 20 stars20 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
17.8 KB, ~4.5k tokens by cl100k_base, as published. Nobody here has run it
React Three Fiber Patterns
Quick Guide: R3F is a React renderer for Three.js. Every Three.js class maps to a JSX element (
<mesh>,<boxGeometry>,<meshStandardMaterial>). Use<Canvas>for scene setup,useFramefor per-frame logic (never setState inside it),useReffor direct mutations, anduseLoader/useGLTFfor assets. Animate via refs inuseFrame, not React state. Events work like DOM events with raycasting built in. Wrap exiting 3D components in<Suspense>for async asset loading.
Import:
import { Canvas, useFrame, useThree, useLoader } from "@react-three/fiber"
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST never call setState inside useFrame -- mutate refs directly for per-frame updates)
(You MUST wrap <Canvas> children that load assets in <Suspense> boundaries)
(You MUST reuse geometries and materials across meshes -- creating new instances per mesh wastes GPU memory)
(You MUST call event.stopPropagation() on pointer events to prevent hits passing through to occluded objects)
(You MUST use named constants for all numeric values -- positions, sizes, speeds, colors -- NO magic numbers)
</critical_requirements>
Auto-detection: React Three Fiber, R3F, @react-three/fiber, @react-three/drei, @react-three/rapier, @react-three/postprocessing, Canvas, useFrame, useThree, useLoader, useGLTF, mesh, boxGeometry, meshStandardMaterial, OrbitControls, drei, three.js, 3D scene, WebGL, instancedMesh
When to use:
- Building 3D scenes, visualizations, or experiences in React
- Loading and displaying 3D models (GLTF, OBJ, FBX)
- Adding physics simulation to 3D objects
- Handling pointer/click interactions on 3D meshes
- Animating objects per-frame (rotation, position, scale)
- Applying post-processing effects (bloom, depth of field, SSAO)
When NOT to use:
- 2D-only UIs (standard React components)
- Static images of 3D content (pre-render instead)
- Performance-critical scenarios where raw Three.js without React overhead is needed
Key patterns covered:
- Canvas setup with camera, shadows, and renderer config
- Declarative meshes, geometries, materials, and lights
- Per-frame animation with
useFrameand refs - Pointer events, raycasting, and event propagation
- Asset loading with
useLoader,useGLTF, and Suspense - Drei helpers (OrbitControls, Environment, Text, Html, Detailed)
- Physics with
@react-three/rapier(RigidBody, colliders, collision events) - Post-processing with
@react-three/postprocessing - Performance: instancing, LOD, on-demand rendering, geometry reuse, disposal
Detailed Resources:
- examples/core.md - Canvas, meshes, materials, lights, camera, useFrame, asset loading, drei helpers
- examples/interaction.md - Events, raycasting, hover/click, drag, pointer capture
- examples/performance.md - Instancing, LOD, disposal, frame loop control, on-demand rendering
- reference.md - Decision frameworks, Canvas props, hook signatures, anti-patterns
<philosophy>
Philosophy
React Three Fiber is a React reconciler for Three.js -- every Three.js object becomes a declarative JSX element. The React tree IS the scene graph. Components mount/unmount meshes, lights, and cameras just like DOM elements. This means React features (Suspense, context, refs, state) all work naturally in 3D.
Core principles:
- Declarative scene graph -- describe WHAT the scene looks like, not HOW to build it imperatively
- Refs for mutations, state for structure -- per-frame updates go through
useRefinuseFrame, structural changes (adding/removing objects) go through React state - Reuse everything -- geometries, materials, and textures are GPU resources; share them across meshes
- Suspense for async -- wrap asset-loading components in
<Suspense>for automatic loading states - Events are raycasted -- pointer events automatically raycast into the scene;
stopPropagationprevents hits on occluded objects
The R3F ecosystem:
| Package | Purpose |
|---|---|
@react-three/fiber | Core renderer -- Canvas, hooks, reconciler |
@react-three/drei | Helpers -- controls, loaders, abstractions, text, HTML overlays |
@react-three/rapier | Physics -- rigid bodies, colliders, collision events |
@react-three/postprocessing | Effects -- bloom, DOF, SSAO, vignette |
<patterns>
Core Patterns
Pattern 1: Canvas and Scene Setup
<Canvas> creates a WebGL context with scene, camera, and renderer. All R3F hooks must be used inside Canvas.
import { Canvas } from "@react-three/fiber";
const CAMERA_FOV = 50;
const CAMERA_POSITION: [number, number, number] = [0, 2, 5];
export function Scene() {
return (
<Canvas
camera={{ fov: CAMERA_FOV, position: CAMERA_POSITION, near: 0.1, far: 100 }}
shadows
dpr={[1, 2]}
frameloop="always"
>
<ambientLight intensity={0.5} />
<directionalLight position={[5, 5, 5]} castShadow />
<mesh castShadow receiveShadow>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
</Canvas>
);
}
Why good: named position constant, shadows enabled on canvas + individual meshes, dpr clamped to prevent excessive resolution on HiDPI displays
See examples/core.md Pattern 1 for full Canvas config, lighting setups, and camera types.
Pattern 2: Per-Frame Animation with useFrame
useFrame runs every frame before render. Mutate refs directly -- never call setState.
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import type { Mesh } from "three";
const ROTATION_SPEED = 1;
export function SpinningBox() {
const meshRef = useRef<Mesh>(null);
useFrame((state, delta) => {
if (!meshRef.current) return;
meshRef.current.rotation.y += ROTATION_SPEED * delta;
});
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="royalblue" />
</mesh>
);
}
Why good: delta-time multiplication makes animation frame-rate independent, ref mutation avoids re-renders, guard clause prevents null access
// BAD: triggers re-render every frame -- destroys performance
function BadSpinningBox() {
const [rotation, setRotation] = useState(0);
useFrame((_, delta) => {
setRotation((r) => r + delta); // setState in useFrame!
});
return <mesh rotation-y={rotation} />;
}
Why bad: setState in useFrame causes a React re-render every frame (~60/s), defeating the purpose of direct GPU mutations
See examples/core.md Pattern 2 for animation with useFrame, clock-based motion, and conditional animation.
Pattern 3: Asset Loading with Suspense
Use useLoader or drei's useGLTF to load models, textures, and other assets. Always wrap in <Suspense>.
import { Suspense } from "react";
import { useGLTF } from "@react-three/drei";
function Model({ url }: { url: string }) {
const { nodes, materials } = useGLTF(url);
return (
<mesh
geometry={(nodes.myMesh as THREE.Mesh).geometry}
material={materials.myMaterial}
/>
);
}
// Preload for faster initial render
useGLTF.preload("/model.glb");
export function SceneWithModel() {
return (
<Canvas>
<Suspense fallback={null}>
<Model url="/model.glb" />
</Suspense>
</Canvas>
);
}
Why good: Suspense handles loading states automatically, preload avoids waterfall, useGLTF extracts named nodes/materials
See examples/core.md Pattern 3 for texture loading, Draco compression, and progressive loading.
Pattern 4: Pointer Events and Raycasting
R3F meshes support DOM-like pointer events. Events are raycasted -- the nearest hit object receives the event first.
const HOVER_COLOR = "hotpink";
const DEFAULT_COLOR = "orange";
const ACTIVE_SCALE = 1.2;
const DEFAULT_SCALE = 1;
export function InteractiveBox() {
const [hovered, setHovered] = useState(false);
const [active, setActive] = useState(false);
return (
<mesh
scale={active ? ACTIVE_SCALE : DEFAULT_SCALE}
onClick={(e) => {
e.stopPropagation();
setActive((a) => !a);
}}
onPointerOver={(e) => {
e.stopPropagation();
setHovered(true);
}}
onPointerOut={() => setHovered(false)}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color={hovered ? HOVER_COLOR : DEFAULT_COLOR} />
</mesh>
);
}
Why good: stopPropagation prevents clicks passing through to objects behind, hover state uses React state (not per-frame), named color constants
See examples/interaction.md for event object properties, pointer capture, drag, and onPointerMissed.
Pattern 5: Drei Helpers
@react-three/drei provides ready-made abstractions for common tasks.
import { OrbitControls, Environment, Text, Html } from "@react-three/drei";
// Camera controls
<OrbitControls enableDamping dampingFactor={0.1} />
// Environment lighting from HDRI preset
<Environment preset="sunset" background />
// 3D text rendered as mesh geometry
<Text fontSize={0.5} position={[0, 2, 0]} color="white">
Hello 3D World
</Text>
// HTML overlaid on 3D position
<Html position={[1, 1, 0]} distanceFactor={10}>
<div className="tooltip">Click me</div>
</Html>
See examples/core.md Pattern 5 for full drei helper examples including Detailed (LOD), ContactShadows, and Float.
Pattern 6: Physics with @react-three/rapier
Wrap the scene in <Physics> and objects in <RigidBody> for physics simulation.
import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
const GRAVITY: [number, number, number] = [0, -9.81, 0];
const FLOOR_SIZE: [number, number, number] = [10, 0.1, 10];
export function PhysicsScene() {
return (
<Physics gravity={GRAVITY}>
{/* Dynamic falling box */}
<RigidBody colliders="cuboid" restitution={0.5}>
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="tomato" />
</mesh>
</RigidBody>
{/* Static floor */}
<RigidBody type="fixed">
<mesh>
<boxGeometry args={FLOOR_SIZE} />
<meshStandardMaterial color="gray" />
</mesh>
</RigidBody>
</Physics>
);
}
Why good: gravity as named constant, explicit collider type, fixed body for static geometry
See examples/core.md Pattern 6 for collision events, sensors, and InstancedRigidBodies.
Pattern 7: Post-Processing Effects
@react-three/postprocessing merges effects into efficient render passes.
import { EffectComposer, Bloom, Vignette } from "@react-three/postprocessing";
const BLOOM_INTENSITY = 0.5;
const BLOOM_LUMINANCE_THRESHOLD = 0.9;
const VIGNETTE_DARKNESS = 0.5;
<EffectComposer>
<Bloom
intensity={BLOOM_INTENSITY}
luminanceThreshold={BLOOM_LUMINANCE_THRESHOLD}
/>
<Vignette darkness={VIGNETTE_DARKNESS} />
</EffectComposer>
See reference.md for common effect combinations and performance considerations.
Pattern 8: Instancing for Many Objects
Use <instancedMesh> to render thousands of identical objects in a single draw call.
import { useRef, useEffect, useMemo } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
const INSTANCE_COUNT = 1000;
export function Particles() {
const meshRef = useRef<THREE.InstancedMesh>(null);
const dummy = useMemo(() => new THREE.Object3D(), []);
useEffect(() => {
if (!meshRef.current) return;
for (let i = 0; i < INSTANCE_COUNT; i++) {
dummy.position.set(
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 10,
);
dummy.updateMatrix();
meshRef.current.setMatrixAt(i, dummy.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
}, [dummy]);
return (
<instancedMesh ref={meshRef} args={[undefined, undefined, INSTANCE_COUNT]}>
<sphereGeometry args={[0.05, 8, 8]} />
<meshBasicMaterial color="white" />
</instancedMesh>
);
}
Why good: single draw call for 1000 objects, useMemo prevents recreating dummy each render, named count constant
See examples/performance.md for animated instances, LOD with Detailed, and on-demand rendering.
Pattern 9: Geometry and Material Reuse
Create shared resources and reference them across meshes to reduce GPU overhead.
import * as THREE from "three";
import { useMemo } from "react";
export function SharedGeometryScene() {
const sharedGeo = useMemo(() => new THREE.SphereGeometry(0.5, 32, 32), []);
const sharedMat = useMemo(() => new THREE.MeshStandardMaterial({ color: "coral" }), []);
return (
<>
<mesh geometry={sharedGeo} material={sharedMat} position={[-2, 0, 0]} />
<mesh geometry={sharedGeo} material={sharedMat} position={[0, 0, 0]} />
<mesh geometry={sharedGeo} material={sharedMat} position={[2, 0, 0]} />
</>
);
}
Why good: one geometry + one material in GPU memory regardless of mesh count, useMemo prevents recreation on re-render
See examples/performance.md for disposal patterns and PerformanceMonitor.
</patterns><decision_framework>
Decision Framework
Choosing an Animation Approach
Is it a per-frame continuous animation (rotation, bob, orbit)?
├─ YES → useFrame + useRef (mutate directly, never setState)
└─ NO → Is it triggered by user interaction (hover, click)?
├─ YES → React state for discrete changes (scale, color)
│ or spring-based animation libraries for smooth transitions
└─ NO → Is it a one-time entrance animation?
└─ YES → useFrame with a progress ref that clamps at 1.0
Choosing a Collider Type
Is the shape a box?
├─ YES → "cuboid" (fastest)
└─ NO → Is it a sphere?
├─ YES → "ball" (fast)
└─ NO → Is it convex (no holes/concavities)?
├─ YES → "hull" (good balance)
└─ NO → "trimesh" (expensive, use sparingly)
Performance Scaling
Are there 10+ identical objects?
├─ YES → instancedMesh (single draw call)
└─ NO → Are objects at varying distances?
├─ YES → LOD with drei's Detailed component
└─ NO → Is the scene mostly static?
├─ YES → frameloop="demand" + invalidate()
└─ NO → Check draw calls (target < 200)
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Calling
setStateinsideuseFrame-- causes 60 re-renders/second, destroys performance - Creating new
Vector3/Matrix4/Object3Dinstances insideuseFrame-- allocates memory every frame, triggers GC pauses - Missing
<Suspense>around components usinguseLoader/useGLTF-- causes uncaught promise errors - Duplicate geometries/materials across identical meshes -- wastes GPU memory; share via
useMemoor module-level instances - Missing
event.stopPropagation()on pointer events -- clicks pass through to occluded objects unexpectedly
Medium Priority Issues:
- Using
frameloop="always"for mostly-static scenes -- drains battery; use"demand"withinvalidate() - More than ~1000 draw calls (each
<mesh>is a draw call) -- use instancing or merge geometries - Not disposing of geometries/materials on unmount -- GPU memory leak (R3F auto-disposes on unmount by default, but manual Three.js objects need explicit cleanup)
- Animating layout-triggering CSS on the Canvas container -- causes reflow; use fixed dimensions
Gotchas & Edge Cases:
- All R3F hooks (
useFrame,useThree,useLoader) must be called inside<Canvas>-- they depend on fiber context useFramecallbacks withrenderPriority >= 1take over the render loop -- you must callgl.render()manuallyuseThreeselectors for Three.js internal properties (likecamera.zoom) are NOT reactive -- useinvalidate()after imperative changes- Three.js uses a Y-up coordinate system --
position={[x, y, z]}where Y is vertical <instancedMesh args={[null, null, count]}>-- the first two args (geometry, material) should beundefinedwhen using child elementsonPointerMissedfires on the Canvas element for clicks that hit no mesh -- useful for deselection- R3F automatically disposes Three.js resources on unmount, but only for objects it created declaratively -- manual
new THREE.*()calls need manual.dispose() <Physics>from rapier must be wrapped in<Suspense>because it loads WASM asynchronously- Event
deltaproperty is mouse-down-to-mouse-up distance in pixels -- useful for distinguishing clicks from drags
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST never call setState inside useFrame -- mutate refs directly for per-frame updates)
(You MUST wrap <Canvas> children that load assets in <Suspense> boundaries)
(You MUST reuse geometries and materials across meshes -- creating new instances per mesh wastes GPU memory)
(You MUST call event.stopPropagation() on pointer events to prevent hits passing through to occluded objects)
(You MUST use named constants for all numeric values -- positions, sizes, speeds, colors -- NO magic numbers)
Failure to follow these rules will cause frame drops, memory leaks, broken interactions, and poor 3D performance.
</critical_reminders>
What ships with it: 4 files
37.5 KB alongside SKILL.md
examples/
- core.md12.0 KB
- interaction.md8.0 KB
- performance.md10.4 KB
- reference.md7.1 KB