# Phase 7-1: Deferred Rendering Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** G-Buffer + Lighting Pass 디퍼드 렌더링 파이프라인으로 다수의 라이트를 효율적으로 처리 **Architecture:** voltex_renderer에 새 모듈 추가. G-Buffer pass(MRT 4개)가 기하 데이터를 기록하고, Lighting pass(풀스크린 삼각형)가 G-Buffer를 읽어 Cook-Torrance BRDF + 섀도우 + IBL 라이팅을 수행. 기존 포워드 PBR은 유지. **Tech Stack:** Rust, wgpu 28.0, WGSL **Spec:** `docs/superpowers/specs/2026-03-25-phase7-1-deferred-rendering.md` --- ## File Structure ### voltex_renderer (추가) - `crates/voltex_renderer/src/gbuffer.rs` — GBuffer 텍스처 생성/리사이즈 (Create) - `crates/voltex_renderer/src/fullscreen_quad.rs` — 풀스크린 삼각형 (Create) - `crates/voltex_renderer/src/deferred_gbuffer.wgsl` — G-Buffer pass 셰이더 (Create) - `crates/voltex_renderer/src/deferred_lighting.wgsl` — Lighting pass 셰이더 (Create) - `crates/voltex_renderer/src/deferred_pipeline.rs` — 파이프라인 생성 함수들 (Create) - `crates/voltex_renderer/src/lib.rs` — 새 모듈 등록 (Modify) ### Example (추가) - `examples/deferred_demo/Cargo.toml` (Create) - `examples/deferred_demo/src/main.rs` (Create) - `Cargo.toml` — workspace members (Modify) --- ## Task 1: GBuffer + Fullscreen Triangle **Files:** - Create: `crates/voltex_renderer/src/gbuffer.rs` - Create: `crates/voltex_renderer/src/fullscreen_quad.rs` - Modify: `crates/voltex_renderer/src/lib.rs` - [ ] **Step 1: gbuffer.rs 작성** ```rust // crates/voltex_renderer/src/gbuffer.rs pub const GBUFFER_POSITION_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float; pub const GBUFFER_NORMAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float; pub const GBUFFER_ALBEDO_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8UnormSrgb; pub const GBUFFER_MATERIAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm; pub struct GBuffer { pub position_view: wgpu::TextureView, pub normal_view: wgpu::TextureView, pub albedo_view: wgpu::TextureView, pub material_view: wgpu::TextureView, pub depth_view: wgpu::TextureView, pub width: u32, pub height: u32, } impl GBuffer { pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self { let position_view = create_rt(device, width, height, GBUFFER_POSITION_FORMAT, "GBuffer Position"); let normal_view = create_rt(device, width, height, GBUFFER_NORMAL_FORMAT, "GBuffer Normal"); let albedo_view = create_rt(device, width, height, GBUFFER_ALBEDO_FORMAT, "GBuffer Albedo"); let material_view = create_rt(device, width, height, GBUFFER_MATERIAL_FORMAT, "GBuffer Material"); let depth_view = create_depth(device, width, height); Self { position_view, normal_view, albedo_view, material_view, depth_view, width, height } } pub fn resize(&mut self, device: &wgpu::Device, width: u32, height: u32) { *self = Self::new(device, width, height); } } fn create_rt(device: &wgpu::Device, w: u32, h: u32, format: wgpu::TextureFormat, label: &str) -> wgpu::TextureView { let tex = device.create_texture(&wgpu::TextureDescriptor { label: Some(label), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); tex.create_view(&wgpu::TextureViewDescriptor::default()) } fn create_depth(device: &wgpu::Device, w: u32, h: u32) -> wgpu::TextureView { let tex = device.create_texture(&wgpu::TextureDescriptor { label: Some("GBuffer Depth"), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: crate::gpu::DEPTH_FORMAT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }); tex.create_view(&wgpu::TextureViewDescriptor::default()) } ``` - [ ] **Step 2: fullscreen_quad.rs 작성** ```rust // crates/voltex_renderer/src/fullscreen_quad.rs use bytemuck::{Pod, Zeroable}; #[repr(C)] #[derive(Copy, Clone, Debug, Pod, Zeroable)] pub struct FullscreenVertex { pub position: [f32; 2], } impl FullscreenVertex { pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, attributes: &[ wgpu::VertexAttribute { offset: 0, shader_location: 0, format: wgpu::VertexFormat::Float32x2, }, ], }; } /// Oversized triangle that covers the entire screen after clipping. pub const FULLSCREEN_VERTICES: [FullscreenVertex; 3] = [ FullscreenVertex { position: [-1.0, -1.0] }, FullscreenVertex { position: [ 3.0, -1.0] }, FullscreenVertex { position: [-1.0, 3.0] }, ]; pub fn create_fullscreen_vertex_buffer(device: &wgpu::Device) -> wgpu::Buffer { use wgpu::util::DeviceExt; device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Fullscreen Vertex Buffer"), contents: bytemuck::cast_slice(&FULLSCREEN_VERTICES), usage: wgpu::BufferUsages::VERTEX, }) } ``` - [ ] **Step 3: lib.rs에 모듈 등록** ```rust pub mod gbuffer; pub mod fullscreen_quad; ``` And add re-exports: ```rust pub use gbuffer::GBuffer; pub use fullscreen_quad::{create_fullscreen_vertex_buffer, FullscreenVertex}; ``` - [ ] **Step 4: 빌드 확인** Run: `cargo build -p voltex_renderer` Expected: 컴파일 성공 - [ ] **Step 5: 커밋** ```bash git add crates/voltex_renderer/src/gbuffer.rs crates/voltex_renderer/src/fullscreen_quad.rs crates/voltex_renderer/src/lib.rs git commit -m "feat(renderer): add GBuffer and fullscreen triangle for deferred rendering" ``` --- ## Task 2: G-Buffer Pass 셰이더 **Files:** - Create: `crates/voltex_renderer/src/deferred_gbuffer.wgsl` - [ ] **Step 1: deferred_gbuffer.wgsl 작성** ```wgsl // G-Buffer pass: writes geometry data to multiple render targets struct CameraUniform { view_proj: mat4x4, model: mat4x4, camera_pos: vec3, }; struct MaterialUniform { base_color: vec4, metallic: f32, roughness: f32, ao: f32, }; @group(0) @binding(0) var camera: CameraUniform; @group(1) @binding(0) var t_diffuse: texture_2d; @group(1) @binding(1) var s_diffuse: sampler; @group(1) @binding(2) var t_normal: texture_2d; @group(1) @binding(3) var s_normal: sampler; @group(2) @binding(0) var material: MaterialUniform; struct VertexInput { @location(0) position: vec3, @location(1) normal: vec3, @location(2) uv: vec2, @location(3) tangent: vec4, }; struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) world_pos: vec3, @location(1) world_normal: vec3, @location(2) uv: vec2, @location(3) world_tangent: vec3, @location(4) world_bitangent: vec3, }; struct GBufferOutput { @location(0) position: vec4, @location(1) normal: vec4, @location(2) albedo: vec4, @location(3) material_out: vec4, }; @vertex fn vs_main(in: VertexInput) -> VertexOutput { var out: VertexOutput; let world_pos = camera.model * vec4(in.position, 1.0); out.world_pos = world_pos.xyz; out.clip_position = camera.view_proj * world_pos; out.world_normal = normalize((camera.model * vec4(in.normal, 0.0)).xyz); out.uv = in.uv; let T = normalize((camera.model * vec4(in.tangent.xyz, 0.0)).xyz); let N = out.world_normal; let B = cross(N, T) * in.tangent.w; out.world_tangent = T; out.world_bitangent = B; return out; } @fragment fn fs_main(in: VertexOutput) -> GBufferOutput { var out: GBufferOutput; // World position out.position = vec4(in.world_pos, 1.0); // Normal mapping let T = normalize(in.world_tangent); let B = normalize(in.world_bitangent); let N_geom = normalize(in.world_normal); let normal_sample = textureSample(t_normal, s_normal, in.uv).rgb; let tangent_normal = normal_sample * 2.0 - 1.0; let TBN = mat3x3(T, B, N_geom); let N = normalize(TBN * tangent_normal); out.normal = vec4(N, 0.0); // Albedo let tex_color = textureSample(t_diffuse, s_diffuse, in.uv); out.albedo = vec4(material.base_color.rgb * tex_color.rgb, 1.0); // Material: R=metallic, G=roughness, B=ao out.material_out = vec4(material.metallic, material.roughness, material.ao, 1.0); return out; } ``` - [ ] **Step 2: 커밋** ```bash git add crates/voltex_renderer/src/deferred_gbuffer.wgsl git commit -m "feat(renderer): add G-Buffer pass shader for deferred rendering" ``` --- ## Task 3: Lighting Pass 셰이더 **Files:** - Create: `crates/voltex_renderer/src/deferred_lighting.wgsl` - [ ] **Step 1: deferred_lighting.wgsl 작성** This shader reuses the Cook-Torrance BRDF functions from pbr_shader.wgsl but reads from G-Buffer instead of vertex attributes. ```wgsl // Deferred Lighting Pass: reads G-Buffer, applies full PBR lighting // Group 0: G-Buffer textures @group(0) @binding(0) var t_position: texture_2d; @group(0) @binding(1) var t_normal: texture_2d; @group(0) @binding(2) var t_albedo: texture_2d; @group(0) @binding(3) var t_material: texture_2d; @group(0) @binding(4) var s_gbuffer: sampler; // Group 1: Lights + Camera struct LightData { position: vec3, light_type: u32, direction: vec3, range: f32, color: vec3, intensity: f32, inner_cone: f32, outer_cone: f32, _padding: vec2, }; struct LightsUniform { lights: array, count: u32, ambient_color: vec3, }; struct CameraPositionUniform { camera_pos: vec3, }; @group(1) @binding(0) var lights_uniform: LightsUniform; @group(1) @binding(1) var camera_data: CameraPositionUniform; // Group 2: Shadow + IBL struct ShadowUniform { light_view_proj: mat4x4, shadow_map_size: f32, shadow_bias: f32, }; @group(2) @binding(0) var t_shadow: texture_depth_2d; @group(2) @binding(1) var s_shadow: sampler_comparison; @group(2) @binding(2) var shadow: ShadowUniform; @group(2) @binding(3) var t_brdf_lut: texture_2d; @group(2) @binding(4) var s_brdf_lut: sampler; // Fullscreen vertex struct VertexOutput { @builtin(position) clip_position: vec4, @location(0) uv: vec2, }; @vertex fn vs_main(@location(0) position: vec2) -> VertexOutput { var out: VertexOutput; out.clip_position = vec4(position, 0.0, 1.0); // Convert clip space [-1,1] to UV [0,1] out.uv = vec2(position.x * 0.5 + 0.5, 1.0 - (position.y * 0.5 + 0.5)); return out; } // === BRDF functions (same as pbr_shader.wgsl) === fn distribution_ggx(N: vec3, H: vec3, roughness: f32) -> f32 { let a = roughness * roughness; let a2 = a * a; let NdotH = max(dot(N, H), 0.0); let NdotH2 = NdotH * NdotH; let denom_inner = NdotH2 * (a2 - 1.0) + 1.0; let denom = 3.14159265358979 * denom_inner * denom_inner; return a2 / denom; } fn geometry_schlick_ggx(NdotV: f32, roughness: f32) -> f32 { let r = roughness + 1.0; let k = (r * r) / 8.0; return NdotV / (NdotV * (1.0 - k) + k); } fn geometry_smith(N: vec3, V: vec3, L: vec3, roughness: f32) -> f32 { let NdotV = max(dot(N, V), 0.0); let NdotL = max(dot(N, L), 0.0); return geometry_schlick_ggx(NdotV, roughness) * geometry_schlick_ggx(NdotL, roughness); } fn fresnel_schlick(cosTheta: f32, F0: vec3) -> vec3 { return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0); } fn attenuation_point(distance: f32, range: f32) -> f32 { let d_over_r = distance / range; let d_over_r4 = d_over_r * d_over_r * d_over_r * d_over_r; let falloff = clamp(1.0 - d_over_r4, 0.0, 1.0); return (falloff * falloff) / (distance * distance + 0.0001); } fn attenuation_spot(light: LightData, L: vec3) -> f32 { let spot_dir = normalize(light.direction); let theta = dot(spot_dir, -L); return clamp( (theta - light.outer_cone) / (light.inner_cone - light.outer_cone + 0.0001), 0.0, 1.0, ); } fn compute_light_contribution( light: LightData, N: vec3, V: vec3, world_pos: vec3, F0: vec3, albedo: vec3, metallic: f32, roughness: f32, ) -> vec3 { var L: vec3; var radiance: vec3; if light.light_type == 0u { L = normalize(-light.direction); radiance = light.color * light.intensity; } else if light.light_type == 1u { let to_light = light.position - world_pos; let dist = length(to_light); L = normalize(to_light); radiance = light.color * light.intensity * attenuation_point(dist, light.range); } else { let to_light = light.position - world_pos; let dist = length(to_light); L = normalize(to_light); radiance = light.color * light.intensity * attenuation_point(dist, light.range) * attenuation_spot(light, L); } let H = normalize(V + L); let NDF = distribution_ggx(N, H, roughness); let G = geometry_smith(N, V, L, roughness); let F = fresnel_schlick(max(dot(H, V), 0.0), F0); let ks = F; let kd = (vec3(1.0) - ks) * (1.0 - metallic); let numerator = NDF * G * F; let NdotL = max(dot(N, L), 0.0); let NdotV = max(dot(N, V), 0.0); let denominator = 4.0 * NdotV * NdotL + 0.0001; let specular = numerator / denominator; return (kd * albedo / 3.14159265358979 + specular) * radiance * NdotL; } fn calculate_shadow(world_pos: vec3) -> f32 { if shadow.shadow_map_size == 0.0 { return 1.0; } let light_space_pos = shadow.light_view_proj * vec4(world_pos, 1.0); let proj_coords = light_space_pos.xyz / light_space_pos.w; let shadow_uv = vec2(proj_coords.x * 0.5 + 0.5, -proj_coords.y * 0.5 + 0.5); let current_depth = proj_coords.z; if shadow_uv.x < 0.0 || shadow_uv.x > 1.0 || shadow_uv.y < 0.0 || shadow_uv.y > 1.0 { return 1.0; } if current_depth > 1.0 || current_depth < 0.0 { return 1.0; } let texel_size = 1.0 / shadow.shadow_map_size; var shadow_val = 0.0; for (var x = -1; x <= 1; x++) { for (var y = -1; y <= 1; y++) { shadow_val += textureSampleCompare(t_shadow, s_shadow, shadow_uv + vec2(f32(x), f32(y)) * texel_size, current_depth - shadow.shadow_bias); } } return shadow_val / 9.0; } fn sample_environment(direction: vec3, roughness: f32) -> vec3 { var env: vec3; if direction.y > 0.0 { env = mix(vec3(0.6, 0.6, 0.5), vec3(0.3, 0.5, 0.9), pow(direction.y, 0.4)); } else { env = mix(vec3(0.6, 0.6, 0.5), vec3(0.1, 0.08, 0.06), pow(-direction.y, 0.4)); } return mix(env, vec3(0.3, 0.35, 0.4), roughness * roughness); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4 { let world_pos = textureSample(t_position, s_gbuffer, in.uv).xyz; let N = normalize(textureSample(t_normal, s_gbuffer, in.uv).xyz); let albedo = textureSample(t_albedo, s_gbuffer, in.uv).rgb; let mat_data = textureSample(t_material, s_gbuffer, in.uv); let metallic = mat_data.r; let roughness = mat_data.g; let ao = mat_data.b; // Skip background pixels (position = 0,0,0 means no geometry) if length(textureSample(t_position, s_gbuffer, in.uv).xyz) < 0.001 { return vec4(0.05, 0.05, 0.08, 1.0); // background color } let V = normalize(camera_data.camera_pos - world_pos); let F0 = mix(vec3(0.04), albedo, metallic); let shadow_factor = calculate_shadow(world_pos); var Lo = vec3(0.0); let light_count = min(lights_uniform.count, 16u); for (var i = 0u; i < light_count; i++) { var contribution = compute_light_contribution( lights_uniform.lights[i], N, V, world_pos, F0, albedo, metallic, roughness, ); if lights_uniform.lights[i].light_type == 0u { contribution = contribution * shadow_factor; } Lo += contribution; } // IBL let NdotV_ibl = max(dot(N, V), 0.0); let R = reflect(-V, N); let irradiance = sample_environment(N, 1.0); let F_env = fresnel_schlick(NdotV_ibl, F0); let kd_ibl = (vec3(1.0) - F_env) * (1.0 - metallic); let diffuse_ibl = kd_ibl * albedo * irradiance; let prefiltered = sample_environment(R, roughness); let brdf_val = textureSample(t_brdf_lut, s_brdf_lut, vec2(NdotV_ibl, roughness)); let specular_ibl = prefiltered * (F0 * brdf_val.r + vec3(brdf_val.g)); let ambient = (diffuse_ibl + specular_ibl) * ao; var color = ambient + Lo; color = color / (color + vec3(1.0)); // Reinhard color = pow(color, vec3(1.0 / 2.2)); // Gamma return vec4(color, 1.0); } ``` - [ ] **Step 2: 커밋** ```bash git add crates/voltex_renderer/src/deferred_lighting.wgsl git commit -m "feat(renderer): add deferred lighting pass shader with Cook-Torrance BRDF" ``` --- ## Task 4: Deferred Pipeline (Rust) **Files:** - Create: `crates/voltex_renderer/src/deferred_pipeline.rs` - Modify: `crates/voltex_renderer/src/lib.rs` - [ ] **Step 1: deferred_pipeline.rs 작성** This file creates both G-Buffer pass and Lighting pass pipelines, plus their bind group layouts. ```rust // crates/voltex_renderer/src/deferred_pipeline.rs use crate::vertex::MeshVertex; use crate::fullscreen_quad::FullscreenVertex; use crate::gbuffer::*; use crate::gpu::DEPTH_FORMAT; // === G-Buffer Pass === pub fn gbuffer_camera_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("GBuffer Camera BGL"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: true, min_binding_size: None, }, count: None, }, ], }) } pub fn create_gbuffer_pipeline( device: &wgpu::Device, camera_layout: &wgpu::BindGroupLayout, texture_layout: &wgpu::BindGroupLayout, material_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Deferred GBuffer Shader"), source: wgpu::ShaderSource::Wgsl(include_str!("deferred_gbuffer.wgsl").into()), }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("GBuffer Pipeline Layout"), bind_group_layouts: &[camera_layout, texture_layout, material_layout], immediate_size: 0, }); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("GBuffer Pipeline"), layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[MeshVertex::LAYOUT], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), targets: &[ Some(wgpu::ColorTargetState { format: GBUFFER_POSITION_FORMAT, blend: None, write_mask: wgpu::ColorWrites::ALL, }), Some(wgpu::ColorTargetState { format: GBUFFER_NORMAL_FORMAT, blend: None, write_mask: wgpu::ColorWrites::ALL, }), Some(wgpu::ColorTargetState { format: GBUFFER_ALBEDO_FORMAT, blend: None, write_mask: wgpu::ColorWrites::ALL, }), Some(wgpu::ColorTargetState { format: GBUFFER_MATERIAL_FORMAT, blend: None, write_mask: wgpu::ColorWrites::ALL, }), ], compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, front_face: wgpu::FrontFace::Ccw, cull_mode: Some(wgpu::Face::Back), ..Default::default() }, depth_stencil: Some(wgpu::DepthStencilState { format: DEPTH_FORMAT, depth_write_enabled: true, depth_compare: wgpu::CompareFunction::Less, stencil: wgpu::StencilState::default(), bias: wgpu::DepthBiasState::default(), }), multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }) } // === Lighting Pass === pub fn lighting_gbuffer_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Lighting GBuffer BGL"), entries: &[ // position texture wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: false }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, // normal texture wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, // albedo texture wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, // material texture wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, // sampler wgpu::BindGroupLayoutEntry { binding: 4, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::NonFiltering), count: None, }, ], }) } pub fn lighting_lights_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Lighting Lights BGL"), entries: &[ // LightsUniform wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, // CameraPositionUniform wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, ], }) } pub fn lighting_shadow_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("Lighting Shadow+IBL BGL"), entries: &[ // shadow depth texture wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Depth, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, // shadow comparison sampler wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison), count: None, }, // ShadowUniform wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None, }, count: None, }, // BRDF LUT texture wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { sample_type: wgpu::TextureSampleType::Float { filterable: true }, view_dimension: wgpu::TextureViewDimension::D2, multisampled: false, }, count: None, }, // BRDF LUT sampler wgpu::BindGroupLayoutEntry { binding: 4, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }) } pub fn create_lighting_pipeline( device: &wgpu::Device, surface_format: wgpu::TextureFormat, gbuffer_layout: &wgpu::BindGroupLayout, lights_layout: &wgpu::BindGroupLayout, shadow_layout: &wgpu::BindGroupLayout, ) -> wgpu::RenderPipeline { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Deferred Lighting Shader"), source: wgpu::ShaderSource::Wgsl(include_str!("deferred_lighting.wgsl").into()), }); let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Lighting Pipeline Layout"), bind_group_layouts: &[gbuffer_layout, lights_layout, shadow_layout], immediate_size: 0, }); device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Lighting Pipeline"), layout: Some(&layout), vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[FullscreenVertex::LAYOUT], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() }, depth_stencil: None, // No depth for fullscreen pass multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }) } ``` - [ ] **Step 2: lib.rs에 deferred_pipeline 등록** ```rust pub mod deferred_pipeline; ``` And re-exports: ```rust pub use deferred_pipeline::{ create_gbuffer_pipeline, create_lighting_pipeline, gbuffer_camera_bind_group_layout, lighting_gbuffer_bind_group_layout, lighting_lights_bind_group_layout, lighting_shadow_bind_group_layout, }; ``` - [ ] **Step 3: 빌드 확인** Run: `cargo build -p voltex_renderer` Expected: 컴파일 성공 Run: `cargo test --workspace` Expected: all pass (기존 200개) - [ ] **Step 4: 커밋** ```bash git add crates/voltex_renderer/src/deferred_pipeline.rs crates/voltex_renderer/src/lib.rs git commit -m "feat(renderer): add deferred rendering pipeline (G-Buffer + Lighting pass)" ``` --- ## Task 5: deferred_demo 예제 **Files:** - Create: `examples/deferred_demo/Cargo.toml` - Create: `examples/deferred_demo/src/main.rs` - Modify: `Cargo.toml` (workspace members) NOTE: 이 예제는 복잡합니다 (GPU 리소스 설정, 바인드 그룹 생성, 2-pass 렌더). 기존 pbr_demo 패턴을 따르되 디퍼드로 변경. 구체 그리드 + 다수 포인트 라이트 씬. 이 태스크는 가장 큰 구현이며, 더 capable한 모델로 실행해야 합니다. - [ ] **Step 1: Cargo.toml** ```toml [package] name = "deferred_demo" version = "0.1.0" edition = "2021" [dependencies] voltex_math.workspace = true voltex_platform.workspace = true voltex_renderer.workspace = true bytemuck.workspace = true pollster.workspace = true wgpu.workspace = true ``` - [ ] **Step 2: main.rs 작성** The example should: 1. Create window + GpuContext 2. Create GBuffer 3. Create G-Buffer pipeline + Lighting pipeline with proper bind group layouts 4. Generate sphere meshes (5x5 grid of metallic/roughness variations) 5. Set up 8 point lights orbiting the scene (to show deferred advantage) 6. Create all uniform buffers, textures, bind groups 7. Main loop: - Update camera (FPS controller) - Update light positions (orbit animation) - Pass 1: G-Buffer pass (render all objects to MRT) - Pass 2: Lighting pass (fullscreen quad, reads G-Buffer) - Present Key: must create CameraPositionUniform buffer (vec3 + padding = 16 bytes) for the lighting pass. - [ ] **Step 3: workspace에 추가** `Cargo.toml` members에 `"examples/deferred_demo"` 추가. - [ ] **Step 4: 빌드 + 실행 확인** Run: `cargo build --bin deferred_demo` Run: `cargo run --bin deferred_demo` (수동 확인) - [ ] **Step 5: 커밋** ```bash git add examples/deferred_demo/ Cargo.toml git commit -m "feat(renderer): add deferred_demo example with multi-light deferred rendering" ``` --- ## Task 6: 문서 업데이트 **Files:** - Modify: `docs/STATUS.md` - Modify: `docs/DEFERRED.md` - [ ] **Step 1: STATUS.md에 Phase 7-1 추가** Phase 6-3 아래에: ```markdown ### Phase 7-1: Deferred Rendering - voltex_renderer: GBuffer (4 MRT: Position/Normal/Albedo/Material + Depth) - voltex_renderer: G-Buffer pass shader (MRT output, TBN normal mapping) - voltex_renderer: Lighting pass shader (fullscreen quad, Cook-Torrance BRDF, multi-light, shadow, IBL) - voltex_renderer: Deferred pipeline (gbuffer + lighting bind group layouts) - examples/deferred_demo (5x5 sphere grid + 8 orbiting point lights) ``` 예제 수 11로 업데이트. - [ ] **Step 2: DEFERRED.md에 Phase 7-1 미뤄진 항목 추가** ```markdown ## Phase 7-1 - **투명 오브젝트** — 디퍼드에서 처리 불가. 별도 포워드 패스 필요. - **G-Buffer 압축** — Position을 depth에서 복원, Normal을 octahedral 인코딩 등 미적용. - **Light Volumes** — 풀스크린 라이팅만. 라이트별 sphere/cone 렌더 미구현. - **Stencil 최적화** — 미구현. ``` - [ ] **Step 3: 커밋** ```bash git add docs/STATUS.md docs/DEFERRED.md git commit -m "docs: add Phase 7-1 deferred rendering status and deferred items" ```