Files
game_engine/docs/superpowers/specs/2026-03-25-phase7-3-rt-shadows.md
2026-03-25 13:25:11 +09:00

5.8 KiB

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

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

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

#[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

셰이더 로직

@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 대신 사용:

// 기존: 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를 요청해야 함:

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)
  • binding 1: normal texture (texture_2d)

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 → 날카로운 그림자 확인