1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
//! Based on: https://community.arm.com/cfs-file/__key/communityserver-blogs-components-weblogfiles/00-00-00-20-66/siggraph2015_2D00_mmg_2D00_marius_2D00_notes.pdf
//!
//! See additional details in the [NUM_SIZES] docs
use super::super::{BloomConfig, Consts};
use bytemuck::{Pod, Zeroable};
use vek::*;
// TODO: auto-tune the number of passes to maintain roughly constant blur per
// unit of FOV so changing resolution / FOV doesn't change the blur appearance
// significantly.
//
/// Blurring is performed while downsampling to the smaller sizes in steps and
/// then upsampling back up to the original resolution. Each level is half the
/// size in both dimensions from the previous. For instance with 5 distinct
/// sizes there is a total of 8 passes going from the largest to the smallest to
/// the largest again:
///
/// 1 -> 1/2 -> 1/4 -> 1/8 -> 1/16 -> 1/8 -> 1/4 -> 1/2 -> 1
/// ~~~~
/// [downsampling] smallest [upsampling]
///
/// The textures used for downsampling are re-used when upsampling.
///
/// Additionally, instead of clearing them the colors are added together in an
/// attempt to obtain a more concentrated bloom near bright areas rather than
/// a uniform blur. In the example above, the added layers would include 1/8,
/// 1/4, and 1/2. The smallest size is not upsampled to and the original full
/// resolution has no blurring and we are already combining the bloom into the
/// full resolution image in a later step, so they are not included here. The 3
/// extra layers added in mean the total luminosity of the final blurred bloom
/// image will be 4 times more than the input image. To account for this, we
/// divide the bloom intensity by 4 before applying it.
///
/// Nevertheless, we have not fully evaluated how this visually compares to the
/// bloom obtained without adding with the previous layers so there is the
/// potential for further artistic investigation here.
///
/// NOTE: This constant includes the full resolution size and it is
/// assumed that there will be at least one smaller image to downsample to and
/// upsample back from (otherwise no blurring would be done). Thus, the minimum
/// valid value is 2 and panicking indexing operations we perform assume this
/// will be at least 2.
pub const NUM_SIZES: usize = 5;
pub struct BindGroup {
pub(in super::super) bind_group: wgpu::BindGroup,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Zeroable, Pod)]
pub struct Locals {
halfpixel: [f32; 2],
}
impl Locals {
pub fn new(source_texture_resolution: Vec2<f32>) -> Self {
Self {
halfpixel: source_texture_resolution.map(|e| 0.5 / e).into_array(),
}
}
}
pub struct BloomLayout {
pub layout: wgpu::BindGroupLayout,
}
impl BloomLayout {
pub fn new(device: &wgpu::Device) -> Self {
Self {
layout: device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
// Color source
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,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
// halfpixel
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,
},
],
}),
}
}
pub fn bind(
&self,
device: &wgpu::Device,
src_color: &wgpu::TextureView,
sampler: &wgpu::Sampler,
half_pixel: Consts<Locals>,
) -> BindGroup {
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &self.layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: wgpu::BindingResource::TextureView(src_color),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::Sampler(sampler),
},
wgpu::BindGroupEntry {
binding: 2,
resource: half_pixel.buf().as_entire_binding(),
},
],
});
BindGroup { bind_group }
}
}
pub struct BloomPipelines {
pub downsample_filtered: wgpu::RenderPipeline,
pub downsample: wgpu::RenderPipeline,
pub upsample: wgpu::RenderPipeline,
}
impl BloomPipelines {
pub fn new(
device: &wgpu::Device,
vs_module: &wgpu::ShaderModule,
downsample_filtered_fs_module: &wgpu::ShaderModule,
downsample_fs_module: &wgpu::ShaderModule,
upsample_fs_module: &wgpu::ShaderModule,
target_format: wgpu::TextureFormat,
layout: &BloomLayout,
bloom_config: &BloomConfig,
) -> Self {
common_base::span!(_guard, "BloomPipelines::new");
let render_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Bloom pipelines layout"),
push_constant_ranges: &[],
bind_group_layouts: &[&layout.layout],
});
let create_pipeline = |label, fs_module, blend| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some(label),
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: vs_module,
entry_point: "main",
buffers: &[],
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
unclipped_depth: false,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: fs_module,
entry_point: "main",
targets: &[Some(wgpu::ColorTargetState {
format: target_format,
blend,
write_mask: wgpu::ColorWrites::ALL,
})],
}),
multiview: None,
})
};
let downsample_filtered_pipeline = create_pipeline(
"Bloom downsample filtered pipeline",
downsample_filtered_fs_module,
None,
);
let downsample_pipeline =
create_pipeline("Bloom downsample pipeline", downsample_fs_module, None);
let upsample_pipeline = create_pipeline(
"Bloom upsample pipeline",
upsample_fs_module,
(!bloom_config.uniform_blur).then_some(wgpu::BlendState {
color: wgpu::BlendComponent {
src_factor: wgpu::BlendFactor::One,
dst_factor: wgpu::BlendFactor::One,
operation: wgpu::BlendOperation::Add,
},
// We don't really use this but we need something here..
alpha: wgpu::BlendComponent::REPLACE,
}),
);
Self {
downsample_filtered: downsample_filtered_pipeline,
downsample: downsample_pipeline,
upsample: upsample_pipeline,
}
}
}