# Phase 7-2: SSGI (Screen-Space Global Illumination) — Design Spec ## Overview 디퍼드 파이프라인에 SSGI 포스트 프로세싱 패스를 추가한다. SSAO 확장형으로, 반구 샘플링을 통해 Ambient Occlusion과 Color Bleeding(간접광)을 동시에 계산한다. ## Scope - SSGI 리소스 (반구 커널, 4x4 노이즈 텍스처, 출력 텍스처) - SSGI 풀스크린 셰이더 (AO + indirect color 계산) - SSGI 파이프라인 + 바인드 그룹 레이아웃 - Lighting Pass 수정 (SSGI 결과를 ambient에 적용) - deferred_demo에 SSGI 통합 ## Out of Scope - 블러 패스 (노이즈 제거용 bilateral blur — 추후 추가) - 반해상도 렌더링 (성능 최적화) - 시간적 누적 (temporal accumulation) - Light Probes ## Render Pass Flow (디퍼드 확장) ``` Pass 1: G-Buffer (기존, 변경 없음) Pass 2: SSGI Pass (NEW) → Rgba16Float 출력 Pass 3: Lighting Pass (수정) → SSGI 텍스처 읽어서 ambient에 적용 ``` ## Module Structure ### 새 파일 - `crates/voltex_renderer/src/ssgi.rs` — SsgiResources, SsgiUniform, 커널/노이즈 생성 - `crates/voltex_renderer/src/ssgi_shader.wgsl` — SSGI 풀스크린 셰이더 ### 수정 파일 - `crates/voltex_renderer/src/deferred_pipeline.rs` — SSGI 파이프라인 + 바인드 그룹 레이아웃 추가 - `crates/voltex_renderer/src/deferred_lighting.wgsl` — SSGI 결과 적용 - `crates/voltex_renderer/src/lib.rs` — ssgi 모듈 등록 - `examples/deferred_demo/src/main.rs` — SSGI 패스 추가 ## Types ### SsgiUniform (128 bytes) ```rust #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] pub struct SsgiUniform { pub projection: [f32; 16], // view → clip pub view: [f32; 16], // world → view pub radius: f32, // 샘플링 반경 (기본 0.5) pub bias: f32, // depth 바이어스 (기본 0.025) pub intensity: f32, // AO 강도 (기본 1.0) pub indirect_strength: f32, // color bleeding 강도 (기본 0.5) } ``` ### SsgiResources ```rust pub struct SsgiResources { pub output_view: TextureView, // Rgba16Float — R=AO, G=indirect_r, B=indirect_g, A=indirect_b pub kernel_buffer: Buffer, // 64 * vec4 = 1024 bytes (반구 샘플) pub noise_view: TextureView, // 4x4 Rgba16Float (랜덤 회전 벡터) pub uniform_buffer: Buffer, // SsgiUniform pub width: u32, pub height: u32, } ``` - `new(device, width, height)` — 리소스 생성, 커널/노이즈 초기화 - `resize(device, width, height)` — 출력 텍스처 재생성 ### 반구 커널 생성 64개 샘플, 반구(+z 방향) 내 랜덤 분포. 중심 가까이에 더 많은 샘플 (코사인 가중): ```rust fn generate_kernel(count: usize) -> Vec<[f32; 4]> { // 의사 랜덤 (시드 고정) // 각 샘플: normalize(random_in_hemisphere) * lerp(0.1, 1.0, scale^2) // scale = i / count } ``` ### 4x4 노이즈 텍스처 16개 랜덤 회전 벡터 (xy 평면). TBN 구성 시 tangent 방향을 랜덤화하여 밴딩 방지. ```rust fn generate_noise() -> Vec<[f32; 4]> { // 16개 vec4(random_x, random_y, 0.0, 0.0) } ``` ## SSGI Shader (ssgi_shader.wgsl) ### 바인드 그룹 **Group 0: G-Buffer** - binding 0: position texture (Float, non-filterable) - binding 1: normal texture (Float, filterable) - binding 2: albedo texture (Float, filterable) - binding 3: sampler (NonFiltering) **Group 1: SSGI Data** - binding 0: SsgiUniform - binding 1: kernel buffer (storage or uniform, 64 * vec4) - binding 2: noise texture - binding 3: noise sampler ### 알고리즘 ``` @fragment fn fs_main(uv): world_pos = sample(t_position, uv) if length(world_pos) < 0.001: discard (background) normal = sample(t_normal, uv) // View space conversion view_pos = (ssgi.view * vec4(world_pos, 1.0)).xyz view_normal = normalize((ssgi.view * vec4(normal, 0.0)).xyz) // Random rotation from noise (4x4 tiling) noise_uv = uv * vec2(width/4.0, height/4.0) random_vec = sample(t_noise, noise_uv).xyz // Construct TBN in view space tangent = normalize(random_vec - view_normal * dot(random_vec, view_normal)) bitangent = cross(view_normal, tangent) TBN = mat3x3(tangent, bitangent, view_normal) occlusion = 0.0 indirect = vec3(0.0) for i in 0..64: // Sample position in view space sample_offset = TBN * kernel[i].xyz * ssgi.radius sample_pos = view_pos + sample_offset // Project to screen clip = ssgi.projection * vec4(sample_pos, 1.0) screen_uv = clip.xy / clip.w * 0.5 + 0.5 screen_uv.y = 1.0 - screen_uv.y // Read actual depth at that screen position sample_world_pos = sample(t_position, screen_uv).xyz sample_view_pos = (ssgi.view * vec4(sample_world_pos, 1.0)).xyz // Occlusion check range_check = smoothstep(0.0, 1.0, ssgi.radius / abs(view_pos.z - sample_view_pos.z)) if sample_view_pos.z >= sample_pos.z + ssgi.bias: occlusion += range_check // Color bleeding: read albedo at occluder position sample_albedo = sample(t_albedo, screen_uv).rgb indirect += sample_albedo * range_check ao = 1.0 - (occlusion / 64.0) * ssgi.intensity indirect = indirect / 64.0 * ssgi.indirect_strength return vec4(ao, indirect) ``` ## Lighting Pass 수정 ### 바인드 그룹 변경 기존 Group 2 (Shadow+IBL, 5 bindings)에 SSGI 출력 추가: - binding 5: SSGI output texture (Float, filterable) - binding 6: SSGI sampler ### 셰이더 변경 ```wgsl // 기존 let ambient = (diffuse_ibl + specular_ibl) * ao; // 변경 let ssgi_data = textureSample(t_ssgi, s_ssgi, in.uv); let ssgi_ao = ssgi_data.r; let indirect_light = ssgi_data.gba; let ambient = (diffuse_ibl + specular_ibl) * ao * ssgi_ao + indirect_light; ``` ## Bind Group Constraint (max 4) **SSGI Pass:** 2 groups (0: G-Buffer, 1: SSGI data) — OK **Lighting Pass:** 기존 3 groups. Group 2에 SSGI binding 추가 (5,6) — 같은 그룹 내 binding 추가이므로 group 수 변화 없음. OK. ## Test Plan ### ssgi.rs - generate_kernel: 64개 샘플, 모두 반구 내 (z >= 0), 정규화됨 - generate_noise: 16개 벡터 - SsgiResources 생성/리사이즈 ### 통합 (수동) - deferred_demo에서 SSGI ON/OFF 비교 - 구석/틈에서 AO 어두워짐 확인 - 밝은 물체 근처에서 color bleeding 확인