Frustum culling (#2861)

# Objective

Implement frustum culling for much better performance on more complex scenes. With the Amazon Lumberyard Bistro scene, I was getting roughly 15fps without frustum culling and 60+fps with frustum culling on a MacBook Pro 16 with i9 9980HK 8c/16t CPU and Radeon Pro 5500M.

macOS does weird things with vsync so even though vsync was off, it really looked like sometimes other applications or the desktop window compositor were interfering, but the difference could be even more as I even saw up to 90+fps sometimes.

## Solution

- Until the https://github.com/bevyengine/rfcs/pull/12 RFC is completed, I wanted to implement at least some of the bounding volume functionality we needed to be able to unblock a bunch of rendering features and optimisations such as frustum culling, fitting the directional light orthographic projection to the relevant meshes in the view, clustered forward rendering, etc.
- I have added `Aabb`, `Frustum`, and `Sphere` types with only the necessary intersection tests for the algorithms used. I also added `CubemapFrusta` which contains a `[Frustum; 6]` and can be used by cube maps such as environment maps, and point light shadow maps.
  - I did do a bit of benchmarking and optimisation on the intersection tests. I compared the [rafx parallel-comparison bitmask approach](c91bd5fcfd/rafx-visibility/src/geometry/frustum.rs (L64-L92)) with a naïve loop that has an early-out in case of a bounding volume being outside of any one of the `Frustum` planes and found them to be very similar, so I chose the simpler and more readable option. I also compared using Vec3 and Vec3A and it turned out that promoting Vec3s to Vec3A improved performance of the culling significantly due to Vec3A operations using SIMD optimisations where Vec3 uses plain scalar operations.
- When loading glTF models, the vertex attribute accessors generally store the minimum and maximum values, which allows for adding AABBs to meshes loaded from glTF for free.
- For meshes without an AABB (`PbrBundle` deliberately does not have an AABB by default), a system is executed that scans over the vertex positions to find the minimum and maximum values along each axis. This is used to construct the AABB.
- The `Frustum::intersects_obb` and `Sphere::insersects_obb` algorithm is from Foundations of Game Engine Development 2: Rendering by Eric Lengyel. There is no OBB type, yet, rather an AABB and the model matrix are passed in as arguments. This calculates a 'relative radius' of the AABB with respect to the plane normal (the plane normal in the Sphere case being something I came up with as the direction pointing from the centre of the sphere to the centre of the AABB) such that it can then do a sphere-sphere intersection test in practice.
- `RenderLayers` were copied over from the current renderer.
- `VisibleEntities` was copied over from the current renderer and a `CubemapVisibleEntities` was added to support `PointLight`s for now. `VisibleEntities` are added to views (cameras and lights) and contain a `Vec<Entity>` that is populated by culling/visibility systems that run in PostUpdate of the app world, and are iterated over in the render world for, for example, queuing up meshes to be drawn by lights for shadow maps and the main pass for cameras.
- `Visibility` and `ComputedVisibility` components were added. The `Visibility` component is user-facing so that, for example, the entity can be marked as not visible in an editor. `ComputedVisibility` on the other hand is the result of the culling/visibility systems and takes `Visibility` into account. So if an entity is marked as not being visible in its `Visibility` component, that will skip culling/visibility intersection tests and just mark the `ComputedVisibility` as false.
- The `ComputedVisibility` is used to decide which meshes to extract.
- I had to add a way to get the far plane from the `CameraProjection` in order to define an explicit far frustum plane for culling. This should perhaps be optional as it is not always desired and in that case, testing 5 planes instead of 6 is a performance win.

I think that's about all. I discussed some of the design with @cart on Discord already so hopefully it's not too far from being mergeable. It works well at least. 😄
This commit is contained in:
Robert Swain 2021-11-07 21:45:52 +00:00
parent fde5d2fe46
commit bc5916cce7
17 changed files with 1110 additions and 147 deletions

View file

@ -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),
));
}
}

View file

@ -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<StandardMaterial>,
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<Item = &VisibleEntities> {
self.data.iter()
}
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut VisibleEntities> {
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,
}

View file

@ -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::<AmbientLight>()
.init_resource::<DirectionalLightShadowMap>()
.init_resource::<PointLightShadowMap>()
.init_resource::<AmbientLight>();
.init_resource::<AmbientLight>()
.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::<Shadow>)

View file

@ -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::<Vec<_>>();
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<DirectionalLight>,
>,
mut visible_entity_query: Query<
(
Entity,
&Visibility,
&mut ComputedVisibility,
Option<&RenderLayers>,
Option<&Aabb>,
Option<&GlobalTransform>,
),
Without<NotShadowCaster>,
>,
) {
// 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
}
}

View file

@ -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<AmbientLight>,
point_light_shadow_map: Res<PointLightShadowMap>,
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
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<BindGroup>,
}
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<ExtractedAmbientLight>,
point_light_shadow_map: Res<ExtractedPointLightShadowMap>,
directional_light_shadow_map: Res<ExtractedDirectionalLightShadowMap>,
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::<Vec<_>>();
// 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::<Shadow>::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::<Shadow>::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<DrawFunctions<Shadow>>,
shadow_pipeline: Res<ShadowPipeline>,
casting_meshes: Query<(Entity, &Handle<Mesh>), Without<NotShadowCaster>>,
casting_meshes: Query<&Handle<Mesh>, Without<NotShadowCaster>>,
render_meshes: Res<RenderAssets<Mesh>>,
mut pipelines: ResMut<SpecializedPipelines<ShadowPipeline>>,
mut pipeline_cache: ResMut<RenderPipelineCache>,
mut view_lights: Query<&ViewLights>,
mut view_light_shadow_phases: Query<&mut RenderPhase<Shadow>>,
mut view_light_shadow_phases: Query<(&LightEntity, &mut RenderPhase<Shadow>)>,
point_light_entities: Query<&CubemapVisibleEntities, With<ExtractedPointLight>>,
directional_light_entities: Query<&VisibleEntities, With<ExtractedDirectionalLight>>,
) {
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::<DrawShadowMesh>()
.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
});
}
}
}
}

View file

@ -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<Mesh>,
Option<&NotShadowReceiver>,
@ -79,6 +82,7 @@ pub fn extract_meshes(
not_caster_query: Query<
(
Entity,
&ComputedVisibility,
&GlobalTransform,
&Handle<Mesh>,
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<ViewUniforms>,
render_meshes: Res<RenderAssets<Mesh>>,
render_materials: Res<RenderAssets<StandardMaterial>>,
standard_material_meshes: Query<(
Entity,
&Handle<StandardMaterial>,
&Handle<Mesh>,
&MeshUniform,
)>,
standard_material_meshes: Query<(&Handle<StandardMaterial>, &Handle<Mesh>, &MeshUniform)>,
mut views: Query<(
Entity,
&ExtractedView,
&ViewLights,
&VisibleEntities,
&mut RenderPhase<Transparent3d>,
)>,
) {
@ -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,
});
}
}
}
}

View file

@ -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(),
}

View file

@ -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::<Camera>()
.register_type::<Visibility>()
.register_type::<ComputedVisibility>()
.register_type::<Aabb>()
.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<ActiveCameras>,
windows: Res<Windows>,
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(),
));
}
}

View file

@ -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 {

View file

@ -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;

View file

@ -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<Aabb> {
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()

View file

@ -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<Item = &Frustum> {
self.frusta.iter()
}
pub fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut Frustum> {
self.frusta.iter_mut()
}
}

View file

@ -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::<Msaa>();
app.init_resource::<Msaa>().add_plugin(VisibilityPlugin);
app.sub_app(RenderApp)
.init_resource::<ViewUniforms>()
.add_system_to_stage(RenderStage::Extract, extract_msaa)

View file

@ -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<VisibleEntity>,
}
impl VisibleEntities {
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &VisibleEntity> {
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::<OrthographicProjection>
.label(UpdateOrthographicFrusta)
.after(TransformSystem::TransformPropagate),
)
.add_system_to_stage(
CoreStage::PostUpdate,
update_frusta::<PerspectiveProjection>
.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<Assets<Mesh>>,
without_aabb: Query<(Entity, &Handle<Mesh>), Without<Aabb>>,
) {
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<T: CameraProjection + Send + Sync + 'static>(
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<Camera>>,
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
}
}

View file

@ -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::<Vec<_>>())
.finish()
}
}
impl std::iter::FromIterator<Layer> for RenderLayers {
fn from_iter<T: IntoIterator<Item = Layer>>(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::<LayerMask>() * 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<Item = Layer> {
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<_>>(),
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]),
<RenderLayers as std::iter::FromIterator<Layer>>::from_iter(vec![0, 1, 2]),
"from_layers and from_iter are equivalent"
)
}
}

View file

@ -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<Image>,
/// 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(),
}
}
}

View file

@ -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<RenderWorld>,
images: Res<Assets<Image>>,
texture_atlases: Res<Assets<TextureAtlas>>,
sprite_query: Query<(&Sprite, &GlobalTransform, &Handle<Image>)>,
atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle<TextureAtlas>)>,
sprite_query: Query<(
&ComputedVisibility,
&Sprite,
&GlobalTransform,
&Handle<Image>,
)>,
atlas_query: Query<(
&ComputedVisibility,
&TextureAtlasSprite,
&GlobalTransform,
&Handle<TextureAtlas>,
)>,
) {
let mut extracted_sprites = render_world.get_resource_mut::<ExtractedSprites>().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 {