diff --git a/pipelined/bevy_gltf2/src/loader.rs b/pipelined/bevy_gltf2/src/loader.rs index ea2dbb0b18..24c75d72db 100644 --- a/pipelined/bevy_gltf2/src/loader.rs +++ b/pipelined/bevy_gltf2/src/loader.rs @@ -5,7 +5,7 @@ use bevy_asset::{ use bevy_core::Name; use bevy_ecs::world::World; use bevy_log::warn; -use bevy_math::Mat4; +use bevy_math::{Mat4, Vec3}; use bevy_pbr2::{PbrBundle, StandardMaterial}; use bevy_render2::{ camera::{ @@ -13,6 +13,7 @@ use bevy_render2::{ }, color::Color, mesh::{Indices, Mesh, VertexAttributeValues}, + primitives::Aabb, texture::{Image, ImageType, TextureError}, }; use bevy_scene::Scene; @@ -528,11 +529,17 @@ fn load_node( let material_asset_path = AssetPath::new_ref(load_context.path(), Some(&material_label)); - parent.spawn_bundle(PbrBundle { - mesh: load_context.get_handle(mesh_asset_path), - material: load_context.get_handle(material_asset_path), - ..Default::default() - }); + let bounds = primitive.bounding_box(); + parent + .spawn_bundle(PbrBundle { + mesh: load_context.get_handle(mesh_asset_path), + material: load_context.get_handle(material_asset_path), + ..Default::default() + }) + .insert(Aabb::from_min_max( + Vec3::from_slice(&bounds.min), + Vec3::from_slice(&bounds.max), + )); } } diff --git a/pipelined/bevy_pbr2/src/bundle.rs b/pipelined/bevy_pbr2/src/bundle.rs index cc7a6b5198..220e8291ee 100644 --- a/pipelined/bevy_pbr2/src/bundle.rs +++ b/pipelined/bevy_pbr2/src/bundle.rs @@ -1,7 +1,11 @@ use crate::{DirectionalLight, PointLight, StandardMaterial}; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; -use bevy_render2::mesh::Mesh; +use bevy_render2::{ + mesh::Mesh, + primitives::{CubemapFrusta, Frustum}, + view::{ComputedVisibility, Visibility, VisibleEntities}, +}; use bevy_transform::components::{GlobalTransform, Transform}; #[derive(Bundle, Clone)] @@ -10,6 +14,10 @@ pub struct PbrBundle { pub material: Handle, pub transform: Transform, pub global_transform: GlobalTransform, + /// User indication of whether an entity is visible + pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } impl Default for PbrBundle { @@ -19,14 +27,41 @@ impl Default for PbrBundle { material: Default::default(), transform: Default::default(), global_transform: Default::default(), + visibility: Default::default(), + computed_visibility: Default::default(), } } } +#[derive(Clone, Debug, Default)] +pub struct CubemapVisibleEntities { + data: [VisibleEntities; 6], +} + +impl CubemapVisibleEntities { + pub fn get(&self, i: usize) -> &VisibleEntities { + &self.data[i] + } + + pub fn get_mut(&mut self, i: usize) -> &mut VisibleEntities { + &mut self.data[i] + } + + pub fn iter(&self) -> impl DoubleEndedIterator { + self.data.iter() + } + + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.data.iter_mut() + } +} + /// A component bundle for "point light" entities #[derive(Debug, Bundle, Default)] pub struct PointLightBundle { pub point_light: PointLight, + pub cubemap_visible_entities: CubemapVisibleEntities, + pub cubemap_frusta: CubemapFrusta, pub transform: Transform, pub global_transform: GlobalTransform, } @@ -35,6 +70,8 @@ pub struct PointLightBundle { #[derive(Debug, Bundle, Default)] pub struct DirectionalLightBundle { pub directional_light: DirectionalLight, + pub frustum: Frustum, + pub visible_entities: VisibleEntities, pub transform: Transform, pub global_transform: GlobalTransform, } diff --git a/pipelined/bevy_pbr2/src/lib.rs b/pipelined/bevy_pbr2/src/lib.rs index 23dfb56fa5..67c5c8e302 100644 --- a/pipelined/bevy_pbr2/src/lib.rs +++ b/pipelined/bevy_pbr2/src/lib.rs @@ -18,8 +18,10 @@ use bevy_render2::{ render_graph::RenderGraph, render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions}, render_resource::{Shader, SpecializedPipelines}, + view::VisibilitySystems, RenderApp, RenderStage, }; +use bevy_transform::TransformSystem; pub mod draw_3d_graph { pub mod node { @@ -49,20 +51,53 @@ impl Plugin for PbrPlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::(); + .init_resource::() + .add_system_to_stage( + CoreStage::PostUpdate, + update_directional_light_frusta + .label(SimulationLightSystems::UpdateDirectionalLightFrusta) + .after(TransformSystem::TransformPropagate), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_point_light_frusta + .label(SimulationLightSystems::UpdatePointLightFrusta) + .after(TransformSystem::TransformPropagate), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + check_light_visibility + .label(SimulationLightSystems::CheckLightVisibility) + .after(TransformSystem::TransformPropagate) + .after(VisibilitySystems::CalculateBounds) + .after(SimulationLightSystems::UpdateDirectionalLightFrusta) + .after(SimulationLightSystems::UpdatePointLightFrusta) + // NOTE: This MUST be scheduled AFTER the core renderer visibility check + // because that resets entity ComputedVisibility for the first view + // which would override any results from this otherwise + .after(VisibilitySystems::CheckVisibility), + ); let render_app = app.sub_app(RenderApp); render_app .add_system_to_stage(RenderStage::Extract, render::extract_meshes) - .add_system_to_stage(RenderStage::Extract, render::extract_lights) + .add_system_to_stage( + RenderStage::Extract, + render::extract_lights.label(RenderLightSystems::ExtractLights), + ) .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_lights.exclusive_system(), + render::prepare_lights + .exclusive_system() + .label(RenderLightSystems::PrepareLights), ) .add_system_to_stage(RenderStage::Queue, render::queue_meshes) - .add_system_to_stage(RenderStage::Queue, render::queue_shadows) + .add_system_to_stage( + RenderStage::Queue, + render::queue_shadows.label(RenderLightSystems::QueueShadows), + ) .add_system_to_stage(RenderStage::Queue, render::queue_shadow_view_bind_group) .add_system_to_stage(RenderStage::Queue, render::queue_transform_bind_group) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) diff --git a/pipelined/bevy_pbr2/src/light.rs b/pipelined/bevy_pbr2/src/light.rs index c5b2f60f56..f662781be0 100644 --- a/pipelined/bevy_pbr2/src/light.rs +++ b/pipelined/bevy_pbr2/src/light.rs @@ -1,4 +1,14 @@ -use bevy_render2::{camera::OrthographicProjection, color::Color}; +use bevy_ecs::prelude::*; +use bevy_math::Mat4; +use bevy_render2::{ + camera::{CameraProjection, OrthographicProjection}, + color::Color, + primitives::{Aabb, CubemapFrusta, Frustum, Sphere}, + view::{ComputedVisibility, RenderLayers, Visibility, VisibleEntities, VisibleEntity}, +}; +use bevy_transform::components::GlobalTransform; + +use crate::{CubeMapFace, CubemapVisibleEntities, CUBE_MAP_FACES}; /// A light that emits light in all directions from a central point. /// @@ -156,3 +166,177 @@ impl Default for AmbientLight { pub struct NotShadowCaster; /// Add this component to make a `Mesh` not receive shadows pub struct NotShadowReceiver; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum SimulationLightSystems { + UpdateDirectionalLightFrusta, + UpdatePointLightFrusta, + CheckLightVisibility, +} + +pub fn update_directional_light_frusta( + mut views: Query<(&GlobalTransform, &DirectionalLight, &mut Frustum)>, +) { + for (transform, directional_light, mut frustum) in views.iter_mut() { + let view_projection = directional_light.shadow_projection.get_projection_matrix() + * transform.compute_matrix().inverse(); + *frustum = Frustum::from_view_projection( + &view_projection, + &transform.translation, + &transform.back(), + directional_light.shadow_projection.far(), + ); + } +} + +pub fn update_point_light_frusta( + mut views: Query<(&GlobalTransform, &PointLight, &mut CubemapFrusta)>, +) { + let projection = Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1); + 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() { + // 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(transform.translation); + let view_backward = transform.back(); + + for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { + let view = view_translation * *view_rotation; + let view_projection = projection * view.compute_matrix().inverse(); + + *frustum = Frustum::from_view_projection( + &view_projection, + &transform.translation, + &view_backward, + point_light.range, + ); + } + } +} + +pub fn check_light_visibility( + mut point_lights: Query<( + &PointLight, + &GlobalTransform, + &CubemapFrusta, + &mut CubemapVisibleEntities, + Option<&RenderLayers>, + )>, + mut directional_lights: Query< + (&Frustum, &mut VisibleEntities, Option<&RenderLayers>), + With, + >, + mut visible_entity_query: Query< + ( + Entity, + &Visibility, + &mut ComputedVisibility, + Option<&RenderLayers>, + Option<&Aabb>, + Option<&GlobalTransform>, + ), + Without, + >, +) { + // Directonal lights + for (frustum, mut visible_entities, maybe_view_mask) in directional_lights.iter_mut() { + visible_entities.entities.clear(); + let view_mask = maybe_view_mask.copied().unwrap_or_default(); + + 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; + } + + // If we have an aabb and transform, do frustum culling + if let (Some(aabb), Some(transform)) = (maybe_aabb, maybe_transform) { + if !frustum.intersects_obb(aabb, &transform.compute_matrix()) { + continue; + } + } + + computed_visibility.is_visible = true; + 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 + } + + // 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(); + } + 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; + } + + // 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(VisibleEntity { 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 + } +} diff --git a/pipelined/bevy_pbr2/src/render/light.rs b/pipelined/bevy_pbr2/src/render/light.rs index 1ecfa4f75c..e839f4785a 100644 --- a/pipelined/bevy_pbr2/src/render/light.rs +++ b/pipelined/bevy_pbr2/src/render/light.rs @@ -1,6 +1,7 @@ use crate::{ - AmbientLight, DirectionalLight, DirectionalLightShadowMap, MeshUniform, NotShadowCaster, - PbrPipeline, PointLight, PointLightShadowMap, TransformBindGroup, SHADOW_SHADER_HANDLE, + AmbientLight, CubemapVisibleEntities, DirectionalLight, DirectionalLightShadowMap, MeshUniform, + NotShadowCaster, PbrPipeline, PointLight, PointLightShadowMap, TransformBindGroup, + SHADOW_SHADER_HANDLE, }; use bevy_asset::Handle; use bevy_core::FloatOrd; @@ -23,12 +24,19 @@ use bevy_render2::{ render_resource::*, renderer::{RenderContext, RenderDevice, RenderQueue}, texture::*, - view::{ExtractedView, ViewUniformOffset, ViewUniforms}, + view::{ExtractedView, ViewUniformOffset, ViewUniforms, VisibleEntities, VisibleEntity}, }; use bevy_transform::components::GlobalTransform; use crevice::std140::AsStd140; use std::num::NonZeroU32; +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum RenderLightSystems { + ExtractLights, + PrepareLights, + QueueShadows, +} + pub struct ExtractedAmbientLight { color: Color, brightness: f32, @@ -273,14 +281,23 @@ impl SpecializedPipeline for ShadowPipeline { } } -// TODO: ultimately these could be filtered down to lights relevant to actual views pub fn extract_lights( mut commands: Commands, ambient_light: Res, point_light_shadow_map: Res, directional_light_shadow_map: Res, - point_lights: Query<(Entity, &PointLight, &GlobalTransform)>, - directional_lights: Query<(Entity, &DirectionalLight, &GlobalTransform)>, + mut point_lights: Query<( + Entity, + &PointLight, + &mut CubemapVisibleEntities, + &GlobalTransform, + )>, + mut directional_lights: Query<( + Entity, + &DirectionalLight, + &mut VisibleEntities, + &GlobalTransform, + )>, ) { commands.insert_resource(ExtractedAmbientLight { color: ambient_light.color, @@ -298,24 +315,28 @@ 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, transform) in point_lights.iter() { - commands.get_or_spawn(entity).insert(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, - 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, - }); + 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, + 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, transform) in directional_lights.iter() { + 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 // NOTE: When using various PCF kernel sizes, this will need to be adjusted, according to: @@ -328,9 +349,9 @@ pub fn extract_lights( ); let directional_light_texel_size = largest_dimension / directional_light_shadow_map.size as f32; - commands - .get_or_spawn(entity) - .insert(ExtractedDirectionalLight { + let render_visible_entities = std::mem::take(visible_entities.into_inner()); + commands.get_or_spawn(entity).insert_bundle(( + ExtractedDirectionalLight { color: directional_light.color, illuminance: directional_light.illuminance, direction: transform.forward(), @@ -340,7 +361,9 @@ pub fn extract_lights( shadow_normal_bias: directional_light.shadow_normal_bias * directional_light_texel_size * std::f32::consts::SQRT_2, - }); + }, + render_visible_entities, + )); } } @@ -349,13 +372,13 @@ const NEGATIVE_X: Vec3 = const_vec3!([-1.0, 0.0, 0.0]); const NEGATIVE_Y: Vec3 = const_vec3!([0.0, -1.0, 0.0]); const NEGATIVE_Z: Vec3 = const_vec3!([0.0, 0.0, -1.0]); -struct CubeMapFace { - target: Vec3, - up: Vec3, +pub(crate) struct CubeMapFace { + pub(crate) target: Vec3, + pub(crate) up: Vec3, } // see https://www.khronos.org/opengl/wiki/Cubemap_Texture -const CUBE_MAP_FACES: [CubeMapFace; 6] = [ +pub(crate) const CUBE_MAP_FACES: [CubeMapFace; 6] = [ // 0 GL_TEXTURE_CUBE_MAP_POSITIVE_X CubeMapFace { target: NEGATIVE_X, @@ -420,6 +443,16 @@ pub struct LightMeta { pub shadow_view_bind_group: Option, } +pub enum LightEntity { + Directional { + light_entity: Entity, + }, + Point { + light_entity: Entity, + face_index: usize, + }, +} + #[allow(clippy::too_many_arguments)] pub fn prepare_lights( mut commands: Commands, @@ -431,12 +464,20 @@ pub fn prepare_lights( ambient_light: Res, point_light_shadow_map: Res, directional_light_shadow_map: Res, - point_lights: Query<&ExtractedPointLight>, - directional_lights: Query<&ExtractedDirectionalLight>, + point_lights: Query<(Entity, &ExtractedPointLight)>, + directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, ) { light_meta.view_gpu_lights.clear(); let ambient_color = ambient_light.color.as_rgba_linear() * ambient_light.brightness; + // Pre-calculate for PointLights + let cube_face_projection = + Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1); + let cube_face_rotations = CUBE_MAP_FACES + .iter() + .map(|CubeMapFace { target, up }| GlobalTransform::identity().looking_at(*target, *up)) + .collect::>(); + // set up light data for each view for entity in views.iter() { let point_light_depth_texture = texture_cache.get( @@ -482,19 +523,15 @@ pub fn prepare_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) in point_lights.iter().enumerate().take(MAX_POINT_LIGHTS) { - let projection = - Mat4::perspective_infinite_reverse_rh(std::f32::consts::FRAC_PI_2, 1.0, 0.1); - + for (light_index, (light_entity, light)) in + point_lights.iter().enumerate().take(MAX_POINT_LIGHTS) + { // 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); - for (face_index, CubeMapFace { target, up }) in CUBE_MAP_FACES.iter().enumerate() { - // use the cubemap projection direction - let view_rotation = GlobalTransform::identity().looking_at(*target, *up); - + for (face_index, view_rotation) in cube_face_rotations.iter().enumerate() { let depth_texture_view = point_light_depth_texture .texture @@ -523,17 +560,21 @@ pub fn prepare_lights( ExtractedView { width: point_light_shadow_map.size as u32, height: point_light_shadow_map.size as u32, - transform: view_translation * view_rotation, - projection, + transform: view_translation * *view_rotation, + projection: cube_face_projection, }, RenderPhase::::default(), + LightEntity::Point { + light_entity, + face_index, + }, )) .id(); view_lights.push(view_light_entity); } gpu_lights.point_lights[light_index] = GpuPointLight { - projection, + 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: (light.color.as_rgba_linear() * light.intensity).into(), @@ -547,7 +588,7 @@ pub fn prepare_lights( }; } - for (i, light) in directional_lights + for (i, (light_entity, light)) in directional_lights .iter() .enumerate() .take(MAX_DIRECTIONAL_LIGHTS) @@ -613,6 +654,7 @@ pub fn prepare_lights( projection, }, RenderPhase::::default(), + LightEntity::Directional { light_entity }, )) .id(); view_lights.push(view_light_entity); @@ -681,37 +723,53 @@ pub fn queue_shadow_view_bind_group( pub fn queue_shadows( shadow_draw_functions: Res>, shadow_pipeline: Res, - casting_meshes: Query<(Entity, &Handle), Without>, + casting_meshes: Query<&Handle, Without>, render_meshes: Res>, mut pipelines: ResMut>, mut pipeline_cache: ResMut, mut view_lights: Query<&ViewLights>, - mut view_light_shadow_phases: Query<&mut RenderPhase>, + mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase)>, + point_light_entities: Query<&CubemapVisibleEntities, With>, + directional_light_entities: Query<&VisibleEntities, With>, ) { for view_lights in view_lights.iter_mut() { - // ultimately lights should check meshes for relevancy (ex: light views can "see" different meshes than the main view can) let draw_shadow_mesh = shadow_draw_functions .read() .get_id::() .unwrap(); for view_light_entity in view_lights.lights.iter().copied() { - let mut shadow_phase = view_light_shadow_phases.get_mut(view_light_entity).unwrap(); - // TODO: this should only queue up meshes that are actually visible by each "light view" - for (entity, mesh_handle) in casting_meshes.iter() { + let (light_entity, mut shadow_phase) = + view_light_shadow_phases.get_mut(view_light_entity).unwrap(); + let visible_entities = match light_entity { + LightEntity::Directional { light_entity } => directional_light_entities + .get(*light_entity) + .expect("Failed to get directional light visible entities"), + LightEntity::Point { + light_entity, + face_index, + } => point_light_entities + .get(*light_entity) + .expect("Failed to get point light visible entities") + .get(*face_index), + }; + for VisibleEntity { entity, .. } in visible_entities.iter() { let mut key = ShadowPipelineKey::empty(); - if let Some(mesh) = render_meshes.get(mesh_handle) { - if mesh.has_tangents { - key |= ShadowPipelineKey::VERTEX_TANGENTS; + 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; + } } - } - let pipeline_id = pipelines.specialize(&mut pipeline_cache, &shadow_pipeline, key); + let pipeline_id = + pipelines.specialize(&mut pipeline_cache, &shadow_pipeline, key); - shadow_phase.add(Shadow { - draw_function: draw_shadow_mesh, - pipeline: pipeline_id, - entity, - distance: 0.0, // TODO: sort back-to-front - }) + shadow_phase.add(Shadow { + draw_function: draw_shadow_mesh, + pipeline: pipeline_id, + entity: *entity, + distance: 0.0, // TODO: sort back-to-front + }); + } } } } diff --git a/pipelined/bevy_pbr2/src/render/mod.rs b/pipelined/bevy_pbr2/src/render/mod.rs index ec0e925d93..c6872af3d1 100644 --- a/pipelined/bevy_pbr2/src/render/mod.rs +++ b/pipelined/bevy_pbr2/src/render/mod.rs @@ -21,7 +21,9 @@ use bevy_render2::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, GpuImage, Image, TextureFormatPixelInfo}, - view::{ExtractedView, Msaa, ViewUniformOffset, ViewUniforms}, + view::{ + ComputedVisibility, ExtractedView, Msaa, ViewUniformOffset, ViewUniforms, VisibleEntities, + }, }; use bevy_transform::components::GlobalTransform; use crevice::std140::AsStd140; @@ -70,6 +72,7 @@ pub fn extract_meshes( caster_query: Query< ( Entity, + &ComputedVisibility, &GlobalTransform, &Handle, Option<&NotShadowReceiver>, @@ -79,6 +82,7 @@ pub fn extract_meshes( not_caster_query: Query< ( Entity, + &ComputedVisibility, &GlobalTransform, &Handle, Option<&NotShadowReceiver>, @@ -87,7 +91,10 @@ pub fn extract_meshes( >, ) { let mut caster_values = Vec::with_capacity(*previous_caster_len); - for (entity, transform, handle, not_receiver) in caster_query.iter() { + for (entity, computed_visibility, transform, handle, not_receiver) in caster_query.iter() { + if !computed_visibility.is_visible { + continue; + } let transform = transform.compute_matrix(); caster_values.push(( entity, @@ -109,7 +116,10 @@ pub fn extract_meshes( commands.insert_or_spawn_batch(caster_values); let mut not_caster_values = Vec::with_capacity(*previous_not_caster_len); - for (entity, transform, handle, not_receiver) in not_caster_query.iter() { + for (entity, computed_visibility, transform, handle, not_receiver) in not_caster_query.iter() { + if !computed_visibility.is_visible { + continue; + } let transform = transform.compute_matrix(); not_caster_values.push(( entity, @@ -613,16 +623,12 @@ pub fn queue_meshes( view_uniforms: Res, render_meshes: Res>, render_materials: Res>, - standard_material_meshes: Query<( - Entity, - &Handle, - &Handle, - &MeshUniform, - )>, + standard_material_meshes: Query<(&Handle, &Handle, &MeshUniform)>, mut views: Query<( Entity, &ExtractedView, &ViewLights, + &VisibleEntities, &mut RenderPhase, )>, ) { @@ -630,7 +636,8 @@ pub fn queue_meshes( view_uniforms.uniforms.binding(), light_meta.view_gpu_lights.binding(), ) { - for (entity, view, view_lights, mut transparent_phase) in views.iter_mut() { + for (entity, view, view_lights, visible_entities, mut transparent_phase) in views.iter_mut() + { let view_bind_group = render_device.create_bind_group(&BindGroupDescriptor { entries: &[ BindGroupEntry { @@ -680,37 +687,39 @@ pub fn queue_meshes( let view_matrix = view.transform.compute_matrix(); let view_row_2 = view_matrix.row(2); - for (entity, material_handle, mesh_handle, mesh_uniform) in - standard_material_meshes.iter() - { - let mut key = PbrPipelineKey::from_msaa_samples(msaa.samples); - if let Some(material) = render_materials.get(material_handle) { - if material - .flags - .contains(StandardMaterialFlags::NORMAL_MAP_TEXTURE) - { - key |= PbrPipelineKey::STANDARDMATERIAL_NORMAL_MAP; + for visible_entity in &visible_entities.entities { + if let Ok((material_handle, mesh_handle, mesh_uniform)) = + standard_material_meshes.get(visible_entity.entity) + { + let mut key = PbrPipelineKey::from_msaa_samples(msaa.samples); + if let Some(material) = render_materials.get(material_handle) { + if material + .flags + .contains(StandardMaterialFlags::NORMAL_MAP_TEXTURE) + { + key |= PbrPipelineKey::STANDARDMATERIAL_NORMAL_MAP; + } + } else { + continue; } - } else { - continue; - } - if let Some(mesh) = render_meshes.get(mesh_handle) { - if mesh.has_tangents { - key |= PbrPipelineKey::VERTEX_TANGENTS; + if let Some(mesh) = render_meshes.get(mesh_handle) { + if mesh.has_tangents { + key |= PbrPipelineKey::VERTEX_TANGENTS; + } } - } - let pipeline_id = pipelines.specialize(&mut pipeline_cache, &pbr_pipeline, key); + let pipeline_id = pipelines.specialize(&mut pipeline_cache, &pbr_pipeline, key); - // NOTE: row 2 of the view matrix dotted with column 3 of the model matrix - // gives the z component of translation of the mesh in view space - let mesh_z = view_row_2.dot(mesh_uniform.transform.col(3)); - // TODO: currently there is only "transparent phase". this should pick transparent vs opaque according to the mesh material - transparent_phase.add(Transparent3d { - entity, - draw_function: draw_pbr, - pipeline: pipeline_id, - distance: mesh_z, - }); + // NOTE: row 2 of the view matrix dotted with column 3 of the model matrix + // gives the z component of translation of the mesh in view space + let mesh_z = view_row_2.dot(mesh_uniform.transform.col(3)); + // TODO: currently there is only "transparent phase". this should pick transparent vs opaque according to the mesh material + transparent_phase.add(Transparent3d { + entity: visible_entity.entity, + draw_function: draw_pbr, + pipeline: pipeline_id, + distance: mesh_z, + }); + } } } } diff --git a/pipelined/bevy_render2/src/camera/bundle.rs b/pipelined/bevy_render2/src/camera/bundle.rs index 265622d844..31beff655b 100644 --- a/pipelined/bevy_render2/src/camera/bundle.rs +++ b/pipelined/bevy_render2/src/camera/bundle.rs @@ -1,10 +1,17 @@ -use crate::camera::{ - Camera, CameraPlugin, DepthCalculation, OrthographicProjection, PerspectiveProjection, - ScalingMode, +use crate::{ + camera::{ + Camera, CameraPlugin, DepthCalculation, OrthographicProjection, PerspectiveProjection, + ScalingMode, + }, + primitives::Frustum, + view::VisibleEntities, }; use bevy_ecs::bundle::Bundle; +use bevy_math::Vec3; use bevy_transform::components::{GlobalTransform, Transform}; +use super::CameraProjection; + /// Component bundle for camera entities with perspective projection /// /// Use this for 3D rendering. @@ -12,6 +19,8 @@ use bevy_transform::components::{GlobalTransform, Transform}; pub struct PerspectiveCameraBundle { pub camera: Camera, pub perspective_projection: PerspectiveProjection, + pub visible_entities: VisibleEntities, + pub frustum: Frustum, pub transform: Transform, pub global_transform: GlobalTransform, } @@ -22,12 +31,22 @@ impl PerspectiveCameraBundle { } pub fn with_name(name: &str) -> Self { + let perspective_projection = PerspectiveProjection::default(); + let view_projection = perspective_projection.get_projection_matrix(); + let frustum = Frustum::from_view_projection( + &view_projection, + &Vec3::ZERO, + &Vec3::Z, + perspective_projection.far(), + ); PerspectiveCameraBundle { camera: Camera { name: Some(name.to_string()), ..Default::default() }, - perspective_projection: Default::default(), + perspective_projection, + visible_entities: VisibleEntities::default(), + frustum, transform: Default::default(), global_transform: Default::default(), } @@ -36,15 +55,7 @@ impl PerspectiveCameraBundle { impl Default for PerspectiveCameraBundle { fn default() -> Self { - PerspectiveCameraBundle { - camera: Camera { - name: Some(CameraPlugin::CAMERA_3D.to_string()), - ..Default::default() - }, - perspective_projection: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - } + PerspectiveCameraBundle::with_name(CameraPlugin::CAMERA_3D) } } @@ -55,6 +66,8 @@ impl Default for PerspectiveCameraBundle { pub struct OrthographicCameraBundle { pub camera: Camera, pub orthographic_projection: OrthographicProjection, + pub visible_entities: VisibleEntities, + pub frustum: Frustum, pub transform: Transform, pub global_transform: GlobalTransform, } @@ -64,44 +77,76 @@ impl OrthographicCameraBundle { // we want 0 to be "closest" and +far to be "farthest" in 2d, so we offset // the camera's translation by far and use a right handed coordinate system let far = 1000.0; + let orthographic_projection = OrthographicProjection { + far, + depth_calculation: DepthCalculation::ZDifference, + ..Default::default() + }; + let transform = Transform::from_xyz(0.0, 0.0, far - 0.1); + let view_projection = + orthographic_projection.get_projection_matrix() * transform.compute_matrix().inverse(); + let frustum = Frustum::from_view_projection( + &view_projection, + &transform.translation, + &transform.back(), + orthographic_projection.far(), + ); OrthographicCameraBundle { camera: Camera { name: Some(CameraPlugin::CAMERA_2D.to_string()), ..Default::default() }, - orthographic_projection: OrthographicProjection { - far, - depth_calculation: DepthCalculation::ZDifference, - ..Default::default() - }, - transform: Transform::from_xyz(0.0, 0.0, far - 0.1), + orthographic_projection, + visible_entities: VisibleEntities::default(), + frustum, + transform, global_transform: Default::default(), } } pub fn new_3d() -> Self { + let orthographic_projection = OrthographicProjection { + scaling_mode: ScalingMode::FixedVertical, + depth_calculation: DepthCalculation::Distance, + ..Default::default() + }; + let view_projection = orthographic_projection.get_projection_matrix(); + let frustum = Frustum::from_view_projection( + &view_projection, + &Vec3::ZERO, + &Vec3::Z, + orthographic_projection.far(), + ); OrthographicCameraBundle { camera: Camera { name: Some(CameraPlugin::CAMERA_3D.to_string()), ..Default::default() }, - orthographic_projection: OrthographicProjection { - scaling_mode: ScalingMode::FixedVertical, - depth_calculation: DepthCalculation::Distance, - ..Default::default() - }, + orthographic_projection, + visible_entities: VisibleEntities::default(), + frustum, transform: Default::default(), global_transform: Default::default(), } } pub fn with_name(name: &str) -> Self { + let orthographic_projection = OrthographicProjection::default(); + let view_projection = orthographic_projection.get_projection_matrix(); + let frustum = Frustum::from_view_projection( + &view_projection, + &Vec3::ZERO, + &Vec3::Z, + orthographic_projection.far(), + ); OrthographicCameraBundle { camera: Camera { name: Some(name.to_string()), ..Default::default() }, - orthographic_projection: Default::default(), + orthographic_projection, + visible_entities: VisibleEntities::default(), + frustum, transform: Default::default(), global_transform: Default::default(), } diff --git a/pipelined/bevy_render2/src/camera/mod.rs b/pipelined/bevy_render2/src/camera/mod.rs index c67e744f71..2d97a9f4fc 100644 --- a/pipelined/bevy_render2/src/camera/mod.rs +++ b/pipelined/bevy_render2/src/camera/mod.rs @@ -12,7 +12,11 @@ pub use bundle::*; pub use camera::*; pub use projection::*; -use crate::{view::ExtractedView, RenderApp, RenderStage}; +use crate::{ + primitives::Aabb, + view::{ComputedVisibility, ExtractedView, Visibility, VisibleEntities}, + RenderApp, RenderStage, +}; use bevy_app::{App, CoreStage, Plugin}; use bevy_ecs::prelude::*; @@ -30,6 +34,9 @@ impl Plugin for CameraPlugin { active_cameras.add(Self::CAMERA_2D); active_cameras.add(Self::CAMERA_3D); app.register_type::() + .register_type::() + .register_type::() + .register_type::() .insert_resource(active_cameras) .add_system_to_stage(CoreStage::PostUpdate, crate::camera::active_cameras_system) .add_system_to_stage( @@ -61,12 +68,14 @@ fn extract_cameras( mut commands: Commands, active_cameras: Res, windows: Res, - query: Query<(Entity, &Camera, &GlobalTransform)>, + query: Query<(Entity, &Camera, &GlobalTransform, &VisibleEntities)>, ) { let mut entities = HashMap::default(); for camera in active_cameras.iter() { let name = &camera.name; - if let Some((entity, camera, transform)) = camera.entity.and_then(|e| query.get(e).ok()) { + if let Some((entity, camera, transform, visible_entities)) = + camera.entity.and_then(|e| query.get(e).ok()) + { entities.insert(name.clone(), entity); if let Some(window) = windows.get(camera.window) { commands.get_or_spawn(entity).insert_bundle(( @@ -80,6 +89,7 @@ fn extract_cameras( width: window.physical_width().max(1), height: window.physical_height().max(1), }, + visible_entities.clone(), )); } } diff --git a/pipelined/bevy_render2/src/camera/projection.rs b/pipelined/bevy_render2/src/camera/projection.rs index fabfd2134e..a0f2dd2a9d 100644 --- a/pipelined/bevy_render2/src/camera/projection.rs +++ b/pipelined/bevy_render2/src/camera/projection.rs @@ -8,6 +8,7 @@ pub trait CameraProjection { fn get_projection_matrix(&self) -> Mat4; fn update(&mut self, width: f32, height: f32); fn depth_calculation(&self) -> DepthCalculation; + fn far(&self) -> f32; } #[derive(Debug, Clone, Reflect)] @@ -31,6 +32,10 @@ impl CameraProjection for PerspectiveProjection { fn depth_calculation(&self) -> DepthCalculation { DepthCalculation::Distance } + + fn far(&self) -> f32 { + self.far + } } impl Default for PerspectiveProjection { @@ -146,6 +151,10 @@ impl CameraProjection for OrthographicProjection { fn depth_calculation(&self) -> DepthCalculation { self.depth_calculation } + + fn far(&self) -> f32 { + self.far + } } impl Default for OrthographicProjection { diff --git a/pipelined/bevy_render2/src/lib.rs b/pipelined/bevy_render2/src/lib.rs index 023efdcd46..1e28acc6fe 100644 --- a/pipelined/bevy_render2/src/lib.rs +++ b/pipelined/bevy_render2/src/lib.rs @@ -1,6 +1,7 @@ pub mod camera; pub mod color; pub mod mesh; +pub mod primitives; pub mod render_asset; pub mod render_component; pub mod render_graph; diff --git a/pipelined/bevy_render2/src/mesh/mesh/mod.rs b/pipelined/bevy_render2/src/mesh/mesh/mod.rs index d6a78face3..204af9445d 100644 --- a/pipelined/bevy_render2/src/mesh/mesh/mod.rs +++ b/pipelined/bevy_render2/src/mesh/mesh/mod.rs @@ -1,6 +1,7 @@ mod conversions; use crate::{ + primitives::Aabb, render_asset::{PrepareAssetError, RenderAsset}, render_resource::Buffer, renderer::RenderDevice, @@ -268,8 +269,36 @@ impl Mesh { self.set_attribute(Mesh::ATTRIBUTE_NORMAL, normals); } + + /// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space + pub fn compute_aabb(&self) -> Option { + if let Some(VertexAttributeValues::Float32x3(values)) = + self.attribute(Mesh::ATTRIBUTE_POSITION) + { + let mut minimum = VEC3_MAX; + let mut maximum = VEC3_MIN; + for p in values { + minimum = minimum.min(Vec3::from_slice(p)); + maximum = maximum.max(Vec3::from_slice(p)); + } + if minimum.x != std::f32::MAX + && minimum.y != std::f32::MAX + && minimum.z != std::f32::MAX + && maximum.x != std::f32::MIN + && maximum.y != std::f32::MIN + && maximum.z != std::f32::MIN + { + return Some(Aabb::from_min_max(minimum, maximum)); + } + } + + None + } } +const VEC3_MIN: Vec3 = const_vec3!([std::f32::MIN, std::f32::MIN, std::f32::MIN]); +const VEC3_MAX: Vec3 = const_vec3!([std::f32::MAX, std::f32::MAX, std::f32::MAX]); + fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c)); (b - a).cross(c - a).normalize().into() diff --git a/pipelined/bevy_render2/src/primitives/mod.rs b/pipelined/bevy_render2/src/primitives/mod.rs new file mode 100644 index 0000000000..f72cc2eb4e --- /dev/null +++ b/pipelined/bevy_render2/src/primitives/mod.rs @@ -0,0 +1,140 @@ +use bevy_ecs::reflect::ReflectComponent; +use bevy_math::{Mat4, Vec3, Vec3A, Vec4}; +use bevy_reflect::Reflect; + +/// An Axis-Aligned Bounding Box +#[derive(Clone, Debug, Default, Reflect)] +#[reflect(Component)] +pub struct Aabb { + pub center: Vec3, + pub half_extents: Vec3, +} + +impl Aabb { + pub fn from_min_max(minimum: Vec3, maximum: Vec3) -> Self { + let center = 0.5 * (maximum + minimum); + let half_extents = 0.5 * (maximum - minimum); + Self { + center, + half_extents, + } + } + + /// Calculate the relative radius of the AABB with respect to a plane + pub fn relative_radius(&self, p_normal: &Vec3A, axes: &[Vec3A]) -> f32 { + // NOTE: dot products on Vec3A use SIMD and even with the overhead of conversion are net faster than Vec3 + let half_extents = Vec3A::from(self.half_extents); + Vec3A::new( + p_normal.dot(axes[0]), + p_normal.dot(axes[1]), + p_normal.dot(axes[2]), + ) + .abs() + .dot(half_extents) + } +} + +#[derive(Debug, Default)] +pub struct Sphere { + pub center: Vec3, + pub radius: f32, +} + +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); + let axes = [ + Vec3A::from(model_to_world.x_axis), + Vec3A::from(model_to_world.y_axis), + Vec3A::from(model_to_world.z_axis), + ]; + let v = Vec3A::from(aabb_center_world) - Vec3A::from(self.center); + let d = v.length(); + let relative_radius = aabb.relative_radius(&(v / d), &axes); + d < self.radius + relative_radius + } +} + +/// A plane defined by a normal and distance value along the normal +/// Any point p is in the plane if n.p = d +/// For planes defining half-spaces such as for frusta, if n.p > d then p is on the positive side of the plane. +#[derive(Clone, Copy, Debug, Default)] +pub struct Plane { + pub normal_d: Vec4, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Frustum { + pub planes: [Plane; 6], +} + +impl Frustum { + // NOTE: This approach of extracting the frustum planes from the view + // projection matrix is from Foundations of Game Engine Development 2 + // Rendering by Lengyel. Slight modification has been made for when + // the far plane is infinite but we still want to cull to a far plane. + pub fn from_view_projection( + view_projection: &Mat4, + view_translation: &Vec3, + view_backward: &Vec3, + far: f32, + ) -> Self { + let row3 = view_projection.row(3); + let mut planes = [Plane::default(); 6]; + for (i, plane) in planes.iter_mut().enumerate().take(5) { + let row = view_projection.row(i / 2); + plane.normal_d = if (i & 1) == 0 && i != 4 { + row3 + row + } else { + row3 - row + } + .normalize(); + } + let far_center = *view_translation - far * *view_backward; + planes[5].normal_d = view_backward + .extend(-view_backward.dot(far_center)) + .normalize(); + Self { planes } + } + + pub fn intersects_sphere(&self, sphere: &Sphere) -> bool { + for plane in &self.planes { + if plane.normal_d.dot(sphere.center.extend(1.0)) + sphere.radius <= 0.0 { + return false; + } + } + true + } + + pub fn intersects_obb(&self, aabb: &Aabb, model_to_world: &Mat4) -> bool { + let aabb_center_world = *model_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), + ]; + + for plane in &self.planes { + let p_normal = Vec3A::from(plane.normal_d); + let relative_radius = aabb.relative_radius(&p_normal, &axes); + if plane.normal_d.dot(aabb_center_world) + relative_radius <= 0.0 { + return false; + } + } + true + } +} + +#[derive(Debug, Default)] +pub struct CubemapFrusta { + pub frusta: [Frustum; 6], +} + +impl CubemapFrusta { + pub fn iter(&self) -> impl DoubleEndedIterator { + self.frusta.iter() + } + pub fn iter_mut(&mut self) -> impl DoubleEndedIterator { + self.frusta.iter_mut() + } +} diff --git a/pipelined/bevy_render2/src/view/mod.rs b/pipelined/bevy_render2/src/view/mod.rs index 17ea7704be..008a3336bb 100644 --- a/pipelined/bevy_render2/src/view/mod.rs +++ b/pipelined/bevy_render2/src/view/mod.rs @@ -1,5 +1,7 @@ +pub mod visibility; pub mod window; +pub use visibility::*; use wgpu::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages}; pub use window::*; @@ -20,7 +22,8 @@ pub struct ViewPlugin; impl Plugin for ViewPlugin { fn build(&self, app: &mut App) { - app.init_resource::(); + app.init_resource::().add_plugin(VisibilityPlugin); + app.sub_app(RenderApp) .init_resource::() .add_system_to_stage(RenderStage::Extract, extract_msaa) diff --git a/pipelined/bevy_render2/src/view/visibility/mod.rs b/pipelined/bevy_render2/src/view/visibility/mod.rs new file mode 100644 index 0000000000..e82044518d --- /dev/null +++ b/pipelined/bevy_render2/src/view/visibility/mod.rs @@ -0,0 +1,187 @@ +mod render_layers; + +pub use render_layers::*; + +use bevy_app::{CoreStage, Plugin}; +use bevy_asset::{Assets, Handle}; +use bevy_ecs::prelude::*; +use bevy_reflect::Reflect; +use bevy_transform::{components::GlobalTransform, TransformSystem}; + +use crate::{ + camera::{Camera, CameraProjection, OrthographicProjection, PerspectiveProjection}, + mesh::Mesh, + primitives::{Aabb, Frustum}, +}; + +/// User indication of whether an entity is visible +#[derive(Clone, Reflect)] +#[reflect(Component)] +pub struct Visibility { + pub is_visible: bool, +} + +impl Default for Visibility { + fn default() -> Self { + Self { is_visible: true } + } +} + +/// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering +#[derive(Clone, Reflect)] +#[reflect(Component)] +pub struct ComputedVisibility { + pub is_visible: bool, +} + +impl Default for ComputedVisibility { + fn default() -> Self { + Self { is_visible: true } + } +} + +#[derive(Clone, Debug)] +pub struct VisibleEntity { + pub entity: Entity, +} + +#[derive(Clone, Default, Debug, Reflect)] +#[reflect(Component)] +pub struct VisibleEntities { + #[reflect(ignore)] + pub entities: Vec, +} + +impl VisibleEntities { + pub fn iter(&self) -> impl DoubleEndedIterator { + self.entities.iter() + } +} + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum VisibilitySystems { + CalculateBounds, + UpdateOrthographicFrusta, + UpdatePerspectiveFrusta, + CheckVisibility, +} + +pub struct VisibilityPlugin; + +impl Plugin for VisibilityPlugin { + fn build(&self, app: &mut bevy_app::App) { + use VisibilitySystems::*; + + app.add_system_to_stage( + CoreStage::PostUpdate, + calculate_bounds.label(CalculateBounds), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_frusta:: + .label(UpdateOrthographicFrusta) + .after(TransformSystem::TransformPropagate), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_frusta:: + .label(UpdatePerspectiveFrusta) + .after(TransformSystem::TransformPropagate), + ) + .add_system_to_stage( + CoreStage::PostUpdate, + check_visibility + .label(CheckVisibility) + .after(CalculateBounds) + .after(UpdateOrthographicFrusta) + .after(UpdatePerspectiveFrusta) + .after(TransformSystem::TransformPropagate), + ); + } +} + +pub fn calculate_bounds( + mut commands: Commands, + meshes: Res>, + without_aabb: Query<(Entity, &Handle), Without>, +) { + for (entity, mesh_handle) in without_aabb.iter() { + if let Some(mesh) = meshes.get(mesh_handle) { + if let Some(aabb) = mesh.compute_aabb() { + commands.entity(entity).insert(aabb); + } + } + } +} + +pub fn update_frusta( + mut views: Query<(&GlobalTransform, &T, &mut Frustum)>, +) { + for (transform, projection, mut frustum) in views.iter_mut() { + let view_projection = + projection.get_projection_matrix() * transform.compute_matrix().inverse(); + *frustum = Frustum::from_view_projection( + &view_projection, + &transform.translation, + &transform.back(), + projection.far(), + ); + } +} + +pub fn check_visibility( + mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With>, + mut visible_entity_query: QuerySet<( + QueryState<&mut ComputedVisibility>, + QueryState<( + Entity, + &Visibility, + &mut ComputedVisibility, + Option<&RenderLayers>, + Option<&Aabb>, + Option<&GlobalTransform>, + )>, + )>, +) { + // Reset the computed visibility to false + for mut computed_visibility in visible_entity_query.q0().iter_mut() { + computed_visibility.is_visible = false; + } + + for (mut visible_entities, frustum, maybe_view_mask) in view_query.iter_mut() { + visible_entities.entities.clear(); + let view_mask = maybe_view_mask.copied().unwrap_or_default(); + + for ( + entity, + visibility, + mut computed_visibility, + maybe_entity_mask, + maybe_aabb, + maybe_transform, + ) in visible_entity_query.q1().iter_mut() + { + 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) { + if !frustum.intersects_obb(aabb, &transform.compute_matrix()) { + continue; + } + } + + computed_visibility.is_visible = true; + 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 + } +} diff --git a/pipelined/bevy_render2/src/view/visibility/render_layers.rs b/pipelined/bevy_render2/src/view/visibility/render_layers.rs new file mode 100644 index 0000000000..afe23468ef --- /dev/null +++ b/pipelined/bevy_render2/src/view/visibility/render_layers.rs @@ -0,0 +1,178 @@ +use bevy_ecs::prelude::ReflectComponent; +use bevy_reflect::Reflect; + +type LayerMask = u32; + +/// An identifier for a rendering layer. +pub type Layer = u8; + +/// Describes which rendering layers an entity belongs to. +/// +/// Cameras with this component will only render entities with intersecting +/// layers. +/// +/// There are 32 layers numbered `0` - [`TOTAL_LAYERS`](RenderLayers::TOTAL_LAYERS). Entities may +/// belong to one or more layers, or no layer at all. +/// +/// The [`Default`] instance of `RenderLayers` contains layer `0`, the first layer. +/// +/// An entity with this component without any layers is invisible. +/// +/// Entities without this component belong to layer `0`. +#[derive(Copy, Clone, Reflect, PartialEq, Eq, PartialOrd, Ord)] +#[reflect(Component, PartialEq)] +pub struct RenderLayers(LayerMask); + +impl std::fmt::Debug for RenderLayers { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("RenderLayers") + .field(&self.iter().collect::>()) + .finish() + } +} + +impl std::iter::FromIterator for RenderLayers { + fn from_iter>(i: T) -> Self { + i.into_iter().fold(Self::none(), |mask, g| mask.with(g)) + } +} + +/// Defaults to containing to layer `0`, the first layer. +impl Default for RenderLayers { + fn default() -> Self { + RenderLayers::layer(0) + } +} + +impl RenderLayers { + /// The total number of layers supported. + pub const TOTAL_LAYERS: usize = std::mem::size_of::() * 8; + + /// Create a new `RenderLayers` belonging to the given layer. + pub fn layer(n: Layer) -> Self { + RenderLayers(0).with(n) + } + + /// Create a new `RenderLayers` that belongs to all layers. + pub fn all() -> Self { + RenderLayers(u32::MAX) + } + + /// Create a new `RenderLayers` that belongs to no layers. + pub fn none() -> Self { + RenderLayers(0) + } + + /// Create a `RenderLayers` from a list of layers. + pub fn from_layers(layers: &[Layer]) -> Self { + layers.iter().copied().collect() + } + + /// Add the given layer. + /// + /// This may be called multiple times to allow an entity to belong + /// to multiple rendering layers. The maximum layer is `TOTAL_LAYERS - 1`. + /// + /// # Panics + /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. + pub fn with(mut self, layer: Layer) -> Self { + assert!(usize::from(layer) < Self::TOTAL_LAYERS); + self.0 |= 1 << layer; + self + } + + /// Removes the given rendering layer. + /// + /// # Panics + /// Panics when called with a layer greater than `TOTAL_LAYERS - 1`. + pub fn without(mut self, layer: Layer) -> Self { + assert!(usize::from(layer) < Self::TOTAL_LAYERS); + self.0 &= !(1 << layer); + self + } + + /// Get an iterator of the layers. + pub fn iter(&self) -> impl Iterator { + let total: Layer = std::convert::TryInto::try_into(Self::TOTAL_LAYERS).unwrap(); + let mask = *self; + (0..total).filter(move |g| RenderLayers::layer(*g).intersects(&mask)) + } + + /// Determine if a `RenderLayers` intersects another. + /// + /// `RenderLayers`s intersect if they share any common layers. + /// + /// A `RenderLayers` with no layers will not match any other + /// `RenderLayers`, even another with no layers. + pub fn intersects(&self, other: &RenderLayers) -> bool { + (self.0 & other.0) > 0 + } +} + +#[cfg(test)] +mod rendering_mask_tests { + use super::{Layer, RenderLayers}; + + #[test] + fn rendering_mask_sanity() { + assert_eq!( + RenderLayers::TOTAL_LAYERS, + 32, + "total layers is what we think it is" + ); + assert_eq!(RenderLayers::layer(0).0, 1, "layer 0 is mask 1"); + assert_eq!(RenderLayers::layer(1).0, 2, "layer 1 is mask 2"); + assert_eq!(RenderLayers::layer(0).with(1).0, 3, "layer 0 + 1 is mask 3"); + assert_eq!( + RenderLayers::layer(0).with(1).without(0).0, + 2, + "layer 0 + 1 - 0 is mask 2" + ); + assert!( + RenderLayers::layer(1).intersects(&RenderLayers::layer(1)), + "layers match like layers" + ); + assert!( + RenderLayers::layer(0).intersects(&RenderLayers(1)), + "a layer of 0 means the mask is just 1 bit" + ); + + assert!( + RenderLayers::layer(0) + .with(3) + .intersects(&RenderLayers::layer(3)), + "a mask will match another mask containing any similar layers" + ); + + assert!( + RenderLayers::default().intersects(&RenderLayers::default()), + "default masks match each other" + ); + + assert!( + !RenderLayers::layer(0).intersects(&RenderLayers::layer(1)), + "masks with differing layers do not match" + ); + assert!( + !RenderLayers(0).intersects(&RenderLayers(0)), + "empty masks don't match" + ); + assert_eq!( + RenderLayers::from_layers(&[0, 2, 16, 30]) + .iter() + .collect::>(), + vec![0, 2, 16, 30], + "from_layers and get_layers should roundtrip" + ); + assert_eq!( + format!("{:?}", RenderLayers::from_layers(&[0, 1, 2, 3])).as_str(), + "RenderLayers([0, 1, 2, 3])", + "Debug instance shows layers" + ); + assert_eq!( + RenderLayers::from_layers(&[0, 1, 2]), + >::from_iter(vec![0, 1, 2]), + "from_layers and from_iter are equivalent" + ) + } +} diff --git a/pipelined/bevy_sprite2/src/bundle.rs b/pipelined/bevy_sprite2/src/bundle.rs index 50f5c74432..77c2a5ac15 100644 --- a/pipelined/bevy_sprite2/src/bundle.rs +++ b/pipelined/bevy_sprite2/src/bundle.rs @@ -4,7 +4,10 @@ use crate::{ }; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; -use bevy_render2::texture::Image; +use bevy_render2::{ + texture::Image, + view::{ComputedVisibility, Visibility}, +}; use bevy_transform::components::{GlobalTransform, Transform}; #[derive(Bundle, Clone)] @@ -13,6 +16,10 @@ pub struct PipelinedSpriteBundle { pub transform: Transform, pub global_transform: GlobalTransform, pub texture: Handle, + /// User indication of whether an entity is visible + pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } impl Default for PipelinedSpriteBundle { @@ -22,6 +29,8 @@ impl Default for PipelinedSpriteBundle { transform: Default::default(), global_transform: Default::default(), texture: Default::default(), + visibility: Default::default(), + computed_visibility: Default::default(), } } } @@ -37,6 +46,10 @@ pub struct PipelinedSpriteSheetBundle { /// Data pertaining to how the sprite is drawn on the screen pub transform: Transform, pub global_transform: GlobalTransform, + /// User indication of whether an entity is visible + pub visibility: Visibility, + /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering + pub computed_visibility: ComputedVisibility, } impl Default for PipelinedSpriteSheetBundle { @@ -46,6 +59,8 @@ impl Default for PipelinedSpriteSheetBundle { texture_atlas: Default::default(), transform: Default::default(), global_transform: Default::default(), + visibility: Default::default(), + computed_visibility: Default::default(), } } } diff --git a/pipelined/bevy_sprite2/src/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs index 1b6e4bd5cf..f88a0ceca4 100644 --- a/pipelined/bevy_sprite2/src/render/mod.rs +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -19,7 +19,7 @@ use bevy_render2::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, Image}, - view::{ViewUniformOffset, ViewUniforms}, + view::{ComputedVisibility, ViewUniformOffset, ViewUniforms}, RenderWorld, }; use bevy_transform::components::GlobalTransform; @@ -188,12 +188,25 @@ pub fn extract_sprites( mut render_world: ResMut, images: Res>, texture_atlases: Res>, - sprite_query: Query<(&Sprite, &GlobalTransform, &Handle)>, - atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle)>, + sprite_query: Query<( + &ComputedVisibility, + &Sprite, + &GlobalTransform, + &Handle, + )>, + atlas_query: Query<( + &ComputedVisibility, + &TextureAtlasSprite, + &GlobalTransform, + &Handle, + )>, ) { let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); extracted_sprites.sprites.clear(); - for (sprite, transform, handle) in sprite_query.iter() { + for (computed_visibility, sprite, transform, handle) in sprite_query.iter() { + if !computed_visibility.is_visible { + continue; + } if let Some(image) = images.get(handle) { let size = image.texture_descriptor.size; @@ -213,7 +226,10 @@ pub fn extract_sprites( }); }; } - for (atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { + for (computed_visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { + if !computed_visibility.is_visible { + continue; + } if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { let rect = texture_atlas.textures[atlas_sprite.index as usize]; extracted_sprites.sprites.push(ExtractedSprite {