From react-native-3d
React Three Fiber native setup, Canvas configuration, scene structure, useFrame, useThree, lights, cameras, and the render loop.
How this skill is triggered — by the user, by Claude, or both
Slash command
/react-native-3d:r3f-native-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill covers core R3F patterns specific to React Native/Expo.
This skill covers core R3F patterns specific to React Native/Expo.
npm install three @react-three/fiber @react-three/drei expo-gl
For TypeScript:
npm install -D @types/three
Critical: Configure Metro to handle 3D asset files.
// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
// Add 3D model extensions
config.resolver.assetExts.push('glb', 'gltf', 'obj', 'mtl', 'hdr');
// Add source extensions for R3F
config.resolver.sourceExts.push('cjs', 'mjs');
module.exports = config;
import { Canvas } from '@react-three/fiber/native';
import { View, StyleSheet } from 'react-native';
export function Basic3DScene() {
return (
<View style={styles.container}>
<Canvas
camera={{ position: [0, 2, 5], fov: 75 }}
gl={{ antialias: true }}
>
<Scene />
</Canvas>
</View>
);
}
function Scene() {
return (
<>
{/* Lights */}
<ambientLight intensity={0.4} />
<directionalLight position={[5, 5, 5]} intensity={1} />
{/* Objects */}
<mesh>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});
<Canvas
// Camera configuration
camera={{
position: [0, 0, 5], // Initial position
fov: 75, // Field of view (perspective)
near: 0.1, // Near clipping plane
far: 1000, // Far clipping plane
orthographic: false, // Use orthographic camera
}}
// WebGL configuration
gl={{
antialias: true, // Smooth edges
alpha: true, // Transparent background
powerPreference: 'high-performance',
}}
// Performance
dpr={[1, 2]} // Pixel ratio range
frameloop="always" // 'always' | 'demand' | 'never'
// Events
onCreated={(state) => {}} // Called when canvas initializes
onPointerMissed={() => {}} // Click/tap on empty space
/>
useFrame runs every frame (~60fps). Use it for animations and continuous updates.
import { useFrame } from '@react-three/fiber/native';
import { useRef } from 'react';
import * as THREE from 'three';
function RotatingCube() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
// state: R3F state (camera, scene, gl, etc.)
// delta: Time since last frame in seconds
if (meshRef.current) {
meshRef.current.rotation.x += delta;
meshRef.current.rotation.y += delta * 0.5;
}
});
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
);
}
useFrame((state) => {
state.clock // THREE.Clock - elapsed time
state.camera // Current camera
state.scene // THREE.Scene
state.gl // WebGL renderer
state.size // { width, height } of canvas
state.pointer // Normalized pointer position { x, y }
state.raycaster // THREE.Raycaster for picking
});
Control execution order with priority (lower = earlier):
// Physics runs first
useFrame((state) => {
updatePhysics();
}, -1);
// Then rendering updates
useFrame((state) => {
updateVisuals();
}, 0);
// Finally, post-processing
useFrame((state) => {
applyEffects();
}, 1);
import { useThree } from '@react-three/fiber/native';
function CameraInfo() {
const { camera, gl, scene, size, pointer, raycaster } = useThree();
console.log('Canvas size:', size.width, size.height);
console.log('Camera position:', camera.position);
return null;
}
function ResponsiveObject() {
const size = useThree((state) => state.size);
// Re-renders only when size changes
return (
<mesh scale={size.width / 100}>
<boxGeometry />
<meshBasicMaterial color="blue" />
</mesh>
);
}
function GameScene() {
return (
<>
<Lighting />
<Environment />
<Player />
<Obstacles />
<UI3D />
</>
);
}
function Lighting() {
return (
<group name="lighting">
<ambientLight intensity={0.3} />
<directionalLight position={[10, 10, 5]} castShadow />
<pointLight position={[-10, 5, -10]} color="#ff9999" />
</group>
);
}
function Environment() {
return (
<group name="environment">
<Ground />
<Sky />
<Trees />
</group>
);
}
function Robot() {
const groupRef = useRef<THREE.Group>(null);
// Moving the group moves all children together
useFrame((state) => {
if (groupRef.current) {
groupRef.current.position.x = Math.sin(state.clock.elapsedTime);
}
});
return (
<group ref={groupRef}>
<Body />
<group position={[0, 1.5, 0]}>
<Head />
</group>
<group position={[-0.5, 0.5, 0]}>
<Arm side="left" />
</group>
<group position={[0.5, 0.5, 0]}>
<Arm side="right" />
</group>
</group>
);
}
<Canvas camera={{
position: [0, 5, 10],
fov: 50, // Field of view in degrees
near: 0.1,
far: 1000,
}}>
<Canvas
orthographic
camera={{
position: [0, 0, 10],
zoom: 50,
near: 0.1,
far: 1000,
}}
>
function CameraSwitcher() {
const perspCam = useRef<THREE.PerspectiveCamera>(null);
const orthoCam = useRef<THREE.OrthographicCamera>(null);
const set = useThree((state) => state.set);
const [isOrtho, setIsOrtho] = useState(false);
useEffect(() => {
if (isOrtho && orthoCam.current) {
set({ camera: orthoCam.current });
} else if (!isOrtho && perspCam.current) {
set({ camera: perspCam.current });
}
}, [isOrtho, set]);
return (
<>
<PerspectiveCamera ref={perspCam} makeDefault={!isOrtho} position={[0, 5, 10]} />
<OrthographicCamera ref={orthoCam} makeDefault={isOrtho} position={[0, 0, 10]} zoom={50} />
</>
);
}
function StandardLighting() {
return (
<>
{/* Base illumination */}
<ambientLight intensity={0.4} color="#ffffff" />
{/* Main directional (sun-like) */}
<directionalLight
position={[10, 10, 5]}
intensity={1}
castShadow
shadow-mapSize={[1024, 1024]}
/>
{/* Fill light (soften shadows) */}
<pointLight position={[-10, 5, -5]} intensity={0.3} color="#aaccff" />
{/* Rim light (edge definition) */}
<spotLight position={[0, 10, -10]} intensity={0.5} angle={0.3} />
</>
);
}
| Light | Use Case | Performance |
|---|---|---|
ambientLight | Base fill, no shadows | Cheapest |
directionalLight | Sun, parallel rays | Medium |
pointLight | Bulbs, omni-directional | Medium |
spotLight | Focused cone | More expensive |
hemisphereLight | Sky + ground colors | Cheap |
// No lighting needed
<meshBasicMaterial color="red" />
// Standard PBR (needs lights)
<meshStandardMaterial
color="red"
metalness={0.5}
roughness={0.5}
/>
// Physical PBR (more realistic)
<meshPhysicalMaterial
color="red"
metalness={0.8}
roughness={0.2}
clearcoat={1}
/>
// Wireframe
<meshBasicMaterial color="white" wireframe />
// Transparent
<meshBasicMaterial color="blue" transparent opacity={0.5} />
// Double-sided
<meshBasicMaterial color="green" side={THREE.DoubleSide} />
import { useGLTF } from '@react-three/drei/native';
function Model({ url }) {
const { scene } = useGLTF(url);
return <primitive object={scene} />;
}
// Preload for better UX
useGLTF.preload('/model.glb');
import { useTexture } from '@react-three/drei/native';
function TexturedBox() {
const texture = useTexture(require('./texture.png'));
return (
<mesh>
<boxGeometry />
<meshStandardMaterial map={texture} />
</mesh>
);
}
import { Suspense } from 'react';
function App() {
return (
<Canvas>
<Suspense fallback={<LoadingIndicator />}>
<HeavyModel />
</Suspense>
</Canvas>
);
}
function LoadingIndicator() {
return (
<mesh>
<sphereGeometry args={[0.5]} />
<meshBasicMaterial color="gray" wireframe />
</mesh>
);
}
Critical for mobile memory management:
function DisposableScene() {
const geometryRef = useRef<THREE.BufferGeometry>();
const materialRef = useRef<THREE.Material>();
useEffect(() => {
return () => {
// Clean up on unmount
geometryRef.current?.dispose();
materialRef.current?.dispose();
};
}, []);
return (
<mesh>
<boxGeometry ref={geometryRef} />
<meshStandardMaterial ref={materialRef} />
</mesh>
);
}
function OptimizedMesh({ color }) {
// Geometry created once
const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
// Material recreated only when color changes
const material = useMemo(
() => new THREE.MeshStandardMaterial({ color }),
[color]
);
return <mesh geometry={geometry} material={material} />;
}
function LODObject({ distance }) {
if (distance > 100) return null; // Don't render if too far
return (
<mesh>
{distance < 20 ? (
<sphereGeometry args={[1, 32, 32]} /> // High detail
) : (
<sphereGeometry args={[1, 8, 8]} /> // Low detail
)}
<meshStandardMaterial />
</mesh>
);
}
// Only render when needed (for static scenes)
<Canvas frameloop="demand">
<StaticScene />
</Canvas>
// Inside scene, trigger render when needed:
function StaticScene() {
const invalidate = useThree((state) => state.invalidate);
const handleChange = () => {
// Something changed, request a render
invalidate();
};
}
npx claudepluginhub smartwatermelon/smartwatermelon-marketplace --plugin react-native-3dBuild declarative 3D scenes with React Three Fiber in React apps using JSX components for Three.js objects. For interactive configurators, games, portfolios, and data viz.
Builds Three.js and React Three Fiber (R3F) 3D scenes: setup, animation, GLTF/GLB loading, physics with @react-three/rapier, WebGPU, Drei helpers, and performance optimization.
Builds interactive 3D web scenes with Three.js using WebGL/WebGPU. Guides on scenes, cameras, renderers, geometries, materials, meshes, lights, animations, and OrbitControls.