use super::*;
#[derive(Default)]
pub struct RenderCache {
pub(crate) batches: std::collections::HashMap<BatchKey, BatchData>,
pub instances: Vec<crate::renderer::gpu_types::InstanceRaw>,
pub draw_items: Vec<DrawItem>,
}
thread_local! {
static RENDER_CACHE: std::cell::RefCell<RenderCache> = std::cell::RefCell::new(RenderCache::default());
}
pub fn clear_render_cache() {
RENDER_CACHE.with(|rc| {
let mut cache = rc.borrow_mut();
cache.batches.clear();
cache.instances.clear();
cache.draw_items.clear();
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum DrawLayer {
Backdrop,
Opaque,
Transparent,
}
pub(crate) fn draw_layer(is_backdrop: bool, is_transparent: bool) -> DrawLayer {
if is_backdrop {
DrawLayer::Backdrop
} else if is_transparent {
DrawLayer::Transparent
} else {
DrawLayer::Opaque
}
}
pub(crate) fn cmp_draw_order(a: (DrawLayer, f32), b: (DrawLayer, f32)) -> std::cmp::Ordering {
use std::cmp::Ordering;
match a.0.cmp(&b.0) {
Ordering::Equal => match a.0 {
DrawLayer::Opaque => Ordering::Equal,
DrawLayer::Backdrop | DrawLayer::Transparent => {
b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal)
}
},
different => different,
}
}
#[derive(Debug, Clone)]
pub struct DrawItem {
pub(super) vbuf: std::sync::Arc<wgpu::Buffer>,
pub(super) vertex_count: u32,
pub(super) ibuf: Option<std::sync::Arc<wgpu::Buffer>>,
pub(super) index_count: u32,
pub(super) index_format: wgpu::IndexFormat,
pub(super) bind_group: std::sync::Arc<wgpu::BindGroup>,
pub(super) unlit: bool,
pub(super) baked_lit: bool,
pub(super) is_skybox: bool,
pub(super) is_backdrop: bool,
pub(super) is_double_sided: bool,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(super) casts_shadows: bool,
pub(super) visible_in_camera: bool,
pub(super) skeleton_bind_group: Option<std::sync::Arc<wgpu::BindGroup>>,
pub(super) is_transparent: bool,
pub(super) layer: DrawLayer,
pub(super) first_instance: u32,
pub(super) camera_count: u32,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(super) shadow_first_instance: u32,
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub(super) shadow_count: u32,
pub(super) sort_depth: f32,
}
impl DrawItem {
pub(super) fn record_draw(
&self,
pass: &mut wgpu::RenderPass<'_>,
instances: std::ops::Range<u32>,
) {
pass.set_vertex_buffer(0, self.vbuf.slice(..));
match &self.ibuf {
Some(ibuf) => {
pass.set_index_buffer(ibuf.slice(..), self.index_format);
pass.draw_indexed(0..self.index_count, 0, instances);
}
None => pass.draw(0..self.vertex_count, instances),
}
}
pub(super) fn camera_instance_range(&self, uploaded: u32) -> std::ops::Range<u32> {
self.first_instance
..(self.first_instance + self.camera_count)
.min(uploaded)
.max(self.first_instance)
}
#[cfg(not(target_arch = "wasm32"))]
pub(super) fn shadow_instance_range(&self, uploaded: u32) -> std::ops::Range<u32> {
self.shadow_first_instance
..(self.shadow_first_instance + self.shadow_count)
.min(uploaded)
.max(self.shadow_first_instance)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct BatchKey {
vbuf_id: usize,
mat_id: usize,
skeleton_id: Option<usize>,
is_transparent: bool,
unlit: bool,
baked_lit: bool,
is_skybox: bool,
is_backdrop: bool,
is_double_sided: bool,
casts_shadows: bool,
visible_in_camera: bool,
}
pub(crate) struct BatchData {
vbuf: std::sync::Arc<wgpu::Buffer>,
ibuf: Option<std::sync::Arc<wgpu::Buffer>>,
index_count: u32,
index_format: wgpu::IndexFormat,
bind_group: std::sync::Arc<wgpu::BindGroup>,
vertex_count: u32,
unlit: bool,
baked_lit: bool,
is_skybox: bool,
is_backdrop: bool,
is_double_sided: bool,
casts_shadows: bool,
visible_in_camera: bool,
skeleton_bind_group: Option<std::sync::Arc<wgpu::BindGroup>>,
is_transparent: bool,
instances: Vec<crate::renderer::gpu_types::InstanceRaw>,
shadow_instances: Vec<crate::renderer::gpu_types::InstanceRaw>,
}
pub(super) fn collect_draw_items(
world: &World,
renderer: &mut Renderer,
unjittered_view_proj: Mat4,
cascade_vp: [Mat4; 4],
cam_pos: Vec3,
) -> (Vec<DrawItem>, u32) {
let renderers = world.borrow::<MeshRenderer>();
let frustum = crate::math::Frustum::from_matrix(&unjittered_view_proj);
let cascade_frusta: [crate::math::Frustum; 4] =
cascade_vp.map(|m| crate::math::Frustum::from_matrix(&m));
RENDER_CACHE.with(|rc| {
let mut cache = rc.borrow_mut();
for batch in cache.batches.values_mut() {
batch.instances.clear();
batch.shadow_instances.clear();
}
cache.instances.clear();
cache.draw_items.clear();
let pooled_storage = world.borrow::<gizmo_core::pool::Pooled>();
macro_rules! process_mesh {
($e:expr, $mesh:expr, $trans:expr, $mat:expr, $skeleton:expr) => {
if renderers.get($e).is_none() {
continue;
}
if pooled_storage.get($e).is_some() {
continue;
}
let routing = crate::renderer::routing::route($mat.material_type);
let center_mat = Mat4::from_translation($mesh.center_offset);
let model = $trans.matrix * center_mat;
let drawn_model = crate::renderer::backdrop::camera_locked_model(
$mat.material_type,
&model,
cam_pos,
);
let local_cx = ($mesh.bounds.min.x + $mesh.bounds.max.x) * 0.5;
let local_cy = ($mesh.bounds.min.y + $mesh.bounds.max.y) * 0.5;
let local_cz = ($mesh.bounds.min.z + $mesh.bounds.max.z) * 0.5;
let world_c = drawn_model.transform_point3(Vec3::new(local_cx, local_cy, local_cz));
let hx = ($mesh.bounds.max.x - $mesh.bounds.min.x) * 0.5;
let hy = ($mesh.bounds.max.y - $mesh.bounds.min.y) * 0.5;
let hz = ($mesh.bounds.max.z - $mesh.bounds.min.z) * 0.5;
let local_r = (hx * hx + hy * hy + hz * hz).sqrt();
let sx = drawn_model.x_axis.truncate().length();
let sy = drawn_model.y_axis.truncate().length();
let sz = drawn_model.z_axis.truncate().length();
let world_r = local_r * sx.max(sy).max(sz);
let drawn_world_aabb = $mesh.bounds.transform(&drawn_model);
let camera_visible = match crate::renderer::classify_visibility_world(
&frustum,
&cascade_frusta,
drawn_world_aabb,
$mat.material_type,
$mat.is_transparent,
$mat.albedo.w,
) {
crate::renderer::Visibility::Culled => continue,
crate::renderer::Visibility::Camera => true,
crate::renderer::Visibility::ShadowOnly => false,
};
let dist_to_cam = crate::renderer::components::effective_lod_distance(
(world_c - cam_pos).length(),
renderers.get($e).map(|r| r.lod_bias).unwrap_or(1.0),
);
let use_lod1 = if !$mesh.lod_vbufs.is_empty() {
dist_to_cam > world_r * 15.0 } else {
false
};
let active_vbuf = if use_lod1 {
$mesh.lod_vbufs[0].clone()
} else {
$mesh.vbuf.clone()
};
let active_vertex_count = if use_lod1 {
$mesh.lod_vertex_counts[0]
} else {
$mesh.vertex_count
};
let (active_ibuf, active_index_count, active_index_format) = if use_lod1 {
(None, 0, wgpu::IndexFormat::Uint32)
} else {
($mesh.ibuf.clone(), $mesh.index_count, $mesh.index_format)
};
let upload_model = crate::renderer::backdrop::instance_model(
$mat.material_type,
&model,
cam_pos,
);
let instance_data = crate::renderer::gpu_types::InstanceRaw::new(
upload_model.to_cols_array_2d(),
[$mat.albedo.x, $mat.albedo.y, $mat.albedo.z, $mat.albedo.w],
$mat.roughness,
$mat.metallic,
routing.instance_flag,
$mat.anisotropy,
$mat.clear_coat,
$mat.subsurface,
$mat.ambient.to_array(),
$mat.emissive.to_array(),
);
let skel_bg = $skeleton.map(|s: &crate::renderer::components::Skeleton| s.bind_group.clone());
let is_skybox = routing.is_skybox;
let baked_lit = routing.baked_lit;
let is_backdrop = routing.is_backdrop;
let unlit = routing.skips_deferred;
let is_transparent = $mat.is_transparent || $mat.albedo.w < 0.99;
let is_double_sided = $mat.is_double_sided;
let shadows = renderers.get($e).map(|r| r.shadows).unwrap_or_default();
let casts_shadows = shadows.casts();
let visible_in_camera = shadows.visible();
let key = BatchKey {
vbuf_id: std::sync::Arc::as_ptr(&active_vbuf) as usize,
mat_id: std::sync::Arc::as_ptr(&$mat.bind_group) as usize,
skeleton_id: skel_bg.as_ref().map(|bg| std::sync::Arc::as_ptr(bg) as usize),
is_transparent,
unlit,
baked_lit,
is_skybox,
is_backdrop,
is_double_sided,
casts_shadows,
visible_in_camera,
};
let batch = cache.batches.entry(key).or_insert_with(|| BatchData {
vbuf: active_vbuf.clone(),
ibuf: active_ibuf.clone(),
index_count: active_index_count,
index_format: active_index_format,
bind_group: $mat.bind_group.clone(),
vertex_count: active_vertex_count,
unlit,
baked_lit,
is_skybox,
is_backdrop,
is_double_sided,
casts_shadows,
visible_in_camera,
skeleton_bind_group: skel_bg,
is_transparent,
instances: Vec::new(),
shadow_instances: Vec::new(),
});
if camera_visible {
batch.instances.push(instance_data);
} else {
batch.shadow_instances.push(instance_data);
}
};
}
let skeletons = world.borrow::<crate::renderer::components::Skeleton>();
let lod_groups = world.borrow::<crate::renderer::components::LodGroup>();
if let Some(mut q) = world.query::<(&Mesh, &gizmo_physics_core::components::GlobalTransform, &Material)>() {
for (e, (mesh, trans, mat)) in q.iter_mut() {
let Some(mesh) = crate::renderer::components::LodGroup::pick(
lod_groups.get(e),
mesh,
cam_pos.distance(trans.matrix.w_axis.truncate()),
) else {
continue;
};
process_mesh!(e, mesh, trans, mat, skeletons.get(e));
}
}
let meshes = world.try_get_resource::<gizmo_core::asset::Assets<Mesh>>().ok();
let materials = world.try_get_resource::<gizmo_core::asset::Assets<Material>>().ok();
if let (Some(meshes), Some(materials)) = (meshes, materials) {
if let Some(mut q) = world.query::<(&gizmo_core::asset::Handle<Mesh>, &gizmo_physics_core::components::GlobalTransform, &gizmo_core::asset::Handle<Material>)>() {
for (e, (h_mesh, trans, h_mat)) in q.iter_mut() {
if let (Some(mesh), Some(mat)) = (meshes.get(h_mesh), materials.get(h_mat)) {
let Some(mesh) = crate::renderer::components::LodGroup::pick(
lod_groups.get(e),
mesh,
cam_pos.distance(trans.matrix.w_axis.truncate()),
) else {
continue;
};
process_mesh!(e, mesh, trans, mat, skeletons.get(e));
}
}
}
}
let mut local_instances: Vec<crate::renderer::gpu_types::InstanceRaw> = std::mem::take(&mut cache.instances);
let mut local_draw_items: Vec<DrawItem> = std::mem::take(&mut cache.draw_items);
for batch in cache.batches.values_mut() {
if batch.is_transparent || batch.is_backdrop {
crate::renderer::sort_back_to_front(&mut batch.instances, cam_pos);
}
}
let batches: Vec<&BatchData> = cache
.batches
.values()
.filter(|b| !(b.instances.is_empty() && b.shadow_instances.is_empty()))
.collect();
for batch in &batches {
let first_instance = local_instances.len() as u32;
let camera_count = batch.instances.len() as u32;
let sort_depth = if batch.is_backdrop {
crate::renderer::batch_depth(&batch.instances, Vec3::ZERO)
} else if batch.is_transparent {
crate::renderer::batch_depth(&batch.instances, cam_pos)
} else {
0.0
};
local_instances.extend(&batch.instances);
local_draw_items.push(DrawItem {
vbuf: batch.vbuf.clone(),
vertex_count: batch.vertex_count,
ibuf: batch.ibuf.clone(),
index_count: batch.index_count,
index_format: batch.index_format,
bind_group: batch.bind_group.clone(),
unlit: batch.unlit,
baked_lit: batch.baked_lit,
is_skybox: batch.is_skybox,
is_backdrop: batch.is_backdrop,
is_double_sided: batch.is_double_sided,
casts_shadows: batch.casts_shadows,
visible_in_camera: batch.visible_in_camera,
skeleton_bind_group: batch.skeleton_bind_group.clone(),
is_transparent: batch.is_transparent,
layer: draw_layer(batch.is_backdrop, batch.is_transparent),
first_instance,
camera_count,
shadow_first_instance: 0,
shadow_count: 0,
sort_depth,
});
}
let draw_item_base = local_draw_items.len() - batches.len();
for (i, batch) in batches.iter().enumerate() {
let shadow_first_instance = local_instances.len() as u32;
local_instances.extend(&batch.shadow_instances);
let item = &mut local_draw_items[draw_item_base + i];
item.shadow_first_instance = shadow_first_instance;
item.shadow_count = batch.shadow_instances.len() as u32;
}
local_draw_items
.sort_by(|a, b| cmp_draw_order((a.layer, a.sort_depth), (b.layer, b.sort_depth)));
cache.instances = local_instances;
cache.draw_items = local_draw_items;
renderer.ensure_instance_capacity(cache.instances.len());
let max_instances = renderer.scene.instance_capacity;
let instances_slice = if cache.instances.len() > max_instances {
&cache.instances[..max_instances]
} else {
&cache.instances
};
if !instances_slice.is_empty() {
renderer.queue.write_buffer(
&renderer.scene.instance_buffer,
0,
bytemuck::cast_slice(instances_slice),
);
}
(cache.draw_items.clone(), instances_slice.len() as u32)
})
}
#[cfg(test)]
mod batch_key_tests {
use super::BatchKey;
#[test]
fn shadow_casting_distinguishes_batches_sharing_a_material() {
let base = BatchKey {
vbuf_id: 1,
mat_id: 42,
skeleton_id: None,
is_transparent: false,
unlit: false,
baked_lit: false,
is_skybox: false,
is_backdrop: false,
is_double_sided: false,
casts_shadows: true,
visible_in_camera: true,
};
let no_cast = BatchKey { casts_shadows: false, ..base.clone() };
let shadow_only = BatchKey { visible_in_camera: false, ..base.clone() };
assert_ne!(base, no_cast, "ShadowCasting::Off must not batch with On");
assert_ne!(base, shadow_only, "ShadowCasting::Only must not batch with On");
assert_ne!(no_cast, shadow_only, "Off and Only are different batches");
}
#[test]
fn routing_flags_distinguish_batches_sharing_a_texture() {
let base = BatchKey {
vbuf_id: 1,
mat_id: 42, skeleton_id: None,
is_transparent: false,
unlit: false,
baked_lit: false,
is_skybox: false,
is_backdrop: false,
is_double_sided: false,
casts_shadows: true,
visible_in_camera: true,
};
let transparent = BatchKey {
is_transparent: true,
..base.clone()
};
let unlit = BatchKey {
unlit: true,
baked_lit: false,
..base.clone()
};
let skybox = BatchKey {
is_skybox: true,
..base.clone()
};
let backdrop = BatchKey {
unlit: true,
is_backdrop: true,
..base.clone()
};
let two_sided = BatchKey { is_double_sided: true, ..base.clone() };
assert_ne!(base, two_sided, "one-sided and double-sided must be separate batches");
assert_ne!(base, transparent, "opaque and transparent must be separate batches");
assert_ne!(base, unlit, "PBR and unlit must be separate batches");
assert_ne!(base, skybox, "PBR and skybox must be separate batches");
assert_ne!(unlit, backdrop, "unlit and backdrop must be separate batches");
assert_eq!(base, base.clone(), "identical materials must still batch together");
}
}
#[cfg(test)]
mod transparent_order_tests {
use super::{cmp_draw_order, draw_layer, DrawLayer, Vec3};
use crate::renderer::batch_depth as batch_sort_depth;
use crate::renderer::gpu_types::InstanceRaw;
use bytemuck::Zeroable;
use DrawLayer::{Backdrop, Opaque, Transparent};
fn inst_at(x: f32, y: f32, z: f32) -> InstanceRaw {
let mut i = InstanceRaw::zeroed();
i.model = [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
[x, y, z, 1.0],
];
i
}
#[test]
fn batch_depth_is_centroid_distance_to_camera() {
let cam = Vec3::new(0.0, 0.0, 0.0);
assert!((batch_sort_depth(&[inst_at(0.0, 0.0, -10.0)], cam) - 10.0).abs() < 1e-3);
let d = batch_sort_depth(&[inst_at(3.0, 0.0, -4.0), inst_at(-3.0, 0.0, -4.0)], cam);
assert!((d - 4.0).abs() < 1e-3, "centroid distance wrong: {d}");
assert_eq!(batch_sort_depth(&[], cam), 0.0);
}
#[test]
fn opaque_first_then_transparent_back_to_front() {
let mut items = vec![
(Transparent, 5.0), (Opaque, 0.0),
(Transparent, 20.0), (Opaque, 0.0),
(Transparent, 12.0), ];
items.sort_by(|a, b| cmp_draw_order(*a, *b));
assert_eq!(
items,
vec![
(Opaque, 0.0),
(Opaque, 0.0),
(Transparent, 20.0),
(Transparent, 12.0),
(Transparent, 5.0)
]
);
}
#[test]
fn transparent_order_independent_of_input_order() {
let mut a = vec![(Transparent, 3.0), (Transparent, 9.0), (Transparent, 1.0)];
let mut b = vec![(Transparent, 1.0), (Transparent, 3.0), (Transparent, 9.0)];
a.sort_by(|x, y| cmp_draw_order(*x, *y));
b.sort_by(|x, y| cmp_draw_order(*x, *y));
assert_eq!(a, b);
assert_eq!(a, vec![(Transparent, 9.0), (Transparent, 3.0), (Transparent, 1.0)]);
}
#[test]
fn backdrops_are_drawn_before_everything_else() {
let mut items = vec![
(Transparent, 900.0), (Opaque, 0.0),
(Backdrop, 300.0),
(Opaque, 0.0),
(Backdrop, 1200.0),
(Transparent, 4.0),
];
items.sort_by(|a, b| cmp_draw_order(*a, *b));
assert_eq!(
items,
vec![
(Backdrop, 1200.0),
(Backdrop, 300.0),
(Opaque, 0.0),
(Opaque, 0.0),
(Transparent, 900.0),
(Transparent, 4.0),
],
"a backdrop drawn after a transparent object paints over it — the transparent \
pipeline writes no depth, so nothing else can put the backdrop underneath"
);
}
#[test]
fn overlapping_backdrops_composite_in_a_deterministic_order() {
let mut a = vec![(Backdrop, 80.0), (Backdrop, 500.0), (Backdrop, 200.0)];
let mut b = vec![(Backdrop, 200.0), (Backdrop, 80.0), (Backdrop, 500.0)];
a.sort_by(|x, y| cmp_draw_order(*x, *y));
b.sort_by(|x, y| cmp_draw_order(*x, *y));
assert_eq!(a, b, "backdrop order must not depend on insertion order");
assert_eq!(a, vec![(Backdrop, 500.0), (Backdrop, 200.0), (Backdrop, 80.0)]);
}
#[test]
fn a_transparent_backdrop_is_still_a_backdrop() {
assert_eq!(draw_layer(true, true), Backdrop);
assert_eq!(draw_layer(true, false), Backdrop);
assert_eq!(draw_layer(false, true), Transparent);
assert_eq!(draw_layer(false, false), Opaque);
}
#[test]
fn backdrop_sort_depth_is_measured_from_the_camera_locked_origin() {
let near_panel = [inst_at(0.0, 0.0, -100.0)];
let far_panel = [inst_at(0.0, 0.0, -400.0)];
for cam in [Vec3::ZERO, Vec3::new(0.0, 0.0, -900.0), Vec3::new(650.0, 20.0, 480.0)] {
let near = batch_sort_depth(&near_panel, Vec3::ZERO);
let far = batch_sort_depth(&far_panel, Vec3::ZERO);
assert!((near - 100.0).abs() < 1e-3 && (far - 400.0).abs() < 1e-3);
assert_eq!(
cmp_draw_order((Backdrop, far), (Backdrop, near)),
std::cmp::Ordering::Less,
"the far panel must paint first"
);
let _ = batch_sort_depth(&near_panel, cam);
}
let flipped_near = batch_sort_depth(&near_panel, Vec3::new(0.0, 0.0, -900.0));
let flipped_far = batch_sort_depth(&far_panel, Vec3::new(0.0, 0.0, -900.0));
assert!(
flipped_near > flipped_far,
"premise: from a camera at z=-900 the 'near' panel is the farther one ({flipped_near} \
vs {flipped_far}) — which is why a backdrop must not be sorted from the camera"
);
}
}