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#import bevy_pbr::prepass_utils
2#import bevy_pbr::utils
3#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
4#import bevy_render::globals::Globals
6#ifdef MULTISAMPLED
7@group(0) @binding(0) var screen_texture: texture_2d<f32>;
8@group(0) @binding(1) var motion_vectors: texture_multisampled_2d<f32>;
9@group(0) @binding(2) var depth: texture_depth_multisampled_2d;
10#else
11@group(0) @binding(0) var screen_texture: texture_2d<f32>;
12@group(0) @binding(1) var motion_vectors: texture_2d<f32>;
13@group(0) @binding(2) var depth: texture_depth_2d;
14#endif
15@group(0) @binding(3) var texture_sampler: sampler;
16struct MotionBlur {
17 shutter_angle: f32,
18 samples: u32,
19#ifdef SIXTEEN_BYTE_ALIGNMENT
20 // WebGL2 structs must be 16 byte aligned.
21 _webgl2_padding: vec2<f32>
22#endif
24@group(0) @binding(4) var<uniform> settings: MotionBlur;
25@group(0) @binding(5) var<uniform> globals: Globals;
27@fragment
28fn fragment(
29 #ifdef MULTISAMPLED
30 @builtin(sample_index) sample_index: u32,
31 #endif
32 in: FullscreenVertexOutput
33) -> @location(0) vec4<f32> {
34 let texture_size = vec2<f32>(textureDimensions(screen_texture));
35 let frag_coords = vec2<i32>(in.uv * texture_size);
37#ifdef MULTISAMPLED
38 let base_color = textureLoad(screen_texture, frag_coords, i32(sample_index));
39#else
40 let base_color = textureSample(screen_texture, texture_sampler, in.uv);
41#endif
43 let shutter_angle = settings.shutter_angle;
45#ifdef MULTISAMPLED
46 let this_motion_vector = textureLoad(motion_vectors, frag_coords, i32(sample_index)).rg;
47#else
48 let this_motion_vector = textureSample(motion_vectors, texture_sampler, in.uv).rg;
49#endif
51#ifdef NO_DEPTH_TEXTURE_SUPPORT
52 let this_depth = 0.0;
53 let depth_supported = false;
54#else
55 let depth_supported = true;
56#ifdef MULTISAMPLED
57 let this_depth = textureLoad(depth, frag_coords, i32(sample_index));
58#else
59 let this_depth = textureSample(depth, texture_sampler, in.uv);
60#endif
61#endif
63 // The exposure vector is the distance that this fragment moved while the camera shutter was
64 // open. This is the motion vector (total distance traveled) multiplied by the shutter angle (a
65 // fraction). In film, the shutter angle is commonly 0.5 or "180 degrees" (out of 360 total).
66 // This means that for a frame time of 20ms, the shutter is only open for 10ms.
67 //
68 // Using a shutter angle larger than 1.0 is non-physical, objects would need to move further
69 // than they physically traveled during a frame, which is not possible. Note: we allow values
70 // larger than 1.0 because it may be desired for artistic reasons.
71 let exposure_vector = shutter_angle * this_motion_vector;
73 var accumulator: vec4<f32>;
74 var weight_total = 0.0;
75 let n_samples = i32(settings.samples);
76 let noise = utils::interleaved_gradient_noise(vec2<f32>(frag_coords), globals.frame_count); // 0 to 1
78 for (var i = -n_samples; i < n_samples; i++) {
79 // The current sample step vector, from in.uv
80 let step_vector = 0.5 * exposure_vector * (f32(i) + noise) / f32(n_samples);
81 var sample_uv = in.uv + step_vector;
83 // If the sample is off screen, skip it.
84 if sample_uv.x < 0.0 || sample_uv.x > 1.0 || sample_uv.y < 0.0 || sample_uv.y > 1.0 {
85 continue;
86 }
88 let sample_coords = vec2<i32>(sample_uv * texture_size);
90 #ifdef MULTISAMPLED
91 let sample_color = textureLoad(screen_texture, sample_coords, i32(sample_index));
92 #else
93 let sample_color = textureSample(screen_texture, texture_sampler, sample_uv);
94 #endif
95 #ifdef MULTISAMPLED
96 let sample_motion = textureLoad(motion_vectors, sample_coords, i32(sample_index)).rg;
97 #else
98 let sample_motion = textureSample(motion_vectors, texture_sampler, sample_uv).rg;
99 #endif
100 #ifdef NO_DEPTH_TEXTURE_SUPPORT
101 let sample_depth = 0.0;
102 #else
103 #ifdef MULTISAMPLED
104 let sample_depth = textureLoad(depth, sample_coords, i32(sample_index));
105 #else
106 let sample_depth = textureSample(depth, texture_sampler, sample_uv);
107 #endif
108 #endif
110 var weight = 1.0;
111 let is_sample_in_fg = !(depth_supported && sample_depth < this_depth && sample_depth > 0.0);
112 // If the depth is 0.0, this fragment has no depth written to it and we assume it is in the
113 // background. This ensures that things like skyboxes, which do not write to depth, are
114 // correctly sampled in motion blur.
115 if sample_depth != 0.0 && is_sample_in_fg {
116 // The following weight calculation is used to eliminate ghosting artifacts that are
117 // common in motion-vector-based motion blur implementations. While some resources
118 // recommend using depth, I've found that sampling the velocity results in significantly
119 // better results. Unlike a depth heuristic, this is not scale dependent.
120 //
121 // The most distracting artifacts occur when a stationary foreground object is
122 // incorrectly sampled while blurring a moving background object, causing the stationary
123 // object to blur when it should be sharp ("background bleeding"). This is most obvious
124 // when the camera is tracking a fast moving object. The tracked object should be sharp,
125 // and should not bleed into the motion blurred background.
126 //
127 // To attenuate these incorrect samples, we compare the motion of the fragment being
128 // blurred to the UV being sampled, to answer the question "is it possible that this
129 // sample was occluding the fragment?"
130 //
131 // Note to future maintainers: proceed with caution when making any changes here, and
132 // ensure you check all occlusion/disocclusion scenarios and fullscreen camera rotation
133 // blur for regressions.
134 let frag_speed = length(step_vector);
135 let sample_speed = length(sample_motion) / 2.0; // Halved because the sample is centered
136 let cos_angle = dot(step_vector, sample_motion) / (frag_speed * sample_speed * 2.0);
137 let motion_similarity = clamp(abs(cos_angle), 0.0, 1.0);
138 if sample_speed * motion_similarity < frag_speed {
139 // Project the sample's motion onto the frag's motion vector. If the sample did not
140 // cover enough distance to reach the original frag, there is no way it could have
141 // influenced this frag at all, and should be discarded.
142 weight = 0.0;
143 }
144 }
145 weight_total += weight;
146 accumulator += weight * sample_color;
147 }
149 let has_moved_less_than_a_pixel =
150 dot(this_motion_vector * texture_size, this_motion_vector * texture_size) < 1.0;
151 // In case no samples were accepted, fall back to base color.
152 // We also fall back if motion is small, to not break antialiasing.
153 if weight_total <= 0.0 || has_moved_less_than_a_pixel {
154 accumulator = base_color;
155 weight_total = 1.0;
156 }
157 return accumulator / weight_total;
Contents