veloren_voxygen/render/pipelines/
bloom.rs

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