renderling/bloom/
shader.rs

1use crabslab::{Id, Slab};
2use glam::{Vec2, Vec4, Vec4Swizzles};
3use spirv_std::{image::Image2d, spirv, Sampler};
4
5/// Bloom vertex shader.
6///
7/// This is a pass-through vertex shader to facilitate a bloom effect.
8#[spirv(vertex)]
9pub fn bloom_vertex(
10    #[spirv(vertex_index)] vertex_index: u32,
11    #[spirv(instance_index)] in_id: u32,
12    out_uv: &mut Vec2,
13    #[spirv(flat)] out_id: &mut u32,
14    #[spirv(position)] out_clip_pos: &mut Vec4,
15) {
16    let i = (vertex_index % 6) as usize;
17    *out_uv = crate::math::UV_COORD_QUAD_CCW[i];
18    *out_clip_pos = crate::math::CLIP_SPACE_COORD_QUAD_CCW[i];
19    *out_id = in_id;
20}
21
22/// Bloom downsampling shader.
23///
24/// Performs successive downsampling from a source texture.
25///
26/// As taken from Call Of Duty method - presented at ACM Siggraph 2014.
27///
28/// This particular method was designed to eliminate
29/// "pulsating artifacts and temporal stability issues".
30#[spirv(fragment)]
31pub fn bloom_downsample_fragment(
32    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
33    // Remember to add bilinear minification filter for this texture!
34    // Remember to use a floating-point texture format (for HDR)!
35    // Remember to use edge clamping for this texture!
36    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
37    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
38    in_uv: Vec2,
39    #[spirv(flat)] in_pixel_size_id: Id<Vec2>,
40    // frag_color
41    downsample: &mut Vec4,
42) {
43    use glam::Vec3;
44
45    let Vec2 { x, y } = slab.read(in_pixel_size_id);
46
47    // Take 13 samples around current texel:
48    // a - b - c
49    // - j - k -
50    // d - e - f
51    // - l - m -
52    // g - h - i
53    // === ('e' is the current texel) ===
54    let a = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y + 2.0 * y));
55    let b = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y + 2.0 * y));
56    let c = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y + 2.0 * y));
57
58    let d = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y));
59    let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y));
60    let f = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y));
61
62    let g = texture.sample(*sampler, Vec2::new(in_uv.x - 2.0 * x, in_uv.y - 2.0 * y));
63    let h = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y - 2.0 * y));
64    let i = texture.sample(*sampler, Vec2::new(in_uv.x + 2.0 * x, in_uv.y - 2.0 * y));
65
66    let j = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y));
67    let k = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y));
68    let l = texture.sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y));
69    let m = texture.sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y));
70
71    // Apply weighted distribution:
72    // 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1
73    // a,b,d,e * 0.125
74    // b,c,e,f * 0.125
75    // d,e,g,h * 0.125
76    // e,f,h,i * 0.125
77    // j,k,l,m * 0.5
78    // This shows 5 square areas that are being sampled. But some of them overlap,
79    // so to have an energy preserving downsample we need to make some adjustments.
80    // The weights are the distributed so that the sum of j,k,l,m (e.g.)
81    // contribute 0.5 to the final color output. The code below is written
82    // to effectively yield this sum. We get:
83    // 0.125*5 + 0.03125*4 + 0.0625*4 = 1
84    let f1 = 0.125;
85    let f2 = 0.0625;
86    let f3 = 0.03125;
87    let center = e * f1;
88    let inner = (j + k + l + m) * f1;
89    let outer = (b + d + h + f) * f2;
90    let furthest = (a + c + g + i) * f3;
91    let min = Vec3::splat(f32::EPSILON).extend(1.0);
92    *downsample = (center + inner + outer + furthest).max(min);
93}
94
95/// Bloom upsampling shader.
96///
97/// This shader performs successive upsampling on a source texture.
98///
99/// Taken from Call Of Duty method, presented at ACM Siggraph 2014.
100#[spirv(fragment)]
101pub fn bloom_upsample_fragment(
102    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
103    // Remember to add bilinear minification filter for this texture!
104    // Remember to use a floating-point texture format (for HDR)!
105    // Remember to use edge clamping for this texture!
106    #[spirv(descriptor_set = 0, binding = 1)] texture: &Image2d,
107    #[spirv(descriptor_set = 0, binding = 2)] sampler: &Sampler,
108    in_uv: Vec2,
109    #[spirv(flat)] filter_radius_id: Id<Vec2>,
110    // frag_color
111    upsample: &mut Vec4,
112) {
113    // The filter kernel is applied with a radius, specified in texture
114    // coordinates, so that the radius will vary across mip resolutions.
115    let Vec2 { x, y } = slab.read(filter_radius_id);
116
117    // Take 9 samples around current texel:
118    // a - b - c
119    // d - e - f
120    // g - h - i
121    // === ('e' is the current texel) ===
122    let a = texture
123        .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y + y))
124        .xyz();
125    let b = texture
126        .sample(*sampler, Vec2::new(in_uv.x, in_uv.y + y))
127        .xyz();
128    let c = texture
129        .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y + y))
130        .xyz();
131
132    let d = texture
133        .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y))
134        .xyz();
135    let e = texture.sample(*sampler, Vec2::new(in_uv.x, in_uv.y)).xyz();
136    let f = texture
137        .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y))
138        .xyz();
139
140    let g = texture
141        .sample(*sampler, Vec2::new(in_uv.x - x, in_uv.y - y))
142        .xyz();
143    let h = texture
144        .sample(*sampler, Vec2::new(in_uv.x, in_uv.y - y))
145        .xyz();
146    let i = texture
147        .sample(*sampler, Vec2::new(in_uv.x + x, in_uv.y - y))
148        .xyz();
149
150    // Apply weighted distribution, by using a 3x3 tent filter:
151    //  1   | 1 2 1 |
152    // -- * | 2 4 2 |
153    // 16   | 1 2 1 |
154    let mut sample = e * 4.0;
155    sample += (b + d + f + h) * 2.0;
156    sample += a + c + g + i;
157    sample *= 1.0 / 16.0;
158    *upsample = sample.extend(0.5);
159}
160
161#[spirv(fragment)]
162#[allow(clippy::too_many_arguments)]
163/// Bloom "mix" shader.
164///
165/// This is the final step in applying bloom in which the computed bloom is
166/// mixed with the source texture according to a strength factor.
167pub fn bloom_mix_fragment(
168    #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] slab: &[u32],
169    #[spirv(descriptor_set = 0, binding = 1)] hdr_texture: &Image2d,
170    #[spirv(descriptor_set = 0, binding = 2)] hdr_sampler: &Sampler,
171    #[spirv(descriptor_set = 0, binding = 3)] bloom_texture: &Image2d,
172    #[spirv(descriptor_set = 0, binding = 4)] bloom_sampler: &Sampler,
173    in_uv: Vec2,
174    #[spirv(flat)] in_bloom_strength_id: Id<f32>,
175    frag_color: &mut Vec4,
176) {
177    let bloom_strength = slab.read(in_bloom_strength_id);
178    let hdr = hdr_texture.sample(*hdr_sampler, in_uv).xyz();
179    let bloom = bloom_texture.sample(*bloom_sampler, in_uv).xyz();
180    let color = hdr.lerp(bloom, bloom_strength);
181    *frag_color = color.extend(1.0)
182}