use core::ops::Range;
use core::time::Duration;
use crate::camera::Frustum;
use crate::light::MAX_LIGHTS;
use crate::math::{Mat4, Vec3};
use crate::mesh::{Draw, Geometry, Meshes, Palette, Placement, Skinned};
use crate::overrun::Overrun;
use crate::post_effect::PostEffectId;
use crate::renderer::mesh_cache::{MeshCache, MeshId, Store};
use crate::renderer::pipelines::{GpuInstance, Relief, Turn, WorldPlane};
use crate::renderer::post::{DEFAULT_BLOOM, DEFAULT_EXPOSURE};
use crate::renderer::skybox::SkyId;
use crate::surface_style::{DrawPass, Styled, SurfaceStyleId};
use crate::{Camera, Light, Material};
const LEVEL: f32 = 1e-6;
pub(crate) struct DrawList<M: Meshes> {
instances: Vec<Draw<M>>,
now: Duration,
camera: Option<Camera>,
skybox: Option<SkyId>,
lights: Vec<Light>,
exposure: Option<f32>,
bloom: Option<f32>,
styles: Vec<Handed>,
screens: Vec<Handed>,
overrun: Overrun,
}
impl<M: Meshes> DrawList<M> {
pub(crate) fn new() -> Self {
Self {
instances: Vec::new(),
now: Duration::ZERO,
camera: None,
skybox: None,
lights: Vec::new(),
exposure: None,
bloom: None,
styles: Vec::new(),
screens: Vec::new(),
overrun: Overrun::default(),
}
}
pub(crate) fn start(&mut self, now: Duration) {
self.now = now;
}
pub(crate) fn push<T>(&mut self, draw: Draw<T>)
where
M: From<T>,
{
self.instances.push(draw.into_set());
}
pub(crate) fn now(&self) -> Duration {
self.now
}
pub(crate) fn set_surface_style(
&mut self,
style: SurfaceStyleId,
write: impl FnOnce(&mut Vec<u8>),
) {
Handed::keep(&mut self.styles, style.0 as usize, write);
}
pub(crate) fn set_post_effect(
&mut self,
screen: PostEffectId,
write: impl FnOnce(&mut Vec<u8>),
) {
Handed::keep(&mut self.screens, screen.0 as usize, write);
}
pub(crate) fn post_effect_values(&self, screen: PostEffectId) -> Option<&[u8]> {
Handed::read(&self.screens, screen.0 as usize)
}
pub(crate) fn surface_style_values(&self, style: SurfaceStyleId) -> Option<&[u8]> {
Handed::read(&self.styles, style.0 as usize)
}
pub(crate) fn set_camera(&mut self, camera: Camera) {
self.camera = Some(camera);
}
pub(crate) fn set_skybox(&mut self, skybox: Option<SkyId>) {
self.skybox = skybox;
}
pub(crate) fn set_exposure(&mut self, exposure: f32) {
self.exposure = Some(exposure.max(0.0));
}
pub(crate) fn set_bloom(&mut self, amount: f32) {
self.bloom = Some(amount.clamp(0.0, 1.0));
}
pub(crate) fn push_light(&mut self, light: Light) {
if self.lights.len() == MAX_LIGHTS {
self.overrun.report(format_args!(
"a frame submitted more than {MAX_LIGHTS} lights; ignoring one"
));
return;
}
self.lights.push(light);
}
pub(crate) fn camera(&self) -> Camera {
self.camera.unwrap_or_default()
}
pub(crate) fn skybox(&self) -> Option<SkyId> {
self.skybox
}
pub(crate) fn lights(&self) -> &[Light] {
&self.lights
}
pub(crate) fn casting(&self) -> bool {
self.lights.iter().any(|light| light.casts)
}
pub(crate) fn exposure(&self) -> f32 {
self.exposure.unwrap_or(DEFAULT_EXPOSURE)
}
pub(crate) fn bloom(&self) -> f32 {
self.bloom.unwrap_or(DEFAULT_BLOOM)
}
pub(crate) fn clear(&mut self) {
self.instances.clear();
self.now = Duration::ZERO;
self.camera = None;
self.skybox = None;
self.lights.clear();
self.exposure = None;
self.bloom = None;
for handed in self.styles.iter_mut().chain(&mut self.screens) {
handed.written = false;
}
}
}
#[derive(Default)]
struct Handed {
values: Vec<u8>,
written: bool,
}
impl Handed {
fn keep(seats: &mut Vec<Self>, at: usize, write: impl FnOnce(&mut Vec<u8>)) {
if at >= seats.len() {
seats.resize_with(at + 1, Self::default);
}
let handed = &mut seats[at];
handed.values.clear();
write(&mut handed.values);
handed.written = true;
}
fn read(seats: &[Self], at: usize) -> Option<&[u8]> {
let handed = seats.get(at)?;
handed.written.then_some(&handed.values[..])
}
}
pub(crate) struct Batch {
pub(crate) mesh: MeshId,
pub(crate) part: u32,
pub(crate) style: Option<SurfaceStyleId>,
pub(crate) turn: Turn,
pub(crate) skinned: bool,
pub(crate) indices: Range<u32>,
pub(crate) instances: Range<u32>,
}
pub(crate) struct Batcher {
recorded: Vec<GpuInstance>,
order: Vec<Placed>,
partitioned: Vec<Placed>,
layers: Vec<Layer>,
glows: Vec<Glow>,
instances: Vec<GpuInstance>,
palette: Palette,
opaque: Vec<Batch>,
cutout: Vec<Batch>,
translucent: Vec<Batch>,
additive: Vec<Batch>,
placed_casters: Vec<Batch>,
sampled_casters: Vec<Batch>,
}
impl Batcher {
pub(crate) fn new() -> Self {
Self {
recorded: Vec::new(),
order: Vec::new(),
partitioned: Vec::new(),
layers: Vec::new(),
glows: Vec::new(),
instances: Vec::new(),
palette: Palette::default(),
opaque: Vec::new(),
cutout: Vec::new(),
translucent: Vec::new(),
additive: Vec::new(),
placed_casters: Vec::new(),
sampled_casters: Vec::new(),
}
}
pub(crate) fn run<M: Meshes>(
&mut self,
draws: &DrawList<M>,
meshes: &mut MeshCache<M>,
aspect: f32,
) {
let Self {
recorded,
order,
partitioned,
layers,
glows,
instances,
palette,
opaque,
cutout,
translucent,
additive,
placed_casters,
sampled_casters,
} = self;
recorded.clear();
order.clear();
layers.clear();
glows.clear();
instances.clear();
palette.clear();
opaque.clear();
cutout.clear();
translucent.clear();
additive.clear();
placed_casters.clear();
sampled_casters.clear();
let casting = draws.casting();
let now = draws.now();
let camera = draws.camera();
let view = camera.view();
let frustum = Frustum::new(camera.view_projection(aspect));
let looking = (view.target() - view.eye()).normalize_or_zero();
let depth_of = |placed: Vec3| (placed - view.eye()).dot(looking);
let level = level_of(looking);
let mut previous: Option<(&M, MeshId)> = None;
let mut grouped = true;
let mut keyed = true;
for draw in &draws.instances {
let mesh = match previous {
Some((key, id)) if key == draw.mesh() => id,
_ => {
let id = meshes.id_of(draw.mesh());
previous = Some((draw.mesh(), id));
id
}
};
let placement = draw.placement(view);
let styled = draw.styled();
let Some(geometry) = meshes.store().geometry(mesh) else {
continue;
};
if !placement.transform().is_finite() {
continue;
}
let visible = frustum.holds(geometry.sphere().placed(placement.transform()));
let mut lying = None;
let mut taken = None;
for part in 0..geometry.part_count() {
if geometry.part_indices(part).is_empty() {
continue;
}
let material = draw.resolved(geometry.part_of(part), geometry.part_material(part));
let casts = casting && casts_shadow(styled, material);
if !(visible || casts) {
continue;
}
let plane = *lying
.get_or_insert_with(|| lain(placement, geometry, styled, draw.anchor(), level));
let run = *taken.get_or_insert_with(|| {
palette.take(
Skinned(mesh.0),
geometry.rig(),
geometry.clips(),
draw.posing(now, geometry.clips()),
)
});
let turn = Turn::of(placement.faced(), plane);
let key = SortKey {
style: styled.map(|style| style.id),
turn,
mesh,
part: part as u32,
};
let pass = pass_of(styled, material);
let relief = Relief::of(geometry.part_relief(part), placement.faced());
let instance = recorded.len() as u32;
match pass {
DrawPass::Additive => glows.push(Glow {
key,
recorded: instance,
}),
DrawPass::Translucent => layers.push(Layer {
depth: depth_of(draw.anchor()),
key,
recorded: instance,
hidden: !visible,
casts,
}),
_ => {
let placed = Placed {
tested: pass == DrawPass::Cutout,
key,
hidden: !visible,
recorded: instance,
casts,
sampled: pass == DrawPass::Cutout || relief.solid() || turn.lies(),
};
if let Some(last) = order.last().filter(|last| placed < **last) {
keyed &= (placed.tested, placed.key) >= (last.tested, last.key);
grouped = false;
}
order.push(placed);
}
}
recorded.push(
GpuInstance::new(placement, material, pass, draw.window(), relief, plane)
.posed(run),
);
}
}
if !grouped {
if keyed {
let one_key = |left: &Placed, right: &Placed| {
(left.tested, left.key) == (right.tested, right.key)
};
for run in order.chunk_by_mut(one_key) {
partitioned.extend(run.iter().filter(|placed| !placed.hidden));
partitioned.extend(run.iter().filter(|placed| placed.hidden));
run.copy_from_slice(partitioned);
partitioned.clear();
}
} else {
order.sort_unstable();
}
}
layers.sort_by(|left, right| right.depth.total_cmp(&left.depth));
if grouped && layers.is_empty() && glows.is_empty() {
core::mem::swap(recorded, instances);
} else {
let sorted = order
.iter()
.map(|placed| placed.recorded)
.chain(layers.iter().map(|layer| layer.recorded))
.chain(glows.iter().map(|glow| glow.recorded));
instances.extend(sorted.map(|index| recorded[index as usize]));
}
for (offset, placed) in order.iter().enumerate() {
let instance = offset as u32;
if !placed.hidden {
let pass = if placed.tested {
&mut *cutout
} else {
&mut *opaque
};
collapse(pass, placed.key, instance, meshes.store());
}
if placed.casts {
let into = if placed.sampled {
&mut *sampled_casters
} else {
&mut *placed_casters
};
collapse(into, placed.key, instance, meshes.store());
}
}
for (offset, layer) in layers.iter().enumerate() {
let instance = (order.len() + offset) as u32;
if !layer.hidden {
collapse(translucent, layer.key, instance, meshes.store());
}
if layer.casts {
collapse(sampled_casters, layer.key, instance, meshes.store());
}
}
for (offset, glow) in glows.iter().enumerate() {
let instance = (order.len() + layers.len() + offset) as u32;
collapse(additive, glow.key, instance, meshes.store());
}
}
pub(crate) fn opaque(&self) -> &[Batch] {
&self.opaque
}
pub(crate) fn cutout(&self) -> &[Batch] {
&self.cutout
}
pub(crate) fn translucent(&self) -> &[Batch] {
&self.translucent
}
pub(crate) fn additive(&self) -> &[Batch] {
&self.additive
}
pub(crate) fn drawn(&self) -> impl Iterator<Item = &Batch> {
self.opaque
.iter()
.chain(&self.cutout)
.chain(&self.translucent)
.chain(&self.additive)
}
pub(crate) fn casters(&self) -> Casters<'_> {
Casters {
plain: &self.placed_casters,
sampled: &self.sampled_casters,
}
}
pub(crate) fn instances(&self) -> &[GpuInstance] {
&self.instances
}
pub(crate) fn palette(&self) -> &[Mat4] {
self.palette.matrices()
}
}
#[derive(Clone, Copy)]
pub(crate) struct Casters<'a> {
plain: &'a [Batch],
sampled: &'a [Batch],
}
impl<'a> Casters<'a> {
pub(crate) fn plain(self) -> &'a [Batch] {
self.plain
}
pub(crate) fn sampled(self) -> &'a [Batch] {
self.sampled
}
pub(crate) fn batches(self) -> impl Iterator<Item = &'a Batch> {
self.plain.iter().chain(self.sampled)
}
}
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
struct SortKey {
style: Option<SurfaceStyleId>,
turn: Turn,
mesh: MeshId,
part: u32,
}
#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
struct Placed {
tested: bool,
key: SortKey,
hidden: bool,
recorded: u32,
casts: bool,
sampled: bool,
}
#[derive(Clone, Copy)]
struct Layer {
depth: f32,
key: SortKey,
recorded: u32,
hidden: bool,
casts: bool,
}
#[derive(Clone, Copy)]
struct Glow {
key: SortKey,
recorded: u32,
}
fn level_of(looking: Vec3) -> Option<Vec3> {
let level = Vec3::new(looking.x, 0.0, looking.z);
(level.length_squared() >= LEVEL).then_some(level)
}
fn lain(
placement: Placement,
geometry: &Geometry,
styled: Option<Styled>,
anchor: Vec3,
level: Option<Vec3>,
) -> WorldPlane {
if placement.faced() {
return level.map_or(WorldPlane::NONE, |level| WorldPlane::upright(level, anchor));
}
if styled.is_some_and(|styled| styled.displaces) {
return WorldPlane::NONE;
}
geometry.plane().map_or(WorldPlane::NONE, |plane| {
WorldPlane::of(plane.placed(placement.transform()))
})
}
fn pass_of(styled: Option<Styled>, material: Material) -> DrawPass {
if let Some(styled) = styled {
return styled.pass;
}
if material.adds() {
DrawPass::Additive
} else if material.translucent() {
DrawPass::Translucent
} else if material.cuts() {
DrawPass::Cutout
} else {
DrawPass::Opaque
}
}
fn casts_shadow(styled: Option<Styled>, material: Material) -> bool {
match styled {
Some(styled) => matches!(styled.pass, DrawPass::Opaque | DrawPass::Cutout),
None => !material.adds() && material.covers(),
}
}
fn collapse(batches: &mut Vec<Batch>, key: SortKey, instance: u32, meshes: &Store) {
match batches.last_mut() {
Some(last)
if (last.mesh, last.part, last.style, last.turn)
== (key.mesh, key.part, key.style, key.turn)
&& last.instances.end == instance =>
{
last.instances.end = instance + 1;
}
_ => {
let Some(geometry) = meshes.geometry(key.mesh) else {
return;
};
batches.push(Batch {
mesh: key.mesh,
part: key.part,
style: key.style,
turn: key.turn,
skinned: geometry.rig().skins(),
indices: geometry.part_indices(key.part as usize),
instances: instance..instance + 1,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::UVec2;
use crate::mesh::{
Frame, Instance, Mesh, MeshData, Part, Placement, Plane, Quad, Sheet, Slot, Sphere,
};
use crate::renderer::mesh_cache::DEFAULT_MEMORY;
use crate::{
Assets, Catalog, Color, Material, Projection, TextureData, Transform, View, meshes,
};
const LUMP: Sphere = Sphere { subdivisions: 0 };
const GLASS: Material = Material::color(Color::rgba(1.0, 1.0, 1.0, 0.5));
const LEAF: Material = Material::color(Color::WHITE).cutout();
const SPARK: Material = Material::color(Color::WHITE).additive();
const LOOKING: Camera = Camera::new(
View::look_at(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO),
Projection::perspective(60.0),
);
const ASPECT: f32 = 16.0 / 9.0;
meshes! { enum Lumps { Sphere, Quad, Plane, Pane, Gapped } }
fn push<T>(draws: &mut DrawList<Lumps>, instance: Instance<T>)
where
Lumps: From<T>,
{
draws.push(instance.record());
}
fn batched<M: Meshes>(draws: &DrawList<M>) -> Batcher {
let mut batcher = Batcher::new();
batcher.run(
draws,
&mut MeshCache::new(std::rc::Rc::new(Assets::default()), DEFAULT_MEMORY),
ASPECT,
);
batcher
}
fn casting() -> DrawList<Lumps> {
let mut draws = DrawList::<Lumps>::new();
draws.push_light(Light::directional(Vec3::NEG_Y, Color::WHITE).shadow());
draws
}
fn placement(at: impl Into<Transform>) -> Placement {
LUMP.at::<()>(at).record().placement(LOOKING.view())
}
fn batch(draws: &DrawList<Lumps>) -> (usize, usize) {
let batcher = batched(draws);
(batcher.opaque().len(), batcher.instances().len())
}
fn panes(depths: &[f32]) -> DrawList<Lumps> {
let mut draws = DrawList::<Lumps>::new();
draws.set_camera(LOOKING);
for &depth in depths {
push(&mut draws, LUMP.at(Vec3::NEG_Z * depth).material(GLASS));
}
draws
}
#[test]
fn material_parameters_never_split_a_batch() {
let mut draws = DrawList::<Lumps>::new();
push(
&mut draws,
LUMP.at(Vec3::ZERO).material(Material::color(Color::WHITE)),
);
push(
&mut draws,
LUMP.at(Vec3::X)
.material(Material::lit(Color::rgb(1.0, 0.0, 0.0))),
);
push(
&mut draws,
LUMP.at(Vec3::Y)
.material(Material::shaded(Color::rgb(0.0, 0.0, 1.0), 0.5)),
);
push(
&mut draws,
LUMP.at(Vec3::Z)
.material(Material::lit(Color::WHITE).roughness(0.25)),
);
assert_eq!(batch(&draws), (1, 4));
}
#[test]
fn facing_splits_a_batch_from_the_draws_a_transform_placed_and_from_nothing_else() {
let mut draws = DrawList::<Lumps>::new();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::ZERO).billboard());
push(&mut draws, LUMP.at(Vec3::X).upright());
push(&mut draws, LUMP.at(Vec3::Y));
assert_eq!(
batch(&draws),
(2, 3),
"the two the camera turned draw through stages of their own"
);
assert_eq!(
batched(&draws)
.opaque()
.iter()
.map(|batch| (batch.turn, batch.instances.clone()))
.collect::<Vec<_>>(),
vec![(Turn::Placed, 0..1), (Turn::Faced, 1..3)],
"and collapse together whichever facing each of them asked for"
);
}
#[test]
fn a_faced_draw_is_turned_toward_the_camera_before_it_is_recorded() {
let mut draws = DrawList::<Lumps>::new();
draws.set_camera(LOOKING);
push(&mut draws, Quad.at(Vec3::NEG_Z).billboard());
let faced = batched(&draws).instances()[0];
assert_eq!(
Quad.at::<()>(Vec3::NEG_Z)
.billboard()
.record()
.placement(LOOKING.view())
.transform(),
Quad.at::<()>(Vec3::NEG_Z)
.record()
.placement(LOOKING.view())
.transform(),
"a square already facing the camera is left where it was"
);
let mut across = DrawList::<Lumps>::new();
across.set_camera(Camera::new(
View::look_at(Vec3::new(10.0, 0.0, -1.0), Vec3::NEG_Z),
Projection::perspective(60.0),
));
push(&mut across, Quad.at(Vec3::NEG_Z).billboard());
assert_ne!(
batched(&across).instances()[0],
faced,
"and one the camera moved past is turned to it"
);
}
#[test]
fn the_part_of_a_texture_a_draw_samples_rides_its_instance() {
let sheet = Sheet::new(UVec2::new(2, 2));
let mut draws = DrawList::<Lumps>::new();
push(&mut draws, LUMP.at(Vec3::ZERO).frame(sheet.cell(3)));
push(&mut draws, LUMP.at(Vec3::X));
let batcher = batched(&draws);
let recorded = |at: Vec3, frame| {
GpuInstance::new(
placement(at),
Material::default(),
DrawPass::Opaque,
frame,
Relief::of(None, false),
WorldPlane::NONE,
)
};
assert_eq!(batcher.opaque().len(), 1, "and never splits a batch");
assert_eq!(
batcher.instances(),
vec![
recorded(Vec3::ZERO, sheet.cell(3)),
recorded(Vec3::X, Frame::default()),
]
);
}
#[test]
fn a_cutout_draw_is_batched_after_every_plain_one_and_apart_from_them() {
let mut draws = DrawList::<Lumps>::new();
push(&mut draws, LUMP.at(Vec3::ZERO).material(LEAF));
push(&mut draws, LUMP.at(Vec3::X));
push(&mut draws, LUMP.at(Vec3::Y).material(LEAF));
let batcher = batched(&draws);
assert_eq!(batcher.opaque().len(), 1);
assert_eq!(batcher.opaque()[0].instances, 0..1);
assert_eq!(batcher.cutout().len(), 1, "the two cutouts collapse");
assert_eq!(batcher.cutout()[0].instances, 1..3);
}
#[test]
fn every_map_takes_the_one_record_of_a_faced_draw_and_a_blended_one_casts_by_its_alpha() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::ZERO));
push(&mut draws, LUMP.at(Vec3::X).billboard());
push(&mut draws, LUMP.at(Vec3::Y).upright());
push(&mut draws, LUMP.at(Vec3::Z));
push(&mut draws, LUMP.at(Vec3::NEG_X).material(LEAF));
push(&mut draws, LUMP.at(Vec3::NEG_Y).material(GLASS));
let batcher = batched(&draws);
let instances = |batches: &[Batch]| {
batches
.iter()
.map(|batch| batch.instances.clone())
.collect::<Vec<_>>()
};
assert_eq!(
instances(batcher.opaque()),
vec![0..2, 2..4],
"all four are drawn, the turned ones apart from the placed ones"
);
assert_eq!(
instances(batcher.casters().plain()),
vec![0..2],
"the two the camera never turned cast by depth alone"
);
assert_eq!(
instances(batcher.casters().sampled()),
vec![2..4, 4..6],
"the turned ones cast the plane the camera stood them in, the \
cutout one casts through its own texture, and the blended one by \
the alpha it covers with"
);
assert_eq!(
batcher.instances().len(),
6,
"one record per draw, and no draw recorded twice"
);
}
#[test]
fn a_faced_draw_leaves_one_record_however_many_lights_a_frame_flags() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, Quad.at(Vec3::ZERO).billboard());
let batcher = batched(&draws);
assert_eq!(batcher.instances().len(), 1, "the camera's own");
assert_eq!(batcher.casters().sampled()[0].instances, 0..1);
assert!(batcher.casters().plain().is_empty());
}
#[test]
fn a_frame_of_plain_draws_casts_every_batch_it_draws() {
let mut draws = casting();
push(&mut draws, LUMP.at(Vec3::ZERO));
push(&mut draws, LUMP.at(Vec3::X));
let batcher = batched(&draws);
assert_eq!(batcher.casters().plain().len(), 1);
assert_eq!(batcher.casters().plain()[0].instances, 0..2);
}
#[test]
fn interleaved_draws_collapse_per_mesh() {
let mut draws = DrawList::<Lumps>::new();
push(&mut draws, Sphere { subdivisions: 1 }.at(Vec3::ZERO));
push(&mut draws, LUMP.at(Vec3::X));
push(&mut draws, Sphere { subdivisions: 1 }.at(Vec3::Y));
assert_eq!(batch(&draws), (2, 3));
}
#[test]
fn distinct_meshes_split_batches() {
let mut draws = DrawList::<Lumps>::new();
push(&mut draws, LUMP.at(Vec3::ZERO));
push(&mut draws, Sphere { subdivisions: 1 }.at(Vec3::X));
assert_eq!(batch(&draws), (2, 2));
}
#[test]
fn a_frame_is_drawn_as_it_left_the_camera_until_it_asks_otherwise() {
let mut draws = DrawList::<Lumps>::new();
assert_eq!(draws.exposure(), DEFAULT_EXPOSURE);
assert_eq!(draws.bloom(), DEFAULT_BLOOM);
draws.set_exposure(1.4);
draws.set_bloom(0.15);
assert_eq!((draws.exposure(), draws.bloom()), (1.4, 0.15));
draws.clear();
assert_eq!((draws.exposure(), draws.bloom()), (1.0, 0.0));
}
#[test]
fn exposure_and_bloom_are_held_inside_what_the_chain_takes() {
let mut draws = DrawList::<Lumps>::new();
draws.set_exposure(-2.0);
draws.set_bloom(4.0);
assert_eq!((draws.exposure(), draws.bloom()), (0.0, 1.0));
draws.set_bloom(-1.0);
assert_eq!(draws.bloom(), 0.0);
draws.set_exposure(64.0);
assert_eq!(draws.exposure(), 64.0, "there is no upper end to exposure");
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct Pane;
impl Catalog for Pane {
fn catalog() -> Vec<Self> {
vec![Self]
}
}
impl Mesh<Half> for Pane {
fn build(&self, assets: &Assets) -> MeshData<Half> {
let square = Quad.build(assets);
let half = square.indices().len() as u32 / 2;
MeshData::in_parts(
square.vertices().to_vec(),
square.indices().to_vec(),
|part| match part {
Half::Painted => Slot::new(half, Material::default()),
Half::Sampled => Slot::new(half, Material::default())
.textured(TextureData::rgba8(UVec2::ONE, vec![0; 4])),
},
)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Half {
Painted,
Sampled,
}
impl Part for Half {
fn from_name(_name: &str) -> Option<Self> {
None
}
fn all() -> Vec<Self> {
vec![Self::Painted, Self::Sampled]
}
fn index(&self) -> u32 {
*self as u32
}
}
#[test]
fn a_slot_is_routed_by_its_own_tint_and_by_nothing_else() {
let mut draws = DrawList::<Lumps>::new();
push(
&mut draws,
Pane.at(Vec3::ZERO).material_of(Half::Painted, GLASS),
);
let batcher = batched(&draws);
assert_eq!(
(batcher.opaque().len(), batcher.translucent().len()),
(1, 1),
"the repainted half blends and the other stays opaque"
);
assert_eq!(
batcher.opaque()[0].part,
1,
"a texture with no opacity in it is drawn as opaque as any other"
);
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
struct Gapped;
impl Catalog for Gapped {
fn catalog() -> Vec<Self> {
vec![Self]
}
}
impl Mesh<Third> for Gapped {
fn build(&self, assets: &Assets) -> MeshData<Third> {
let square = Quad.build(assets);
let half = square.indices().len() as u32 / 2;
MeshData::in_parts(
square.vertices().to_vec(),
square.indices().to_vec(),
|part| match part {
Third::Gap => Slot::new(0, Material::default()),
Third::Front | Third::Back => Slot::new(half, Material::default()),
},
)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum Third {
Front,
Gap,
Back,
}
impl Part for Third {
fn from_name(_name: &str) -> Option<Self> {
None
}
fn all() -> Vec<Self> {
vec![Self::Front, Self::Gap, Self::Back]
}
fn index(&self) -> u32 {
*self as u32
}
}
#[test]
fn a_slot_that_claims_no_indices_costs_no_draw_and_is_still_named() {
let mut draws = casting();
push(
&mut draws,
Gapped.at(Vec3::ZERO).material_of(Third::Gap, GLASS),
);
let batcher = batched(&draws);
assert_eq!(
batcher
.opaque()
.iter()
.map(|batch| batch.part)
.collect::<Vec<_>>(),
vec![0, 2],
"only the halves that hold indices are drawn"
);
assert!(
batcher.translucent().is_empty() && batcher.casters().batches().count() == 2,
"the empty slot takes an override without drawing or casting"
);
}
#[test]
fn translucent_draws_are_sorted_back_to_front_behind_the_opaque_ones() {
let mut draws = panes(&[1.0, 9.0, 5.0]);
push(&mut draws, LUMP.at(Vec3::NEG_Z * 3.0));
let batcher = batched(&draws);
let recorded = |depth: f32, material, pass| {
GpuInstance::new(
placement(Vec3::NEG_Z * depth),
material,
pass,
Frame::default(),
Relief::of(None, false),
WorldPlane::NONE,
)
};
assert_eq!(
batcher.instances(),
vec![
recorded(3.0, Material::default(), DrawPass::Opaque),
recorded(9.0, GLASS, DrawPass::Translucent),
recorded(5.0, GLASS, DrawPass::Translucent),
recorded(1.0, GLASS, DrawPass::Translucent),
]
);
assert_eq!(batcher.opaque()[0].instances, 0..1);
assert_eq!(batcher.translucent()[0].instances, 1..4);
}
#[test]
fn translucent_draws_of_one_mesh_and_slot_collapse_once_they_are_sorted() {
let mut apart = panes(&[9.0, 1.0]);
push(
&mut apart,
Sphere { subdivisions: 1 }
.at(Vec3::NEG_Z * 5.0)
.material(GLASS),
);
let mut together = panes(&[9.0, 8.0]);
push(
&mut together,
Sphere { subdivisions: 1 }.at(Vec3::NEG_Z).material(GLASS),
);
assert_eq!(
batched(&apart).translucent().len(),
3,
"another mesh sorts between them, so each draws on its own"
);
let joined = batched(&together);
assert_eq!(joined.translucent().len(), 2);
assert_eq!(joined.translucent()[0].instances, 0..2);
}
#[test]
fn an_additive_draw_is_routed_ahead_of_what_its_alpha_and_its_cutout_ask_for() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::ZERO).material(SPARK));
push(
&mut draws,
LUMP.at(Vec3::X)
.material(Material::color(Color::rgba(1.0, 1.0, 1.0, 0.5)).additive()),
);
push(&mut draws, LUMP.at(Vec3::Y).material(SPARK.cutout()));
let batcher = batched(&draws);
assert_eq!(
batcher.additive().len(),
1,
"and the three collapse into one"
);
assert_eq!(batcher.additive()[0].instances, 0..3);
assert!(
batcher.opaque().is_empty()
&& batcher.cutout().is_empty()
&& batcher.translucent().is_empty(),
"an alpha under one and a cutout leave the routing to the addition"
);
assert!(batcher.casters().plain().is_empty(), "and none of it casts");
}
#[test]
fn additive_draws_are_batched_after_the_sorted_ones_as_they_were_submitted() {
let mut draws = panes(&[1.0, 9.0]);
push(&mut draws, LUMP.at(Vec3::NEG_Z * 5.0));
push(&mut draws, LUMP.at(Vec3::NEG_Z).material(SPARK));
push(
&mut draws,
Sphere { subdivisions: 1 }
.at(Vec3::NEG_Z * 7.0)
.material(SPARK),
);
let batcher = batched(&draws);
let glow = |depth: f32| {
GpuInstance::new(
placement(Vec3::NEG_Z * depth),
SPARK,
DrawPass::Additive,
Frame::default(),
Relief::of(None, false),
WorldPlane::NONE,
)
};
assert_eq!(batcher.opaque()[0].instances, 0..1);
assert_eq!(batcher.translucent()[0].instances, 1..3);
assert_eq!(
batcher
.additive()
.iter()
.map(|batch| batch.instances.clone())
.collect::<Vec<_>>(),
vec![3..4, 4..5]
);
assert_eq!(
(batcher.instances()[3], batcher.instances()[4]),
(glow(1.0), glow(7.0)),
"the nearer of the two keeps the place it was submitted in"
);
}
#[test]
fn a_faded_draw_blends_and_casts_through_the_pass_that_samples_its_alpha() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::ZERO).faded(0.5));
push(&mut draws, LUMP.at(Vec3::X).faded(0.0));
let batcher = batched(&draws);
assert_eq!(
batcher
.translucent()
.iter()
.map(|batch| batch.instances.clone())
.collect::<Vec<_>>(),
vec![0..2],
"both blend, the wholly faded one covering nothing"
);
assert_eq!(
batcher.casters().sampled()[0].instances,
0..1,
"and the one that still covers something casts, by its own alpha"
);
assert!(batcher.opaque().is_empty() && batcher.casters().plain().is_empty());
}
#[test]
fn a_fade_scales_the_alpha_of_every_slot_a_draw_resolved_and_a_whole_one_leaves_it() {
let half = Material::lit(Color::WHITE.with_alpha(0.5));
let quarter = Material::color(Color::WHITE.with_alpha(0.25));
let mut faded = DrawList::<Lumps>::new();
push(
&mut faded,
Pane.at(Vec3::ZERO)
.material_of(Half::Painted, GLASS)
.faded(0.5),
);
let mut painted = DrawList::<Lumps>::new();
push(
&mut painted,
Pane.at(Vec3::ZERO)
.material_of(Half::Painted, quarter)
.material_of(Half::Sampled, half),
);
assert_eq!(
batched(&faded).instances(),
batched(&painted).instances(),
"an overridden slot and an authored one fade alike"
);
let mut whole = DrawList::<Lumps>::new();
push(&mut whole, Pane.at(Vec3::ZERO).faded(1.0));
let mut plain = DrawList::<Lumps>::new();
push(&mut plain, Pane.at(Vec3::ZERO));
assert_eq!(
batched(&whole).instances(),
batched(&plain).instances(),
"and a fade of one records what the draw would have without it"
);
}
#[test]
fn a_fade_moves_a_cutout_draw_to_the_blended_pass_and_leaves_an_additive_one_adding() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::ZERO).material(LEAF).faded(0.5));
push(&mut draws, LUMP.at(Vec3::X).material(SPARK).faded(0.5));
let batcher = batched(&draws);
assert_eq!(batcher.translucent()[0].instances, 0..1);
assert_eq!(batcher.additive()[0].instances, 1..2);
assert_eq!(
batcher.casters().sampled()[0].instances,
0..1,
"the faded cutout casts its shape, the addition still nothing"
);
assert_eq!(batcher.casters().sampled().len(), 1);
assert!(batcher.cutout().is_empty());
}
#[test]
fn a_translucent_draw_placed_nowhere_joins_no_batch() {
let mut draws = panes(&[1.0, 2.0]);
push(&mut draws, LUMP.at(Vec3::NAN).material(GLASS));
let batcher = batched(&draws);
assert_eq!(batcher.instances().len(), 2);
assert_eq!(batcher.translucent()[0].instances, 0..2);
}
#[test]
fn caster_batches_are_built_only_for_a_frame_whose_lights_flag_a_shadow() {
let frame = |light: Light| {
let mut draws = DrawList::<Lumps>::new();
draws.set_camera(LOOKING);
draws.push_light(light);
push(&mut draws, LUMP.at(Vec3::ZERO));
push(&mut draws, LUMP.at(Vec3::Z * 40.0));
batched(&draws)
};
let sun = Light::directional(Vec3::NEG_Y, Color::WHITE);
let unflagged = frame(sun);
assert_eq!(
unflagged.casters().batches().count(),
0,
"no map of this frame reads them"
);
assert_eq!(
unflagged.instances().len(),
1,
"and the draw the camera left out is recorded for nothing"
);
let flagged = frame(sun.shadow());
assert_eq!(flagged.casters().plain()[0].instances, 0..2);
assert_eq!(flagged.instances().len(), 2);
}
#[test]
fn a_draw_the_camera_never_reaches_is_batched_by_no_pass_and_casts_all_the_same() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::ZERO));
push(&mut draws, LUMP.at(Vec3::Z * 40.0));
let batcher = batched(&draws);
assert_eq!(batcher.opaque().len(), 1);
assert_eq!(
batcher.opaque()[0].instances,
0..1,
"the one behind the camera is drawn by nothing"
);
assert_eq!(batcher.instances().len(), 2);
assert_eq!(
batcher.casters().plain()[0].instances,
0..2,
"and a light's map still takes it"
);
}
#[test]
fn a_draw_the_camera_never_reaches_leaves_the_batch_of_the_ones_it_does_whole() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::ZERO));
push(&mut draws, LUMP.at(Vec3::Z * 40.0));
push(&mut draws, LUMP.at(Vec3::X));
let batcher = batched(&draws);
assert_eq!(batcher.opaque().len(), 1, "the two it draws collapse");
assert_eq!(batcher.opaque()[0].instances, 0..2);
assert_eq!(
batcher
.casters()
.plain()
.iter()
.map(|batch| batch.instances.clone())
.collect::<Vec<_>>(),
vec![0..3],
"and the one it misses casts behind them"
);
}
fn ranges(submitted: &[(Sphere, Vec3)]) -> (Vec<Range<u32>>, Vec<Range<u32>>) {
let mut draws = casting();
draws.set_camera(LOOKING);
for &(mesh, at) in submitted {
push(&mut draws, mesh.at(at));
}
let batcher = batched(&draws);
let instances = |batches: &[Batch]| {
batches
.iter()
.map(|batch| batch.instances.clone())
.collect::<Vec<_>>()
};
(
instances(batcher.opaque()),
instances(batcher.casters().plain()),
)
}
#[test]
fn draws_the_camera_never_reaches_split_no_batch_whatever_order_their_keys_came_in() {
const LARGE: Sphere = Sphere { subdivisions: 1 };
const AWAY: Vec3 = Vec3::new(0.0, 0.0, 40.0);
let expected = (vec![0..2, 3..5], vec![0..3, 3..6]);
assert_eq!(
ranges(&[
(LUMP, Vec3::ZERO),
(LUMP, AWAY),
(LUMP, Vec3::X),
(LARGE, AWAY),
(LARGE, Vec3::Y),
(LARGE, Vec3::NEG_X),
]),
expected,
"each mesh draws once, with the two it misses behind what it draws"
);
assert_eq!(
ranges(&[
(LUMP, Vec3::ZERO),
(LARGE, AWAY),
(LUMP, AWAY),
(LARGE, Vec3::Y),
(LUMP, Vec3::X),
(LARGE, Vec3::NEG_X),
]),
expected,
"and the same submission interleaved leaves the same batches"
);
}
#[test]
fn a_transparent_draw_the_camera_never_reaches_still_casts_and_is_drawn_by_nothing() {
let mut draws = casting();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::Z * 40.0).material(GLASS));
push(&mut draws, LUMP.at(Vec3::Z * 40.0).material(SPARK));
push(
&mut draws,
LUMP.at(Vec3::Z * 40.0).billboard().material(GLASS),
);
let batcher = batched(&draws);
assert!(batcher.translucent().is_empty() && batcher.additive().is_empty());
assert_eq!(
batcher.casters().sampled().len(),
2,
"the two blended ones cast, the placed one apart from the turned one"
);
assert!(batcher.casters().plain().is_empty());
assert_eq!(
batcher.instances().len(),
2,
"and the added one, casting nothing, is held by nothing"
);
}
#[test]
fn a_draw_lying_across_the_edge_of_what_the_camera_sees_is_drawn() {
let mut draws = DrawList::<Lumps>::new();
draws.set_camera(LOOKING);
push(&mut draws, LUMP.at(Vec3::Z * 10.3));
assert_eq!(
batch(&draws),
(1, 1),
"a sphere over the near plane is kept"
);
}
#[test]
fn a_frame_hands_a_style_the_values_it_last_wrote_and_nothing_after_it_is_drawn() {
let mut draws = DrawList::<Lumps>::new();
assert_eq!(draws.surface_style_values(SurfaceStyleId(0)), None);
draws.set_surface_style(SurfaceStyleId(0), |into| {
into.extend_from_slice(&[1, 2, 3, 4])
});
draws.set_surface_style(SurfaceStyleId(0), |into| into.extend_from_slice(&[5, 6]));
assert_eq!(
draws.surface_style_values(SurfaceStyleId(0)),
Some(&[5, 6][..])
);
assert_eq!(draws.surface_style_values(SurfaceStyleId(1)), None);
draws.clear();
assert_eq!(draws.surface_style_values(SurfaceStyleId(0)), None);
}
#[test]
fn faced_draws_standing_in_one_plane_take_their_depth_from_that_one_plane() {
let level = level_of(Vec3::NEG_Z).expect("a level view stands its draws in a plane");
let standing = |x, z| WorldPlane::upright(level, Vec3::new(x, 0.0, z));
assert_eq!(
standing(-1.0, 4.0),
standing(3.0, 4.0),
"two draws the view stood at one distance down it name one plane"
);
assert_ne!(
standing(1.0, 4.0),
standing(8.0, 9.0),
"and one standing further down it names another"
);
assert_eq!(
level_of(Vec3::NEG_Y),
None,
"a view with no level direction to look along stands them in none"
);
}
#[test]
fn a_plane_hands_its_depth_to_every_faced_draw_of_it_whatever_pass_it_lands_in() {
let mut draws = DrawList::<Lumps>::new();
draws.set_camera(LOOKING);
for material in [Material::default(), LEAF, GLASS, SPARK] {
push(&mut draws, LUMP.at(Vec3::ZERO).upright().material(material));
}
let level = level_of(Vec3::NEG_Z).expect("a level view stands its draws in a plane");
let held = |material, pass| {
GpuInstance::new(
LUMP.at::<()>(Vec3::ZERO)
.upright()
.record()
.placement(LOOKING.view()),
material,
pass,
Frame::default(),
Relief::of(None, false),
WorldPlane::upright(level, Vec3::ZERO),
)
};
assert_eq!(
batched(&draws).instances(),
vec![
held(Material::default(), DrawPass::Opaque),
held(LEAF, DrawPass::Cutout),
held(GLASS, DrawPass::Translucent),
held(SPARK, DrawPass::Additive),
],
"a draw that writes no depth takes that depth too, so it clears \
what the draws of its plane wrote"
);
}
#[test]
fn draws_their_own_transform_lays_in_one_world_plane_take_their_depth_from_it() {
let ground = Transform::from_scale(Vec3::splat(4.0));
let lifted = Transform::from(Vec3::new(0.0, 0.015, 0.0));
let laid = [
(ground, Material::default(), DrawPass::Opaque),
(Vec3::X.into(), LEAF, DrawPass::Cutout),
(Vec3::NEG_X.into(), GLASS, DrawPass::Translucent),
(Vec3::Z.into(), SPARK, DrawPass::Additive),
(lifted, Material::default(), DrawPass::Opaque),
];
let mut draws = DrawList::<Lumps>::new();
draws.set_camera(LOOKING);
for (at, material, _) in laid {
push(&mut draws, Plane.at(at).material(material));
}
let square = Plane
.build(&Assets::default())
.erased()
.expect("a square is built whole")
.plane()
.expect("a square lies in a plane");
let lying = |transform| WorldPlane::of(square.placed(transform));
let held = |at: usize, plane| {
let (transform, material, pass) = laid[at];
GpuInstance::new(
Plane.at::<()>(transform).record().placement(LOOKING.view()),
material,
pass,
Frame::default(),
Relief::of(None, false),
plane,
)
};
assert_ne!(
lying(ground),
lying(lifted),
"a square lifted off the ground lies in a plane of its own"
);
assert_eq!(
batched(&draws).instances(),
vec![
held(0, lying(ground)),
held(4, lying(lifted)),
held(1, lying(ground)),
held(2, lying(ground)),
held(3, lying(ground)),
],
"every square laid in the ground's own plane takes that plane, \
whatever pass draws it and whatever its own transform is"
);
}
#[test]
fn lights_past_the_cap_are_ignored() {
let mut draws = DrawList::<Lumps>::new();
for _ in 0..MAX_LIGHTS + 8 {
draws.push_light(Light::directional(Vec3::NEG_Y, Color::WHITE));
}
assert_eq!(draws.lights().len(), MAX_LIGHTS);
}
}