use std::f32::consts::TAU;
use std::sync::Arc;
use bevy_ecs::prelude::{Entity, World};
use super::physics_benchmark::grid;
use super::{
Camera, DirectionalLight, GpuCondition, GpuEventMode, GpuEventPayload,
GpuPhysicsClassWatches, GpuPhysicsRule, MeshRenderer, Name, ObjectClasses,
PhysicsBenchmark, PhysicsBody, PointLight, RigidBody, RigidBodyKind,
SimulationClass, Visibility,
};
use crate::assets::{
procedural_primitive_mesh, procedural_sphere_mesh, AlphaMode, AssetServer,
Handle, LodGroupAsset, LodLevel, LodMetric, MaterialAsset, MeshAsset,
MorphTargets, PrimitiveShape, SkinWeights, TextureAsset, TextureColorSpace,
TextureSampler,
};
use crate::Transform;
pub const RENDER_BENCHMARK_FRAMES: u32 = 600;
pub const RENDER_BENCHMARK_BODIES: usize = 1_000;
pub const RENDER_BENCHMARK_POINT_LIGHTS: usize = 24;
pub const RENDER_BENCHMARK_PILLARS: usize = 40;
pub const RENDER_BENCHMARK_BODY_CLASS: &str = "benchmark_bodies";
pub fn spawn_render_benchmark(world: &mut World) -> Entity {
let (cube, sphere, materials) = {
let mut assets =
world.get_resource_or_insert_with(AssetServer::default);
let sphere = assets.meshes.insert(procedural_sphere_mesh(16));
let materials: Vec<_> = (0..5)
.map(|index| {
let t = index as f32 / 4.0;
assets.materials.insert(MaterialAsset {
base_color: [0.9 - 0.5 * t, 0.5, 0.2 + 0.6 * t, 1.0],
metallic: t,
roughness: 0.9 - 0.8 * t,
..MaterialAsset::default()
})
})
.collect();
(assets.fallback_mesh, sphere, materials)
};
let mut spawn_mesh =
|name: String, transform, mesh: Handle<MeshAsset>, material| {
world
.spawn((
Name(name),
transform,
MeshRenderer {
mesh,
material,
cast_shadows: true,
receive_shadows: true,
},
Visibility::default(),
))
.id()
};
let ground = spawn_mesh(
"Benchmark Ground".into(),
Transform {
position: [0.0, -0.5, 0.0],
scale: [80.0, 1.0, 80.0],
..Transform::default()
},
cube,
materials[0],
);
let pillars = (0..49)
.map(|cell| grid(cell, 7, 8.0))
.filter(|[x, z]| x.abs() > 8.0 || z.abs() > 8.0);
for (index, [x, z]) in pillars.enumerate() {
let material = materials[index % materials.len()];
spawn_mesh(
format!("Benchmark Pillar {index}"),
Transform {
position: [x, 3.0, z],
scale: [1.0, 6.0, 1.0],
..Transform::default()
},
cube,
material,
);
spawn_mesh(
format!("Benchmark Sphere {index}"),
Transform {
position: [x, 7.0, z],
scale: [2.0; 3],
..Transform::default()
},
sphere,
material,
);
}
let bodies = PhysicsBenchmark::Mixed.bodies(RENDER_BENCHMARK_BODIES);
let mut body_entities = Vec::with_capacity(bodies.len());
for (index, body) in bodies.into_iter().enumerate() {
let material = materials[index % materials.len()];
let entity = spawn_mesh(
format!("Benchmark Body {index}"),
body.transform,
cube,
material,
);
body_entities.push((entity, body.solver));
}
let physics = |solver, kind| {
(
PhysicsBody {
simulation: SimulationClass::Gpu,
solver,
custom_shader: None,
},
RigidBody {
kind,
..RigidBody::default()
},
super::Collider::default(),
)
};
world
.entity_mut(ground)
.insert(physics(super::PhysicsSolver::Full, RigidBodyKind::Fixed));
for (entity, solver) in body_entities {
world.entity_mut(entity).insert((
physics(solver, RigidBodyKind::Dynamic),
ObjectClasses::new([RENDER_BENCHMARK_BODY_CLASS]),
));
}
if let Some(mut watches) =
world.get_resource_mut::<GpuPhysicsClassWatches>()
{
watches.add(
RENDER_BENCHMARK_BODY_CLASS,
GpuPhysicsRule::new(
"benchmark_body_landed",
GpuCondition::position_y().less_than(1.0),
)
.mode(GpuEventMode::OnEnter)
.payload(GpuEventPayload::Position),
);
}
world.spawn((
Name("Benchmark Sun".into()),
Transform {
rotation: crate::engine::rotation_facing([-0.4, -1.0, -0.3]),
..Transform::default()
},
DirectionalLight {
illuminance: 20_000.0,
..DirectionalLight::default()
},
));
for index in 0..RENDER_BENCHMARK_POINT_LIGHTS {
let angle = TAU * index as f32 / RENDER_BENCHMARK_POINT_LIGHTS as f32;
let mut color = [0.3; 3];
color[index % 3] = 1.0;
world.spawn((
Name(format!("Benchmark Light {index}")),
Transform::new([12.0 * angle.cos(), 3.0, 12.0 * angle.sin()]),
PointLight {
color,
intensity: 400.0,
range: 10.0,
shadows: false,
radius: 0.0,
},
));
}
world
.spawn((
Name("Benchmark Camera".into()),
render_benchmark_camera(0),
Camera {
active: true,
..Camera::default()
},
))
.id()
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RenderBenchmarkExtras {
pub screens: usize,
pub screen_size: [u32; 2],
pub screen_every: u32,
pub bears: usize,
pub bear_updates: usize,
pub without_bodies: bool,
pub lights: usize,
pub city: usize,
pub effects: bool,
pub probes: bool,
pub rain: bool,
}
pub const RENDER_BENCHMARK_RAIN: [usize; 3] = [50_000, 16, 16];
pub const RENDER_BENCHMARK_RAIN_GROUND: usize = 257;
pub const RENDER_BENCHMARK_PROBES: [u32; 3] = [4, 4, 4];
pub const RENDER_BENCHMARK_EFFECTS: [usize; 3] = [50_000, 2_000, 300];
pub const RENDER_BENCHMARK_EFFECT_EMITTERS: usize = 20;
#[must_use]
pub fn render_benchmark_city_assets(buildings: usize) -> [usize; 3] {
[
(buildings / 10).clamp(1, 1000),
(buildings / 50).clamp(1, 200),
(buildings / 156).clamp(1, 64),
]
}
pub const RENDER_BENCHMARK_CITY_TEXTURE: u32 = 1024;
const CITY_LOD_DISTANCE: f32 = 60.0;
const CITY_BLOCK: usize = 5;
const CITY_LOT: f32 = 4.0;
const CITY_STREET: f32 = 8.0;
#[must_use]
pub fn render_benchmark_building(index: usize) -> String {
format!("Benchmark Building {index}")
}
fn city_layout(buildings: usize) -> (usize, f32) {
let blocks = buildings.div_ceil(CITY_BLOCK * CITY_BLOCK);
let side = (blocks as f32).sqrt().ceil().max(1.0) as usize;
(side, side as f32 * city_pitch() / 2.0)
}
fn city_pitch() -> f32 {
CITY_BLOCK as f32 * CITY_LOT + CITY_STREET
}
fn city_building_size(index: usize) -> [f32; 3] {
let hash = (index as u32 ^ 0x9e37_79b9).wrapping_mul(2_654_435_761);
let unit = |shift: u32| ((hash >> shift) & 255) as f32 / 255.0;
[
2.0 + 1.5 * unit(0),
4.0 + 36.0 * unit(8),
2.0 + 1.5 * unit(16),
]
}
fn city_lot(index: usize, side: usize, half: f32) -> [f32; 2] {
let lots = CITY_BLOCK * CITY_BLOCK;
let (block, lot) = (index / lots, index % lots);
let axis = |block: usize, lot: usize| {
-half
+ CITY_STREET
+ city_pitch() * block as f32
+ CITY_LOT * (lot as f32 + 0.5)
};
[
axis(block % side, lot % CITY_BLOCK),
axis(block / side, lot / CITY_BLOCK),
]
}
fn scaled_mesh(mesh: &MeshAsset, size: [f32; 3]) -> MeshAsset {
let mut mesh = mesh.clone();
for vertex in &mut mesh.vertices {
let normal = [0, 1, 2].map(|axis| vertex.normal[axis] / size[axis]);
let length = normal.iter().map(|n| n * n).sum::<f32>().sqrt();
for axis in 0..3 {
vertex.position[axis] *= size[axis];
vertex.normal[axis] = normal[axis] / length.max(f32::EPSILON);
}
}
mesh
}
fn city_texture(index: usize) -> TextureAsset {
let edge = RENDER_BENCHMARK_CITY_TEXTURE as usize;
let mut rgba8 = Vec::with_capacity(edge * edge * 4);
for y in 0..edge {
for x in 0..edge {
let window = x % 64 > 16 && y % 64 > 24;
let lit = (index * 37 + x / 64 * 13 + y / 64 * 7).is_multiple_of(5);
let value = match (window, lit) {
(false, _) => 110u8,
(true, false) => 40,
(true, true) => 230,
};
rgba8.extend_from_slice(&[
value,
value,
value.saturating_sub(30),
255,
]);
}
}
TextureAsset {
size: [RENDER_BENCHMARK_CITY_TEXTURE; 2],
rgba8,
color_space: TextureColorSpace::Srgb,
sampler: TextureSampler::default(),
}
}
fn spawn_city(world: &mut World, buildings: usize) {
if buildings == 0 {
return;
}
let [meshes, materials, textures] = render_benchmark_city_assets(buildings);
let (meshes, materials) = {
let mut assets =
world.get_resource_or_insert_with(AssetServer::default);
let textures: Vec<_> = (0..textures)
.map(|index| assets.textures.insert(city_texture(index)))
.collect();
let materials: Vec<_> = (0..materials)
.map(|index| {
let tint = (index % 4) as f32 / 3.0;
assets.materials.insert(MaterialAsset {
name: format!("Benchmark City {index}"),
base_color: [0.6 + 0.4 * tint, 0.7, 1.0 - 0.4 * tint, 1.0],
roughness: 0.7,
base_color_texture: Some(textures[index % textures.len()]),
..MaterialAsset::default()
})
})
.collect();
let tower = procedural_primitive_mesh(PrimitiveShape::Cylinder);
let block = procedural_primitive_mesh(PrimitiveShape::Cube);
let meshes: Vec<_> = (0..meshes)
.map(|index| {
let size = city_building_size(index);
let near = assets.meshes.insert(scaled_mesh(&tower, size));
let far = assets.meshes.insert(scaled_mesh(&block, size));
assets.lod_groups.insert(LodGroupAsset {
metric: LodMetric::Distance,
levels: vec![
LodLevel {
mesh: near,
until: CITY_LOD_DISTANCE,
},
LodLevel {
mesh: far,
until: f32::INFINITY,
},
],
});
near
})
.collect();
(meshes, materials)
};
let (side, half) = city_layout(buildings);
for index in 0..buildings {
let [x, z] = city_lot(index, side, half);
let height = city_building_size(index % meshes.len())[1];
world.spawn((
Name(render_benchmark_building(index)),
Transform {
position: [x, height / 2.0, z],
..Transform::default()
},
MeshRenderer {
mesh: meshes[index % meshes.len()],
material: materials[index % materials.len()],
cast_shadows: true,
receive_shadows: true,
},
Visibility::default(),
));
}
}
#[must_use]
pub fn render_benchmark_character(index: usize) -> String {
format!("Benchmark Character {index}")
}
fn spawn_effects(world: &mut World) {
let [particles, blended, characters] = RENDER_BENCHMARK_EFFECTS;
world.insert_resource(super::GpuSkinning);
let (sphere, glass, body, skin) = {
let mut assets =
world.get_resource_or_insert_with(AssetServer::default);
let sphere = assets.meshes.insert(procedural_sphere_mesh(8));
let glass: Vec<_> = (0..4)
.map(|index| {
let mut color = [0.5, 0.7, 0.9, 0.35];
color[index % 3] = 1.0;
assets.materials.insert(MaterialAsset {
name: format!("Benchmark Glass {index}"),
alpha_mode: AlphaMode::Blend,
base_color: color,
roughness: 0.1,
..MaterialAsset::default()
})
})
.collect();
let body = scaled_mesh(
&procedural_primitive_mesh(PrimitiveShape::Cylinder),
[0.4, 1.8, 0.4],
);
let weights = SkinWeights {
joints: vec![[0, 1, 0, 0]; body.vertices.len()],
weights: body
.vertices
.iter()
.map(|vertex| {
let up = (vertex.position[1] / 0.9).clamp(0.0, 1.0);
[1.0 - up, up, 0.0, 0.0]
})
.collect(),
};
let body = assets.meshes.insert(body);
assets.deformers.cooked.insert(
body.key(),
Arc::new((Some(weights), MorphTargets::default())),
);
let skin = assets.materials.insert(MaterialAsset {
name: "Benchmark Character".into(),
base_color: [0.8, 0.5, 0.4, 1.0],
roughness: 0.6,
..MaterialAsset::default()
});
(sphere, glass, body, skin)
};
let per_emitter = (particles / RENDER_BENCHMARK_EFFECT_EMITTERS) as u32;
for index in 0..RENDER_BENCHMARK_EFFECT_EMITTERS {
let angle =
TAU * index as f32 / RENDER_BENCHMARK_EFFECT_EMITTERS as f32;
let additive = index % 2 == 1;
world.spawn((
Name(format!("Benchmark Emitter {index}")),
Transform {
position: [10.0 * angle.sin(), 1.0, 10.0 * angle.cos()],
..Transform::default()
},
super::ParticleEmitter {
rate: per_emitter as f32 / 2.5,
max_particles: per_emitter,
prewarm: true,
lifetime: [2.0, 3.0],
speed: [1.0, 3.0],
size: [0.1, 0.2],
spread: 0.5,
gravity: 1.0,
emissive: if additive { 2.0 } else { 1.0 },
blend: if additive {
super::ParticleBlend::Additive
} else {
super::ParticleBlend::Alpha
},
simulation: super::ParticleSimulation::Gpu,
..super::ParticleEmitter::default()
},
super::ParticleSystem::default(),
));
}
let side = (blended as f32).sqrt().ceil() as usize;
for index in 0..blended {
let [x, z] = grid(index, side, 0.5);
world.spawn((
Name(format!("Benchmark Glass {index}")),
Transform {
position: [x, 6.0 + 0.5 * (index % 3) as f32, z],
scale: [0.3; 3],
..Transform::default()
},
MeshRenderer {
mesh: sphere,
material: glass[index % glass.len()],
cast_shadows: false,
receive_shadows: true,
},
Visibility::default(),
));
}
let bend = |angle: f32, time: f32| super::Keyframe {
time,
value: vec![0.0, 0.0, angle],
};
let animation = super::Animation {
clips: vec![super::AnimationClip {
name: "bend".into(),
duration: 1.0,
repeat: super::TweenRepeat::Loop,
tracks: vec![super::AnimationTrack {
target: "Hip/Spine".into(),
property: super::AnimationProperty::Rotation,
keys: vec![bend(-0.4, 0.0), bend(0.4, 0.5), bend(-0.4, 1.0)],
..super::AnimationTrack::default()
}],
..super::AnimationClip::default()
}],
autoplay: "bend".into(),
..super::Animation::default()
};
let side = (characters as f32).sqrt().ceil() as usize;
for index in 0..characters {
let [x, z] = grid(index, side, 1.2);
let character = world
.spawn((
Name(render_benchmark_character(index)),
Transform {
position: [x, 0.9, z],
..Transform::default()
},
MeshRenderer {
mesh: body,
material: skin,
cast_shadows: true,
receive_shadows: true,
},
Visibility::default(),
super::Skin {
joints: vec!["Hip".into(), "Hip/Spine".into()],
inverse_bind: Vec::new(),
},
animation.clone(),
))
.id();
let hip = world.spawn((Name("Hip".into()), Transform::default())).id();
let spine = world
.spawn((Name("Spine".into()), Transform::default()))
.id();
for (child, parent) in [(hip, character), (spine, hip)] {
super::hierarchy::set_parent(world, child, parent)
.expect("fresh entities have no parent");
}
}
}
fn spawn_rain(world: &mut World) {
let [particles, emitters, roofs] = RENDER_BENCHMARK_RAIN;
let samples = RENDER_BENCHMARK_RAIN_GROUND;
let spacing = 64.0 / (samples - 1) as f32;
let ground = super::Heightfield {
heights: (0..samples)
.map(|row| {
(0..samples)
.map(|column| {
let [x, z] =
[column, row].map(|at| at as f32 * spacing);
0.3 + 0.3 * (x * 0.3).sin() * (z * 0.2).cos()
})
.collect()
})
.collect(),
spacing,
..super::Heightfield::default()
};
let (cube, ground_mesh, ground_material, roof_material) = {
let mut assets =
world.get_resource_or_insert_with(AssetServer::default);
let ground_mesh = assets.meshes.insert(ground.mesh());
let ground_material = assets.materials.insert(MaterialAsset {
name: "Benchmark Wet Ground".into(),
base_color: [0.25, 0.3, 0.2, 1.0],
roughness: 0.3,
..MaterialAsset::default()
});
let roof_material = assets.materials.insert(MaterialAsset {
name: "Benchmark Roof".into(),
base_color: [0.5, 0.2, 0.15, 1.0],
roughness: 0.8,
..MaterialAsset::default()
});
(
assets.fallback_mesh,
ground_mesh,
ground_material,
roof_material,
)
};
world.spawn((
Name("Benchmark Rain Ground".into()),
Transform::default(),
MeshRenderer {
mesh: ground_mesh,
material: ground_material,
cast_shadows: false,
receive_shadows: true,
},
Visibility::default(),
ground,
));
let side = (emitters as f32).sqrt().ceil() as usize;
let per_emitter = (particles / emitters) as u32;
for index in 0..emitters {
let [x, z] = grid(index, side, 14.0);
world.spawn((
Name(format!("Benchmark Rain {index}")),
Transform {
position: [x, 20.0, z],
..Transform::default()
},
super::ParticleEmitter {
rate: per_emitter as f32 / 1.5,
max_particles: per_emitter,
shape: super::EmitterShape::Box,
shape_size: [7.0, 0.5, 7.0],
lifetime: [3.0, 3.0],
speed: [4.0, 6.0],
size: [0.02, 0.04],
direction: [0.0, -1.0, 0.0],
spread: 0.05,
gravity: 9.8,
color_over_life: vec![super::ColorKey {
t: 0.0,
color: [0.7, 0.8, 1.0, 0.5],
}],
facing: super::ParticleFacing::Velocity,
stretch: 0.03,
blockers: true,
..super::ParticleEmitter::default()
},
super::ParticleSystem::default(),
));
}
let side = (roofs as f32).sqrt().ceil() as usize;
for index in 0..roofs {
let [x, z] = grid(index, side, 14.0);
world.spawn((
Name(format!("Benchmark Roof {index}")),
Transform {
position: [x + 2.0, 4.0, z + 2.0],
scale: [5.0, 0.3, 5.0],
..Transform::default()
},
MeshRenderer {
mesh: cube,
material: roof_material,
cast_shadows: true,
receive_shadows: true,
},
Visibility::default(),
super::ParticleBlocker { extents: [0.5; 3] },
));
}
}
#[must_use]
pub fn render_benchmark_city_camera(buildings: usize, frame: u32) -> Transform {
let (side, half) = city_layout(buildings);
let street =
|index: usize| -half + CITY_STREET / 2.0 + city_pitch() * index as f32;
let near = street(side / 4);
let far = street((side - 1 - side / 4).max(side / 4 + 1));
let turn = (frame % RENDER_BENCHMARK_FRAMES) as f32
/ RENDER_BENCHMARK_FRAMES as f32
* 4.0;
let leg = (turn as usize).min(3);
let along = near + (far - near) * (turn - leg as f32);
let back = far + near - along;
let ([x, z], direction) = match leg {
0 => ([along, near], [1.0, 0.0]),
1 => ([far, along], [0.0, 1.0]),
2 => ([back, far], [-1.0, 0.0]),
_ => ([near, back], [0.0, -1.0]),
};
Transform {
position: [x, 1.7, z],
rotation: crate::engine::rotation_facing([
direction[0],
-0.1,
direction[1],
]),
..Transform::default()
}
}
pub const RENDER_BENCHMARK_SHADOWED_LIGHTS: [usize; 2] = [8, 4];
#[must_use]
pub fn render_benchmark_bear(index: usize) -> String {
format!("Benchmark Bear {index}")
}
pub fn spawn_render_benchmark_extras(
world: &mut World,
extras: RenderBenchmarkExtras,
) -> Vec<Entity> {
let (cube, sphere, bear_material, screen_materials) = {
let mut assets =
world.get_resource_or_insert_with(AssetServer::default);
let sphere = assets.meshes.insert(procedural_sphere_mesh(8));
let bear = assets.materials.insert(MaterialAsset {
base_color: [0.6, 0.4, 0.3, 1.0],
roughness: 0.8,
..MaterialAsset::default()
});
let screens: Vec<_> = (0..extras.screens)
.map(|index| {
assets.materials.insert(MaterialAsset {
name: format!("Benchmark Screen {index}"),
base_color: [0.0, 0.0, 0.0, 1.0],
emissive: [1.0; 3],
..MaterialAsset::default()
})
})
.collect();
(assets.fallback_mesh, sphere, bear, screens)
};
for (index, material) in screen_materials.into_iter().enumerate() {
let angle = TAU * index as f32 / extras.screens as f32;
let (sin, cos) = angle.sin_cos();
let camera = format!("Benchmark Feed {index}");
world.spawn((
Name(camera.clone()),
Transform {
position: [6.0 * sin, 4.0, 6.0 * cos],
rotation: crate::engine::rotation_facing([sin, -0.3, cos]),
..Transform::default()
},
Camera::default(),
));
world.spawn((
Name(format!("Benchmark Screen {index}")),
Transform {
position: [20.0 * sin, 4.0, 20.0 * cos],
rotation: crate::engine::rotation_facing([-sin, 0.0, -cos]),
scale: [3.2, 1.8, 0.1],
},
MeshRenderer {
mesh: cube,
material,
cast_shadows: false,
receive_shadows: false,
},
Visibility::default(),
super::CameraScreen {
camera,
size: extras.screen_size,
update_every: extras.screen_every.max(1),
..Default::default()
},
));
}
if extras.without_bodies {
let mut bodies = world.query::<(Entity, &ObjectClasses)>();
let bodies: Vec<_> = bodies
.iter(world)
.filter(|(_, classes)| {
classes.contains(RENDER_BENCHMARK_BODY_CLASS)
})
.map(|(entity, _)| entity)
.collect();
for body in bodies {
world.despawn(body);
}
}
let side = (extras.lights as f32).sqrt().ceil() as usize;
let spacing = 72.0 / side.max(1) as f32;
for index in 0..extras.lights {
let [x, z] = grid(index, side, spacing);
let transform = Transform {
position: [x, 3.0, z],
rotation: crate::engine::rotation_facing([0.0, -1.0, 0.0]),
..Transform::default()
};
let mut color = [0.4; 3];
color[index % 3] = 1.0;
let name = Name(format!("Benchmark Street Light {index}"));
let [points, spots] = RENDER_BENCHMARK_SHADOWED_LIGHTS;
if index % 2 == 0 {
world.spawn((
name,
transform,
PointLight {
color,
intensity: 300.0,
range: 8.0,
shadows: index / 2 < points,
radius: 0.0,
},
));
} else {
world.spawn((
name,
transform,
super::SpotLight {
color,
intensity: 600.0,
range: 8.0,
shadows: index / 2 < spots,
..super::SpotLight::default()
},
));
}
}
spawn_city(world, extras.city);
if extras.effects {
spawn_effects(world);
}
if extras.rain {
spawn_rain(world);
}
if extras.probes {
world.spawn((
Name("Benchmark Light Probes".into()),
Transform {
position: [0.0, 6.0, 0.0],
..Transform::default()
},
super::LightProbeGrid {
extents: [24.0, 6.0, 24.0],
resolution: RENDER_BENCHMARK_PROBES,
intensity: 1.0,
},
));
}
let side = (extras.bears as f32).sqrt().ceil() as usize;
(0..extras.bears)
.map(|index| {
world
.spawn((
Name(render_benchmark_bear(index)),
bear_transform(index, side, 0),
MeshRenderer {
mesh: sphere,
material: bear_material,
cast_shadows: true,
receive_shadows: true,
},
Visibility::default(),
))
.id()
})
.collect()
}
pub fn update_render_benchmark_bears(
world: &mut World,
bears: &[Entity],
extras: RenderBenchmarkExtras,
frame: u32,
) {
let side = (bears.len() as f32).sqrt().ceil() as usize;
for (index, &bear) in bears.iter().take(extras.bear_updates).enumerate() {
if let Some(mut transform) = world.get_mut::<Transform>(bear) {
*transform = bear_transform(index, side, frame);
}
}
}
fn bear_transform(index: usize, side: usize, frame: u32) -> Transform {
let [x, z] = grid(index, side.max(1), 0.6);
let bob = 0.05 * (frame as f32 * 0.2 + index as f32).sin();
Transform {
position: [x, 0.2 + bob, z],
scale: [0.4; 3],
..Transform::default()
}
}
#[must_use]
pub fn render_benchmark_camera(frame: u32) -> Transform {
let turn = (frame % RENDER_BENCHMARK_FRAMES) as f32
/ RENDER_BENCHMARK_FRAMES as f32;
let angle = TAU * turn;
let reach = 28.0 + 12.0 * (2.0 * angle).cos();
let height = 10.0 + 6.0 * (2.0 * angle).cos();
let position = [reach * angle.sin(), height, reach * angle.cos()];
let target = [0.0, 2.0, 0.0];
Transform {
position,
rotation: crate::engine::rotation_facing(
[0, 1, 2].map(|axis| target[axis] - position[axis]),
),
..Transform::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn camera_path_is_closed_repeatable_and_faces_the_scene() {
assert_eq!(
render_benchmark_camera(0),
render_benchmark_camera(RENDER_BENCHMARK_FRAMES)
);
for frame in 0..RENDER_BENCHMARK_FRAMES {
let camera = render_benchmark_camera(frame);
assert_eq!(camera, render_benchmark_camera(frame));
let [x, y, z] = camera.position;
let reach = (x * x + z * z).sqrt();
assert!((15.9..=40.1).contains(&reach), "{frame}: {reach}");
assert!(y > 3.9, "{frame}: {y}");
assert!(camera.rotation[0] < 0.0, "{frame}");
}
let quarter = render_benchmark_camera(RENDER_BENCHMARK_FRAMES / 4);
assert!(quarter.position[0] > 15.0, "{quarter:?}");
}
#[test]
fn scene_spawns_the_same_fixed_content_every_time() {
let counts = || {
let mut world = World::new();
let camera = spawn_render_benchmark(&mut world);
assert!(world.get::<Camera>(camera).unwrap().active);
let mut meshes =
world.query::<(&Name, &Transform, &MeshRenderer)>();
let layout: Vec<_> = meshes
.iter(&world)
.map(|(name, transform, _)| (name.0.clone(), *transform))
.collect();
let bodies = world.query::<&PhysicsBody>().iter(&world).count();
let points = world.query::<&PointLight>().iter(&world).count();
let suns = world.query::<&DirectionalLight>().iter(&world).count();
(layout, bodies, points, suns)
};
let (layout, bodies, points, suns) = counts();
assert_eq!(
layout.len(),
1 + 2 * RENDER_BENCHMARK_PILLARS + RENDER_BENCHMARK_BODIES
);
assert_eq!(bodies, 1 + RENDER_BENCHMARK_BODIES);
assert_eq!((points, suns), (RENDER_BENCHMARK_POINT_LIGHTS, 1));
assert_eq!(counts().0, layout);
}
#[test]
fn extra_lights_alternate_points_and_spots_with_few_shadows() {
let mut world = World::new();
spawn_render_benchmark(&mut world);
spawn_render_benchmark_extras(
&mut world,
RenderBenchmarkExtras {
lights: 301,
..RenderBenchmarkExtras::default()
},
);
let points: Vec<_> =
world.query::<&PointLight>().iter(&world).copied().collect();
let spots: Vec<_> = world
.query::<&super::super::SpotLight>()
.iter(&world)
.copied()
.collect();
assert_eq!(points.len(), RENDER_BENCHMARK_POINT_LIGHTS + 151);
assert_eq!(spots.len(), 150);
let shadowed = [
points.iter().filter(|light| light.shadows).count(),
spots.iter().filter(|light| light.shadows).count(),
];
assert_eq!(shadowed, RENDER_BENCHMARK_SHADOWED_LIGHTS);
}
#[test]
fn city_shares_lod_meshes_and_textures_and_its_streets_are_clear() {
let buildings = 600;
let mut world = World::new();
spawn_render_benchmark_extras(
&mut world,
RenderBenchmarkExtras {
city: buildings,
..RenderBenchmarkExtras::default()
},
);
let [meshes, materials, textures] =
render_benchmark_city_assets(buildings);
assert_eq!([meshes, materials, textures], [60, 12, 3]);
assert_eq!(render_benchmark_city_assets(10_000), [1000, 200, 64]);
let assets = world.resource::<AssetServer>();
assert_eq!(assets.lod_groups.len(), meshes);
let city_materials = assets
.materials
.iter()
.filter(|(_, material)| material.name.starts_with("Benchmark City"))
.count();
assert_eq!(city_materials, materials);
let lots: Vec<_> = world
.query::<(&Name, &Transform, &MeshRenderer)>()
.iter(&world)
.filter(|(name, ..)| name.0.starts_with("Benchmark Building"))
.map(|(_, transform, renderer)| (transform.position, renderer.mesh))
.collect();
assert_eq!(lots.len(), buildings);
let distinct: std::collections::HashSet<_> =
lots.iter().map(|(_, mesh)| *mesh).collect();
assert_eq!(distinct.len(), meshes);
let first = render_benchmark_city_camera(buildings, 0);
let last =
render_benchmark_city_camera(buildings, RENDER_BENCHMARK_FRAMES);
assert_eq!(first, last);
for frame in (0..RENDER_BENCHMARK_FRAMES).step_by(5) {
let eye = render_benchmark_city_camera(buildings, frame).position;
assert_eq!(eye[1], 1.7);
for (position, _) in &lots {
let clear = (eye[0] - position[0]).abs() > CITY_LOT / 2.0
|| (eye[2] - position[2]).abs() > CITY_LOT / 2.0;
assert!(
clear,
"frame {frame} at {eye:?} is inside {position:?}"
);
}
}
}
#[test]
fn effects_fill_gpu_emitters_and_skin_animated_characters() {
let mut app = super::super::App::new();
app.add_plugin(super::super::HybridPhysicsPlugin).unwrap();
app.add_plugin(super::super::RenderExtractPlugin).unwrap();
spawn_render_benchmark_extras(
app.world_mut(),
RenderBenchmarkExtras {
effects: true,
..RenderBenchmarkExtras::default()
},
);
let [particles, blended, characters] = RENDER_BENCHMARK_EFFECTS;
let world = app.world_mut();
let emitters: Vec<_> = world
.query::<&super::super::ParticleEmitter>()
.iter(world)
.map(|emitter| (emitter.max_particles, emitter.simulation))
.collect();
assert_eq!(emitters.len(), RENDER_BENCHMARK_EFFECT_EMITTERS);
let total: u32 = emitters.iter().map(|(max, _)| max).sum();
assert_eq!(total as usize, particles);
assert!(emitters.iter().all(|(_, simulation)| {
*simulation == super::super::ParticleSimulation::Gpu
}));
let materials: Vec<_> = world
.query::<&MeshRenderer>()
.iter(world)
.map(|renderer| renderer.material)
.collect();
let assets = world.resource::<AssetServer>();
let see_through = materials
.iter()
.filter(|&&material| {
assets.materials.get(material).unwrap().alpha_mode
== AlphaMode::Blend
})
.count();
assert_eq!(see_through, blended);
let tick = std::time::Duration::from_secs_f64(1.0 / 60.0);
for _ in 0..10 {
app.update(tick).unwrap();
}
let jobs = super::super::skin_jobs(app.world_mut());
assert_eq!(jobs.len(), characters);
let bent = jobs[0].matrices[1];
assert_ne!(bent, jobs[0].matrices[0], "the spine bends off the hip");
}
#[test]
fn probes_add_one_full_grid_over_the_scene() {
let mut world = World::new();
spawn_render_benchmark_extras(
&mut world,
RenderBenchmarkExtras {
probes: true,
..RenderBenchmarkExtras::default()
},
);
let grids: Vec<_> = world
.query::<&super::super::LightProbeGrid>()
.iter(&world)
.map(|grid| grid.resolution)
.collect();
assert_eq!(grids, [RENDER_BENCHMARK_PROBES]);
assert_eq!(RENDER_BENCHMARK_PROBES.iter().product::<u32>(), 64);
}
#[test]
fn rain_comes_from_blocked_cpu_emitters_over_ground_and_roofs() {
let mut world = World::new();
spawn_render_benchmark_extras(
&mut world,
RenderBenchmarkExtras {
rain: true,
..RenderBenchmarkExtras::default()
},
);
let [particles, emitters, roofs] = RENDER_BENCHMARK_RAIN;
let rain: Vec<_> = world
.query::<&super::super::ParticleEmitter>()
.iter(&world)
.map(|emitter| {
(emitter.blockers, emitter.simulation, emitter.max_particles)
})
.collect();
assert_eq!(rain.len(), emitters);
assert!(rain.iter().all(|&(blockers, simulation, _)| {
blockers && simulation == super::super::ParticleSimulation::Cpu
}));
let cap: u32 = rain.iter().map(|(.., max)| max).sum();
assert_eq!(cap as usize, particles);
let blockers = world
.query::<&super::super::ParticleBlocker>()
.iter(&world)
.count();
assert_eq!(blockers, roofs);
let ground = world
.query::<&super::super::Heightfield>()
.single(&world)
.unwrap();
assert_eq!(ground.heights.len(), RENDER_BENCHMARK_RAIN_GROUND);
}
}