# PBR Texture Maps 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:** Add metallic/roughness/AO (ORM) and emissive texture map sampling to PBR shaders, extending bind group 1 from 4 to 8 bindings. **Architecture:** Extend `pbr_texture_bind_group_layout` with 4 new bindings (ORM texture+sampler, emissive texture+sampler). Update forward PBR shader and deferred G-Buffer shader. Default 1x1 white/black textures when maps not provided. MaterialUniform values become multipliers for texture values. **Tech Stack:** wgpu 28.0, WGSL shaders. No new Rust crates. --- ### Task 1: Texture Utility Additions **Files:** - Modify: `crates/voltex_renderer/src/texture.rs` - [ ] **Step 1: Write test for black_1x1** ```rust #[test] fn test_black_1x1_exists() { // This is a compile/API test — GPU tests need device // We verify the function signature exists and the module compiles } ``` - [ ] **Step 2: Add black_1x1 and extended layout functions** Add to `texture.rs`: ```rust /// 1x1 black texture for emissive default (no emission). pub fn black_1x1( device: &wgpu::Device, queue: &wgpu::Queue, layout: &wgpu::BindGroupLayout, ) -> GpuTexture { Self::from_rgba(device, queue, 1, 1, &[0, 0, 0, 255], layout) } /// Extended PBR texture layout: albedo + normal + ORM + emissive (8 bindings). pub fn pbr_full_texture_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("PBR Full Texture Bind Group Layout"), entries: &[ // 0-1: albedo (existing) wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, // 2-3: normal map (existing) wgpu::BindGroupLayoutEntry { binding: 2, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 3, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, // 4-5: ORM (AO/Roughness/Metallic) — NEW wgpu::BindGroupLayoutEntry { binding: 4, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 5, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, // 6-7: Emissive — NEW wgpu::BindGroupLayoutEntry { binding: 6, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 7, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], }) } /// Create bind group for full PBR textures (albedo + normal + ORM + emissive). pub fn create_pbr_full_texture_bind_group( device: &wgpu::Device, layout: &wgpu::BindGroupLayout, albedo_view: &wgpu::TextureView, albedo_sampler: &wgpu::Sampler, normal_view: &wgpu::TextureView, normal_sampler: &wgpu::Sampler, orm_view: &wgpu::TextureView, orm_sampler: &wgpu::Sampler, emissive_view: &wgpu::TextureView, emissive_sampler: &wgpu::Sampler, ) -> wgpu::BindGroup { // ... 8 entries at bindings 0-7 } ``` - [ ] **Step 3: Add exports to lib.rs** ```rust pub use texture::{pbr_full_texture_bind_group_layout, create_pbr_full_texture_bind_group}; ``` - [ ] **Step 4: Commit** ```bash git add crates/voltex_renderer/src/texture.rs crates/voltex_renderer/src/lib.rs git commit -m "feat(renderer): add ORM and emissive texture bind group layout" ``` --- ### Task 2: Update PBR Forward Shader **Files:** - Modify: `crates/voltex_renderer/src/pbr_shader.wgsl` - [ ] **Step 1: Add ORM and emissive bindings** Add after existing normal map bindings: ```wgsl @group(1) @binding(4) var t_orm: texture_2d; @group(1) @binding(5) var s_orm: sampler; @group(1) @binding(6) var t_emissive: texture_2d; @group(1) @binding(7) var s_emissive: sampler; ``` - [ ] **Step 2: Update fragment shader to sample ORM** Replace lines that read material params directly: ```wgsl // Before: // let metallic = material.metallic; // let roughness = material.roughness; // let ao = material.ao; // After: let orm_sample = textureSample(t_orm, s_orm, in.uv); let ao = orm_sample.r * material.ao; let roughness = orm_sample.g * material.roughness; let metallic = orm_sample.b * material.metallic; ``` - [ ] **Step 3: Add emissive to final color** ```wgsl let emissive = textureSample(t_emissive, s_emissive, in.uv).rgb; // Before tone mapping: var color = ambient + Lo + emissive; ``` - [ ] **Step 4: Verify existing examples still compile** The existing examples use `pbr_texture_bind_group_layout` (4 bindings). They should continue to work with the OLD layout — the new `pbr_full_texture_bind_group_layout` is a separate function. The shader needs to match whichever layout is used. **Strategy:** Create a NEW shader variant `pbr_shader_full.wgsl` with ORM+emissive support, keeping the old shader untouched for backward compatibility. OR add a preprocessor approach. **Simpler approach:** Just update `pbr_shader.wgsl` to include the new bindings AND update ALL examples that use it to use the full layout with default white/black textures. This avoids shader duplication. - [ ] **Step 5: Commit** ```bash git add crates/voltex_renderer/src/pbr_shader.wgsl git commit -m "feat(renderer): add ORM and emissive texture sampling to PBR shader" ``` --- ### Task 3: Update Deferred G-Buffer Shader **Files:** - Modify: `crates/voltex_renderer/src/deferred_gbuffer.wgsl` - Modify: `crates/voltex_renderer/src/deferred_lighting.wgsl` - [ ] **Step 1: Add ORM/emissive bindings to G-Buffer shader** Same pattern as forward shader — add bindings 4-7, sample ORM for metallic/roughness/ao, store in material_data output. - [ ] **Step 2: Store emissive in G-Buffer** Option: Pack emissive into existing G-Buffer outputs or add a 5th MRT. **Recommended:** Store emissive in material_data.a (was padding=1.0). Simple, no extra MRT. ```wgsl // G-Buffer output location(3): [metallic, roughness, ao, emissive_intensity] // Emissive color stored as luminance for simplicity, or use location(2).a out.material_data = vec4(metallic, roughness, ao, emissive_luminance); ``` Actually simpler: add emissive directly in the lighting pass as a texture sample pass-through. But that requires the emissive texture in the lighting pass too. **Simplest approach:** Write emissive to albedo output additively, since emissive bypasses lighting. ```wgsl // In deferred_gbuffer.wgsl: let emissive = textureSample(t_emissive, s_emissive, in.uv).rgb; // Store emissive luminance in material_data.w (was 1.0 padding) let emissive_lum = dot(emissive, vec3(0.299, 0.587, 0.114)); out.material_data = vec4(metallic, roughness, ao, emissive_lum); ``` Then in `deferred_lighting.wgsl`, read `material_data.w` as emissive intensity and add `albedo * emissive_lum` to final color. - [ ] **Step 3: Update deferred_lighting.wgsl** ```wgsl let emissive_lum = textureSample(g_material, s_gbuffer, uv).w; // After lighting calculation: color += albedo * emissive_lum; ``` - [ ] **Step 4: Commit** ```bash git add crates/voltex_renderer/src/deferred_gbuffer.wgsl crates/voltex_renderer/src/deferred_lighting.wgsl git commit -m "feat(renderer): add ORM and emissive to deferred rendering pipeline" ``` --- ### Task 4: Update Examples + Pipeline Creation **Files:** - Modify: `crates/voltex_renderer/src/pbr_pipeline.rs` - Modify: `examples/pbr_demo/src/main.rs` - Modify: `examples/ibl_demo/src/main.rs` - Modify: `crates/voltex_renderer/src/deferred_pipeline.rs` - [ ] **Step 1: Update examples to use full texture layout** In each example that uses `pbr_texture_bind_group_layout`: - Switch to `pbr_full_texture_bind_group_layout` - Create default ORM texture (white_1x1 = ao=1, roughness=1, metallic=1 — but material multipliers control actual values) - Create default emissive texture (black_1x1 = no emission) - Pass all 4 texture pairs to `create_pbr_full_texture_bind_group` - [ ] **Step 2: Update deferred_pipeline.rs** Update gbuffer pipeline layout to use the full texture layout. - [ ] **Step 3: Build and test** ```bash cargo build --workspace cargo test --workspace ``` - [ ] **Step 4: Commit** ```bash git commit -m "feat(renderer): update examples and pipelines for full PBR texture maps" ```