Files
game_engine/docs/superpowers/specs/2026-03-25-phase5-2-rigidbody.md
2026-03-25 11:37:16 +09:00

148 lines
4.5 KiB
Markdown

# Phase 5-2: Rigid Body Simulation — Design Spec
## Overview
`voltex_physics`에 리지드바디 시뮬레이션을 추가한다.
Semi-implicit Euler 적분 + 임펄스 기반 충돌 응답으로, 물체가 떨어지고 충돌하여 튕기는 기본 물리를 구현한다.
## Scope
- RigidBody 컴포넌트 (질량, 속도, 반발 계수)
- 중력 + Semi-implicit Euler 적분
- 임펄스 기반 충돌 응답 (선형만)
- 위치 보정 (penetration resolution)
- physics_step 통합 함수
## Out of Scope
- 각속도/회전 물리 (관성 텐서 필요, 추후 추가)
- 마찰 (Coulomb friction)
- Sequential Impulse 솔버
- 연속 충돌 감지 (CCD)
- Sleep/Island 시스템
## Module Structure
기존 `voltex_physics`에 추가:
```
crates/voltex_physics/src/
├── (기존) collider.rs, contact.rs, narrow.rs, bvh.rs, collision.rs, lib.rs
├── rigid_body.rs — RigidBody 컴포넌트, PhysicsConfig
├── integrator.rs — Semi-implicit Euler 적분
└── solver.rs — 임펄스 기반 충돌 응답 + 위치 보정 + physics_step
```
## Types
### RigidBody (ECS 컴포넌트)
```rust
#[derive(Debug, Clone, Copy)]
pub struct RigidBody {
pub velocity: Vec3,
pub angular_velocity: Vec3,
pub mass: f32,
pub restitution: f32,
pub gravity_scale: f32,
}
```
- `mass == 0.0` → 정적 물체 (무한 질량, 중력 영향 없음, 움직이지 않음)
- `mass > 0.0` → 동적 물체
- `restitution` — 반발 계수 (0.0 = 완전 비탄성, 1.0 = 완전 탄성)
- `gravity_scale` — 중력 배율 (기본 1.0)
- `angular_velocity` — 필드만 존재, 이번 Phase에서는 적분하지 않음
**Methods:**
- `dynamic(mass: f32) -> Self` — 동적 물체 생성 (velocity=0, restitution=0.3, gravity_scale=1.0)
- `statik() -> Self` — 정적 물체 생성 (mass=0, velocity=0, restitution=0.3)
- `inv_mass(&self) -> f32` — mass가 0이면 0.0, 아니면 1.0/mass
- `is_static(&self) -> bool` — mass == 0.0
### PhysicsConfig
```rust
pub struct PhysicsConfig {
pub gravity: Vec3,
pub fixed_dt: f32,
}
```
- `default()` → gravity = (0, -9.81, 0), fixed_dt = 1.0/60.0
## Functions
### integrate (integrator.rs)
```rust
pub fn integrate(world: &mut World, config: &PhysicsConfig)
```
Transform + RigidBody를 가진 동적 entity 순회:
1. `velocity += gravity * gravity_scale * dt`
2. `position += velocity * dt`
정적 물체(mass == 0.0)는 건너뜀.
### resolve_collisions (solver.rs)
```rust
pub fn resolve_collisions(world: &mut World, contacts: &[ContactPoint])
```
각 ContactPoint에 대해:
1. 두 entity의 RigidBody 조회 (없으면 스킵)
2. 상대 속도 계산: `v_rel = velocity_a - velocity_b`
3. 법선 방향 성분: `v_rel_n = v_rel · normal`
4. 분리 중이면 스킵: `v_rel_n > 0`
5. 반발 계수: `e = min(restitution_a, restitution_b)`
6. 임펄스 크기: `j = -(1 + e) * v_rel_n / (inv_mass_a + inv_mass_b)`
7. 속도 업데이트: `v_a += j * normal * inv_mass_a`, `v_b -= j * normal * inv_mass_b`
**위치 보정 (Positional Correction):**
- 침투 깊이(depth)에 비례하여 물체를 법선 방향으로 분리
- Baumgarte stabilization: `correction = max(depth - slop, 0) * percent / (inv_mass_a + inv_mass_b)`
- slop = 0.01 (작은 침투 허용), percent = 0.4 (보정 비율)
- `pos_a -= correction * inv_mass_a * normal`, `pos_b += correction * inv_mass_b * normal`
### physics_step (solver.rs)
```rust
pub fn physics_step(world: &mut World, config: &PhysicsConfig)
```
1. `integrate(world, config)`
2. `let contacts = detect_collisions(world)`
3. `resolve_collisions(world, &contacts)`
## ECS Integration Notes
- RigidBody는 Transform + Collider와 함께 entity에 추가
- 물리 스텝은 게임 루프에서 fixed_update 타이밍에 호출
- detect_collisions는 Phase 5-1의 기존 함수 재사용
## Conventions
- 단위: SI (미터, 초, 킬로그램)
- 중력: (0, -9.81, 0) — Y-up 좌표계
- 법선: Phase 5-1과 동일 (entity_a → entity_b 방향)
## Test Plan
### rigid_body.rs
- `dynamic()`: 기본값 확인, inv_mass 정확도
- `statik()`: mass=0, inv_mass=0, is_static=true
- `with_restitution`: 설정 반영
### integrator.rs
- 중력 낙하: 1프레임 후 위치 = (0, -9.81/60/60 ≈ position 변화)
- 정적 물체: integrate 후 위치 불변
- 초기 속도: velocity가 있는 경우 position 변화 확인
### solver.rs
- 두 동적 구체 정면 충돌: 속도 반전 확인
- 동적 구체 vs 정적 바닥: 구체만 튕김, 바닥 불변
- 위치 보정: 침투 깊이만큼 분리
- physics_step E2E: 구체가 바닥 위에서 낙하 → 충돌 → 반발