use glam::{Mat4, Vec3, Vec4};
use super::{
Assets3D, Camera3D, Lighting, LodGroup, LodPick, LodRenderer, LodSettings, LodShadow, Model, ReceivesDecals,
ShadowMesh, Skeleton, SkinFrame, SkinOf, TERRAIN_LODS, TerrainTile, Transform3D, linear,
};
use crate::World;
pub(crate) const SHADOW_MAP_SIZE: u32 = 2048;
pub(crate) const MIN_SHADOW_RESOLUTION: u32 = 256;
pub(crate) const MAX_SHADOW_RESOLUTION: u32 = 4096;
pub(crate) const MAX_CASCADES: usize = 4;
pub(crate) const MAX_SHADOW_VIEWS: usize = MAX_CASCADES + super::lights::MAX_LIGHT_SHADOW_VIEWS;
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
pub(crate) struct Instance3D {
pub model: [[f32; 4]; 4],
pub color: [f32; 4],
}
impl Instance3D {
pub(crate) fn with_fade(mut self, fade: f32) -> Self {
self.model[1][3] = fade;
self
}
pub(crate) fn fade(&self) -> f32 {
self.model[1][3]
}
}
pub(crate) const FADED: u32 = 1 << 31;
type Entry = (u32, Instance3D, bool, u32);
#[derive(Default)]
pub(crate) struct Frame3D {
pub instances: Vec<Instance3D>,
pub probes: [super::probe::ProbeUniform; 4],
pub probe_uploads: Vec<(u32, std::sync::Arc<Vec<u16>>)>,
pub probe_bakes: Vec<(u32, [u32; 6])>,
pub particles: super::particles::ParticleFrame,
pub lights: super::lights::LightGrid,
pub decals: super::projected::DecalGrid,
pub decal_array: Option<u32>,
pub runs: Vec<(u32, u32, u32)>,
pub shadow_runs: [Vec<(u32, u32, u32)>; MAX_SHADOW_VIEWS],
pub view_proj: Mat4,
pub cascades: usize,
pub shadow_size: u32,
pub light_shadow_size: u32,
pub light_atlas: u32,
pub light_budget: usize,
pub size_limit: u32,
pub light_views: usize,
pub sun_view_proj: [Mat4; MAX_CASCADES],
pub cascade_texel: [f32; MAX_CASCADES],
pub redrawn: u32,
cascade_cache: [Option<CachedCascade>; MAX_CASCADES],
frame_number: u32,
pub effects: Vec<(u32, u32)>,
pub computes: Vec<(u32, u32, u32)>,
pub visible: usize,
pub occlusion: Option<super::occlusion::OcclusionGrid>,
pub occluded: usize,
pub views: Vec<ViewFrame>,
pub sky_light: Option<(Lighting, super::Sky, super::SkyLight)>,
pub skins: SkinFrame,
pub lod: LodSettings,
pub motion: super::motion::Motion,
pub gi: Option<Box<super::gi::GiFrame>>,
scratch: Vec<Entry>,
counts: Vec<u32>,
flags: Vec<u32>,
lod_picks: Vec<(LodPick, u32)>,
}
#[derive(Default)]
pub(crate) struct ViewFrame {
pub target: u32,
pub size: (u32, u32),
pub camera: Vec3,
pub view_proj: Mat4,
pub frame: Frame3D,
}
impl Frame3D {
pub fn collect(
&mut self,
world: &mut World,
assets: &Assets3D,
skins: &SkinFrame,
camera: &Camera3D,
lighting: &Lighting,
) {
self.view_proj = camera.view_proj();
self.particles.collect(world, camera);
self.shadow_size = lighting.shadow_size(self.size_limit);
self.light_shadow_size = lighting.light_shadow_size(self.size_limit);
self.light_atlas = lighting.light_shadow_atlas_size(self.size_limit);
self.light_budget = if lighting.shadows {
lighting.light_shadow_budget()
} else {
0
};
self.lights.collect(
world,
camera,
self.light_budget,
self.light_shadow_size,
self.light_atlas,
);
self.decals.records.clear();
self.decals.lists.clear();
self.decal_array = assets.decal_array.map(|a| a.id);
if assets.decals_supported {
self.decals.collect(
world,
camera,
self.lights.depth,
assets.decal_array.map_or(0, |a| a.layers),
self.lights.lists.len(),
self.lights.lights.len(),
);
}
self.light_views = self.lights.shadow_views.len();
self.cascades = if lighting.shadows && lighting.shadow_distance > 0.0 {
(lighting.shadow_cascades as usize).clamp(1, MAX_CASCADES)
} else {
0
};
let splits = cascade_splits(camera.near, lighting.shadow_distance, self.cascades);
let refresh = lighting.shadow_refresh.clamp(1, MAX_SHADOW_REFRESH);
let first_far = if refresh > 1 {
far_cascades(self.cascades)
} else {
self.cascades
};
self.frame_number = self.frame_number.wrapping_add(1);
self.redrawn = 0;
let mut sun_planes = [[Vec4::ZERO; 5]; MAX_CASCADES];
for i in 0..self.cascades {
let near = if i == 0 { camera.near } else { splits[i - 1] };
if i >= first_far {
let (center, radius) = slice_sphere(camera, near, splits[i]);
let turn = (self.frame_number + i as u32).is_multiple_of(refresh);
let kept = self.cascade_cache[i].filter(|c| {
!turn
&& c.size == self.shadow_size
&& c.cascades == self.cascades
&& c.sun == lighting.sun_direction
&& center.distance(c.center) + radius <= c.reach
});
if let Some(c) = kept {
self.sun_view_proj[i] = c.view_proj;
self.cascade_texel[i] = c.texel;
sun_planes[i] = [Vec4::new(0.0, 0.0, 0.0, -f32::MAX); 5];
continue;
}
let (m, texel, reach) = sun_box(
camera,
lighting.sun_direction,
near,
splits[i],
self.shadow_size,
1.0 + CACHE_MARGIN,
);
self.cascade_cache[i] = Some(CachedCascade {
view_proj: m,
texel,
center,
reach,
sun: lighting.sun_direction,
size: self.shadow_size,
cascades: self.cascades,
});
self.sun_view_proj[i] = m;
self.cascade_texel[i] = texel;
} else {
self.cascade_cache[i] = None;
let (m, texel) = sun_view_proj(camera, lighting.sun_direction, near, splits[i], self.shadow_size);
self.sun_view_proj[i] = m;
self.cascade_texel[i] = texel;
}
self.redrawn |= 1 << i;
let [l, r, b, t, _, f] = frustum_planes(self.sun_view_proj[i]);
sun_planes[i] = [l, r, b, t, f];
}
for cache in &mut self.cascade_cache[self.cascades..] {
*cache = None;
}
let sun_planes = &sun_planes[..self.cascades];
let mut light_planes = [[Vec4::ZERO; 6]; super::lights::MAX_LIGHT_SHADOW_VIEWS];
for (planes, m) in light_planes.iter_mut().zip(&self.lights.shadow_views) {
*planes = frustum_planes(*m);
}
let light_planes = &light_planes[..self.light_views];
self.instances.clear();
self.runs.clear();
self.motion.begin();
for runs in &mut self.shadow_runs {
runs.clear();
}
self.scratch.clear();
let planes = frustum_planes(self.view_proj);
let receivers = frustum_planes(
Camera3D {
far: lighting
.shadow_distance
.clamp(camera.near * 2.0, camera.far.max(camera.near * 2.0)),
..*camera
}
.view_proj(),
);
let sunlight = lighting.sun_direction.try_normalize().unwrap_or(Vec3::NEG_Y);
let sweep = lighting.shadow_distance * 2.0;
let texels = self.cascade_texel;
let scratch = &mut self.scratch;
let motion = &mut self.motion;
let occlusion = self.occlusion.as_ref().filter(|_| camera.occlusion_culling);
let occluded = std::sync::atomic::AtomicUsize::new(0);
let classify_at = |matrix: Mat4, scale: f32, bounds: (Vec3, f32), shadow_bounds: Option<(Vec3, f32)>| -> u32 {
let (center, radius) = (matrix.transform_point3(bounds.0), bounds.1 * scale);
let mut seen = sphere_visible(&planes, center, radius);
if seen
&& let Some(grid) = occlusion
&& grid.hides(center - radius, center + radius, camera.position)
{
occluded.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
seen = false;
}
let mut flags = u32::from(seen);
if let Some((center, radius)) = shadow_bounds {
let (world_center, world_radius) = (matrix.transform_point3(center), radius * scale);
let reach = world_center.distance(camera.position) + sweep;
if capsule_visible(&receivers, world_center, world_center + sunlight * reach, world_radius) {
for (i, p) in sun_planes.iter().enumerate() {
if world_radius * 2.0 >= texels[i] && sphere_visible(p, world_center, world_radius) {
flags |= 2 << i;
}
}
}
for (i, p) in light_planes.iter().enumerate() {
if sphere_visible(p, world_center, world_radius) {
flags |= 2 << (MAX_CASCADES + i);
}
}
}
flags
};
let classify = |t: &Transform3D, bounds: (Vec3, f32), shadow_bounds: Option<(Vec3, f32)>| -> u32 {
classify_at(t.matrix(), t.scale.abs().max_element(), bounds, shadow_bounds)
};
let model_meshes = |m: &Model, shadow: Option<&ShadowMesh>| {
let bounds = assets.live_bounds(m.mesh.id)?;
let caster = match shadow {
Some(ShadowMesh(caster)) => *caster,
None => Some(m.mesh),
};
let caster = caster.filter(|c| assets.casts_shadows(c.id));
let shadow_bounds = caster.and_then(|c| assets.live_bounds(c.id));
let mut meshes = [caster.map_or(m.mesh.id, |c| c.id); 1 + MAX_CASCADES];
meshes[0] = m.mesh.id;
Some((meshes, bounds, shadow_bounds))
};
let shadow_only = |shadow: &ShadowMesh| {
let mesh = shadow.0.filter(|c| assets.casts_shadows(c.id))?;
Some((mesh.id, assets.live_bounds(mesh.id)?))
};
let view_mesh = |meshes: &[u32; 1 + MAX_CASCADES], view: usize| {
if view < MAX_CASCADES {
meshes[view + 1]
} else {
meshes[1]
}
};
let mut add_instance = |meshes: [u32; 1 + MAX_CASCADES], instance: Instance3D, flags: u32, key: u64| {
let (seen, casts) = (flags & 1 != 0, flags >> 1);
let mut done = 0u32; let camera_mesh = meshes[0];
let in_cascades = |mesh: u32| {
(0..MAX_SHADOW_VIEWS).fold(0u32, |m, i| {
if casts & (1 << i) != 0 && view_mesh(&meshes, i) == mesh {
m | 1 << i
} else {
m
}
})
};
if seen {
motion.note(key, camera_mesh, &instance);
let mask = in_cascades(camera_mesh);
done |= mask;
scratch.push((camera_mesh, instance, true, mask));
}
while casts & !done != 0 {
let first = (casts & !done).trailing_zeros() as usize;
let mesh = view_mesh(&meshes, first);
let mask = in_cascades(mesh);
done |= mask;
scratch.push((mesh, instance, false, mask));
}
};
let instance = |t: &Transform3D, color: [f32; 4], receives: f32| {
let mut model = t.matrix().to_cols_array_2d();
model[0][3] = receives;
Instance3D { model, color }
};
let all_decals = super::projected::receiver_mask(None);
let posed = |m: &Model, s: Option<&Skeleton>| match assets.skin(m.mesh.id) {
Some(_) => s.is_some(),
None => assets.morph(m.mesh.id).is_some(),
};
let flags = &mut self.flags;
world
.query::<(
&Transform3D,
&Model,
Option<&ShadowMesh>,
Option<&Skeleton>,
Option<&ReceivesDecals>,
)>()
.without::<SkinOf>()
.par_collect_into(flags, |(t, m, shadow, skeleton, _)| {
if posed(m, skeleton) {
return 0;
}
model_meshes(m, shadow).map_or(0, |(_, bounds, shadow_bounds)| classify(t, bounds, shadow_bounds))
});
let mut at = 0;
world
.query::<(
crate::Entity,
&Transform3D,
&Model,
Option<&ShadowMesh>,
Option<&Skeleton>,
Option<&ReceivesDecals>,
)>()
.without::<SkinOf>()
.for_each(|(e, t, m, shadow, _, receives)| {
let f = flags[at];
at += 1;
if f != 0
&& let Some((meshes, _, _)) = model_meshes(m, shadow)
{
add_instance(
meshes,
instance(t, linear(m.color), super::projected::receiver_mask(receives)),
f,
e.to_bits(),
);
}
});
world
.query::<(&Transform3D, &ShadowMesh)>()
.without::<Model>()
.par_collect_into(flags, |(t, shadow)| {
shadow_only(shadow).map_or(0, |(_, bounds)| classify(t, bounds, Some(bounds)) & !1)
});
let mut at = 0;
world
.query::<(&Transform3D, &ShadowMesh)>()
.without::<Model>()
.for_each(|(t, shadow)| {
let f = flags[at];
at += 1;
if f != 0
&& let Some((mesh, _)) = shadow_only(shadow)
{
add_instance(
[mesh; 1 + MAX_CASCADES],
instance(t, [1.0; 4], all_decals),
f,
super::motion::NO_KEY,
);
}
});
let real_meshes = assets.bounds.len() as u32;
for (k, d) in skins.draws.iter().enumerate() {
if d.bounds.1 < 0.0 {
continue;
}
let caster = assets.casts_shadows(d.mesh).then_some(d.bounds);
let f = classify_at(d.matrix, d.scale, d.bounds, caster);
if f != 0 {
add_instance([real_meshes + k as u32; 1 + MAX_CASCADES], d.instance, f, d.entity);
}
}
let lod = self.lod;
let lod_flags = |t: &Transform3D, r: &LodRenderer, drawn: bool, casts: bool| -> Option<(Mat4, u32)> {
let bounds = assets.live_bounds(r.mesh.id)?;
let matrix = t.matrix() * r.offset.matrix();
let scale = t.scale.abs().max_element() * r.offset.scale.abs().max_element();
let shadow = (casts && assets.casts_shadows(r.mesh.id)).then_some(bounds);
let seen = classify_at(matrix, scale, bounds, shadow);
Some((matrix, if drawn { seen } else { seen & !1 }))
};
let picks = &mut self.lod_picks;
world
.query::<(&Transform3D, &LodGroup)>()
.par_collect_into(picks, |(t, g)| {
let pick = g.pick_for(t, camera, &lod);
let mut seen = 0;
lod_draws(g, pick, |r, drawn, casts, _| {
seen |= lod_flags(t, r, drawn, casts).map_or(0, |(_, f)| f);
});
(pick, seen)
});
let mut at = 0;
world
.query::<(&Transform3D, &LodGroup, Option<&ReceivesDecals>)>()
.for_each(|(t, g, decals)| {
let (pick, seen) = picks[at];
at += 1;
if seen != 0 {
let receives = super::projected::receiver_mask(decals);
lod_draws(g, pick, |r, drawn, casts, fade| {
if let Some((matrix, f)) = lod_flags(t, r, drawn, casts)
&& f != 0
{
let mut model = matrix.to_cols_array_2d();
model[0][3] = receives;
let instance = Instance3D {
model,
color: linear(r.color),
};
add_instance(
[r.mesh.id; 1 + MAX_CASCADES],
instance.with_fade(fade),
f,
super::motion::NO_KEY,
);
}
});
}
});
world
.query::<(crate::Entity, &Transform3D, &TerrainTile, Option<&ShadowMesh>)>()
.for_each(|(e, t, tile, shadow)| {
let Some((center, radius)) = assets.live_bounds(tile.lods[0].id) else {
return;
};
let scale = t.scale.abs().max_element();
let away = t.matrix().transform_point3(center).distance(camera.position) - radius * scale;
let level = tile.lod(away.max(0.0));
let meshes: [u32; 1 + MAX_CASCADES] = std::array::from_fn(|i| {
tile.lods[if i == 0 {
level
} else {
level.max(i - 1).min(TERRAIN_LODS - 1)
}]
.id
});
if meshes.iter().all(|&m| assets.live_bounds(m).is_some()) {
let casts = !matches!(shadow, Some(ShadowMesh(None)));
let f = classify(t, (center, radius), casts.then_some((center, radius)));
if f != 0 {
add_instance(meshes, instance(t, [1.0; 4], all_decals), f, e.to_bits());
}
}
});
self.motion.finish();
let meshes = assets.bounds.len() + skins.draws.len();
let (counts, instances) = (&mut self.counts, &mut self.instances);
self.occluded = occluded.into_inner();
self.visible = sort_runs(&self.scratch, |e| e.2, meshes, counts, instances, &mut self.runs);
for i in shadow_views(self.cascades, self.light_views) {
let runs = &mut self.shadow_runs[i];
sort_runs(&self.scratch, |e| e.3 & (1 << i) != 0, meshes, counts, instances, runs);
}
self.collect_gi(world, assets, skins, camera, lighting);
}
}
pub(crate) fn shadow_views(cascades: usize, light_views: usize) -> impl Iterator<Item = usize> + Clone {
(0..cascades).chain(MAX_CASCADES..MAX_CASCADES + light_views)
}
fn sort_runs(
scratch: &[Entry],
keep: impl Fn(&Entry) -> bool,
meshes: usize,
counts: &mut Vec<u32>,
instances: &mut Vec<Instance3D>,
runs: &mut Vec<(u32, u32, u32)>,
) -> usize {
let bucket = |e: &Entry| e.0 as usize + if e.1.fade() != 0.0 { meshes } else { 0 };
counts.clear();
counts.resize(meshes * 2, 0);
for e in scratch.iter().filter(|e| keep(e)) {
counts[bucket(e)] += 1;
}
let base = instances.len() as u32;
let mut start = base;
for (b, count) in counts.iter_mut().enumerate() {
if *count > 0 {
let faded = if b >= meshes { FADED } else { 0 };
runs.push(((b % meshes) as u32 | faded, start, *count));
}
let n = *count;
*count = start; start += n;
}
let added = (start - base) as usize;
instances.resize(instances.len() + added, Instance3D::default());
for e in scratch.iter().filter(|e| keep(e)) {
let at = &mut counts[bucket(e)];
instances[*at as usize] = e.1;
*at += 1;
}
added
}
fn lod_draws(group: &LodGroup, pick: LodPick, mut f: impl FnMut(&LodRenderer, bool, bool, f32)) {
for (level, fade) in group.drawn(pick).into_iter().flatten() {
let own_shadow = matches!(level.shadow, LodShadow::Same);
for r in &level.renderers {
f(r, true, own_shadow, fade);
}
if let LodShadow::Casters(casters) = &level.shadow {
for r in casters {
f(r, false, true, fade);
}
}
}
}
pub(crate) fn cascade_splits(near: f32, distance: f32, count: usize) -> [f32; MAX_CASCADES] {
const LOG_SHARE: f32 = 0.75;
let near = near.max(1e-3);
let distance = distance.max(near * 2.0);
let mut splits = [distance; MAX_CASCADES];
for (i, split) in splits.iter_mut().enumerate().take(count.saturating_sub(1)) {
let k = (i + 1) as f32 / count as f32;
let even = near + (distance - near) * k;
let log = near * (distance / near).powf(k);
*split = even + (log - even) * LOG_SHARE;
}
splits
}
pub(crate) fn sun_view_proj(camera: &Camera3D, sun_direction: Vec3, near: f32, far: f32, size: u32) -> (Mat4, f32) {
let (m, texel, _) = sun_box(camera, sun_direction, near, far, size, 1.0);
(m, texel)
}
fn slice_sphere(camera: &Camera3D, near: f32, far: f32) -> (Vec3, f32) {
let far = far.max(camera.near * 2.0);
let slice = Camera3D {
near: near.clamp(camera.near, far * 0.5),
far,
..*camera
}
.view_proj()
.inverse();
let mut corners = [Vec3::ZERO; 8];
for (i, corner) in corners.iter_mut().enumerate() {
let ndc = Vec4::new(
if i & 1 == 0 { -1.0 } else { 1.0 },
if i & 2 == 0 { -1.0 } else { 1.0 },
if i & 4 == 0 { 0.0 } else { 1.0 },
1.0,
);
let p = slice * ndc;
*corner = p.truncate() / p.w;
}
let center = corners.iter().copied().sum::<Vec3>() / 8.0;
let radius = corners.iter().map(|c| c.distance(center)).fold(0.0, f32::max);
(center, radius)
}
fn sun_box(camera: &Camera3D, sun_direction: Vec3, near: f32, far: f32, size: u32, grow: f32) -> (Mat4, f32, f32) {
let light = sun_direction.try_normalize().unwrap_or(Vec3::NEG_Y);
let (center, radius) = slice_sphere(camera, near, far);
let radius = (radius * grow * 16.0).ceil() / 16.0;
let up = if light.y.abs() > 0.99 { Vec3::Z } else { Vec3::Y };
let rotation = glam::camera::rh::view::look_to_mat4(Vec3::ZERO, light, up);
let texel = 2.0 * radius / size.max(1) as f32;
let snapped = (rotation.transform_point3(center) / texel).round() * texel;
let center = rotation.inverse().transform_point3(snapped);
let back = radius * 4.0;
let view = glam::camera::rh::view::look_to_mat4(center - light * back, light, up);
let projection =
glam::camera::rh::proj::directx::orthographic(-radius, radius, -radius, radius, 0.0, back + radius);
(projection * view, texel, radius - texel * 2.0)
}
pub(crate) fn far_cascades(cascades: usize) -> usize {
cascades.div_ceil(2).max(1)
}
#[derive(Clone, Copy, Debug)]
struct CachedCascade {
view_proj: Mat4,
texel: f32,
center: Vec3,
reach: f32,
sun: Vec3,
size: u32,
cascades: usize,
}
const CACHE_MARGIN: f32 = 0.2;
pub(crate) const MAX_SHADOW_REFRESH: u32 = 8;
fn frustum_planes(m: Mat4) -> [Vec4; 6] {
let r = [m.row(0), m.row(1), m.row(2), m.row(3)];
let planes = [r[3] + r[0], r[3] - r[0], r[3] + r[1], r[3] - r[1], r[2], r[3] - r[2]];
planes.map(|p| p / p.truncate().length().max(f32::MIN_POSITIVE))
}
fn sphere_visible(planes: &[Vec4], center: Vec3, radius: f32) -> bool {
planes.iter().all(|p| p.truncate().dot(center) + p.w >= -radius)
}
fn capsule_visible(planes: &[Vec4], a: Vec3, b: Vec3, radius: f32) -> bool {
planes
.iter()
.all(|p| (p.truncate().dot(a) + p.w).max(p.truncate().dot(b) + p.w) >= -radius)
}
#[cfg(test)]
mod tests {
use super::super::TERRAIN_LODS;
use super::*;
use crate::{Color, Mesh};
use glam::Vec2;
fn camera() -> Camera3D {
let mut camera = Camera3D::new(Vec2::new(800.0, 800.0));
camera.position = Vec3::new(0.0, 0.0, 10.0);
camera.look_at(Vec3::ZERO);
camera.far = 100.0;
camera
}
#[test]
fn spheres_outside_the_view_are_culled() {
let planes = frustum_planes(camera().view_proj());
let seen = |at: Vec3| sphere_visible(&planes, at, 1.0);
assert!(seen(Vec3::ZERO));
assert!(!seen(Vec3::new(0.0, 0.0, 20.0)), "behind");
assert!(!seen(Vec3::new(0.0, 0.0, -200.0)), "past far");
assert!(!seen(Vec3::new(50.0, 0.0, 0.0)), "far right");
assert!(seen(Vec3::new(5.0, 0.0, 0.0)), "just in");
assert!(seen(Vec3::new(0.0, 0.0, 10.5)), "big enough to reach in");
}
#[test]
fn visible_models_are_grouped_by_mesh() {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 1.0); 3],
..Default::default()
};
let model = |id| Model::new(Mesh { id }).with_color(Color::WHITE);
for (i, mesh) in [2, 0, 2, 2, 0].into_iter().enumerate() {
world.spawn((Transform3D::at(Vec3::new(i as f32, 0.0, 0.0)), model(mesh)));
}
world.spawn((Transform3D::at(Vec3::new(0.0, 0.0, 50.0)), model(1))); let mut frame = Frame3D::default();
let lighting = Lighting {
shadows: false,
..Default::default()
};
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera(), &lighting);
assert_eq!(frame.runs, vec![(0, 0, 2), (2, 2, 3)]);
assert_eq!(frame.cascades, 0);
assert!(frame.shadow_runs.iter().all(Vec::is_empty));
let xs: Vec<f32> = frame.instances.iter().map(|i| i.model[3][0]).collect();
assert_eq!(xs, vec![1.0, 4.0, 0.0, 2.0, 3.0]);
}
#[test]
fn terrain_tiles_draw_coarser_further_away() {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 1.0); TERRAIN_LODS],
..Default::default()
};
let tile = TerrainTile {
lods: std::array::from_fn(|id| Mesh { id: id as u32 }),
lod_distance: 5.0,
};
world.spawn((Transform3D::at(Vec3::new(0.0, 0.0, 7.0)), tile)); world.spawn((Transform3D::at(Vec3::ZERO), tile)); world.spawn((Transform3D::at(Vec3::new(0.0, 0.0, -30.0)), tile)); let lighting = Lighting {
shadows: false,
..Default::default()
};
let mut frame = Frame3D::default();
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera(), &lighting);
assert_eq!(frame.runs, vec![(0, 0, 1), (1, 1, 1), (3, 2, 1)]);
}
#[test]
fn shadow_casters_include_models_outside_the_view() {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 1.0)],
..Default::default()
};
let mut camera = camera();
camera.position = Vec3::new(0.0, 5.0, 10.0);
camera.look_at(Vec3::ZERO);
let lighting = Lighting {
sun_direction: Vec3::new(0.0, -1.0, -0.2),
shadow_distance: 30.0,
shadow_cascades: 1,
..Default::default()
};
let model = Model::new(Mesh { id: 0 });
world.spawn((Transform3D::at(Vec3::ZERO), model)); world.spawn((Transform3D::at(Vec3::new(0.0, 40.0, 2.0)), model)); world.spawn((Transform3D::at(Vec3::new(500.0, 0.0, 0.0)), model)); let mut frame = Frame3D::default();
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera, &lighting);
assert_eq!(frame.visible, 1);
assert_eq!(frame.runs, vec![(0, 0, 1)]);
assert_eq!(frame.shadow_runs[0], vec![(0, 1, 2)]);
}
#[test]
fn shadowless_meshes_cast_no_shadow() {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 1.0); 2],
shadowless: vec![false, true],
..Default::default()
};
let mut camera = camera();
camera.position = Vec3::new(0.0, 5.0, 10.0);
camera.look_at(Vec3::ZERO);
let lighting = Lighting {
sun_direction: Vec3::new(0.0, -1.0, -0.2),
shadow_distance: 30.0,
shadow_cascades: 1,
..Default::default()
};
world.spawn((Transform3D::at(Vec3::ZERO), Model::new(Mesh { id: 0 })));
world.spawn((Transform3D::at(Vec3::X), Model::new(Mesh { id: 1 }))); world.spawn((Transform3D::at(Vec3::new(0.0, 40.0, 2.0)), Model::new(Mesh { id: 1 }))); let mut frame = Frame3D::default();
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera, &lighting);
assert_eq!(frame.visible, 2);
assert_eq!(frame.runs, vec![(0, 0, 1), (1, 1, 1)]);
assert_eq!(frame.shadow_runs[0], vec![(0, 2, 1)]);
}
#[test]
fn farther_shadow_cascades_draw_coarser_terrain() {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 500.0); TERRAIN_LODS],
..Default::default()
};
let tile = TerrainTile {
lods: std::array::from_fn(|id| Mesh { id: id as u32 }),
lod_distance: 5.0,
};
world.spawn((Transform3D::default(), tile));
let lighting = Lighting {
shadow_distance: 100.0,
shadow_cascades: 4,
..Default::default()
};
let mut frame = Frame3D::default();
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera(), &lighting);
assert_eq!(frame.runs.iter().map(|r| r.0).collect::<Vec<_>>(), vec![0]);
let meshes: Vec<Vec<u32>> = frame
.shadow_runs
.iter()
.take(MAX_CASCADES)
.map(|c| c.iter().map(|r| r.0).collect())
.collect();
assert_eq!(meshes, vec![vec![0], vec![1], vec![2], vec![3]]);
}
#[test]
fn the_shadow_box_holds_the_near_view_and_snaps_to_texels() {
let camera = camera();
let sun_dir = Vec3::new(-0.3, -1.0, -0.2);
let (sun, _) = sun_view_proj(&camera, sun_dir, camera.near, 20.0, SHADOW_MAP_SIZE);
let in_box = |p: Vec3| {
let c = sun * p.extend(1.0);
let n = c.truncate() / c.w;
n.x.abs() <= 1.0 && n.y.abs() <= 1.0 && (0.0..=1.0).contains(&n.z)
};
assert!(in_box(Vec3::ZERO), "what the camera looks at");
assert!(in_box(camera.position + camera.forward() * 15.0));
assert!(!in_box(Vec3::new(0.0, 0.0, -200.0)), "past the shadow distance");
let mut moved = camera;
moved.position.x += 0.013;
let a = sun * Vec4::new(0.0, 0.0, 0.0, 1.0);
let b = sun_view_proj(&moved, sun_dir, moved.near, 20.0, SHADOW_MAP_SIZE).0 * Vec4::new(0.0, 0.0, 0.0, 1.0);
let texels = (a - b).truncate().truncate() * SHADOW_MAP_SIZE as f32 / 2.0;
assert!((texels - texels.round()).length() < 2e-2, "{texels}");
}
#[test]
fn cascades_grow_outwards_and_end_at_the_shadow_distance() {
let s = cascade_splits(0.1, 900.0, 4);
assert!(s[0] > 0.1 && s[0] < s[1] && s[1] < s[2] && s[2] < s[3], "{s:?}");
assert_eq!(s[3], 900.0);
assert!(
s[0] < 900.0 / 8.0,
"the first cascade is much smaller than even steps: {s:?}"
);
assert_eq!(cascade_splits(0.1, 60.0, 1)[0], 60.0);
}
#[test]
fn near_models_cast_into_near_cascades_only() {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 0.5)],
..Default::default()
};
let mut camera = camera();
camera.position = Vec3::new(0.0, 2.0, 0.0);
camera.look_at(Vec3::new(0.0, 2.0, -1.0));
camera.near = 0.1;
camera.far = 1000.0;
let lighting = Lighting {
sun_direction: Vec3::new(0.0, -1.0, 0.0),
shadow_distance: 400.0,
..Default::default()
};
let model = Model::new(Mesh { id: 0 });
world.spawn((Transform3D::at(Vec3::new(0.0, 0.0, -3.0)), model)); world.spawn((Transform3D::at(Vec3::new(0.0, 0.0, -350.0)), model)); let mut frame = Frame3D::default();
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera, &lighting);
assert_eq!(frame.cascades, 4);
let counts = frame.shadow_runs.map(|r| r.iter().map(|r| r.2).sum::<u32>());
assert_eq!(counts[0], 1, "only the near model is in the first cascade");
assert_eq!(
counts[3], 2,
"the last cascade's box reaches the far model, and back to the camera"
);
assert!(
frame.cascade_texel[0] < frame.cascade_texel[3] / 10.0,
"near texels are much finer"
);
}
#[test]
fn lod_instances_carry_their_decal_layers_and_fade_in_separate_slots() {
use crate::three::projected::ReceivesDecals;
use crate::{LodGroup, LodLevel, LodRenderer};
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 0.5), (Vec3::ZERO, 0.5)],
..Default::default()
};
let camera = camera(); let group = LodGroup::new(
4.0,
vec![
LodLevel::new(0.3, vec![LodRenderer::new(Mesh { id: 0 })]).with_fade(0.5),
LodLevel::new(0.05, vec![LodRenderer::new(Mesh { id: 1 })]),
],
);
world.spawn((Transform3D::default(), group, ReceivesDecals(0b101)));
let mut frame = Frame3D::default();
let lighting = Lighting {
shadows: false,
..Default::default()
};
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera, &lighting);
let seen = &frame.instances[..frame.visible];
assert_eq!(seen.len(), 2, "both levels draw during the fade");
for i in seen {
assert_eq!(i.model[0][3], 5.0, "the group's decal layers");
assert!(i.fade() != 0.0 && i.fade().abs() < 1.0, "{}", i.fade());
}
assert!(seen[0].fade() * seen[1].fade() < 0.0, "complementary patterns");
}
fn cache_scene(refresh: u32) -> (World, Assets3D, Camera3D, Lighting) {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 0.5)],
..Default::default()
};
let mut camera = camera();
camera.position = Vec3::new(0.0, 2.0, 0.0);
camera.look_at(Vec3::new(0.0, 2.0, -1.0));
camera.near = 0.1;
camera.far = 1000.0;
let lighting = Lighting {
sun_direction: Vec3::new(0.2, -1.0, 0.1),
shadow_distance: 400.0,
shadow_refresh: refresh,
..Default::default()
};
let model = Model::new(Mesh { id: 0 });
world.spawn((Transform3D::at(Vec3::new(0.0, 0.0, -3.0)), model));
world.spawn((Transform3D::at(Vec3::new(0.0, 0.0, -350.0)), model));
(world, assets, camera, lighting)
}
#[test]
fn far_cascades_take_turns_and_keep_their_maps_in_between() {
let (mut world, assets, camera, lighting) = cache_scene(2);
let mut frame = Frame3D::default();
let mut collect = |frame: &mut Frame3D, camera: &Camera3D, lighting: &Lighting| {
frame.collect(&mut world, &assets, &SkinFrame::default(), camera, lighting);
(frame.redrawn, frame.sun_view_proj, frame.shadow_runs.clone())
};
let (first, matrices, _) = collect(&mut frame, &camera, &lighting);
assert_eq!(first, 0b1111, "the first frame draws every cascade");
let (second, kept, runs) = collect(&mut frame, &camera, &lighting);
let (third, _, _) = collect(&mut frame, &camera, &lighting);
assert_eq!(second & 0b11, 0b11);
assert_eq!(third & 0b11, 0b11);
assert_eq!((second | third) & 0b1100, 0b1100);
assert_eq!((second & third) & 0b1100, 0, "{second:b} {third:b}");
let skipped = if second & 0b100 == 0 { 2 } else { 3 };
assert_eq!(
kept[skipped], matrices[skipped],
"a kept cascade samples the map it drew"
);
assert!(runs[skipped].is_empty(), "and draws nothing");
let drawn = 5 - skipped;
assert!(!runs[drawn].is_empty(), "the other far cascade draws its casters");
}
#[test]
fn a_kept_cascade_is_drawn_again_when_the_camera_leaves_it_or_the_sun_moves() {
let (mut world, assets, mut camera, mut lighting) = cache_scene(8);
let mut frame = Frame3D::default();
let mut redrawn = |camera: &Camera3D, lighting: &Lighting| {
frame.collect(&mut world, &assets, &SkinFrame::default(), camera, lighting);
frame.redrawn
};
redrawn(&camera, &lighting);
let still = redrawn(&camera, &lighting);
assert!((still & 0b1100).count_ones() <= 1, "{still:b}");
camera.position.x += 1.0;
let step = redrawn(&camera, &lighting);
assert!((step & 0b1100).count_ones() <= 1, "{step:b}");
camera.position.x += 500.0;
assert_eq!(redrawn(&camera, &lighting), 0b1111);
redrawn(&camera, &lighting);
lighting.sun_direction.x += 0.01;
assert_eq!(redrawn(&camera, &lighting), 0b1111);
lighting.shadow_resolution = 1024;
assert_eq!(redrawn(&camera, &lighting), 0b1111);
}
#[test]
fn refresh_1_draws_every_cascade_every_frame_in_tight_boxes() {
let (mut world, assets, camera, lighting) = cache_scene(1);
let mut frame = Frame3D::default();
for _ in 0..3 {
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera, &lighting);
assert_eq!(frame.redrawn, 0b1111);
}
let splits = cascade_splits(camera.near, lighting.shadow_distance, 4);
let (tight, _) = sun_view_proj(
&camera,
lighting.sun_direction,
splits[2],
splits[3],
lighting.shadow_size(0),
);
assert_eq!(frame.sun_view_proj[3], tight);
let (mut world, assets, camera, lighting) = cache_scene(2);
let mut cached = Frame3D::default();
cached.collect(&mut world, &assets, &SkinFrame::default(), &camera, &lighting);
assert!(cached.cascade_texel[3] > frame.cascade_texel[3] * 1.15);
assert_eq!(cached.cascade_texel[0], frame.cascade_texel[0]);
}
#[test]
fn the_far_half_of_the_cascades_can_be_kept() {
assert_eq!((1..=4).map(far_cascades).collect::<Vec<_>>(), [1, 1, 2, 2]);
}
fn cascade_counts(models: &[(Vec3, f32)], sun: Vec3) -> [u32; MAX_CASCADES] {
let mut world = World::new();
let assets = Assets3D {
bounds: vec![(Vec3::ZERO, 1.0)],
..Default::default()
};
let mut camera = camera();
camera.position = Vec3::new(0.0, 2.0, 0.0);
camera.look_at(Vec3::new(0.0, 2.0, -1.0)); camera.near = 0.1;
camera.far = 1000.0;
let lighting = Lighting {
sun_direction: sun,
shadow_distance: 200.0,
shadow_refresh: 1,
..Default::default()
};
for &(at, radius) in models {
let mut t = Transform3D::at(at);
t.scale = Vec3::splat(radius);
world.spawn((t, Model::new(Mesh { id: 0 })));
}
let mut frame = Frame3D::default();
frame.collect(&mut world, &assets, &SkinFrame::default(), &camera, &lighting);
let mut counts = [0; MAX_CASCADES];
for (i, runs) in frame.shadow_runs.iter().take(MAX_CASCADES).enumerate() {
counts[i] = runs.iter().map(|r| r.2).sum();
}
counts
}
#[test]
fn casters_whose_shadow_cant_reach_the_view_are_skipped() {
let tree = [(Vec3::new(0.0, 3.0, 15.0), 3.0)];
let towards_view = cascade_counts(&tree, Vec3::new(0.0, -0.5, -1.0));
let away = cascade_counts(&tree, Vec3::new(0.0, -0.5, 1.0));
assert!(towards_view.iter().sum::<u32>() > 0, "{towards_view:?}");
assert_eq!(away, [0; MAX_CASCADES]);
}
#[test]
fn casters_smaller_than_a_texel_are_left_out_of_coarse_cascades() {
let sun = Vec3::new(0.2, -1.0, 0.1);
let small = cascade_counts(&[(Vec3::new(0.0, 0.0, -150.0), 0.02)], sun);
let big = cascade_counts(&[(Vec3::new(0.0, 0.0, -150.0), 0.5)], sun);
assert_eq!(small[3], 0, "{small:?}");
assert_eq!(big[3], 1, "{big:?}");
let near = cascade_counts(&[(Vec3::new(0.0, 2.0, -3.0), 0.02)], sun);
assert_eq!(near[0], 1, "{near:?}");
}
}