bevy_render/view/mod.rs
1pub mod composition;
2pub mod visibility;
3pub mod window;
4
5use bevy_camera::{
6 primitives::Frustum, Camera, CameraMainTextureUsages, ClearColor, ClearColorConfig,
7 CompositingSpace, Exposure, MainPassResolutionOverride, NormalizedRenderTarget,
8};
9use bevy_diagnostic::FrameCount;
10pub use composition::*;
11pub use visibility::*;
12pub use window::*;
13
14use crate::{
15 camera::{ExtractedCamera, MipBias, NormalizedRenderTargetExt as _, TemporalJitter},
16 extract_component::ExtractComponentPlugin,
17 occlusion_culling::OcclusionCulling,
18 render_asset::RenderAssets,
19 render_phase::ViewRangefinder3d,
20 render_resource::{
21 BindGroup, DynamicUniformBuffer, ShaderType, Texture, TextureView, TextureViewId,
22 },
23 renderer::{RenderDevice, RenderQueue},
24 sync_world::MainEntity,
25 texture::{
26 CachedTexture, ColorAttachment, DepthStencilAttachment, GpuImage, ManualTextureViews,
27 OutputColorAttachment, TextureCache,
28 },
29 GpuResourceAppExt, Render, RenderApp, RenderSystems,
30};
31use alloc::sync::{Arc, Weak};
32use bevy_app::{App, Plugin};
33use bevy_color::{LinearRgba, Oklaba, Srgba};
34use bevy_derive::{Deref, DerefMut};
35use bevy_ecs::prelude::*;
36use bevy_extract_macros::ExtractComponent;
37use bevy_image::ToExtents;
38use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles};
39use bevy_platform::collections::{hash_map::Entry, HashMap};
40use bevy_reflect::{std_traits::ReflectDefault, Reflect};
41use bevy_transform::components::GlobalTransform;
42use core::{
43 ops::Range,
44 sync::atomic::{AtomicUsize, Ordering},
45};
46use wgpu::{
47 BufferUsages, Color as WgpuColor, RenderPassColorAttachment, RenderPassDepthStencilAttachment,
48 StoreOp, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
49};
50
51/// The matrix that converts from the RGB to the LMS color space.
52///
53/// To derive this, first we convert from RGB to [CIE 1931 XYZ]:
54///
55/// ```text
56/// ⎡ X ⎤ ⎡ 0.490 0.310 0.200 ⎤ ⎡ R ⎤
57/// ⎢ Y ⎥ = ⎢ 0.177 0.812 0.011 ⎥ ⎢ G ⎥
58/// ⎣ Z ⎦ ⎣ 0.000 0.010 0.990 ⎦ ⎣ B ⎦
59/// ```
60///
61/// Then we convert to LMS according to the [CAM16 standard matrix]:
62///
63/// ```text
64/// ⎡ L ⎤ ⎡ 0.401 0.650 -0.051 ⎤ ⎡ X ⎤
65/// ⎢ M ⎥ = ⎢ -0.250 1.204 0.046 ⎥ ⎢ Y ⎥
66/// ⎣ S ⎦ ⎣ -0.002 0.049 0.953 ⎦ ⎣ Z ⎦
67/// ```
68///
69/// The resulting matrix is just the concatenation of these two matrices, to do
70/// the conversion in one step.
71///
72/// [CIE 1931 XYZ]: https://en.wikipedia.org/wiki/CIE_1931_color_space
73/// [CAM16 standard matrix]: https://en.wikipedia.org/wiki/LMS_color_space
74static RGB_TO_LMS: Mat3 = mat3(
75 vec3(0.311692, 0.0905138, 0.00764433),
76 vec3(0.652085, 0.901341, 0.0486554),
77 vec3(0.0362225, 0.00814478, 0.943700),
78);
79
80/// The inverse of the [`RGB_TO_LMS`] matrix, converting from the LMS color
81/// space back to RGB.
82static LMS_TO_RGB: Mat3 = mat3(
83 vec3(4.06305, -0.40791, -0.0118812),
84 vec3(-2.93241, 1.40437, -0.0486532),
85 vec3(-0.130646, 0.00353630, 1.0605344),
86);
87
88/// The [CIE 1931] *xy* chromaticity coordinates of the [D65 white point].
89///
90/// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space
91/// [D65 white point]: https://en.wikipedia.org/wiki/Standard_illuminant#D65_values
92static D65_XY: Vec2 = vec2(0.31272, 0.32903);
93
94/// The [D65 white point] in [LMS color space].
95///
96/// [LMS color space]: https://en.wikipedia.org/wiki/LMS_color_space
97/// [D65 white point]: https://en.wikipedia.org/wiki/Standard_illuminant#D65_values
98static D65_LMS: Vec3 = vec3(0.975538, 1.01648, 1.08475);
99
100/// Mask bits (5-bit) for use in pipeline key bitfields.
101pub const COLOR_TARGET_FORMAT_MASK_BITS: u32 = 0b11111;
102
103/// Encode a [`TextureFormat`] as a 5-bit code for use in pipeline key bitfields.
104///
105/// Covers all WebGPU renderable and blendable texture formats. Some of them need optional features.
106/// See <https://gpuweb.github.io/gpuweb/#plain-color-formats>.
107#[inline]
108pub fn texture_format_to_code(format: TextureFormat) -> Option<u8> {
109 Some(match format {
110 TextureFormat::R8Unorm => 0,
111 TextureFormat::R8Snorm => 1,
112 TextureFormat::Rg8Unorm => 2,
113 TextureFormat::Rg8Snorm => 3,
114 TextureFormat::Rgba8Unorm => 4,
115 TextureFormat::Rgba8UnormSrgb => 5,
116 TextureFormat::Rgba8Snorm => 6,
117 TextureFormat::Bgra8Unorm => 7,
118 TextureFormat::Bgra8UnormSrgb => 8,
119 TextureFormat::R16Float => 11,
120 TextureFormat::R16Unorm => 9,
121 TextureFormat::R16Snorm => 10,
122 TextureFormat::Rg16Float => 12,
123 TextureFormat::Rg16Unorm => 13,
124 TextureFormat::Rg16Snorm => 14,
125 TextureFormat::Rgba16Float => 15,
126 TextureFormat::Rgba16Unorm => 16,
127 TextureFormat::Rgba16Snorm => 17,
128 TextureFormat::R32Float => 18,
129 TextureFormat::Rg32Float => 19,
130 TextureFormat::Rgba32Float => 20,
131 TextureFormat::Rgb10a2Unorm => 21,
132 TextureFormat::Rg11b10Ufloat => 22,
133 _ => return None,
134 })
135}
136
137/// Decode a 5-bit code back into a [`TextureFormat`].
138///
139/// Inverse of [`texture_format_to_code`].
140#[inline]
141pub fn texture_format_from_code(code: u8) -> Option<TextureFormat> {
142 Some(match code {
143 0 => TextureFormat::R8Unorm,
144 1 => TextureFormat::R8Snorm,
145 2 => TextureFormat::Rg8Unorm,
146 3 => TextureFormat::Rg8Snorm,
147 4 => TextureFormat::Rgba8Unorm,
148 5 => TextureFormat::Rgba8UnormSrgb,
149 6 => TextureFormat::Rgba8Snorm,
150 7 => TextureFormat::Bgra8Unorm,
151 8 => TextureFormat::Bgra8UnormSrgb,
152 11 => TextureFormat::R16Float,
153 9 => TextureFormat::R16Unorm,
154 10 => TextureFormat::R16Snorm,
155 12 => TextureFormat::Rg16Float,
156 13 => TextureFormat::Rg16Unorm,
157 14 => TextureFormat::Rg16Snorm,
158 15 => TextureFormat::Rgba16Float,
159 16 => TextureFormat::Rgba16Unorm,
160 17 => TextureFormat::Rgba16Snorm,
161 18 => TextureFormat::R32Float,
162 19 => TextureFormat::Rg32Float,
163 20 => TextureFormat::Rgba32Float,
164 21 => TextureFormat::Rgb10a2Unorm,
165 22 => TextureFormat::Rg11b10Ufloat,
166 _ => return None,
167 })
168}
169
170pub struct ViewPlugin;
171
172impl Plugin for ViewPlugin {
173 fn build(&self, app: &mut App) {
174 app
175 // NOTE: windows.is_changed() handles cases where a window was resized
176 .add_plugins((
177 ExtractComponentPlugin::<Msaa>::default(),
178 ExtractComponentPlugin::<OcclusionCulling>::default(),
179 RenderVisibilityRangePlugin,
180 ));
181
182 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
183 render_app.configure_sets(
184 Render,
185 ResolveCompositingSpaces
186 .in_set(RenderSystems::CreateViews)
187 .after(crate::camera::sort_cameras),
188 );
189 render_app.add_systems(
190 Render,
191 (
192 resolve_composition_spaces.in_set(ResolveCompositingSpaces),
193 // `TextureView`s need to be dropped before reconfiguring window surfaces.
194 clear_view_attachments
195 .in_set(RenderSystems::PrepareViews)
196 .before(create_surfaces),
197 cleanup_view_targets_for_resize
198 .in_set(RenderSystems::PrepareViews)
199 .before(create_surfaces),
200 prepare_view_attachments
201 .in_set(RenderSystems::PrepareViews)
202 .before(prepare_view_targets)
203 .after(prepare_windows),
204 prepare_view_targets
205 .in_set(RenderSystems::PrepareViews)
206 .after(prepare_windows)
207 .after(crate::render_asset::prepare_assets::<GpuImage>)
208 .ambiguous_with(crate::camera::sort_cameras), // doesn't use `sorted_camera_index_for_target`
209 prepare_view_uniforms.in_set(RenderSystems::PrepareResources),
210 collect_visible_cpu_culled_entities.in_set(RenderSystems::PrepareAssets),
211 ),
212 );
213 }
214 }
215
216 fn finish(&self, app: &mut App) {
217 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
218 render_app
219 .init_gpu_resource::<ViewUniforms>()
220 .init_gpu_resource::<ViewTargetAttachments>();
221 }
222 }
223}
224
225/// Component for configuring the number of samples for [Multi-Sample Anti-Aliasing](https://en.wikipedia.org/wiki/Multisample_anti-aliasing)
226/// for a [`Camera`].
227///
228/// Defaults to 4 samples. A higher number of samples results in smoother edges.
229///
230/// Some advanced rendering features may require that MSAA is disabled.
231///
232/// Note that the web currently only supports 1 or 4 samples.
233#[derive(
234 Component,
235 Default,
236 Clone,
237 Copy,
238 ExtractComponent,
239 Reflect,
240 PartialEq,
241 PartialOrd,
242 Eq,
243 Hash,
244 Debug,
245)]
246#[reflect(Component, Default, PartialEq, Hash, Debug)]
247#[extract_app(RenderApp)]
248pub enum Msaa {
249 Off = 1,
250 Sample2 = 2,
251 #[default]
252 Sample4 = 4,
253 Sample8 = 8,
254}
255
256impl Msaa {
257 #[inline]
258 pub fn samples(&self) -> u32 {
259 *self as u32
260 }
261
262 pub fn from_samples(samples: u32) -> Self {
263 match samples {
264 1 => Msaa::Off,
265 2 => Msaa::Sample2,
266 4 => Msaa::Sample4,
267 8 => Msaa::Sample8,
268 _ => panic!("Unsupported MSAA sample count: {samples}"),
269 }
270 }
271}
272
273/// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity.
274///
275/// The tonemapping pass lives in `bevy_core_pipeline`. The type is defined in
276/// `bevy_render` so render-world code can read it.
277#[derive(
278 Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
279)]
280#[extract_component_filter(With<Camera>)]
281#[reflect(Component, Debug, Hash, Default, PartialEq)]
282#[extract_app(RenderApp)]
283pub enum Tonemapping {
284 /// Bypass tonemapping. No color grading, exposure, or dither applies.
285 None,
286 /// Identity tone curve. [`ColorGrading`], exposure, and [`DebandDither`]
287 /// still apply. The output is unbounded display-linear.
288 Linear,
289 /// Suffers from lots hue shifting, brights don't desaturate naturally.
290 /// Bright primaries and secondaries don't desaturate at all.
291 Reinhard,
292 /// Suffers from hue shifting. Brights don't desaturate much at all across the spectrum.
293 ReinhardLuminance,
294 /// Same base implementation that Godot 4.0 uses for Tonemap ACES.
295 /// <https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl>
296 /// Not neutral, has a very specific aesthetic, intentional and dramatic hue shifting.
297 /// Bright greens and reds turn orange. Bright blues turn magenta.
298 /// Significantly increased contrast. Brights desaturate across the spectrum.
299 AcesFitted,
300 /// By Troy Sobotka
301 /// <https://github.com/sobotka/AgX>
302 /// Very neutral. Image is somewhat desaturated when compared to other tonemappers.
303 /// Little to no hue shifting. Subtle [Abney shifting](https://en.wikipedia.org/wiki/Abney_effect).
304 /// NOTE: Requires the `tonemapping_luts` cargo feature.
305 AgX,
306 /// By Tomasz Stachowiak
307 /// Has little hue shifting in the darks and mids, but lots in the brights. Brights desaturate across the spectrum.
308 /// Is sort of between Reinhard and `ReinhardLuminance`. Conceptually similar to reinhard-jodie.
309 /// Designed as a compromise if you want e.g. decent skin tones in low light, but can't afford to re-do your
310 /// VFX to look good without hue shifting.
311 SomewhatBoringDisplayTransform,
312 /// Current Bevy default.
313 /// By Tomasz Stachowiak
314 /// <https://github.com/h3r2tic/tony-mc-mapface>
315 /// Very neutral. Subtle but intentional hue shifting. Brights desaturate across the spectrum.
316 /// Comment from author:
317 /// Tony is a display transform intended for real-time applications such as games.
318 /// It is intentionally boring, does not increase contrast or saturation, and stays close to the
319 /// input stimulus where compression isn't necessary.
320 /// Brightness-equivalent luminance of the input stimulus is compressed. The non-linearity resembles Reinhard.
321 /// Color hues are preserved during compression, except for a deliberate [Bezold–Brücke shift](https://en.wikipedia.org/wiki/Bezold%E2%80%93Br%C3%BCcke_shift).
322 /// To avoid posterization, selective desaturation is employed, with care to avoid the [Abney effect](https://en.wikipedia.org/wiki/Abney_effect).
323 /// NOTE: Requires the `tonemapping_luts` cargo feature.
324 #[default]
325 TonyMcMapface,
326 /// Default Filmic Display Transform from blender.
327 /// Somewhat neutral. Suffers from hue shifting. Brights desaturate across the spectrum.
328 /// NOTE: Requires the `tonemapping_luts` cargo feature.
329 BlenderFilmic,
330 /// Despite its name, it is not considered to be neutral.
331 /// Highly saturated colors and tends to produce a very high contrast image.
332 /// Suffers from significant [Abney shifting](https://en.wikipedia.org/wiki/Abney_effect), and tends to crush grays and desaturated colors.
333 /// Designed for e-commerce to faithfully reproduce the colors of brand's logos when used with low brightness grayscale lighting.
334 /// See [the KhronosGroup spec](https://github.com/KhronosGroup/ToneMapping/tree/main/PBR_Neutral) for more information.
335 KhronosPbrNeutral,
336}
337
338impl Tonemapping {
339 pub fn is_enabled(&self) -> bool {
340 *self != Tonemapping::None
341 }
342}
343
344/// Enables a debanding shader that applies dithering to mitigate color banding in the final image for a given [`Camera`] entity.
345#[derive(
346 Component, Debug, Hash, Clone, Copy, Reflect, Default, ExtractComponent, PartialEq, Eq,
347)]
348#[extract_component_filter(With<Camera>)]
349#[reflect(Component, Debug, Hash, Default, PartialEq)]
350#[extract_app(RenderApp)]
351pub enum DebandDither {
352 #[default]
353 Disabled,
354 Enabled,
355}
356
357/// An identifier for a view that is stable across frames.
358///
359/// We can't use [`Entity`] for this because render world entities aren't
360/// stable, and we can't use just [`MainEntity`] because some main world views
361/// extract to multiple render world views. For example, a directional light
362/// extracts to one render world view per cascade, and a point light extracts to
363/// one render world view per cubemap face. So we pair the main entity with an
364/// *auxiliary entity* and a *subview index*, which *together* uniquely identify
365/// a view in the render world in a way that's stable from frame to frame.
366#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
367pub struct RetainedViewEntity {
368 /// The main entity that this view corresponds to.
369 pub main_entity: MainEntity,
370
371 /// Another entity associated with the view entity.
372 ///
373 /// This is currently used for shadow cascades. If there are multiple
374 /// cameras, each camera needs to have its own set of shadow cascades. Thus
375 /// the light and subview index aren't themselves enough to uniquely
376 /// identify a shadow cascade: we need the camera that the cascade is
377 /// associated with as well. This entity stores that camera.
378 pub auxiliary_entity: Option<MainEntity>,
379
380 /// The index of the view corresponding to the entity.
381 ///
382 /// For example, for point lights that cast shadows, this is the index of
383 /// the cubemap face (0 through 5 inclusive). For directional lights, this
384 /// is the index of the cascade.
385 pub subview_index: u32,
386}
387
388impl RetainedViewEntity {
389 /// Creates a new [`RetainedViewEntity`] from the given main world entity,
390 /// auxiliary main world entity, and subview index.
391 ///
392 /// See [`RetainedViewEntity::subview_index`] for an explanation of what
393 /// `auxiliary_entity` and `subview_index` are.
394 pub fn new(
395 main_entity: MainEntity,
396 auxiliary_entity: Option<MainEntity>,
397 subview_index: u32,
398 ) -> Self {
399 Self {
400 main_entity,
401 auxiliary_entity,
402 subview_index,
403 }
404 }
405}
406
407/// Describes a view in the render world.
408///
409/// Each entity in the main world can potentially extract to multiple views,
410/// each of which have a [`RetainedViewEntity::subview_index`].
411/// For instance, point lights with shadows extract to 6 subviews,
412/// one for each side of the shadow cubemap.
413/// [`Camera3d`](bevy_camera::Camera3d) extracts into a [`ExtractedView`]
414/// and [`ExtractedCamera`] component.
415#[derive(Component)]
416pub struct ExtractedView {
417 /// The entity in the main world corresponding to this render world view.
418 pub retained_view_entity: RetainedViewEntity,
419 /// Typically a column-major right-handed projection matrix, one of either:
420 ///
421 /// Perspective (infinite reverse z)
422 /// ```text
423 /// f = 1 / tan(fov_y_radians / 2)
424 ///
425 /// ⎡ f / aspect 0 0 0 ⎤
426 /// ⎢ 0 f 0 0 ⎥
427 /// ⎢ 0 0 0 near ⎥
428 /// ⎣ 0 0 -1 0 ⎦
429 /// ```
430 ///
431 /// Orthographic
432 /// ```text
433 /// w = right - left
434 /// h = top - bottom
435 /// d = far - near
436 /// cw = -right - left
437 /// ch = -top - bottom
438 ///
439 /// ⎡ 2 / w 0 0 cw / w ⎤
440 /// ⎢ 0 2 / h 0 ch / h ⎥
441 /// ⎢ 0 0 1 / d far / d ⎥
442 /// ⎣ 0 0 0 1 ⎦
443 /// ```
444 ///
445 /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic
446 ///
447 /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]`
448 ///
449 /// Custom projections are also possible however.
450 pub clip_from_view: Mat4,
451 pub world_from_view: GlobalTransform,
452 // The view-projection matrix. When provided it is used instead of deriving it from
453 // `projection` and `transform` fields, which can be helpful in cases where numerical
454 // stability matters and there is a more direct way to derive the view-projection matrix.
455 pub clip_from_world: Option<Mat4>,
456 /// The [`TextureFormat`] this view will render to. Note that this may diverge from
457 /// the [`RenderTarget`](bevy_camera::RenderTarget)'s texture format. Among other
458 /// reasons, [`Hdr`](bevy_camera::Hdr) sets an the internal render target format
459 /// override to ensure sufficient precision is present for lighting calculations.
460 pub target_format: TextureFormat,
461 // uvec4(origin.x, origin.y, width, height)
462 pub viewport: UVec4,
463 pub color_grading: ColorGrading,
464
465 /// Whether to switch culling mode so that materials that request backface
466 /// culling cull front faces, and vice versa.
467 ///
468 /// This is typically used for cameras that mirror the world that they
469 /// render across a plane, because doing that flips the winding of each
470 /// polygon.
471 ///
472 /// This setting doesn't affect materials that disable backface culling.
473 pub invert_culling: bool,
474}
475
476impl ExtractedView {
477 /// Creates a 3D rangefinder for a view
478 pub fn rangefinder3d(&self) -> ViewRangefinder3d {
479 ViewRangefinder3d::from_world_from_view(&self.world_from_view.affine())
480 }
481}
482
483/// Configures filmic color grading parameters to adjust the image appearance.
484///
485/// Color grading is applied just before tonemapping for a given [`Camera`]
486/// entity, with the sole exception of the `post_saturation` value in
487/// [`ColorGradingGlobal`], which is applied after tonemapping.
488#[derive(Component, Reflect, Debug, Default, Clone, PartialEq)]
489#[reflect(Component, Default, Debug, Clone, PartialEq)]
490pub struct ColorGrading {
491 /// Filmic color grading values applied to the image as a whole (as opposed
492 /// to individual sections, like shadows and highlights).
493 pub global: ColorGradingGlobal,
494
495 /// Color grading values that are applied to the darker parts of the image.
496 ///
497 /// The cutoff points can be customized with the
498 /// [`ColorGradingGlobal::midtones_range`] field.
499 pub shadows: ColorGradingSection,
500
501 /// Color grading values that are applied to the parts of the image with
502 /// intermediate brightness.
503 ///
504 /// The cutoff points can be customized with the
505 /// [`ColorGradingGlobal::midtones_range`] field.
506 pub midtones: ColorGradingSection,
507
508 /// Color grading values that are applied to the lighter parts of the image.
509 ///
510 /// The cutoff points can be customized with the
511 /// [`ColorGradingGlobal::midtones_range`] field.
512 pub highlights: ColorGradingSection,
513}
514
515/// Filmic color grading values applied to the image as a whole (as opposed to
516/// individual sections, like shadows and highlights).
517#[derive(Clone, Debug, Reflect, PartialEq)]
518#[reflect(Default, Clone, PartialEq)]
519pub struct ColorGradingGlobal {
520 /// Exposure value (EV) offset, measured in stops.
521 pub exposure: f32,
522
523 /// An adjustment made to the [CIE 1931] chromaticity *x* value.
524 ///
525 /// Positive values make the colors redder. Negative values make the colors
526 /// bluer. This has no effect on luminance (brightness).
527 ///
528 /// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
529 pub temperature: f32,
530
531 /// An adjustment made to the [CIE 1931] chromaticity *y* value.
532 ///
533 /// Positive values make the colors more magenta. Negative values make the
534 /// colors greener. This has no effect on luminance (brightness).
535 ///
536 /// [CIE 1931]: https://en.wikipedia.org/wiki/CIE_1931_color_space#CIE_xy_chromaticity_diagram_and_the_CIE_xyY_color_space
537 pub tint: f32,
538
539 /// An adjustment to the [hue], in radians.
540 ///
541 /// Adjusting this value changes the perceived colors in the image: red to
542 /// yellow to green to blue, etc. It has no effect on the saturation or
543 /// brightness of the colors.
544 ///
545 /// [hue]: https://en.wikipedia.org/wiki/HSL_and_HSV#Formal_derivation
546 pub hue: f32,
547
548 /// Saturation adjustment applied after tonemapping.
549 /// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
550 /// with luminance defined by ITU-R BT.709
551 /// Values above 1.0 increase saturation.
552 pub post_saturation: f32,
553
554 /// The luminance (brightness) ranges that are considered part of the
555 /// "midtones" of the image.
556 ///
557 /// This affects which [`ColorGradingSection`]s apply to which colors. Note
558 /// that the sections smoothly blend into one another, to avoid abrupt
559 /// transitions.
560 ///
561 /// The default value is 0.2 to 0.7.
562 pub midtones_range: Range<f32>,
563}
564
565/// The [`ColorGrading`] structure, packed into the most efficient form for the
566/// GPU.
567#[derive(Clone, Copy, Debug, ShaderType)]
568pub struct ColorGradingUniform {
569 pub balance: Mat3,
570 pub saturation: Vec3,
571 pub contrast: Vec3,
572 pub gamma: Vec3,
573 pub gain: Vec3,
574 pub lift: Vec3,
575 pub midtone_range: Vec2,
576 pub exposure: f32,
577 pub hue: f32,
578 pub post_saturation: f32,
579}
580
581/// A section of color grading values that can be selectively applied to
582/// shadows, midtones, and highlights.
583#[derive(Reflect, Debug, Copy, Clone, PartialEq)]
584#[reflect(Clone, PartialEq)]
585pub struct ColorGradingSection {
586 /// Values below 1.0 desaturate, with a value of 0.0 resulting in a grayscale image
587 /// with luminance defined by ITU-R BT.709.
588 /// Values above 1.0 increase saturation.
589 pub saturation: f32,
590
591 /// Adjusts the range of colors.
592 ///
593 /// A value of 1.0 applies no changes. Values below 1.0 move the colors more
594 /// toward a neutral gray. Values above 1.0 spread the colors out away from
595 /// the neutral gray.
596 pub contrast: f32,
597
598 /// A nonlinear luminance adjustment, mainly affecting the high end of the
599 /// range.
600 ///
601 /// This is the *n* exponent in the standard [ASC CDL] formula for color
602 /// correction:
603 ///
604 /// ```text
605 /// out = (i × s + o)ⁿ
606 /// ```
607 ///
608 /// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
609 pub gamma: f32,
610
611 /// A linear luminance adjustment, mainly affecting the middle part of the
612 /// range.
613 ///
614 /// This is the *s* factor in the standard [ASC CDL] formula for color
615 /// correction:
616 ///
617 /// ```text
618 /// out = (i × s + o)ⁿ
619 /// ```
620 ///
621 /// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
622 pub gain: f32,
623
624 /// A fixed luminance adjustment, mainly affecting the lower part of the
625 /// range.
626 ///
627 /// This is the *o* term in the standard [ASC CDL] formula for color
628 /// correction:
629 ///
630 /// ```text
631 /// out = (i × s + o)ⁿ
632 /// ```
633 ///
634 /// [ASC CDL]: https://en.wikipedia.org/wiki/ASC_CDL#Combined_Function
635 pub lift: f32,
636}
637
638impl Default for ColorGradingGlobal {
639 fn default() -> Self {
640 Self {
641 exposure: 0.0,
642 temperature: 0.0,
643 tint: 0.0,
644 hue: 0.0,
645 post_saturation: 1.0,
646 midtones_range: 0.2..0.7,
647 }
648 }
649}
650
651impl Default for ColorGradingSection {
652 fn default() -> Self {
653 Self {
654 saturation: 1.0,
655 contrast: 1.0,
656 gamma: 1.0,
657 gain: 1.0,
658 lift: 0.0,
659 }
660 }
661}
662
663impl ColorGrading {
664 /// Creates a new [`ColorGrading`] instance in which shadows, midtones, and
665 /// highlights all have the same set of color grading values.
666 pub fn with_identical_sections(
667 global: ColorGradingGlobal,
668 section: ColorGradingSection,
669 ) -> ColorGrading {
670 ColorGrading {
671 global,
672 highlights: section,
673 midtones: section,
674 shadows: section,
675 }
676 }
677
678 /// Returns an iterator that visits the shadows, midtones, and highlights
679 /// sections, in that order.
680 pub fn all_sections(&self) -> impl Iterator<Item = &ColorGradingSection> {
681 [&self.shadows, &self.midtones, &self.highlights].into_iter()
682 }
683
684 /// Applies the given mutating function to the shadows, midtones, and
685 /// highlights sections, in that order.
686 ///
687 /// Returns an array composed of the results of such evaluation, in that
688 /// order.
689 pub fn all_sections_mut(&mut self) -> impl Iterator<Item = &mut ColorGradingSection> {
690 [&mut self.shadows, &mut self.midtones, &mut self.highlights].into_iter()
691 }
692}
693
694/// A resource, part of the render world, that stores the resolved origin for
695/// LOD selection for shadow maps of point and spot lights.
696#[derive(Default, Resource, Debug)]
697pub struct RenderShadowLodOrigin(pub Vec3);
698
699#[derive(Clone, ShaderType)]
700pub struct ViewUniform {
701 pub clip_from_world: Mat4,
702 pub unjittered_clip_from_world: Mat4,
703 pub world_from_clip: Mat4,
704 pub world_from_view: Mat4,
705 pub view_from_world: Mat4,
706 /// Typically a column-major right-handed projection matrix, one of either:
707 ///
708 /// Perspective (infinite reverse z)
709 /// ```text
710 /// f = 1 / tan(fov_y_radians / 2)
711 ///
712 /// ⎡ f / aspect 0 0 0 ⎤
713 /// ⎢ 0 f 0 0 ⎥
714 /// ⎢ 0 0 0 near ⎥
715 /// ⎣ 0 0 -1 0 ⎦
716 /// ```
717 ///
718 /// Orthographic
719 /// ```text
720 /// w = right - left
721 /// h = top - bottom
722 /// d = far - near
723 /// cw = -right - left
724 /// ch = -top - bottom
725 ///
726 /// ⎡ 2 / w 0 0 cw / w ⎤
727 /// ⎢ 0 2 / h 0 ch / h ⎥
728 /// ⎢ 0 0 1 / d far / d ⎥
729 /// ⎣ 0 0 0 1 ⎦
730 /// ```
731 ///
732 /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic
733 ///
734 /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]`
735 ///
736 /// Custom projections are also possible however.
737 pub clip_from_view: Mat4,
738 pub view_from_clip: Mat4,
739 pub world_position: Vec3,
740 pub exposure: f32,
741 // viewport(x_origin, y_origin, width, height)
742 pub viewport: Vec4,
743 pub main_pass_viewport: Vec4,
744 /// 6 world-space half spaces (normal: vec3, distance: f32) ordered left, right, top, bottom, near, far.
745 /// The normal vectors point towards the interior of the frustum.
746 /// A half space contains `p` if `normal.dot(p) + distance > 0.`
747 pub frustum: [Vec4; 6],
748 /// The world-space position of the camera used to resolve visibility
749 /// ranges.
750 ///
751 /// This is the position of the camera itself, unless this view isn't
752 /// associated with a camera, in which case it's the position of the primary
753 /// camera.
754 pub lod_view_world_position: Vec3,
755 pub color_grading: ColorGradingUniform,
756 pub mip_bias: f32,
757 pub frame_count: u32,
758}
759
760#[derive(Resource)]
761pub struct ViewUniforms {
762 pub uniforms: DynamicUniformBuffer<ViewUniform>,
763}
764
765impl FromWorld for ViewUniforms {
766 fn from_world(world: &mut World) -> Self {
767 let mut uniforms = DynamicUniformBuffer::default();
768 uniforms.set_label(Some("view_uniforms_buffer"));
769
770 let render_device = world.resource::<RenderDevice>();
771 if render_device.limits().max_storage_buffers_per_shader_stage > 0 {
772 uniforms.add_usages(BufferUsages::STORAGE);
773 }
774
775 Self { uniforms }
776 }
777}
778
779#[derive(Component)]
780pub struct ViewUniformOffset {
781 pub offset: u32,
782}
783
784#[derive(Component, Clone)]
785pub struct ViewTarget {
786 main_textures: MainTargetTextures,
787 main_texture_format: TextureFormat,
788 /// 0 represents `main_textures.a`, 1 represents `main_textures.b`
789 /// This is shared across view targets with the same render target
790 main_texture: Arc<AtomicUsize>,
791 /// The final output attachment this view will present to, if available.
792 out_texture: Option<OutputColorAttachment>,
793}
794
795/// Contains [`OutputColorAttachment`] used for each target present on any view in the current
796/// frame, after being prepared by [`prepare_view_attachments`]. Users that want to override
797/// the default output color attachment for a specific target can do so by adding a
798/// [`OutputColorAttachment`] to this resource before [`prepare_view_targets`] is called.
799#[derive(Resource, Default, Deref, DerefMut)]
800pub struct ViewTargetAttachments(HashMap<NormalizedRenderTarget, OutputColorAttachment>);
801
802pub struct PostProcessWrite<'a> {
803 pub source: &'a TextureView,
804 pub source_texture: &'a Texture,
805 pub destination: &'a TextureView,
806 pub destination_texture: &'a Texture,
807}
808
809impl From<ColorGrading> for ColorGradingUniform {
810 fn from(component: ColorGrading) -> Self {
811 // Compute the balance matrix that will be used to apply the white
812 // balance adjustment to an RGB color. Our general approach will be to
813 // convert both the color and the developer-supplied white point to the
814 // LMS color space, apply the conversion, and then convert back.
815 //
816 // First, we start with the CIE 1931 *xy* values of the standard D65
817 // illuminant:
818 // <https://en.wikipedia.org/wiki/Standard_illuminant#D65_values>
819 //
820 // We then adjust them based on the developer's requested white balance.
821 let white_point_xy = D65_XY + vec2(-component.global.temperature, component.global.tint);
822
823 // Convert the white point from CIE 1931 *xy* to LMS. First, we convert to XYZ:
824 //
825 // Y Y
826 // Y = 1 X = ─ x Z = ─ (1 - x - y)
827 // y y
828 //
829 // Then we convert from XYZ to LMS color space, using the CAM16 matrix
830 // from <https://en.wikipedia.org/wiki/LMS_color_space#Later_CIECAMs>:
831 //
832 // ⎡ L ⎤ ⎡ 0.401 0.650 -0.051 ⎤ ⎡ X ⎤
833 // ⎢ M ⎥ = ⎢ -0.250 1.204 0.046 ⎥ ⎢ Y ⎥
834 // ⎣ S ⎦ ⎣ -0.002 0.049 0.953 ⎦ ⎣ Z ⎦
835 //
836 // The following formula is just a simplification of the above.
837
838 let white_point_lms = vec3(0.701634, 1.15856, -0.904175)
839 + (vec3(-0.051461, 0.045854, 0.953127)
840 + vec3(0.452749, -0.296122, -0.955206) * white_point_xy.x)
841 / white_point_xy.y;
842
843 // Now that we're in LMS space, perform the white point scaling.
844 let white_point_adjustment = Mat3::from_diagonal(D65_LMS / white_point_lms);
845
846 // Finally, combine the RGB → LMS → corrected LMS → corrected RGB
847 // pipeline into a single 3×3 matrix.
848 let balance = LMS_TO_RGB * white_point_adjustment * RGB_TO_LMS;
849
850 Self {
851 balance,
852 saturation: vec3(
853 component.shadows.saturation,
854 component.midtones.saturation,
855 component.highlights.saturation,
856 ),
857 contrast: vec3(
858 component.shadows.contrast,
859 component.midtones.contrast,
860 component.highlights.contrast,
861 ),
862 gamma: vec3(
863 component.shadows.gamma,
864 component.midtones.gamma,
865 component.highlights.gamma,
866 ),
867 gain: vec3(
868 component.shadows.gain,
869 component.midtones.gain,
870 component.highlights.gain,
871 ),
872 lift: vec3(
873 component.shadows.lift,
874 component.midtones.lift,
875 component.highlights.lift,
876 ),
877 midtone_range: vec2(
878 component.global.midtones_range.start,
879 component.global.midtones_range.end,
880 ),
881 exposure: component.global.exposure,
882 hue: component.global.hue,
883 post_saturation: component.global.post_saturation,
884 }
885 }
886}
887
888/// Add this component to a camera to disable *indirect mode*.
889///
890/// Indirect mode, automatically enabled on supported hardware, allows Bevy to
891/// offload transform and cull operations to the GPU, reducing CPU overhead.
892/// Doing this, however, reduces the amount of control that your app has over
893/// instancing decisions. In certain circumstances, you may want to disable
894/// indirect drawing so that your app can manually instance meshes as it sees
895/// fit. See the `custom_shader_instancing` example.
896///
897/// The vast majority of applications will not need to use this component, as it
898/// generally reduces rendering performance.
899///
900/// Note: This component should only be added when initially spawning a camera. Adding
901/// or removing after spawn can result in unspecified behavior.
902#[derive(Component, Default)]
903pub struct NoIndirectDrawing;
904
905impl ViewTarget {
906 /// Retrieve this target's main texture's color attachment.
907 pub fn get_color_attachment(&self) -> RenderPassColorAttachment<'_> {
908 if self.main_texture.load(Ordering::SeqCst) == 0 {
909 self.main_textures.a.get_attachment()
910 } else {
911 self.main_textures.b.get_attachment()
912 }
913 }
914
915 /// Retrieve this target's "unsampled" main texture's color attachment.
916 pub fn get_unsampled_color_attachment(&self) -> RenderPassColorAttachment<'_> {
917 if self.main_texture.load(Ordering::SeqCst) == 0 {
918 self.main_textures.a.get_unsampled_attachment()
919 } else {
920 self.main_textures.b.get_unsampled_attachment()
921 }
922 }
923
924 /// The "main" unsampled texture.
925 pub fn main_texture(&self) -> &Texture {
926 if self.main_texture.load(Ordering::SeqCst) == 0 {
927 &self.main_textures.a.texture.texture
928 } else {
929 &self.main_textures.b.texture.texture
930 }
931 }
932
933 /// The _other_ "main" unsampled texture.
934 /// In most cases you should use [`Self::main_texture`] instead and never this.
935 /// The textures will naturally be swapped when [`Self::post_process_write`] is called.
936 ///
937 /// A use case for this is to be able to prepare a bind group for all main textures
938 /// ahead of time.
939 pub fn main_texture_other(&self) -> &Texture {
940 if self.main_texture.load(Ordering::SeqCst) == 0 {
941 &self.main_textures.b.texture.texture
942 } else {
943 &self.main_textures.a.texture.texture
944 }
945 }
946
947 /// The "main" unsampled texture.
948 pub fn main_texture_view(&self) -> &TextureView {
949 if self.main_texture.load(Ordering::SeqCst) == 0 {
950 &self.main_textures.a.texture.default_view
951 } else {
952 &self.main_textures.b.texture.default_view
953 }
954 }
955
956 /// The _other_ "main" unsampled texture view.
957 /// In most cases you should use [`Self::main_texture_view`] instead and never this.
958 /// The textures will naturally be swapped when [`Self::post_process_write`] is called.
959 ///
960 /// A use case for this is to be able to prepare a bind group for all main textures
961 /// ahead of time.
962 pub fn main_texture_other_view(&self) -> &TextureView {
963 if self.main_texture.load(Ordering::SeqCst) == 0 {
964 &self.main_textures.b.texture.default_view
965 } else {
966 &self.main_textures.a.texture.default_view
967 }
968 }
969
970 /// The "main" sampled texture.
971 pub fn sampled_main_texture(&self) -> Option<&Texture> {
972 self.main_textures
973 .a
974 .resolve_target
975 .as_ref()
976 .map(|sampled| &sampled.texture)
977 }
978
979 /// The "main" sampled texture view.
980 pub fn sampled_main_texture_view(&self) -> Option<&TextureView> {
981 self.main_textures
982 .a
983 .resolve_target
984 .as_ref()
985 .map(|sampled| &sampled.default_view)
986 }
987
988 /// Currently bevy's main texture format can be:
989 /// - If rendering to screen:
990 /// For HDR, it's `Rgba16Float`.
991 /// For LDR, it's `Rgba8Unorm` when [`CompositingSpace::Srgb`], otherwise `Rgba8UnormSrgb`.
992 /// - If rendering to texture: the format is the same as texture view's format.
993 #[inline]
994 pub fn main_texture_format(&self) -> TextureFormat {
995 self.main_texture_format
996 }
997
998 /// The final texture this view will render to.
999 #[inline]
1000 pub fn out_texture(&self) -> Option<&TextureView> {
1001 self.out_texture.as_ref().map(|t| &t.view)
1002 }
1003
1004 /// The final texture this view will render to, as a color attachment.
1005 pub fn out_texture_color_attachment(
1006 &self,
1007 clear_color: Option<LinearRgba>,
1008 ) -> Option<RenderPassColorAttachment<'_>> {
1009 self.out_texture
1010 .as_ref()
1011 .map(|t| t.get_attachment(clear_color))
1012 }
1013
1014 /// Whether the final texture this view will render to needs to be presented.
1015 pub fn needs_present(&self) -> bool {
1016 self.out_texture
1017 .as_ref()
1018 .is_some_and(OutputColorAttachment::needs_present)
1019 }
1020
1021 /// The format of the final texture this view will render to, if any.
1022 #[inline]
1023 pub fn out_texture_view_format(&self) -> Option<TextureFormat> {
1024 self.out_texture.as_ref().map(|t| t.view_format)
1025 }
1026
1027 /// This will start a new "post process write", which assumes that the caller
1028 /// will write the [`PostProcessWrite`]'s `source` to the `destination`.
1029 ///
1030 /// `source` is the "current" main texture. This will internally flip this
1031 /// [`ViewTarget`]'s main texture to the `destination` texture, so the caller
1032 /// _must_ ensure `source` is copied to `destination`, with or without modifications.
1033 /// Failing to do so will cause the current main texture information to be lost.
1034 pub fn post_process_write(&self) -> PostProcessWrite<'_> {
1035 let old_is_a_main_texture = self.main_texture.fetch_xor(1, Ordering::SeqCst);
1036 // if the old main texture is a, then the post processing must write from a to b
1037 if old_is_a_main_texture == 0 {
1038 self.main_textures.b.mark_as_cleared();
1039 PostProcessWrite {
1040 source: &self.main_textures.a.texture.default_view,
1041 source_texture: &self.main_textures.a.texture.texture,
1042 destination: &self.main_textures.b.texture.default_view,
1043 destination_texture: &self.main_textures.b.texture.texture,
1044 }
1045 } else {
1046 self.main_textures.a.mark_as_cleared();
1047 PostProcessWrite {
1048 source: &self.main_textures.b.texture.default_view,
1049 source_texture: &self.main_textures.b.texture.texture,
1050 destination: &self.main_textures.a.texture.default_view,
1051 destination_texture: &self.main_textures.a.texture.texture,
1052 }
1053 }
1054 }
1055}
1056
1057pub trait CreatePostProcessBindGroup:
1058 Fn(&TextureView) -> (TextureViewId, BindGroup) + Copy
1059{
1060}
1061
1062impl<T: Fn(&TextureView) -> (TextureViewId, BindGroup) + Copy> CreatePostProcessBindGroup for T {}
1063
1064/// Used to create a [`PostProcessBindGroupCache`]
1065pub struct PostProcessBindGroupCacheBuilder<F>
1066where
1067 F: CreatePostProcessBindGroup,
1068{
1069 create_bind_group: F,
1070}
1071
1072impl<F> PostProcessBindGroupCacheBuilder<F>
1073where
1074 F: CreatePostProcessBindGroup,
1075{
1076 /// Initializes the [`PostProcessBindGroupCacheBuilder`]
1077 ///
1078 /// The closure should create the bind group that contains the source texture of a post
1079 /// processing effect
1080 pub fn new(create_bind_group: F) -> Self {
1081 Self { create_bind_group }
1082 }
1083
1084 /// Generates the bind group cache based on the `Self::create_bind_group` closure
1085 pub fn generate_bind_groups(&self, view_target: &ViewTarget) -> PostProcessBindGroupCache {
1086 PostProcessBindGroupCache::new(view_target, self.create_bind_group)
1087 }
1088}
1089
1090/// Caches the bind groups for each main textures used when making a post processing effect
1091pub struct PostProcessBindGroupCache {
1092 a: (TextureViewId, BindGroup),
1093 b: (TextureViewId, BindGroup),
1094}
1095
1096impl PostProcessBindGroupCache {
1097 /// Creates the bind group for both main textures used in post processing effects
1098 pub fn new<F: CreatePostProcessBindGroup>(
1099 view_target: &ViewTarget,
1100 create_bind_group: F,
1101 ) -> Self {
1102 Self {
1103 a: create_bind_group(view_target.main_texture_view()),
1104 b: create_bind_group(view_target.main_texture_other_view()),
1105 }
1106 }
1107
1108 /// Determines if the bind groups need to be updated based on the main textures id
1109 ///
1110 /// If you have anything else that determines if the bind groups need to be
1111 /// updated you need to do it manually
1112 pub fn should_update(&self, view_target: &ViewTarget) -> bool {
1113 let (texture_a, _) = &self.a;
1114 if *texture_a != view_target.main_texture_view().id() {
1115 return true;
1116 }
1117 let (texture_b, _) = &self.b;
1118 if *texture_b != view_target.main_texture_other_view().id() {
1119 return true;
1120 }
1121
1122 false
1123 }
1124
1125 /// Updates the bind group associated with each main textures
1126 ///
1127 /// Use the builder to get the callback used when creating the bind group
1128 pub fn update<F: CreatePostProcessBindGroup>(
1129 &mut self,
1130 view_target: &ViewTarget,
1131 builder: PostProcessBindGroupCacheBuilder<F>,
1132 ) {
1133 self.a = (builder.create_bind_group)(view_target.main_texture_view());
1134 self.b = (builder.create_bind_group)(view_target.main_texture_other_view());
1135 }
1136
1137 /// Gets the bind group associated with the current main texture
1138 ///
1139 /// Generally, this will be the bind group associated with the source texture
1140 pub fn get_current_bind_group(&self, texture: &TextureView) -> &BindGroup {
1141 let (_, bind_group) = if self.a.0 == texture.id() {
1142 &self.a
1143 } else {
1144 &self.b
1145 };
1146 bind_group
1147 }
1148}
1149
1150#[derive(Component)]
1151pub struct ViewDepthStencilTexture {
1152 pub attachment: DepthStencilAttachment,
1153}
1154
1155impl ViewDepthStencilTexture {
1156 pub fn new(
1157 texture: CachedTexture,
1158 depth_clear_value: Option<f32>,
1159 stencil_clear_value: Option<u32>,
1160 ) -> Self {
1161 let attachment =
1162 DepthStencilAttachment::new(texture, None, depth_clear_value, stencil_clear_value);
1163 Self { attachment }
1164 }
1165
1166 pub fn texture(&self) -> &Texture {
1167 &self.attachment.texture.texture
1168 }
1169
1170 pub fn get_attachment(&self, store: StoreOp) -> RenderPassDepthStencilAttachment<'_> {
1171 self.attachment.get_attachment(store)
1172 }
1173}
1174
1175pub fn prepare_view_uniforms(
1176 mut commands: Commands,
1177 render_device: Res<RenderDevice>,
1178 render_queue: Res<RenderQueue>,
1179 mut view_uniforms: ResMut<ViewUniforms>,
1180 views: Query<(
1181 Entity,
1182 Option<&ExtractedCamera>,
1183 &ExtractedView,
1184 Option<&Frustum>,
1185 Option<&TemporalJitter>,
1186 Option<&MipBias>,
1187 Option<&MainPassResolutionOverride>,
1188 )>,
1189 frame_count: Res<FrameCount>,
1190 shadow_lod_origin: Option<Res<RenderShadowLodOrigin>>,
1191) {
1192 let view_iter = views.iter();
1193 let view_count = view_iter.len();
1194 let Some(mut writer) =
1195 view_uniforms
1196 .uniforms
1197 .get_writer(view_count, &render_device, &render_queue)
1198 else {
1199 return;
1200 };
1201 for (
1202 entity,
1203 extracted_camera,
1204 extracted_view,
1205 frustum,
1206 temporal_jitter,
1207 mip_bias,
1208 resolution_override,
1209 ) in &views
1210 {
1211 let viewport = extracted_view.viewport.as_vec4();
1212 let mut main_pass_viewport = viewport;
1213 if let Some(resolution_override) = resolution_override {
1214 main_pass_viewport.z = resolution_override.0.x as f32;
1215 main_pass_viewport.w = resolution_override.0.y as f32;
1216 }
1217
1218 let unjittered_projection = extracted_view.clip_from_view;
1219 let mut clip_from_view = unjittered_projection;
1220
1221 if let Some(temporal_jitter) = temporal_jitter {
1222 temporal_jitter.jitter_projection(&mut clip_from_view, main_pass_viewport.zw());
1223 }
1224
1225 let view_from_clip = clip_from_view.inverse();
1226 let world_from_view = extracted_view.world_from_view.to_matrix();
1227 let view_from_world = world_from_view.inverse();
1228
1229 let clip_from_world = if temporal_jitter.is_some() {
1230 clip_from_view * view_from_world
1231 } else {
1232 extracted_view
1233 .clip_from_world
1234 .unwrap_or_else(|| clip_from_view * view_from_world)
1235 };
1236
1237 // Map Frustum type to shader array<vec4<f32>, 6>
1238 let frustum = frustum
1239 .map(|frustum| frustum.half_spaces.map(|h| h.normal_d()))
1240 .unwrap_or([Vec4::ZERO; 6]);
1241
1242 // Determine the position of the camera used for resolving visibility
1243 // ranges (LODs).
1244 let lod_view_world_position = match (&extracted_camera, &shadow_lod_origin) {
1245 (Some(_), _) | (None, None) => {
1246 // If we're rendering a camera directly (i.e. we're not
1247 // rendering a shadow map), we use this camera's position as the
1248 // LOD view position.
1249 extracted_view.world_from_view.translation()
1250 }
1251 (None, Some(shadow_lod_origin))
1252 if extracted_view
1253 .retained_view_entity
1254 .auxiliary_entity
1255 .is_none() =>
1256 {
1257 // If this is a shadow map not associated with a camera (a point
1258 // light or spot light shadow map), use the shadow LOD origin.
1259 shadow_lod_origin.0
1260 }
1261 (None, Some(shadow_lod_origin)) => {
1262 // Otherwise, if we're rendering a shadow map that is associated
1263 // with a camera (i.e. a directional light shadow map, at
1264 // present), we use the position of that camera as the LOD view
1265 // position. This ensures that each rendered object has a shadow
1266 // and that no invisible objects have shadows.
1267 match extracted_view
1268 .retained_view_entity
1269 .auxiliary_entity
1270 .and_then(|entity| views.get(*entity).ok())
1271 {
1272 Some((_, _, camera_view, _, _, _, _)) => {
1273 camera_view.world_from_view.translation()
1274 }
1275 None => shadow_lod_origin.0,
1276 }
1277 }
1278 };
1279
1280 let view_uniforms = ViewUniformOffset {
1281 offset: writer.write(&ViewUniform {
1282 clip_from_world,
1283 unjittered_clip_from_world: unjittered_projection * view_from_world,
1284 world_from_clip: world_from_view * view_from_clip,
1285 world_from_view,
1286 view_from_world,
1287 clip_from_view,
1288 view_from_clip,
1289 world_position: extracted_view.world_from_view.translation(),
1290 exposure: extracted_camera
1291 .map(|c| c.exposure)
1292 .unwrap_or_else(|| Exposure::default().exposure()),
1293 viewport,
1294 main_pass_viewport,
1295 frustum,
1296 lod_view_world_position,
1297 color_grading: extracted_view.color_grading.clone().into(),
1298 mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0,
1299 frame_count: frame_count.0,
1300 }),
1301 };
1302
1303 commands.entity(entity).insert(view_uniforms);
1304 }
1305}
1306
1307#[derive(Clone)]
1308struct MainTargetTextures {
1309 a: ColorAttachment,
1310 b: ColorAttachment,
1311 /// 0 represents `main_textures.a`, 1 represents `main_textures.b`
1312 /// This is shared across view targets with the same render target
1313 main_texture: Arc<AtomicUsize>,
1314}
1315
1316/// Prepares the view target [`OutputColorAttachment`] for each view in the current frame.
1317pub fn prepare_view_attachments(
1318 images: Res<RenderAssets<GpuImage>>,
1319 manual_texture_views: Res<ManualTextureViews>,
1320 cameras: Query<&ExtractedCamera>,
1321 mut view_target_attachments: ResMut<ViewTargetAttachments>,
1322 windows: Query<(MainEntity, &ExtractedWindow)>,
1323) {
1324 for camera in cameras.iter() {
1325 let Some(target) = &camera.target else {
1326 continue;
1327 };
1328
1329 if matches!(camera.output_mode, bevy_camera::CameraOutputMode::Skip) {
1330 continue;
1331 }
1332
1333 match view_target_attachments.entry(target.clone()) {
1334 Entry::Occupied(_) => {}
1335 Entry::Vacant(entry) => {
1336 let Some(attachment) = target
1337 .get_texture_view(&windows, &images, &manual_texture_views)
1338 .cloned()
1339 .zip(target.get_texture_view_format(&windows, &images, &manual_texture_views))
1340 .map(|(view, format)| OutputColorAttachment::new(view.clone(), format))
1341 else {
1342 continue;
1343 };
1344 entry.insert(attachment);
1345 }
1346 };
1347 }
1348}
1349
1350/// Clears the view target [`OutputColorAttachment`]s.
1351pub fn clear_view_attachments(mut view_target_attachments: ResMut<ViewTargetAttachments>) {
1352 view_target_attachments.clear();
1353}
1354
1355pub fn cleanup_view_targets_for_resize(
1356 mut commands: Commands,
1357 windows: Query<(MainEntity, &ExtractedWindow)>,
1358 cameras: Query<(Entity, &ExtractedCamera), With<ViewTarget>>,
1359) {
1360 for (entity, camera) in &cameras {
1361 if let Some(NormalizedRenderTarget::Window(window_ref)) = &camera.target
1362 && let Some((_, window)) = windows.iter().find(|(e, _)| *e == window_ref.entity())
1363 && (window.size_changed || window.present_mode_changed)
1364 {
1365 commands.entity(entity).remove::<ViewTarget>();
1366 }
1367 }
1368}
1369
1370/// The settings that decide which cameras share main textures. Cameras with
1371/// equal keys share one allocation in [`prepare_view_targets`], and
1372/// [`resolve_composition_spaces`] groups them the same way.
1373type MainTextureKey = (
1374 Option<NormalizedRenderTarget>,
1375 TextureUsages,
1376 TextureFormat,
1377 Msaa,
1378);
1379
1380fn main_texture_key(
1381 camera: &ExtractedCamera,
1382 view: &ExtractedView,
1383 texture_usage: &CameraMainTextureUsages,
1384 msaa: Msaa,
1385) -> MainTextureKey {
1386 (
1387 camera.target.clone(),
1388 texture_usage.0,
1389 view.target_format,
1390 msaa,
1391 )
1392}
1393
1394pub fn prepare_view_targets(
1395 mut commands: Commands,
1396 clear_color_global: Res<ClearColor>,
1397 render_device: Res<RenderDevice>,
1398 mut texture_cache: ResMut<TextureCache>,
1399 cameras: Query<(
1400 Entity,
1401 &ExtractedCamera,
1402 &ExtractedView,
1403 &CameraMainTextureUsages,
1404 &Msaa,
1405 Option<&ResolvedCompositingSpace>,
1406 )>,
1407 view_target_attachments: Res<ViewTargetAttachments>,
1408 mut main_texture_atomics: Local<HashMap<MainTextureKey, Weak<AtomicUsize>>>,
1409) {
1410 main_texture_atomics.retain(|_, weak| weak.strong_count() > 0);
1411
1412 let mut textures = <HashMap<_, _>>::default();
1413 for (entity, camera, view, texture_usage, msaa, resolved_space) in cameras.iter() {
1414 let Some(target_size) = camera.physical_target_size else {
1415 // If we don't have a target size, we can't create the main texture and have to bail
1416 commands.entity(entity).try_remove::<ViewTarget>();
1417 continue;
1418 };
1419
1420 let out_attachment = camera
1421 .target
1422 .as_ref()
1423 .and_then(|target| view_target_attachments.get(target));
1424
1425 // If we have no output and the camera is set to clear, we can skip rendering
1426 // entirely.
1427 if out_attachment.is_none() && !matches!(camera.clear_color, ClearColorConfig::None) {
1428 commands.entity(entity).try_remove::<ViewTarget>();
1429 continue;
1430 }
1431
1432 let main_texture_format = view.target_format;
1433
1434 let clear_color = match camera.clear_color {
1435 ClearColorConfig::Custom(color) => Some(color),
1436 ClearColorConfig::None => None,
1437 _ => Some(clear_color_global.0),
1438 };
1439
1440 // Convert clear color to the format expected by the main texture
1441 let resolved_space = ResolvedCompositingSpace::space(resolved_space);
1442 let converted_clear_color: Option<WgpuColor> =
1443 clear_color.map(|color| match resolved_space {
1444 // If main texture stores Oklab or Srgb, convert Color to it for correct clear.
1445 Some(CompositingSpace::Oklab) => Oklaba::from(color).into(),
1446 Some(CompositingSpace::Srgb) => Srgba::from(color).into(),
1447 Some(CompositingSpace::Linear) | None => LinearRgba::from(color).into(),
1448 });
1449
1450 let key = main_texture_key(camera, view, texture_usage, *msaa);
1451 let (a, b, sampled, main_texture) = textures.entry(key.clone()).or_insert_with(|| {
1452 let descriptor = TextureDescriptor {
1453 label: None,
1454 size: target_size.to_extents(),
1455 mip_level_count: 1,
1456 sample_count: 1,
1457 dimension: TextureDimension::D2,
1458 format: main_texture_format,
1459 usage: texture_usage.0,
1460 view_formats: match main_texture_format {
1461 TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb],
1462 TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb],
1463 _ => &[],
1464 },
1465 };
1466 let a = texture_cache.get(
1467 &render_device,
1468 TextureDescriptor {
1469 label: Some("main_texture_a"),
1470 ..descriptor
1471 },
1472 );
1473 let b = texture_cache.get(
1474 &render_device,
1475 TextureDescriptor {
1476 label: Some("main_texture_b"),
1477 ..descriptor
1478 },
1479 );
1480 let sampled = if msaa.samples() > 1 {
1481 let sampled = texture_cache.get(
1482 &render_device,
1483 TextureDescriptor {
1484 label: Some("main_texture_sampled"),
1485 size: target_size.to_extents(),
1486 mip_level_count: 1,
1487 sample_count: msaa.samples(),
1488 dimension: TextureDimension::D2,
1489 format: main_texture_format,
1490 usage: TextureUsages::RENDER_ATTACHMENT,
1491 view_formats: descriptor.view_formats,
1492 },
1493 );
1494 Some(sampled)
1495 } else {
1496 None
1497 };
1498 // re-use the same atomics frame to frame for views with the same main texture
1499 // to ensure post process writes persist through msaa writeback
1500 let main_texture = match main_texture_atomics.entry(key) {
1501 Entry::Occupied(e) => e
1502 .get()
1503 .upgrade()
1504 .expect("dead weaks were pruned at top of system"),
1505 Entry::Vacant(e) => {
1506 let arc = Arc::new(AtomicUsize::new(0));
1507 e.insert(Arc::downgrade(&arc));
1508 arc
1509 }
1510 };
1511 (a, b, sampled, main_texture)
1512 });
1513
1514 let main_textures = MainTargetTextures {
1515 a: ColorAttachment::new(a.clone(), sampled.clone(), None, converted_clear_color),
1516 b: ColorAttachment::new(b.clone(), sampled.clone(), None, converted_clear_color),
1517 main_texture: main_texture.clone(),
1518 };
1519
1520 commands.entity(entity).insert(ViewTarget {
1521 main_texture: main_textures.main_texture.clone(),
1522 main_textures,
1523 main_texture_format,
1524 out_texture: out_attachment.cloned(),
1525 });
1526 }
1527}