Skip to content

Commit 5e38499

Browse files
committed
Move the Gaussian blur shader function into a reusable library.
The depth of field pass has a medium-performance Gaussian blur implementation that's well-commented and battle-tested, but it's currently private to that pass. Gaussian blur is useful enough that it's worth exposing to application shaders, so this PR factors that code out into a reusable shader library.
1 parent bb23d0d commit 5e38499

File tree

3 files changed

+81
-65
lines changed

3 files changed

+81
-65
lines changed

crates/bevy_post_process/src/dof/dof.wgsl

Lines changed: 3 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
1919
#import bevy_pbr::mesh_view_bindings::view
2020
#import bevy_pbr::view_transformations::depth_ndc_to_view_z
21+
#import bevy_post_process::gaussian_blur::gaussian_blur
2122
#import bevy_render::view::View
2223

2324
// Parameters that control the depth of field effect. See
@@ -136,69 +137,6 @@ fn calculate_circle_of_confusion(in_frag_coord: vec4<f32>) -> f32 {
136137
return clamp(candidate_coc * framebuffer_size.y, 0.0, max_coc_diameter);
137138
}
138139

139-
// Performs a single direction of the separable Gaussian blur kernel.
140-
//
141-
// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
142-
// `position` input to the fragment).
143-
//
144-
// * `coc` is the diameter (not the radius) of the circle of confusion for this
145-
// fragment.
146-
//
147-
// * `frag_offset` is the vector, in screen-space units, from one sample to the
148-
// next. For a horizontal blur this will be `vec2(1.0, 0.0)`; for a vertical
149-
// blur this will be `vec2(0.0, 1.0)`.
150-
//
151-
// Returns the resulting color of the fragment.
152-
fn gaussian_blur(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {
153-
// Usually σ (the standard deviation) is half the radius, and the radius is
154-
// half the CoC. So we multiply by 0.25.
155-
let sigma = coc * 0.25;
156-
157-
// 1.5σ is a good, somewhat aggressive default for support—the number of
158-
// texels on each side of the center that we process.
159-
let support = i32(ceil(sigma * 1.5));
160-
let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_a));
161-
let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_a));
162-
163-
// The probability density function of the Gaussian blur is (up to constant factors) `exp(-1 / 2σ² *
164-
// x²). We precalculate the constant factor here to avoid having to
165-
// calculate it in the inner loop.
166-
let exp_factor = -1.0 / (2.0 * sigma * sigma);
167-
168-
// Accumulate samples on both sides of the current texel. Go two at a time,
169-
// taking advantage of bilinear filtering.
170-
var sum = textureSampleLevel(color_texture_a, color_texture_sampler, uv, 0.0).rgb;
171-
var weight_sum = 1.0;
172-
for (var i = 1; i <= support; i += 2) {
173-
// This is a well-known trick to reduce the number of needed texture
174-
// samples by a factor of two. We seek to accumulate two adjacent
175-
// samples c₀ and c₁ with weights w₀ and w₁ respectively, with a single
176-
// texture sample at a carefully chosen location. Observe that:
177-
//
178-
// k ⋅ lerp(c₀, c₁, t) = w₀⋅c₀ + w₁⋅c₁
179-
//
180-
// w₁
181-
// if k = w₀ + w₁ and t = ───────
182-
// w₀ + w₁
183-
//
184-
// Therefore, if we sample at a distance of t = w₁ / (w₀ + w₁) texels in
185-
// between the two texel centers and scale by k = w₀ + w₁ afterward, we
186-
// effectively evaluate w₀⋅c₀ + w₁⋅c₁ with a single texture lookup.
187-
let w0 = exp(exp_factor * f32(i) * f32(i));
188-
let w1 = exp(exp_factor * f32(i + 1) * f32(i + 1));
189-
let uv_offset = offset * (f32(i) + w1 / (w0 + w1));
190-
let weight = w0 + w1;
191-
192-
sum += (
193-
textureSampleLevel(color_texture_a, color_texture_sampler, uv + uv_offset, 0.0).rgb +
194-
textureSampleLevel(color_texture_a, color_texture_sampler, uv - uv_offset, 0.0).rgb
195-
) * weight;
196-
weight_sum += weight * 2.0;
197-
}
198-
199-
return vec4(sum / weight_sum, 1.0);
200-
}
201-
202140
// Performs a box blur in a single direction, sampling `color_texture_a`.
203141
//
204142
// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
@@ -255,14 +193,14 @@ fn box_blur_b(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f
255193
@fragment
256194
fn gaussian_horizontal(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
257195
let coc = calculate_circle_of_confusion(in.position);
258-
return gaussian_blur(in.position, coc, vec2(1.0, 0.0));
196+
return gaussian_blur(color_texture_a, color_texture_sampler, in.position, coc, vec2(1.0, 0.0));
259197
}
260198

261199
// Calculates the vertical component of the separable Gaussian blur.
262200
@fragment
263201
fn gaussian_vertical(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
264202
let coc = calculate_circle_of_confusion(in.position);
265-
return gaussian_blur(in.position, coc, vec2(0.0, 1.0));
203+
return gaussian_blur(color_texture_a, color_texture_sampler, in.position, coc, vec2(0.0, 1.0));
266204
}
267205

268206
// Calculates the vertical and first diagonal components of the separable
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// A library containing a 1D Gaussian blur kernel.
2+
//
3+
// This is used by depth of field, but you can also use it in custom
4+
// postprocessing passes.
5+
6+
#define_import_path bevy_post_process::gaussian_blur
7+
8+
// Performs a single direction of the separable Gaussian blur kernel.
9+
//
10+
// * `color_texture` is the texture to blur.
11+
//
12+
// * `color_texture_sampler` is a sampler to sample the texture. It must have
13+
// linear filtering for both minification and magnification.
14+
//
15+
// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
16+
// `position` input to the fragment).
17+
//
18+
// * `coc` is the diameter (not the radius) of the circle of confusion for this
19+
// fragment.
20+
//
21+
// * `frag_offset` is the vector, in screen-space units, from one sample to the
22+
// next. For a horizontal blur this will be `vec2(1.0, 0.0)`; for a vertical
23+
// blur this will be `vec2(0.0, 1.0)`.
24+
//
25+
// Returns the resulting color of the fragment.
26+
fn gaussian_blur(color_texture: texture_2d<f32>, color_texture_sampler: sampler, frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {
27+
// Usually σ (the standard deviation) is half the radius, and the radius is
28+
// half the CoC. So we multiply by 0.25.
29+
let sigma = coc * 0.25;
30+
31+
// 1.5σ is a good, somewhat aggressive default for support—the number of
32+
// texels on each side of the center that we process.
33+
let support = i32(ceil(sigma * 1.5));
34+
let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture));
35+
let offset = frag_offset / vec2<f32>(textureDimensions(color_texture));
36+
37+
// The probability density function of the Gaussian blur is (up to constant factors) `exp(-1 / 2σ² *
38+
// x²). We precalculate the constant factor here to avoid having to
39+
// calculate it in the inner loop.
40+
let exp_factor = -1.0 / (2.0 * sigma * sigma);
41+
42+
// Accumulate samples on both sides of the current texel. Go two at a time,
43+
// taking advantage of bilinear filtering.
44+
var sum = textureSampleLevel(color_texture, color_texture_sampler, uv, 0.0).rgb;
45+
var weight_sum = 1.0;
46+
for (var i = 1; i <= support; i += 2) {
47+
// This is a well-known trick to reduce the number of needed texture
48+
// samples by a factor of two. We seek to accumulate two adjacent
49+
// samples c₀ and c₁ with weights w₀ and w₁ respectively, with a single
50+
// texture sample at a carefully chosen location. Observe that:
51+
//
52+
// k ⋅ lerp(c₀, c₁, t) = w₀⋅c₀ + w₁⋅c₁
53+
//
54+
// w₁
55+
// if k = w₀ + w₁ and t = ───────
56+
// w₀ + w₁
57+
//
58+
// Therefore, if we sample at a distance of t = w₁ / (w₀ + w₁) texels in
59+
// between the two texel centers and scale by k = w₀ + w₁ afterward, we
60+
// effectively evaluate w₀⋅c₀ + w₁⋅c₁ with a single texture lookup.
61+
let w0 = exp(exp_factor * f32(i) * f32(i));
62+
let w1 = exp(exp_factor * f32(i + 1) * f32(i + 1));
63+
let uv_offset = offset * (f32(i) + w1 / (w0 + w1));
64+
let weight = w0 + w1;
65+
66+
sum += (
67+
textureSampleLevel(color_texture, color_texture_sampler, uv + uv_offset, 0.0).rgb +
68+
textureSampleLevel(color_texture, color_texture_sampler, uv - uv_offset, 0.0).rgb
69+
) * weight;
70+
weight_sum += weight * 2.0;
71+
}
72+
73+
return vec4(sum / weight_sum, 1.0);
74+
}
75+

crates/bevy_post_process/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ use crate::{
1818
motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin,
1919
};
2020
use bevy_app::{App, Plugin};
21+
use bevy_shader::load_shader_library;
2122

2223
/// Adds bloom, motion blur, depth of field, and chromatic aberration support.
2324
#[derive(Default)]
2425
pub struct PostProcessPlugin;
2526

2627
impl Plugin for PostProcessPlugin {
2728
fn build(&self, app: &mut App) {
29+
load_shader_library!(app, "gaussian_blur.wgsl");
30+
2831
app.add_plugins((
2932
MsaaWritebackPlugin,
3033
BloomPlugin,

0 commit comments

Comments
 (0)