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: "main",
171                    buffers: &[],
172                },
173                primitive: wgpu::PrimitiveState {
174                    topology: wgpu::PrimitiveTopology::TriangleList,
175                    strip_index_format: None,
176                    front_face: wgpu::FrontFace::Ccw,
177                    cull_mode: None,
178                    unclipped_depth: false,
179                    polygon_mode: wgpu::PolygonMode::Fill,
180                    conservative: false,
181                },
182                depth_stencil: None,
183                multisample: wgpu::MultisampleState {
184                    count: 1,
185                    mask: !0,
186                    alpha_to_coverage_enabled: false,
187                },
188                fragment: Some(wgpu::FragmentState {
189                    module: fs_module,
190                    entry_point: "main",
191                    targets: &[Some(wgpu::ColorTargetState {
192                        format: target_format,
193                        blend,
194                        write_mask: wgpu::ColorWrites::ALL,
195                    })],
196                }),
197                multiview: None,
198            })
199        };
200
201        let downsample_filtered_pipeline = create_pipeline(
202            "Bloom downsample filtered pipeline",
203            downsample_filtered_fs_module,
204            None,
205        );
206        let downsample_pipeline =
207            create_pipeline("Bloom downsample pipeline", downsample_fs_module, None);
208        let upsample_pipeline = create_pipeline(
209            "Bloom upsample pipeline",
210            upsample_fs_module,
211            (!bloom_config.uniform_blur).then_some(wgpu::BlendState {
212                color: wgpu::BlendComponent {
213                    src_factor: wgpu::BlendFactor::One,
214                    dst_factor: wgpu::BlendFactor::One,
215                    operation: wgpu::BlendOperation::Add,
216                },
217                // We don't really use this but we need something here..
218                alpha: wgpu::BlendComponent::REPLACE,
219            }),
220        );
221
222        Self {
223            downsample_filtered: downsample_filtered_pipeline,
224            downsample: downsample_pipeline,
225            upsample: upsample_pipeline,
226        }
227    }
228}