use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use bevy_ecs::change_detection::{DetectChanges, Tick};
use bevy_ecs::entity::Entity;
use bevy_ecs::prelude::{Component, Or, Ref, Resource, With, World};
use crate::assets::{Handle, MaterialAsset, MeshAsset};
use super::{
AmbientLight, App, AppError, Camera, DirectionalLight, GlobalTransform,
MeshRenderer, Plugin, PointLight, Projection, RenderBounds, ScheduleStage,
SkyLight, SpotLight, ToneMapping, 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,
pub bounds: Option<RenderBounds>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtractedCamera {
pub entity: Entity,
pub transform: GlobalTransform,
pub projection: Projection,
pub priority: i32,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtractedScreen {
pub material: crate::assets::Handle<crate::assets::MaterialAsset>,
pub camera: ExtractedCamera,
pub size: [u32; 2],
}
#[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(Clone, Debug, Default, PartialEq)]
pub struct ExtractedConditionShader {
pub source: String,
pub params: Vec<[f32; 4]>,
pub events_per_body: u32,
}
impl From<String> for ExtractedConditionShader {
fn from(source: String) -> Self {
Self {
source,
params: Vec::new(),
events_per_body: 1,
}
}
}
#[derive(Resource, Default, Clone)]
pub struct RenderWorld {
pub renderables: Vec<ExtractedRenderable>,
pub renderables_revision: u64,
pub active_camera: Option<ExtractedCamera>,
pub views: Vec<(ExtractedCamera, [f32; 4])>,
pub directional_lights: Vec<ExtractedDirectionalLight>,
pub point_lights: Vec<ExtractedPointLight>,
pub spot_lights: Vec<ExtractedSpotLight>,
pub ambient_light: Option<AmbientLight>,
pub sky_light: Option<SkyLight>,
pub environment:
Option<(crate::assets::Handle<crate::assets::TextureAsset>, f32)>,
pub reflection_probes: Vec<([f32; 3], super::ReflectionProbe)>,
pub tone_mapping: Option<ToneMapping>,
pub fog: Option<super::Fog>,
pub bloom: Option<super::Bloom>,
pub color_grading: Option<super::ColorGrading>,
pub screens: Vec<ExtractedScreen>,
pub ambient_occlusion: Option<super::AmbientOcclusion>,
pub particles: Vec<super::ParticleBatch>,
pub lights_revision: u64,
pub report: ExtractionReport,
pub gpu_physics: Vec<super::ExtractedGpuPhysicsBody>,
pub gpu_physics_signature: Option<u64>,
pub gpu_physics_revision: u64,
pub gpu_physics_commands: Vec<(super::PhysicsId, super::GpuBodyCommand)>,
pub gpu_physics_command_ticks: Vec<u64>,
pub gpu_physics_read_all: bool,
pub gpu_physics_reset: bool,
pub gpu_physics_commands_serial: u64,
pub gpu_colliders: Vec<super::GpuCollider>,
pub gpu_condition_shaders: Vec<ExtractedConditionShader>,
pub gpu_solver_shaders: Vec<String>,
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],
pub quality: super::QualityProfile,
pub culling: super::CullingMode,
pub antialiasing: super::Antialiasing,
pub shadows: super::ShadowQuality,
pub reflections_disabled: bool,
cached: HashMap<Entity, ExtractedRenderable>,
renderables_signature: Option<u64>,
renderables_fingerprint: Option<(Tick, [usize; 6])>,
}
#[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) {
super::update_skins(world);
let previous_renderables_signature =
world.resource::<RenderWorld>().renderables_signature;
let renderables_signature = if renderables_changed(world) {
Some(renderables_signature(world))
} else {
previous_renderables_signature
};
let renderables = (previous_renderables_signature != renderables_signature)
.then(|| collect_renderables(world));
let active_camera = collect_active_camera(world);
let views = collect_views(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_first::<AmbientLight>(world);
let sky_light = collect_first::<SkyLight>(world);
let environment = {
let mut query = world.query::<(Entity, &super::EnvironmentMap)>();
query
.iter(world)
.filter_map(|(entity, map)| {
Some((entity.index(), map.handle?, map.intensity))
})
.min_by_key(|(entity, ..)| *entity)
.map(|(_, handle, intensity)| (handle, intensity))
};
let reflection_probes = {
let mut query =
world
.query::<(Entity, &GlobalTransform, &super::ReflectionProbe)>();
let mut probes = query
.iter(world)
.map(|(entity, transform, probe)| {
let [x, y, z, _] = transform.matrix[3];
(entity.index(), [x, y, z], *probe)
})
.collect::<Vec<_>>();
probes.sort_by_key(|(entity, ..)| *entity);
probes
.into_iter()
.map(|(_, center, probe)| (center, probe))
.collect()
};
let tone_mapping = collect_first::<ToneMapping>(world);
let fog = collect_first::<super::Fog>(world);
let bloom = collect_first::<super::Bloom>(world);
let color_grading = collect_first::<super::ColorGrading>(world);
let screens = collect_screens(world);
let ambient_occlusion = collect_first::<super::AmbientOcclusion>(world);
let particles = collect_particles(world);
let has_gpu_physics_resources = world
.contains_resource::<super::PhysicsIdRegistry>()
&& world.contains_resource::<super::GpuEventRegistry>()
&& world.contains_resource::<super::GpuPhysicsClassWatches>();
super::hybrid_physics::stamp_gpu_commands(world);
let commands = world
.get_resource_mut::<super::GpuPhysicsCommands>()
.map(|mut commands| std::mem::take(&mut *commands))
.unwrap_or_default();
let condition_shaders = world
.get_resource::<super::GpuConditionShaders>()
.map(|shaders| shaders.0.clone())
.unwrap_or_default();
let condition_shaders =
match world.get_resource_mut::<super::GpuEventRegistry>() {
Some(mut registry) => condition_shaders
.iter()
.map(|shader| ExtractedConditionShader {
source: shader.resolve(&mut registry),
params: shader.params.clone(),
events_per_body: shader.events_per_body.max(1),
})
.collect(),
None => Vec::new(),
};
let gpu_physics_signature = has_gpu_physics_resources
.then(|| super::hybrid_physics::simple_gpu_physics_signature(world));
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 gpu_colliders = if gpu_physics.as_ref().is_some_and(Vec::is_empty) {
Vec::new()
} else {
world
.get_resource::<super::PhysicsWorld>()
.map(super::PhysicsWorld::gpu_colliders)
.unwrap_or_default()
};
let time = *world.resource::<super::FrameTime>();
let physics_settings = world.resource::<super::PhysicsSettings>().clone();
let render_settings = world.resource::<super::RenderSettings>();
let (
background_color,
quality,
culling,
antialiasing,
shadows,
reflections,
) = (
render_settings.background_color,
render_settings.quality,
render_settings.culling,
render_settings.antialiasing,
render_settings.shadows,
render_settings.reflections,
);
let mut render_world = world.resource_mut::<RenderWorld>();
match renderables {
None => {
render_world.report = ExtractionReport {
total: render_world.renderables.len(),
..ExtractionReport::default()
};
}
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);
render_world.cached = renderables
.iter()
.copied()
.map(|renderable| (renderable.entity, renderable))
.collect();
render_world.report = ExtractionReport {
added,
changed,
removed,
total: renderables.len(),
};
render_world.renderables = renderables;
render_world.renderables_revision =
render_world.renderables_revision.wrapping_add(1);
}
}
render_world.renderables_signature = renderables_signature;
render_world.active_camera = active_camera;
render_world.views = views;
render_world.tone_mapping = tone_mapping;
render_world.fog = fog;
render_world.bloom = bloom;
render_world.color_grading = color_grading;
render_world.screens = screens;
render_world.ambient_occlusion = ambient_occlusion;
render_world.particles = particles;
render_world.environment = environment;
render_world.reflection_probes = reflection_probes;
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.sky_light != sky_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;
render_world.sky_light = sky_light;
}
if let Some(gpu_physics) =
gpu_physics.filter(|bodies| *bodies != render_world.gpu_physics)
{
render_world.gpu_solver_shaders = custom_solver_shaders(&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;
if !commands.commands.is_empty()
|| commands.read_all_states
|| commands.reset_to_authored
{
render_world.gpu_physics_commands = commands.commands;
render_world.gpu_physics_command_ticks = commands.apply_ticks;
render_world.gpu_physics_read_all = commands.read_all_states;
render_world.gpu_physics_reset = commands.reset_to_authored;
render_world.gpu_physics_commands_serial += 1;
}
render_world.gpu_condition_shaders = condition_shaders;
render_world.gpu_colliders = gpu_colliders;
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;
render_world.quality = quality;
render_world.culling = culling;
render_world.antialiasing = antialiasing;
render_world.shadows = shadows;
render_world.reflections_disabled = !reflections;
}
fn collect_particles(world: &mut World) -> Vec<super::ParticleBatch> {
let mut query = world.query::<(
Entity,
&super::ParticleEmitter,
&super::ParticleSystem,
Option<&GlobalTransform>,
)>();
let world = &*world;
let mut batches = query
.iter(world)
.filter(|(entity, _, system, _)| {
!system.particles.is_empty() && visible_in_hierarchy(world, *entity)
})
.map(|(entity, emitter, system, global)| super::ParticleBatch {
entity: Some(entity),
..super::particle_batch(emitter, system, global)
})
.filter(|batch| !batch.instances.is_empty())
.collect::<Vec<_>>();
batches.sort_by_key(|batch| batch.entity.map(Entity::index));
batches
}
fn renderables_changed(world: &mut World) -> bool {
let this_run = world.increment_change_tick();
let last_run = world
.resource::<RenderWorld>()
.renderables_fingerprint
.map(|(tick, _)| tick);
let newer = |tick: Tick| {
last_run.is_none_or(|last_run| tick.is_newer_than(last_run, this_run))
};
let mut counts = [0; 6];
let mut changed = false;
let mut query = world.query_filtered::<(
Option<Ref<GlobalTransform>>,
Option<Ref<MeshRenderer>>,
Option<Ref<Visibility>>,
Option<Ref<super::Parent>>,
Option<Ref<RenderBounds>>,
Option<Ref<super::SkinnedMesh>>,
), Or<(
With<MeshRenderer>,
With<Visibility>,
With<super::Parent>,
With<RenderBounds>,
)>>();
for (transform, renderer, visibility, parent, bounds, skinned) in
query.iter(world)
{
let ticks = [
transform.map(|value| value.last_changed()),
renderer.map(|value| value.last_changed()),
visibility.map(|value| value.last_changed()),
parent.map(|value| value.last_changed()),
bounds.map(|value| value.last_changed()),
skinned.map(|value| value.last_changed()),
];
for (count, tick) in counts.iter_mut().zip(ticks) {
if let Some(tick) = tick {
*count += 1;
changed |= newer(tick);
}
}
}
let mut render_world = world.resource_mut::<RenderWorld>();
changed |= render_world
.renderables_fingerprint
.is_none_or(|(_, previous)| previous != counts);
render_world.renderables_fingerprint = Some((this_run, counts));
changed
}
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<&RenderBounds>,
Option<&super::SkinnedMesh>,
)>();
let world = &*world;
for (entity, transform, renderer, bounds, skinned) in query.iter(world) {
if !visible_in_hierarchy(world, entity) {
continue;
}
count += 1;
entity.to_bits().hash(&mut hasher);
skinned
.map_or(renderer.mesh, |skinned| skinned.mesh)
.key()
.hash(&mut hasher);
renderer.material.key().hash(&mut hasher);
renderer.cast_shadows.hash(&mut hasher);
renderer.receive_shadows.hash(&mut hasher);
format!("{bounds:?}").hash(&mut hasher);
for row in transform.matrix {
for value in row {
value.to_bits().hash(&mut hasher);
}
}
}
count.hash(&mut hasher);
hasher.finish()
}
pub fn visible_in_hierarchy(world: &World, entity: Entity) -> bool {
let mut current = Some(entity);
for _ in 0..1024 {
let Some(entity) = current else {
return true;
};
if world
.get::<Visibility>(entity)
.is_some_and(|visibility| !visibility.visible)
{
return false;
}
current = world.get::<super::Parent>(entity).map(|parent| parent.0);
}
true
}
fn collect_renderables(world: &mut World) -> Vec<ExtractedRenderable> {
let mut query = world.query::<(
Entity,
&GlobalTransform,
&MeshRenderer,
Option<&RenderBounds>,
Option<&super::SkinnedMesh>,
)>();
let world_ref = &*world;
let mut renderables = query
.iter(world_ref)
.filter(|(entity, ..)| visible_in_hierarchy(world_ref, *entity))
.map(|(entity, transform, renderer, bounds, skinned)| {
ExtractedRenderable {
entity,
transform: *transform,
mesh: skinned.map_or(renderer.mesh, |skinned| skinned.mesh),
material: renderer.material,
cast_shadows: renderer.cast_shadows,
receive_shadows: renderer.receive_shadows,
bounds: bounds.copied(),
}
})
.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)
.max_by_key(|(entity, _, camera)| {
(
camera.viewport.is_none(),
camera.priority,
std::cmp::Reverse(entity.to_bits()),
)
})
.map(|(entity, transform, camera)| ExtractedCamera {
entity,
transform: *transform,
projection: camera.projection,
priority: camera.priority,
})
}
fn collect_views(world: &mut World) -> Vec<(ExtractedCamera, [f32; 4])> {
if world.resource::<RenderCameraOverride>().entity.is_some() {
return Vec::new();
}
let mut query = world.query::<(Entity, &GlobalTransform, &Camera)>();
let mut views: Vec<_> = query
.iter(world)
.filter(|(_, _, camera)| camera.active)
.filter_map(|(entity, transform, camera)| {
let view = ExtractedCamera {
entity,
transform: *transform,
projection: camera.projection,
priority: camera.priority,
};
Some((view, camera.viewport?))
})
.collect();
views.sort_by_key(|(camera, _)| {
(camera.priority, std::cmp::Reverse(camera.entity.to_bits()))
});
views
}
fn collect_screens(world: &mut World) -> Vec<ExtractedScreen> {
let mut cameras =
world.query::<(Entity, &super::Name, &GlobalTransform, &Camera)>();
let cameras: Vec<_> = cameras
.iter(world)
.map(|(entity, name, transform, camera)| {
(
name.0.clone(),
ExtractedCamera {
entity,
transform: *transform,
projection: camera.projection,
priority: camera.priority,
},
)
})
.collect();
let mut query =
world.query::<(Entity, &super::CameraScreen, &MeshRenderer)>();
let mut screens: Vec<_> = query
.iter(world)
.filter(|(_, screen, _)| !screen.size.contains(&0))
.filter_map(|(entity, screen, renderer)| {
let (_, camera) =
cameras.iter().find(|(name, _)| *name == screen.camera)?;
Some((
entity.index(),
ExtractedScreen {
material: renderer.material,
camera: *camera,
size: screen.size,
},
))
})
.collect();
screens.sort_by_key(|(entity, _)| *entity);
screens.into_iter().map(|(_, screen)| screen).collect()
}
fn collect_directional_lights(
world: &mut World,
) -> Vec<ExtractedDirectionalLight> {
let mut query =
world.query::<(Entity, &GlobalTransform, &DirectionalLight)>();
let world = &*world;
let mut lights = query
.iter(world)
.filter(|(entity, ..)| visible_in_hierarchy(world, *entity))
.map(|(entity, transform, light)| ExtractedDirectionalLight {
entity,
transform: *transform,
light: *light,
})
.collect::<Vec<_>>();
lights.sort_by_key(|light| light.entity.index());
lights
}
fn collect_point_lights(world: &mut World) -> Vec<ExtractedPointLight> {
let mut query = world.query::<(Entity, &GlobalTransform, &PointLight)>();
let world = &*world;
let mut lights = query
.iter(world)
.filter(|(entity, ..)| visible_in_hierarchy(world, *entity))
.map(|(entity, transform, light)| ExtractedPointLight {
entity,
transform: *transform,
light: *light,
})
.collect::<Vec<_>>();
lights.sort_by_key(|light| light.entity.index());
lights
}
fn collect_spot_lights(world: &mut World) -> Vec<ExtractedSpotLight> {
let mut query = world.query::<(Entity, &GlobalTransform, &SpotLight)>();
let world = &*world;
let mut lights = query
.iter(world)
.filter(|(entity, ..)| visible_in_hierarchy(world, *entity))
.map(|(entity, transform, light)| ExtractedSpotLight {
entity,
transform: *transform,
light: *light,
})
.collect::<Vec<_>>();
lights.sort_by_key(|light| light.entity.index());
lights
}
fn collect_first<T: Component + Copy>(world: &mut World) -> Option<T> {
let mut query = world.query::<(Entity, &T)>();
query
.iter(world)
.min_by_key(|(entity, _)| entity.index())
.map(|(_, light)| *light)
}
fn custom_solver_shaders(
bodies: &[super::ExtractedGpuPhysicsBody],
) -> Vec<String> {
let paths = bodies
.iter()
.filter_map(|body| body.custom_shader.as_deref())
.collect::<std::collections::BTreeSet<_>>();
paths
.into_iter()
.map(|path| {
let glsl = std::fs::read_to_string(path).unwrap_or_else(|error| {
format!("#error cannot read custom solver {path}: {error}")
});
super::custom_solver_source(path, &glsl)
})
.collect()
}
#[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 screens_find_their_camera_by_name() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
let server = AssetServer::default();
let renderer = renderer(&server);
app.insert_resource(server);
let camera = Camera {
active: false,
..Camera::default()
};
let cam = app.spawn((
Transform::new([0.0, 0.0, 5.0]),
super::super::Name("Cam B".into()),
camera,
));
let screen = |name: &str, size| super::super::CameraScreen {
camera: name.into(),
size,
};
app.spawn((Transform::default(), renderer, screen("Cam B", [64, 32])));
app.spawn((Transform::default(), renderer, screen("Nobody", [64, 32])));
app.spawn((Transform::default(), renderer, screen("Cam B", [0, 32])));
app.update(Duration::ZERO).unwrap();
let screens = &app.world().resource::<RenderWorld>().screens;
assert_eq!(screens.len(), 1);
assert_eq!(screens[0].camera.entity, cam);
assert_eq!(screens[0].size, [64, 32]);
}
#[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);
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);
}
#[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 viewport_cameras_draw_over_the_full_window_one_in_priority_order() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
let camera = |priority, viewport| Camera {
active: true,
priority,
viewport,
..Camera::default()
};
let left = [0.0, 0.0, 0.5, 1.0];
let right = [0.5, 0.0, 0.5, 1.0];
let top = app.spawn((Transform::default(), camera(9, Some(right))));
let low = app.spawn((Transform::default(), camera(2, Some(left))));
app.update(Duration::ZERO).unwrap();
let world = app.world().resource::<RenderWorld>();
let order: Vec<_> =
world.views.iter().map(|(c, v)| (c.entity, *v)).collect();
assert_eq!(order, [(low, left), (top, right)]);
assert_eq!(world.active_camera.map(|c| c.entity), Some(top));
let full = app.spawn((Transform::default(), camera(0, None)));
app.update(Duration::ZERO).unwrap();
let world = app.world().resource::<RenderWorld>();
assert_eq!(world.active_camera.map(|c| c.entity), Some(full));
assert_eq!(world.views.len(), 2);
}
#[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);
}
#[test]
fn removing_a_hidden_visibility_after_a_quiet_frame_shows_the_object() {
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 { visible: false },
));
app.update(Duration::ZERO).unwrap();
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().report.total, 0);
app.world_mut().entity_mut(entity).remove::<Visibility>();
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().report.total, 1);
}
#[test]
fn lights_under_hidden_objects_are_not_extracted() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
let parent =
app.spawn((Transform::default(), Visibility { visible: false }));
let light = app.spawn((Transform::default(), PointLight::default()));
app.set_parent(light, parent).unwrap();
app.update(Duration::ZERO).unwrap();
assert!(app
.world()
.resource::<RenderWorld>()
.point_lights
.is_empty());
app.world_mut()
.get_mut::<Visibility>(parent)
.unwrap()
.visible = true;
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().point_lights.len(), 1);
}
#[test]
fn render_bounds_overrides_are_extracted_and_track_edits() {
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::new([5.0, 0.0, 0.0]),
renderer,
RenderBounds::Sphere {
center: [0.0, 1.0, 0.0],
radius: 2.0,
},
));
let bounds = |app: &App| {
app.world().resource::<RenderWorld>().renderables[0].bounds
};
app.update(Duration::ZERO).unwrap();
assert_eq!(
bounds(&app),
Some(RenderBounds::Sphere {
center: [0.0, 1.0, 0.0],
radius: 2.0,
}),
"the override stays in local space"
);
let edited = RenderBounds::Aabb {
min: [-1.0; 3],
max: [1.0; 3],
};
*app.world_mut().get_mut::<RenderBounds>(entity).unwrap() = edited;
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().report.changed, 1);
assert_eq!(bounds(&app), Some(edited));
app.world_mut().entity_mut(entity).remove::<RenderBounds>();
app.update(Duration::ZERO).unwrap();
assert_eq!(bounds(&app), None, "the renderer uses the mesh box");
}
#[test]
fn hiding_a_parent_hides_its_children() {
let mut app = App::new();
app.add_plugin(RenderExtractPlugin).unwrap();
let server = AssetServer::default();
let renderer = renderer(&server);
app.insert_resource(server);
let parent = app.spawn((Transform::default(), Visibility::default()));
app.spawn((
Transform::default(),
renderer,
super::super::Parent(parent),
));
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().report.total, 1);
app.world_mut()
.get_mut::<Visibility>(parent)
.unwrap()
.visible = false;
app.update(Duration::ZERO).unwrap();
assert_eq!(app.world().resource::<RenderWorld>().report.total, 0);
}
}