perf(scene): multi-resolution mesh LOD (Phase 3.4)
ACIS SAT entities (Solid3D / Region / Body) were tessellated at one
fixed sampling density (CIRC_SEGS=48, GRID_U=32, GRID_V=16) regardless
of how far they were from the camera. Distant solids paid the full
triangle bill for fragments that mostly resolved to sub-pixel size.
Generate three pre-built LODs per entity at load time:
* LOD 0 (HIGH) — 48/32/16 segments. Use above 200 px projected diag.
* LOD 1 (MID) — 24/16/8. Use 50–200 px.
* LOD 2 (LOW) — 12/8/4. Use below 50 px.
Tessellation cost is paid 3× on load (cheap — typical files have few
solids). Render picks per-frame via `compute_mesh_lod` from each mesh's
`world_aabb` projected diagonal. When a level is missing (e.g. a solid
entity that only emits LOD 0), the selector walks down to the nearest
available slot.
API ripples:
* New `LodConfig` (HIGH/MID/LOW) drives `tess_sat`, `tess_cone_face`,
`tess_sphere_face`, `tess_torus_face`.
* `tessellate_solid3d / _region / _body` now return `MeshLodSet`.
* `MeshLodSet { lods: Vec<MeshModel>, world_aabb }` + `from_single`
constructor so interactive truck-based commands (BOX/CYLINDER/etc.)
that only generate one tessellation stay one-line.
* GPU side gets `MeshLodGpu { lods: Vec<MeshGpu>, world_aabb }` and
a `mesh_lod_levels` per-frame index vector.
* STL/STEP export pulls slot 0 explicitly so the on-disk geometry
isn't downgraded by the view-dependent ladder.
Background-thread lazy generation (per the roadmap) is deferred — the
synchronous eager build is fine for typical CAD files. Switch to a
queue + background tessellator when files with hundreds of solids
become a load-time bottleneck.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
e7675be0bc
commit
8c08ae104a
8 changed files with 321 additions and 59 deletions
|
|
@ -1066,7 +1066,7 @@ impl H7CAD {
|
|||
let color = [0.6f32, 0.6, 0.8, 1.0]; // default colour; command embedded it
|
||||
let _ = color; // color is captured inside mesh_fn
|
||||
if let Some(mesh) = mesh_fn(name) {
|
||||
self.tabs[i].scene.meshes.insert(handle, mesh);
|
||||
self.tabs[i].scene.meshes.insert(handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output("Solid created.");
|
||||
|
|
@ -1126,7 +1126,7 @@ impl H7CAD {
|
|||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
self.tabs[i].scene.meshes.insert(new_handle, mesh);
|
||||
self.tabs[i].scene.meshes.insert(new_handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output("EXTRUDE: solid created.");
|
||||
} else {
|
||||
|
|
@ -1201,7 +1201,7 @@ impl H7CAD {
|
|||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
self.tabs[i].scene.meshes.insert(new_handle, mesh);
|
||||
self.tabs[i].scene.meshes.insert(new_handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(&format!("REVOLVE: solid created ({:.0}°).", angle_deg));
|
||||
|
|
@ -1359,7 +1359,7 @@ impl H7CAD {
|
|||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
self.tabs[i].scene.meshes.insert(new_handle, mesh);
|
||||
self.tabs[i].scene.meshes.insert(new_handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output("SWEEP: solid created.");
|
||||
} else {
|
||||
|
|
@ -1443,7 +1443,7 @@ impl H7CAD {
|
|||
let new_entity = empty_solid3d();
|
||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||
mesh.name = format!("{}", new_handle.value());
|
||||
self.tabs[i].scene.meshes.insert(new_handle, mesh);
|
||||
self.tabs[i].scene.meshes.insert(new_handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"LOFT: solid created from {} profiles.",
|
||||
|
|
|
|||
|
|
@ -382,8 +382,15 @@ impl H7CAD {
|
|||
Message::StlExportPath(Some(path)) => {
|
||||
// Re-build STL bytes (we can't easily pass them through the message).
|
||||
let i = self.active_tab;
|
||||
let meshes: Vec<crate::scene::mesh_model::MeshModel> =
|
||||
self.tabs[i].scene.meshes.values().cloned().collect();
|
||||
// STL gets the highest-resolution LOD (slot 0) so the
|
||||
// exported geometry isn't downgraded by the view-dependent
|
||||
// mesh LOD ladder used for rendering.
|
||||
let meshes: Vec<crate::scene::mesh_model::MeshModel> = self.tabs[i]
|
||||
.scene
|
||||
.meshes
|
||||
.values()
|
||||
.filter_map(|s| s.lods.first().cloned())
|
||||
.collect();
|
||||
let mesh_refs: Vec<&crate::scene::mesh_model::MeshModel> = meshes.iter().collect();
|
||||
match crate::io::stl::build_stl(&mesh_refs) {
|
||||
Some(bytes) => match std::fs::write(&path, bytes) {
|
||||
|
|
@ -428,8 +435,13 @@ impl H7CAD {
|
|||
|
||||
Message::StepExportPath(Some(path)) => {
|
||||
let i = self.active_tab;
|
||||
let meshes: Vec<crate::scene::mesh_model::MeshModel> =
|
||||
self.tabs[i].scene.meshes.values().cloned().collect();
|
||||
// Export uses LOD 0 (full resolution); see StlExportPath above.
|
||||
let meshes: Vec<crate::scene::mesh_model::MeshModel> = self.tabs[i]
|
||||
.scene
|
||||
.meshes
|
||||
.values()
|
||||
.filter_map(|s| s.lods.first().cloned())
|
||||
.collect();
|
||||
let mesh_refs: Vec<&crate::scene::mesh_model::MeshModel> = meshes.iter().collect();
|
||||
match crate::io::step::build_step(&mesh_refs) {
|
||||
Some(text) => match std::fs::write(&path, text.as_bytes()) {
|
||||
|
|
@ -490,7 +502,10 @@ impl H7CAD {
|
|||
let entity = empty_solid3d();
|
||||
let handle = self.tabs[i].scene.add_entity(entity);
|
||||
if !handle.is_null() {
|
||||
self.tabs[i].scene.meshes.insert(handle, mesh);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.meshes
|
||||
.insert(handle, crate::scene::MeshLodSet::from_single(mesh));
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"IMPORTOBJ: imported \"{}\" as mesh.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue