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 the4// X direction, and then in the Y direction. This is asymptotically more5// efficient than performing a 2D convolution.6//7// The Bokeh blur uses a similar, but more complex, separable convolution8// technique. The algorithm is described in Colin Barré-Brisebois, "Hexagonal9// Bokeh Blur Revisited" [1]. It's motivated by the observation that we can use10// separable convolutions not only to produce boxes but to produce11// parallelograms. Thus, by performing three separable convolutions in sequence,12// we can produce a hexagonal shape. The first and second convolutions are done13// simultaneously using multiple render targets to cut the total number of14// 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::FullscreenVertexOutput19#import bevy_pbr::mesh_view_bindings::view20#import bevy_pbr::view_transformations::depth_ndc_to_view_z21#import bevy_render::view::View23// Parameters that control the depth of field effect. See24// `bevy_core_pipeline::dof::DepthOfFieldUniforms` for information on what these25// 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 distance31 /// from the center of the lens to the principal foci of the lens". The32 /// default value, 50 mm, is considered representative of human eyesight.33 /// Real-world lenses range from anywhere from 5 mm for "fisheye" lenses to34 /// 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_length39 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-slow51 /// behavior.52 max_circle_of_confusion_diameter: f32,54 /// The depth value that we clamp distant objects to. See the comment in55 /// [`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,64}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>,72}74// @group(0) @binding(0) is `mesh_view_bindings::view`.76// The depth texture for the main view.77#ifdef MULTISAMPLED78@group(0) @binding(1) var depth_texture: texture_depth_multisampled_2d;79#else // MULTISAMPLED80@group(0) @binding(1) var depth_texture: texture_depth_2d;81#endif // MULTISAMPLED83// 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 as87// part of the second bokeh pass.88#ifdef DUAL_INPUT89@group(0) @binding(3) var color_texture_b: texture_2d<f32>;90#endif // DUAL_INPUT92// The global uniforms, representing data backed by buffers shared among all93// 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// cos(-30°), used for the bokeh blur.102const COS_NEG_FRAC_PI_6: f32 = 0.8660254037844387;103// sin(-30°), used for the bokeh blur.104const SIN_NEG_FRAC_PI_6: f32 = -0.5;105// cos(-150°), used for the bokeh blur.106const COS_NEG_FRAC_PI_5_6: f32 = -0.8660254037844387;107// sin(-150°), used for the bokeh blur.108const SIN_NEG_FRAC_PI_5_6: f32 = -0.5;110// Calculates and returns the diameter (not the radius) of the [circle of111// confusion].112//113// [circle of confusion]: https://en.wikipedia.org/wiki/Circle_of_confusion114fn calculate_circle_of_confusion(in_frag_coord: vec4<f32>) -> f32 {115 // Unpack the depth of field parameters.116 let focus = dof_params.focal_distance;117 let f = dof_params.focal_length;118 let scale = dof_params.coc_scale_factor;119 let max_coc_diameter = dof_params.max_circle_of_confusion_diameter;121 // Sample the depth.122 let frag_coord = vec2<i32>(floor(in_frag_coord.xy));123 let raw_depth = textureLoad(depth_texture, frag_coord, 0);124 let depth = min(-depth_ndc_to_view_z(raw_depth), dof_params.max_depth);126 // Calculate the circle of confusion.127 //128 // This is just the formula from Wikipedia [1].129 //130 // [1]: https://en.wikipedia.org/wiki/Circle_of_confusion#Determining_a_circle_of_confusion_diameter_from_the_object_field131 let candidate_coc = scale * abs(depth - focus) / (depth * (focus - f));133 let framebuffer_size = vec2<f32>(textureDimensions(color_texture_a));134 return clamp(candidate_coc * framebuffer_size.y, 0.0, max_coc_diameter);135}137// Performs a single direction of the separable Gaussian blur kernel.138//139// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the140// `position` input to the fragment).141//142// * `coc` is the diameter (not the radius) of the circle of confusion for this143// fragment.144//145// * `frag_offset` is the vector, in screen-space units, from one sample to the146// next. For a horizontal blur this will be `vec2(1.0, 0.0)`; for a vertical147// blur this will be `vec2(0.0, 1.0)`.148//149// Returns the resulting color of the fragment.150fn gaussian_blur(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {151 // Usually σ (the standard deviation) is half the radius, and the radius is152 // half the CoC. So we multiply by 0.25.153 let sigma = coc * 0.25;155 // 1.5σ is a good, somewhat aggressive default for support—the number of156 // texels on each side of the center that we process.157 let support = i32(ceil(sigma * 1.5));158 let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_a));159 let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_a));161 // The probability density function of the Gaussian blur is (up to constant factors) `exp(-1 / 2σ² *162 // x²). We precalculate the constant factor here to avoid having to163 // calculate it in the inner loop.164 let exp_factor = -1.0 / (2.0 * sigma * sigma);166 // Accumulate samples on both sides of the current texel. Go two at a time,167 // taking advantage of bilinear filtering.168 var sum = textureSampleLevel(color_texture_a, color_texture_sampler, uv, 0.0).rgb;169 var weight_sum = 1.0;170 for (var i = 1; i <= support; i += 2) {171 // This is a well-known trick to reduce the number of needed texture172 // samples by a factor of two. We seek to accumulate two adjacent173 // samples c₀ and c₁ with weights w₀ and w₁ respectively, with a single174 // texture sample at a carefully chosen location. Observe that:175 //176 // k ⋅ lerp(c₀, c₁, t) = w₀⋅c₀ + w₁⋅c₁177 //178 // w₁179 // if k = w₀ + w₁ and t = ───────180 // w₀ + w₁181 //182 // Therefore, if we sample at a distance of t = w₁ / (w₀ + w₁) texels in183 // between the two texel centers and scale by k = w₀ + w₁ afterward, we184 // effectively evaluate w₀⋅c₀ + w₁⋅c₁ with a single texture lookup.185 let w0 = exp(exp_factor * f32(i) * f32(i));186 let w1 = exp(exp_factor * f32(i + 1) * f32(i + 1));187 let uv_offset = offset * (f32(i) + w1 / (w0 + w1));188 let weight = w0 + w1;190 sum += (191 textureSampleLevel(color_texture_a, color_texture_sampler, uv + uv_offset, 0.0).rgb +192 textureSampleLevel(color_texture_a, color_texture_sampler, uv - uv_offset, 0.0).rgb193 ) * weight;194 weight_sum += weight * 2.0;195 }197 return vec4(sum / weight_sum, 1.0);198}200// Performs a box blur in a single direction, sampling `color_texture_a`.201//202// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the203// `position` input to the fragment).204//205// * `coc` is the diameter (not the radius) of the circle of confusion for this206// fragment.207//208// * `frag_offset` is the vector, in screen-space units, from one sample to the209// next. This need not be horizontal or vertical.210fn box_blur_a(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {211 let support = i32(round(coc * 0.5));212 let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_a));213 let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_a));215 // Accumulate samples in a single direction.216 var sum = vec3(0.0);217 for (var i = 0; i <= support; i += 1) {218 sum += textureSampleLevel(219 color_texture_a, color_texture_sampler, uv + offset * f32(i), 0.0).rgb;220 }222 return vec4(sum / vec3(1.0 + f32(support)), 1.0);223}225// Performs a box blur in a single direction, sampling `color_texture_b`.226//227// * `frag_coord` is the screen-space pixel coordinate of the fragment (i.e. the228// `position` input to the fragment).229//230// * `coc` is the diameter (not the radius) of the circle of confusion for this231// fragment.232//233// * `frag_offset` is the vector, in screen-space units, from one sample to the234// next. This need not be horizontal or vertical.235#ifdef DUAL_INPUT236fn box_blur_b(frag_coord: vec4<f32>, coc: f32, frag_offset: vec2<f32>) -> vec4<f32> {237 let support = i32(round(coc * 0.5));238 let uv = frag_coord.xy / vec2<f32>(textureDimensions(color_texture_b));239 let offset = frag_offset / vec2<f32>(textureDimensions(color_texture_b));241 // Accumulate samples in a single direction.242 var sum = vec3(0.0);243 for (var i = 0; i <= support; i += 1) {244 sum += textureSampleLevel(245 color_texture_b, color_texture_sampler, uv + offset * f32(i), 0.0).rgb;246 }248 return vec4(sum / vec3(1.0 + f32(support)), 1.0);249}250#endif252// Calculates the horizontal component of the separable Gaussian blur.253@fragment254fn gaussian_horizontal(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {255 let coc = calculate_circle_of_confusion(in.position);256 return gaussian_blur(in.position, coc, vec2(1.0, 0.0));257}259// Calculates the vertical component of the separable Gaussian blur.260@fragment261fn gaussian_vertical(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {262 let coc = calculate_circle_of_confusion(in.position);263 return gaussian_blur(in.position, coc, vec2(0.0, 1.0));264}266// Calculates the vertical and first diagonal components of the separable267// hexagonal bokeh blur.268//269// ╱270// ╱271// •272// │273// │274@fragment275fn bokeh_pass_0(in: FullscreenVertexOutput) -> DualOutput {276 let coc = calculate_circle_of_confusion(in.position);277 let vertical = box_blur_a(in.position, coc, vec2(0.0, 1.0));278 let diagonal = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6));280 // Note that the diagonal part is pre-mixed with the vertical component.281 var output: DualOutput;282 output.output_0 = vertical;283 output.output_1 = mix(vertical, diagonal, 0.5);284 return output;285}287// Calculates the second diagonal components of the separable hexagonal bokeh288// blur.289//290// ╲ ╱291// ╲ ╱292// •293#ifdef DUAL_INPUT294@fragment295fn bokeh_pass_1(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {296 let coc = calculate_circle_of_confusion(in.position);297 let output_0 = box_blur_a(in.position, coc, vec2(COS_NEG_FRAC_PI_6, SIN_NEG_FRAC_PI_6));298 let output_1 = box_blur_b(in.position, coc, vec2(COS_NEG_FRAC_PI_5_6, SIN_NEG_FRAC_PI_5_6));299 return mix(output_0, output_1, 0.5);300}301#endif