feat(renderer): add SSGI pass to deferred_demo (AO + color bleeding)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,9 @@ use voltex_renderer::{
|
||||
lighting_gbuffer_bind_group_layout, lighting_lights_bind_group_layout,
|
||||
lighting_shadow_bind_group_layout,
|
||||
pbr_texture_bind_group_layout, create_pbr_texture_bind_group,
|
||||
SsgiResources, SsgiUniform,
|
||||
ssgi_gbuffer_bind_group_layout, ssgi_data_bind_group_layout, create_ssgi_pipeline,
|
||||
|
||||
};
|
||||
use wgpu::util::DeviceExt;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
@@ -58,6 +61,15 @@ struct AppState {
|
||||
pbr_texture_bind_group: wgpu::BindGroup,
|
||||
material_bind_group: wgpu::BindGroup,
|
||||
|
||||
// SSGI pass resources
|
||||
ssgi: SsgiResources,
|
||||
ssgi_pipeline: wgpu::RenderPipeline,
|
||||
ssgi_gbuffer_bind_group: wgpu::BindGroup,
|
||||
ssgi_data_bind_group: wgpu::BindGroup,
|
||||
ssgi_gb_layout: wgpu::BindGroupLayout,
|
||||
#[allow(dead_code)]
|
||||
ssgi_data_layout: wgpu::BindGroupLayout,
|
||||
|
||||
// Lighting pass resources
|
||||
lighting_pipeline: wgpu::RenderPipeline,
|
||||
fullscreen_vb: wgpu::Buffer,
|
||||
@@ -69,12 +81,14 @@ struct AppState {
|
||||
|
||||
// Layouts needed for rebuild on resize
|
||||
gbuffer_layout: wgpu::BindGroupLayout,
|
||||
shadow_layout: wgpu::BindGroupLayout,
|
||||
|
||||
// Keep textures alive
|
||||
_albedo_tex: GpuTexture,
|
||||
_normal_tex: (wgpu::Texture, wgpu::TextureView, wgpu::Sampler),
|
||||
_shadow_map: ShadowMap,
|
||||
_ibl: IblResources,
|
||||
_shadow_uniform_buffer: wgpu::Buffer,
|
||||
|
||||
input: InputState,
|
||||
timer: GameTimer,
|
||||
@@ -183,6 +197,56 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
&mat_layout,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// SSGI pass resources
|
||||
// ---------------------------------------------------------------
|
||||
let ssgi = SsgiResources::new(&gpu.device, &gpu.queue, gpu.config.width, gpu.config.height);
|
||||
let ssgi_gb_layout = ssgi_gbuffer_bind_group_layout(&gpu.device);
|
||||
let ssgi_data_layout = ssgi_data_bind_group_layout(&gpu.device);
|
||||
let ssgi_pipeline = create_ssgi_pipeline(&gpu.device, &ssgi_gb_layout, &ssgi_data_layout);
|
||||
|
||||
// SSGI G-Buffer bind group (group 0): position, normal, albedo, non-filtering sampler
|
||||
let ssgi_nearest_sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("SSGI GBuffer Nearest Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Nearest,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
let ssgi_gbuffer_bind_group = create_ssgi_gbuffer_bind_group(
|
||||
&gpu.device,
|
||||
&ssgi_gb_layout,
|
||||
&gbuffer,
|
||||
&ssgi_nearest_sampler,
|
||||
);
|
||||
|
||||
// SSGI data bind group (group 1): uniform, kernel, noise texture, noise sampler
|
||||
let ssgi_data_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("SSGI Data Bind Group"),
|
||||
layout: &ssgi_data_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: ssgi.uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: ssgi.kernel_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::TextureView(&ssgi.noise_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::Sampler(&ssgi.noise_sampler),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Lighting pass bind group layouts
|
||||
// ---------------------------------------------------------------
|
||||
@@ -243,7 +307,7 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
],
|
||||
});
|
||||
|
||||
// Shadow + IBL bind group (group 2)
|
||||
// Shadow + IBL + SSGI bind group (group 2) — now 7 entries
|
||||
let shadow_map = ShadowMap::new(&gpu.device);
|
||||
let ibl = IblResources::new(&gpu.device, &gpu.queue);
|
||||
let shadow_uniform = ShadowUniform {
|
||||
@@ -257,33 +321,28 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
contents: bytemuck::cast_slice(&[shadow_uniform]),
|
||||
usage: wgpu::BufferUsages::UNIFORM,
|
||||
});
|
||||
let shadow_bind_group = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Lighting Shadow Bind Group"),
|
||||
layout: &shadow_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&shadow_map.view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&shadow_map.sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: shadow_uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::TextureView(&ibl.brdf_lut_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: wgpu::BindingResource::Sampler(&ibl.brdf_lut_sampler),
|
||||
},
|
||||
],
|
||||
|
||||
let ssgi_filtering_sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("SSGI Filtering Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let shadow_bind_group = create_shadow_bind_group(
|
||||
&gpu.device,
|
||||
&shadow_layout,
|
||||
&shadow_map,
|
||||
&shadow_uniform_buffer,
|
||||
&ibl,
|
||||
&ssgi,
|
||||
&ssgi_filtering_sampler,
|
||||
);
|
||||
|
||||
// Lighting pipeline
|
||||
let lighting_pipeline = create_lighting_pipeline(
|
||||
&gpu.device,
|
||||
@@ -306,6 +365,12 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
camera_bind_group,
|
||||
pbr_texture_bind_group,
|
||||
material_bind_group,
|
||||
ssgi,
|
||||
ssgi_pipeline,
|
||||
ssgi_gbuffer_bind_group,
|
||||
ssgi_data_bind_group,
|
||||
ssgi_gb_layout,
|
||||
ssgi_data_layout,
|
||||
lighting_pipeline,
|
||||
fullscreen_vb,
|
||||
gbuffer_bind_group,
|
||||
@@ -314,10 +379,12 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
light_buffer,
|
||||
cam_pos_buffer,
|
||||
gbuffer_layout,
|
||||
shadow_layout,
|
||||
_albedo_tex: albedo_tex,
|
||||
_normal_tex: normal_tex,
|
||||
_shadow_map: shadow_map,
|
||||
_ibl: ibl,
|
||||
_shadow_uniform_buffer: shadow_uniform_buffer,
|
||||
input: InputState::new(),
|
||||
timer: GameTimer::new(60),
|
||||
cam_aligned_size,
|
||||
@@ -360,8 +427,13 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
state.gpu.resize(size.width, size.height);
|
||||
if size.width > 0 && size.height > 0 {
|
||||
state.camera.aspect = size.width as f32 / size.height as f32;
|
||||
|
||||
// Resize G-Buffer
|
||||
state.gbuffer.resize(&state.gpu.device, size.width, size.height);
|
||||
|
||||
// Resize SSGI output texture
|
||||
state.ssgi.resize(&state.gpu.device, size.width, size.height);
|
||||
|
||||
// Recreate gbuffer bind group with new texture views
|
||||
let nearest_sampler = state.gpu.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("GBuffer Nearest Sampler"),
|
||||
@@ -379,6 +451,45 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
&state.gbuffer,
|
||||
&nearest_sampler,
|
||||
);
|
||||
|
||||
// Recreate SSGI G-Buffer bind group (gbuffer views changed)
|
||||
let ssgi_nearest_sampler = state.gpu.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("SSGI GBuffer Nearest Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Nearest,
|
||||
min_filter: wgpu::FilterMode::Nearest,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
state.ssgi_gbuffer_bind_group = create_ssgi_gbuffer_bind_group(
|
||||
&state.gpu.device,
|
||||
&state.ssgi_gb_layout,
|
||||
&state.gbuffer,
|
||||
&ssgi_nearest_sampler,
|
||||
);
|
||||
|
||||
// Recreate shadow bind group (ssgi output_view changed)
|
||||
let ssgi_filtering_sampler = state.gpu.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("SSGI Filtering Sampler"),
|
||||
address_mode_u: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_v: wgpu::AddressMode::ClampToEdge,
|
||||
address_mode_w: wgpu::AddressMode::ClampToEdge,
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
min_filter: wgpu::FilterMode::Linear,
|
||||
mipmap_filter: wgpu::MipmapFilterMode::Nearest,
|
||||
..Default::default()
|
||||
});
|
||||
state.shadow_bind_group = create_shadow_bind_group(
|
||||
&state.gpu.device,
|
||||
&state.shadow_layout,
|
||||
&state._shadow_map,
|
||||
&state._shadow_uniform_buffer,
|
||||
&state._ibl,
|
||||
&state.ssgi,
|
||||
&ssgi_filtering_sampler,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,6 +635,20 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
bytemuck::cast_slice(&[cam_pos_uniform]),
|
||||
);
|
||||
|
||||
// Update SSGI uniform with current camera matrices
|
||||
let proj_mat = state.camera.projection_matrix();
|
||||
let view_mat = state.camera.view_matrix();
|
||||
let ssgi_uniform = SsgiUniform {
|
||||
projection: *proj_mat.as_slice(),
|
||||
view: *view_mat.as_slice(),
|
||||
..SsgiUniform::default()
|
||||
};
|
||||
state.gpu.queue.write_buffer(
|
||||
&state.ssgi.uniform_buffer,
|
||||
0,
|
||||
bytemuck::bytes_of(&ssgi_uniform),
|
||||
);
|
||||
|
||||
// Render
|
||||
let output = match state.gpu.surface.get_current_texture() {
|
||||
Ok(t) => t,
|
||||
@@ -620,7 +745,38 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Pass 2: Lighting (fullscreen) ----
|
||||
// ---- Pass 2: SSGI ----
|
||||
{
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("SSGI Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &state.ssgi.output_view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||
r: 1.0,
|
||||
g: 1.0,
|
||||
b: 1.0,
|
||||
a: 1.0,
|
||||
}),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
rpass.set_pipeline(&state.ssgi_pipeline);
|
||||
rpass.set_bind_group(0, &state.ssgi_gbuffer_bind_group, &[]);
|
||||
rpass.set_bind_group(1, &state.ssgi_data_bind_group, &[]);
|
||||
rpass.set_vertex_buffer(0, state.fullscreen_vb.slice(..));
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
// ---- Pass 3: Lighting (fullscreen) ----
|
||||
{
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Lighting Pass"),
|
||||
@@ -702,6 +858,83 @@ fn create_gbuffer_bind_group(
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper: create the SSGI pass G-Buffer bind group.
|
||||
fn create_ssgi_gbuffer_bind_group(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
gbuffer: &GBuffer,
|
||||
sampler: &wgpu::Sampler,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("SSGI GBuffer Bind Group"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&gbuffer.position_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&gbuffer.normal_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::TextureView(&gbuffer.albedo_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper: create the shadow + IBL + SSGI bind group (7 entries).
|
||||
fn create_shadow_bind_group(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
shadow_map: &ShadowMap,
|
||||
shadow_uniform_buffer: &wgpu::Buffer,
|
||||
ibl: &IblResources,
|
||||
ssgi: &SsgiResources,
|
||||
ssgi_sampler: &wgpu::Sampler,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Lighting Shadow+IBL+SSGI Bind Group"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&shadow_map.view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(&shadow_map.sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: shadow_uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: wgpu::BindingResource::TextureView(&ibl.brdf_lut_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 4,
|
||||
resource: wgpu::BindingResource::Sampler(&ibl.brdf_lut_sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 5,
|
||||
resource: wgpu::BindingResource::TextureView(&ssgi.output_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 6,
|
||||
resource: wgpu::BindingResource::Sampler(ssgi_sampler),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert HSV hue (0..1) to RGB.
|
||||
fn hue_to_rgb(h: f32) -> (f32, f32, f32) {
|
||||
let h6 = h * 6.0;
|
||||
|
||||
Reference in New Issue
Block a user