[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
---
|
||||
id: frontend-three-r3f
|
||||
title: Three.js / React Three Fiber — 3D 웹
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [frontend, 3d, threejs, r3f, vibe-coding]
|
||||
tech_stack: { language: "TS / React", applicable_to: ["Frontend"] }
|
||||
applied_in: []
|
||||
aliases: [Three.js, React Three Fiber, drei, Rapier, Spline, WebGL]
|
||||
---
|
||||
|
||||
# Three.js / R3F (React Three Fiber)
|
||||
|
||||
> Web 3D 표준. **Three.js (vanilla) / React Three Fiber (React)** + **drei (helpers)** + **Rapier (physics)**. WebGL → WebGPU 미래.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Scene: 3D 공간.
|
||||
- Mesh: geometry + material.
|
||||
- Camera: 시점.
|
||||
- Light: 조명.
|
||||
- Renderer: WebGL / WebGPU.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### 기본 R3F
|
||||
```tsx
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { OrbitControls, Box } from '@react-three/drei';
|
||||
import { useRef } from 'react';
|
||||
|
||||
function RotatingBox() {
|
||||
const ref = useRef<THREE.Mesh>(null);
|
||||
useFrame((_, delta) => {
|
||||
ref.current!.rotation.y += delta;
|
||||
});
|
||||
return (
|
||||
<Box ref={ref} args={[1, 1, 1]}>
|
||||
<meshStandardMaterial color="orange" />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Scene() {
|
||||
return (
|
||||
<Canvas camera={{ position: [3, 3, 5] }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||
<RotatingBox />
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### drei (helpers)
|
||||
```tsx
|
||||
import {
|
||||
OrbitControls, Environment, ContactShadows, Float,
|
||||
Text, Html, useGLTF, useTexture, PerspectiveCamera, Stage,
|
||||
} from '@react-three/drei';
|
||||
|
||||
<Canvas>
|
||||
<Stage>
|
||||
<Float speed={2} rotationIntensity={0.5}>
|
||||
<mesh>...</mesh>
|
||||
</Float>
|
||||
</Stage>
|
||||
|
||||
<Text fontSize={1} color="white">Hello 3D</Text>
|
||||
|
||||
<Html position={[0, 2, 0]}>
|
||||
<div>HTML overlay</div>
|
||||
</Html>
|
||||
</Canvas>
|
||||
```
|
||||
|
||||
### GLTF 모델 로드
|
||||
```tsx
|
||||
function Model() {
|
||||
const { scene, animations } = useGLTF('/model.glb');
|
||||
return <primitive object={scene} />;
|
||||
}
|
||||
|
||||
// Optimization — useGLTF preload
|
||||
useGLTF.preload('/model.glb');
|
||||
```
|
||||
|
||||
→ DRACO compression 자동 (drei 가).
|
||||
|
||||
### Animation (모델)
|
||||
```tsx
|
||||
import { useAnimations } from '@react-three/drei';
|
||||
|
||||
function AnimatedModel() {
|
||||
const ref = useRef<THREE.Group>(null);
|
||||
const { scene, animations } = useGLTF('/dancer.glb');
|
||||
const { actions } = useAnimations(animations, ref);
|
||||
|
||||
useEffect(() => {
|
||||
actions.idle?.play();
|
||||
}, []);
|
||||
|
||||
return <primitive ref={ref} object={scene} />;
|
||||
}
|
||||
```
|
||||
|
||||
### Physics (Rapier)
|
||||
```tsx
|
||||
import { Physics, RigidBody } from '@react-three/rapier';
|
||||
|
||||
<Canvas>
|
||||
<Physics gravity={[0, -9.81, 0]}>
|
||||
<RigidBody>
|
||||
<Box position={[0, 5, 0]} />
|
||||
</RigidBody>
|
||||
<RigidBody type="fixed">
|
||||
<Plane args={[10, 10]} rotation={[-Math.PI / 2, 0, 0]} />
|
||||
</RigidBody>
|
||||
</Physics>
|
||||
</Canvas>
|
||||
```
|
||||
|
||||
### Material
|
||||
```tsx
|
||||
<mesh>
|
||||
<boxGeometry args={[1, 1, 1]} />
|
||||
<meshStandardMaterial
|
||||
color="hotpink"
|
||||
metalness={0.5}
|
||||
roughness={0.3}
|
||||
map={texture}
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
// MeshPhysicalMaterial — 더 사실적
|
||||
<meshPhysicalMaterial
|
||||
clearcoat={1}
|
||||
transmission={0.5} // 반투명 / glass
|
||||
thickness={0.5}
|
||||
ior={1.5}
|
||||
/>
|
||||
```
|
||||
|
||||
### Shader (custom GLSL)
|
||||
```glsl
|
||||
// vertex.glsl
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
|
||||
// fragment.glsl
|
||||
varying vec2 vUv;
|
||||
uniform float uTime;
|
||||
void main() {
|
||||
vec3 color = vec3(sin(vUv.x + uTime), sin(vUv.y + uTime), 1.0);
|
||||
gl_FragColor = vec4(color, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
<mesh>
|
||||
<planeGeometry args={[2, 2]} />
|
||||
<shaderMaterial
|
||||
vertexShader={vertexShader}
|
||||
fragmentShader={fragmentShader}
|
||||
uniforms={{ uTime: { value: 0 } }}
|
||||
/>
|
||||
</mesh>
|
||||
```
|
||||
|
||||
### Post-processing
|
||||
```tsx
|
||||
import { EffectComposer, Bloom, ChromaticAberration } from '@react-three/postprocessing';
|
||||
|
||||
<Canvas>
|
||||
<Scene />
|
||||
<EffectComposer>
|
||||
<Bloom intensity={1.5} luminanceThreshold={0.5} />
|
||||
<ChromaticAberration offset={[0.001, 0.001]} />
|
||||
</EffectComposer>
|
||||
</Canvas>
|
||||
```
|
||||
|
||||
### Performance
|
||||
```tsx
|
||||
// Instances (수천 mesh)
|
||||
import { Instances, Instance } from '@react-three/drei';
|
||||
|
||||
<Instances limit={1000}>
|
||||
<boxGeometry />
|
||||
<meshStandardMaterial />
|
||||
{data.map((d, i) => (
|
||||
<Instance key={i} position={d.pos} color={d.color} />
|
||||
))}
|
||||
</Instances>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// LOD (Level of Detail)
|
||||
import { Detailed } from '@react-three/drei';
|
||||
|
||||
<Detailed distances={[0, 10, 20]}>
|
||||
<HighDetailMesh />
|
||||
<MidDetailMesh />
|
||||
<LowDetailMesh />
|
||||
</Detailed>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Frustum culling 자동
|
||||
// Manual: ref.current.frustumCulled = true (default)
|
||||
|
||||
// Shadows — 비싸. 작은 set 만
|
||||
<directionalLight castShadow shadow-mapSize={[1024, 1024]} />
|
||||
<mesh receiveShadow castShadow>
|
||||
```
|
||||
|
||||
### Mobile / WebGL fallback
|
||||
```tsx
|
||||
// 작은 화면 / GPU = quality down
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
<Canvas dpr={isMobile ? 1 : 2} shadows={!isMobile}>
|
||||
```
|
||||
|
||||
### WebGPU (미래)
|
||||
```tsx
|
||||
import { WebGPURenderer } from 'three/examples/jsm/renderers/webgpu/WebGPURenderer.js';
|
||||
|
||||
<Canvas
|
||||
gl={(canvas) => new WebGPURenderer({ canvas })}
|
||||
>
|
||||
```
|
||||
|
||||
→ WebGL 보다 빠름. 2025+ stable.
|
||||
|
||||
### React-spring + R3F
|
||||
```tsx
|
||||
import { useSpring, animated } from '@react-spring/three';
|
||||
|
||||
const { position } = useSpring({ position: hovered ? [0, 1, 0] : [0, 0, 0] });
|
||||
<animated.mesh position={position} ... />
|
||||
```
|
||||
|
||||
### 화면 → 3D coordinates
|
||||
```ts
|
||||
import { useThree } from '@react-three/fiber';
|
||||
|
||||
function ClickToWorld() {
|
||||
const { camera, raycaster, mouse } = useThree();
|
||||
|
||||
const handle = (e: MouseEvent) => {
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
// raycaster.ray 로 intersection
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Spline (no-code 3D)
|
||||
```tsx
|
||||
import Spline from '@splinetool/react-spline';
|
||||
|
||||
<Spline scene="https://prod.spline.design/xxx/scene.splinecode" />
|
||||
```
|
||||
|
||||
→ Designer 가 Spline app 에서 만들고 R3F 가 import.
|
||||
|
||||
### Bundle size 주의
|
||||
```
|
||||
Three.js: ~600KB (큰)
|
||||
+ drei, postprocessing, rapier ...
|
||||
|
||||
→ Dynamic import + code split
|
||||
```
|
||||
|
||||
```tsx
|
||||
const Scene = lazy(() => import('./Scene'));
|
||||
<Suspense fallback={<Loader />}>
|
||||
<Scene />
|
||||
</Suspense>
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| Product showcase | R3F + drei + GLTF |
|
||||
| Game | R3F + Rapier physics |
|
||||
| Data viz | R3F + custom shader |
|
||||
| Designer 만든 scene | Spline |
|
||||
| 매우 simple (1-2 model) | <model-viewer> |
|
||||
| 강력 / vanilla | Three.js direct |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **모든 frame setState**: re-render. useFrame 안 ref.
|
||||
- **Shadow 모든 light + 큰 mapSize**: 60fps 깨짐.
|
||||
- **너무 많은 mesh (1000+)**: Instances.
|
||||
- **모델 압축 안 함 (GLB 100MB)**: load 느림. DRACO + Meshopt.
|
||||
- **WebGL fallback 없음 + GPU 약**: blank 화면.
|
||||
- **Mobile 무 dpr 2**: 발열.
|
||||
- **Memory leak (useEffect cleanup 없음)**: GPU resources 안 release.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- R3F + drei + Rapier 가 표준 stack.
|
||||
- GLTF + DRACO compression.
|
||||
- Instances / LOD / shadow 절제.
|
||||
- Suspense + lazy load.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Frontend_Animation_Motion]]
|
||||
- [[Web_Performance_Core_Vitals]]
|
||||
- [[Perf_Bundle_Analysis]]
|
||||
Reference in New Issue
Block a user