diff --git a/crates/voltex_renderer/src/deferred_lighting.wgsl b/crates/voltex_renderer/src/deferred_lighting.wgsl index 602fb2e..5ba4f44 100644 --- a/crates/voltex_renderer/src/deferred_lighting.wgsl +++ b/crates/voltex_renderer/src/deferred_lighting.wgsl @@ -303,13 +303,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4 { let ssgi_indirect = ssgi_data.gba; let ambient = (diffuse_ibl + specular_ibl) * ao * ssgi_ao + ssgi_indirect; - var color = ambient + Lo; - - // Reinhard tone mapping - color = color / (color + vec3(1.0)); - - // Gamma correction - color = pow(color, vec3(1.0 / 2.2)); + // Output raw HDR linear colour; tonemap is applied in a separate tonemap pass. + let color = ambient + Lo; return vec4(color, alpha); } diff --git a/crates/voltex_renderer/src/deferred_pipeline.rs b/crates/voltex_renderer/src/deferred_pipeline.rs index c04cc14..bac182a 100644 --- a/crates/voltex_renderer/src/deferred_pipeline.rs +++ b/crates/voltex_renderer/src/deferred_pipeline.rs @@ -7,6 +7,9 @@ use crate::gbuffer::{ use crate::light::CameraUniform; use crate::ssgi::{SsgiUniform, SSGI_OUTPUT_FORMAT}; use crate::rt_shadow::{RtShadowUniform, RT_SHADOW_FORMAT}; +use crate::hdr::HDR_FORMAT; +use crate::bloom::BloomUniform; +use crate::tonemap::TonemapUniform; /// Bind group layout for the G-Buffer pass camera uniform (dynamic offset, group 0). pub fn gbuffer_camera_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { @@ -616,3 +619,287 @@ pub fn create_rt_shadow_pipeline( cache: None, }) } + +// ── Bloom pipelines ─────────────────────────────────────────────────────────── + +/// Bind group layout shared by both bloom pipelines (group 0): +/// binding 0 — input texture (filterable) +/// binding 1 — filtering sampler +/// binding 2 — BloomUniform buffer +pub fn bloom_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Bloom Bind Group Layout"), + entries: &[ + // binding 0: input HDR texture (filterable) + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + // binding 1: filtering sampler + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // binding 2: BloomUniform buffer + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }, + count: None, + }, + ], + }) +} + +/// Create the bloom downsample render pipeline. +/// Entry point: `fs_downsample`. No blending — overwrites the mip target. +pub fn create_bloom_downsample_pipeline( + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, +) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Bloom Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Bloom Downsample Pipeline Layout"), + bind_group_layouts: &[layout], + immediate_size: 0, + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Bloom Downsample Pipeline"), + layout: Some(&pipeline_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_downsample"), + targets: &[Some(wgpu::ColorTargetState { + format: HDR_FORMAT, + blend: None, + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }) +} + +/// Create the bloom upsample render pipeline. +/// Entry point: `fs_upsample`. Additive blending (One + One) to accumulate mips. +pub fn create_bloom_upsample_pipeline( + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, +) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Bloom Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("bloom_shader.wgsl").into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Bloom Upsample Pipeline Layout"), + bind_group_layouts: &[layout], + immediate_size: 0, + }); + + let additive = wgpu::BlendState { + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + alpha: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::One, + dst_factor: wgpu::BlendFactor::One, + operation: wgpu::BlendOperation::Add, + }, + }; + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Bloom Upsample Pipeline"), + layout: Some(&pipeline_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_upsample"), + targets: &[Some(wgpu::ColorTargetState { + format: HDR_FORMAT, + blend: Some(additive), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }) +} + +// ── Tonemap pipeline ────────────────────────────────────────────────────────── + +/// Bind group layout for the tonemap pass (group 0): +/// binding 0 — HDR texture (filterable) +/// binding 1 — bloom texture (filterable) +/// binding 2 — filtering sampler +/// binding 3 — TonemapUniform buffer +pub fn tonemap_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Tonemap Bind Group Layout"), + entries: &[ + // binding 0: HDR texture (filterable) + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + count: None, + }, + // binding 1: bloom texture (filterable) + 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, + }, + // binding 2: filtering sampler + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // binding 3: TonemapUniform buffer + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new( + std::mem::size_of::() as u64, + ), + }, + count: None, + }, + ], + }) +} + +/// Create the tonemap render pipeline. +/// Writes to the surface swapchain format. +pub fn create_tonemap_pipeline( + device: &wgpu::Device, + surface_format: wgpu::TextureFormat, + layout: &wgpu::BindGroupLayout, +) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Tonemap Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("tonemap_shader.wgsl").into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Tonemap Pipeline Layout"), + bind_group_layouts: &[layout], + immediate_size: 0, + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Tonemap Pipeline"), + layout: Some(&pipeline_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, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }) +} diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index 3e78c31..f398e58 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -42,6 +42,8 @@ pub use deferred_pipeline::{ lighting_gbuffer_bind_group_layout, lighting_lights_bind_group_layout, lighting_shadow_bind_group_layout, ssgi_gbuffer_bind_group_layout, ssgi_data_bind_group_layout, create_ssgi_pipeline, rt_shadow_gbuffer_bind_group_layout, rt_shadow_data_bind_group_layout, create_rt_shadow_pipeline, + bloom_bind_group_layout, create_bloom_downsample_pipeline, create_bloom_upsample_pipeline, + tonemap_bind_group_layout, create_tonemap_pipeline, }; pub use ssgi::{SsgiResources, SsgiUniform, SSGI_OUTPUT_FORMAT}; pub use rt_accel::{RtAccel, RtInstance, BlasMeshData, mat4_to_tlas_transform};