fix(render): share repeated block meshes

Keep placement transforms and bounds separate from immutable mesh data to prevent large drawings from exhausting memory or stalling selection.
This commit is contained in:
Hakan Seven 2026-08-12 11:01:36 +03:00
commit 7b4af0aa75
6 changed files with 408 additions and 399 deletions

View file

@ -89,6 +89,9 @@ fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64
} }
fn mesh_interaction_aabb(set: &model::mesh_model::MeshLodSet) -> Option<[f64; 6]> { fn mesh_interaction_aabb(set: &model::mesh_model::MeshLodSet) -> Option<[f64; 6]> {
if let Some(aabb) = set.instance_aabb {
return Some(aabb);
}
let mut aabb = [ let mut aabb = [
f64::INFINITY, f64::INFINITY,
f64::INFINITY, f64::INFINITY,
@ -1213,95 +1216,63 @@ fn offset_mesh_lod_set(mut set: MeshLodSet) -> MeshLodSet {
set set
} }
/// Instance a block-local mesh into the render frame: apply the accumulated /// Build a lightweight placement over immutable block-local geometry.
/// INSERT transform (block-local → world/DXF) then subtract world_offset, so a
/// block scaled at the INSERT renders at the right size. Normals are rotated by
/// the transform's linear part and re-normalized. (#123)
fn transform_block_mesh_lod_set( fn transform_block_mesh_lod_set(
set: &MeshLodSet, set: &MeshLodSet,
xform: &acadrust::types::Transform, xform: &acadrust::types::Transform,
) -> MeshLodSet { ) -> MeshLodSet {
use acadrust::types::Vector3; use acadrust::types::Vector3;
let mut out = set.clone(); let source = set.instance_source.clone().unwrap_or_else(|| {
out.instance_transform = Some(*xform); std::sync::Arc::new(model::mesh_model::MeshInstanceSource {
let mut min_x = f32::INFINITY; handle: set.entity_handle().unwrap_or(Handle::new(0)),
let mut min_y = f32::INFINITY; lods: set.lods.clone(),
let mut max_x = f32::NEG_INFINITY; edge_verts: set.edge_verts.clone(),
let mut max_y = f32::NEG_INFINITY; edge_verts_low: set.edge_verts_low.clone(),
for lod in &mut out.lods { curved_gens: set.curved_gens.clone(),
let has_low = lod.verts_low.len() == lod.verts.len(); })
if !has_low { });
lod.verts_low = vec![[0.0; 3]; lod.verts.len()]; let mut bounds = [
} f64::INFINITY,
for (v, vl) in lod.verts.iter_mut().zip(lod.verts_low.iter_mut()) { f64::INFINITY,
// Reconstruct the block-local f64, apply the INSERT transform and f64::INFINITY,
// subtract world_offset in f64, then re-split into (high, low). f64::NEG_INFINITY,
let w = xform.apply(Vector3::new( f64::NEG_INFINITY,
v[0] as f64 + vl[0] as f64, f64::NEG_INFINITY,
v[1] as f64 + vl[1] as f64, ];
v[2] as f64 + vl[2] as f64, for x in [set.world_aabb[0], set.world_aabb[2]] {
)); for y in [set.world_aabb[1], set.world_aabb[3]] {
let ax = w.x; for z in [set.z_aabb[0], set.z_aabb[1]] {
let ay = w.y; let point = xform.apply(Vector3::new(x as f64, y as f64, z as f64));
let az = w.z; for (axis, value) in [point.x, point.y, point.z].into_iter().enumerate() {
let hx = ax as f32; bounds[axis] = bounds[axis].min(value);
let hy = ay as f32; bounds[axis + 3] = bounds[axis + 3].max(value);
let hz = az as f32; }
*v = [hx, hy, hz];
*vl = [
(ax - hx as f64) as f32,
(ay - hy as f64) as f32,
(az - hz as f64) as f32,
];
if hx < min_x {
min_x = hx;
}
if hy < min_y {
min_y = hy;
}
if hx > max_x {
max_x = hx;
}
if hy > max_y {
max_y = hy;
}
}
for n in &mut lod.normals {
let d = xform.apply_rotation(Vector3::new(n[0] as f64, n[1] as f64, n[2] as f64));
let len = (d.x * d.x + d.y * d.y + d.z * d.z).sqrt();
if len > 1e-12 {
n[0] = (d.x / len) as f32;
n[1] = (d.y / len) as f32;
n[2] = (d.z / len) as f32;
} }
} }
} }
// Apply the same INSERT transform to the feature edges. MeshLodSet {
{ lods: Vec::new(),
let n = out.edge_verts.len(); material: set.material.clone(),
if out.edge_verts_low.len() != n { face_materials: set.face_materials.clone(),
out.edge_verts_low = vec![[0.0; 3]; n]; visual_style: set.visual_style.clone(),
} complete: set.complete,
for (v, vl) in out.edge_verts.iter_mut().zip(out.edge_verts_low.iter_mut()) { edge_verts: Vec::new(),
let w = xform.apply(Vector3::new( edge_verts_low: Vec::new(),
v[0] as f64 + vl[0] as f64, curved_gens: Vec::new(),
v[1] as f64 + vl[1] as f64, metrics: set.metrics,
v[2] as f64 + vl[2] as f64, world_aabb: [
)); bounds[0] as f32,
let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32); bounds[1] as f32,
*v = [hx, hy, hz]; bounds[3] as f32,
*vl = [ bounds[4] as f32,
(w.x - hx as f64) as f32, ],
(w.y - hy as f64) as f32, z_aabb: [bounds[2] as f32, bounds[5] as f32],
(w.z - hz as f64) as f32, instance_source: Some(source),
]; instance_transform: Some(*xform),
} instance_handle: None,
instance_color: set.display_color(),
instance_aabb: bounds.iter().all(|value| value.is_finite()).then_some(bounds),
} }
if min_x.is_finite() {
out.world_aabb = [min_x, min_y, max_x, max_y];
}
out.recompute_aabb();
out
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -1538,8 +1509,7 @@ pub struct Scene {
Arc<crate::scene::pick::interaction_index::InteractionIndex>, Arc<crate::scene::pick::interaction_index::InteractionIndex>,
)>, )>,
>, >,
/// Auxiliary broad phase for objects that have no wire representation: /// Auxiliary broad phase for hatch-only objects.
/// hatch-only and solid-only block instances.
interaction_handle_index_cache: RefCell< interaction_handle_index_cache: RefCell<
Option<( Option<(
u64, u64,
@ -6169,15 +6139,11 @@ impl Scene {
} }
let mut lookup: HashMap<Handle, Vec<u32>> = HashMap::default(); let mut lookup: HashMap<Handle, Vec<u32>> = HashMap::default();
for (index, set) in meshes.iter().enumerate() { for (index, set) in meshes.iter().enumerate() {
let Some(value) = set let Some(handle) = set.entity_handle() else {
.lods
.first()
.and_then(|mesh| mesh.name.parse::<u64>().ok())
else {
continue; continue;
}; };
lookup lookup
.entry(Handle::new(value)) .entry(handle)
.or_default() .or_default()
.push(index as u32); .push(index as u32);
} }
@ -6490,18 +6456,6 @@ impl Scene {
true true
} }
fn mesh_visible_for_interaction(&self, handle: Handle) -> bool {
self.mesh_entity_visible(handle)
&& self.document.get_entity(handle).is_some_and(|entity| {
!self.interaction_layer_frozen(&entity.common().layer)
&& self.belongs_to_visible_block(
handle,
entity.common().owner_handle,
self.interaction_block_handle(),
)
})
}
/// One transformed mesh per block-definition solid instance reached from an /// One transformed mesh per block-definition solid instance reached from an
/// INSERT owned by `layout_block`. Nested INSERTs accumulate their /// INSERT owned by `layout_block`. Nested INSERTs accumulate their
/// transform. Empty when no block solids exist. (#123) /// transform. Empty when no block solids exist. (#123)
@ -6541,8 +6495,7 @@ impl Scene {
return; return;
}; };
let inherit = self.mesh_inherit_for_path(&context.insert_path); let inherit = self.mesh_inherit_for_path(&context.insert_path);
let own_alpha = let own_alpha = set.display_color().map_or(1.0, |color| color[3]);
set.lods.first().map(|mesh| mesh.color[3]).unwrap_or(1.0);
let mut transformed = let mut transformed =
transform_block_mesh_lod_set(set, &context.transform); transform_block_mesh_lod_set(set, &context.transform);
if let Some(color) = self.block_mesh_override_color( if let Some(color) = self.block_mesh_override_color(
@ -6551,9 +6504,7 @@ impl Scene {
inherit.as_ref(), inherit.as_ref(),
own_alpha, own_alpha,
) { ) {
for mesh in &mut transformed.lods { transformed.instance_color = Some(color);
mesh.color = color;
}
if let Some(material) = transformed.material.as_mut() { if let Some(material) = transformed.material.as_mut() {
if material.handle.is_none() { if material.handle.is_none() {
material.diffuse = color; material.diffuse = color;
@ -6569,10 +6520,7 @@ impl Scene {
self.material_base_dir.as_deref(), self.material_base_dir.as_deref(),
); );
} }
let root_name = context.root_handle.value().to_string(); transformed.instance_handle = Some(context.root_handle);
for mesh in &mut transformed.lods {
mesh.name = root_name.clone();
}
out.push(transformed); out.push(transformed);
}, },
); );
@ -7449,20 +7397,6 @@ impl Scene {
} }
} }
} }
for set in self.interaction_meshes_arc().iter() {
let Some(mesh) = set.lods.first() else {
continue;
};
let Ok(handle) = mesh.name.parse::<u64>() else {
continue;
};
if !self.mesh_visible_for_interaction(Handle::new(handle)) {
continue;
}
if let Some(aabb) = mesh_interaction_aabb(set) {
entries.push((handle, aabb));
}
}
let index = let index =
Arc::new(crate::scene::pick::interaction_index::InteractionHandleIndex::build(entries)); Arc::new(crate::scene::pick::interaction_index::InteractionHandleIndex::build(entries));
*self.interaction_handle_index_cache.borrow_mut() = *self.interaction_handle_index_cache.borrow_mut() =
@ -7677,7 +7611,6 @@ impl Scene {
.collect(); .collect();
let insert_hatches = self.insert_hatches_for_click(); let insert_hatches = self.insert_hatches_for_click();
let meshes = self.interaction_meshes_arc(); let meshes = self.interaction_meshes_arc();
let mesh_lookup = self.mesh_pick_lookup(&meshes);
handles.extend( handles.extend(
self.interaction_handle_index() self.interaction_handle_index()
.query_xy(aabb) .query_xy(aabb)
@ -7685,12 +7618,19 @@ impl Scene {
.filter_map(|value| { .filter_map(|value| {
let handle = Handle::new(value); let handle = Handle::new(value);
(self.hatch_visible_for_interaction(handle) (self.hatch_visible_for_interaction(handle)
|| insert_hatches.contains_key(&handle) || insert_hatches.contains_key(&handle))
|| (mesh_lookup.contains_key(&handle)
&& self.mesh_visible_for_interaction(handle)))
.then_some(handle) .then_some(handle)
}), }),
); );
handles.extend(meshes.iter().filter_map(|set| {
let handle = set.entity_handle()?;
let bounds = mesh_interaction_aabb(set)?;
(bounds[3] >= aabb[0]
&& bounds[0] <= aabb[2]
&& bounds[4] >= aabb[1]
&& bounds[1] <= aabb[3])
.then_some(handle)
}));
handles handles
} }
@ -7703,22 +7643,17 @@ impl Scene {
view_rot: glam::Mat4, view_rot: glam::Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: iced::Rectangle, bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>, _candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> { ) -> Vec<Handle> {
let meshes = self.interaction_meshes_arc(); let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes); let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else( let handles: Vec<Handle> = lookup.keys().copied().collect();
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let mut out = Vec::new(); let mut out = Vec::new();
for handle in handles { for handle in handles {
if !self.mesh_visible_for_interaction(handle) if matches!(
|| matches!( self.document.get_entity(handle),
self.document.get_entity(handle), Some(EntityType::Insert(_))
Some(EntityType::Insert(_)) ) {
)
{
continue; continue;
} }
let Some(indices) = lookup.get(&handle) else { let Some(indices) = lookup.get(&handle) else {
@ -7728,12 +7663,12 @@ impl Scene {
.iter() .iter()
.filter_map(|&index| meshes.get(index as usize)) .filter_map(|&index| meshes.get(index as usize))
.any(|set| { .any(|set| {
set.lods.first().is_some_and(|mesh| { set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_box_hit( !pick::hit_test::mesh_box_hit(
a, a,
b, b,
crossing, crossing,
std::iter::once((handle, mesh)), std::iter::once((handle, mesh, set.instance_transform)),
view_rot, view_rot,
eye, eye,
bounds, bounds,
@ -7877,22 +7812,17 @@ impl Scene {
view_rot: glam::Mat4, view_rot: glam::Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: iced::Rectangle, bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>, _candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> { ) -> Vec<Handle> {
let meshes = self.interaction_meshes_arc(); let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes); let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else( let handles: Vec<Handle> = lookup.keys().copied().collect();
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let mut out = Vec::new(); let mut out = Vec::new();
for handle in handles { for handle in handles {
if !self.mesh_visible_for_interaction(handle) if matches!(
|| matches!( self.document.get_entity(handle),
self.document.get_entity(handle), Some(EntityType::Insert(_))
Some(EntityType::Insert(_)) ) {
)
{
continue; continue;
} }
let Some(indices) = lookup.get(&handle) else { let Some(indices) = lookup.get(&handle) else {
@ -7902,11 +7832,11 @@ impl Scene {
.iter() .iter()
.filter_map(|&index| meshes.get(index as usize)) .filter_map(|&index| meshes.get(index as usize))
.any(|set| { .any(|set| {
set.lods.first().is_some_and(|mesh| { set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_poly_hit( !pick::hit_test::mesh_poly_hit(
poly, poly,
crossing, crossing,
std::iter::once((handle, mesh)), std::iter::once((handle, mesh, set.instance_transform)),
view_rot, view_rot,
eye, eye,
bounds, bounds,
@ -7971,60 +7901,34 @@ impl Scene {
view_rot: glam::Mat4, view_rot: glam::Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: iced::Rectangle, bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>, _candidate_handles: Option<&HashSet<Handle>>,
coarse_lod: bool, coarse_lod: bool,
) -> Option<Handle> { ) -> Option<Handle> {
// Reuse the renderer's expanded mesh set (top-level solids + per-INSERT // Reuse the renderer's cached top-level and block-instance mesh set.
// block instances), cached per geometry epoch — so hover no longer
// re-expands every block instance on each move. Every `MeshLodSet`
// carries its handle (in `mesh.name`) and a 3D AABB.
let meshes = self.interaction_meshes_arc(); let meshes = self.interaction_meshes_arc();
let lookup = candidate_handles.map(|_| self.mesh_pick_lookup(&meshes)); let sets: Vec<(Handle, &MeshLodSet)> = meshes
// Candidate handles already came from the precise f64 interaction BVH. .iter()
// When no index is active the source is small/non-resident, so exact .filter_map(|set| {
// triangles are safer than reintroducing the old f32 AABB precision loss. let handle = set.entity_handle()?;
let mut sets: Vec<(Handle, &MeshLodSet)> = Vec::new(); Some((handle, set))
if let (Some(handles), Some(lookup)) = (candidate_handles, lookup.as_ref()) { })
for handle in handles { .collect();
if !self.mesh_visible_for_interaction(*handle) {
continue;
}
let Some(indices) = lookup.get(handle) else {
continue;
};
sets.extend(
indices
.iter()
.filter_map(|&index| meshes.get(index as usize).map(|set| (*handle, set))),
);
}
} else {
sets.extend(meshes.iter().filter_map(|set| {
let handle = set
.lods
.first()?
.name
.parse::<u64>()
.ok()
.map(Handle::new)?;
self.mesh_visible_for_interaction(handle)
.then_some((handle, set))
}));
}
if coarse_lod {
return pick::hit_test::mesh_click_hit(
cursor,
sets.iter()
.filter_map(|(handle, set)| set.lods.last().map(|mesh| (*handle, mesh))),
view_rot,
eye,
bounds,
);
}
pick::hit_test::mesh_click_hit( pick::hit_test::mesh_click_hit(
cursor, cursor,
sets.iter() sets.iter().filter_map(|(handle, set)| {
.filter_map(|(handle, set)| set.lods.first().map(|mesh| (*handle, mesh))), let lods = set.geometry_lods();
let mesh = if coarse_lod {
lods.last()?
} else {
lods.first()?
};
Some((
*handle,
mesh,
set.instance_transform,
mesh_interaction_aabb(set)?,
))
}),
view_rot, view_rot,
eye, eye,
bounds, bounds,
@ -8043,24 +7947,20 @@ impl Scene {
view_rot: glam::Mat4, view_rot: glam::Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: iced::Rectangle, bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>, _candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> { ) -> Vec<Handle> {
if self.block_meshes.is_empty() { if self.block_meshes.is_empty() {
return Vec::new(); return Vec::new();
} }
let meshes = self.interaction_meshes_arc(); let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes); let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else( let handles: Vec<Handle> = lookup.keys().copied().collect();
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let mut out = Vec::new(); let mut out = Vec::new();
for handle in handles { for handle in handles {
if !matches!( if !matches!(
self.document.get_entity(handle), self.document.get_entity(handle),
Some(EntityType::Insert(_)) Some(EntityType::Insert(_))
) || !self.mesh_visible_for_interaction(handle) ) {
{
continue; continue;
} }
let Some(indices) = lookup.get(&handle) else { let Some(indices) = lookup.get(&handle) else {
@ -8070,12 +7970,12 @@ impl Scene {
.iter() .iter()
.filter_map(|&index| meshes.get(index as usize)) .filter_map(|&index| meshes.get(index as usize))
.any(|set| { .any(|set| {
set.lods.first().map_or(false, |m| { set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_box_hit( !pick::hit_test::mesh_box_hit(
a, a,
b, b,
crossing, crossing,
std::iter::once((handle, m)), std::iter::once((handle, mesh, set.instance_transform)),
view_rot, view_rot,
eye, eye,
bounds, bounds,
@ -8098,24 +7998,20 @@ impl Scene {
view_rot: glam::Mat4, view_rot: glam::Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: iced::Rectangle, bounds: iced::Rectangle,
candidate_handles: Option<&HashSet<Handle>>, _candidate_handles: Option<&HashSet<Handle>>,
) -> Vec<Handle> { ) -> Vec<Handle> {
if self.block_meshes.is_empty() { if self.block_meshes.is_empty() {
return Vec::new(); return Vec::new();
} }
let meshes = self.interaction_meshes_arc(); let meshes = self.interaction_meshes_arc();
let lookup = self.mesh_pick_lookup(&meshes); let lookup = self.mesh_pick_lookup(&meshes);
let handles: Vec<Handle> = candidate_handles.map_or_else( let handles: Vec<Handle> = lookup.keys().copied().collect();
|| lookup.keys().copied().collect(),
|candidates| candidates.iter().copied().collect(),
);
let mut out = Vec::new(); let mut out = Vec::new();
for handle in handles { for handle in handles {
if !matches!( if !matches!(
self.document.get_entity(handle), self.document.get_entity(handle),
Some(EntityType::Insert(_)) Some(EntityType::Insert(_))
) || !self.mesh_visible_for_interaction(handle) ) {
{
continue; continue;
} }
let Some(indices) = lookup.get(&handle) else { let Some(indices) = lookup.get(&handle) else {
@ -8125,11 +8021,11 @@ impl Scene {
.iter() .iter()
.filter_map(|&index| meshes.get(index as usize)) .filter_map(|&index| meshes.get(index as usize))
.any(|set| { .any(|set| {
set.lods.first().map_or(false, |m| { set.geometry_lods().first().is_some_and(|mesh| {
!pick::hit_test::mesh_poly_hit( !pick::hit_test::mesh_poly_hit(
poly, poly,
crossing, crossing,
std::iter::once((handle, m)), std::iter::once((handle, mesh, set.instance_transform)),
view_rot, view_rot,
eye, eye,
bounds, bounds,

View file

@ -221,8 +221,12 @@ impl MeshMaterial {
} }
pub fn apply_to(&self, set: &mut super::mesh_model::MeshLodSet) { pub fn apply_to(&self, set: &mut super::mesh_model::MeshLodSet) {
for lod in &mut set.lods { if set.instance_source.is_some() {
lod.color = self.diffuse; set.instance_color = Some(self.diffuse);
} else {
for lod in &mut set.lods {
lod.color = self.diffuse;
}
} }
set.material = Some(self.clone()); set.material = Some(self.clone());
} }
@ -236,7 +240,7 @@ impl MeshMaterial {
self.apply_to(set); self.apply_to(set);
set.face_materials.clear(); set.face_materials.clear();
let handles: rustc_hash::FxHashSet<Handle> = set let handles: rustc_hash::FxHashSet<Handle> = set
.lods .geometry_lods()
.iter() .iter()
.flat_map(|lod| lod.triangle_material_handles.iter().flatten().copied()) .flat_map(|lod| lod.triangle_material_handles.iter().flatten().copied())
.collect(); .collect();

View file

@ -104,6 +104,12 @@ pub struct MeshLodSet {
pub instance_source: Option<std::sync::Arc<MeshInstanceSource>>, pub instance_source: Option<std::sync::Arc<MeshInstanceSource>>,
/// Accumulated block-local → world transform for this rendered instance. /// Accumulated block-local → world transform for this rendered instance.
pub instance_transform: Option<acadrust::types::Transform>, pub instance_transform: Option<acadrust::types::Transform>,
/// Parent INSERT selected for this rendered block instance.
pub instance_handle: Option<acadrust::Handle>,
/// Effective colour after INSERT inheritance, without copying the mesh.
pub instance_color: Option<[f32; 4]>,
/// Precise world bounds used by the interaction index.
pub instance_aabb: Option<[f64; 6]>,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -112,6 +118,7 @@ pub struct MeshInstanceSource {
pub lods: Vec<MeshModel>, pub lods: Vec<MeshModel>,
pub edge_verts: Vec<[f32; 3]>, pub edge_verts: Vec<[f32; 3]>,
pub edge_verts_low: Vec<[f32; 3]>, pub edge_verts_low: Vec<[f32; 3]>,
pub curved_gens: Vec<CurvedGen>,
} }
/// 3D bounds of every LOD's vertices: `([min_x, min_y, max_x, max_y], [min_z, max_z])`. /// 3D bounds of every LOD's vertices: `([min_x, min_y, max_x, max_y], [min_z, max_z])`.
@ -225,6 +232,9 @@ impl MeshLodSet {
z_aabb, z_aabb,
instance_source: None, instance_source: None,
instance_transform: None, instance_transform: None,
instance_handle: None,
instance_color: None,
instance_aabb: None,
} }
} }
@ -250,7 +260,35 @@ impl MeshLodSet {
lods: self.lods.clone(), lods: self.lods.clone(),
edge_verts: self.edge_verts.clone(), edge_verts: self.edge_verts.clone(),
edge_verts_low: self.edge_verts_low.clone(), edge_verts_low: self.edge_verts_low.clone(),
curved_gens: self.curved_gens.clone(),
})); }));
self.instance_transform = None; self.instance_transform = None;
self.instance_handle = None;
self.instance_color = None;
self.instance_aabb = None;
}
pub fn geometry_lods(&self) -> &[MeshModel] {
self.instance_source
.as_ref()
.map_or(self.lods.as_slice(), |source| source.lods.as_slice())
}
pub fn entity_handle(&self) -> Option<acadrust::Handle> {
self.instance_handle.or_else(|| {
self.lods
.first()
.and_then(|mesh| mesh.name.parse::<u64>().ok())
.map(acadrust::Handle::new)
})
}
pub fn display_color(&self) -> Option<[f32; 4]> {
self.instance_color.or_else(|| {
self.geometry_lods()
.iter()
.find(|mesh| !mesh.indices.is_empty())
.map(|mesh| mesh.color)
})
} }
} }

View file

@ -658,64 +658,183 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>(
hits.into_iter().map(|(_, name)| name).collect() hits.into_iter().map(|(_, name)| name).collect()
} }
pub fn mesh_click_hit<'a>( pub(crate) fn mesh_click_hit<'a>(
cursor: Point, cursor: Point,
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>, meshes: impl Iterator<
Item = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
[f64; 6],
),
>,
view_rot: Mat4, view_rot: Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: Rectangle, bounds: Rectangle,
) -> Option<Handle> { ) -> Option<Handle> {
let mut best: Option<(f32, Handle)> = None; let profile = crate::perf::enabled().then(std::time::Instant::now);
for (handle, mesh) in meshes { let mut set_count = 0usize;
let v = &mesh.verts; let mut bound_hits = 0usize;
let idx = &mesh.indices; let mut exact_triangles = 0usize;
let lo = &mesh.verts_low; let ndc = glam::Vec3::new(
// Indexed meshes reuse most vertices across several triangles. cursor.x / bounds.width * 2.0 - 1.0,
// Project once per vertex instead of three matrix transforms per 1.0 - cursor.y / bounds.height * 2.0,
// triangle; dense solid misses are otherwise the worst-case hover. 0.0,
let projected: Vec<(Point, f32)> = v );
.iter() let inverse_view = view_rot.inverse();
.enumerate() let near = eye + inverse_view.project_point3(ndc).as_dvec3();
.map(|(i, &vertex)| { let far = eye
let ndc = + inverse_view
view_rot.project_point3((mesh_vert(vertex, lo, i) - eye).as_vec3()); .project_point3(glam::Vec3::new(ndc.x, ndc.y, 1.0))
( .as_dvec3();
Point::new( let world_direction = (far - near).normalize_or_zero();
(ndc.x + 1.0) * 0.5 * bounds.width, if !near.is_finite() || !world_direction.is_finite() || world_direction.length_squared() < 1e-18
(1.0 - ndc.y) * 0.5 * bounds.height, {
return None;
}
let mut best: Option<(f64, Handle)> = None;
for (handle, mesh, transform, aabb) in meshes {
set_count += 1;
let Some((near_t, _)) = ray_aabb(near, world_direction, aabb) else {
continue;
};
bound_hits += 1;
exact_triangles += mesh.indices.len() / 3;
if best.is_some_and(|(distance, _)| near_t > distance) {
continue;
}
let model = transform.map(codec_transform_matrix);
let (origin, direction) = if let Some(model) = model {
if !model.is_finite() || model.determinant().abs() <= 1e-18 {
continue;
}
let inverse = model.inverse();
let origin = inverse.transform_point3(near);
let direction = inverse.transform_vector3(world_direction).normalize_or_zero();
(origin, direction)
} else {
(near, world_direction)
};
if direction.length_squared() < 1e-18 {
continue;
}
let local_t = mesh
.indices
.chunks_exact(3)
.filter_map(|triangle| {
ray_triangle(
origin,
direction,
mesh_vert(
mesh.verts[triangle[0] as usize],
&mesh.verts_low,
triangle[0] as usize,
),
mesh_vert(
mesh.verts[triangle[1] as usize],
&mesh.verts_low,
triangle[1] as usize,
),
mesh_vert(
mesh.verts[triangle[2] as usize],
&mesh.verts_low,
triangle[2] as usize,
), ),
ndc.z,
) )
}) })
.collect(); .min_by(f64::total_cmp);
let mut t = 0; if let Some(local_t) = local_t {
while t + 2 < idx.len() { let local_hit = origin + direction * local_t;
let tri = [idx[t] as usize, idx[t + 1] as usize, idx[t + 2] as usize]; let world_hit = model.map_or(local_hit, |model| model.transform_point3(local_hit));
t += 3; let distance = (world_hit - near).dot(world_direction);
let mut sp = [Point::ORIGIN; 3]; if distance >= 0.0 && best.is_none_or(|(current, _)| distance < current) {
let mut depth = 0.0f32; best = Some((distance, handle));
for (j, &k) in tri.iter().enumerate() {
let (point, z) = projected[k];
sp[j] = point;
depth += z;
}
if point_in_polygon(cursor, &sp) {
let d = depth / 3.0;
if best.map_or(true, |(bd, _)| d < bd) {
best = Some((d, handle));
}
break; // one hit per mesh is enough
} }
} }
} }
if let Some(started) = profile {
crate::perf_record!(
"[perf] mesh-ray {:>7.1}ms sets={} bounds={} source_triangles={}",
started.elapsed().as_secs_f64() * 1000.0,
set_count,
bound_hits,
exact_triangles,
);
}
best.map(|(_, h)| h) best.map(|(_, h)| h)
} }
fn codec_transform_matrix(transform: acadrust::types::Transform) -> glam::DMat4 {
let matrix = transform.matrix.m;
glam::DMat4::from_cols_array(&[
matrix[0][0], matrix[1][0], matrix[2][0], matrix[3][0],
matrix[0][1], matrix[1][1], matrix[2][1], matrix[3][1],
matrix[0][2], matrix[1][2], matrix[2][2], matrix[3][2],
matrix[0][3], matrix[1][3], matrix[2][3], matrix[3][3],
])
}
fn ray_aabb(origin: glam::DVec3, direction: glam::DVec3, aabb: [f64; 6]) -> Option<(f64, f64)> {
let mut near = 0.0_f64;
let mut far = f64::INFINITY;
for axis in 0..3 {
let origin = origin[axis];
let direction = direction[axis];
if direction.abs() <= 1e-18 {
if origin < aabb[axis] || origin > aabb[axis + 3] {
return None;
}
continue;
}
let first = (aabb[axis] - origin) / direction;
let second = (aabb[axis + 3] - origin) / direction;
near = near.max(first.min(second));
far = far.min(first.max(second));
if far < near {
return None;
}
}
Some((near, far))
}
fn ray_triangle(
origin: glam::DVec3,
direction: glam::DVec3,
a: glam::DVec3,
b: glam::DVec3,
c: glam::DVec3,
) -> Option<f64> {
let edge1 = b - a;
let edge2 = c - a;
let cross = direction.cross(edge2);
let determinant = edge1.dot(cross);
if determinant.abs() <= 1e-12 {
return None;
}
let inverse = determinant.recip();
let offset = origin - a;
let u = offset.dot(cross) * inverse;
if !(0.0..=1.0).contains(&u) {
return None;
}
let q = offset.cross(edge1);
let v = direction.dot(q) * inverse;
if v < 0.0 || u + v > 1.0 {
return None;
}
let distance = edge2.dot(q) * inverse;
(distance >= 0.0).then_some(distance)
}
/// Reconstruct a mesh vertex's absolute f64 position from its high/low pair — /// Reconstruct a mesh vertex's absolute f64 position from its high/low pair —
/// without the low residual the f32 high alone is ~0.5 m off at UTM scale and /// without the low residual the f32 high alone is ~0.5 m off at UTM scale and
/// box / lasso / face selection lands on the wrong place. /// box / lasso / face selection lands on the wrong place.
#[inline] #[inline]
fn mesh_vert(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 { fn mesh_vert(
hi: [f32; 3],
low: &[[f32; 3]],
i: usize,
) -> glam::DVec3 {
let l = low.get(i).copied().unwrap_or([0.0; 3]); let l = low.get(i).copied().unwrap_or([0.0; 3]);
glam::DVec3::new( glam::DVec3::new(
hi[0] as f64 + l[0] as f64, hi[0] as f64 + l[0] as f64,
@ -727,6 +846,7 @@ fn mesh_vert(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
/// Project a mesh's vertices to screen space. /// Project a mesh's vertices to screen space.
fn project_mesh_verts( fn project_mesh_verts(
mesh: &MeshModel, mesh: &MeshModel,
transform: Option<acadrust::types::Transform>,
view_rot: Mat4, view_rot: Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: Rectangle, bounds: Rectangle,
@ -735,7 +855,12 @@ fn project_mesh_verts(
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, &w)| { .map(|(i, &w)| {
let ndc = view_rot.project_point3((mesh_vert(w, &mesh.verts_low, i) - eye).as_vec3()); let point = mesh_vert(w, &mesh.verts_low, i);
let point = transform.map_or(point, |transform| {
let point = transform.apply(acadrust::types::Vector3::new(point.x, point.y, point.z));
glam::DVec3::new(point.x, point.y, point.z)
});
let ndc = view_rot.project_point3((point - eye).as_vec3());
Point::new( Point::new(
(ndc.x + 1.0) * 0.5 * bounds.width, (ndc.x + 1.0) * 0.5 * bounds.width,
(1.0 - ndc.y) * 0.5 * bounds.height, (1.0 - ndc.y) * 0.5 * bounds.height,
@ -769,7 +894,13 @@ pub fn mesh_box_hit<'a>(
a: Point, a: Point,
b: Point, b: Point,
crossing: bool, crossing: bool,
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>, meshes: impl Iterator<
Item = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
),
>,
view_rot: Mat4, view_rot: Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: Rectangle, bounds: Rectangle,
@ -784,8 +915,8 @@ pub fn mesh_box_hit<'a>(
Point::new(min_x, max_y), Point::new(min_x, max_y),
]; ];
let mut out = Vec::new(); let mut out = Vec::new();
for (h, mesh) in meshes { for (h, mesh, transform) in meshes {
let proj = project_mesh_verts(mesh, view_rot, eye, bounds); let proj = project_mesh_verts(mesh, transform, view_rot, eye, bounds);
if proj.is_empty() { if proj.is_empty() {
continue; continue;
} }
@ -807,7 +938,13 @@ pub fn mesh_box_hit<'a>(
pub fn mesh_poly_hit<'a>( pub fn mesh_poly_hit<'a>(
poly: &[Point], poly: &[Point],
crossing: bool, crossing: bool,
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>, meshes: impl Iterator<
Item = (
Handle,
&'a MeshModel,
Option<acadrust::types::Transform>,
),
>,
view_rot: Mat4, view_rot: Mat4,
eye: glam::DVec3, eye: glam::DVec3,
bounds: Rectangle, bounds: Rectangle,
@ -816,8 +953,8 @@ pub fn mesh_poly_hit<'a>(
return Vec::new(); return Vec::new();
} }
let mut out = Vec::new(); let mut out = Vec::new();
for (h, mesh) in meshes { for (h, mesh, transform) in meshes {
let proj = project_mesh_verts(mesh, view_rot, eye, bounds); let proj = project_mesh_verts(mesh, transform, view_rot, eye, bounds);
if proj.is_empty() { if proj.is_empty() {
continue; continue;
} }

View file

@ -975,8 +975,9 @@ fn mesh_spatial_key(set: &MeshLodSet, bounds: [f32; 6]) -> u64 {
struct MeshBatchPart<'a> { struct MeshBatchPart<'a> {
set: &'a MeshLodSet, set: &'a MeshLodSet,
mesh: &'a MeshModel, mesh: &'a MeshModel,
display_mesh: &'a MeshModel,
uv_mesh: &'a MeshModel, uv_mesh: &'a MeshModel,
entity_handle: Option<acadrust::Handle>,
display_color: [f32; 4],
material: Option<&'a crate::scene::model::material_model::MeshMaterial>, material: Option<&'a crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4], color: [f32; 4],
indices: std::sync::Arc<[u32]>, indices: std::sync::Arc<[u32]>,
@ -1157,9 +1158,7 @@ fn build_instanced_chunk(
.set .set
.visual_style .visual_style
.as_ref() .as_ref()
.map_or(first.display_mesh.color, |style| { .map_or(first.display_color, |style| style.edge_color(first.display_color));
style.edge_color(first.display_mesh.color)
});
let edge_key = (source_handle, edge_color.map(f32::to_bits)); let edge_key = (source_handle, edge_color.map(f32::to_bits));
let shared_edge_buffer = first let shared_edge_buffer = first
.include_edges .include_edges
@ -1229,13 +1228,7 @@ fn build_instanced_chunk(
bounds[3] = bounds[3].max(part.set.world_aabb[2]); bounds[3] = bounds[3].max(part.set.world_aabb[2]);
bounds[4] = bounds[4].max(part.set.world_aabb[3]); bounds[4] = bounds[4].max(part.set.world_aabb[3]);
bounds[5] = bounds[5].max(part.set.z_aabb[1]); bounds[5] = bounds[5].max(part.set.z_aabb[1]);
if let Some(handle) = part if let Some(handle) = part.entity_handle {
.display_mesh
.name
.parse::<u64>()
.ok()
.map(acadrust::Handle::new)
{
handles.insert(handle); handles.insert(handle);
if first.include_faces { if first.include_faces {
highlights.push(MeshBatchRange { highlights.push(MeshBatchRange {
@ -1332,24 +1325,6 @@ fn mesh_bounds(mesh: &MeshModel) -> [f32; 6] {
bounds bounds
} }
fn material_uses_model_mapper(
material: Option<&crate::scene::model::material_model::MeshMaterial>,
) -> bool {
material.is_some_and(|material| {
[
&material.diffuse_map,
&material.specular_map,
&material.reflection_map,
&material.opacity_map,
&material.bump_map,
&material.refraction_map,
&material.normal_map,
]
.into_iter()
.any(|map| map.image.is_some() && map.auto_transform & 4 != 0)
})
}
fn material_is_transparent( fn material_is_transparent(
material: Option<&crate::scene::model::material_model::MeshMaterial>, material: Option<&crate::scene::model::material_model::MeshMaterial>,
color: [f32; 4], color: [f32; 4],
@ -1557,34 +1532,30 @@ pub fn build_mesh_batch_filtered(
let mut face_partitions: rustc_hash::FxHashMap<FacePartitionKey, Vec<CachedFacePart<'_>>> = let mut face_partitions: rustc_hash::FxHashMap<FacePartitionKey, Vec<CachedFacePart<'_>>> =
rustc_hash::FxHashMap::default(); rustc_hash::FxHashMap::default();
for set in sets { for set in sets {
let Some(display_mesh) = set.lods.iter().find(|mesh| !mesh.indices.is_empty()) else { let Some(mesh) = set
.geometry_lods()
.iter()
.find(|mesh| !mesh.indices.is_empty())
else {
continue; continue;
}; };
let display_handle = display_mesh let display_handle = set.entity_handle();
.name
.parse::<u64>()
.ok()
.map(acadrust::Handle::new);
if handles.is_some_and(|wanted| { if handles.is_some_and(|wanted| {
display_handle.is_none_or(|handle| !wanted.contains(&handle)) display_handle.is_none_or(|handle| !wanted.contains(&handle))
}) { }) {
continue; continue;
} }
let source_mesh = set let display_color = set.display_color().unwrap_or(mesh.color);
.instance_source
.as_ref()
.and_then(|source| source.lods.iter().find(|mesh| !mesh.indices.is_empty()));
let mesh = source_mesh.unwrap_or(display_mesh);
let source_identity = set.instance_source.as_ref().map_or_else( let source_identity = set.instance_source.as_ref().map_or_else(
|| (false, display_mesh as *const MeshModel as usize as u64), || (false, mesh as *const MeshModel as usize as u64),
|source| (true, source.handle.value()), |source| (true, source.handle.value()),
); );
let triangle_count = display_mesh.indices.len() / 3; let triangle_count = mesh.indices.len() / 3;
let has_face_materials = let has_face_materials =
display_mesh.triangle_material_handles.len() == triangle_count mesh.triangle_material_handles.len() == triangle_count
&& !set.face_materials.is_empty(); && !set.face_materials.is_empty();
let has_face_colors = display_mesh.triangle_colors.len() == triangle_count let has_face_colors = mesh.triangle_colors.len() == triangle_count
&& display_mesh.triangle_colors.iter().any(Option::is_some); && mesh.triangle_colors.iter().any(Option::is_some);
let include_faces = set let include_faces = set
.visual_style .visual_style
.as_ref() .as_ref()
@ -1595,7 +1566,7 @@ pub fn build_mesh_batch_filtered(
.map_or(true, |style| style.edges_visible()); .map_or(true, |style| style.edges_visible());
if !has_face_materials && !has_face_colors { if !has_face_materials && !has_face_colors {
let base_color = let base_color =
set.material.as_ref().map_or(display_mesh.color, |material| material.diffuse); set.material.as_ref().map_or(display_color, |material| material.diffuse);
let color = set let color = set
.visual_style .visual_style
.as_ref() .as_ref()
@ -1603,7 +1574,7 @@ pub fn build_mesh_batch_filtered(
let (shared_indices, shared_hash) = source_indices let (shared_indices, shared_hash) = source_indices
.entry(source_identity) .entry(source_identity)
.or_insert_with(|| { .or_insert_with(|| {
let indices = std::sync::Arc::<[u32]>::from(display_mesh.indices.as_slice()); let indices = std::sync::Arc::<[u32]>::from(mesh.indices.as_slice());
let hash = index_hash(indices.as_ref()); let hash = index_hash(indices.as_ref());
(indices, hash) (indices, hash)
}) })
@ -1611,8 +1582,9 @@ pub fn build_mesh_batch_filtered(
ordered.push(MeshBatchPart { ordered.push(MeshBatchPart {
set, set,
mesh, mesh,
display_mesh,
uv_mesh: mesh, uv_mesh: mesh,
entity_handle: display_handle,
display_color,
material: set.material.as_ref(), material: set.material.as_ref(),
color, color,
indices: shared_indices, indices: shared_indices,
@ -1637,10 +1609,10 @@ pub fn build_mesh_batch_filtered(
let partition_key = FacePartitionKey { let partition_key = FacePartitionKey {
instanced: source_identity.0, instanced: source_identity.0,
source: source_identity.1, source: source_identity.1,
base_material: material_key(set.material.as_ref(), display_mesh.color), base_material: material_key(set.material.as_ref(), display_color),
face_materials, face_materials,
visual_style: visual_style_partition_key(set.visual_style.as_ref()), visual_style: visual_style_partition_key(set.visual_style.as_ref()),
display_color: display_mesh.color.map(f32::to_bits), display_color: display_color.map(f32::to_bits),
include_faces, include_faces,
include_edges, include_edges,
}; };
@ -1655,18 +1627,18 @@ pub fn build_mesh_batch_filtered(
Vec<u32>, Vec<u32>,
), ),
> = std::collections::BTreeMap::new(); > = std::collections::BTreeMap::new();
for (triangle, indices) in display_mesh.indices.chunks_exact(3).enumerate() { for (triangle, indices) in mesh.indices.chunks_exact(3).enumerate() {
let material = if has_face_materials { let material = if has_face_materials {
display_mesh.triangle_material_handles[triangle] mesh.triangle_material_handles[triangle]
.and_then(|handle| set.face_materials.get(&handle)) .and_then(|handle| set.face_materials.get(&handle))
.or(set.material.as_ref()) .or(set.material.as_ref())
} else { } else {
set.material.as_ref() set.material.as_ref()
}; };
let base_color = let base_color =
material.map_or(display_mesh.color, |material| material.diffuse); material.map_or(display_color, |material| material.diffuse);
let base_color = if has_face_colors { let base_color = if has_face_colors {
display_mesh.triangle_colors[triangle].unwrap_or(base_color) mesh.triangle_colors[triangle].unwrap_or(base_color)
} else { } else {
base_color base_color
}; };
@ -1700,8 +1672,9 @@ pub fn build_mesh_batch_filtered(
ordered.push(MeshBatchPart { ordered.push(MeshBatchPart {
set, set,
mesh, mesh,
display_mesh,
uv_mesh: mesh, uv_mesh: mesh,
entity_handle: display_handle,
display_color,
material: part.material, material: part.material,
color: part.color, color: part.color,
indices: part.indices, indices: part.indices,
@ -1738,25 +1711,34 @@ pub fn build_mesh_batch_filtered(
mesh_spatial_key(part.set, spatial_bounds), mesh_spatial_key(part.set, spatial_bounds),
) )
}); });
let storage_instancing = let storage_instancing = device.limits().max_storage_buffers_per_shader_stage > 0;
device.limits().max_storage_buffers_per_shader_stage > 0;
let mut instance_groups: std::collections::BTreeMap< let mut instance_groups: std::collections::BTreeMap<
InstanceGroupKey, InstanceGroupKey,
Vec<MeshBatchPart<'_>>, Vec<MeshBatchPart<'_>>,
> = std::collections::BTreeMap::new(); > = std::collections::BTreeMap::new();
let mut direct_parts = Vec::with_capacity(ordered.len()); let mut direct_parts = Vec::with_capacity(ordered.len());
for mut part in ordered { for part in ordered {
let eligible = storage_instancing let eligible = part.set.instance_transform.is_some()
&& part.set.instance_transform.is_some()
&& part.set.instance_source.is_some() && part.set.instance_source.is_some()
&& !material_uses_model_mapper(part.material) && part
&& part.mesh.verts.len() <= max_verts .mesh
&& part.indices.len() / 3 <= max_tris .verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
<= hard_budget
&& part.indices.len().saturating_mul(2 * std::mem::size_of::<u32>())
<= hard_budget
&& part && part
.set .set
.instance_source .instance_source
.as_ref() .as_ref()
.is_some_and(|source| source.edge_verts.len() <= max_verts) .is_some_and(|source| {
source
.edge_verts
.len()
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
<= hard_budget
})
&& part && part
.indices .indices
.iter() .iter()
@ -1782,46 +1764,9 @@ pub fn build_mesh_batch_filtered(
.or_default() .or_default()
.push(part); .push(part);
} else { } else {
// Compatibility and non-instanced meshes keep their already
// transformed display vertices in the ordinary static batch.
part.mesh = part.display_mesh;
direct_parts.push(part); direct_parts.push(part);
} }
} }
let sparse_groups: Vec<_> = instance_groups
.iter()
.filter_map(|(key, parts)| {
let first = parts.first()?;
let geometry_bytes = first
.mesh
.verts
.len()
.saturating_mul(std::mem::size_of::<MeshVertex>())
.saturating_add(first.indices.len().saturating_mul(12))
.saturating_add(
first
.set
.instance_source
.as_ref()
.map_or(0, |source| {
source
.edge_verts
.len()
.saturating_mul(std::mem::size_of::<MeshEdgeVertex>())
}),
);
let saved_bytes = geometry_bytes.saturating_mul(parts.len().saturating_sub(1));
(parts.len() < 4 && saved_bytes < 512 * 1024).then_some(*key)
})
.collect();
for key in sparse_groups {
if let Some(parts) = instance_groups.remove(&key) {
for mut part in parts {
part.mesh = part.display_mesh;
direct_parts.push(part);
}
}
}
direct_parts.sort_by_key(|part| { direct_parts.sort_by_key(|part| {
( (
material_key(part.material, part.color), material_key(part.material, part.color),
@ -1838,12 +1783,7 @@ pub fn build_mesh_batch_filtered(
let mesh = part.mesh; let mesh = part.mesh;
let material = part.material; let material = part.material;
let part_color = part.color; let part_color = part.color;
let entity_handle = part let entity_handle = part.entity_handle;
.display_mesh
.name
.parse::<u64>()
.ok()
.map(acadrust::Handle::new);
let key = material_key(material, part_color); let key = material_key(material, part_color);
if active_key.is_some_and(|active| active != key) if active_key.is_some_and(|active| active != key)
&& (!verts.is_empty() || !edge_verts.is_empty()) && (!verts.is_empty() || !edge_verts.is_empty())
@ -1887,7 +1827,7 @@ pub fn build_mesh_batch_filtered(
let edge_color = set let edge_color = set
.visual_style .visual_style
.as_ref() .as_ref()
.map_or(mesh.color, |style| style.edge_color(mesh.color)); .map_or(part.display_color, |style| style.edge_color(part.display_color));
let vtx = |vi: usize| { let vtx = |vi: usize| {
let normal = if has_normals { let normal = if has_normals {
mesh.normals[vi] mesh.normals[vi]
@ -2259,10 +2199,11 @@ pub fn build_mesh_batch_filtered(
// granularity. Small block definitions get spatial clusters of roughly // granularity. Small block definitions get spatial clusters of roughly
// 64–256 INSERTs; a huge source is duplicated only a few times. // 64–256 INSERTs; a huge source is duplicated only a few times.
let max_clusters = ((64 * 1024 * 1024) / source_bytes).clamp(1, 64); let max_clusters = ((64 * 1024 * 1024) / source_bytes).clamp(1, 64);
let cluster_len = parts let cluster_len = if storage_instancing {
.len() parts.len().div_ceil(max_clusters).max(64)
.div_ceil(max_clusters) } else {
.max(64); 1
};
for cluster in parts.chunks(cluster_len) { for cluster in parts.chunks(cluster_len) {
if let Some((chunk, triangles, profile)) = if let Some((chunk, triangles, profile)) =
build_instanced_chunk( build_instanced_chunk(

View file

@ -2694,11 +2694,7 @@ impl Pipeline {
let mut slots = rustc_hash::FxHashMap::default(); let mut slots = rustc_hash::FxHashMap::default();
let mut groups: Vec<Vec<&crate::scene::model::mesh_model::MeshLodSet>> = Vec::new(); let mut groups: Vec<Vec<&crate::scene::model::mesh_model::MeshLodSet>> = Vec::new();
for (index, set) in sets.iter().enumerate() { for (index, set) in sets.iter().enumerate() {
let color = set let color = set.display_color().unwrap_or([0.0, 0.0, 0.0, 1.0]);
.lods
.first()
.map(|mesh| mesh.color)
.unwrap_or([0.0, 0.0, 0.0, 1.0]);
let key = match (&set.instance_source, set.instance_transform) { let key = match (&set.instance_source, set.instance_transform) {
(Some(source), Some(transform)) => { (Some(source), Some(transform)) => {
let matrix = &transform.matrix.m; let matrix = &transform.matrix.m;
@ -2771,11 +2767,7 @@ impl Pipeline {
) )
}) })
.collect(); .collect();
let color = source let color = source.display_color().unwrap_or([0.0, 0.0, 0.0, 1.0]);
.lods
.first()
.map(|mesh| mesh.color)
.unwrap_or([0.0, 0.0, 0.0, 1.0]);
let mk = |w: glam::DVec3| -> SilhouetteVertex { let mk = |w: glam::DVec3| -> SilhouetteVertex {
let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32); let (hx, hy, hz) = (w.x as f32, w.y as f32, w.z as f32);
SilhouetteVertex { SilhouetteVertex {
@ -2813,7 +2805,13 @@ impl Pipeline {
}); });
} }
}; };
for generator in &source.curved_gens { let generators = source
.instance_source
.as_ref()
.map_or(source.curved_gens.as_slice(), |instance| {
instance.curved_gens.as_slice()
});
for generator in generators {
let transformed; let transformed;
let silhouette_source = if let Some(transform) = source.instance_transform { let silhouette_source = if let Some(transform) = source.instance_transform {
let origin = transform.apply(acadrust::types::Vector3::ZERO); let origin = transform.apply(acadrust::types::Vector3::ZERO);
@ -3259,12 +3257,7 @@ impl Pipeline {
} }
let current_handles: rustc_hash::FxHashSet<_> = meshes let current_handles: rustc_hash::FxHashSet<_> = meshes
.iter() .iter()
.filter_map(|set| { .filter_map(MeshLodSet::entity_handle)
set.lods
.first()
.and_then(|mesh| mesh.name.parse::<u64>().ok())
.map(acadrust::Handle::new)
})
.collect(); .collect();
let changed: rustc_hash::FxHashSet<_> = changes let changed: rustc_hash::FxHashSet<_> = changes
.iter() .iter()