docs: add Phase 5-1 through 6-3 specs, plans, and Cargo.lock
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# Phase 5-1: Collision Detection — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
`voltex_physics` crate에 충돌 감지 시스템을 구현한다.
|
||||
Broad phase (BVH) + Narrow phase (전용 함수) 구조로, Sphere와 Box 콜라이더를 지원한다.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `voltex_math` — Vec3, Mat4, AABB(신규)
|
||||
- `voltex_ecs` — World, Entity, Transform, SparseSet
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
crates/voltex_physics/
|
||||
├── Cargo.toml
|
||||
└── src/
|
||||
├── lib.rs — public exports
|
||||
├── aabb.rs — AABB type (voltex_math에 추가)
|
||||
├── collider.rs — Collider enum
|
||||
├── contact.rs — ContactPoint
|
||||
├── bvh.rs — BvhTree (broad phase)
|
||||
├── narrow.rs — sphere_vs_sphere, sphere_vs_box, box_vs_box
|
||||
└── collision.rs — detect_collisions (ECS 통합)
|
||||
```
|
||||
|
||||
참고: AABB는 `voltex_math`에 추가한다 (스펙에 명시된 기본 수학 타입).
|
||||
|
||||
## Types
|
||||
|
||||
### AABB (voltex_math에 추가)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AABB {
|
||||
pub min: Vec3,
|
||||
pub max: Vec3,
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `new(min, max) -> Self`
|
||||
- `from_center_half_extents(center, half_extents) -> Self`
|
||||
- `center() -> Vec3`
|
||||
- `half_extents() -> Vec3`
|
||||
- `contains_point(point: Vec3) -> bool`
|
||||
- `intersects(other: &AABB) -> bool`
|
||||
- `merged(other: &AABB) -> AABB` — 두 AABB를 감싸는 최소 AABB
|
||||
- `surface_area() -> f32` — BVH SAH 비용 계산용
|
||||
|
||||
### Collider (voltex_physics)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Collider {
|
||||
Sphere { radius: f32 },
|
||||
Box { half_extents: Vec3 },
|
||||
}
|
||||
```
|
||||
|
||||
- ECS 컴포넌트로 사용. 형상(shape)만 저장.
|
||||
- 위치는 같은 entity의 `Transform.position`에서 가져온다.
|
||||
- `aabb(&self, position: Vec3) -> AABB` — broad phase용 바운딩 박스 생성
|
||||
|
||||
### ContactPoint (voltex_physics)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ContactPoint {
|
||||
pub entity_a: Entity,
|
||||
pub entity_b: Entity,
|
||||
pub normal: Vec3, // A에서 B 방향 단위 법선
|
||||
pub depth: f32, // 침투 깊이 (양수 = 겹침)
|
||||
pub point_on_a: Vec3, // A 표면의 접촉점
|
||||
pub point_on_b: Vec3, // B 표면의 접촉점
|
||||
}
|
||||
```
|
||||
|
||||
### BvhTree (voltex_physics)
|
||||
|
||||
바이너리 트리. 각 리프는 하나의 Entity + AABB.
|
||||
|
||||
```rust
|
||||
pub struct BvhTree {
|
||||
nodes: Vec<BvhNode>,
|
||||
}
|
||||
|
||||
enum BvhNode {
|
||||
Leaf { entity: Entity, aabb: AABB },
|
||||
Internal { aabb: AABB, left: usize, right: usize },
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `build(entries: &[(Entity, AABB)]) -> Self` — 중앙값 분할(median split)로 구축. 가장 긴 축 기준 정렬 후 이분.
|
||||
- `query_pairs(&self) -> Vec<(Entity, Entity)>` — 트리 순회로 AABB가 겹치는 리프 쌍 반환.
|
||||
|
||||
매 프레임 rebuild한다 (동적 씬 대응). 최적화(incremental update)는 성능 문제 발생 시 추후 적용.
|
||||
|
||||
### Narrow Phase Functions (voltex_physics::narrow)
|
||||
|
||||
모든 함수는 위치(Vec3)와 형상 파라미터를 받아 `Option<ContactPoint>`를 반환한다.
|
||||
Entity 정보는 호출부에서 채운다.
|
||||
|
||||
```rust
|
||||
pub fn sphere_vs_sphere(
|
||||
pos_a: Vec3, radius_a: f32,
|
||||
pos_b: Vec3, radius_b: f32,
|
||||
) -> Option<(Vec3, f32, Vec3, Vec3)> // (normal, depth, point_a, point_b)
|
||||
|
||||
pub fn sphere_vs_box(
|
||||
sphere_pos: Vec3, radius: f32,
|
||||
box_pos: Vec3, half_extents: Vec3,
|
||||
) -> Option<(Vec3, f32, Vec3, Vec3)>
|
||||
|
||||
pub fn box_vs_box(
|
||||
pos_a: Vec3, half_a: Vec3,
|
||||
pos_b: Vec3, half_b: Vec3,
|
||||
) -> Option<(Vec3, f32, Vec3, Vec3)>
|
||||
```
|
||||
|
||||
**box_vs_box**: SAT (Separating Axis Theorem) 기반. 축 정렬(AABB vs AABB)만 지원한다.
|
||||
회전된 OBB는 Convex Hull 추가 시 GJK/EPA로 대체.
|
||||
|
||||
### ECS Integration (voltex_physics::collision)
|
||||
|
||||
```rust
|
||||
pub fn detect_collisions(world: &World) -> Vec<ContactPoint>
|
||||
```
|
||||
|
||||
1. `Transform` + `Collider`를 가진 entity를 `query2`로 수집
|
||||
2. 각 entity의 AABB 계산 (`collider.aabb(transform.position)`)
|
||||
3. `BvhTree::build()` → `query_pairs()`로 broad phase 후보 추출
|
||||
4. 각 후보 쌍에 대해 collider 타입 조합에 맞는 narrow phase 함수 호출
|
||||
5. 접촉이 있으면 `ContactPoint`에 entity 정보를 채워 결과에 추가
|
||||
|
||||
## Conventions
|
||||
|
||||
- 법선(normal): entity_a에서 entity_b 방향
|
||||
- 침투 깊이(depth): 양수 = 겹침, 0 이하 = 접촉 없음
|
||||
- 좌표계: 기존 voltex_math 규약 (오른손 좌표계)
|
||||
- WGSL vec3 alignment 규칙은 AABB에 해당 없음 (GPU에 올리지 않음)
|
||||
|
||||
## Test Plan
|
||||
|
||||
### voltex_math (AABB)
|
||||
- `new`, `from_center_half_extents` 생성
|
||||
- `center`, `half_extents` 계산
|
||||
- `contains_point`: 내부/외부/경계
|
||||
- `intersects`: 겹침/분리/접선
|
||||
- `merged`: 두 AABB 합집합
|
||||
- `surface_area`: 정확도
|
||||
|
||||
### voltex_physics
|
||||
- **narrow::sphere_vs_sphere**: 겹침, 분리, 접선, 완전 포함
|
||||
- **narrow::sphere_vs_box**: 면/모서리/꼭짓점 접촉, 분리, 내부 포함
|
||||
- **narrow::box_vs_box**: 각 축 겹침, 분리, 접선
|
||||
- **collider::aabb**: Sphere/Box의 AABB 생성
|
||||
- **bvh::build**: 빈 입력, 단일, 다수 엔트리
|
||||
- **bvh::query_pairs**: 겹치는 쌍 정확성, 분리된 쌍 미포함
|
||||
- **collision::detect_collisions**: ECS 통합 E2E
|
||||
|
||||
## Out of Scope (Phase 5-1)
|
||||
|
||||
- Capsule, Convex Hull 콜라이더
|
||||
- GJK/EPA 알고리즘
|
||||
- 회전된 박스(OBB) 충돌
|
||||
- 리지드바디 시뮬레이션 (Phase 5-2)
|
||||
- 레이캐스팅 (Phase 5-3)
|
||||
- 연속 충돌 감지 (CCD)
|
||||
147
docs/superpowers/specs/2026-03-25-phase5-2-rigidbody.md
Normal file
147
docs/superpowers/specs/2026-03-25-phase5-2-rigidbody.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 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: 구체가 바닥 위에서 낙하 → 충돌 → 반발
|
||||
125
docs/superpowers/specs/2026-03-25-phase5-3-raycasting.md
Normal file
125
docs/superpowers/specs/2026-03-25-phase5-3-raycasting.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Phase 5-3: Raycasting — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
`voltex_math`에 Ray 타입을 추가하고, `voltex_physics`에 기하 교차 함수와 BVH 가속 레이캐스트를 구현한다.
|
||||
|
||||
## Scope
|
||||
|
||||
- Ray 타입 (voltex_math)
|
||||
- ray_vs_aabb, ray_vs_sphere, ray_vs_box 교차 함수
|
||||
- BVH 가속 ECS 레이캐스트: `raycast(world, ray, max_dist) -> Option<RayHit>`
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Ray vs Plane, Triangle, Mesh
|
||||
- 연속 레이캐스트 (sweep)
|
||||
- 다중 hit 반환 (raycast_all)
|
||||
|
||||
## Module Structure
|
||||
|
||||
### voltex_math (수정)
|
||||
- `crates/voltex_math/src/ray.rs` — Ray 타입 (Create)
|
||||
- `crates/voltex_math/src/lib.rs` — Ray 모듈 등록 (Modify)
|
||||
|
||||
### voltex_physics (추가)
|
||||
- `crates/voltex_physics/src/ray.rs` — ray_vs_aabb, ray_vs_sphere, ray_vs_box (Create)
|
||||
- `crates/voltex_physics/src/raycast.rs` — RayHit, raycast ECS 통합 (Create)
|
||||
- `crates/voltex_physics/src/lib.rs` — 새 모듈 등록 (Modify)
|
||||
|
||||
## Types
|
||||
|
||||
### Ray (voltex_math)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Ray {
|
||||
pub origin: Vec3,
|
||||
pub direction: Vec3,
|
||||
}
|
||||
```
|
||||
|
||||
- `new(origin, direction) -> Self` — direction을 정규화하여 저장
|
||||
- `at(t: f32) -> Vec3` — `origin + direction * t`
|
||||
|
||||
### RayHit (voltex_physics)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct RayHit {
|
||||
pub entity: Entity,
|
||||
pub t: f32,
|
||||
pub point: Vec3,
|
||||
pub normal: Vec3,
|
||||
}
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
### ray_vs_aabb (voltex_physics::ray)
|
||||
|
||||
```rust
|
||||
pub fn ray_vs_aabb(ray: &Ray, aabb: &AABB) -> Option<f32>
|
||||
```
|
||||
|
||||
Slab method. t값만 반환 (BVH 순회용, 법선 불필요).
|
||||
ray가 AABB 내부에서 시작하면 t=0 반환.
|
||||
|
||||
### ray_vs_sphere (voltex_physics::ray)
|
||||
|
||||
```rust
|
||||
pub fn ray_vs_sphere(ray: &Ray, center: Vec3, radius: f32) -> Option<(f32, Vec3)>
|
||||
```
|
||||
|
||||
이차방정식 풀이. (t, normal) 반환.
|
||||
ray가 구 내부에서 시작하면 far intersection 반환.
|
||||
|
||||
### ray_vs_box (voltex_physics::ray)
|
||||
|
||||
```rust
|
||||
pub fn ray_vs_box(ray: &Ray, center: Vec3, half_extents: Vec3) -> Option<(f32, Vec3)>
|
||||
```
|
||||
|
||||
Slab method + 진입 면에서 법선 계산. (t, normal) 반환.
|
||||
ray가 박스 내부에서 시작하면 t=0, 진행 방향 기준 법선 반환.
|
||||
|
||||
### raycast (voltex_physics::raycast)
|
||||
|
||||
```rust
|
||||
pub fn raycast(world: &World, ray: &Ray, max_dist: f32) -> Option<RayHit>
|
||||
```
|
||||
|
||||
1. Transform + Collider 가진 entity 수집, AABB 계산
|
||||
2. BvhTree::build()로 BVH 구축
|
||||
3. 모든 리프를 순회하며 ray_vs_aabb로 broad phase
|
||||
4. AABB hit인 경우 콜라이더 타입별 정밀 교차:
|
||||
- Sphere → ray_vs_sphere
|
||||
- Box → ray_vs_box
|
||||
5. t < max_dist인 hit 중 가장 가까운 것 반환
|
||||
|
||||
NOTE: BVH 조기 종료 최적화는 추후. 첫 구현은 모든 리프 검사 후 최소 t 선택.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Ray direction은 항상 단위 벡터
|
||||
- t >= 0 인 교차만 유효 (ray 뒤쪽 무시)
|
||||
- normal은 ray가 진입하는 면의 바깥 방향
|
||||
- max_dist: t의 상한
|
||||
|
||||
## Test Plan
|
||||
|
||||
### voltex_math (Ray)
|
||||
- new: direction 정규화 확인
|
||||
- at: 정확한 점 계산
|
||||
|
||||
### voltex_physics (ray.rs)
|
||||
- ray_vs_aabb: hit, miss, 내부 시작 (t=0)
|
||||
- ray_vs_sphere: hit (t, normal 정확도), miss, 접선, 내부 시작
|
||||
- ray_vs_box: 각 면 hit, miss, 내부 시작
|
||||
|
||||
### voltex_physics (raycast.rs)
|
||||
- 빈 world → None
|
||||
- 단일 entity hit
|
||||
- 여러 entity 중 가장 가까운 것 반환
|
||||
- miss (max_dist 초과)
|
||||
- Sphere + Box 혼합
|
||||
203
docs/superpowers/specs/2026-03-25-phase6-1-audio.md
Normal file
203
docs/superpowers/specs/2026-03-25-phase6-1-audio.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Phase 6-1: Audio System Foundation — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
`voltex_audio` crate를 신규 생성한다. WAV 파서, WASAPI 백엔드(Windows), 채널 기반 오디오 스레드로 기본 사운드 재생을 구현한다.
|
||||
|
||||
## Scope
|
||||
|
||||
- WAV 파서 (PCM 16-bit, mono/stereo)
|
||||
- AudioClip (파싱된 오디오 데이터, f32 샘플)
|
||||
- WASAPI 백엔드 (Windows, shared mode, FFI 직접 호출)
|
||||
- 오디오 스레드 + mpsc channel 명령 처리
|
||||
- AudioSystem API (load_wav, play, stop, set_volume)
|
||||
- 믹싱 (다중 동시 재생, 볼륨, 루프)
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- macOS (CoreAudio), Linux (ALSA/PulseAudio) 백엔드
|
||||
- 3D 오디오 (거리 감쇠, 패닝)
|
||||
- 믹서 (채널 그룹, 페이드)
|
||||
- OGG/Vorbis 디코더
|
||||
- 비동기 로딩
|
||||
- ECS 통합 (AudioSource 컴포넌트 등)
|
||||
|
||||
## Module Structure
|
||||
|
||||
```
|
||||
crates/voltex_audio/
|
||||
├── Cargo.toml
|
||||
└── src/
|
||||
├── lib.rs — public exports
|
||||
├── wav.rs — WAV 파서
|
||||
├── audio_clip.rs — AudioClip 타입
|
||||
├── backend_wasapi.rs — WASAPI FFI (Windows 전용, cfg(target_os))
|
||||
├── mixer_thread.rs — 오디오 스레드 + 믹싱 로직
|
||||
└── audio_system.rs — AudioSystem (메인 스레드 API)
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- 없음 (외부 crate 없음)
|
||||
- Windows FFI: `windows-sys` 스타일이 아닌 직접 `extern "system"` 선언
|
||||
- `std::sync::mpsc`, `std::thread`, `std::sync::Arc`
|
||||
|
||||
## Types
|
||||
|
||||
### AudioClip
|
||||
|
||||
```rust
|
||||
#[derive(Clone)]
|
||||
pub struct AudioClip {
|
||||
pub samples: Vec<f32>, // interleaved, normalized -1.0~1.0
|
||||
pub sample_rate: u32,
|
||||
pub channels: u16,
|
||||
}
|
||||
```
|
||||
|
||||
### AudioCommand (내부)
|
||||
|
||||
```rust
|
||||
enum AudioCommand {
|
||||
Play { clip_index: usize, volume: f32, looping: bool },
|
||||
Stop { clip_index: usize },
|
||||
SetVolume { clip_index: usize, volume: f32 },
|
||||
StopAll,
|
||||
Shutdown,
|
||||
}
|
||||
```
|
||||
|
||||
### PlayingSound (내부, 오디오 스레드)
|
||||
|
||||
```rust
|
||||
struct PlayingSound {
|
||||
clip_index: usize,
|
||||
position: usize, // 현재 샘플 위치
|
||||
volume: f32,
|
||||
looping: bool,
|
||||
}
|
||||
```
|
||||
|
||||
### AudioSystem (public API)
|
||||
|
||||
```rust
|
||||
pub struct AudioSystem {
|
||||
sender: Sender<AudioCommand>,
|
||||
_thread: JoinHandle<()>,
|
||||
}
|
||||
```
|
||||
|
||||
**Methods:**
|
||||
- `new(clips: Vec<AudioClip>) -> Result<Self, String>` — WASAPI 초기화, 오디오 스레드 시작. clips를 Arc로 공유.
|
||||
- `play(clip_index: usize, volume: f32, looping: bool)` — 재생 명령
|
||||
- `stop(clip_index: usize)` — 정지
|
||||
- `set_volume(clip_index: usize, volume: f32)` — 볼륨 변경
|
||||
- `stop_all()` — 전체 정지
|
||||
- `Drop` — Shutdown + join
|
||||
|
||||
## WAV Parser
|
||||
|
||||
### 지원 포맷
|
||||
- RIFF WAV, PCM (format_tag = 1)
|
||||
- 16-bit 샘플만
|
||||
- Mono (1ch) 또는 Stereo (2ch)
|
||||
- 임의 sample rate
|
||||
|
||||
### 파싱 과정
|
||||
1. RIFF 헤더 검증 ("RIFF", "WAVE")
|
||||
2. "fmt " 청크: format_tag, channels, sample_rate, bits_per_sample 읽기
|
||||
3. "data" 청크: raw PCM 데이터
|
||||
4. i16 → f32 변환: `sample as f32 / 32768.0`
|
||||
|
||||
### 에러
|
||||
- 파일 읽기 실패
|
||||
- RIFF/WAVE 시그니처 불일치
|
||||
- format_tag != 1 (non-PCM)
|
||||
- bits_per_sample != 16
|
||||
- data 청크 미발견
|
||||
|
||||
```rust
|
||||
pub fn parse_wav(data: &[u8]) -> Result<AudioClip, String>
|
||||
```
|
||||
|
||||
바이트 슬라이스를 받아 파싱. 파일 I/O는 호출부에서 처리.
|
||||
|
||||
## WASAPI Backend
|
||||
|
||||
### 초기화 순서
|
||||
1. `CoInitializeEx(null, COINIT_MULTITHREADED)`
|
||||
2. `CoCreateInstance(CLSID_MMDeviceEnumerator)` → `IMMDeviceEnumerator`
|
||||
3. `enumerator.GetDefaultAudioEndpoint(eRender, eConsole)` → `IMMDevice`
|
||||
4. `device.Activate(IID_IAudioClient)` → `IAudioClient`
|
||||
5. `client.GetMixFormat()` — 장치 기본 포맷 확인
|
||||
6. `client.Initialize(AUDCLNT_SHAREMODE_SHARED, ...)` — shared mode
|
||||
7. `client.GetService(IID_IAudioRenderClient)` → `IAudioRenderClient`
|
||||
8. `client.Start()` — 재생 시작
|
||||
|
||||
### 버퍼 쓰기 루프
|
||||
1. `client.GetCurrentPadding()` → 사용 가능한 프레임 수 계산
|
||||
2. `render_client.GetBuffer(frames)` → 버퍼 포인터
|
||||
3. 믹싱된 샘플을 버퍼에 쓰기
|
||||
4. `render_client.ReleaseBuffer(frames)`
|
||||
5. `thread::sleep(Duration::from_millis(5))` — CPU 사용 조절
|
||||
|
||||
### FFI 타입
|
||||
COM 인터페이스를 vtable 기반 raw pointer로 직접 선언. `#[repr(C)]` 구조체 사용.
|
||||
|
||||
### 샘플 레이트 변환
|
||||
클립의 sample_rate와 장치의 sample_rate가 다른 경우, 선형 보간으로 리샘플링.
|
||||
|
||||
### 채널 변환
|
||||
- Mono 클립 → Stereo 장치: 양 채널에 동일 샘플
|
||||
- Stereo 클립 → 장치 채널 수 매칭
|
||||
|
||||
## Mixing
|
||||
|
||||
오디오 스레드에서 수행하는 순수 함수:
|
||||
|
||||
```rust
|
||||
fn mix_sounds(
|
||||
output: &mut [f32],
|
||||
playing: &mut Vec<PlayingSound>,
|
||||
clips: &[AudioClip],
|
||||
device_sample_rate: u32,
|
||||
device_channels: u16,
|
||||
)
|
||||
```
|
||||
|
||||
1. output 버퍼를 0으로 초기화
|
||||
2. 각 PlayingSound에 대해:
|
||||
- 클립에서 샘플 읽기 (리샘플링/채널 변환 적용)
|
||||
- volume 곱하기
|
||||
- output에 합산
|
||||
- position 전진. 끝에 도달하면 looping이면 0으로, 아니면 제거
|
||||
3. 클리핑: -1.0~1.0으로 clamp
|
||||
|
||||
## Test Plan
|
||||
|
||||
### wav.rs (단위 테스트 가능)
|
||||
- 유효한 WAV 바이트 → AudioClip 파싱 성공
|
||||
- 잘못된 RIFF 헤더 → 에러
|
||||
- non-PCM format → 에러
|
||||
- 24-bit → 에러 (16-bit만 지원)
|
||||
- 빈 data 청크 → 빈 samples
|
||||
- i16→f32 변환 정확도 (32767 → ~1.0, -32768 → -1.0)
|
||||
|
||||
### audio_clip.rs
|
||||
- 생성, 속성 확인
|
||||
|
||||
### mixer_thread.rs (순수 함수 테스트)
|
||||
- 단일 사운드 믹싱 → 출력 = 클립 샘플 * 볼륨
|
||||
- 두 사운드 합산
|
||||
- 클리핑 (-1.0~1.0)
|
||||
- 루핑: 끝에 도달 후 처음부터 재개
|
||||
- 비루핑: 끝에 도달 후 제거
|
||||
|
||||
### WASAPI + AudioSystem
|
||||
- 실제 하드웨어 필요 → `examples/audio_demo`로 수동 테스트
|
||||
- WAV 파일 로드 → play → 소리 확인
|
||||
|
||||
## Example
|
||||
|
||||
`examples/audio_demo` — WAV 파일을 로드하고 키 입력으로 재생/정지하는 데모.
|
||||
테스트용 WAV 파일은 코드로 생성 (440Hz 사인파, 1초).
|
||||
163
docs/superpowers/specs/2026-03-25-phase6-2-3d-audio.md
Normal file
163
docs/superpowers/specs/2026-03-25-phase6-2-3d-audio.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# Phase 6-2: 3D Audio — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
`voltex_audio`에 3D 공간 오디오를 추가한다. 거리 기반 감쇠와 스테레오 패닝으로 소리의 공간감을 표현한다.
|
||||
|
||||
## Scope
|
||||
|
||||
- Listener (위치, 방향)
|
||||
- SpatialParams (이미터 위치, min/max 거리)
|
||||
- 거리 감쇠 (inverse distance)
|
||||
- 스테레오 패닝 (equal-power)
|
||||
- play_3d, set_listener API
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 도플러 효과
|
||||
- HRTF
|
||||
- Reverb/Echo
|
||||
- 다중 리스너
|
||||
|
||||
## Module Structure
|
||||
|
||||
### voltex_audio (수정/추가)
|
||||
- `crates/voltex_audio/src/spatial.rs` — Listener, SpatialParams, 감쇠/패닝 순수 함수 (Create)
|
||||
- `crates/voltex_audio/src/mixing.rs` — PlayingSound에 spatial 추가, mix에 spatial 적용 (Modify)
|
||||
- `crates/voltex_audio/src/audio_system.rs` — Play3d, SetListener 명령 추가 (Modify)
|
||||
- `crates/voltex_audio/src/lib.rs` — spatial 모듈 등록 (Modify)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `voltex_math` — Vec3 (위치, 방향 계산)
|
||||
- `crates/voltex_audio/Cargo.toml`에 voltex_math 의존 추가
|
||||
|
||||
## Types
|
||||
|
||||
### Listener
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Listener {
|
||||
pub position: Vec3,
|
||||
pub forward: Vec3,
|
||||
pub right: Vec3,
|
||||
}
|
||||
```
|
||||
|
||||
- `default()` → position=ZERO, forward=-Z, right=X
|
||||
|
||||
### SpatialParams
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SpatialParams {
|
||||
pub position: Vec3,
|
||||
pub min_distance: f32,
|
||||
pub max_distance: f32,
|
||||
}
|
||||
```
|
||||
|
||||
- `new(position, min_distance, max_distance)` — 생성
|
||||
- `at(position)` — min=1.0, max=50.0 기본값으로 간편 생성
|
||||
|
||||
## Functions (spatial.rs)
|
||||
|
||||
### distance_attenuation
|
||||
|
||||
```rust
|
||||
pub fn distance_attenuation(distance: f32, min_dist: f32, max_dist: f32) -> f32
|
||||
```
|
||||
|
||||
- distance <= min_dist → 1.0
|
||||
- distance >= max_dist → 0.0
|
||||
- 사이: `min_dist / distance` (inverse distance, clamped)
|
||||
|
||||
### stereo_pan
|
||||
|
||||
```rust
|
||||
pub fn stereo_pan(listener: &Listener, emitter_pos: Vec3) -> (f32, f32)
|
||||
```
|
||||
|
||||
1. direction = (emitter_pos - listener.position).normalize()
|
||||
2. pan = direction.dot(listener.right) — -1.0(왼쪽) ~ 1.0(오른쪽)
|
||||
3. Equal-power panning:
|
||||
- left_gain = cos(pan * PI/4 + PI/4) — pan=-1일 때 1.0, pan=1일 때 0.0
|
||||
- right_gain = sin(pan * PI/4 + PI/4) — pan=-1일 때 0.0, pan=1일 때 1.0
|
||||
4. 이미터가 리스너 위치와 동일하면 (1.0, 1.0) 반환
|
||||
|
||||
### compute_spatial_gains
|
||||
|
||||
```rust
|
||||
pub fn compute_spatial_gains(listener: &Listener, spatial: &SpatialParams) -> (f32, f32, f32)
|
||||
```
|
||||
|
||||
Returns (attenuation, left_gain, right_gain).
|
||||
편의 함수: distance_attenuation + stereo_pan 결합.
|
||||
|
||||
## Mixing Integration
|
||||
|
||||
### PlayingSound 변경
|
||||
|
||||
```rust
|
||||
pub struct PlayingSound {
|
||||
pub clip_index: usize,
|
||||
pub position: usize,
|
||||
pub volume: f32,
|
||||
pub looping: bool,
|
||||
pub spatial: Option<SpatialParams>, // NEW
|
||||
}
|
||||
```
|
||||
|
||||
### mix_sounds 변경
|
||||
|
||||
시그니처에 `listener: &Listener` 파라미터 추가:
|
||||
|
||||
```rust
|
||||
pub fn mix_sounds(
|
||||
output: &mut [f32],
|
||||
playing: &mut Vec<PlayingSound>,
|
||||
clips: &[AudioClip],
|
||||
device_sample_rate: u32,
|
||||
device_channels: u16,
|
||||
frames: usize,
|
||||
listener: &Listener, // NEW
|
||||
)
|
||||
```
|
||||
|
||||
spatial이 Some인 사운드:
|
||||
1. compute_spatial_gains로 (attenuation, left, right) 계산
|
||||
2. 볼륨에 attenuation 곱하기
|
||||
3. 스테레오 출력에 left/right gain 적용
|
||||
|
||||
spatial이 None인 사운드 (2D):
|
||||
- 기존 동작 유지 (전체 볼륨 균등)
|
||||
|
||||
## AudioSystem API 변경
|
||||
|
||||
### 새 명령
|
||||
|
||||
```rust
|
||||
AudioCommand::Play3d { clip_index, volume, looping, spatial: SpatialParams },
|
||||
AudioCommand::SetListener { position: Vec3, forward: Vec3, right: Vec3 },
|
||||
```
|
||||
|
||||
### 새 메서드
|
||||
|
||||
```rust
|
||||
pub fn play_3d(&self, clip_index: usize, volume: f32, looping: bool, spatial: SpatialParams)
|
||||
pub fn set_listener(&self, position: Vec3, forward: Vec3, right: Vec3)
|
||||
```
|
||||
|
||||
## Test Plan
|
||||
|
||||
### spatial.rs
|
||||
- distance_attenuation: at min_dist (1.0), at max_dist (0.0), between, zero distance
|
||||
- stereo_pan: right side → right gain > left, left side → left gain > right, front → roughly equal, same position → (1.0, 1.0)
|
||||
- compute_spatial_gains: combines both correctly
|
||||
|
||||
### mixing.rs (spatial integration)
|
||||
- 2D sound unchanged (spatial=None, listener 무관)
|
||||
- 3D sound at max_distance → near-silent output
|
||||
- 3D sound at min_distance → full volume
|
||||
- 3D sound on right → right channel louder
|
||||
128
docs/superpowers/specs/2026-03-25-phase6-3-mixer.md
Normal file
128
docs/superpowers/specs/2026-03-25-phase6-3-mixer.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Phase 6-3: Mixer — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
`voltex_audio`에 그룹 기반 믹서를 추가한다. BGM/SFX/Voice 그룹별 독립 볼륨 제어와 페이드 인/아웃을 지원한다.
|
||||
|
||||
## Scope
|
||||
|
||||
- MixGroup enum (Master, Bgm, Sfx, Voice)
|
||||
- GroupState (볼륨 + 페이드)
|
||||
- MixerState (전체 그룹 관리, effective_volume = group * master)
|
||||
- PlayingSound에 group 필드 추가
|
||||
- mix_sounds에 mixer 파라미터 추가
|
||||
- AudioSystem API: set_group_volume, fade_group
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 동적 그룹 생성 (고정 4개만)
|
||||
- 그룹 간 라우팅/버스
|
||||
- 이펙트 체인 (reverb, EQ 등)
|
||||
- 페이드 커브 (선형만)
|
||||
|
||||
## Module Structure
|
||||
|
||||
- `crates/voltex_audio/src/mix_group.rs` — MixGroup, GroupState, MixerState (Create)
|
||||
- `crates/voltex_audio/src/mixing.rs` — PlayingSound에 group, mix_sounds에 mixer (Modify)
|
||||
- `crates/voltex_audio/src/audio_system.rs` — SetGroupVolume, FadeGroup 명령 (Modify)
|
||||
- `crates/voltex_audio/src/lib.rs` — mix_group 모듈 등록 (Modify)
|
||||
|
||||
## Types
|
||||
|
||||
### MixGroup
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum MixGroup {
|
||||
Master,
|
||||
Bgm,
|
||||
Sfx,
|
||||
Voice,
|
||||
}
|
||||
```
|
||||
|
||||
### GroupState
|
||||
|
||||
```rust
|
||||
pub struct GroupState {
|
||||
pub volume: f32,
|
||||
pub fade_target: f32,
|
||||
pub fade_speed: f32,
|
||||
}
|
||||
```
|
||||
|
||||
- `new()` → volume=1.0, fade_target=1.0, fade_speed=0.0
|
||||
- `tick(dt: f32)` — volume을 fade_target으로 fade_speed * dt만큼 이동. 도달하면 fade_speed=0.
|
||||
|
||||
### MixerState
|
||||
|
||||
```rust
|
||||
pub struct MixerState {
|
||||
groups: [GroupState; 4], // Master, Bgm, Sfx, Voice 순서
|
||||
}
|
||||
```
|
||||
|
||||
배열 인덱스로 접근 (HashMap 대신 간결하게).
|
||||
|
||||
- `new()` — 전부 volume=1.0
|
||||
- `set_volume(group, volume)` — 즉시 설정, fade 중지
|
||||
- `fade(group, target, duration)` — fade_speed = |target - current| / duration
|
||||
- `tick(dt)` — 모든 그룹 업데이트
|
||||
- `volume(group) -> f32` — 해당 그룹 현재 볼륨
|
||||
- `effective_volume(group) -> f32` — group.volume * master.volume (Master 그룹은 자기 자신만)
|
||||
|
||||
## Mixing Integration
|
||||
|
||||
### PlayingSound 변경
|
||||
|
||||
```rust
|
||||
pub group: MixGroup, // 기본 Sfx
|
||||
```
|
||||
|
||||
- `new()` → group = MixGroup::Sfx
|
||||
- `new_3d()` → group = MixGroup::Sfx
|
||||
- 기존 생성자에 group 파라미터 추가하지 않음 (기본값 사용). 필요 시 직접 설정.
|
||||
|
||||
### mix_sounds 변경
|
||||
|
||||
`mixer: &MixerState` 파라미터 추가.
|
||||
|
||||
각 사운드의 최종 볼륨 계산:
|
||||
```
|
||||
base_volume = sound.volume * mixer.effective_volume(sound.group)
|
||||
```
|
||||
이후 spatial gains 적용은 기존과 동일.
|
||||
|
||||
### AudioCommand 추가
|
||||
|
||||
```rust
|
||||
SetGroupVolume { group: MixGroup, volume: f32 },
|
||||
FadeGroup { group: MixGroup, target: f32, duration: f32 },
|
||||
```
|
||||
|
||||
### AudioSystem 메서드 추가
|
||||
|
||||
```rust
|
||||
pub fn set_group_volume(&self, group: MixGroup, volume: f32)
|
||||
pub fn fade_group(&self, group: MixGroup, target: f32, duration: f32)
|
||||
```
|
||||
|
||||
### 오디오 스레드
|
||||
|
||||
- `MixerState` 인스턴스 보유
|
||||
- 매 루프: `mixer.tick(dt)` 호출 (dt ≈ 5ms)
|
||||
- mix_sounds에 `&mixer` 전달
|
||||
|
||||
## Test Plan
|
||||
|
||||
### mix_group.rs
|
||||
- GroupState::tick: 페이드 진행, 목표 도달 시 정지
|
||||
- MixerState::set_volume: 즉시 반영
|
||||
- MixerState::fade: 여러 tick 후 목표 도달
|
||||
- MixerState::effective_volume: group * master
|
||||
- Master=0이면 모든 그룹 effective=0
|
||||
|
||||
### mixing.rs (통합)
|
||||
- group 볼륨 적용: Sfx volume=0.5 → 출력 절반
|
||||
- master=0 → 전체 무음
|
||||
- 기존 2D/3D 테스트는 mixer volume=1.0으로 변화 없음
|
||||
Reference in New Issue
Block a user