203 lines
6.3 KiB
Markdown
203 lines
6.3 KiB
Markdown
# 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 확인
|