feat(renderer): add HDR + Bloom + ACES tonemap to deferred_demo
Lighting pass now renders to Rgba16Float HDR target. A 5-level bloom mip chain (downsample with bright-extract threshold, then additive upsample) feeds into an ACES tonemap pass that composites the final LDR output to the swapchain surface. All resources resize correctly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,11 @@ use voltex_renderer::{
|
||||
ssgi_gbuffer_bind_group_layout, ssgi_data_bind_group_layout, create_ssgi_pipeline,
|
||||
RtAccel, RtInstance, BlasMeshData, RtShadowResources, RtShadowUniform,
|
||||
rt_shadow_gbuffer_bind_group_layout, rt_shadow_data_bind_group_layout, create_rt_shadow_pipeline,
|
||||
HdrTarget, HDR_FORMAT,
|
||||
BloomResources, BloomUniform, mip_sizes, BLOOM_MIP_COUNT,
|
||||
TonemapUniform,
|
||||
bloom_bind_group_layout, create_bloom_downsample_pipeline, create_bloom_upsample_pipeline,
|
||||
tonemap_bind_group_layout, create_tonemap_pipeline,
|
||||
};
|
||||
use wgpu::util::DeviceExt;
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
@@ -91,6 +96,22 @@ struct AppState {
|
||||
#[allow(dead_code)]
|
||||
vertex_count: u32,
|
||||
|
||||
// HDR + Bloom + Tonemap resources
|
||||
hdr_target: HdrTarget,
|
||||
bloom: BloomResources,
|
||||
bloom_down_pipeline: wgpu::RenderPipeline,
|
||||
bloom_up_pipeline: wgpu::RenderPipeline,
|
||||
bloom_layout: wgpu::BindGroupLayout,
|
||||
bloom_down_bind_groups: Vec<wgpu::BindGroup>,
|
||||
bloom_up_bind_groups: Vec<wgpu::BindGroup>,
|
||||
bloom_threshold_buffer: wgpu::Buffer,
|
||||
bloom_no_threshold_buffer: wgpu::Buffer,
|
||||
linear_sampler: wgpu::Sampler,
|
||||
tonemap_pipeline: wgpu::RenderPipeline,
|
||||
tonemap_layout: wgpu::BindGroupLayout,
|
||||
tonemap_bind_group: wgpu::BindGroup,
|
||||
tonemap_uniform_buffer: wgpu::Buffer,
|
||||
|
||||
// Layouts needed for rebuild on resize
|
||||
gbuffer_layout: wgpu::BindGroupLayout,
|
||||
shadow_layout: wgpu::BindGroupLayout,
|
||||
@@ -429,15 +450,85 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
&rt_shadow_filtering_sampler,
|
||||
);
|
||||
|
||||
// Lighting pipeline
|
||||
// Lighting pipeline — now renders to HDR format instead of surface
|
||||
let lighting_pipeline = create_lighting_pipeline(
|
||||
&gpu.device,
|
||||
gpu.surface_format,
|
||||
HDR_FORMAT,
|
||||
&gbuffer_layout,
|
||||
&lights_layout,
|
||||
&shadow_layout,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// HDR Target
|
||||
// ---------------------------------------------------------------
|
||||
let hdr_target = HdrTarget::new(&gpu.device, gpu.config.width, gpu.config.height);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Bloom resources + pipelines
|
||||
// ---------------------------------------------------------------
|
||||
let bloom = BloomResources::new(&gpu.device, gpu.config.width, gpu.config.height);
|
||||
let bloom_layout_bg = bloom_bind_group_layout(&gpu.device);
|
||||
let bloom_down_pipeline = create_bloom_downsample_pipeline(&gpu.device, &bloom_layout_bg);
|
||||
let bloom_up_pipeline = create_bloom_upsample_pipeline(&gpu.device, &bloom_layout_bg);
|
||||
|
||||
let linear_sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("Bloom Linear 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()
|
||||
});
|
||||
|
||||
// Two bloom uniform buffers: one with threshold, one without
|
||||
let bloom_threshold_uniform = BloomUniform::default(); // threshold=1.0
|
||||
let bloom_threshold_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Bloom Threshold UBO"),
|
||||
contents: bytemuck::bytes_of(&bloom_threshold_uniform),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
let bloom_no_threshold_uniform = BloomUniform {
|
||||
threshold: 0.0,
|
||||
soft_threshold: 0.0,
|
||||
_padding: [0.0; 2],
|
||||
};
|
||||
let bloom_no_threshold_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Bloom No-Threshold UBO"),
|
||||
contents: bytemuck::bytes_of(&bloom_no_threshold_uniform),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
// Create bloom bind groups
|
||||
let bloom_down_bind_groups = create_bloom_down_bind_groups(
|
||||
&gpu.device, &bloom_layout_bg, &hdr_target, &bloom,
|
||||
&linear_sampler, &bloom_threshold_buffer, &bloom_no_threshold_buffer,
|
||||
);
|
||||
let bloom_up_bind_groups = create_bloom_up_bind_groups(
|
||||
&gpu.device, &bloom_layout_bg, &bloom,
|
||||
&linear_sampler, &bloom_no_threshold_buffer,
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Tonemap pipeline + bind group
|
||||
// ---------------------------------------------------------------
|
||||
let tonemap_layout_bg = tonemap_bind_group_layout(&gpu.device);
|
||||
let tonemap_pipeline = create_tonemap_pipeline(&gpu.device, gpu.surface_format, &tonemap_layout_bg);
|
||||
|
||||
let tonemap_uniform = TonemapUniform::default();
|
||||
let tonemap_uniform_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("Tonemap Uniform Buffer"),
|
||||
contents: bytemuck::bytes_of(&tonemap_uniform),
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
});
|
||||
|
||||
let tonemap_bind_group = create_tonemap_bind_group(
|
||||
&gpu.device, &tonemap_layout_bg, &hdr_target, &bloom,
|
||||
&linear_sampler, &tonemap_uniform_buffer,
|
||||
);
|
||||
|
||||
self.state = Some(AppState {
|
||||
window,
|
||||
gpu,
|
||||
@@ -472,6 +563,20 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
shadow_bind_group,
|
||||
light_buffer,
|
||||
cam_pos_buffer,
|
||||
hdr_target,
|
||||
bloom,
|
||||
bloom_down_pipeline,
|
||||
bloom_up_pipeline,
|
||||
bloom_layout: bloom_layout_bg,
|
||||
bloom_down_bind_groups,
|
||||
bloom_up_bind_groups,
|
||||
bloom_threshold_buffer,
|
||||
bloom_no_threshold_buffer,
|
||||
linear_sampler,
|
||||
tonemap_pipeline,
|
||||
tonemap_layout: tonemap_layout_bg,
|
||||
tonemap_bind_group,
|
||||
tonemap_uniform_buffer,
|
||||
gbuffer_layout,
|
||||
shadow_layout,
|
||||
_albedo_tex: albedo_tex,
|
||||
@@ -612,6 +717,28 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
&state.rt_shadow,
|
||||
&rt_shadow_filtering_sampler,
|
||||
);
|
||||
|
||||
// Resize HDR target
|
||||
state.hdr_target.resize(&state.gpu.device, size.width, size.height);
|
||||
|
||||
// Resize bloom mip chain
|
||||
state.bloom.resize(&state.gpu.device, size.width, size.height);
|
||||
|
||||
// Recreate bloom bind groups
|
||||
state.bloom_down_bind_groups = create_bloom_down_bind_groups(
|
||||
&state.gpu.device, &state.bloom_layout, &state.hdr_target, &state.bloom,
|
||||
&state.linear_sampler, &state.bloom_threshold_buffer, &state.bloom_no_threshold_buffer,
|
||||
);
|
||||
state.bloom_up_bind_groups = create_bloom_up_bind_groups(
|
||||
&state.gpu.device, &state.bloom_layout, &state.bloom,
|
||||
&state.linear_sampler, &state.bloom_no_threshold_buffer,
|
||||
);
|
||||
|
||||
// Recreate tonemap bind group
|
||||
state.tonemap_bind_group = create_tonemap_bind_group(
|
||||
&state.gpu.device, &state.tonemap_layout, &state.hdr_target, &state.bloom,
|
||||
&state.linear_sampler, &state.tonemap_uniform_buffer,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,12 +1056,12 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
);
|
||||
}
|
||||
|
||||
// ---- Pass 4: Lighting (fullscreen) ----
|
||||
// ---- Pass 4: Lighting → HDR target ----
|
||||
{
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Lighting Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
view: &state.hdr_target.view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
@@ -961,6 +1088,90 @@ impl ApplicationHandler for DeferredDemoApp {
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
// ---- Pass 5: Bloom downsample chain ----
|
||||
{
|
||||
let ms = mip_sizes(state.gpu.config.width, state.gpu.config.height);
|
||||
for i in 0..BLOOM_MIP_COUNT {
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Bloom Downsample"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &state.bloom.mip_views[i],
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
let (mw, mh) = ms[i];
|
||||
rpass.set_viewport(0.0, 0.0, mw as f32, mh as f32, 0.0, 1.0);
|
||||
rpass.set_pipeline(&state.bloom_down_pipeline);
|
||||
rpass.set_bind_group(0, &state.bloom_down_bind_groups[i], &[]);
|
||||
rpass.set_vertex_buffer(0, state.fullscreen_vb.slice(..));
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Pass 6: Bloom upsample chain ----
|
||||
{
|
||||
let ms = mip_sizes(state.gpu.config.width, state.gpu.config.height);
|
||||
for j in 0..BLOOM_MIP_COUNT - 1 {
|
||||
let target_mip = BLOOM_MIP_COUNT - 2 - j; // 3, 2, 1, 0
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Bloom Upsample"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &state.bloom.mip_views[target_mip],
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Load,
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
let (mw, mh) = ms[target_mip];
|
||||
rpass.set_viewport(0.0, 0.0, mw as f32, mh as f32, 0.0, 1.0);
|
||||
rpass.set_pipeline(&state.bloom_up_pipeline);
|
||||
rpass.set_bind_group(0, &state.bloom_up_bind_groups[j], &[]);
|
||||
rpass.set_vertex_buffer(0, state.fullscreen_vb.slice(..));
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Pass 7: Tonemap → surface ----
|
||||
{
|
||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Tonemap Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
depth_slice: None,
|
||||
ops: wgpu::Operations {
|
||||
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||
store: wgpu::StoreOp::Store,
|
||||
},
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
occlusion_query_set: None,
|
||||
timestamp_writes: None,
|
||||
multiview_mask: None,
|
||||
});
|
||||
|
||||
rpass.set_pipeline(&state.tonemap_pipeline);
|
||||
rpass.set_bind_group(0, &state.tonemap_bind_group, &[]);
|
||||
rpass.set_vertex_buffer(0, state.fullscreen_vb.slice(..));
|
||||
rpass.draw(0..3, 0..1);
|
||||
}
|
||||
|
||||
state.gpu.queue.submit(std::iter::once(encoder.finish()));
|
||||
output.present();
|
||||
}
|
||||
@@ -1147,6 +1358,112 @@ fn create_rt_shadow_data_bg(
|
||||
})
|
||||
}
|
||||
|
||||
/// Helper: create bloom downsample bind groups (5 total).
|
||||
/// Pass 0 reads from hdr_target, uses threshold buffer.
|
||||
/// Passes 1-4 read from the previous mip, use no-threshold buffer.
|
||||
fn create_bloom_down_bind_groups(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
hdr_target: &HdrTarget,
|
||||
bloom: &BloomResources,
|
||||
sampler: &wgpu::Sampler,
|
||||
threshold_buf: &wgpu::Buffer,
|
||||
no_threshold_buf: &wgpu::Buffer,
|
||||
) -> Vec<wgpu::BindGroup> {
|
||||
(0..BLOOM_MIP_COUNT)
|
||||
.map(|i| {
|
||||
let input_view = if i == 0 { &hdr_target.view } else { &bloom.mip_views[i - 1] };
|
||||
let uniform_buf = if i == 0 { threshold_buf } else { no_threshold_buf };
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some(&format!("Bloom Down BG {}", i)),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(input_view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: uniform_buf.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Helper: create bloom upsample bind groups (4 total).
|
||||
/// Each reads from mip[i+1] and renders additively into mip[i].
|
||||
/// Order: j=0 → input=mip[4] target=mip[3], j=1 → input=mip[3] target=mip[2], etc.
|
||||
fn create_bloom_up_bind_groups(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
bloom: &BloomResources,
|
||||
sampler: &wgpu::Sampler,
|
||||
no_threshold_buf: &wgpu::Buffer,
|
||||
) -> Vec<wgpu::BindGroup> {
|
||||
(0..BLOOM_MIP_COUNT - 1)
|
||||
.map(|j| {
|
||||
let source_mip = BLOOM_MIP_COUNT - 1 - j; // 4, 3, 2, 1
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some(&format!("Bloom Up BG {}", j)),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&bloom.mip_views[source_mip]),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: no_threshold_buf.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Helper: create the tonemap bind group.
|
||||
fn create_tonemap_bind_group(
|
||||
device: &wgpu::Device,
|
||||
layout: &wgpu::BindGroupLayout,
|
||||
hdr_target: &HdrTarget,
|
||||
bloom: &BloomResources,
|
||||
sampler: &wgpu::Sampler,
|
||||
uniform_buffer: &wgpu::Buffer,
|
||||
) -> wgpu::BindGroup {
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("Tonemap Bind Group"),
|
||||
layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: wgpu::BindingResource::TextureView(&hdr_target.view),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: wgpu::BindingResource::TextureView(&bloom.mip_views[0]),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: wgpu::BindingResource::Sampler(sampler),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: uniform_buffer.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
/// 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