# 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 기본값 (검정 = 발광 없음)