Search across packages, versions, files, declarations, and comments. Use multiple terms to narrow results (AND), and add @vertex, @fragment, or @compute to filter by shader stage.
1//! Bloom works by creating an intermediate texture with a bunch of mip levels, each half the size of the previous.
2//! You then downsample each mip (starting with the original texture) to the lower resolution mip under it, going in order.
3//! You then upsample each mip (starting from the smallest mip) and blend with the higher resolution mip above it (ending on the original texture).
4//!
5//! References:
6//! * [COD] - Next Generation Post Processing in Call of Duty - http://www.iryoku.com/next-generation-post-processing-in-call-of-duty-advanced-warfare
7//! * [PBB] - Physically Based Bloom - https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom
9struct BloomUniforms {
10 threshold_precomputations: vec4<f32>,
11 viewport: vec4<f32>,
12 scale: vec2<f32>,
13 aspect: f32,
14};
16@group(0) @binding(0) var input_texture: texture_2d<f32>;
17@group(0) @binding(1) var s: sampler;
19@group(0) @binding(2) var<uniform> uniforms: BloomUniforms;
21@if(FIRST_DOWNSAMPLE)
22/// https://catlikecoding.com/unity/tutorials/advanced-rendering/bloom/#3.4
23fn soft_threshold(color: vec3<f32>) -> vec3<f32> {
24 let brightness = max(color.r, max(color.g, color.b));
25 var softness = brightness - uniforms.threshold_precomputations.y;
26 softness = clamp(softness, 0.0, uniforms.threshold_precomputations.z);
27 softness = softness * softness * uniforms.threshold_precomputations.w;
28 var contribution = max(brightness - uniforms.threshold_precomputations.x, softness);
29 contribution /= max(brightness, 0.00001); // Prevent division by 0
30 return color * contribution;
33/// luminance coefficients from Rec. 709.
34/// https://en.wikipedia.org/wiki/Rec._709
35fn tonemapping_luminance(v: vec3<f32>) -> f32 {
36 return dot(v, vec3<f32>(0.2126, 0.7152, 0.0722));
39/// http://graphicrants.blogspot.com/2013/12/tone-mapping.html
40fn karis_average(color: vec3<f32>) -> f32 {
41 // Luminance calculated based on Rec. 709 color primaries.
42 // This must be done in *linear* color space.
43 let luma = tonemapping_luminance(color) / 4.0;
44 return 1.0 / (1.0 + luma);
47/// [COD] slide 153
48fn sample_input_13_tap(uv: vec2<f32>) -> vec3<f32> {
49 var a: vec3<f32>;
50 var b: vec3<f32>;
51 var c: vec3<f32>;
52 var d: vec3<f32>;
53 var e: vec3<f32>;
54 var f: vec3<f32>;
55 var g: vec3<f32>;
56 var h: vec3<f32>;
57 var i: vec3<f32>;
58 var j: vec3<f32>;
59 var k: vec3<f32>;
60 var l: vec3<f32>;
61 var m: vec3<f32>;
62@if(UNIFORM_SCALE) {
63 // This is the fast path. When the bloom scale is uniform, the 13 tap sampling kernel can be
64 // expressed with constant offsets.
65 //
66 // It's possible that this isn't meaningfully faster than the "slow" path. However, because it
67 // is hard to test performance on all platforms, and uniform bloom is the most common case, this
68 // path was retained when adding non-uniform (anamorphic) bloom. This adds a small, but nonzero,
69 // cost to maintainability, but it does help me sleep at night.
70 a = textureSample(input_texture, s, uv, vec2<i32>(-2, 2)).rgb;
71 b = textureSample(input_texture, s, uv, vec2<i32>(0, 2)).rgb;
72 c = textureSample(input_texture, s, uv, vec2<i32>(2, 2)).rgb;
73 d = textureSample(input_texture, s, uv, vec2<i32>(-2, 0)).rgb;
74 e = textureSample(input_texture, s, uv).rgb;
75 f = textureSample(input_texture, s, uv, vec2<i32>(2, 0)).rgb;
76 g = textureSample(input_texture, s, uv, vec2<i32>(-2, -2)).rgb;
77 h = textureSample(input_texture, s, uv, vec2<i32>(0, -2)).rgb;
78 i = textureSample(input_texture, s, uv, vec2<i32>(2, -2)).rgb;
79 j = textureSample(input_texture, s, uv, vec2<i32>(-1, 1)).rgb;
80 k = textureSample(input_texture, s, uv, vec2<i32>(1, 1)).rgb;
81 l = textureSample(input_texture, s, uv, vec2<i32>(-1, -1)).rgb;
82 m = textureSample(input_texture, s, uv, vec2<i32>(1, -1)).rgb;
83} @else {
84 // This is the flexible, but potentially slower, path for non-uniform sampling. Because the
85 // sample is not a constant, and it can fall outside of the limits imposed on constant sample
86 // offsets (-8..8), we have to compute the pixel offset in uv coordinates using the size of the
87 // texture.
88 //
89 // It isn't clear if this is meaningfully slower than using the offset syntax, the spec doesn't
90 // mention it anywhere: https://www.w3.org/TR/WGSL/#texturesample, but the fact that the offset
91 // syntax uses a const-expr implies that it allows some compiler optimizations - maybe more
92 // impactful on mobile?
93 let scale = uniforms.scale;
94 let ps = scale / vec2<f32>(textureDimensions(input_texture));
95 let pl = 2.0 * ps;
96 let ns = -1.0 * ps;
97 let nl = -2.0 * ps;
98 a = textureSample(input_texture, s, uv + vec2<f32>(nl.x, pl.y)).rgb;
99 b = textureSample(input_texture, s, uv + vec2<f32>(0.00, pl.y)).rgb;
100 c = textureSample(input_texture, s, uv + vec2<f32>(pl.x, pl.y)).rgb;
101 d = textureSample(input_texture, s, uv + vec2<f32>(nl.x, 0.00)).rgb;
102 e = textureSample(input_texture, s, uv).rgb;
103 f = textureSample(input_texture, s, uv + vec2<f32>(pl.x, 0.00)).rgb;
104 g = textureSample(input_texture, s, uv + vec2<f32>(nl.x, nl.y)).rgb;
105 h = textureSample(input_texture, s, uv + vec2<f32>(0.00, nl.y)).rgb;
106 i = textureSample(input_texture, s, uv + vec2<f32>(pl.x, nl.y)).rgb;
107 j = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ps.y)).rgb;
108 k = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ps.y)).rgb;
109 l = textureSample(input_texture, s, uv + vec2<f32>(ns.x, ns.y)).rgb;
110 m = textureSample(input_texture, s, uv + vec2<f32>(ps.x, ns.y)).rgb;
113@if(FIRST_DOWNSAMPLE) {
114 // [COD] slide 168
115 //
116 // The first downsample pass reads from the rendered frame which may exhibit
117 // 'fireflies' (individual very bright pixels) that should not cause the bloom effect.
118 //
119 // The first downsample uses a firefly-reduction method proposed by Brian Karis
120 // which takes a weighted-average of the samples to limit their luma range to [0, 1].
121 // This implementation matches the LearnOpenGL article [PBB].
122 var group0 = (a + b + d + e) * (0.125f / 4.0f);
123 var group1 = (b + c + e + f) * (0.125f / 4.0f);
124 var group2 = (d + e + g + h) * (0.125f / 4.0f);
125 var group3 = (e + f + h + i) * (0.125f / 4.0f);
126 var group4 = (j + k + l + m) * (0.5f / 4.0f);
127 group0 *= karis_average(group0);
128 group1 *= karis_average(group1);
129 group2 *= karis_average(group2);
130 group3 *= karis_average(group3);
131 group4 *= karis_average(group4);
132 return group0 + group1 + group2 + group3 + group4;
133} @else {
134 var sample = (a + c + g + i) * 0.03125;
135 sample += (b + d + f + h) * 0.0625;
136 sample += (e + j + k + l + m) * 0.125;
137 return sample;
141/// [COD] slide 162
142fn sample_input_3x3_tent(uv: vec2<f32>) -> vec3<f32> {
143 // While this is probably technically incorrect, it makes nonuniform bloom smoother, without
144 // having any impact on uniform bloom, which simply evaluates to 1.0 here.
145 let frag_size = uniforms.scale / vec2<f32>(textureDimensions(input_texture));
146 let x = frag_size.x;
147 let y = frag_size.y;
149 let a = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y + y)).rgb;
150 let b = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y + y)).rgb;
151 let c = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y + y)).rgb;
153 let d = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y)).rgb;
154 let e = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y)).rgb;
155 let f = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y)).rgb;
157 let g = textureSample(input_texture, s, vec2<f32>(uv.x - x, uv.y - y)).rgb;
158 let h = textureSample(input_texture, s, vec2<f32>(uv.x, uv.y - y)).rgb;
159 let i = textureSample(input_texture, s, vec2<f32>(uv.x + x, uv.y - y)).rgb;
161 var sample = e * 0.25;
162 sample += (b + d + f + h) * 0.125;
163 sample += (a + c + g + i) * 0.0625;
165 return sample;
168@if(FIRST_DOWNSAMPLE)
169@fragment
170fn downsample_first(@location(0) output_uv: vec2<f32>) -> @location(0) vec4<f32> {
171 let sample_uv = uniforms.viewport.xy + output_uv * uniforms.viewport.zw;
172 var sample = sample_input_13_tap(sample_uv);
173 // Lower bound of 0.0001 is to avoid propagating multiplying by 0.0 through the
174 // downscaling and upscaling which would result in black boxes.
175 // The upper bound is to prevent NaNs.
176 // with f32::MAX (E+38) Chrome fails with ":value 340282346999999984391321947108527833088.0 cannot be represented as 'f32'"
177 sample = clamp(sample, vec3<f32>(0.0001), vec3<f32>(3.40282347E+37));
179@if(USE_THRESHOLD)
180 sample = soft_threshold(sample);
182 return vec4<f32>(sample, 1.0);
185@fragment
186fn downsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
187 return vec4<f32>(sample_input_13_tap(uv), 1.0);
190@fragment
191fn upsample(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
192 return vec4<f32>(sample_input_3x3_tent(uv), 1.0);
Contents