docs: add Phase 7-1 through 7-3 specs and plans

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-25 13:25:11 +09:00
parent 643a329338
commit ba610f48dc
6 changed files with 2851 additions and 0 deletions

View File

@@ -0,0 +1,199 @@
# Phase 7-1: Deferred Rendering — Design Spec
## Overview
`voltex_renderer`에 디퍼드 렌더링 파이프라인을 추가한다. 기존 포워드 PBR은 유지하고, G-Buffer + Lighting Pass 구조의 디퍼드 파이프라인을 새 모듈로 구현한다.
## Scope
- G-Buffer (4 MRT: Position, Normal, Albedo, Material + Depth)
- G-Buffer Pass 셰이더 (기하 데이터 기록)
- Lighting Pass 셰이더 (풀스크린 쿼드, Cook-Torrance BRDF, 멀티 라이트, 섀도우, IBL)
- 풀스크린 삼각형
- deferred_demo 예제
## Out of Scope
- 포워드 파이프라인 제거/변경
- 투명 오브젝트 (디퍼드에서 처리 어려움, 별도 포워드 패스 필요)
- G-Buffer 압축/최적화 (octahedral normal, depth-position 복원 등)
- Light volumes (sphere/cone 렌더링으로 라이트 컬링)
- Stencil 기반 최적화
## Render Pass Architecture
### Pass 1: G-Buffer Pass
MRT(Multiple Render Targets)로 기하 데이터 기록.
| RT | Format | Content |
|----|--------|---------|
| RT0 | Rgba32Float | World Position (xyz) |
| RT1 | Rgba16Float | World Normal (xyz, normalized) |
| RT2 | Rgba8UnormSrgb | Albedo (rgb) |
| RT3 | Rgba8Unorm | R=metallic, G=roughness, B=ao |
| Depth | Depth32Float | Depth (기존 공유) |
**Bind Groups:**
- Group 0 (dynamic): CameraUniform (view_proj, model)
- Group 1: PBR Textures (albedo + normal map)
- Group 2 (dynamic): MaterialUniform
**Shader:** 버텍스 → 월드 변환, 프래그먼트 → G-Buffer 기록. TBN 노멀맵 적용.
### Pass 2: Lighting Pass
풀스크린 삼각형 렌더, G-Buffer를 텍스처로 읽어 라이팅 계산.
**Bind Groups:**
- Group 0: G-Buffer textures (4개) + sampler
- Group 1: LightsUniform + CameraPosition
- Group 2: Shadow map + shadow sampler + ShadowUniform + BRDF LUT + BRDF sampler
**Shader:** 기존 pbr_shader.wgsl의 Cook-Torrance BRDF 로직을 재사용.
- G-Buffer에서 position, normal, albedo, metallic/roughness/ao 읽기
- 멀티 라이트 루프 (directional, point, spot)
- PCF 섀도우
- IBL ambient (procedural sky + BRDF LUT)
- Reinhard 톤매핑 + 감마 보정
## Module Structure
### 새 파일
- `crates/voltex_renderer/src/gbuffer.rs` — GBuffer 타입 (텍스처 생성/리사이즈)
- `crates/voltex_renderer/src/fullscreen_quad.rs` — 풀스크린 삼각형 정점
- `crates/voltex_renderer/src/deferred_pipeline.rs` — 파이프라인 생성 (gbuffer pass + lighting pass)
- `crates/voltex_renderer/src/deferred_gbuffer.wgsl` — G-Buffer pass 셰이더
- `crates/voltex_renderer/src/deferred_lighting.wgsl` — Lighting pass 셰이더
### 수정 파일
- `crates/voltex_renderer/src/lib.rs` — 새 모듈 등록
## Types
### GBuffer
```rust
pub struct GBuffer {
pub position_view: TextureView, // Rgba32Float
pub normal_view: TextureView, // Rgba16Float
pub albedo_view: TextureView, // Rgba8UnormSrgb
pub material_view: TextureView, // Rgba8Unorm
pub depth_view: TextureView, // Depth32Float
pub width: u32,
pub height: u32,
}
```
- `new(device, width, height) -> Self`
- `resize(device, width, height)` — 윈도우 리사이즈 시 재생성
### DeferredPipeline
```rust
pub struct DeferredPipeline {
pub gbuffer_pipeline: RenderPipeline,
pub lighting_pipeline: RenderPipeline,
pub gbuffer_bind_group_layouts: [BindGroupLayout; 3], // camera, texture, material
pub lighting_bind_group_layouts: [BindGroupLayout; 3], // gbuffer, lights, shadow+ibl
}
```
- `new(device, surface_format) -> Self`
### Fullscreen Triangle
```rust
pub struct FullscreenTriangle {
pub vertex_buffer: Buffer,
}
```
3 정점: (-1,-1), (3,-1), (-1,3) — 클리핑으로 화면 커버. UV는 셰이더에서 position으로 계산.
## Bind Group Details
### G-Buffer Pass
**Group 0 — Camera (dynamic offset):**
- binding 0: CameraUniform (view_proj, model, camera_pos)
**Group 1 — Textures:**
- binding 0: albedo texture
- binding 1: albedo sampler
- binding 2: normal map texture
- binding 3: normal map sampler
**Group 2 — Material (dynamic offset):**
- binding 0: MaterialUniform (base_color, metallic, roughness, ao)
### Lighting Pass
**Group 0 — G-Buffer:**
- binding 0: position texture
- binding 1: normal texture
- binding 2: albedo texture
- binding 3: material texture
- binding 4: sampler (shared, nearest)
**Group 1 — Lights:**
- binding 0: LightsUniform
- binding 1: CameraPositionUniform (vec3 + padding)
**Group 2 — Shadow + IBL:**
- binding 0: shadow depth texture
- binding 1: shadow comparison sampler
- binding 2: ShadowUniform
- binding 3: BRDF LUT texture
- binding 4: BRDF LUT sampler
## Shader Summary
### deferred_gbuffer.wgsl
Vertex: position → world (model * pos), normal → world (model * normal), TBN 계산, UV 전달.
Fragment outputs (4 targets):
```wgsl
struct GBufferOutput {
@location(0) position: vec4<f32>,
@location(1) normal: vec4<f32>,
@location(2) albedo: vec4<f32>,
@location(3) material: vec4<f32>,
}
```
- position.xyz = world position
- normal.xyz = TBN-mapped world normal
- albedo.rgb = texture sample * base_color
- material = vec4(metallic, roughness, ao, 1.0)
### deferred_lighting.wgsl
Vertex: 풀스크린 삼각형, UV 계산.
Fragment:
1. G-Buffer 샘플링
2. Cook-Torrance BRDF (기존 pbr_shader.wgsl 로직)
3. 멀티 라이트 루프
4. PCF 섀도우
5. IBL ambient
6. Reinhard 톤매핑 + 감마
## Test Plan
### gbuffer.rs
- GBuffer 생성: 텍스처 크기 확인
- 리사이즈: 새 크기로 재생성
### fullscreen_quad.rs
- 정점 데이터: 3개 정점, 올바른 좌표
### 통합 (수동)
- deferred_demo 예제: 다수 포인트 라이트 + 디퍼드 렌더링
- G-Buffer 시각화 (디버그용: position/normal/albedo 각각 출력)
## Constraints
- max_bind_groups=4: G-Buffer pass 3개, Lighting pass 3개 사용 → 제약 내
- MRT: wgpu는 최대 8개 color attachment 지원. 4개 사용.
- Rgba32Float: Position에 32-bit float 사용 (정밀도 우선, 최적화는 추후)

View File

@@ -0,0 +1,202 @@
# 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 확인

View File

@@ -0,0 +1,197 @@
# Phase 7-3: RT Shadows — Design Spec
## Overview
wgpu의 EXPERIMENTAL_RAY_QUERY를 활용하여 하드웨어 레이트레이싱 기반 그림자를 구현한다. 기존 PCF shadow map을 대체하는 정확한 그림자.
## Hardware Requirements
- GPU: RTX 20xx+ / RDNA2+ (ray query 지원)
- wgpu Features: EXPERIMENTAL_RAY_QUERY
- 검증 완료: RTX 4050 Laptop GPU, Vulkan backend
## Scope
- BLAS/TLAS acceleration structure 생성 관리
- RT Shadow 컴퓨트 셰이더 (ray query로 directional light shadow)
- RT Shadow 출력 텍스처 (R8Unorm)
- Lighting Pass에 RT shadow 통합
- deferred_demo에 RT shadow 적용
## Out of Scope
- RT Reflections
- RT AO
- Point/Spot light RT shadows
- Soft RT shadows (multi-ray)
- BLAS 재빌드 (정적 지오메트리만)
## Render Pass Flow (디퍼드 확장)
```
Pass 1: G-Buffer (변경 없음)
Pass 2: SSGI (변경 없음)
Pass 3: RT Shadow (NEW) — 컴퓨트 셰이더, ray query로 shadow 텍스처 출력
Pass 4: Lighting (수정) — RT shadow 텍스처 사용
```
## Module Structure
### 새 파일
- `crates/voltex_renderer/src/rt_accel.rs` — RtAccel (BLAS/TLAS 관리)
- `crates/voltex_renderer/src/rt_shadow.rs` — RtShadowResources + 컴퓨트 파이프라인
- `crates/voltex_renderer/src/rt_shadow_shader.wgsl` — RT shadow 컴퓨트 셰이더
### 수정 파일
- `crates/voltex_renderer/src/deferred_pipeline.rs` — lighting shadow bind group에 RT shadow 텍스처 추가
- `crates/voltex_renderer/src/deferred_lighting.wgsl` — RT shadow 사용
- `crates/voltex_renderer/src/lib.rs` — 새 모듈 등록
- `examples/deferred_demo/src/main.rs` — RT shadow 통합
## Types
### RtAccel
```rust
pub struct RtAccel {
pub blas_list: Vec<wgpu::Blas>,
pub tlas_package: wgpu::TlasPackage,
}
```
**Methods:**
- `new(device, meshes: &[(vertex_buffer, index_buffer, vertex_count, index_count)], transforms: &[[f32; 12]])` — BLAS 빌드, TLAS 구성
- BLAS: 메시별 삼각형 지오메트리 (BlasTriangleGeometry)
- TLAS: 인스턴스 배열 (TlasInstance with transform, blas index)
**BLAS 생성:**
1. BlasTriangleGeometrySizeDescriptor (vertex_count, index_count, vertex_format: Float32x3)
2. device.create_blas(size, flags: PREFER_FAST_TRACE)
3. encoder.build_acceleration_structures with BlasBuildEntry (vertex_buffer, index_buffer, geometry)
**TLAS 생성:**
1. device.create_tlas(max_instances: transform_count)
2. TlasPackage에 TlasInstance 채움 (transform [3x4 row-major], blas_index, mask: 0xFF)
3. encoder.build_acceleration_structures with tlas_package
### RtShadowResources
```rust
pub struct RtShadowResources {
pub shadow_view: TextureView, // R8Unorm, STORAGE_BINDING
pub shadow_texture: Texture,
pub uniform_buffer: Buffer, // RtShadowUniform
pub width: u32,
pub height: u32,
}
```
### RtShadowUniform
```rust
#[repr(C)]
pub struct RtShadowUniform {
pub light_direction: [f32; 3],
pub _pad0: f32,
pub width: u32,
pub height: u32,
pub _pad1: [u32; 2],
}
```
## RT Shadow Compute Shader
### 바인드 그룹
**Group 0: G-Buffer**
- binding 0: position texture (Float, non-filterable)
- binding 1: normal texture (Float, filterable)
**Group 1: RT Data**
- binding 0: TLAS (acceleration_structure)
- binding 1: RT shadow output (storage texture, r32float, write)
- binding 2: RtShadowUniform
### 셰이더 로직
```wgsl
@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
if id.x >= uniforms.width || id.y >= uniforms.height { return; }
let world_pos = textureLoad(t_position, id.xy, 0).xyz;
// Skip background
if dot(world_pos, world_pos) < 0.001 {
textureStore(t_shadow_out, id.xy, vec4(1.0));
return;
}
let normal = normalize(textureLoad(t_normal, id.xy, 0).xyz * 2.0 - 1.0);
let ray_origin = world_pos + normal * 0.01; // bias off surface
let ray_dir = normalize(-uniforms.light_direction);
var rq: ray_query;
rayQueryInitialize(&rq, tlas, RAY_FLAG_TERMINATE_ON_FIRST_HIT,
0xFFu, ray_origin, 0.001, ray_dir, 1000.0);
rayQueryProceed(&rq);
var shadow = 1.0; // lit by default
if rayQueryGetCommittedIntersectionType(&rq) != RAY_QUERY_COMMITTED_INTERSECTION_NONE {
shadow = 0.0; // occluded
}
textureStore(t_shadow_out, id.xy, vec4(shadow, 0.0, 0.0, 0.0));
}
```
## Lighting Pass 수정
RT shadow 텍스처를 기존 shadow_factor 대신 사용:
```wgsl
// 기존: let shadow_factor = calculate_shadow(world_pos);
// 변경: RT shadow map에서 직접 읽기
let rt_shadow = textureSample(t_rt_shadow, s_rt_shadow, uv).r;
let shadow_factor = rt_shadow;
```
기존 PCF shadow map 관련 바인딩은 유지하되 사용하지 않음 (호환성).
RT shadow 텍스처를 Group 2의 추가 바인딩(7, 8)으로 추가.
## Device Creation 변경
RT feature를 요청해야 함:
```rust
let (device, queue) = adapter.request_device(&DeviceDescriptor {
required_features: Features::EXPERIMENTAL_RAY_QUERY,
..
}).await;
```
기존 GpuContext::new()는 features를 요청하지 않으므로, deferred_demo에서 직접 device를 생성하거나 GpuContext에 optional features 파라미터를 추가.
## Bind Group Details
### RT Shadow Compute
**Group 0:**
- binding 0: position texture (texture_2d<f32>)
- binding 1: normal texture (texture_2d<f32>)
**Group 1:**
- binding 0: acceleration_structure (TLAS)
- binding 1: storage texture (r32float, write)
- binding 2: uniform buffer (RtShadowUniform)
### Lighting Pass Group 2 (확장)
기존 7 bindings (0-6: shadow+IBL+SSGI) + 추가:
- binding 7: RT shadow texture (Float, filterable)
- binding 8: RT shadow sampler (Filtering)
## Test Plan
- rt_accel.rs: 빌드 확인만 (GPU 의존)
- rt_shadow.rs: RtShadowUniform 크기, 리소스 생성
- 통합: deferred_demo에서 RT shadow ON, 기존 PCF OFF → 날카로운 그림자 확인