diff --git a/pipelined/bevy_core_pipeline/src/main_pass_3d.rs b/pipelined/bevy_core_pipeline/src/main_pass_3d.rs index b63bb80949b2b..a539d6279d80a 100644 --- a/pipelined/bevy_core_pipeline/src/main_pass_3d.rs +++ b/pipelined/bevy_core_pipeline/src/main_pass_3d.rs @@ -59,7 +59,7 @@ impl Node for MainPass3dNode { let pass_descriptor = RenderPassDescriptor { label: Some("main_opaque_pass_3d"), // NOTE: The opaque pass clears and initializes the color - // buffer as well as writing to it. + // buffer as well as writing to it. color_attachments: &[target.get_color_attachment(Operations { load: LoadOp::Clear(clear_color.0.into()), store: true, @@ -135,8 +135,8 @@ impl Node for MainPass3dNode { depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { view: &depth.view, // NOTE: For the transparent pass we load the depth buffer but do not write to it. - // As the opaque and alpha mask passes run first, opaque meshes can occlude - // transparent ones. + // As the opaque and alpha mask passes run first, opaque meshes can occlude + // transparent ones. depth_ops: Some(Operations { load: LoadOp::Load, store: false, diff --git a/pipelined/bevy_pbr2/Cargo.toml b/pipelined/bevy_pbr2/Cargo.toml index d4c238d6eed0c..6f10fe814b70f 100644 --- a/pipelined/bevy_pbr2/Cargo.toml +++ b/pipelined/bevy_pbr2/Cargo.toml @@ -24,6 +24,7 @@ bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features bevy_render2 = { path = "../bevy_render2", version = "0.5.0" } bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" } bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" } +bevy_window = { path = "../../crates/bevy_window", version = "0.5.0" } # other bitflags = "1.2" diff --git a/pipelined/bevy_pbr2/src/lib.rs b/pipelined/bevy_pbr2/src/lib.rs index 466345b086176..96a26b29e12b9 100644 --- a/pipelined/bevy_pbr2/src/lib.rs +++ b/pipelined/bevy_pbr2/src/lib.rs @@ -60,6 +60,29 @@ impl Plugin for PbrPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() + .add_system_to_stage( + CoreStage::PostUpdate, + // NOTE: Clusters need to have been added before update_clusters is run so + // add as an exclusive system + add_clusters + .exclusive_system() + .label(SimulationLightSystems::AddClusters), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + // NOTE: Must come after add_clusters! + update_clusters + .label(SimulationLightSystems::UpdateClusters) + .after(TransformSystem::TransformPropagate), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + assign_lights_to_clusters + .label(SimulationLightSystems::AssignLightsToClusters) + .after(TransformSystem::TransformPropagate) + .after(SimulationLightSystems::UpdateClusters), + ) .add_system_to_stage( CoreStage::PostUpdate, update_directional_light_frusta @@ -70,11 +93,12 @@ impl Plugin for PbrPlugin { CoreStage::PostUpdate, update_point_light_frusta .label(SimulationLightSystems::UpdatePointLightFrusta) - .after(TransformSystem::TransformPropagate), + .after(TransformSystem::TransformPropagate) + .after(SimulationLightSystems::AssignLightsToClusters), ) .add_system_to_stage( CoreStage::PostUpdate, - check_light_visibility + check_light_mesh_visibility .label(SimulationLightSystems::CheckLightVisibility) .after(TransformSystem::TransformPropagate) .after(VisibilitySystems::CalculateBounds) @@ -88,6 +112,10 @@ impl Plugin for PbrPlugin { let render_app = app.sub_app(RenderApp); render_app + .add_system_to_stage( + RenderStage::Extract, + render::extract_clusters.label(RenderLightSystems::ExtractClusters), + ) .add_system_to_stage( RenderStage::Extract, render::extract_lights.label(RenderLightSystems::ExtractLights), @@ -100,6 +128,15 @@ impl Plugin for PbrPlugin { .exclusive_system() .label(RenderLightSystems::PrepareLights), ) + .add_system_to_stage( + RenderStage::Prepare, + // this is added as an exclusive system because it contributes new views. it must run (and have Commands applied) + // _before_ the `prepare_views()` system is run. ideally this becomes a normal system when "stageless" features come out + render::prepare_clusters + .exclusive_system() + .label(RenderLightSystems::PrepareClusters) + .after(RenderLightSystems::PrepareLights), + ) .add_system_to_stage( RenderStage::Queue, render::queue_shadows.label(RenderLightSystems::QueueShadows), @@ -111,6 +148,7 @@ impl Plugin for PbrPlugin { .init_resource::() .init_resource::>() .init_resource::() + .init_resource::() .init_resource::>() .init_resource::>(); diff --git a/pipelined/bevy_pbr2/src/light.rs b/pipelined/bevy_pbr2/src/light.rs index 9144306d9907b..e42978729b2da 100644 --- a/pipelined/bevy_pbr2/src/light.rs +++ b/pipelined/bevy_pbr2/src/light.rs @@ -1,14 +1,19 @@ +use std::collections::HashSet; + use bevy_ecs::prelude::*; -use bevy_math::Mat4; +use bevy_math::{Mat4, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_render2::{ - camera::{CameraProjection, OrthographicProjection}, + camera::{Camera, CameraProjection, OrthographicProjection}, color::Color, primitives::{Aabb, CubemapFrusta, Frustum, Sphere}, - view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities, VisibleEntity}, + view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities}, }; use bevy_transform::components::GlobalTransform; +use bevy_window::Windows; -use crate::{CubeMapFace, CubemapVisibleEntities, CUBE_MAP_FACES}; +use crate::{ + CubeMapFace, CubemapVisibleEntities, ViewClusterBindings, CUBE_MAP_FACES, POINT_LIGHT_NEAR_Z, +}; /// A light that emits light in all directions from a central point. /// @@ -175,11 +180,366 @@ pub struct NotShadowReceiver; #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub enum SimulationLightSystems { + AddClusters, + UpdateClusters, + AssignLightsToClusters, UpdateDirectionalLightFrusta, UpdatePointLightFrusta, CheckLightVisibility, } +// Clustered-forward rendering notes +// The main initial reference material used was this rather accessible article: +// http://www.aortiz.me/2018/12/21/CG.html +// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of: +// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/ +// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.) +// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa’s Siggraph 2016 talk about Doom 2016: +// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf + +#[derive(Component, Debug)] +pub struct Clusters { + /// Tile size + pub(crate) tile_size: UVec2, + /// Number of clusters in x / y / z in the view frustum + pub(crate) axis_slices: UVec3, + aabbs: Vec, + pub(crate) lights: Vec, +} + +impl Clusters { + fn new(tile_size: UVec2, screen_size: UVec2, z_slices: u32) -> Self { + let mut clusters = Self { + tile_size, + axis_slices: Default::default(), + aabbs: Default::default(), + lights: Default::default(), + }; + clusters.update(tile_size, screen_size, z_slices); + clusters + } + + fn from_screen_size_and_z_slices(screen_size: UVec2, z_slices: u32) -> Self { + let aspect_ratio = screen_size.x as f32 / screen_size.y as f32; + let n_tiles_y = + ((ViewClusterBindings::MAX_OFFSETS as u32 / z_slices) as f32 / aspect_ratio).sqrt(); + // NOTE: Round down the number of tiles in order to avoid overflowing the maximum number of + // clusters. + let n_tiles = UVec2::new( + (aspect_ratio * n_tiles_y).floor() as u32, + n_tiles_y.floor() as u32, + ); + Clusters::new((screen_size + UVec2::ONE) / n_tiles, screen_size, Z_SLICES) + } + + fn update(&mut self, tile_size: UVec2, screen_size: UVec2, z_slices: u32) { + self.tile_size = tile_size; + self.axis_slices = UVec3::new( + (screen_size.x + 1) / tile_size.x, + (screen_size.y + 1) / tile_size.y, + z_slices, + ); + } +} + +fn clip_to_view(inverse_projection: Mat4, clip: Vec4) -> Vec4 { + let view = inverse_projection * clip; + view / view.w +} + +fn screen_to_view(screen_size: Vec2, inverse_projection: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 { + let tex_coord = screen / screen_size; + let clip = Vec4::new( + tex_coord.x * 2.0 - 1.0, + (1.0 - tex_coord.y) * 2.0 - 1.0, + ndc_z, + 1.0, + ); + clip_to_view(inverse_projection, clip) +} + +// Calculate the intersection of a ray from the eye through the view space position to a z plane +fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 { + let v = p - origin; + let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v); + origin + t * v +} + +fn compute_aabb_for_cluster( + z_near: f32, + z_far: f32, + tile_size: Vec2, + screen_size: Vec2, + inverse_projection: Mat4, + cluster_dimensions: UVec3, + ijk: UVec3, +) -> Aabb { + let ijk = ijk.as_vec3(); + + // Calculate the minimum and maximum points in screen space + let p_min = ijk.xy() * tile_size; + let p_max = p_min + tile_size; + + // Convert to view space at the near plane + // NOTE: 1.0 is the near plane due to using reverse z projections + let p_min = screen_to_view(screen_size, inverse_projection, p_min, 1.0); + let p_max = screen_to_view(screen_size, inverse_projection, p_max, 1.0); + + let z_far_over_z_near = -z_far / -z_near; + let cluster_near = -z_near * z_far_over_z_near.powf(ijk.z / cluster_dimensions.z as f32); + // NOTE: This could be simplified to: + // let cluster_far = cluster_near * z_far_over_z_near; + let cluster_far = -z_near * z_far_over_z_near.powf((ijk.z + 1.0) / cluster_dimensions.z as f32); + + // Calculate the four intersection points of the min and max points with the cluster near and far planes + let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near); + let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far); + let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near); + let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far); + + let cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far)); + let cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far)); + + Aabb::from_min_max(cluster_min, cluster_max) +} + +const Z_SLICES: u32 = 24; + +pub fn add_clusters( + mut commands: Commands, + windows: Res, + cameras: Query<(Entity, &Camera), Without>, +) { + for (entity, camera) in cameras.iter() { + let window = windows.get(camera.window).unwrap(); + let clusters = Clusters::from_screen_size_and_z_slices( + UVec2::new(window.physical_width(), window.physical_height()), + Z_SLICES, + ); + commands.entity(entity).insert(clusters); + } +} + +pub fn update_clusters(windows: Res, mut views: Query<(&Camera, &mut Clusters)>) { + for (camera, mut clusters) in views.iter_mut() { + let inverse_projection = camera.projection_matrix.inverse(); + let window = windows.get(camera.window).unwrap(); + let screen_size_u32 = UVec2::new(window.physical_width(), window.physical_height()); + *clusters = + Clusters::from_screen_size_and_z_slices(screen_size_u32, clusters.axis_slices.z); + let screen_size = screen_size_u32.as_vec2(); + let tile_size_u32 = clusters.tile_size; + let tile_size = tile_size_u32.as_vec2(); + + // Calculate view space AABBs + // NOTE: It is important that these are iterated in a specific order + // so that we can calculate the cluster index in the fragment shader! + // I (Rob Swain) choose to scan along rows of tiles in x,y, and for each tile then scan + // along z + let mut aabbs = Vec::with_capacity( + (clusters.axis_slices.y * clusters.axis_slices.x * clusters.axis_slices.z) as usize, + ); + for y in 0..clusters.axis_slices.y { + for x in 0..clusters.axis_slices.x { + for z in 0..clusters.axis_slices.z { + aabbs.push(compute_aabb_for_cluster( + camera.near, + camera.far, + tile_size, + screen_size, + inverse_projection, + clusters.axis_slices, + UVec3::new(x, y, z), + )); + } + } + } + clusters.aabbs = aabbs; + } +} + +#[derive(Clone, Component, Debug, Default)] +pub struct VisiblePointLights { + pub entities: Vec, +} + +impl VisiblePointLights { + pub fn from_light_count(count: usize) -> Self { + Self { + entities: Vec::with_capacity(count), + } + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + self.entities.iter() + } + + pub fn len(&self) -> usize { + self.entities.len() + } + + pub fn is_empty(&self) -> bool { + self.entities.is_empty() + } +} + +fn view_z_to_z_slice(cluster_factors: Vec2, view_z: f32) -> u32 { + // NOTE: had to use -view_z to make it positive else log(negative) is nan + ((-view_z).ln() * cluster_factors.x - cluster_factors.y).floor() as u32 +} + +fn ndc_position_to_cluster( + cluster_dimensions: UVec3, + cluster_factors: Vec2, + ndc_p: Vec3, + view_z: f32, +) -> UVec3 { + let cluster_dimensions_f32 = cluster_dimensions.as_vec3(); + let frag_coord = + (ndc_p.xy() * Vec2::new(0.5, -0.5) + Vec2::splat(0.5)).clamp(Vec2::ZERO, Vec2::ONE); + let xy = (frag_coord * cluster_dimensions_f32.xy()).floor(); + let z_slice = view_z_to_z_slice(cluster_factors, view_z); + xy.as_uvec2() + .extend(z_slice) + .clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE) +} + +fn cluster_to_index(cluster_dimensions: UVec3, cluster: UVec3) -> usize { + ((cluster.y * cluster_dimensions.x + cluster.x) * cluster_dimensions.z + cluster.z) as usize +} + +// NOTE: Run this before update_point_light_frusta! +pub fn assign_lights_to_clusters( + mut commands: Commands, + mut global_lights: ResMut, + mut views: Query<(Entity, &GlobalTransform, &Camera, &Frustum, &mut Clusters)>, + lights: Query<(Entity, &GlobalTransform, &PointLight)>, +) { + let light_count = lights.iter().count(); + let mut global_lights_set = HashSet::with_capacity(light_count); + for (view_entity, view_transform, camera, frustum, mut clusters) in views.iter_mut() { + let view_transform = view_transform.compute_matrix(); + let inverse_view_transform = view_transform.inverse(); + let cluster_count = clusters.aabbs.len(); + let z_slices_of_ln_zfar_over_znear = + clusters.axis_slices.z as f32 / (camera.far / camera.near).ln(); + let cluster_factors = Vec2::new( + z_slices_of_ln_zfar_over_znear, + camera.near.ln() * z_slices_of_ln_zfar_over_znear, + ); + + let mut clusters_lights = + vec![VisiblePointLights::from_light_count(light_count); cluster_count]; + let mut visible_lights_set = HashSet::with_capacity(light_count); + + for (light_entity, light_transform, light) in lights.iter() { + let light_sphere = Sphere { + center: light_transform.translation, + radius: light.range, + }; + + // Check if the light is within the view frustum + if !frustum.intersects_sphere(&light_sphere) { + continue; + } + + // Calculate an AABB for the light in view space, find the corresponding clusters for the min and max + // points of the AABB, then iterate over just those clusters for this light + let light_aabb_view = Aabb { + center: (inverse_view_transform * light_sphere.center.extend(1.0)).xyz(), + half_extents: Vec3::splat(light_sphere.radius), + }; + let (light_aabb_view_min, light_aabb_view_max) = + (light_aabb_view.min(), light_aabb_view.max()); + // Is there a cheaper way to do this? The problem is that because of perspective + // the point at max z but min xy may be less xy in screenspace, and similar. As + // such, projecting the min and max xy at both the closer and further z and taking + // the min and max of those projected points addresses this. + let ( + light_aabb_view_xymin_near, + light_aabb_view_xymin_far, + light_aabb_view_xymax_near, + light_aabb_view_xymax_far, + ) = ( + light_aabb_view_min, + light_aabb_view_min.xy().extend(light_aabb_view_max.z), + light_aabb_view_max.xy().extend(light_aabb_view_min.z), + light_aabb_view_max, + ); + let ( + light_aabb_clip_xymin_near, + light_aabb_clip_xymin_far, + light_aabb_clip_xymax_near, + light_aabb_clip_xymax_far, + ) = ( + camera.projection_matrix * light_aabb_view_xymin_near.extend(1.0), + camera.projection_matrix * light_aabb_view_xymin_far.extend(1.0), + camera.projection_matrix * light_aabb_view_xymax_near.extend(1.0), + camera.projection_matrix * light_aabb_view_xymax_far.extend(1.0), + ); + let ( + light_aabb_ndc_xymin_near, + light_aabb_ndc_xymin_far, + light_aabb_ndc_xymax_near, + light_aabb_ndc_xymax_far, + ) = ( + light_aabb_clip_xymin_near.xyz() / light_aabb_clip_xymin_near.w, + light_aabb_clip_xymin_far.xyz() / light_aabb_clip_xymin_far.w, + light_aabb_clip_xymax_near.xyz() / light_aabb_clip_xymax_near.w, + light_aabb_clip_xymax_far.xyz() / light_aabb_clip_xymax_far.w, + ); + let (light_aabb_ndc_min, light_aabb_ndc_max) = ( + light_aabb_ndc_xymin_near + .min(light_aabb_ndc_xymin_far) + .min(light_aabb_ndc_xymax_near) + .min(light_aabb_ndc_xymax_far), + light_aabb_ndc_xymin_near + .max(light_aabb_ndc_xymin_far) + .max(light_aabb_ndc_xymax_near) + .max(light_aabb_ndc_xymax_far), + ); + let min_cluster = ndc_position_to_cluster( + clusters.axis_slices, + cluster_factors, + light_aabb_ndc_min, + light_aabb_view_min.z, + ); + let max_cluster = ndc_position_to_cluster( + clusters.axis_slices, + cluster_factors, + light_aabb_ndc_max, + light_aabb_view_max.z, + ); + let (min_cluster, max_cluster) = + (min_cluster.min(max_cluster), min_cluster.max(max_cluster)); + for y in min_cluster.y..=max_cluster.y { + for x in min_cluster.x..=max_cluster.x { + for z in min_cluster.z..=max_cluster.z { + let cluster_index = + cluster_to_index(clusters.axis_slices, UVec3::new(x, y, z)); + let cluster_aabb = &clusters.aabbs[cluster_index]; + if light_sphere.intersects_obb(cluster_aabb, &view_transform) { + global_lights_set.insert(light_entity); + visible_lights_set.insert(light_entity); + clusters_lights[cluster_index].entities.push(light_entity); + } + } + } + } + } + + for cluster_lights in clusters_lights.iter_mut() { + cluster_lights.entities.shrink_to_fit(); + } + + clusters.lights = clusters_lights; + commands.entity(view_entity).insert(VisiblePointLights { + entities: visible_lights_set.into_iter().collect(), + }); + } + global_lights.entities = global_lights_set.into_iter().collect(); +} + pub fn update_directional_light_frusta( mut views: Query<(&GlobalTransform, &DirectionalLight, &mut Frustum)>, ) { @@ -202,20 +562,30 @@ pub fn update_directional_light_frusta( } } +// NOTE: Run this after assign_lights_to_clusters! pub fn update_point_light_frusta( - mut views: Query<(&GlobalTransform, &PointLight, &mut CubemapFrusta)>, + global_lights: Res, + mut views: Query<(Entity, &GlobalTransform, &PointLight, &mut CubemapFrusta)>, ) { - let projection = Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1); + let projection = + Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z); let view_rotations = CUBE_MAP_FACES .iter() .map(|CubeMapFace { target, up }| GlobalTransform::identity().looking_at(*target, *up)) .collect::>(); - for (transform, point_light, mut cubemap_frusta) in views.iter_mut() { + let global_lights_set = global_lights + .entities + .iter() + .copied() + .collect::>(); + for (entity, transform, point_light, mut cubemap_frusta) in views.iter_mut() { // The frusta are used for culling meshes to the light for shadow mapping // so if shadow mapping is disabled for this light, then the frusta are // not needed. - if !point_light.shadows_enabled { + // Also, if the light is not relevant for any cluster, it will not be in the + // global lights set and so there is no need to update its frusta. + if !point_light.shadows_enabled || !global_lights_set.contains(&entity) { continue; } @@ -239,7 +609,10 @@ pub fn update_point_light_frusta( } } -pub fn check_light_visibility( +pub fn check_light_mesh_visibility( + // NOTE: VisiblePointLights is an alias for VisibleEntities so the Without + // is needed to avoid an unnecessary QuerySet + visible_point_lights: Query<&VisiblePointLights, Without>, mut point_lights: Query<( &PointLight, &GlobalTransform, @@ -304,7 +677,7 @@ pub fn check_light_visibility( } computed_visibility.is_visible = true; - visible_entities.entities.push(VisibleEntity { entity }); + visible_entities.entities.push(entity); } // TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize @@ -312,67 +685,76 @@ pub fn check_light_visibility( } // Point lights - for (point_light, transform, cubemap_frusta, mut cubemap_visible_entities, maybe_view_mask) in - point_lights.iter_mut() - { - for visible_entities in cubemap_visible_entities.iter_mut() { - visible_entities.entities.clear(); - } - - // NOTE: If shadow mapping is disabled for the light then it must have no visible entities - if !point_light.shadows_enabled { - continue; - } - - let view_mask = maybe_view_mask.copied().unwrap_or_default(); - let light_sphere = Sphere { - center: transform.translation, - radius: point_light.range, - }; - - for ( - entity, - visibility, - mut computed_visibility, - maybe_entity_mask, - maybe_aabb, - maybe_transform, - ) in visible_entity_query.iter_mut() - { - if !visibility.is_visible { - continue; - } - - let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); - if !view_mask.intersects(&entity_mask) { - continue; - } + for visible_lights in visible_point_lights.iter() { + for light_entity in visible_lights.entities.iter().copied() { + if let Ok(( + point_light, + transform, + cubemap_frusta, + mut cubemap_visible_entities, + maybe_view_mask, + )) = point_lights.get_mut(light_entity) + { + for visible_entities in cubemap_visible_entities.iter_mut() { + visible_entities.entities.clear(); + } - // If we have an aabb and transform, do frustum culling - if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { - let model_to_world = transform.compute_matrix(); - // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light - if !light_sphere.intersects_obb(aabb, &model_to_world) { + // NOTE: If shadow mapping is disabled for the light then it must have no visible entities + if !point_light.shadows_enabled { continue; } - for (frustum, visible_entities) in cubemap_frusta - .iter() - .zip(cubemap_visible_entities.iter_mut()) + + let view_mask = maybe_view_mask.copied().unwrap_or_default(); + let light_sphere = Sphere { + center: transform.translation, + radius: point_light.range, + }; + + for ( + entity, + visibility, + mut computed_visibility, + maybe_entity_mask, + maybe_aabb, + maybe_transform, + ) in visible_entity_query.iter_mut() { - if frustum.intersects_obb(aabb, &model_to_world) { + if !visibility.is_visible { + continue; + } + + let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); + if !view_mask.intersects(&entity_mask) { + continue; + } + + // If we have an aabb and transform, do frustum culling + if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { + let model_to_world = transform.compute_matrix(); + // Do a cheap sphere vs obb test to prune out most meshes outside the sphere of the light + if !light_sphere.intersects_obb(aabb, &model_to_world) { + continue; + } + for (frustum, visible_entities) in cubemap_frusta + .iter() + .zip(cubemap_visible_entities.iter_mut()) + { + if frustum.intersects_obb(aabb, &model_to_world) { + computed_visibility.is_visible = true; + visible_entities.entities.push(entity); + } + } + } else { computed_visibility.is_visible = true; - visible_entities.entities.push(VisibleEntity { entity }); + for visible_entities in cubemap_visible_entities.iter_mut() { + visible_entities.entities.push(entity) + } } } - } else { - computed_visibility.is_visible = true; - for visible_entities in cubemap_visible_entities.iter_mut() { - visible_entities.entities.push(VisibleEntity { entity }) - } + + // TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize + // to prevent holding unneeded memory } } - - // TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize - // to prevent holding unneeded memory } } diff --git a/pipelined/bevy_pbr2/src/render/light.rs b/pipelined/bevy_pbr2/src/render/light.rs index dff2c21b59ce6..9b705473a3ea8 100644 --- a/pipelined/bevy_pbr2/src/render/light.rs +++ b/pipelined/bevy_pbr2/src/render/light.rs @@ -1,7 +1,7 @@ use crate::{ - AmbientLight, CubemapVisibleEntities, DirectionalLight, DirectionalLightShadowMap, DrawMesh, - MeshPipeline, NotShadowCaster, PointLight, PointLightShadowMap, SetMeshBindGroup, - SHADOW_SHADER_HANDLE, + AmbientLight, Clusters, CubemapVisibleEntities, DirectionalLight, DirectionalLightShadowMap, + DrawMesh, MeshPipeline, NotShadowCaster, PointLight, PointLightShadowMap, SetMeshBindGroup, + VisiblePointLights, SHADOW_SHADER_HANDLE, }; use bevy_asset::Handle; use bevy_core::FloatOrd; @@ -10,9 +10,9 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; -use bevy_math::{const_vec3, Mat4, Vec3, Vec4}; +use bevy_math::{const_vec3, Mat4, UVec3, UVec4, Vec3, Vec4, Vec4Swizzles}; use bevy_render2::{ - camera::CameraProjection, + camera::{Camera, CameraProjection}, color::Color, mesh::Mesh, render_asset::RenderAssets, @@ -25,17 +25,18 @@ use bevy_render2::{ render_resource::*, renderer::{RenderContext, RenderDevice, RenderQueue}, texture::*, - view::{ - ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities, VisibleEntity, - }, + view::{ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities}, }; use bevy_transform::components::GlobalTransform; +use bevy_utils::{tracing::warn, HashMap}; use crevice::std140::AsStd140; use std::num::NonZeroU32; #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub enum RenderLightSystems { + ExtractClusters, ExtractLights, + PrepareClusters, PrepareLights, QueueShadows, } @@ -69,6 +70,8 @@ pub struct ExtractedDirectionalLight { shadows_enabled: bool, shadow_depth_bias: f32, shadow_normal_bias: f32, + near: f32, + far: f32, } pub type ExtractedDirectionalLightShadowMap = DirectionalLightShadowMap; @@ -76,18 +79,20 @@ pub type ExtractedDirectionalLightShadowMap = DirectionalLightShadowMap; #[repr(C)] #[derive(Copy, Clone, AsStd140, Default, Debug)] pub struct GpuPointLight { - projection: Mat4, - color: Vec4, - position: Vec3, - inverse_square_range: f32, - radius: f32, - near: f32, - far: f32, + // The lower-right 2x2 values of the projection matrix 22 23 32 33 + projection_lr: Vec4, + color_inverse_square_range: Vec4, + position_radius: Vec4, flags: u32, shadow_depth_bias: f32, shadow_normal_bias: f32, } +#[derive(AsStd140)] +pub struct GpuPointLights { + data: [GpuPointLight; MAX_POINT_LIGHTS], +} + // NOTE: These must match the bit flags in bevy_pbr2/src/render/pbr.frag! bitflags::bitflags! { #[repr(transparent)] @@ -123,17 +128,23 @@ bitflags::bitflags! { #[derive(Copy, Clone, Debug, AsStd140)] pub struct GpuLights { // TODO: this comes first to work around a WGSL alignment issue. We need to solve this issue before releasing the renderer rework - point_lights: [GpuPointLight; MAX_POINT_LIGHTS], directional_lights: [GpuDirectionalLight; MAX_DIRECTIONAL_LIGHTS], ambient_color: Vec4, - n_point_lights: u32, + cluster_dimensions: UVec4, + // xy are vec2(cluster_dimensions.xy) / vec2(view.width, view.height) + // z is cluster_dimensions.z / log(far / near) + // w is cluster_dimensions.z * log(near) / log(far / near) + cluster_factors: Vec4, n_directional_lights: u32, } // NOTE: this must be kept in sync with the same constants in pbr.frag -pub const MAX_POINT_LIGHTS: usize = 10; +pub const MAX_POINT_LIGHTS: usize = 256; +// FIXME: How should we handle shadows for clustered forward? Limiting to maximum 10 +// point light shadow maps for now +pub const MAX_POINT_LIGHT_SHADOW_MAPS: usize = 10; pub const MAX_DIRECTIONAL_LIGHTS: usize = 1; -pub const POINT_SHADOW_LAYERS: u32 = (6 * MAX_POINT_LIGHTS) as u32; +pub const POINT_SHADOW_LAYERS: u32 = (6 * MAX_POINT_LIGHT_SHADOW_MAPS) as u32; pub const DIRECTIONAL_SHADOW_LAYERS: u32 = MAX_DIRECTIONAL_LIGHTS as u32; pub const SHADOW_FORMAT: TextureFormat = TextureFormat::Depth32Float; @@ -308,17 +319,38 @@ impl SpecializedPipeline for ShadowPipeline { } } +#[derive(Component)] +pub struct ExtractedClusterConfig { + /// Number of clusters in x / y / z in the view frustum + axis_slices: UVec3, +} + +#[derive(Component)] +pub struct ExtractedClustersPointLights { + data: Vec, +} + +pub fn extract_clusters(mut commands: Commands, views: Query<(Entity, &Clusters), With>) { + for (entity, clusters) in views.iter() { + commands.get_or_spawn(entity).insert_bundle(( + ExtractedClustersPointLights { + data: clusters.lights.clone(), + }, + ExtractedClusterConfig { + axis_slices: clusters.axis_slices, + }, + )); + } +} + pub fn extract_lights( mut commands: Commands, ambient_light: Res, point_light_shadow_map: Res, directional_light_shadow_map: Res, - mut point_lights: Query<( - Entity, - &PointLight, - &mut CubemapVisibleEntities, - &GlobalTransform, - )>, + global_point_lights: Res, + // visible_point_lights: Query<&VisiblePointLights>, + mut point_lights: Query<(&PointLight, &mut CubemapVisibleEntities, &GlobalTransform)>, mut directional_lights: Query<( Entity, &DirectionalLight, @@ -342,28 +374,34 @@ pub fn extract_lights( // NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to: // https://catlikecoding.com/unity/tutorials/custom-srp/point-and-spot-shadows/ let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32; - for (entity, point_light, cubemap_visible_entities, transform) in point_lights.iter_mut() { - let render_cubemap_visible_entities = std::mem::take(cubemap_visible_entities.into_inner()); - commands.get_or_spawn(entity).insert_bundle(( - ExtractedPointLight { - color: point_light.color, - // NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian - // for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower - // for details. - intensity: point_light.intensity / (4.0 * std::f32::consts::PI), - range: point_light.range, - radius: point_light.radius, - transform: *transform, - shadows_enabled: point_light.shadows_enabled, - shadow_depth_bias: point_light.shadow_depth_bias, - // The factor of SQRT_2 is for the worst-case diagonal offset - shadow_normal_bias: point_light.shadow_normal_bias - * point_light_texel_size - * std::f32::consts::SQRT_2, - }, - render_cubemap_visible_entities, - )); + + for entity in global_point_lights.iter().copied() { + if let Ok((point_light, cubemap_visible_entities, transform)) = point_lights.get_mut(entity) + { + let render_cubemap_visible_entities = + std::mem::take(cubemap_visible_entities.into_inner()); + commands.get_or_spawn(entity).insert_bundle(( + ExtractedPointLight { + color: point_light.color, + // NOTE: Map from luminous power in lumens to luminous intensity in lumens per steradian + // for a point light. See https://google.github.io/filament/Filament.html#mjx-eqn-pointLightLuminousPower + // for details. + intensity: point_light.intensity / (4.0 * std::f32::consts::PI), + range: point_light.range, + radius: point_light.radius, + transform: *transform, + shadows_enabled: point_light.shadows_enabled, + shadow_depth_bias: point_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: point_light.shadow_normal_bias + * point_light_texel_size + * std::f32::consts::SQRT_2, + }, + render_cubemap_visible_entities, + )); + } } + for (entity, directional_light, visible_entities, transform) in directional_lights.iter_mut() { // Calulate the directional light shadow map texel size using the largest x,y dimension of // the orthographic projection divided by the shadow map resolution @@ -390,12 +428,16 @@ pub fn extract_lights( shadow_normal_bias: directional_light.shadow_normal_bias * directional_light_texel_size * std::f32::consts::SQRT_2, + near: directional_light.shadow_projection.near, + far: directional_light.shadow_projection.far, }, render_visible_entities, )); } } +pub(crate) const POINT_LIGHT_NEAR_Z: f32 = 0.1f32; + // Can't do `Vec3::Y * -1.0` because mul isn't const const NEGATIVE_X: Vec3 = const_vec3!([-1.0, 0.0, 0.0]); const NEGATIVE_Y: Vec3 = const_vec3!([0.0, -1.0, 0.0]); @@ -476,6 +518,12 @@ pub struct ViewLightsUniformOffset { pub offset: u32, } +#[derive(Default)] +pub struct GlobalLightMeta { + pub gpu_point_lights: UniformVec, + pub entity_to_index: HashMap, +} + #[derive(Default)] pub struct LightMeta { pub view_gpu_lights: DynamicUniformVec, @@ -499,8 +547,12 @@ pub fn prepare_lights( mut texture_cache: ResMut, render_device: Res, render_queue: Res, + mut global_light_meta: ResMut, mut light_meta: ResMut, - views: Query>>, + views: Query< + (Entity, &ExtractedView, &ExtractedClusterConfig), + With>, + >, ambient_light: Res, point_light_shadow_map: Res, directional_light_shadow_map: Res, @@ -511,14 +563,53 @@ pub fn prepare_lights( // Pre-calculate for PointLights let cube_face_projection = - Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1); + Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, POINT_LIGHT_NEAR_Z); let cube_face_rotations = CUBE_MAP_FACES .iter() .map(|CubeMapFace { target, up }| GlobalTransform::identity().looking_at(*target, *up)) .collect::>(); + global_light_meta.gpu_point_lights.clear(); + global_light_meta.entity_to_index.clear(); + let n_point_lights = point_lights.iter().count(); + if global_light_meta.entity_to_index.capacity() < n_point_lights { + global_light_meta.entity_to_index.reserve(n_point_lights); + } + let mut gpu_point_lights = [GpuPointLight::default(); MAX_POINT_LIGHTS]; + for (index, (entity, light)) in point_lights.iter().enumerate() { + let mut flags = PointLightFlags::NONE; + if light.shadows_enabled { + flags |= PointLightFlags::SHADOWS_ENABLED; + } + gpu_point_lights[index] = GpuPointLight { + projection_lr: Vec4::new( + cube_face_projection.z_axis.z, + cube_face_projection.z_axis.w, + cube_face_projection.w_axis.z, + cube_face_projection.w_axis.w, + ), + // premultiply color by intensity + // we don't use the alpha at all, so no reason to multiply only [0..3] + color_inverse_square_range: (Vec4::from_slice(&light.color.as_linear_rgba_f32()) + * light.intensity) + .xyz() + .extend(1.0 / (light.range * light.range)), + position_radius: light.transform.translation.extend(light.radius), + flags: flags.bits, + shadow_depth_bias: light.shadow_depth_bias, + shadow_normal_bias: light.shadow_normal_bias, + }; + global_light_meta.entity_to_index.insert(entity, index); + } + global_light_meta.gpu_point_lights.push(GpuPointLights { + data: gpu_point_lights, + }); + global_light_meta + .gpu_point_lights + .write_buffer(&render_device, &render_queue); + // set up light data for each view - for entity in views.iter() { + for (entity, extracted_view, clusters) in views.iter() { let point_light_depth_texture = texture_cache.get( &render_device, TextureDescriptor { @@ -553,23 +644,37 @@ pub fn prepare_lights( ); let mut view_lights = Vec::new(); + let z_times_ln_far_over_near = + clusters.axis_slices.z as f32 / (extracted_view.far / extracted_view.near).ln(); let mut gpu_lights = GpuLights { + directional_lights: [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS], ambient_color: Vec4::from_slice(&ambient_light.color.as_linear_rgba_f32()) * ambient_light.brightness, - n_point_lights: point_lights.iter().len() as u32, + cluster_factors: Vec4::new( + clusters.axis_slices.x as f32 / extracted_view.width as f32, + clusters.axis_slices.y as f32 / extracted_view.height as f32, + z_times_ln_far_over_near, + extracted_view.near.ln() * z_times_ln_far_over_near, + ), + cluster_dimensions: clusters.axis_slices.extend(0), n_directional_lights: directional_lights.iter().len() as u32, - point_lights: [GpuPointLight::default(); MAX_POINT_LIGHTS], - directional_lights: [GpuDirectionalLight::default(); MAX_DIRECTIONAL_LIGHTS], }; // TODO: this should select lights based on relevance to the view instead of the first ones that show up in a query - for (light_index, (light_entity, light)) in point_lights.iter().enumerate() { - // ignore scale because we don't want to effectively scale light radius and range - // by applying those as a view transform to shadow map rendering of objects - // and ignore rotation because we want the shadow map projections to align with the axes - let view_translation = GlobalTransform::from_translation(light.transform.translation); + let mut point_light_count = 0; + for (light_entity, light) in point_lights.iter() { + if point_light_count < MAX_POINT_LIGHT_SHADOW_MAPS && light.shadows_enabled { + point_light_count += 1; + let light_index = *global_light_meta + .entity_to_index + .get(&light_entity) + .unwrap(); + // ignore scale because we don't want to effectively scale light radius and range + // by applying those as a view transform to shadow map rendering of objects + // and ignore rotation because we want the shadow map projections to align with the axes + let view_translation = + GlobalTransform::from_translation(light.transform.translation); - if light.shadows_enabled { for (face_index, view_rotation) in cube_face_rotations.iter().enumerate() { let depth_texture_view = point_light_depth_texture @@ -601,6 +706,8 @@ pub fn prepare_lights( height: point_light_shadow_map.size as u32, transform: view_translation * *view_rotation, projection: cube_face_projection, + near: POINT_LIGHT_NEAR_Z, + far: light.range, }, RenderPhase::::default(), LightEntity::Point { @@ -612,26 +719,6 @@ pub fn prepare_lights( view_lights.push(view_light_entity); } } - - let mut flags = PointLightFlags::NONE; - if light.shadows_enabled { - flags |= PointLightFlags::SHADOWS_ENABLED; - } - - gpu_lights.point_lights[light_index] = GpuPointLight { - projection: cube_face_projection, - // premultiply color by intensity - // we don't use the alpha at all, so no reason to multiply only [0..3] - color: Vec4::from_slice(&light.color.as_linear_rgba_f32()) * light.intensity, - radius: light.radius, - position: light.transform.translation, - inverse_square_range: 1.0 / (light.range * light.range), - near: 0.1, - far: light.range, - flags: flags.bits, - shadow_depth_bias: light.shadow_depth_bias, - shadow_normal_bias: light.shadow_normal_bias, - }; } for (i, (light_entity, light)) in directional_lights @@ -656,7 +743,7 @@ pub fn prepare_lights( let intensity = light.illuminance * exposure; // NOTE: A directional light seems to have to have an eye position on the line along the direction of the light - // through the world origin. I (Rob Swain) do not yet understand why it cannot be translated away from this. + // through the world origin. I (Rob Swain) do not yet understand why it cannot be translated away from this. let view = Mat4::look_at_rh(Vec3::ZERO, light.direction, Vec3::Y); // NOTE: This orthographic projection defines the volume within which shadows from a directional light can be cast let projection = light.projection; @@ -705,6 +792,8 @@ pub fn prepare_lights( height: directional_light_shadow_map.size as u32, transform: GlobalTransform::from_matrix(view.inverse()), projection, + near: light.near, + far: light.far, }, RenderPhase::::default(), LightEntity::Directional { light_entity }, @@ -760,6 +849,142 @@ pub fn prepare_lights( .write_buffer(&render_device, &render_queue); } +const CLUSTER_OFFSET_MASK: u32 = (1 << 24) - 1; +const CLUSTER_COUNT_SIZE: u32 = 8; +const CLUSTER_COUNT_MASK: u32 = (1 << 8) - 1; +const POINT_LIGHT_INDEX_MASK: u32 = (1 << 8) - 1; + +// NOTE: With uniform buffer max binding size as 16384 bytes +// that means we can fit say 128 point lights in one uniform +// buffer, which means the count can be at most 128 so it +// needs 7 bits, use 8 for convenience. +// The array of indices can also use u8 and that means the +// offset in to the array of indices needs to be able to address +// 16384 values. lod2(16384) = 21 bits. +// This means we can pack the offset into the upper 24 bits of a u32 +// and the count into the lower 8 bits. +// FIXME: Probably there are endianness concerns here????!!!!! +fn pack_offset_and_count(offset: usize, count: usize) -> u32 { + ((offset as u32 & CLUSTER_OFFSET_MASK) << CLUSTER_COUNT_SIZE) + | (count as u32 & CLUSTER_COUNT_MASK) +} + +#[derive(Component, Default)] +pub struct ViewClusterBindings { + n_indices: usize, + // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment + pub cluster_light_index_lists: UniformVec<[UVec4; Self::MAX_UNIFORM_ITEMS]>, + n_offsets: usize, + // NOTE: UVec4 is because all arrays in Std140 layout have 16-byte alignment + pub cluster_offsets_and_counts: UniformVec<[UVec4; Self::MAX_UNIFORM_ITEMS]>, +} + +impl ViewClusterBindings { + pub const MAX_OFFSETS: usize = 16384 / 4; + const MAX_UNIFORM_ITEMS: usize = Self::MAX_OFFSETS / 4; + const MAX_INDICES: usize = 16384; + + pub fn reserve_and_clear(&mut self) { + self.cluster_light_index_lists.clear(); + self.cluster_light_index_lists + .push([UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]); + self.cluster_offsets_and_counts.clear(); + self.cluster_offsets_and_counts + .push([UVec4::ZERO; Self::MAX_UNIFORM_ITEMS]); + } + + pub fn push_offset_and_count(&mut self, offset: usize, count: usize) { + let array_index = self.n_offsets >> 2; // >> 2 is equivalent to / 4 + if array_index >= Self::MAX_UNIFORM_ITEMS { + warn!("cluster offset and count out of bounds!"); + return; + } + let component = self.n_offsets & ((1 << 2) - 1); + let packed = pack_offset_and_count(offset, count); + + self.cluster_offsets_and_counts.get_mut(0)[array_index][component] = packed; + + self.n_offsets += 1; + } + + pub fn n_indices(&self) -> usize { + self.n_indices + } + + pub fn push_index(&mut self, index: usize) { + let array_index = self.n_indices >> 4; // >> 4 is equivalent to / 16 + let component = (self.n_indices >> 2) & ((1 << 2) - 1); + let sub_index = self.n_indices & ((1 << 2) - 1); + let index = index as u32 & POINT_LIGHT_INDEX_MASK; + + self.cluster_light_index_lists.get_mut(0)[array_index][component] |= + index << (8 * sub_index); + + self.n_indices += 1; + } +} + +pub fn prepare_clusters( + mut commands: Commands, + render_device: Res, + render_queue: Res, + global_light_meta: Res, + views: Query< + ( + Entity, + &ExtractedClusterConfig, + &ExtractedClustersPointLights, + ), + With>, + >, +) { + for (entity, cluster_config, extracted_clusters) in views.iter() { + let mut view_clusters_bindings = ViewClusterBindings::default(); + view_clusters_bindings.reserve_and_clear(); + + let mut indices_full = false; + + let mut cluster_index = 0; + for _y in 0..cluster_config.axis_slices.y { + for _x in 0..cluster_config.axis_slices.x { + for _z in 0..cluster_config.axis_slices.z { + let offset = view_clusters_bindings.n_indices(); + let cluster_lights = &extracted_clusters.data[cluster_index]; + let count = cluster_lights.len(); + view_clusters_bindings.push_offset_and_count(offset, count); + + if !indices_full { + for entity in cluster_lights.iter() { + if let Some(light_index) = global_light_meta.entity_to_index.get(entity) + { + if view_clusters_bindings.n_indices() + >= ViewClusterBindings::MAX_INDICES + { + warn!("Cluster light index lists is full! The PointLights in the view are affecting too many clusters."); + indices_full = true; + break; + } + view_clusters_bindings.push_index(*light_index); + } + } + } + + cluster_index += 1; + } + } + } + + view_clusters_bindings + .cluster_light_index_lists + .write_buffer(&render_device, &render_queue); + view_clusters_bindings + .cluster_offsets_and_counts + .write_buffer(&render_device, &render_queue); + + commands.get_or_spawn(entity).insert(view_clusters_bindings); + } +} + pub fn queue_shadow_view_bind_group( render_device: Res, shadow_pipeline: Res, @@ -813,10 +1038,10 @@ pub fn queue_shadows( .get(*face_index), }; // NOTE: Lights with shadow mapping disabled will have no visible entities - // so no meshes will be queued - for VisibleEntity { entity, .. } in visible_entities.iter() { + // so no meshes will be queued + for entity in visible_entities.iter().copied() { let mut key = ShadowPipelineKey::empty(); - if let Ok(mesh_handle) = casting_meshes.get(*entity) { + if let Ok(mesh_handle) = casting_meshes.get(entity) { if let Some(mesh) = render_meshes.get(mesh_handle) { if mesh.has_tangents { key |= ShadowPipelineKey::VERTEX_TANGENTS; @@ -828,7 +1053,7 @@ pub fn queue_shadows( shadow_phase.add(Shadow { draw_function: draw_shadow_mesh, pipeline: pipeline_id, - entity: *entity, + entity, distance: 0.0, // TODO: sort back-to-front }); } diff --git a/pipelined/bevy_pbr2/src/render/mesh.rs b/pipelined/bevy_pbr2/src/render/mesh.rs index 567745f919e0f..115b12fa5f530 100644 --- a/pipelined/bevy_pbr2/src/render/mesh.rs +++ b/pipelined/bevy_pbr2/src/render/mesh.rs @@ -1,6 +1,6 @@ use crate::{ - GpuLights, LightMeta, NotShadowCaster, NotShadowReceiver, ShadowPipeline, - ViewLightsUniformOffset, ViewShadowBindings, + GlobalLightMeta, GpuLights, LightMeta, NotShadowCaster, NotShadowReceiver, ShadowPipeline, + ViewClusterBindings, ViewLightsUniformOffset, ViewShadowBindings, }; use bevy_app::Plugin; use bevy_asset::{Assets, Handle, HandleUntyped}; @@ -237,6 +237,47 @@ impl FromWorld for MeshPipeline { }, count: None, }, + // PointLights + BindGroupLayoutEntry { + binding: 6, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + // NOTE: Static size for uniform buffers. GpuPointLight has a padded + // size of 128 bytes, so 16384 / 128 = 128 point lights max + min_binding_size: BufferSize::new(16384), + }, + count: None, + }, + // ClusteredLightIndexLists + BindGroupLayoutEntry { + binding: 7, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + // NOTE: With 128 point lights max, indices need 7 bits. Use u8 for + // convenience. + min_binding_size: BufferSize::new(16384), + }, + count: None, + }, + // ClusterOffsetsAndCounts + BindGroupLayoutEntry { + binding: 8, + visibility: ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + // NOTE: The offset needs to address 16384 indices, which needs 21 bits. + // The count can be at most all 128 lights so 7 bits. + // Pack the offset into the upper 24 bits and the count into the + // lower 8 bits for convenience. + min_binding_size: BufferSize::new(16384), + }, + count: None, + }, ], label: Some("mesh_view_layout"), }); @@ -514,20 +555,23 @@ pub struct MeshViewBindGroup { pub value: BindGroup, } +#[allow(clippy::too_many_arguments)] pub fn queue_mesh_view_bind_groups( mut commands: Commands, render_device: Res, mesh_pipeline: Res, shadow_pipeline: Res, light_meta: Res, + global_light_meta: Res, view_uniforms: Res, - mut views: Query<(Entity, &ViewShadowBindings)>, + mut views: Query<(Entity, &ViewShadowBindings, &ViewClusterBindings)>, ) { - if let (Some(view_binding), Some(light_binding)) = ( + if let (Some(view_binding), Some(light_binding), Some(point_light_binding)) = ( view_uniforms.uniforms.binding(), light_meta.view_gpu_lights.binding(), + global_light_meta.gpu_point_lights.binding(), ) { - for (entity, view_shadow_bindings) in views.iter_mut() { + for (entity, view_shadow_bindings, view_cluster_bindings) in views.iter_mut() { let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { entries: &[ BindGroupEntry { @@ -560,6 +604,24 @@ pub fn queue_mesh_view_bind_groups( &shadow_pipeline.directional_light_sampler, ), }, + BindGroupEntry { + binding: 6, + resource: point_light_binding.clone(), + }, + BindGroupEntry { + binding: 7, + resource: view_cluster_bindings + .cluster_light_index_lists + .binding() + .unwrap(), + }, + BindGroupEntry { + binding: 8, + resource: view_cluster_bindings + .cluster_offsets_and_counts + .binding() + .unwrap(), + }, ], label: Some("mesh_view_bind_group"), layout: &mesh_pipeline.view_layout, diff --git a/pipelined/bevy_pbr2/src/render/mesh_view_bind_group.wgsl b/pipelined/bevy_pbr2/src/render/mesh_view_bind_group.wgsl index a9eb0f60d8846..d4d506fba1fc8 100644 --- a/pipelined/bevy_pbr2/src/render/mesh_view_bind_group.wgsl +++ b/pipelined/bevy_pbr2/src/render/mesh_view_bind_group.wgsl @@ -1,18 +1,20 @@ [[block]] struct View { view_proj: mat4x4; + inverse_view: mat4x4; projection: mat4x4; world_position: vec3; + near: f32; + far: f32; + width: f32; + height: f32; }; struct PointLight { - projection: mat4x4; - color: vec4; - position: vec3; - inverse_square_range: f32; - radius: f32; - near: f32; - far: f32; + // NOTE: [2][2] [2][3] [3][2] [3][3] + projection_lr: vec4; + color_inverse_square_range: vec4; + position_radius: vec4; // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32; shadow_depth_bias: f32; @@ -36,14 +38,35 @@ let DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT: u32 = 1u; [[block]] struct Lights { // NOTE: this array size must be kept in sync with the constants defined bevy_pbr2/src/render/light.rs - // TODO: this can be removed if we move to storage buffers for light arrays - point_lights: array; - directional_lights: array; + directional_lights: array; ambient_color: vec4; - n_point_lights: u32; + // x/y/z dimensions + cluster_dimensions: vec4; + // xy are vec2(cluster_dimensions.xy) / vec2(view.width, view.height) + // z is cluster_dimensions.z / log(far / near) + // w is cluster_dimensions.z * log(near) / log(far / near) + cluster_factors: vec4; n_directional_lights: u32; }; +[[block]] +struct PointLights { + data: array; +}; + +[[block]] +struct ClusterLightIndexLists { + // each u32 contains 4 u8 indices into the PointLights array + data: array, 1024u>; +}; + +[[block]] +struct ClusterOffsetsAndCounts { + // each u32 contains a 24-bit index into ClusterLightIndexLists in the high 24 bits + // and an 8-bit count of the number of lights in the low 8 bits + data: array, 1024u>; +}; + [[group(0), binding(0)]] var view: View; [[group(0), binding(1)]] @@ -56,3 +79,9 @@ var point_shadow_textures_sampler: sampler_comparison; var directional_shadow_textures: texture_depth_2d_array; [[group(0), binding(5)]] var directional_shadow_textures_sampler: sampler_comparison; +[[group(0), binding(6)]] +var point_lights: PointLights; +[[group(0), binding(7)]] +var cluster_light_index_lists: ClusterLightIndexLists; +[[group(0), binding(8)]] +var cluster_offsets_and_counts: ClusterOffsetsAndCounts; diff --git a/pipelined/bevy_pbr2/src/render/mod.rs b/pipelined/bevy_pbr2/src/render/mod.rs index 348f7db955280..0b72317a57813 100644 --- a/pipelined/bevy_pbr2/src/render/mod.rs +++ b/pipelined/bevy_pbr2/src/render/mod.rs @@ -249,7 +249,7 @@ pub fn queue_meshes( for visible_entity in &visible_entities.entities { if let Ok((material_handle, mesh_handle, mesh_uniform)) = - standard_material_meshes.get(visible_entity.entity) + standard_material_meshes.get(*visible_entity) { if let Some(material) = render_materials.get(material_handle) { let mut pbr_key = PbrPipelineKey { @@ -275,37 +275,37 @@ pub fn queue_meshes( match material.alpha_mode { AlphaMode::Opaque => { opaque_phase.add(Opaque3d { - entity: visible_entity.entity, + entity: *visible_entity, draw_function: draw_opaque_pbr, pipeline: pipeline_id, // NOTE: Front-to-back ordering for opaque with ascending sort means near should have the - // lowest sort key and getting further away should increase. As we have - // -z in front of the camera, values in view space decrease away from the - // camera. Flipping the sign of mesh_z results in the correct front-to-back ordering + // lowest sort key and getting further away should increase. As we have + // -z in front of the camera, values in view space decrease away from the + // camera. Flipping the sign of mesh_z results in the correct front-to-back ordering distance: -mesh_z, }); } AlphaMode::Mask(_) => { alpha_mask_phase.add(AlphaMask3d { - entity: visible_entity.entity, + entity: *visible_entity, draw_function: draw_alpha_mask_pbr, pipeline: pipeline_id, // NOTE: Front-to-back ordering for alpha mask with ascending sort means near should have the - // lowest sort key and getting further away should increase. As we have - // -z in front of the camera, values in view space decrease away from the - // camera. Flipping the sign of mesh_z results in the correct front-to-back ordering + // lowest sort key and getting further away should increase. As we have + // -z in front of the camera, values in view space decrease away from the + // camera. Flipping the sign of mesh_z results in the correct front-to-back ordering distance: -mesh_z, }); } AlphaMode::Blend => { transparent_phase.add(Transparent3d { - entity: visible_entity.entity, + entity: *visible_entity, draw_function: draw_transparent_pbr, pipeline: pipeline_id, // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the - // lowest sort key and getting closer should increase. As we have - // -z in front of the camera, the largest distance is -far with values increasing toward the - // camera. As such we can just use mesh_z as the distance + // lowest sort key and getting closer should increase. As we have + // -z in front of the camera, the largest distance is -far with values increasing toward the + // camera. As such we can just use mesh_z as the distance distance: mesh_z, }); } diff --git a/pipelined/bevy_pbr2/src/render/pbr.wgsl b/pipelined/bevy_pbr2/src/render/pbr.wgsl index 45e6fc2a923de..ec240265b9334 100644 --- a/pipelined/bevy_pbr2/src/render/pbr.wgsl +++ b/pipelined/bevy_pbr2/src/render/pbr.wgsl @@ -239,23 +239,57 @@ fn reinhard_extended_luminance(color: vec3, max_white_l: f32) -> vec3 return change_luminance(color, l_new); } +fn view_z_to_z_slice(view_z: f32) -> u32 { + // NOTE: had to use -view_z to make it positive else log(negative) is nan + return u32(floor(log(-view_z) * lights.cluster_factors.z - lights.cluster_factors.w)); +} + +fn fragment_cluster_index(frag_coord: vec2, view_z: f32) -> u32 { + let xy = vec2(floor(frag_coord * lights.cluster_factors.xy)); + let z_slice = view_z_to_z_slice(view_z); + return (xy.y * lights.cluster_dimensions.x + xy.x) * lights.cluster_dimensions.z + z_slice; +} + +struct ClusterOffsetAndCount { + offset: u32; + count: u32; +}; + +fn unpack_offset_and_count(cluster_index: u32) -> ClusterOffsetAndCount { + let offset_and_count = cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)]; + var output: ClusterOffsetAndCount; + // The offset is stored in the upper 24 bits + output.offset = (offset_and_count >> 8u) & ((1u << 24u) - 1u); + // The count is stored in the lower 8 bits + output.count = offset_and_count & ((1u << 8u) - 1u); + return output; +} + +fn get_light_id(index: u32) -> u32 { + // The index is correct but in cluster_light_index_lists we pack 4 u8s into a u32 + // This means the index into cluster_light_index_lists is index / 4 + let indices = cluster_light_index_lists.data[index >> 4u][(index >> 2u) & ((1u << 2u) - 1u)]; + // And index % 4 gives the sub-index of the u8 within the u32 so we shift by 8 * sub-index + return (indices >> (8u * (index & ((1u << 2u) - 1u)))) & ((1u << 8u) - 1u); +} + fn point_light( world_position: vec3, light: PointLight, roughness: f32, NdotV: f32, N: vec3, V: vec3, R: vec3, F0: vec3, diffuseColor: vec3 ) -> vec3 { - let light_to_frag = light.position.xyz - world_position.xyz; + let light_to_frag = light.position_radius.xyz - world_position.xyz; let distance_square = dot(light_to_frag, light_to_frag); let rangeAttenuation = - getDistanceAttenuation(distance_square, light.inverse_square_range); + getDistanceAttenuation(distance_square, light.color_inverse_square_range.w); // Specular. // Representative Point Area Lights. // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 let a = roughness; let centerToRay = dot(light_to_frag, R) * R - light_to_frag; - let closestPoint = light_to_frag + centerToRay * saturate(light.radius * inverseSqrt(dot(centerToRay, centerToRay))); + let closestPoint = light_to_frag + centerToRay * saturate(light.position_radius.w * inverseSqrt(dot(centerToRay, centerToRay))); let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); - let normalizationFactor = a / saturate(a + (light.radius * 0.5 * LspecLengthInverse)); + let normalizationFactor = a / saturate(a + (light.position_radius.w * 0.5 * LspecLengthInverse)); let specularIntensity = normalizationFactor * normalizationFactor; var L: vec3 = closestPoint * LspecLengthInverse; // normalize() equivalent? @@ -291,7 +325,7 @@ fn point_light( // TODO compensate for energy loss https://google.github.io/filament/Filament.html#materialsystem/improvingthebrdfs/energylossinspecularreflectance - return ((diffuse + specular_light) * light.color.rgb) * (rangeAttenuation * NoL); + return ((diffuse + specular_light) * light.color_inverse_square_range.rgb) * (rangeAttenuation * NoL); } fn directional_light(light: DirectionalLight, roughness: f32, NdotV: f32, normal: vec3, view: vec3, R: vec3, F0: vec3, diffuseColor: vec3) -> vec3 { @@ -309,12 +343,12 @@ fn directional_light(light: DirectionalLight, roughness: f32, NdotV: f32, normal return (specular_light + diffuse) * light.color.rgb * NoL; } -fn fetch_point_shadow(light_id: i32, frag_position: vec4, surface_normal: vec3) -> f32 { - let light = lights.point_lights[light_id]; +fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { + let light = point_lights.data[light_id]; // because the shadow maps align with the axes and the frustum planes are at 45 degrees // we can get the worldspace depth by taking the largest absolute axis - let surface_to_light = light.position.xyz - frag_position.xyz; + let surface_to_light = light.position_radius.xyz - frag_position.xyz; let surface_to_light_abs = abs(surface_to_light); let distance_to_light = max(surface_to_light_abs.x, max(surface_to_light_abs.y, surface_to_light_abs.z)); @@ -326,38 +360,27 @@ fn fetch_point_shadow(light_id: i32, frag_position: vec4, surface_normal: v let offset_position = frag_position.xyz + normal_offset + depth_offset; // similar largest-absolute-axis trick as above, but now with the offset fragment position - let frag_ls = light.position.xyz - offset_position.xyz; + let frag_ls = light.position_radius.xyz - offset_position.xyz; let abs_position_ls = abs(frag_ls); let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z)); // NOTE: These simplifications come from multiplying: - // projection * vec4(0, 0, -major_axis_magnitude, 1.0) - // and keeping only the terms that have any impact on the depth. + // projection * vec4(0, 0, -major_axis_magnitude, 1.0) + // and keeping only the terms that have any impact on the depth. // Projection-agnostic approach: - let z = -major_axis_magnitude * light.projection[2][2] + light.projection[3][2]; - let w = -major_axis_magnitude * light.projection[2][3] + light.projection[3][3]; - - // For perspective_rh: - // let proj_r = light.far / (light.near - light.far); - // let z = -major_axis_magnitude * proj_r + light.near * proj_r; - // let w = major_axis_magnitude; - - // For perspective_infinite_reverse_rh: - // let z = light.near; - // let w = major_axis_magnitude; - - let depth = z / w; + let zw = -major_axis_magnitude * light.projection_lr.xy + light.projection_lr.zw; + let depth = zw.x / zw.y; // do the lookup, using HW PCF and comparison // NOTE: Due to the non-uniform control flow above, we must use the Level variant of - // textureSampleCompare to avoid undefined behaviour due to some of the fragments in - // a quad (2x2 fragments) being processed not being sampled, and this messing with - // mip-mapping functionality. The shadow maps have no mipmaps so Level just samples - // from LOD 0. + // textureSampleCompare to avoid undefined behaviour due to some of the fragments in + // a quad (2x2 fragments) being processed not being sampled, and this messing with + // mip-mapping functionality. The shadow maps have no mipmaps so Level just samples + // from LOD 0. return textureSampleCompareLevel(point_shadow_textures, point_shadow_textures_sampler, frag_ls, i32(light_id), depth); } -fn fetch_directional_shadow(light_id: i32, frag_position: vec4, surface_normal: vec3) -> f32 { +fn fetch_directional_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { let light = lights.directional_lights[light_id]; // The normal bias is scaled to the texel size. @@ -384,12 +407,25 @@ fn fetch_directional_shadow(light_id: i32, frag_position: vec4, surface_nor let depth = offset_position_ndc.z; // do the lookup, using HW PCF and comparison // NOTE: Due to non-uniform control flow above, we must use the level variant of the texture - // sampler to avoid use of implicit derivatives causing possible undefined behavior. + // sampler to avoid use of implicit derivatives causing possible undefined behavior. return textureSampleCompareLevel(directional_shadow_textures, directional_shadow_textures_sampler, light_local, i32(light_id), depth); } +fn hsv2rgb(hue: f32, saturation: f32, value: f32) -> vec3 { + let rgb = clamp( + abs( + ((hue * 6.0 + vec3(0.0, 4.0, 2.0)) % 6.0) - 3.0 + ) - 1.0, + vec3(0.0), + vec3(1.0) + ); + + return value * mix( vec3(1.0), rgb, vec3(saturation)); +} + struct FragmentInput { [[builtin(front_facing)]] is_front: bool; + [[builtin(position)]] frag_coord: vec4; [[location(0)]] world_position: vec4; [[location(1)]] world_normal: vec3; [[location(2)]] uv: vec2; @@ -495,28 +531,34 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { // accumulate color var light_accum: vec3 = vec3(0.0); - let n_point_lights = i32(lights.n_point_lights); - let n_directional_lights = i32(lights.n_directional_lights); - for (var i: i32 = 0; i < n_point_lights; i = i + 1) { - let light = lights.point_lights[i]; - var shadow: f32; + + let view_z = dot(vec4( + view.inverse_view[0].z, + view.inverse_view[1].z, + view.inverse_view[2].z, + view.inverse_view[3].z + ), in.world_position); + let cluster_index = fragment_cluster_index(in.frag_coord.xy, view_z); + let offset_and_count = unpack_offset_and_count(cluster_index); + for (var i: u32 = offset_and_count.offset; i < offset_and_count.offset + offset_and_count.count; i = i + 1u) { + let light_id = get_light_id(i); + let light = point_lights.data[light_id]; + var shadow: f32 = 1.0; if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && (light.flags & POINT_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { - shadow = fetch_point_shadow(i, in.world_position, in.world_normal); - } else { - shadow = 1.0; + shadow = fetch_point_shadow(light_id, in.world_position, in.world_normal); } let light_contrib = point_light(in.world_position.xyz, light, roughness, NdotV, N, V, R, F0, diffuse_color); light_accum = light_accum + light_contrib * shadow; } - for (var i: i32 = 0; i < n_directional_lights; i = i + 1) { + + let n_directional_lights = lights.n_directional_lights; + for (var i: u32 = 0u; i < n_directional_lights; i = i + 1u) { let light = lights.directional_lights[i]; - var shadow: f32; + var shadow: f32 = 1.0; if ((mesh.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u && (light.flags & DIRECTIONAL_LIGHT_FLAGS_SHADOWS_ENABLED_BIT) != 0u) { shadow = fetch_directional_shadow(i, in.world_position, in.world_normal); - } else { - shadow = 1.0; } let light_contrib = directional_light(light, roughness, NdotV, N, V, R, F0, diffuse_color); light_accum = light_accum + light_contrib * shadow; @@ -531,6 +573,32 @@ fn fragment(in: FragmentInput) -> [[location(0)]] vec4 { emissive.rgb * output_color.a, output_color.a); +#ifdef CLUSTERED_FORWARD_DEBUG + // Cluster allocation debug (using 'over' alpha blending) + let cluster_debug_mode = 1; + let cluster_overlay_alpha = 1.0; + if (cluster_debug_mode == 0) { + // NOTE: This debug mode visualises the z-slices + var z_slice: u32 = view_z_to_z_slice(view_z); + // A hack to make the colors alternate a bit more + if ((z_slice & 1u) == 1u) { + z_slice = z_slice + lights.cluster_dimensions.z / 2u; + } + let slice_color = hsv2rgb(f32(z_slice) / f32(lights.cluster_dimensions.z + 1u), 1.0, 0.5); + output_color = vec4( + (1.0 - cluster_overlay_alpha) * output_color.rgb + cluster_overlay_alpha * slice_color, + output_color.a + ); + } elseif (cluster_debug_mode == 1) { + // NOTE: This debug mode visualises the number of lights within the cluster that contains + // the fragment. It shows a sort of lighting complexity measure. + output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r + + cluster_overlay_alpha * smoothStep(0.0, 16.0, f32(offset_and_count.count)); + output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + + cluster_overlay_alpha * (1.0 - smoothStep(0.0, 16.0, f32(offset_and_count.count))); + } +#endif + // tone_mapping output_color = vec4(reinhard_luminance(output_color.rgb), output_color.a); // Gamma correction. diff --git a/pipelined/bevy_render2/src/camera/bundle.rs b/pipelined/bevy_render2/src/camera/bundle.rs index 31beff655b9f5..8f61527345954 100644 --- a/pipelined/bevy_render2/src/camera/bundle.rs +++ b/pipelined/bevy_render2/src/camera/bundle.rs @@ -42,6 +42,8 @@ impl PerspectiveCameraBundle { PerspectiveCameraBundle { camera: Camera { name: Some(name.to_string()), + near: perspective_projection.near, + far: perspective_projection.far, ..Default::default() }, perspective_projection, @@ -94,6 +96,8 @@ impl OrthographicCameraBundle { OrthographicCameraBundle { camera: Camera { name: Some(CameraPlugin::CAMERA_2D.to_string()), + near: orthographic_projection.near, + far: orthographic_projection.far, ..Default::default() }, orthographic_projection, @@ -120,6 +124,8 @@ impl OrthographicCameraBundle { OrthographicCameraBundle { camera: Camera { name: Some(CameraPlugin::CAMERA_3D.to_string()), + near: orthographic_projection.near, + far: orthographic_projection.far, ..Default::default() }, orthographic_projection, @@ -142,6 +148,8 @@ impl OrthographicCameraBundle { OrthographicCameraBundle { camera: Camera { name: Some(name.to_string()), + near: orthographic_projection.near, + far: orthographic_projection.far, ..Default::default() }, orthographic_projection, diff --git a/pipelined/bevy_render2/src/camera/camera.rs b/pipelined/bevy_render2/src/camera/camera.rs index 7a05bbe9ff1b2..0b74ce8a09901 100644 --- a/pipelined/bevy_render2/src/camera/camera.rs +++ b/pipelined/bevy_render2/src/camera/camera.rs @@ -23,6 +23,8 @@ pub struct Camera { pub window: WindowId, #[reflect(ignore)] pub depth_calculation: DepthCalculation, + pub near: f32, + pub far: f32, } #[derive(Debug, Clone, Copy, Reflect, Serialize, Deserialize)] diff --git a/pipelined/bevy_render2/src/camera/mod.rs b/pipelined/bevy_render2/src/camera/mod.rs index 4e2c146ea76fe..2fc020447a45b 100644 --- a/pipelined/bevy_render2/src/camera/mod.rs +++ b/pipelined/bevy_render2/src/camera/mod.rs @@ -88,6 +88,8 @@ fn extract_cameras( transform: *transform, width: window.physical_width().max(1), height: window.physical_height().max(1), + near: camera.near, + far: camera.far, }, visible_entities.clone(), )); diff --git a/pipelined/bevy_render2/src/primitives/mod.rs b/pipelined/bevy_render2/src/primitives/mod.rs index 7d0df7a1be4c6..a99f0f2435dfc 100644 --- a/pipelined/bevy_render2/src/primitives/mod.rs +++ b/pipelined/bevy_render2/src/primitives/mod.rs @@ -32,6 +32,23 @@ impl Aabb { .abs() .dot(half_extents) } + + pub fn min(&self) -> Vec3 { + self.center - self.half_extents + } + + pub fn max(&self) -> Vec3 { + self.center + self.half_extents + } +} + +impl From for Aabb { + fn from(sphere: Sphere) -> Self { + Self { + center: sphere.center, + half_extents: Vec3::splat(sphere.radius), + } + } } #[derive(Debug, Default)] @@ -41,12 +58,12 @@ pub struct Sphere { } impl Sphere { - pub fn intersects_obb(&self, aabb: &Aabb, model_to_world: &Mat4) -> bool { - let aabb_center_world = *model_to_world * aabb.center.extend(1.0); + pub fn intersects_obb(&self, aabb: &Aabb, local_to_world: &Mat4) -> bool { + let aabb_center_world = *local_to_world * aabb.center.extend(1.0); let axes = [ - Vec3A::from(model_to_world.x_axis), - Vec3A::from(model_to_world.y_axis), - Vec3A::from(model_to_world.z_axis), + Vec3A::from(local_to_world.x_axis), + Vec3A::from(local_to_world.y_axis), + Vec3A::from(local_to_world.z_axis), ]; let v = Vec3A::from(aabb_center_world) - Vec3A::from(self.center); let d = v.length(); diff --git a/pipelined/bevy_render2/src/render_resource/uniform_vec.rs b/pipelined/bevy_render2/src/render_resource/uniform_vec.rs index 73d9c2a535fca..8cbc1f09c910f 100644 --- a/pipelined/bevy_render2/src/render_resource/uniform_vec.rs +++ b/pipelined/bevy_render2/src/render_resource/uniform_vec.rs @@ -63,6 +63,10 @@ impl UniformVec { index } + pub fn get_mut(&mut self, index: usize) -> &mut T { + &mut self.values[index] + } + pub fn reserve(&mut self, capacity: usize, device: &RenderDevice) -> bool { if capacity > self.capacity { self.capacity = capacity; @@ -96,6 +100,10 @@ impl UniformVec { pub fn clear(&mut self) { self.values.clear(); } + + pub fn values(&self) -> &[T] { + &self.values + } } pub struct DynamicUniformVec { diff --git a/pipelined/bevy_render2/src/view/mod.rs b/pipelined/bevy_render2/src/view/mod.rs index 2db9a26176137..2134638ed7520 100644 --- a/pipelined/bevy_render2/src/view/mod.rs +++ b/pipelined/bevy_render2/src/view/mod.rs @@ -64,13 +64,20 @@ pub struct ExtractedView { pub transform: GlobalTransform, pub width: u32, pub height: u32, + pub near: f32, + pub far: f32, } #[derive(Clone, AsStd140)] pub struct ViewUniform { view_proj: Mat4, + inverse_view: Mat4, projection: Mat4, world_position: Vec3, + near: f32, + far: f32, + width: f32, + height: f32, } #[derive(Default)] @@ -123,11 +130,17 @@ fn prepare_view_uniforms( view_uniforms.uniforms.clear(); for (entity, camera) in views.iter() { let projection = camera.projection; + let inverse_view = camera.transform.compute_matrix().inverse(); let view_uniforms = ViewUniformOffset { offset: view_uniforms.uniforms.push(ViewUniform { - view_proj: projection * camera.transform.compute_matrix().inverse(), + view_proj: projection * inverse_view, + inverse_view, projection, world_position: camera.transform.translation, + near: camera.near, + far: camera.far, + width: camera.width as f32, + height: camera.height as f32, }), }; diff --git a/pipelined/bevy_render2/src/view/visibility/mod.rs b/pipelined/bevy_render2/src/view/visibility/mod.rs index 5fee0293ec88e..50924d06ae466 100644 --- a/pipelined/bevy_render2/src/view/visibility/mod.rs +++ b/pipelined/bevy_render2/src/view/visibility/mod.rs @@ -40,22 +40,25 @@ impl Default for ComputedVisibility { } } -#[derive(Clone, Debug)] -pub struct VisibleEntity { - pub entity: Entity, -} - -#[derive(Component, Clone, Default, Debug, Reflect)] +#[derive(Clone, Component, Default, Debug, Reflect)] #[reflect(Component)] pub struct VisibleEntities { #[reflect(ignore)] - pub entities: Vec, + pub entities: Vec, } impl VisibleEntities { - pub fn iter(&self) -> impl DoubleEndedIterator { + pub fn iter(&self) -> impl DoubleEndedIterator { self.entities.iter() } + + pub fn len(&self) -> usize { + self.entities.len() + } + + pub fn is_empty(&self) -> bool { + self.entities.is_empty() + } } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] @@ -178,7 +181,7 @@ pub fn check_visibility( } computed_visibility.is_visible = true; - visible_entities.entities.push(VisibleEntity { entity }); + visible_entities.entities.push(entity); } // TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize