diff --git a/docs/superpowers/specs/2026-03-25-phase3b-4a-deferred.md b/docs/superpowers/specs/2026-03-25-phase3b-4a-deferred.md new file mode 100644 index 0000000..b330653 --- /dev/null +++ b/docs/superpowers/specs/2026-03-25-phase3b-4a-deferred.md @@ -0,0 +1,156 @@ +# Phase 3b-4a Deferred Items Spec + +## A. 씬 직렬화 (`voltex_ecs`) + +### JSON 직렬화 (`scene.rs` 확장) + +- `serialize_scene_json(world, registry) -> String` +- `deserialize_scene_json(world, json, registry) -> Result, String>` +- voltex_ecs 내부에 미니 JSON writer + 미니 JSON parser (voltex_renderer 의존 없음) + +포맷: +```json +{"version":1,"entities":[ + {"transform":{"position":[0,0,0],"rotation":[0,0,0],"scale":[1,1,1]}, + "tag":"player","parent":null,"components":{"rigid_body":{...}}} +]} +``` + +### 바이너리 씬 포맷 (`binary_scene.rs` 신규) + +- `serialize_scene_binary(world, registry) -> Vec` +- `deserialize_scene_binary(world, data, registry) -> Result, String>` + +포맷: +``` +[4] magic "VSCN" +[4] version u32 LE +[4] entity_count u32 LE +per entity: + [4] component_count u32 LE + per component: + [2] name_len u16 LE + [N] name bytes (UTF-8) + [4] data_len u32 LE + [N] data bytes +``` + +### 임의 컴포넌트 등록 (`component_registry.rs` 신규) + +```rust +pub type SerializeFn = fn(&World, Entity) -> Option>; +pub type DeserializeFn = fn(&mut World, Entity, &[u8]) -> Result<(), String>; + +pub struct ComponentRegistry { + entries: Vec, +} + +struct ComponentEntry { + name: String, + serialize: SerializeFn, + deserialize: DeserializeFn, +} + +impl ComponentRegistry { + pub fn new() -> Self + pub fn register(&mut self, name: &str, ser: SerializeFn, deser: DeserializeFn) + pub fn register_defaults(&mut self) // Transform, Parent, Tag +} +``` + +--- + +## B. 비동기 로딩 + 핫 리로드 (`voltex_asset`) + +### 비동기 로딩 (`loader.rs` 신규) + +```rust +pub enum LoadState { + Loading, + Ready, + Failed(String), +} + +pub struct AssetLoader { + sender: Sender, + receiver: Receiver, + thread: Option>, + pending: HashMap, +} + +impl AssetLoader { + pub fn new() -> Self // 워커 스레드 1개 시작 + pub fn load( + &mut self, path: PathBuf, + parse: fn(&[u8]) -> Result, + ) -> Handle // 즉시 핸들 반환, 백그라운드 로딩 + pub fn state(&self, handle: Handle) -> LoadState + pub fn process_loaded(&mut self, assets: &mut Assets) // 매 프레임 호출 + pub fn shutdown(self) +} +``` + +### 핫 리로드 (`watcher.rs` 신규) + +```rust +pub struct FileWatcher { + watched: HashMap, // path → last_modified + poll_interval: Duration, + last_poll: Instant, +} + +impl FileWatcher { + pub fn new(poll_interval: Duration) -> Self + pub fn watch(&mut self, path: PathBuf) + pub fn unwatch(&mut self, path: &Path) + pub fn poll_changes(&mut self) -> Vec // std::fs::metadata 기반 +} +``` + +- 외부 크레이트 없음, `std::fs::metadata().modified()` 사용 +- 변경 감지 시 AssetLoader로 재로딩 트리거 +- 기존 Handle은 유지, AssetStorage에서 in-place swap (generation 유지) + +--- + +## C. PBR 텍스처 맵 (`voltex_renderer`) + +### 텍스처 바인딩 확장 + +Group 1 확장 (4→8 바인딩): +``` +Binding 0-1: Albedo texture + sampler (기존) +Binding 2-3: Normal map texture + sampler (기존) +Binding 4-5: Metallic/Roughness/AO (ORM) texture + sampler (신규) +Binding 6-7: Emissive texture + sampler (신규) +``` + +- ORM 텍스처: R=AO, G=Roughness, B=Metallic (glTF ORM 패턴) +- 텍스처 없으면 기본 1x1 white 사용 + +### 셰이더 변경 + +`pbr_shader.wgsl` + `deferred_gbuffer.wgsl`: +```wgsl +@group(1) @binding(4) var t_orm: texture_2d; +@group(1) @binding(5) var s_orm: sampler; +@group(1) @binding(6) var t_emissive: texture_2d; +@group(1) @binding(7) var s_emissive: sampler; + +// Fragment: +let orm = textureSample(t_orm, s_orm, in.uv); +let ao = orm.r * material.ao; +let roughness = orm.g * material.roughness; +let metallic = orm.b * material.metallic; +let emissive = textureSample(t_emissive, s_emissive, in.uv).rgb; +// ... add emissive to final color +``` + +### MaterialUniform 변경 없음 +- 기존 metallic/roughness/ao 값은 텍스처 값의 승수(multiplier)로 작동 +- 텍스처 없을 때 white(1,1,1) × material 값 = 기존과 동일 결과 + +### 텍스처 유틸 확장 (`texture.rs`) +- `pbr_full_texture_bind_group_layout()` — 8 바인딩 레이아웃 +- `create_pbr_full_texture_bind_group()` — albedo + normal + ORM + emissive +- `black_1x1()` — emissive 기본값 (검정 = 발광 없음)