본문 바로가기
Graphic/R3F

65 React Three Rapier

by curious week 2025. 8. 19.

React Three Rapier 정리 (Physics in R3F)

 

1. Cannon.js → Rapier

  • Cannon.js / cannon-es: 오래된 물리 엔진, PMNDRS에서 fork(cannon-es) 유지 중.
  • Rapier:
    • 2019년 Rust로 작성 → WebAssembly 덕분에 브라우저에서 거의 네이티브 성능.
    • Determinism: 동일 조건 → 모든 기기에서 동일한 결과.
    • 2D/3D 모두 지원.
    • Three.js에 종속되지 않음 (독립 엔진).
    • 공식 사이트: rapier.rs

2. React Three Rapier

  • PMNDRS 팀이 R3F에 Rapier를 래핑한 라이브러리.
  • 설치:→ 특정 버전 고정 (버그 최소화 목적).
npm install @react-three/rapier@2.0

3. 기본 사용법

<Physics>

import { Physics } from '@react-three/rapier';

<Physics>{/* 여기에 들어오는 모든 객체는 물리 시뮬레이션 적용 */}</Physics>;

<RigidBody>

import { RigidBody } from '@react-three/rapier';

<Physics>
  {/* 동적 객체 (기본값: dynamic) */}
  <RigidBody>
    <mesh castShadow position={[-2, 2, 0]}>
      <sphereGeometry />
      <meshStandardMaterial color="orange" />
    </mesh>
  </RigidBody>

  {/* 고정 객체 (type="fixed") */}
  <RigidBody type="fixed">
    <mesh receiveShadow position-y={-1.25}>
      <boxGeometry args={[10, 0.5, 10]} />
      <meshStandardMaterial color="greenyellow" />
    </mesh>
  </RigidBody>
</Physics>;
  • type (string)
    • "dynamic" (기본): 중력, 충돌 적용됨.
    • "fixed": 움직이지 않는 오브젝트.
    • "kinematicPosition" / "kinematicVelocity": 직접 위치·속도를 제어하는 경우.

4. 디버그 모드

<Physics debug>{/* wireframe 형태로 실제 Collider 시각화 */}</Physics>
  • 주의: 성능 저하 발생 → 개발 중에만 사용.

5. Collider 종류

자동 생성

colliders="cuboid" or ''

  • cuboid (기본값)

colliders="ball"

  • ball → 구체 충돌

colliders="hull"

  • hull → Convex Hull (구멍 무시), 전체 면을 감싼 모양

colliders="trimesh"

  • trimesh → 삼각형 메시 기반 (동적 객체엔 비추천, 성능 이슈)
<RigidBody colliders="ball">
  <mesh>
    <sphereGeometry />
    <meshStandardMaterial color="orange" />
  </mesh>
</RigidBody>

수동 생성

import { CuboidCollider, BallCollider } from '@react-three/rapier';

<RigidBody colliders={false}>
  <mesh>
    <torusGeometry args={[1, 0.5, 16, 32]} />
  </mesh>
  {/* 직접 Collider 추가 */}
  <CuboidCollider args={[1.5, 1.5, 0.5]} />
  <BallCollider args={[1.5]} />
</RigidBody>;

 

  • collider는 scale을 따로 설정할 수 없음.
  • 기본적으로 cube가 적용되어 있으므로 colliders={false} 설정 후 적용
  • args (Array) → Collider별 고유 파라미터

1. CuboidCollider

  • <CuboidCollider args={[x, y, z]} />
    • x, y, z: half extents (절반 크기).
    • 실제 충돌체의 크기는 2x * 2y * 2z.
    • 예: args={[1, 2, 3]} → 실제 크기 = (2, 4, 6).

2. BallCollider

  • <BallCollider args={[r]} />
    • r: 반지름(radius).
    • 예: args={[1.5]} → 반지름 1.5짜리 구 충돌체.

3. CylinderCollider

  • <CylinderCollider args={[halfHeight, radius]} />
    • halfHeight: 중심에서 위·아래로의 절반 높이.
    • radius: 원통 반지름.
    • 실제 높이는 halfHeight * 2.

6. RigidBody 제어

참조 및 힘/토크 적용

const cube = useRef()

<RigidBody ref={cube} position={[1.5, 2, 0]}>
  <mesh onClick={cubeJump}>
    <boxGeometry />
    <meshStandardMaterial color="mediumpurple" />
  </mesh>
</RigidBody>

const cubeJump = () => {
  // 위쪽으로 순간적인 힘 (Impulse)
  cube.current.applyImpulse({ x: 0, y: 5, z: 0 })
  // 랜덤 회전
  cube.current.applyTorqueImpulse({
    x: Math.random() - 0.5,
    y: Math.random() - 0.5,
    z: Math.random() - 0.5,
  })
}
  • Impulse 계열 → “짧고 순간적인 충격”
  • Force 계열 → “계속 누르는 힘”
  • set 계열 → “물리 무시하고 강제 값 지정”

RigidBody 주요 제어 메서드

메서드 | 순간적 / 지속적 | 직선 / 회전 | 설명

applyImpulse 순간 직선 “점프, 충격”
addForce 지속 직선 “바람, 추진력”
applyTorqueImpulse 순간 회전 “랜덤 회전 충격”
addTorque 지속 회전 “계속 도는 바퀴”
setTranslation 직접 위치 순간이동
setRotation 직접 회전 강제 회전
setLinvel 직접 속도 선속도 제어
setAngvel 직접 회전속도 각속도 제어
resetForces 힘 초기화
resetTorques 회전력 초기화

6-1. applyImpulse(impulse: Vector3, wakeUp = true)

  • 역할: 순간적인 힘(Impulse) → 물체를 한 번 “퉁!” 치는 효과.
  • 인자
    • impulse: { x: number, y: number, z: number }
    • wakeUp?: boolean (기본 true) → 수면(sleep) 중인 물체를 깨움
  • 사용 시기
    • 점프, 총알에 맞음, 폭발 충격 같은 짧고 순간적인 힘.
cube.current.applyImpulse({ x: 0, y: 5, z: 0 })

6-2. addForce(force: Vector3, wakeUp = true)

  • 역할: 지속적인 힘 → 매 프레임 누적되는 힘.
  • 인자
    • force: { x: number, y: number, z: number }
  • 사용 시기
    • 중력 외의 지속적 영향: 바람, 끌어당김, 제트 추진 등.
cube.current.addForce({ x: 1, y: 0, z: 0 })

6-3. applyTorqueImpulse(torque: Vector3, wakeUp = true)

  • 역할: 회전 충격(순간적인 회전 힘).
  • 인자
    • torque: { x: number, y: number, z: number }
  • 사용 시기
    • 랜덤 회전, 폭발 후 회전, 충돌 반동.
cube.current.applyTorqueImpulse({
  x: Math.random() - 0.5,
  y: Math.random() - 0.5,
  z: Math.random() - 0.5,
})

6-4. addTorque(torque: Vector3, wakeUp = true)

  • 역할: 지속적인 회전 힘.
  • 사용 시기
    • 바퀴 계속 돌리기, 프로펠러, 행성 자전 같은 지속 회전.
cube.current.addTorque({ x: 0, y: 1, z: 0 })

6-5. setTranslation(translation: Vector3, wakeUp = true)

  • 역할: 물체 위치를 직접 “순간이동”.
  • 사용 시기
    • 리셋 버튼 → 원위치 이동.
  • ⚠️ 물리 시뮬레이션 무시하고 강제로 순간이동.
cube.current.setTranslation({ x: 0, y: 5, z: 0 })

6-6. setRotation(rotation: Quaternion, wakeUp = true)

  • 역할: 물체 회전을 직접 설정.
  • 사용 시기
    • 초기화, 방향 강제 설정.
cube.current.setRotation({ x: 0, y: 0, z: 0, w: 1 })

6-7. setLinvel(velocity: Vector3, wakeUp = true)

  • 역할: 선형 속도(linear velocity)를 직접 설정.
  • 사용 시기
    • “지금 이 순간 속도를 이렇게 만들어라” → 즉시 움직임 제어.
    • 캐릭터 이동을 직접 velocity로 제어할 때.
cube.current.setLinvel({ x: 2, y: 0, z: 0 })

6-8. setAngvel(velocity: Vector3, wakeUp = true)

  • 역할: 각속도(angular velocity, 회전 속도) 설정.
  • 사용 시기
    • 바퀴처럼 계속 도는 회전 속도 유지.
cube.current.setAngvel({ x: 0, y: 2, z: 0 })

6-9. resetForces(wakeUp = true) / resetTorques(wakeUp = true)

  • 역할: 지금까지 누적된 힘/토크를 초기화.
  • 사용 시기
    • 갑자기 멈추게 만들고 싶을 때.
    • 컨트롤러에서 힘을 제거할 때.
cube.current.resetForces()
cube.current.resetTorques()

7. 물리 속성

Gravity

<Physics gravity={[0, -9.81, 0]} />
<RigidBody gravityScale={0.2} />  {/* 개별 객체 중력 스케일 */}

Restitution (탄성)

<RigidBody restitution={1} />  {/* 0=안 튐, 1=완벽 반발 */}

Friction (마찰)

<RigidBody friction={0.7} />   {/* 0=미끄럼, 1=빠른 멈춤 */}

Mass

<CuboidCollider args={[0.5, 0.5, 0.5]} mass={2} />
  • mass 값에 따라 충돌 반응·임펄스 강도 달라짐.

8. Kinematic 객체

직접 움직임/회전을 제어할 때 사용.

const twister = useRef()

<RigidBody ref={twister} type="kinematicPosition">
  <mesh scale={[0.4, 0.4, 3]}>
    <boxGeometry />
    <meshStandardMaterial color="red" />
  </mesh>
</RigidBody>

useFrame((state) => {
  const time = state.clock.getElapsedTime()
  twister.current.setNextKinematicTranslation({
    x: Math.cos(time) * 2,
    y: -0.8,
    z: Math.sin(time) * 2,
  })
})

9. 이벤트 (Event Hooks)

<RigidBody
  onCollisionEnter={() => console.log('collision')}
  onCollisionExit={() => console.log('exit')}
  onSleep={() => console.log('sleep')}
  onWake={() => console.log('wake')}
/>
  • 충돌 이벤트 (onCollisionEnter, onCollisionExit) → “다른 물체와 부딪히고/떨어질 때”
  • 수면 이벤트 (onSleep, onWake) → “물리 엔진 최적화 상태(멈춤 ↔ 움직임)”

RigidBody 이벤트 (Event Hooks)

이벤트 | 발생 시점 | 인자 | 활용 예시

onCollisionEnter 다른 RigidBody와 처음 부딪힐 때 event: { target, other, manifold } 점프 가능 여부, 충돌 트리거
onCollisionExit 충돌 후 떨어져 분리될 때 event: { target, other } 바닥 이탈 감지, 상태 전환
onSleep 물체가 충분히 멈추고 수면 상태로 들어갈 때 없음 최적화 체크, “정지 애니메이션”
onWake 수면 상태였다가 다시 움직일 때 없음 효과음, 파티클 시작

9-1. onCollisionEnter

  • 역할: 다른 RigidBody와 처음 충돌했을 때 한 번 발생.
  • 인자
    • (event: CollisionEnterPayload)
    • 주요 속성:
      • target : 내 RigidBody
      • other : 충돌한 상대 RigidBody
      • manifold : 충돌 지점/법선 등 상세 정보
  • 사용 시기
    • 플레이어가 바닥에 닿았는지 체크 (점프 가능 여부)
    • 총알이 적에게 맞았을 때 이벤트 트리거
  • 예시
<RigidBody
  onCollisionEnter={(e) => {
    console.log('충돌 시작!', e.other.rigidBodyObject.name)
  }}
/>

9-2. onCollisionExit

  • 역할: 충돌하고 있던 두 RigidBody가 분리될 때 발생.
  • 인자
    • (event: CollisionExitPayload)
    • target, other 동일
  • 사용 시기
    • 바닥에서 떨어졌는지 감지 → 점프 불가능 상태로 전환
    • 적과 부딪힌 뒤 다시 떨어졌는지 체크
  • 예시
<RigidBody
  onCollisionExit={() => {
    console.log('충돌 종료 (바닥에서 떨어짐)')
  }}
/>

9-3. onSleep

  • 역할: RigidBody가 움직임이 멈추고 수면 상태로 들어갈 때 발생.
  • 인자 없음
  • 사용 시기
    • 물리 시뮬레이션 최적화 확인용
    • “가만히 멈췄을 때 애니메이션 전환” (예: 쓰러진 박스가 멈추면 먼지 파티클 발생)
  • 예시
<RigidBody
  onSleep={() => console.log('정지 상태 (sleep)')}
/>

9-4. onWake

  • 역할: 수면 상태였던 RigidBody가 다시 움직이기 시작할 때 발생.
  • 인자 없음
  • 사용 시기
    • 멈춰 있던 오브젝트가 외부 힘(충돌/중력 등)으로 다시 움직일 때 이벤트 처리
    • 깨어난 물체에 효과음/파티클 추가
  • 예시
<RigidBody
  onWake={() => console.log('움직이기 시작 (wake)')}
/>

10. 모델 적용

const hamburger = useGLTF('./hamburger.glb')

<RigidBody colliders="hull" position={[0, 4, 0]}>
  <primitive object={hamburger.scene} scale={0.25} />
</RigidBody>
  • 모델은 자동으로 각 파트별 collider 생성.
  • colliders="hull" / "trimesh"로 수정 가능.

11. InstancedRigidBodies (대량 처리)

const cubesCount = 100
const instances = useMemo(() =>
  Array.from({ length: cubesCount }, (_, i) => ({
    key: 'cube_' + i,
    position: [(Math.random() - 0.5) * 8, 6 + i * 0.2, (Math.random() - 0.5) * 8],
    rotation: [Math.random(), Math.random(), Math.random()],
  }))
, [])

<InstancedRigidBodies instances={instances}>
  <instancedMesh castShadow receiveShadow args={[null, null, cubesCount]}>
    <boxGeometry />
    <meshStandardMaterial color="tomato" />
  </instancedMesh>
</InstancedRigidBodies>
  • InstancedRigidBodies → 물리와 instancedMesh 자동 동기화.
  • 수백 개 이상의 물리 오브젝트도 효율적으로 처리 가능.

결론

  • Rapier + React Three Rapier는 R3F에서 물리 적용을 매우 단순화.
  • 자동 collider → 빠른 prototyping.
  • 수동 collider → 정밀 제어 가능.
  • Forces/Impulse/Kinematics → 게임 로직 유연 구현.
  • InstancedRigidBodies → 대규모 오브젝트 처리 가능.

더 나아가기: 고급 기능

1. Joints (관절 / Joints)

Rapier에는 여러 Joint(조인트, 연결부) 타입이 있어서 서로 다른 RigidBody를 연결할 수 있습니다.
이를 활용하면 관절, 로프, 체인, 로봇 팔, 차량 서스펜션 등을 구현할 수 있어요.

종류

  • FixedJoint
    두 RigidBody를 하나처럼 고정. (焊接된 것처럼 움직임 없음)
  • RevoluteJoint
    한 축을 기준으로만 회전 가능 (예: 문짝 경첩, 바퀴).
  • PrismaticJoint
    한 축 방향으로만 이동 가능 (예: 슬라이딩 도어).
  • SphericalJoint
    3D 공간에서 회전 가능 (예: 볼조인트, 사람 어깨/엉덩이 관절).
  • GenericJoint
    위의 제약을 커스터마이징 가능 (고급).

React Three Rapier 사용 예시

import './style.css';
import ReactDOM from 'react-dom/client';
import { Canvas } from '@react-three/fiber';
import Experience from './Experience.jsx';
import Add from './Add.jsx';
import { Physics } from '@react-three/rapier';
import { Center } from '@react-three/drei';

const root = ReactDOM.createRoot(document.querySelector('#root'));

root.render(
  <Canvas
    shadows
    camera={{
      fov: 45,
      near: 0.1,
      far: 200,
      position: [4, 2, 6],
    }}>
    <Center>
      <Physics debug>
        <Add />
      </Physics>
    </Center>
  </Canvas>,
);
import { Physics, RigidBody, useRevoluteJoint } from '@react-three/rapier';
import { useEffect, useRef } from 'react';
import { Perf } from 'r3f-perf';
import { OrbitControls } from '@react-three/drei';

const Add = () => {
  const bodyA = useRef(); // 고정 틀
  const bodyB = useRef(); // 매달린 공(동적)

  // ── 힌지 정의 ─────────────────────────────────────────────
  // - 앵커/축은 양쪽 바디의 "로컬 좌표계" 기준 (docs)
  // - 여기선: 틀 큐브의 '아래쪽 면 중앙' ↔ 공의 '윗면(반지름만큼 위)'
  //   * 틀 큐브 크기: 0.2 → half = 0.1 ⇒ [0, -0.1, 0]
  //   * 공 반지름: 0.2          ⇒ [0,  0.2, 0]
  // - 회전축: Z축 기준 힌지 → [0, 0, 1]
  const joint = useRevoluteJoint(bodyA, bodyB, [
    [0, -0.1, 0], // bodyA 로컬 앵커
    [0, 0.2, 0], // bodyB 로컬 앵커
    [0, 0, 1], // 회전축(로컬)
  ]);

  useEffect(() => {
    if (!joint.current) return;
    // -90° ~ +90° 리밋
    joint.current.setLimits(-Math.PI / 2, Math.PI / 2);

    // 살짝 밀어서 시작(정지 상태 방지)
    bodyB.current?.applyImpulse({ x: 30, y: 0, z: 0 }, true);
  }, [joint]);

  // ── 초기 위치 정렬 팁 ─────────────────────────────────────
  // 월드에서 앵커가 맞닿게 하려면:
  //    posB = posA + anchorA - anchorB   (성분별)
  // 아래에서 posA=[0,2,0], anchorA=[0,-0.1,0], anchorB=[0,0.2,0] 이므로
  //    posB.y = 2 + (-0.1) - 0.2 = 1.7

  return (
    <>
      <Perf position="top-left" />
      <OrbitControls makeDefault />

      <directionalLight castShadow position={[1, 2, 3]} intensity={4.5} />
      <ambientLight intensity={1.5} />

      {/* 고정된 바(작은 큐브) */}
      <RigidBody
        ref={bodyA}
        type="fixed"
        colliders="cuboid"
        position={[0, 2, 0]}>
        <mesh>
          <boxGeometry args={[0.2, 0.2, 0.2]} />
          <meshStandardMaterial color="gray" />
        </mesh>
      </RigidBody>

      {/* 매달린 공(동적) */}
      <RigidBody ref={bodyB} colliders="ball" position={[0, 1.7, 0]}>
        <mesh>
          <sphereGeometry args={[0.2, 16, 16]} />
          <meshStandardMaterial color="orange" />
        </mesh>
      </RigidBody>
    </>
  );
};

export default Add;

👉 이렇게 하면 추에 매달린 진자(Pendulum) 를 만들 수 있습니다.
여러 개 연결하면 로프, 체인, 뱀 같은 구조물도 가능.


2. HeightfieldCollider (지형 충돌체)

일반적으로 큰 Terrain(산/언덕)을 모델링하고 TrimeshCollider로 쓰면 무겁고 충돌 버그가 생기기 쉽습니다.
→ 대신 Rapier는 HeightfieldCollider라는 최적화된 방식을 제공합니다.

원리

  • Grid 기반으로, 각 격자의 높이만 기록.
  • 내부는 단순한 Heightmap 데이터 (2D 배열).
  • 구멍이나 동굴은 불가능 (2D 배열로 표면만 표현).

사용 예시

import './style.css';
import ReactDOM from 'react-dom/client';
import { Canvas } from '@react-three/fiber';
import { Physics } from '@react-three/rapier';
import { Center } from '@react-three/drei';

import Test from './Test.jsx';

const root = ReactDOM.createRoot(document.querySelector('#root'));

root.render(
  <Canvas
    shadows
    camera={{
      fov: 45,
      near: 0.1,
      far: 200,
      position: [4, 2, 6],
    }}>
    <Center>
      <Physics debug>
        <Test />
      </Physics>
    </Center>
  </Canvas>,
);
import * as THREE from 'three';
import { useMemo } from 'react';
import { HeightfieldCollider, RigidBody } from '@react-three/rapier';
import { OrbitControls } from '@react-three/drei';

export default function Test() {
  const H2D = [
    [0.0, 0.5, 1.0, 0.2],
    [0.2, 0.8, 1.5, 0.3],
    [0.1, 0.6, 0.7, 0.4],
  ];

  const { width, height, heights1D } = useMemo(() => {
    const rows = H2D.length; // 3
    const cols = H2D[0].length; // 4

    // subdivisions (사각형 수) = 꼭짓점-1
    const width = cols - 1; // X 방향 subdivs = 3
    const height = rows - 1; // Z 방향 subdivs = 2

    // column-major(flatten): c 먼저, r 다음
    const flat = new Float32Array(rows * cols);
    let k = 0;
    for (let c = 0; c < cols; c++) {
      for (let r = 0; r < rows; r++) {
        flat[k++] = H2D[r][c];
      }
    }

    return { width, height, heights1D: flat };
  }, []);
  return (
    <>
      <OrbitControls makeDefault />

      <RigidBody type="fixed" position={[0, 0, 0]}>
        <HeightfieldCollider
          // args: [width, height, heights(1D), scale]
          args={[
            width, // number: X subdivs
            height, // number: Z subdivs
            heights1D, // Float32Array: column-major
            { x: 8, y: 2, z: 6 }, // scale: { x: 가로, y: 높이배율, z: 세로 }
          ]}
        />
      </RigidBody>
      <RigidBody position={[0, 3, 0]} colliders="ball" restitution={0.2}>
        <mesh>
          <sphereGeometry args={[0.25, 16, 16]} />
          <meshStandardMaterial color="orange" />
        </mesh>
      </RigidBody>
    </>
  );
}
  • args[0]: 고도(height) 값 배열 (Float32Array 권장).
  • args[1]: 그리드 크기 (열/행 개수).
  • args[2]: 스케일링 (x, y = 가로 세로 크기, z = 높이 배율).

👉 이 방식은 대규모 Terrain을 다룰 때 매우 가볍고 빠름.
👉 단점: 구멍/터널 표현 불가.


3. Rapier + R3F 발전 상황

  • Rapier 자체는 Rust 기반이라 빠르게 발전 중.
  • react-three-rapier는 PMNDRS 팀이 유지 보수 중.
  • 현재까지 주요 발전 방향:
    1. Joints API 보강 → 더 직관적 사용 가능하도록 개선.
    2. Collider 최적화  hull, trimesh 충돌 안정성 향상.
    3. Event 개선 → 충돌 이벤트에서 상세 정보 제공.
    4. Debug 도구 강화 → 성능 저하 없는 시각화 준비 중.

공식 참고:


정리

  • Joints: 여러 물체 연결 → 로프, 로봇 팔, 탈 것 가능.
  • HeightfieldCollider: Heightmap 기반 지형 충돌체 → 대규모 Terrain 효율적 처리.
  • Rapier + R3F: 빠른 업데이트 진행 중, 특히 Joint/Collider 기능 확장에 집중.

/**
 * Experience (R3F + Rapier)
 * - OrbitControls / Perf: 카메라와 성능 HUD
 * - Physics: 물리 컨텍스트
 * - Dynamic/Fixed/Kinematic rigid bodies + colliders
 */

import { OrbitControls, useGLTF } from '@react-three/drei';
import { Perf } from 'r3f-perf';
import {
  InstancedRigidBodies,
  CuboidCollider,
  RigidBody,
  Physics,
  BallCollider,
  CylinderCollider,
} from '@react-three/rapier';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';

export default function Experience() {
  // [오디오] 충돌 사운드
  // 역할: 충돌 시 소리 재생. 브라우저 자동재생 정책 때문에 user gesture 후 play 권장.
  const [hitSound] = useState(() => new Audio('./hit.mp3'));

  // [Ref] 동적 박스와 트위스터 핸들
  const cube = useRef();
  const twister = useRef();

  // [액션] 점프 + 토크 임펄스
  // 역할: 질량(mass)에 비례한 임펄스를 적용해, 질량이 달라도 비슷한 체감 점프 높이.
  const cubeJump = () => {
    const mass = cube.current.mass(); // number
    cube.current.applyImpulse({ x: 0, y: 5 * mass, z: 0 }); // upward
    cube.current.applyTorqueImpulse({
      x: Math.random() - 0.5,
      y: Math.random() - 0.5,
      z: Math.random() - 0.5,
    });
  };

  // [프레임] Kinematic 바디 구동
  // 역할: type="kinematicPosition" 인 twister를 매 프레임 회전/이동
  useFrame((state) => {
    const time = state.clock.getElapsedTime();

    // 회전: Euler -> Quaternion
    const euler = new THREE.Euler(0, time * 3, 0);
    const q = new THREE.Quaternion().setFromEuler(euler);
    twister.current.setNextKinematicRotation(q);

    // 원형 경로 이동
    const angle = time * 0.5;
    const x = Math.cos(angle) * 2;
    const z = Math.sin(angle) * 2;
    twister.current.setNextKinematicTranslation({ x, y: -0.8, z });
  });

  // [이벤트] 충돌 시작
  // payload: { manifold, target, other, ... } 형태(상세는 라이브러리 타입 참고)
  // type: Parameters<NonNullable<React.ComponentProps<typeof RigidBody>['onCollisionEnter']>>[0]
  const collisionEnter = () => {
    // hitSound.currentTime = 0;
    // hitSound.volume = Math.random();
    // hitSound.play(); // 사용자 입력 이후 호출 권장
  };

  // [모델] GLTF 로드
  const hamburger = useGLTF('./hamburger.glb');

  // [인스턴스드 바디들] 초기 상태 생성
  const cubeCount = 300;
  const cubeInstances = useMemo(() => {
    //  instances Type: Array<{ key: string, position: [number, number, number], rotation: [number, number, number]}>
    // 선택: scale?: [number,number,number]
    // 선택: linearVelocity?: [number,number,number]
    // 선택: angularVelocity?: [number,number,number]

    const instances = [];

    for (let i = 0; i < cubeCount; i++) {
      instances.push({
        key: 'instance' + i,
        position: [
          (Math.random() - 0.5) * 8,
          6 + i * 0.2,
          (Math.random() - 0.5) * 8,
        ],
        rotation: [Math.random(), Math.random(), Math.random()],
      });
    }
    return instances;
  }, [cubeCount]);

  return (
    <>
      <Perf position="top-left" />
      <OrbitControls makeDefault />

      <directionalLight castShadow position={[1, 2, 3]} intensity={4.5} />
      <ambientLight intensity={1.5} />

      <Physics
        // role: 물리 세계 설정
        // props:
        // - gravity?: [number,number,number]
        // - debug?: boolean (Collider wireframe 표시)
        gravity={[0, -9.81, 0]}
        // debug
      >
        {/* [Sphere] Dynamic 바디 + 수동 Collider */}
        <RigidBody
          // colliders="ball" // 자동 생성도 가능하지만…
          colliders={false} // ❗커스텀 콜라이더를 직접 붙일 때는 false
          position={[-1.5, 4, 0]}>
          <mesh castShadow>
            <sphereGeometry /> {/* 기본 반지름 = 1 */}
            <meshStandardMaterial color="orange" />
          </mesh>
          <BallCollider
            // args: [radius]
            args={[1]} // ✅ 올바른 시그니처
            // restitution={0.5} friction={0.5} density={1}
            // sensor // true면 통과하고 이벤트만 발생
          />
        </RigidBody>

        {/* [Cube] Dynamic 바디 */}
        <RigidBody
          ref={cube}
          colliders={false} // 직접 CuboidCollider로 지정
          gravityScale={1} // 개별 중력 배율
          restitution={0} // 튕김
          friction={1} // 마찰
          position={[1.5, 4, 0]} // 초기 위치(런타임 변경 X)
          onCollisionEnter={collisionEnter}
          // onCollisionExit={() => {}}
          // onSleep={() => {}}
          // onWake={() => {}}
        >
          <mesh castShadow onClick={cubeJump}>
            <boxGeometry args={[1, 1, 1]} />
            <meshStandardMaterial color="mediumpurple" />
          </mesh>
          <CuboidCollider
            // args: [hx, hy, hz] (half extents) → box 1×1×1과 정확히 일치
            args={[0.5, 0.5, 0.5]}
            // mass={1} // 또는 density 사용 권장
            // restitution friction density sensor position rotation ...
          />
        </RigidBody>

        {/* [Twister] Kinematic 바디(코드로 구동) */}
        <RigidBody
          ref={twister}
          position={[0, -0.8, 0]}
          friction={0}
          type="kinematicPosition">
          <mesh castShadow scale={[0.4, 0.4, 3]}>
            <boxGeometry />
            <meshStandardMaterial color="red" />
          </mesh>
          {/* ❗Collider 필요. scale 반영해서 half-extent 직접 계산 */}
          <CuboidCollider args={[0.2, 0.2, 1.5]} />
        </RigidBody>

        {/* [Burger] GLTF + 근사 Collider */}
        <RigidBody colliders={false} position={[0, 4, 0]}>
          <primitive object={hamburger.scene} scale={0.25} />
          {/* 주의: Mesh scale은 Collider에 자동 반영되지 않음 */}
          {/* 예) 원래가 halfHeight=0.5, radius=1.25라면, 스케일 0.25 반영값으로 줄여야 정확 */}
          <CylinderCollider
            args={[0.5 * 0.25, 1.25 * 0.25]} // ← 스케일 반영 버전 권장
          />
        </RigidBody>

        {/* [Floor] Fixed 바디(바닥) */}
        <RigidBody type="fixed" restitution={0} friction={1}>
          <mesh receiveShadow position-y={-1.25}>
            <boxGeometry args={[10, 0.5, 10]} />
            <meshStandardMaterial color="greenyellow" />
          </mesh>
        </RigidBody>

        {/* [Walls] Fixed 바디(벽) - Collider만(렌더링 없음) */}
        <RigidBody type="fixed">
          <CuboidCollider args={[5, 2, 0.5]} position={[0, 1, 5.25]} />
          <CuboidCollider args={[5, 2, 0.5]} position={[0, 1, -5.25]} />
          <CuboidCollider args={[0.5, 2, 5]} position={[5.25, 1, 0]} />
          <CuboidCollider args={[0.5, 2, 5]} position={[-5.25, 1, 0]} />
        </RigidBody>

        {/* [Instanced] 대량 박스 */}
        <InstancedRigidBodies instances={cubeInstances}>
          <instancedMesh
            castShadow
            receiveShadow
            args={[null, null, cubeCount]}>
            <boxGeometry />
            <meshStandardMaterial />
          </instancedMesh>
        </InstancedRigidBodies>
      </Physics>
    </>
  );
}

'Graphic > R3F' 카테고리의 다른 글

66 Mini-Game: Marble Race  (2) 2025.08.20
64 Laptop Scene  (1) 2025.08.18
63 후처리(Post Processing)  (5) 2025.08.18
62 Pointer Events (Mouse Events)  (7) 2025.08.18
61 Portal Scene  (0) 2025.08.18