React Three Fiber 기술 분석(2/5)
Three.jsReact Three Fiber 기술 분석 - Explode View 구현
3D 모델의 각 파츠를 중심에서 바깥으로 펼쳐서 내부 구조를 보여주는 분해 뷰 기능을 구현합니다.
2025-02-05
5 min read
#React Three Fiber#Three.js#애니메이션#3D
React Three Fiber 기술 분석시리즈 목차
개요
3D 모델의 각 파츠를 중심에서 바깥으로 펼쳐서 내부 구조를 보여주는 기능입니다.
구현 원리
핵심 아이디어: 각 메쉬를 모델 중심에서 바깥 방향으로 이동
[조립 상태] [분해 상태]
┌─┐ ┌─┐
┌─┼─┼─┐ ↗ │ │ ↖
│ │ │ │ ────→ ┌─┐ └─┘ ┌─┐
└─┼─┼─┘ │ │ │ │
└─┘ └─┘ └─┘
단계별 구현
1단계: 방향 벡터 계산
각 메쉬가 어느 방향으로 이동해야 하는지 계산합니다.
// 모델 로드 시 한 번만 실행
const calculateExplodeDirections = (meshes: THREE.Mesh[]) => {
// 전체 모델의 중심점 계산
const boundingBox = new THREE.Box3();
meshes.forEach(mesh => boundingBox.expandByObject(mesh));
const center = boundingBox.getCenter(new THREE.Vector3());
return meshes.map(mesh => {
const meshCenter = new THREE.Vector3();
new THREE.Box3().setFromObject(mesh).getCenter(meshCenter);
// 중심 → 메쉬 방향 = 폭발 방향
const direction = meshCenter.clone().sub(center).normalize();
return {
mesh,
originalPosition: mesh.position.clone(),
direction,
};
});
};
2단계: useFrame으로 위치 업데이트
R3F의 useFrame은 매 프레임마다 실행됩니다. 여기서 위치를 업데이트합니다.
useFrame(() => {
const { explodeLevel } = useModelStore.getState();
parts.forEach(({ mesh, originalPosition, direction }) => {
// 목표 위치 = 원래 위치 + (방향 × 분해 정도 × 거리)
const targetPosition = originalPosition.clone().add(
direction.clone().multiplyScalar(explodeLevel * EXPLODE_DISTANCE)
);
// 부드러운 보간
mesh.position.lerp(targetPosition, 0.1);
});
});
lerp를 사용하는 이유
Vector3.lerp(target, alpha)는 현재 위치에서 목표까지 alpha 비율만큼 이동합니다.
alpha = 0.1일 때:
프레임 1: 0 → 0.1 (10% 이동)
프레임 2: 0.1 → 0.19 (남은 거리의 10%)
프레임 3: 0.19 → 0.27
...
처음엔 빠르게, 목표에 가까워질수록 느려지는 자연스러운 easing 효과를 줍니다.
alpha 값에 따른 체감
| alpha | 체감 |
|---|---|
| 0.05 이하 | 너무 느림 |
| 0.1~0.15 | 자연스러움 |
| 0.3 이상 | 기계적인 느낌 |
대안 비교
Tween 라이브러리 (GSAP, Tween.js)
gsap.to(mesh.position, {
x: targetX,
duration: 0.5,
ease: "power2.out"
});
장점: 정교한 easing, 시퀀스 제어
단점: 슬라이더로 실시간 제어하려면 매번 tween을 취소하고 다시 생성해야 함
결론
슬라이더 실시간 제어 → lerp 방식이 적합
버튼 클릭 애니메이션 → Tween이 적합
실제 구현 코드
// components/TestModel.tsx
const SelectableMesh = ({ part, index }: Props) => {
const meshRef = useRef<THREE.Mesh>(null);
const { explodeLevel, meshPositions } = useModelStore();
const originalPosition = useRef(new THREE.Vector3());
const explodeDirection = useRef(new THREE.Vector3());
useEffect(() => {
if (meshRef.current) {
originalPosition.current.copy(meshRef.current.position);
explodeDirection.current
.copy(originalPosition.current)
.normalize();
}
}, []);
useFrame(() => {
if (!meshRef.current) return;
// 수동 이동 위치가 있으면 그걸 기준으로
const basePos = meshPositions[part.id]
? new THREE.Vector3(
meshPositions[part.id].x,
meshPositions[part.id].y,
meshPositions[part.id].z
)
: originalPosition.current.clone();
// 분해 오프셋 적용
const targetPosition = basePos.add(
explodeDirection.current.clone().multiplyScalar(explodeLevel * 2)
);
meshRef.current.position.lerp(targetPosition, 0.1);
});
return <mesh ref={meshRef} geometry={part.geometry} />;
};
휠 컨트롤 연동
슬라이더 외에 Shift + 마우스 휠로도 분해 정도를 조절할 수 있습니다.
const handleWheel = useCallback((e: WheelEvent) => {
if (!e.shiftKey) return; // 일반 휠은 줌으로 사용
e.preventDefault();
const delta = e.deltaY > 0 ? -0.05 : 0.05;
const newLevel = Math.max(0, Math.min(1, explodeLevel + delta));
setExplodeLevel(newLevel);
}, [explodeLevel, setExplodeLevel]);
// passive: false로 preventDefault 허용
container.addEventListener('wheel', handleWheel, { passive: false });
개선 가능한 점
조립 순서 기반 분해
현재는 "중심에서 바깥으로" 단순 방식입니다. 실제 조립 순서를 반영하면 더 직관적입니다.
// 조립 순서가 높은 부품(나중에 조립)이 먼저 분해
const adjustedLevel = Math.max(0, (level - (1 - orderRatio)) * 2);
분해 방향 커스터마이징
특정 부품은 위로, 특정 부품은 옆으로 분해되도록 방향을 지정할 수 있습니다.
// 모델 메타데이터에 분해 방향 포함
const customDirection = part.explodeDirection || calculateDefaultDirection();
시리즈: React Three Fiber 기술 분석
- 개요
- Explode View 구현 ← 현재 글
- 메쉬 선택/이동
- 포스트 프로세싱
- Zustand 상태 관리