use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::ops::Range;
use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{Resource, World};
use crate::assets::{Handle, MaterialAsset, MeshAsset};
use super::{
AmbientLight, App, AppError, Camera, DirectionalLight, GlobalTransform,
MeshRenderer, Plugin, PointLight, Projection, ScheduleStage, SpotLight,
Visibility,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtractedRenderable {
pub entity: Entity,
pub transform: GlobalTransform,
pub mesh: Handle<MeshAsset>,
pub material: Handle<MaterialAsset>,
pub cast_shadows: bool,
pub receive_shadows: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtractedCamera {
pub entity: Entity,
pub transform: GlobalTransform,
pub projection: Projection,
pub priority: i32,
}
#[derive(Resource, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RenderCameraOverride {
pub entity: Option<Entity>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtractedDirectionalLight {
pub entity: Entity,
pub transform: GlobalTransform,
pub light: DirectionalLight,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtractedPointLight {
pub entity: Entity,
pub transform: GlobalTransform,
pub light: PointLight,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtractedSpotLight {
pub entity: Entity,
pub transform: GlobalTransform,
pub light: SpotLight,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ExtractionReport {
pub added: usize,
pub changed: usize,
pub removed: usize,
pub total: usize,
}
#[derive(Resource, Default)]
pub struct RenderWorld {
pub renderables: Vec<ExtractedRenderable>,
pub renderables_revision: u64,
pub active_camera: Option<ExtractedCamera>,
pub directional_lights: Vec<ExtractedDirectionalLight>,
pub point_lights: Vec<ExtractedPointLight>,
pub spot_lights: Vec<ExtractedSpotLight>,
pub ambient_light: Option<AmbientLight>,
pub lights_revision: u64,
pub dirty_ranges: Vec<Range<usize>>,
pub report: ExtractionReport,
pub gpu_physics: Vec<super::ExtractedGpuPhysicsBody>,
pub gpu_physics_signature: Option<u64>,
pub gpu_physics_revision: u64,
pub physics_tick: u64,
pub fixed_delta_seconds: f32,
pub elapsed_seconds: f32,
pub physics_gravity: [f32; 3],
pub physics_enabled: bool,
pub background_color: [f32; 4],
cached: HashMap<Entity, ExtractedRenderable>,
previous_order: Vec<Entity>,
renderables_signature: Option<u64>,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct RenderExtractPlugin;
impl Plugin for RenderExtractPlugin {
fn build(&self, app: &mut App) -> Result<(), AppError> {
app.insert_resource(RenderWorld::default())
.insert_resource(RenderCameraOverride::default())
.add_systems(ScheduleStage::RenderExtract, extract_render_world);
Ok(())
}
}
pub fn extract_render_world(world: &mut World) {
let renderables_signature = renderables_signature(world);
let previous_renderables_signature =
world.resource::<RenderWorld>().renderables_signature;
let renderables = (previous_renderables_signature
!= Some(renderables_signature))
.then(|| collect_renderables(world));
let active_camera = collect_active_camera(world);
let directional_lights = collect_directional_lights(world);
let point_lights = collect_point_lights(world);
let spot_lights = collect_spot_lights(world);
let ambient_light = collect_ambient_light(world);
let has_gpu_physics_resources = world
.contains_resource::<super::PhysicsIdRegistry>()
&& world.contains_resource::<super::GpuEventRegistry>()
&& world.contains_resource::<super::GpuPhysicsClassWatches>();
let gpu_physics_signature = has_gpu_physics_resources
.then(|| super::hybrid_physics::simple_gpu_physics_signature(world))
.flatten();
let previous_gpu_signature =
world.resource::<RenderWorld>().gpu_physics_signature;
let gpu_physics = if gpu_physics_signature.is_some()
&& gpu_physics_signature == previous_gpu_signature
{
None
} else if has_gpu_physics_resources {
Some(super::hybrid_physics::extract_gpu_physics_bodies(world))
} else {
Some(Vec::new())
};
let time = *world.resource::<super::FrameTime>();
let physics_settings = world.resource::<super::PhysicsSettings>().clone();
let background_color =
world.resource::<super::RenderSettings>().background_color;
let mut render_world = world.resource_mut::<RenderWorld>();
match renderables {
None => {
render_world.report = ExtractionReport {
total: render_world.renderables.len(),
..ExtractionReport::default()
};
render_world.dirty_ranges.clear();
}
Some(renderables) => {
let current_entities = renderables
.iter()
.map(|renderable| renderable.entity)
.collect::<HashSet<_>>();
let removed = render_world
.cached
.keys()
.filter(|entity| !current_entities.contains(entity))
.count();
let mut added = 0;
let mut dirty_entities = HashSet::new();
for renderable in &renderables {
match render_world.cached.get(&renderable.entity) {
None => {
added += 1;
dirty_entities.insert(renderable.entity);
}
Some(previous) if previous != renderable => {
dirty_entities.insert(renderable.entity);
}
Some(_) => {}
}
}
let changed = dirty_entities.len().saturating_sub(added);
let order = renderables
.iter()
.map(|renderable| renderable.entity)
.collect::<Vec<_>>();
let dirty_ranges = if order != render_world.previous_order {
(!renderables.is_empty())
.then_some(0..renderables.len())
.into_iter()
.collect()
} else {
contiguous_ranges(renderables.iter().enumerate().filter_map(
|(index, renderable)| {
dirty_entities
.contains(&renderable.entity)
.then_some(index)
},
))
};
render_world.cached = renderables
.iter()
.copied()
.map(|renderable| (renderable.entity, renderable))
.collect();
render_world.previous_order = order;
render_world.report = ExtractionReport {
added,
changed,
removed,
total: renderables.len(),
};
render_world.dirty_ranges = dirty_ranges;
render_world.renderables = renderables;
render_world.renderables_revision =
render_world.renderables_revision.wrapping_add(1);
}
}
render_world.renderables_signature = Some(renderables_signature);
render_world.active_camera = active_camera;
if render_world.directional_lights != directional_lights
|| render_world.point_lights != point_lights
|| render_world.spot_lights != spot_lights
|| render_world.ambient_light != ambient_light
{
render_world.lights_revision =
render_world.lights_revision.wrapping_add(1);
render_world.directional_lights = directional_lights;
render_world.point_lights = point_lights;
render_world.spot_lights = spot_lights;
render_world.ambient_light = ambient_light;
}
if let Some(gpu_physics) = gpu_physics {
render_world.gpu_physics = gpu_physics;
render_world.gpu_physics_revision =
render_world.gpu_physics_revision.wrapping_add(1);
}
render_world.gpu_physics_signature = gpu_physics_signature;
render_world.physics_tick = time.fixed_tick;
render_world.fixed_delta_seconds = time.fixed_delta.as_secs_f32();
render_world.elapsed_seconds = time.elapsed.as_secs_f32();
render_world.physics_gravity = physics_settings.gravity;
render_world.physics_enabled = physics_settings.enabled;
render_world.background_color = background_color;
}
fn renderables_signature(world: &mut World) -> u64 {
let mut hasher = super::FastHasher::default();
let mut count = 0_u64;
let mut query = world.query::<(
Entity,
&GlobalTransform,
&MeshRenderer,
Option<&Visibility>,
)>();
for (entity, transform, renderer, visibility) in query.iter(world) {
if visibility.is_some_and(|visibility| !visibility.visible) {
continue;
}
count += 1;
entity.to_bits().hash(&mut hasher);
renderer.mesh.key().hash(&mut hasher);
renderer.material.key().hash(&mut hasher);
renderer.cast_shadows.hash(&mut hasher);
renderer.receive_shadows.hash(&mut hasher);
for row in transform.matrix {
for value in row {
value.to_bits().hash(&mut hasher);
}
}
}
count.hash(&mut hasher);
hasher.finish()
}
fn collect_renderables(world: &mut World) -> Vec<ExtractedRenderable> {
let mut query = world.query::<(
Entity,
&GlobalTransform,
&MeshRenderer,
Option<&Visibility>,
)>();
let mut renderables = query
.iter(world)
.filter(|(_, _, _, visibility)| {
visibility.is_none_or(|visibility| visibility.visible)
})
.map(|(entity, transform, renderer, _)| ExtractedRenderable {
entity,
transform: *transform,
mesh: renderer.mesh,
material: renderer.material,
cast_shadows: renderer.cast_shadows,
receive_shadows: renderer.receive_shadows,
})
.collect::<Vec<_>>();
renderables.sort_by_key(|renderable| {
(
renderable.mesh.key(),
renderable.material.key(),
renderable.entity.to_bits(),
)
});
renderables
}
fn collect_active_camera(world: &mut World) -> Option<ExtractedCamera> {
let override_entity = world.resource::<RenderCameraOverride>().entity;
let mut query = world.query::<(Entity, &GlobalTransform, &Camera)>();
if let Some(entity) = override_entity {
if let Ok((entity, transform, camera)) = query.get(world, entity) {
return Some(ExtractedCamera {
entity,
transform: *transform,
projection: camera.projection,
priority: camera.priority,
});
}
}
query
.iter(world)
.filter(|(_, _, camera)| camera.active)
.map(|(entity, transform, camera)| ExtractedCamera {
entity,
transform: *transform,
projection: camera.projection,
priority: camera.priority,
})
.max_by_key(|camera| {
(camera.priority, std::cmp::Reverse(camera.entity.to_bits()))
})
}
fn collect_directional_lights(
world: &mut World,
) -> Vec<ExtractedDirectionalLight> {
let mut query =
world.query::<(Entity, &GlobalTransform, &DirectionalLight)>();
let mut lights = query
.iter(world)
.map(|(entity, transform, light)| ExtractedDirectionalLight {
entity,
transform: *transform,
light: *light,
})
.collect::<Vec<_>>();
lights.sort_by_key(|light| light.entity.to_bits());
lights
}
fn collect_point_lights(world: &mut World) -> Vec<ExtractedPointLight> {
let mut query = world.query::<(Entity, &GlobalTransform, &PointLight)>();
let mut lights = query
.iter(world)
.map(|(entity, transform, light)| ExtractedPointLight {
entity,
transform: *transform,
light: *light,
})
.collect::<Vec<_>>();
lights.sort_by_key(|light| light.entity.to_bits());
lights
}
fn collect_spot_lights(world: &mut World) -> Vec<ExtractedSpotLight> {
let mut query = world.query::<(Entity, &GlobalTransform, &SpotLight)>();
let mut lights = query
.iter(world)
.map(|(entity, transform, light)| ExtractedSpotLight {
entity,
transform: *transform,
light: *light,
})
.collect::<Vec<_>>();
lights.sort_by_key(|light| light.entity.to_bits());
lights
}
fn collect_ambient_light(world: &mut World) -> Option<AmbientLight> {
let mut query = world.query::<(Entity, &AmbientLight)>();
query
.iter(world)
.min_by_key(|(entity, _)| entity.to_bits())
.map(|(_, light)| *light)
}
fn contiguous_ranges(
indices: impl Iterator<Item = usize>,
) -> Vec<Range<usize>> {
let mut ranges: Vec<Range<usize>> = Vec::new();
for index in indices {
match ranges.last_mut() {
Some(range) if range.end == index => range.end += 1,
_ => ranges.push(index..index + 1),
}
}
ranges
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use crate::assets::AssetServer;
use crate::Transform;
use super::*;
fn renderer(server: &AssetServer) -> MeshRenderer {
MeshRenderer {
mesh: server.fallback_mesh,
material: server.fallback_material,
cast_shadows: true,
receive_shadows: true,
}
}
#[test]
fn extraction_tracks_changes_removals_and_stable_order() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
let server = AssetServer::default();
let renderer = renderer(&server);
app.insert_resource(server);
let first = app.spawn((Transform::new([1.0, 0.0, 0.0]), renderer));
let second = app.spawn((Transform::new([2.0, 0.0, 0.0]), renderer));
app.update(Duration::ZERO).unwrap();
let render_world = app.world().resource::<RenderWorld>();
assert_eq!(render_world.report.added, 2);
assert_eq!(render_world.dirty_ranges, vec![0..2]);
app.world_mut()
.get_mut::<Transform>(second)
.unwrap()
.position[0] = 3.0;
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().report.changed, 1);
app.despawn(first).unwrap();
app.update(Duration::ZERO).unwrap();
let render_world = app.world().resource::<RenderWorld>();
assert_eq!(render_world.report.removed, 1);
assert_eq!(render_world.report.total, 1);
assert_eq!(render_world.dirty_ranges, vec![0..1]);
}
#[test]
fn extraction_selects_highest_priority_active_camera() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
app.spawn((
Transform::default(),
Camera {
active: true,
priority: 1,
..Camera::default()
},
));
let expected = app.spawn((
Transform::default(),
Camera {
active: true,
priority: 10,
..Camera::default()
},
));
app.update(Duration::ZERO).unwrap();
assert_eq!(
app.world()
.resource::<RenderWorld>()
.active_camera
.map(|camera| camera.entity),
Some(expected)
);
}
#[test]
fn camera_override_can_select_an_inactive_editor_camera() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
app.spawn((
Transform::default(),
Camera {
active: true,
priority: 10,
..Camera::default()
},
));
let editor_camera = app.spawn((
Transform::new([0.0, 3.0, 8.0]),
Camera {
active: false,
..Camera::default()
},
));
app.world_mut()
.resource_mut::<RenderCameraOverride>()
.entity = Some(editor_camera);
app.update(Duration::ZERO).unwrap();
assert_eq!(
app.world()
.resource::<RenderWorld>()
.active_camera
.map(|camera| camera.entity),
Some(editor_camera)
);
}
#[test]
fn hidden_entities_are_removed_from_render_world() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
let server = AssetServer::default();
let renderer = renderer(&server);
app.insert_resource(server);
let entity =
app.spawn((Transform::default(), renderer, Visibility::default()));
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().report.total, 1);
app.world_mut()
.get_mut::<Visibility>(entity)
.unwrap()
.visible = false;
app.update(Duration::ZERO).unwrap();
let render_world = app.world().resource::<RenderWorld>();
assert_eq!(render_world.report.total, 0);
assert_eq!(render_world.report.removed, 1);
}
}