xiaohongshu
xiaohongshu
dev-r3f-r3f-fundamentals
React Three Fiber core patterns for scene composition and game loop
Full skill instructions
"Declarative 3D – compose scenes like React components."
Use when:
useFrameimport { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
function App() {
return (
<Canvas camera={{ position: [0, 5, 10], fov: 50 }}>
<ambientLight intensity={0.5} />
<pointLight position={[10, 10, 10]} />
<mesh>
<boxGeometry />
<meshStandardMaterial color="orange" />
</mesh>
<OrbitControls />
</Canvas>
);
}
| Need | Use |
|---|---|
| Basic 3D scene | <Canvas> with mesh + geometry + material |
| Camera controls | <OrbitControls> or custom camera rig |
| Animation loop | useFrame hook |
| Access Three.js | useThree hook |
| Load assets | useLoader or <Suspense> with drei loaders |
| Performance | <Instances>, LOD, or useInstancedMesh |
// Simple mesh component
export function Box({ position = [0, 0, 0] }) {
return (
<mesh position={position}>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="royalblue" />
</mesh>
);
}
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
export function SpinningBox() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state, delta) => {
if (meshRef.current) {
meshRef.current.rotation.x += delta;
meshRef.current.rotation.y += delta * 0.5;
}
});
return (
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial color="hotpink" />
</mesh>
);
}
import { useThree } from '@react-three/fiber';
export function CameraLogger() {
const { camera, gl, scene, size } = useThree();
useFrame(() => {
// Access camera position
console.log(camera.position.toArray());
});
return null;
}
import { useGameStore } from '@/store/gameStore';
export function GameLoop() {
const { phase, updatePhase } = useGameStore();
useFrame((state, delta) => {
// Fixed timestep update
const fixedDelta = Math.min(delta, 1 / 30);
// Update game logic
updatePhase(fixedDelta);
});
return null;
}
import { Instances, Instance } from '@react-three/drei';
export function ManyBoxes({ count = 1000 }) {
return (
<Instances limit={count}>
<boxGeometry />
<meshStandardMaterial />
{Array.from({ length: count }, (_, i) => (
<Instance
key={i}
position={[Math.random() * 100 - 50, Math.random() * 100 - 50, Math.random() * 100 - 50]}
/>
))}
</Instances>
);
}
DON'T:
useFrame (causes GC pressure)useState for rapidly changing values (use refs instead)import * as THREE)position={[x, y, z]} with changing values (creates new array each render)FBXLoader.load() in useEffect – breaks R3F's suspense systemDO:
position-x, position-y, position-z for animated valuesuseFBX, useGLTF) for model loading – integrates with R3F suspenseconst tempVec = new THREE.Vector3();
function MovingObject() {
const meshRef = useRef<THREE.Mesh>(null);
useFrame((state) => {
tempVec.set(Math.sin(state.clock.elapsedTime), 0, Math.cos(state.clock.elapsedTime));
meshRef.current?.position.copy(tempVec);
});
return <mesh ref={meshRef}>...</mesh>;
}
function ConditionalMesh({ visible }) {
// Don't render if not visible - saves GPU
if (!visible) return null;
return <mesh>...</mesh>;
}
import { useFBX } from '@react-three/drei';
import { Suspense } from 'react';
// ❌ WRONG - Manual loading breaks R3F suspense
// useEffect(() => {
// new FBXLoader().load(url, (obj) => setFbx(obj));
// }, [url]);
// ✅ CORRECT - Use drei's useFBX hook
function CharacterModel({ url }: { url: string }) {
const fbx = useFBX(url);
return <primitive object={fbx} />;
}
// Wrap in Suspense for loading states
function Scene() {
return (
<Suspense fallback={<LoadingSpinner />}>
<CharacterModel url="/models/character.fbx" />
</Suspense>
);
}
Before implementing R3F component:
useFBX/useGLTF from drei (not manual loaders)State management for R3F scenes using Zustand with vanilla pattern.
// src/store/types.ts - Shared types
export interface Vector3Like {
x: number;
y: number;
z: number;
}
export interface PlayerState {
position: Vector3Like;
rotation: number;
health: number;
weapon: string;
}
// src/store/playerStore.ts - Vanilla Zustand (no React dependencies)
import { mutate } from 'zustand/mutate';
import type { PlayerState, Vector3Like } from './types';
interface PlayerStore {
state: PlayerState;
setPosition: (position: Vector3Like) => void;
setHealth: (health: number) => void;
reset: () => void;
}
const initialState: PlayerState = {
position: { x: 0, y: 0, z: 0 },
rotation: 0,
health: 100,
weapon: 'blaster',
};
export const usePlayerStore = create<PlayerStore>((set) => ({
state: initialState,
setPosition: (position) =>
set((store) => ({
state: mutate(store.state, { position }),
})),
setHealth: (health) =>
set((store) => ({
state: mutate(store.state, { health }),
})),
reset: () => set({ state: initialState }),
}));
// src/components/player/Player.tsx
import { usePlayerStore } from '@/store/playerStore';
import { useRef } from 'react';
import { useFrame } from '@react-three/fiber';
export function Player() {
const { state, setPosition } = usePlayerStore();
const meshRef = useRef<THREE.Mesh>(null);
// Sync store position to mesh
useFrame(() => {
if (meshRef.current) {
meshRef.current.position.set(
state.position.x,
state.position.y,
state.position.z
);
}
});
return (
<mesh ref={meshRef}>
<boxGeometry args={[1, 2, 1]} />
<meshStandardMaterial color="orange" />
</mesh>
);
}
// src/store/playerStore.ts
import { devtools } from 'zustand/middleware';
export const usePlayerStore = create<PlayerStore>()(
devtools(
(set) => ({
state: initialState,
// ... actions
}),
{ name: 'PlayerStore' }
)
);
❌ DON'T:
setState directly in useFrame (causes infinite loops)mutate from zustand/mutate)✅ DO:
window.__ZUSTAND__ for debugging// src/tests/store/playerStore.test.ts
import { renderHook, act } from '@testing-library/react';
import { usePlayerStore } from '@/store/playerStore';
describe('playerStore', () => {
it('updates position', () => {
const { result } = renderHook(() => usePlayerStore());
act(() => {
result.current.setPosition({ x: 10, y: 0, z: 5 });
});
expect(result.current.state.position).toEqual({ x: 10, y: 0, z: 5 });
});
});
Sources:
Validated working camera distances from feat-tps-003 (2026-01-27):
| Mode | Distance Value | Character Framing | Notes |
|---|---|---|---|
| Hipfire | 3.5 units | 30-40% of screen | Character waist up visible |
| Aim (ADS) | 1.5 units | Closer view | For precision shooting |
These values provide proper TPS (Third-Person Shooter) framing where the character is clearly visible on the left side of the screen with enough surrounding context.
⚠️ CRITICAL: Over-the-shoulder view requires BOTH position offset AND look-at offset.
| Parameter | Value (left shoulder view) | Notes |
|---|---|---|
| shoulderOffsetRight | 0.75 units | Camera position offset (right) |
| shoulderOffsetLeft | -0.75 units | Camera position offset (left) |
| look-at offset | Same as position offset | Often missed - REQUIRED for proper composition |
Common anti-pattern from feat-tps-004:
Example implementation:
// Position offset (standard)
const _vec3_position = new Vector3(0, 1.6, 3.5)
.add(_vec3_right.clone().multiplyScalar(shoulderOffset));
// Look-at offset (CRITICAL - often missed)
const _vec3_lookAt = targetPosition.clone()
.add(_vec3_right.clone().multiplyScalar(shoulderOffset));
camera.position.copy(_vec3_position);
camera.lookAt(_vec3_lookAt);
Acceptance criteria validation:
Learned from ui-001: Functional UI is insufficient - requires design system with tokens, aspect ratio, and professional styling.
// src/components/ui/tokens.ts - Single source of truth for UI values
export const UITokens = {
// Base resolution for all calculations
base: {
width: 1920,
height: 1080,
aspectRatio: 16 / 9,
},
// Spacing scale (8px base unit)
spacing: {
xs: '8px',
sm: '16px',
md: '24px',
lg: '32px',
xl: '48px',
xxl: '64px',
},
// Typography
fontSize: {
xs: '14px',
sm: '16px',
base: '18px',
lg: '24px',
xl: '32px',
'2xl': '48px',
'3xl': '64px',
display: '128px',
},
// Get responsive scale factor
getScale(windowWidth: number, windowHeight: number): number {
const scale = Math.min(
windowWidth / this.base.width,
windowHeight / (this.base.width * 9 / 16)
);
return Math.max(0.5, Math.min(2, scale));
},
};
// src/components/ui/AspectContainer.tsx
import React, { useEffect, useState } from 'react';
export function AspectContainer({ children }: { children: React.ReactNode }) {
const [scale, setScale] = useState(1);
useEffect(() => {
const updateScale = () => {
const s = UITokens.getScale(window.innerWidth, window.innerHeight);
setScale(s);
};
updateScale();
window.addEventListener('resize', updateScale);
return () => window.removeEventListener('resize', updateScale);
}, []);
return (
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black">
<div
className="relative overflow-hidden"
style={{
aspectRatio: 16 / 9,
transform: `scale(${scale})`,
transformOrigin: 'center',
}}
>
{children}
</div>
</div>
);
}
import { UITokens } from '@/components/ui/tokens';
export function GameButton({ children, onClick }) {
return (
<motion.button
onClick={onClick}
style={{
padding: UITokens.spacing.md,
fontSize: UITokens.fontSize.lg,
// Use token colors, not hardcoded values
background: 'var(--color-primary-500)',
}}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.97 }}
>
{children}
</motion.button>
);
}
/* src/index.css - Define CSS variables for design tokens */
:root {
/* Base dimensions */
--ui-base-width: 1920px;
--ui-base-height: 1080px;
--ui-aspect-ratio: 16 / 9;
/* Spacing scale */
--spacing-xs: 8px;
--spacing-sm: 16px;
--spacing-md: 24px;
--spacing-lg: 32px;
/* Typography */
--font-display: 'Orbitron', sans-serif;
--font-ui: 'Rajdhani', sans-serif;
/* Colors - Orange/Cyan theme */
--color-primary: #f97316;
--color-secondary: #06b6d4;
--color-metal-900: #0a0a0a;
--color-metal-700: #262626;
/* Glow effects */
--glow-primary: rgba(249, 115, 22, 0.6);
}
// src/components/ui/Button.tsx - Production-ready button
import { motion } from 'framer-motion';
import { UITokens } from './tokens';
interface ButtonProps {
variant?: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
onClick?: () => void;
}
export function Button({
variant = 'primary',
size = 'md',
children,
onClick,
}: ButtonProps) {
const sizeStyles = {
sm: { height: '48px', padding: '0 24px', fontSize: UITokens.fontSize.sm },
md: { height: '64px', padding: '0 32px', fontSize: UITokens.fontSize.lg },
lg: { height: '80px', padding: '0 48px', fontSize: UITokens.fontSize.xl },
};
return (
<motion.button
className={`button button-${variant}`}
style={sizeStyles[size]}
onClick={onClick}
whileHover={{ scale: 1.05, x: -4 }}
whileTap={{ scale: 0.97, x: 2 }}
transition={{
type: 'spring',
stiffness: 400,
damping: 17,
}}
>
{children}
</motion.button>
);
}
// src/components/ui/easing.ts
export const GameEasing = {
snap: [0.22, 1, 0.36, 1] as const,
reveal: [0.16, 1, 0.3, 1] as const,
bounce: [0.34, 1.56, 0.64, 1] as const,
press: [0.4, 0, 0.2, 1] as const,
};
// Usage with Framer Motion
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, ease: GameEasing.snap }}
/>
// App.tsx - Layered architecture
export function App() {
return (
<>
{/* 3D Scene Layer */}
<Canvas>
<ambientLight />
<GameScene />
</Canvas>
{/* UI Layer - 16:9 container ensures consistent layout */}
<AspectContainer>
<HUD />
</AspectContainer>
</>
);
}
Sources:
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience