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// Performs depth of field postprocessing, with both Gaussian and bokeh kernels.
2//
3// Gaussian blur is performed as a separable convolution: first blurring in the
4// X direction, and then in the Y direction. This is asymptotically more
5// efficient than performing a 2D convolution.
6//
7// The Bokeh blur uses a similar, but more complex, separable convolution
8// technique. The algorithm is described in Colin Barré-Brisebois, "Hexagonal
9// Bokeh Blur Revisited" [1]. It's motivated by the observation that we can use
10// separable convolutions not only to produce boxes but to produce
11// parallelograms. Thus, by performing three separable convolutions in sequence,
12// we can produce a hexagonal shape. The first and second convolutions are done
13// simultaneously using multiple render targets to cut the total number of
14// passes down to two.
15//
16// [1]: https://colinbarrebrisebois.com/2017/04/18/hexagonal-bokeh-blur-revisited-part-2-improved-2-pass-version/
18#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
19#import bevy_pbr::mesh_view_bindings::view
20#import bevy_pbr::view_transformations::depth_ndc_to_view_z
21#import bevy_render::view::View
23// Parameters that control the depth of field effect. See
24// `bevy_core_pipeline::dof::DepthOfFieldUniforms` for information on what these
25// parameters mean.
26struct DepthOfFieldParams {
27 /// The distance in meters to the location in focus.
28 focal_distance: f32,
30 /// The [focal length]. Physically speaking, this represents "the distance
31 /// from the center of the lens to the principal foci of the lens". The
32 /// default value, 50 mm, is considered representative of human eyesight.
33 /// Real-world lenses range from anywhere from 5 mm for "fisheye" lenses to
34 /// 2000 mm for "super-telephoto" lenses designed for very distant objects.
35 ///
36 /// The higher the value, the more blurry objects not in focus will be.
37 ///
38 /// [focal length]: https://en.wikipedia.org/wiki/Focal_length
39 focal_length: f32,
41 /// The premultiplied factor that we scale the circle of confusion by.
42 ///
43 /// This is calculated as `focal_length² / (sensor_height * aperture_f_stops)`.
44 coc_scale_factor: f32,
46 /// The maximum diameter, in pixels, that we allow a circle of confusion to be.
47 ///
48 /// A circle of confusion essentially describes the size of a blur.
49 ///
50 /// This value is nonphysical but is useful for avoiding pathologically-slow
51 /// behavior.
52 max_circle_of_confusion_diameter: f32,
54 /// The depth value that we clamp distant objects to. See the comment in
55 /// [`DepthOfField`] for more information.
56 max_depth: f32,
58 /// Padding.
59 pad_a: u32,
60 /// Padding.
61 pad_b: u32,
62 /// Padding.
63 pad_c: u32,
66// The first bokeh pass outputs to two render targets. We declare them here.
67struct DualOutput {
68 // The vertical output.
69 @location(0) output_0: vec4<f32>,
70 // The diagonal output.
71 @location(1) output_1: vec4<f32>,
74// @group(0) @binding(0) is `mesh_view_bindings::view`.
76// The depth texture for the main view.
77#ifdef MULTISAMPLED
78@group(0) @binding(1) var depth_texture: texture_depth_multisampled_2d;
79#else // MULTISAMPLED
80@group(0) @binding(1) var depth_texture: texture_depth_2d;
81#endif // MULTISAMPLED
83// The main color texture.
84@group(0) @binding(2) var color_texture_a: texture_2d<f32>;
86// The auxiliary color texture that we're sampling from. This is only used as
87// part of the second bokeh pass.
88#ifdef DUAL_INPUT
89@group(0) @binding(3) var color_texture_b: texture_2d<f32>;
90#endif // DUAL_INPUT
92// The global uniforms, representing data backed by buffers shared among all
93// views in the scene.
95// The parameters that control the depth of field effect.
96@group(1) @binding(0) var<uniform> dof_params: DepthOfFieldParams;
98// The sampler that's used to fetch texels from the source color buffer.
99@group(1) @binding(1) var color_texture_sampler: sampler;
101// used to ensure `depth * (focus - f)` is always a positive number,
102const EPSILON: f32 = 1.19209290e-07;
103// cos(-30°), used for the bokeh blur.
104const COS_NEG_FRAC_PI_6: f32 = 0.8660254037844387;
105// sin(-30°), used for the bokeh blur.
106const SIN_NEG_FRAC_PI_6: f32 = -0.5;
107// cos(-150°), used for the bokeh blur.
108const COS_NEG_FRAC_PI_5_6: f32 = -0.8660254037844387;
109// sin(-150°), used for the bokeh blur.
110const SIN_NEG_FRAC_PI_5_6: f32 = -0.5;
112// Calculates and returns the diameter (not the radius) of the [circle of
113// confusion].
114//
115// [circle of confusion]: https://en.wikipedia.org/wiki/Circle_of_confusion
116fn calculate_circle_of_confusion(in_frag_coord: vec4<f32>) -> f32 {
117 // Unpack the depth of field parameters.
118 let focus = dof_params.focal_distance;
119 let f = dof_params.focal_length;
120 let scale = dof_params.coc_scale_factor;
121 let max_coc_diameter = dof_params.max_circle_of_confusion_diameter;
123 // Sample the depth.
124 let frag_coord = vec2<i32>(floor(in_frag_coord.xy));
125 let raw_depth = textureLoad(depth_texture, frag_coord, 0);
126 let depth = min(-depth_ndc_to_view_z(raw_depth), dof_params.max_depth);
128 // Calculate the circle of confusion.
129 //
130 // This is just the formula from Wikipedia [1].
131 //
132 // [1]: https://en.wikipedia.org/wiki/Circle_of_confusion#Determining_a_circle_of_confusion_diameter_from_the_object_field
133 let candidate_coc = scale * abs(depth - focus) / (depth * max(focus - f, EPSILON));
135 let framebuffer_size = vec2<f32>(textureDimensions(color_texture_a));
136 return clamp(candidate_coc * framebuffer_size.y, 0.0, max_coc_diameter);
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.
152fn 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;
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));
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);
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;
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 }
199 return vec4(sum / weight_sum, 1.0);
202// Performs a box blur in a single direction, sampling `color_texture_a`.
203//
204// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
205// `position` input to the fragment).
206//
207// * `coc` is the diameter (not the radius) of the circle of confusion for this
208// fragment.
209//
210// * `frag_offset` is the vector, in screen-space units, from one sample to the
211// next. This need not be horizontal or vertical.
212fn box_blur_a(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {
213 let support = i32(round(coc * 0.5));
214 let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_a));
215 let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_a));
217 // Accumulate samples in a single direction.
218 var sum = vec3(0.0);
219 for (var i = 0; i <= support; i += 1) {
220 sum += textureSampleLevel(
221 color_texture_a, color_texture_sampler, uv + offset * f32(i), 0.0).rgb;
222 }
224 return vec4(sum / vec3(1.0 + f32(support)), 1.0);
227// Performs a box blur in a single direction, sampling `color_texture_b`.
228//
229// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the
230// `position` input to the fragment).
231//
232// * `coc` is the diameter (not the radius) of the circle of confusion for this
233// fragment.
234//
235// * `frag_offset` is the vector, in screen-space units, from one sample to the
236// next. This need not be horizontal or vertical.
237#ifdef DUAL_INPUT
238fn box_blur_b(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {
239 let support = i32(round(coc * 0.5));
240 let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_b));
241 let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_b));
243 // Accumulate samples in a single direction.
244 var sum = vec3(0.0);
245 for (var i = 0; i <= support; i += 1) {
246 sum += textureSampleLevel(
247 color_texture_b, color_texture_sampler, uv + offset * f32(i), 0.0).rgb;
248 }
250 return vec4(sum / vec3(1.0 + f32(support)), 1.0);
252#endif
254// Calculates the horizontal component of the separable Gaussian blur.
255@fragment
256fn gaussian_horizontal(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
257 let coc = calculate_circle_of_confusion(in.position);
258 return gaussian_blur(in.position, coc, vec2(1.0, 0.0));
261// Calculates the vertical component of the separable Gaussian blur.
262@fragment
263fn gaussian_vertical(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
264 let coc = calculate_circle_of_confusion(in.position);
265 return gaussian_blur(in.position, coc, vec2(0.0, 1.0));
268// Calculates the vertical and first diagonal components of the separable
269// hexagonal bokeh blur.
270//
271// ╱
272// ╱
273// •
274// │
275// │
276@fragment
277fn bokeh_pass_0(in: FullscreenVertexOutput) -> DualOutput {
278 let coc = calculate_circle_of_confusion(in.position);
279 let vertical = box_blur_a(in.position, coc, vec2(0.0, 1.0));
280 let diagonal = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6));
282 // Note that the diagonal part is pre-mixed with the vertical component.
283 var output: DualOutput;
284 output.output_0 = vertical;
285 output.output_1 = mix(vertical, diagonal, 0.5);
286 return output;
289// Calculates the second diagonal components of the separable hexagonal bokeh
290// blur.
291//
292// ╲ ╱
293// ╲ ╱
294// •
295#ifdef DUAL_INPUT
296@fragment
297fn bokeh_pass_1(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
298 let coc = calculate_circle_of_confusion(in.position);
299 let output_0 = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6));
300 let output_1 = box_blur_b(in.position, coc, vec2(COS_NEG_FRAC_PI_5_6, SIN_NEG_FRAC_PI_5_6));
301 return mix(output_0, output_1, 0.5);
303#endif
Contents