pub mod visibility;
pub mod window;
use bevy_asset::{load_internal_asset, Handle};
pub use visibility::*;
pub use window::*;
use crate::{
camera::{
CameraMainTextureUsages, ClearColor, ClearColorConfig, Exposure, ExtractedCamera,
ManualTextureViews, MipBias, NormalizedRenderTarget, TemporalJitter,
},
extract_component::ExtractComponentPlugin,
prelude::Shader,
primitives::Frustum,
render_asset::RenderAssets,
render_phase::ViewRangefinder3d,
render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView},
renderer::{RenderDevice, RenderQueue},
texture::{
CachedTexture, ColorAttachment, DepthAttachment, GpuImage, OutputColorAttachment,
TextureCache,
},
Render, RenderApp, RenderSet,
};
use alloc::sync::Arc;
use bevy_app::{App, Plugin};
use bevy_color::LinearRgba;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use bevy_image::BevyDefault as _;
use bevy_math::{mat3, vec2, vec3, Mat3, Mat4, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render_macros::ExtractComponent;
use bevy_transform::components::GlobalTransform;
use bevy_utils::{hashbrown::hash_map::Entry, HashMap};
use core::{
ops::Range,
sync::atomic::{AtomicUsize, Ordering},
};
use wgpu::{
BufferUsages, Extent3d, RenderPassColorAttachment, RenderPassDepthStencilAttachment, StoreOp,
TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
};
pub const VIEW_TYPE_HANDLE: Handle<Shader> = Handle::weak_from_u128(15421373904451797197);
static RGB_TO_LMS: Mat3 = mat3(
vec3(0.311692, 0.0905138, 0.00764433),
vec3(0.652085, 0.901341, 0.0486554),
vec3(0.0362225, 0.00814478, 0.943700),
);
static LMS_TO_RGB: Mat3 = mat3(
vec3(4.06305, -0.40791, -0.0118812),
vec3(-2.93241, 1.40437, -0.0486532),
vec3(-0.130646, 0.00353630, 1.0605344),
);
static D65_XY: Vec2 = vec2(0.31272, 0.32903);
static D65_LMS: Vec3 = vec3(0.975538, 1.01648, 1.08475);
pub struct ViewPlugin;
impl Plugin for ViewPlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(app, VIEW_TYPE_HANDLE, "view.wgsl", Shader::from_wgsl);
app.register_type::<InheritedVisibility>()
.register_type::<ViewVisibility>()
.register_type::<Msaa>()
.register_type::<NoFrustumCulling>()
.register_type::<RenderLayers>()
.register_type::<Visibility>()
.register_type::<VisibleEntities>()
.register_type::<ColorGrading>()
.add_plugins((
ExtractComponentPlugin::<Msaa>::default(),
VisibilityPlugin,
VisibilityRangePlugin,
));
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.add_systems(
Render,
(
clear_view_attachments
.in_set(RenderSet::ManageViews)
.before(create_surfaces),
prepare_view_attachments
.in_set(RenderSet::ManageViews)
.before(prepare_view_targets)
.after(prepare_windows),
prepare_view_targets
.in_set(RenderSet::ManageViews)
.after(prepare_windows)
.after(crate::render_asset::prepare_assets::<GpuImage>)
.ambiguous_with(crate::camera::sort_cameras), prepare_view_uniforms.in_set(RenderSet::PrepareResources),
),
);
}
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<ViewUniforms>()
.init_resource::<ViewTargetAttachments>();
}
}
}
#[derive(
Component,
Default,
Clone,
Copy,
ExtractComponent,
Reflect,
PartialEq,
PartialOrd,
Eq,
Hash,
Debug,
)]
#[reflect(Component, Default, PartialEq, Hash, Debug)]
pub enum Msaa {
Off = 1,
Sample2 = 2,
#[default]
Sample4 = 4,
Sample8 = 8,
}
impl Msaa {
#[inline]
pub fn samples(&self) -> u32 {
*self as u32
}
}
#[derive(Component)]
pub struct ExtractedView {
pub clip_from_view: Mat4,
pub world_from_view: GlobalTransform,
pub clip_from_world: Option<Mat4>,
pub hdr: bool,
pub viewport: UVec4,
pub color_grading: ColorGrading,
}
impl ExtractedView {
pub fn rangefinder3d(&self) -> ViewRangefinder3d {
ViewRangefinder3d::from_world_from_view(&self.world_from_view.compute_matrix())
}
}
#[derive(Component, Reflect, Debug, Default, Clone)]
#[reflect(Component, Default, Debug)]
pub struct ColorGrading {
pub global: ColorGradingGlobal,
pub shadows: ColorGradingSection,
pub midtones: ColorGradingSection,
pub highlights: ColorGradingSection,
}
#[derive(Clone, Debug, Reflect)]
#[reflect(Default)]
pub struct ColorGradingGlobal {
pub exposure: f32,
pub temperature: f32,
pub tint: f32,
pub hue: f32,
pub post_saturation: f32,
pub midtones_range: Range<f32>,
}
#[derive(Clone, Copy, Debug, ShaderType)]
pub struct ColorGradingUniform {
pub balance: Mat3,
pub saturation: Vec3,
pub contrast: Vec3,
pub gamma: Vec3,
pub gain: Vec3,
pub lift: Vec3,
pub midtone_range: Vec2,
pub exposure: f32,
pub hue: f32,
pub post_saturation: f32,
}
#[derive(Reflect, Debug, Copy, Clone, PartialEq)]
pub struct ColorGradingSection {
pub saturation: f32,
pub contrast: f32,
pub gamma: f32,
pub gain: f32,
pub lift: f32,
}
impl Default for ColorGradingGlobal {
fn default() -> Self {
Self {
exposure: 0.0,
temperature: 0.0,
tint: 0.0,
hue: 0.0,
post_saturation: 1.0,
midtones_range: 0.2..0.7,
}
}
}
impl Default for ColorGradingSection {
fn default() -> Self {
Self {
saturation: 1.0,
contrast: 1.0,
gamma: 1.0,
gain: 1.0,
lift: 0.0,
}
}
}
impl ColorGrading {
pub fn with_identical_sections(
global: ColorGradingGlobal,
section: ColorGradingSection,
) -> ColorGrading {
ColorGrading {
global,
highlights: section,
midtones: section,
shadows: section,
}
}
pub fn all_sections(&self) -> impl Iterator<Item = &ColorGradingSection> {
[&self.shadows, &self.midtones, &self.highlights].into_iter()
}
pub fn all_sections_mut(&mut self) -> impl Iterator<Item = &mut ColorGradingSection> {
[&mut self.shadows, &mut self.midtones, &mut self.highlights].into_iter()
}
}
#[derive(Clone, ShaderType)]
pub struct ViewUniform {
pub clip_from_world: Mat4,
pub unjittered_clip_from_world: Mat4,
pub world_from_clip: Mat4,
pub world_from_view: Mat4,
pub view_from_world: Mat4,
pub clip_from_view: Mat4,
pub view_from_clip: Mat4,
pub world_position: Vec3,
pub exposure: f32,
pub viewport: Vec4,
pub frustum: [Vec4; 6],
pub color_grading: ColorGradingUniform,
pub mip_bias: f32,
}
#[derive(Resource)]
pub struct ViewUniforms {
pub uniforms: DynamicUniformBuffer<ViewUniform>,
}
impl FromWorld for ViewUniforms {
fn from_world(world: &mut World) -> Self {
let mut uniforms = DynamicUniformBuffer::default();
uniforms.set_label(Some("view_uniforms_buffer"));
let render_device = world.resource::<RenderDevice>();
if render_device.limits().max_storage_buffers_per_shader_stage > 0 {
uniforms.add_usages(BufferUsages::STORAGE);
}
Self { uniforms }
}
}
#[derive(Component)]
pub struct ViewUniformOffset {
pub offset: u32,
}
#[derive(Component)]
pub struct ViewTarget {
main_textures: MainTargetTextures,
main_texture_format: TextureFormat,
main_texture: Arc<AtomicUsize>,
out_texture: OutputColorAttachment,
}
#[derive(Resource, Default, Deref, DerefMut)]
pub struct ViewTargetAttachments(HashMap<NormalizedRenderTarget, OutputColorAttachment>);
pub struct PostProcessWrite<'a> {
pub source: &'a TextureView,
pub destination: &'a TextureView,
}
impl From<ColorGrading> for ColorGradingUniform {
fn from(component: ColorGrading) -> Self {
let white_point_xy = D65_XY + vec2(-component.global.temperature, component.global.tint);
let white_point_lms = vec3(0.701634, 1.15856, -0.904175)
+ (vec3(-0.051461, 0.045854, 0.953127)
+ vec3(0.452749, -0.296122, -0.955206) * white_point_xy.x)
/ white_point_xy.y;
let white_point_adjustment = Mat3::from_diagonal(D65_LMS / white_point_lms);
let balance = LMS_TO_RGB * white_point_adjustment * RGB_TO_LMS;
Self {
balance,
saturation: vec3(
component.shadows.saturation,
component.midtones.saturation,
component.highlights.saturation,
),
contrast: vec3(
component.shadows.contrast,
component.midtones.contrast,
component.highlights.contrast,
),
gamma: vec3(
component.shadows.gamma,
component.midtones.gamma,
component.highlights.gamma,
),
gain: vec3(
component.shadows.gain,
component.midtones.gain,
component.highlights.gain,
),
lift: vec3(
component.shadows.lift,
component.midtones.lift,
component.highlights.lift,
),
midtone_range: vec2(
component.global.midtones_range.start,
component.global.midtones_range.end,
),
exposure: component.global.exposure,
hue: component.global.hue,
post_saturation: component.global.post_saturation,
}
}
}
#[derive(Component)]
pub struct GpuCulling;
#[derive(Component)]
pub struct NoCpuCulling;
impl ViewTarget {
pub const TEXTURE_FORMAT_HDR: TextureFormat = TextureFormat::Rgba16Float;
pub fn get_color_attachment(&self) -> RenderPassColorAttachment {
if self.main_texture.load(Ordering::SeqCst) == 0 {
self.main_textures.a.get_attachment()
} else {
self.main_textures.b.get_attachment()
}
}
pub fn get_unsampled_color_attachment(&self) -> RenderPassColorAttachment {
if self.main_texture.load(Ordering::SeqCst) == 0 {
self.main_textures.a.get_unsampled_attachment()
} else {
self.main_textures.b.get_unsampled_attachment()
}
}
pub fn main_texture(&self) -> &Texture {
if self.main_texture.load(Ordering::SeqCst) == 0 {
&self.main_textures.a.texture.texture
} else {
&self.main_textures.b.texture.texture
}
}
pub fn main_texture_other(&self) -> &Texture {
if self.main_texture.load(Ordering::SeqCst) == 0 {
&self.main_textures.b.texture.texture
} else {
&self.main_textures.a.texture.texture
}
}
pub fn main_texture_view(&self) -> &TextureView {
if self.main_texture.load(Ordering::SeqCst) == 0 {
&self.main_textures.a.texture.default_view
} else {
&self.main_textures.b.texture.default_view
}
}
pub fn main_texture_other_view(&self) -> &TextureView {
if self.main_texture.load(Ordering::SeqCst) == 0 {
&self.main_textures.b.texture.default_view
} else {
&self.main_textures.a.texture.default_view
}
}
pub fn sampled_main_texture(&self) -> Option<&Texture> {
self.main_textures
.a
.resolve_target
.as_ref()
.map(|sampled| &sampled.texture)
}
pub fn sampled_main_texture_view(&self) -> Option<&TextureView> {
self.main_textures
.a
.resolve_target
.as_ref()
.map(|sampled| &sampled.default_view)
}
#[inline]
pub fn main_texture_format(&self) -> TextureFormat {
self.main_texture_format
}
#[inline]
pub fn is_hdr(&self) -> bool {
self.main_texture_format == ViewTarget::TEXTURE_FORMAT_HDR
}
#[inline]
pub fn out_texture(&self) -> &TextureView {
&self.out_texture.view
}
pub fn out_texture_color_attachment(
&self,
clear_color: Option<LinearRgba>,
) -> RenderPassColorAttachment {
self.out_texture.get_attachment(clear_color)
}
#[inline]
pub fn out_texture_format(&self) -> TextureFormat {
self.out_texture.format
}
pub fn post_process_write(&self) -> PostProcessWrite {
let old_is_a_main_texture = self.main_texture.fetch_xor(1, Ordering::SeqCst);
if old_is_a_main_texture == 0 {
self.main_textures.b.mark_as_cleared();
PostProcessWrite {
source: &self.main_textures.a.texture.default_view,
destination: &self.main_textures.b.texture.default_view,
}
} else {
self.main_textures.a.mark_as_cleared();
PostProcessWrite {
source: &self.main_textures.b.texture.default_view,
destination: &self.main_textures.a.texture.default_view,
}
}
}
}
#[derive(Component)]
pub struct ViewDepthTexture {
pub texture: Texture,
attachment: DepthAttachment,
}
impl ViewDepthTexture {
pub fn new(texture: CachedTexture, clear_value: Option<f32>) -> Self {
Self {
texture: texture.texture,
attachment: DepthAttachment::new(texture.default_view, clear_value),
}
}
pub fn get_attachment(&self, store: StoreOp) -> RenderPassDepthStencilAttachment {
self.attachment.get_attachment(store)
}
pub fn view(&self) -> &TextureView {
&self.attachment.view
}
}
pub fn prepare_view_uniforms(
mut commands: Commands,
render_device: Res<RenderDevice>,
render_queue: Res<RenderQueue>,
mut view_uniforms: ResMut<ViewUniforms>,
views: Query<(
Entity,
Option<&ExtractedCamera>,
&ExtractedView,
Option<&Frustum>,
Option<&TemporalJitter>,
Option<&MipBias>,
)>,
) {
let view_iter = views.iter();
let view_count = view_iter.len();
let Some(mut writer) =
view_uniforms
.uniforms
.get_writer(view_count, &render_device, &render_queue)
else {
return;
};
for (entity, extracted_camera, extracted_view, frustum, temporal_jitter, mip_bias) in &views {
let viewport = extracted_view.viewport.as_vec4();
let unjittered_projection = extracted_view.clip_from_view;
let mut clip_from_view = unjittered_projection;
if let Some(temporal_jitter) = temporal_jitter {
temporal_jitter.jitter_projection(&mut clip_from_view, viewport.zw());
}
let view_from_clip = clip_from_view.inverse();
let world_from_view = extracted_view.world_from_view.compute_matrix();
let view_from_world = world_from_view.inverse();
let clip_from_world = if temporal_jitter.is_some() {
clip_from_view * view_from_world
} else {
extracted_view
.clip_from_world
.unwrap_or_else(|| clip_from_view * view_from_world)
};
let frustum = frustum
.map(|frustum| frustum.half_spaces.map(|h| h.normal_d()))
.unwrap_or([Vec4::ZERO; 6]);
let view_uniforms = ViewUniformOffset {
offset: writer.write(&ViewUniform {
clip_from_world,
unjittered_clip_from_world: unjittered_projection * view_from_world,
world_from_clip: world_from_view * view_from_clip,
world_from_view,
view_from_world,
clip_from_view,
view_from_clip,
world_position: extracted_view.world_from_view.translation(),
exposure: extracted_camera
.map(|c| c.exposure)
.unwrap_or_else(|| Exposure::default().exposure()),
viewport,
frustum,
color_grading: extracted_view.color_grading.clone().into(),
mip_bias: mip_bias.unwrap_or(&MipBias(0.0)).0,
}),
};
commands.entity(entity).insert(view_uniforms);
}
}
#[derive(Clone)]
struct MainTargetTextures {
a: ColorAttachment,
b: ColorAttachment,
main_texture: Arc<AtomicUsize>,
}
pub fn prepare_view_attachments(
windows: Res<ExtractedWindows>,
images: Res<RenderAssets<GpuImage>>,
manual_texture_views: Res<ManualTextureViews>,
cameras: Query<&ExtractedCamera>,
mut view_target_attachments: ResMut<ViewTargetAttachments>,
) {
for camera in cameras.iter() {
let Some(target) = &camera.target else {
continue;
};
match view_target_attachments.entry(target.clone()) {
Entry::Occupied(_) => {}
Entry::Vacant(entry) => {
let Some(attachment) = target
.get_texture_view(&windows, &images, &manual_texture_views)
.cloned()
.zip(target.get_texture_format(&windows, &images, &manual_texture_views))
.map(|(view, format)| {
OutputColorAttachment::new(view.clone(), format.add_srgb_suffix())
})
else {
continue;
};
entry.insert(attachment);
}
};
}
}
pub fn clear_view_attachments(mut view_target_attachments: ResMut<ViewTargetAttachments>) {
view_target_attachments.clear();
}
pub fn prepare_view_targets(
mut commands: Commands,
clear_color_global: Res<ClearColor>,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
cameras: Query<(
Entity,
&ExtractedCamera,
&ExtractedView,
&CameraMainTextureUsages,
&Msaa,
)>,
view_target_attachments: Res<ViewTargetAttachments>,
) {
let mut textures = HashMap::default();
for (entity, camera, view, texture_usage, msaa) in cameras.iter() {
let (Some(target_size), Some(target)) = (camera.physical_target_size, &camera.target)
else {
continue;
};
let Some(out_attachment) = view_target_attachments.get(target) else {
continue;
};
let size = Extent3d {
width: target_size.x,
height: target_size.y,
depth_or_array_layers: 1,
};
let main_texture_format = if view.hdr {
ViewTarget::TEXTURE_FORMAT_HDR
} else {
TextureFormat::bevy_default()
};
let clear_color = match camera.clear_color {
ClearColorConfig::Custom(color) => Some(color),
ClearColorConfig::None => None,
_ => Some(clear_color_global.0),
};
let (a, b, sampled, main_texture) = textures
.entry((camera.target.clone(), view.hdr, msaa))
.or_insert_with(|| {
let descriptor = TextureDescriptor {
label: None,
size,
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: main_texture_format,
usage: texture_usage.0,
view_formats: match main_texture_format {
TextureFormat::Bgra8Unorm => &[TextureFormat::Bgra8UnormSrgb],
TextureFormat::Rgba8Unorm => &[TextureFormat::Rgba8UnormSrgb],
_ => &[],
},
};
let a = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("main_texture_a"),
..descriptor
},
);
let b = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("main_texture_b"),
..descriptor
},
);
let sampled = if msaa.samples() > 1 {
let sampled = texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("main_texture_sampled"),
size,
mip_level_count: 1,
sample_count: msaa.samples(),
dimension: TextureDimension::D2,
format: main_texture_format,
usage: TextureUsages::RENDER_ATTACHMENT,
view_formats: descriptor.view_formats,
},
);
Some(sampled)
} else {
None
};
let main_texture = Arc::new(AtomicUsize::new(0));
(a, b, sampled, main_texture)
});
let converted_clear_color = clear_color.map(Into::into);
let main_textures = MainTargetTextures {
a: ColorAttachment::new(a.clone(), sampled.clone(), converted_clear_color),
b: ColorAttachment::new(b.clone(), sampled.clone(), converted_clear_color),
main_texture: main_texture.clone(),
};
commands.entity(entity).insert(ViewTarget {
main_texture: main_textures.main_texture.clone(),
main_textures,
main_texture_format,
out_texture: out_attachment.clone(),
});
}
}