forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
e7675be0bc
commit
8c08ae104a
8 files changed
+321
-59
No files matched your search
@@ -1066,7 +1066,7 @@ impl H7CAD {
|
|||||||
let color = [0.6f32, 0.6, 0.8, 1.0]; // default colour; command embedded it
|
let color = [0.6f32, 0.6, 0.8, 1.0]; // default colour; command embedded it
|
||||||
let _ = color; // color is captured inside mesh_fn
|
let _ = color; // color is captured inside mesh_fn
|
||||||
if let Some(mesh) = mesh_fn(name) {
|
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.tabs[i].dirty = true;
|
||||||
self.command_line.push_output("Solid created.");
|
self.command_line.push_output("Solid created.");
|
||||||
@@ -1126,7 +1126,7 @@ impl H7CAD {
|
|||||||
let new_entity = empty_solid3d();
|
let new_entity = empty_solid3d();
|
||||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||||
mesh.name = format!("{}", new_handle.value());
|
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.tabs[i].dirty = true;
|
||||||
self.command_line.push_output("EXTRUDE: solid created.");
|
self.command_line.push_output("EXTRUDE: solid created.");
|
||||||
} else {
|
} else {
|
||||||
@@ -1201,7 +1201,7 @@ impl H7CAD {
|
|||||||
let new_entity = empty_solid3d();
|
let new_entity = empty_solid3d();
|
||||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||||
mesh.name = format!("{}", new_handle.value());
|
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.tabs[i].dirty = true;
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(&format!("REVOLVE: solid created ({:.0}°).", angle_deg));
|
.push_output(&format!("REVOLVE: solid created ({:.0}°).", angle_deg));
|
||||||
@@ -1359,7 +1359,7 @@ impl H7CAD {
|
|||||||
let new_entity = empty_solid3d();
|
let new_entity = empty_solid3d();
|
||||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||||
mesh.name = format!("{}", new_handle.value());
|
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.tabs[i].dirty = true;
|
||||||
self.command_line.push_output("SWEEP: solid created.");
|
self.command_line.push_output("SWEEP: solid created.");
|
||||||
} else {
|
} else {
|
||||||
@@ -1443,7 +1443,7 @@ impl H7CAD {
|
|||||||
let new_entity = empty_solid3d();
|
let new_entity = empty_solid3d();
|
||||||
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
let new_handle = self.tabs[i].scene.add_entity(new_entity);
|
||||||
mesh.name = format!("{}", new_handle.value());
|
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.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"LOFT: solid created from {} profiles.",
|
"LOFT: solid created from {} profiles.",
|
||||||
|
|||||||
+20
-5
@@ -382,8 +382,15 @@ impl H7CAD {
|
|||||||
Message::StlExportPath(Some(path)) => {
|
Message::StlExportPath(Some(path)) => {
|
||||||
// Re-build STL bytes (we can't easily pass them through the message).
|
// Re-build STL bytes (we can't easily pass them through the message).
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let meshes: Vec<crate::scene::mesh_model::MeshModel> =
|
// STL gets the highest-resolution LOD (slot 0) so the
|
||||||
self.tabs[i].scene.meshes.values().cloned().collect();
|
// 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();
|
let mesh_refs: Vec<&crate::scene::mesh_model::MeshModel> = meshes.iter().collect();
|
||||||
match crate::io::stl::build_stl(&mesh_refs) {
|
match crate::io::stl::build_stl(&mesh_refs) {
|
||||||
Some(bytes) => match std::fs::write(&path, bytes) {
|
Some(bytes) => match std::fs::write(&path, bytes) {
|
||||||
@@ -428,8 +435,13 @@ impl H7CAD {
|
|||||||
|
|
||||||
Message::StepExportPath(Some(path)) => {
|
Message::StepExportPath(Some(path)) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let meshes: Vec<crate::scene::mesh_model::MeshModel> =
|
// Export uses LOD 0 (full resolution); see StlExportPath above.
|
||||||
self.tabs[i].scene.meshes.values().cloned().collect();
|
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();
|
let mesh_refs: Vec<&crate::scene::mesh_model::MeshModel> = meshes.iter().collect();
|
||||||
match crate::io::step::build_step(&mesh_refs) {
|
match crate::io::step::build_step(&mesh_refs) {
|
||||||
Some(text) => match std::fs::write(&path, text.as_bytes()) {
|
Some(text) => match std::fs::write(&path, text.as_bytes()) {
|
||||||
@@ -490,7 +502,10 @@ impl H7CAD {
|
|||||||
let entity = empty_solid3d();
|
let entity = empty_solid3d();
|
||||||
let handle = self.tabs[i].scene.add_entity(entity);
|
let handle = self.tabs[i].scene.add_entity(entity);
|
||||||
if !handle.is_null() {
|
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.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!(
|
self.command_line.push_output(&format!(
|
||||||
"IMPORTOBJ: imported \"{}\" as mesh.",
|
"IMPORTOBJ: imported \"{}\" as mesh.",
|
||||||
|
|||||||
@@ -26,3 +26,50 @@ impl MeshModel {
|
|||||||
pub const WHITE: [f32; 4] = [1.00, 1.00, 1.00, 1.0];
|
pub const WHITE: [f32; 4] = [1.00, 1.00, 1.00, 1.0];
|
||||||
pub const SELECTED: [f32; 4] = [0.15, 0.55, 1.00, 1.0];
|
pub const SELECTED: [f32; 4] = [0.15, 0.55, 1.00, 1.0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bundle of mesh tessellations at different sampling densities, picked
|
||||||
|
/// per frame by the render pipeline based on the projected pixel size of
|
||||||
|
/// `world_aabb`. Phase 3.4 LOD ladder:
|
||||||
|
///
|
||||||
|
/// | LOD | Source | Use when projected diagonal |
|
||||||
|
/// |-----|------------|------------------------------|
|
||||||
|
/// | 0 | HIGH | > 200 px |
|
||||||
|
/// | 1 | MID (½) | 50–200 px |
|
||||||
|
/// | 2 | LOW (¼) | < 50 px |
|
||||||
|
///
|
||||||
|
/// `lods` holds up to one MeshModel per LOD level (high → low). Empty
|
||||||
|
/// slots fall back to the nearest available LOD at render time.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MeshLodSet {
|
||||||
|
pub lods: Vec<MeshModel>,
|
||||||
|
/// World XY AABB `[min_x, min_y, max_x, max_y]` of the mesh — used
|
||||||
|
/// by the per-frame LOD selector to compute the projected pixel
|
||||||
|
/// diagonal.
|
||||||
|
pub world_aabb: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeshLodSet {
|
||||||
|
/// Wrap a single MeshModel as a one-LOD set. Used by interactive
|
||||||
|
/// commands that only produce one tessellation (e.g. truck-based
|
||||||
|
/// BOX/CYLINDER creation). The LOD selector will pick slot 0 for
|
||||||
|
/// every zoom level.
|
||||||
|
pub fn from_single(mesh: MeshModel) -> Self {
|
||||||
|
let mut min_x = f32::INFINITY;
|
||||||
|
let mut min_y = f32::INFINITY;
|
||||||
|
let mut max_x = f32::NEG_INFINITY;
|
||||||
|
let mut max_y = f32::NEG_INFINITY;
|
||||||
|
for &[x, y, _] in &mesh.verts {
|
||||||
|
if !x.is_finite() || !y.is_finite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if x < min_x { min_x = x; }
|
||||||
|
if y < min_y { min_y = y; }
|
||||||
|
if x > max_x { max_x = x; }
|
||||||
|
if y > max_y { max_y = y; }
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
lods: vec![mesh],
|
||||||
|
world_aabb: [min_x, min_y, max_x, max_y],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-10
@@ -27,6 +27,8 @@ use camera::Camera;
|
|||||||
pub use camera::Projection;
|
pub use camera::Projection;
|
||||||
pub use hatch_model::HatchModel;
|
pub use hatch_model::HatchModel;
|
||||||
pub use image_model::ImageModel;
|
pub use image_model::ImageModel;
|
||||||
|
pub use mesh_model::MeshLodSet;
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub use mesh_model::MeshModel;
|
pub use mesh_model::MeshModel;
|
||||||
pub use object::{GripApply, GripDef};
|
pub use object::{GripApply, GripDef};
|
||||||
pub use pipeline::uniforms::Uniforms;
|
pub use pipeline::uniforms::Uniforms;
|
||||||
@@ -69,7 +71,7 @@ pub struct DerivedCaches {
|
|||||||
pub local_extent_max: f32,
|
pub local_extent_max: f32,
|
||||||
pub hatches: HashMap<Handle, HatchModel>,
|
pub hatches: HashMap<Handle, HatchModel>,
|
||||||
pub images: HashMap<Handle, ImageModel>,
|
pub images: HashMap<Handle, ImageModel>,
|
||||||
pub meshes: HashMap<Handle, MeshModel>,
|
pub meshes: HashMap<Handle, MeshLodSet>,
|
||||||
/// Number of entities removed by the corrupt-entity guard during load.
|
/// Number of entities removed by the corrupt-entity guard during load.
|
||||||
/// Reported back to the UI so the user knows when a file had parser-junk
|
/// Reported back to the UI so the user knows when a file had parser-junk
|
||||||
/// entities silently dropped.
|
/// entities silently dropped.
|
||||||
@@ -177,7 +179,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// meshes (parallel tessellation)
|
// meshes (parallel tessellation)
|
||||||
let meshes: HashMap<Handle, MeshModel> = mesh_handles
|
let meshes: HashMap<Handle, MeshLodSet> = mesh_handles
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.filter_map(|&handle| {
|
.filter_map(|&handle| {
|
||||||
let e = doc.get_entity(handle)?;
|
let e = doc.get_entity(handle)?;
|
||||||
@@ -414,7 +416,7 @@ pub struct Scene {
|
|||||||
/// Cached image models, keyed by geometry_epoch.
|
/// Cached image models, keyed by geometry_epoch.
|
||||||
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
|
image_cache: RefCell<Option<(u64, Arc<Vec<ImageModel>>)>>,
|
||||||
/// Cached mesh models, keyed by geometry_epoch.
|
/// Cached mesh models, keyed by geometry_epoch.
|
||||||
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshModel>>)>>,
|
mesh_cache: RefCell<Option<(u64, Arc<Vec<MeshLodSet>>)>>,
|
||||||
/// Per-viewport wire cache for paper-space rendering.
|
/// Per-viewport wire cache for paper-space rendering.
|
||||||
/// Maps vp_handle → (geometry_epoch, Arc<Vec<WireModel>>).
|
/// Maps vp_handle → (geometry_epoch, Arc<Vec<WireModel>>).
|
||||||
viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>,
|
viewport_wire_cache: RefCell<HashMap<Handle, (u64, Arc<Vec<WireModel>>)>>,
|
||||||
@@ -436,7 +438,7 @@ pub struct Scene {
|
|||||||
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
|
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
|
||||||
pub hatches: HashMap<Handle, HatchModel>,
|
pub hatches: HashMap<Handle, HatchModel>,
|
||||||
/// GPU render data for solid meshes (truck Shell/Solid tessellation).
|
/// GPU render data for solid meshes (truck Shell/Solid tessellation).
|
||||||
pub meshes: HashMap<Handle, MeshModel>,
|
pub meshes: HashMap<Handle, MeshLodSet>,
|
||||||
/// GPU render data for raster images (RasterImage entities), keyed by handle.
|
/// GPU render data for raster images (RasterImage entities), keyed by handle.
|
||||||
pub images: HashMap<Handle, ImageModel>,
|
pub images: HashMap<Handle, ImageModel>,
|
||||||
/// The viewport that is currently "entered" (MSPACE mode).
|
/// The viewport that is currently "entered" (MSPACE mode).
|
||||||
@@ -1097,7 +1099,7 @@ impl Scene {
|
|||||||
arc
|
arc
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn meshes_arc(&self) -> Arc<Vec<MeshModel>> {
|
pub(super) fn meshes_arc(&self) -> Arc<Vec<MeshLodSet>> {
|
||||||
{
|
{
|
||||||
let cache = self.mesh_cache.borrow();
|
let cache = self.mesh_cache.borrow();
|
||||||
if let Some((cached_epoch, ref arc)) = *cache {
|
if let Some((cached_epoch, ref arc)) = *cache {
|
||||||
@@ -3163,12 +3165,18 @@ impl Scene {
|
|||||||
let dx = (new[0] - old[0]) as f32;
|
let dx = (new[0] - old[0]) as f32;
|
||||||
let dy = (new[1] - old[1]) as f32;
|
let dy = (new[1] - old[1]) as f32;
|
||||||
let dz = (new[2] - old[2]) as f32;
|
let dz = (new[2] - old[2]) as f32;
|
||||||
if let Some(mesh) = self.meshes.get_mut(&handle) {
|
if let Some(set) = self.meshes.get_mut(&handle) {
|
||||||
for v in &mut mesh.verts {
|
for lod in &mut set.lods {
|
||||||
v[0] += dx;
|
for v in &mut lod.verts {
|
||||||
v[1] += dy;
|
v[0] += dx;
|
||||||
v[2] += dz;
|
v[1] += dy;
|
||||||
|
v[2] += dz;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
set.world_aabb[0] += dx;
|
||||||
|
set.world_aabb[1] += dy;
|
||||||
|
set.world_aabb[2] += dx;
|
||||||
|
set.world_aabb[3] += dy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// ------
|
// ------
|
||||||
// 40 B / vertex
|
// 40 B / vertex
|
||||||
|
|
||||||
use crate::scene::mesh_model::MeshModel;
|
use crate::scene::mesh_model::{MeshLodSet, MeshModel};
|
||||||
use iced::wgpu;
|
use iced::wgpu;
|
||||||
use iced::wgpu::util::DeviceExt;
|
use iced::wgpu::util::DeviceExt;
|
||||||
|
|
||||||
@@ -55,6 +55,27 @@ pub struct MeshGpu {
|
|||||||
pub index_count: u32,
|
pub index_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPU-side bundle of MeshLodSet — one MeshGpu per available LOD plus
|
||||||
|
/// the world-XY AABB needed to pick a level per frame.
|
||||||
|
pub struct MeshLodGpu {
|
||||||
|
pub lods: Vec<MeshGpu>,
|
||||||
|
pub world_aabb: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeshLodGpu {
|
||||||
|
pub fn new(device: &wgpu::Device, set: &MeshLodSet) -> Self {
|
||||||
|
Self {
|
||||||
|
lods: set
|
||||||
|
.lods
|
||||||
|
.iter()
|
||||||
|
.filter(|m| !m.indices.is_empty())
|
||||||
|
.map(|m| MeshGpu::new(device, m))
|
||||||
|
.collect(),
|
||||||
|
world_aabb: set.world_aabb,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl MeshGpu {
|
impl MeshGpu {
|
||||||
pub fn new(device: &wgpu::Device, mesh: &MeshModel) -> Self {
|
pub fn new(device: &wgpu::Device, mesh: &MeshModel) -> Self {
|
||||||
let has_normals = mesh.normals.len() == mesh.verts.len();
|
let has_normals = mesh.normals.len() == mesh.verts.len();
|
||||||
|
|||||||
+102
-11
@@ -12,14 +12,14 @@ use iced::{Rectangle, Size};
|
|||||||
pub use face3d_gpu::Face3DGpu;
|
pub use face3d_gpu::Face3DGpu;
|
||||||
pub use hatch_gpu::HatchGpu;
|
pub use hatch_gpu::HatchGpu;
|
||||||
pub use image_gpu::ImageGpu;
|
pub use image_gpu::ImageGpu;
|
||||||
pub use mesh_gpu::MeshGpu;
|
pub use mesh_gpu::MeshLodGpu;
|
||||||
pub use uniforms::Uniforms;
|
pub use uniforms::Uniforms;
|
||||||
pub use viewcube::ViewCubePipeline;
|
pub use viewcube::ViewCubePipeline;
|
||||||
pub use wire_gpu::WireGpu;
|
pub use wire_gpu::WireGpu;
|
||||||
|
|
||||||
use crate::scene::hatch_model::HatchModel;
|
use crate::scene::hatch_model::HatchModel;
|
||||||
use crate::scene::image_model::ImageModel;
|
use crate::scene::image_model::ImageModel;
|
||||||
use crate::scene::mesh_model::MeshModel;
|
use crate::scene::mesh_model::MeshLodSet;
|
||||||
use crate::scene::wire_model::WireModel;
|
use crate::scene::wire_model::WireModel;
|
||||||
|
|
||||||
/// MSAA sample count for the main drawing pipelines.
|
/// MSAA sample count for the main drawing pipelines.
|
||||||
@@ -70,7 +70,11 @@ pub struct Pipeline {
|
|||||||
gpu_images: Vec<ImageGpu>,
|
gpu_images: Vec<ImageGpu>,
|
||||||
/// Pixel scissor rects [x, y, w, h] for viewport-clipped images. Recomputed each frame.
|
/// Pixel scissor rects [x, y, w, h] for viewport-clipped images. Recomputed each frame.
|
||||||
image_pixel_scissors: Vec<Option<[u32; 4]>>,
|
image_pixel_scissors: Vec<Option<[u32; 4]>>,
|
||||||
gpu_meshes: Vec<MeshGpu>,
|
gpu_meshes: Vec<MeshLodGpu>,
|
||||||
|
/// Per-mesh LOD level (0=high, 1=mid, 2=low) picked each frame from
|
||||||
|
/// the projected pixel diagonal. Mirrors `hatch_pixel_scissors` —
|
||||||
|
/// recomputed in `compute_mesh_lod`.
|
||||||
|
mesh_lod_levels: Vec<usize>,
|
||||||
/// Batched 3DFACE fill (all faces in one buffer) and edges (merged wire).
|
/// Batched 3DFACE fill (all faces in one buffer) and edges (merged wire).
|
||||||
gpu_face3d_fill: Option<Face3DGpu>,
|
gpu_face3d_fill: Option<Face3DGpu>,
|
||||||
gpu_face3d_edges: Vec<WireGpu>,
|
gpu_face3d_edges: Vec<WireGpu>,
|
||||||
@@ -607,6 +611,7 @@ impl Pipeline {
|
|||||||
gpu_images: vec![],
|
gpu_images: vec![],
|
||||||
image_pixel_scissors: vec![],
|
image_pixel_scissors: vec![],
|
||||||
gpu_meshes: vec![],
|
gpu_meshes: vec![],
|
||||||
|
mesh_lod_levels: vec![],
|
||||||
gpu_face3d_fill: None,
|
gpu_face3d_fill: None,
|
||||||
gpu_face3d_edges: vec![],
|
gpu_face3d_edges: vec![],
|
||||||
viewcube,
|
viewcube,
|
||||||
@@ -697,11 +702,23 @@ impl Pipeline {
|
|||||||
self.gpu_face3d_edges = WireGpu::from_batch(device, face3d_wires);
|
self.gpu_face3d_edges = WireGpu::from_batch(device, face3d_wires);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upload_meshes(&mut self, device: &wgpu::Device, meshes: &[MeshModel]) {
|
pub fn upload_meshes(&mut self, device: &wgpu::Device, meshes: &[MeshLodSet]) {
|
||||||
self.gpu_meshes = meshes
|
self.gpu_meshes = meshes
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|m| !m.indices.is_empty())
|
.filter(|s| s.lods.iter().any(|m| !m.indices.is_empty()))
|
||||||
.map(|m| MeshGpu::new(device, m))
|
.map(|s| MeshLodGpu::new(device, s))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-frame mesh LOD selector. Picks slot 0/1/2 based on the
|
||||||
|
/// projected pixel diagonal of each mesh's `world_aabb` (Phase 3.4
|
||||||
|
/// ladder: >200 px → 0, 50–200 → 1, <50 → 2). Falls back to the
|
||||||
|
/// nearest available lower slot when a level wasn't generated.
|
||||||
|
pub fn compute_mesh_lod(&mut self, view_proj: glam::Mat4, clip_w: u32, clip_h: u32) {
|
||||||
|
self.mesh_lod_levels = self
|
||||||
|
.gpu_meshes
|
||||||
|
.iter()
|
||||||
|
.map(|m| pick_mesh_lod(m, view_proj, clip_w, clip_h))
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -887,12 +904,22 @@ impl Pipeline {
|
|||||||
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
|
||||||
pass.set_pipeline(&self.mesh_pipeline);
|
pass.set_pipeline(&self.mesh_pipeline);
|
||||||
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
pass.set_bind_group(0, &self.uniform_bind_group, &[]);
|
||||||
for mesh in &self.gpu_meshes {
|
for (i, set) in self.gpu_meshes.iter().enumerate() {
|
||||||
if mesh.index_count > 0 {
|
let level = self
|
||||||
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
.mesh_lod_levels
|
||||||
pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
.get(i)
|
||||||
pass.draw_indexed(0..mesh.index_count, 0, 0..1);
|
.copied()
|
||||||
|
.unwrap_or(0)
|
||||||
|
.min(set.lods.len().saturating_sub(1));
|
||||||
|
let Some(mesh) = set.lods.get(level) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if mesh.index_count == 0 {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
|
||||||
|
pass.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
|
||||||
|
pass.draw_indexed(0..mesh.index_count, 0, 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,6 +1211,70 @@ impl Pipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Project a mesh's `world_aabb` and pick a LOD slot:
|
||||||
|
/// diagonal > 200 px → 0 (HIGH)
|
||||||
|
/// diagonal 50–200 px → 1 (MID)
|
||||||
|
/// diagonal < 50 px → 2 (LOW)
|
||||||
|
/// When the picked slot is missing from `gpu_meshes[i].lods`, walks down
|
||||||
|
/// to the next available coarser/finer level so the mesh always renders.
|
||||||
|
fn pick_mesh_lod(
|
||||||
|
mesh: &MeshLodGpu,
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
clip_w: u32,
|
||||||
|
clip_h: u32,
|
||||||
|
) -> usize {
|
||||||
|
let diag_px = aabb_diagonal_pixels(mesh.world_aabb, view_proj, clip_w, clip_h);
|
||||||
|
let target = if diag_px > 200.0 {
|
||||||
|
0
|
||||||
|
} else if diag_px > 50.0 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
2
|
||||||
|
};
|
||||||
|
// Walk down to nearest available LOD (some entities won't have all 3).
|
||||||
|
for level in (0..=target).rev() {
|
||||||
|
if mesh.lods.get(level).is_some() {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aabb_diagonal_pixels(
|
||||||
|
aabb: [f32; 4],
|
||||||
|
view_proj: glam::Mat4,
|
||||||
|
clip_w: u32,
|
||||||
|
clip_h: u32,
|
||||||
|
) -> f32 {
|
||||||
|
let [x0, y0, x1, y1] = aabb;
|
||||||
|
if !x0.is_finite() || !y0.is_finite() || !x1.is_finite() || !y1.is_finite() {
|
||||||
|
return f32::INFINITY;
|
||||||
|
}
|
||||||
|
let w = clip_w as f32;
|
||||||
|
let h = clip_h as f32;
|
||||||
|
let corners = [
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x0, y0, 0.0)),
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x1, y0, 0.0)),
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x0, y1, 0.0)),
|
||||||
|
view_proj.project_point3(glam::Vec3::new(x1, y1, 0.0)),
|
||||||
|
];
|
||||||
|
let mut min_px = f32::INFINITY;
|
||||||
|
let mut max_px = f32::NEG_INFINITY;
|
||||||
|
let mut min_py = f32::INFINITY;
|
||||||
|
let mut max_py = f32::NEG_INFINITY;
|
||||||
|
for c in &corners {
|
||||||
|
let px = (c.x + 1.0) * 0.5 * w;
|
||||||
|
let py = (1.0 - c.y) * 0.5 * h;
|
||||||
|
if px < min_px { min_px = px; }
|
||||||
|
if px > max_px { max_px = px; }
|
||||||
|
if py < min_py { min_py = py; }
|
||||||
|
if py > max_py { max_py = py; }
|
||||||
|
}
|
||||||
|
let dx = max_px - min_px;
|
||||||
|
let dy = max_py - min_py;
|
||||||
|
(dx * dx + dy * dy).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
/// Return `true` when the world-XY AABB's screen-space size is below the
|
/// Return `true` when the world-XY AABB's screen-space size is below the
|
||||||
/// given pixel threshold. Used by LOD passes (hatch skip, etc.) to drop
|
/// given pixel threshold. Used by LOD passes (hatch skip, etc.) to drop
|
||||||
/// draw calls that wouldn't contribute a visible pixel.
|
/// draw calls that wouldn't contribute a visible pixel.
|
||||||
|
|||||||
+3
-2
@@ -14,7 +14,7 @@ use std::sync::Arc;
|
|||||||
use super::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
|
use super::pipeline::viewcube::{hover_id, VIEWCUBE_PX};
|
||||||
use super::pipeline::Pipeline;
|
use super::pipeline::Pipeline;
|
||||||
use super::tessellate;
|
use super::tessellate;
|
||||||
use super::{HatchModel, ImageModel, MeshModel, Scene, Uniforms, WireModel};
|
use super::{HatchModel, ImageModel, MeshLodSet, Scene, Uniforms, WireModel};
|
||||||
|
|
||||||
// ── PaperViewportPipeline / PaperViewportPrimitive ────────────────────────
|
// ── PaperViewportPipeline / PaperViewportPrimitive ────────────────────────
|
||||||
//
|
//
|
||||||
@@ -86,7 +86,7 @@ pub struct Primitive {
|
|||||||
/// Wipeout fills — rendered in a separate pass AFTER wires.
|
/// Wipeout fills — rendered in a separate pass AFTER wires.
|
||||||
pub(super) wipeout_hatches: Arc<Vec<HatchModel>>,
|
pub(super) wipeout_hatches: Arc<Vec<HatchModel>>,
|
||||||
pub(super) images: Arc<Vec<ImageModel>>,
|
pub(super) images: Arc<Vec<ImageModel>>,
|
||||||
pub(super) meshes: Arc<Vec<MeshModel>>,
|
pub(super) meshes: Arc<Vec<MeshLodSet>>,
|
||||||
pub(super) uniforms: Uniforms,
|
pub(super) uniforms: Uniforms,
|
||||||
/// Camera rotation matrix derived from the quaternion.
|
/// Camera rotation matrix derived from the quaternion.
|
||||||
/// Used by the ViewCube pipeline — no gimbal lock.
|
/// Used by the ViewCube pipeline — no gimbal lock.
|
||||||
@@ -148,6 +148,7 @@ impl shader::Primitive for Primitive {
|
|||||||
pipeline.compute_wipeout_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_wipeout_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
pipeline.compute_image_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_image_scissors(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
pipeline.compute_hatch_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
pipeline.compute_hatch_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
|
pipeline.compute_mesh_lod(self.uniforms.view_proj, clip_size.width, clip_size.height);
|
||||||
if self.show_viewcube {
|
if self.show_viewcube {
|
||||||
pipeline.viewcube.upload(
|
pipeline.viewcube.upload(
|
||||||
queue,
|
queue,
|
||||||
|
|||||||
+104
-25
@@ -20,18 +20,54 @@ use acadrust::entities::acis::{
|
|||||||
};
|
};
|
||||||
use acadrust::entities::{Body, Region, Solid3D};
|
use acadrust::entities::{Body, Region, Solid3D};
|
||||||
|
|
||||||
use crate::scene::mesh_model::MeshModel;
|
use crate::scene::mesh_model::{MeshLodSet, MeshModel};
|
||||||
|
|
||||||
// Number of arc segments per full circle for curved surface sampling.
|
/// Per-LOD sampling density. Higher values = finer mesh = more triangles.
|
||||||
const CIRC_SEGS: usize = 48;
|
#[derive(Copy, Clone, Debug)]
|
||||||
// Grid resolution for sphere / torus latitude / longitude subdivision.
|
pub struct LodConfig {
|
||||||
const GRID_U: usize = 32;
|
/// Arc segments per full circle for curved-surface sampling.
|
||||||
const GRID_V: usize = 16;
|
pub circ_segs: usize,
|
||||||
|
/// Longitudinal grid count for sphere / torus surfaces.
|
||||||
|
pub grid_u: usize,
|
||||||
|
/// Latitudinal grid count for sphere / torus surfaces.
|
||||||
|
pub grid_v: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LodConfig {
|
||||||
|
/// LOD 0 — full resolution. The pre-Phase-3.4 baseline.
|
||||||
|
pub const HIGH: LodConfig = LodConfig {
|
||||||
|
circ_segs: 48,
|
||||||
|
grid_u: 32,
|
||||||
|
grid_v: 16,
|
||||||
|
};
|
||||||
|
/// LOD 1 — half-resolution. Use between ~50–200 px projected diagonal.
|
||||||
|
pub const MID: LodConfig = LodConfig {
|
||||||
|
circ_segs: 24,
|
||||||
|
grid_u: 16,
|
||||||
|
grid_v: 8,
|
||||||
|
};
|
||||||
|
/// LOD 2 — quarter-resolution. Use below ~50 px.
|
||||||
|
pub const LOW: LodConfig = LodConfig {
|
||||||
|
circ_segs: 12,
|
||||||
|
grid_u: 8,
|
||||||
|
grid_v: 4,
|
||||||
|
};
|
||||||
|
/// Returns the three LOD configs in `[high, mid, low]` order — matches
|
||||||
|
/// the `MeshLodSet::lods` slot ordering.
|
||||||
|
pub const fn all() -> [LodConfig; 3] {
|
||||||
|
[Self::HIGH, Self::MID, Self::LOW]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Public entry point ────────────────────────────────────────────────────────
|
// ── Public entry point ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Tessellate a SAT document into mesh buffers — shared by all ACIS entities.
|
/// Tessellate a SAT document into mesh buffers — shared by all ACIS entities.
|
||||||
fn tessellate_sat(sat: &SatDocument, name: String, color: [f32; 4]) -> Option<MeshModel> {
|
fn tessellate_sat(
|
||||||
|
sat: &SatDocument,
|
||||||
|
name: String,
|
||||||
|
color: [f32; 4],
|
||||||
|
lod: LodConfig,
|
||||||
|
) -> Option<MeshModel> {
|
||||||
let mut verts: Vec<[f32; 3]> = Vec::new();
|
let mut verts: Vec<[f32; 3]> = Vec::new();
|
||||||
let mut normals: Vec<[f32; 3]> = Vec::new();
|
let mut normals: Vec<[f32; 3]> = Vec::new();
|
||||||
let mut indices: Vec<u32> = Vec::new();
|
let mut indices: Vec<u32> = Vec::new();
|
||||||
@@ -49,17 +85,17 @@ fn tessellate_sat(sat: &SatDocument, name: String, color: [f32; 4]) -> Option<Me
|
|||||||
}
|
}
|
||||||
"cone-surface" => {
|
"cone-surface" => {
|
||||||
if let Some(cone) = SatConeSurface::from_record(surf_rec) {
|
if let Some(cone) = SatConeSurface::from_record(surf_rec) {
|
||||||
tess_cone_face(sat, &face, &cone, &mut verts, &mut normals, &mut indices);
|
tess_cone_face(sat, &face, &cone, lod, &mut verts, &mut normals, &mut indices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"sphere-surface" => {
|
"sphere-surface" => {
|
||||||
if let Some(sphere) = SatSphereSurface::from_record(surf_rec) {
|
if let Some(sphere) = SatSphereSurface::from_record(surf_rec) {
|
||||||
tess_sphere_face(&sphere, &mut verts, &mut normals, &mut indices);
|
tess_sphere_face(&sphere, lod, &mut verts, &mut normals, &mut indices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"torus-surface" => {
|
"torus-surface" => {
|
||||||
if let Some(torus) = SatTorusSurface::from_record(surf_rec) {
|
if let Some(torus) = SatTorusSurface::from_record(surf_rec) {
|
||||||
tess_torus_face(&torus, &mut verts, &mut normals, &mut indices);
|
tess_torus_face(&torus, lod, &mut verts, &mut normals, &mut indices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -78,6 +114,46 @@ fn tessellate_sat(sat: &SatDocument, name: String, color: [f32; 4]) -> Option<Me
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tessellate a SAT document at all three LODs and bundle them into a
|
||||||
|
/// `MeshLodSet` ready for the render pipeline to pick a level per frame.
|
||||||
|
fn tessellate_sat_lods(
|
||||||
|
sat: &SatDocument,
|
||||||
|
name: String,
|
||||||
|
color: [f32; 4],
|
||||||
|
) -> Option<MeshLodSet> {
|
||||||
|
let configs = LodConfig::all();
|
||||||
|
let mut lods: Vec<MeshModel> = Vec::with_capacity(3);
|
||||||
|
for lod in configs {
|
||||||
|
if let Some(m) = tessellate_sat(sat, name.clone(), color, lod) {
|
||||||
|
lods.push(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lods.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let world_aabb = mesh_aabb(&lods[0]);
|
||||||
|
Some(MeshLodSet { lods, world_aabb })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// World-XY AABB of the mesh — used by the render-pipeline LOD selector
|
||||||
|
/// to pick a level based on projected pixel diagonal.
|
||||||
|
fn mesh_aabb(mesh: &MeshModel) -> [f32; 4] {
|
||||||
|
let mut min_x = f32::INFINITY;
|
||||||
|
let mut min_y = f32::INFINITY;
|
||||||
|
let mut max_x = f32::NEG_INFINITY;
|
||||||
|
let mut max_y = f32::NEG_INFINITY;
|
||||||
|
for &[x, y, _] in &mesh.verts {
|
||||||
|
if !x.is_finite() || !y.is_finite() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if x < min_x { min_x = x; }
|
||||||
|
if y < min_y { min_y = y; }
|
||||||
|
if x > max_x { max_x = x; }
|
||||||
|
if y > max_y { max_y = y; }
|
||||||
|
}
|
||||||
|
[min_x, min_y, max_x, max_y]
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_acis(
|
fn parse_acis(
|
||||||
sat_fn: impl FnOnce() -> Option<SatDocument>,
|
sat_fn: impl FnOnce() -> Option<SatDocument>,
|
||||||
is_binary: bool,
|
is_binary: bool,
|
||||||
@@ -92,40 +168,40 @@ fn parse_acis(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellate a `Region` entity (2D planar ACIS body) into a `MeshModel`.
|
/// Tessellate a `Region` entity (2D planar ACIS body) at all three LOD levels.
|
||||||
pub fn tessellate_region(region: &Region, color: [f32; 4]) -> Option<MeshModel> {
|
pub fn tessellate_region(region: &Region, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||||
let sat = parse_acis(
|
let sat = parse_acis(
|
||||||
|| region.parse_sat(),
|
|| region.parse_sat(),
|
||||||
region.acis_data.is_binary,
|
region.acis_data.is_binary,
|
||||||
®ion.acis_data.sab_data,
|
®ion.acis_data.sab_data,
|
||||||
)?;
|
)?;
|
||||||
let name = region.common.handle.value().to_string();
|
let name = region.common.handle.value().to_string();
|
||||||
tessellate_sat(&sat, name, color)
|
tessellate_sat_lods(&sat, name, color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellate a `Body` entity (3D ACIS body) into a `MeshModel`.
|
/// Tessellate a `Body` entity (3D ACIS body) at all three LOD levels.
|
||||||
pub fn tessellate_body(body: &Body, color: [f32; 4]) -> Option<MeshModel> {
|
pub fn tessellate_body(body: &Body, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||||
let sat = parse_acis(
|
let sat = parse_acis(
|
||||||
|| body.parse_sat(),
|
|| body.parse_sat(),
|
||||||
body.acis_data.is_binary,
|
body.acis_data.is_binary,
|
||||||
&body.acis_data.sab_data,
|
&body.acis_data.sab_data,
|
||||||
)?;
|
)?;
|
||||||
let name = body.common.handle.value().to_string();
|
let name = body.common.handle.value().to_string();
|
||||||
tessellate_sat(&sat, name, color)
|
tessellate_sat_lods(&sat, name, color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tessellate a `Solid3D` entity into a GPU-ready `MeshModel`.
|
/// Tessellate a `Solid3D` entity at all three LOD levels.
|
||||||
///
|
///
|
||||||
/// Returns `None` when the entity has no parseable SAT data or produces no
|
/// Returns `None` when the entity has no parseable SAT data or produces no
|
||||||
/// triangles (e.g. the solid uses only unsupported surface types).
|
/// triangles (e.g. the solid uses only unsupported surface types).
|
||||||
pub fn tessellate_solid3d(solid: &Solid3D, color: [f32; 4]) -> Option<MeshModel> {
|
pub fn tessellate_solid3d(solid: &Solid3D, color: [f32; 4]) -> Option<MeshLodSet> {
|
||||||
let sat = parse_acis(
|
let sat = parse_acis(
|
||||||
|| solid.parse_sat(),
|
|| solid.parse_sat(),
|
||||||
solid.acis_data.is_binary,
|
solid.acis_data.is_binary,
|
||||||
&solid.acis_data.sab_data,
|
&solid.acis_data.sab_data,
|
||||||
)?;
|
)?;
|
||||||
let name = solid.common.handle.value().to_string();
|
let name = solid.common.handle.value().to_string();
|
||||||
tessellate_sat(&sat, name, color)
|
tessellate_sat_lods(&sat, name, color)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Topology helpers ──────────────────────────────────────────────────────────
|
// ── Topology helpers ──────────────────────────────────────────────────────────
|
||||||
@@ -262,6 +338,7 @@ fn tess_cone_face(
|
|||||||
sat: &SatDocument,
|
sat: &SatDocument,
|
||||||
face: &SatFace,
|
face: &SatFace,
|
||||||
cone: &SatConeSurface,
|
cone: &SatConeSurface,
|
||||||
|
lod: LodConfig,
|
||||||
verts: &mut Vec<[f32; 3]>,
|
verts: &mut Vec<[f32; 3]>,
|
||||||
normals: &mut Vec<[f32; 3]>,
|
normals: &mut Vec<[f32; 3]>,
|
||||||
indices: &mut Vec<u32>,
|
indices: &mut Vec<u32>,
|
||||||
@@ -285,8 +362,8 @@ fn tess_cone_face(
|
|||||||
let (h_min, h_max, theta_min, theta_max, full_circle) =
|
let (h_min, h_max, theta_min, theta_max, full_circle) =
|
||||||
angular_range(cx, cy, cz, axis, u_dir, v_dir, &poly);
|
angular_range(cx, cy, cz, axis, u_dir, v_dir, &poly);
|
||||||
|
|
||||||
let segs_u = CIRC_SEGS;
|
let segs_u = lod.circ_segs;
|
||||||
let segs_v = segs_u / 4; // height subdivisions
|
let segs_v = (segs_u / 4).max(1); // height subdivisions
|
||||||
|
|
||||||
let theta_span = if full_circle {
|
let theta_span = if full_circle {
|
||||||
TAU
|
TAU
|
||||||
@@ -416,6 +493,7 @@ fn angular_range(
|
|||||||
|
|
||||||
fn tess_sphere_face(
|
fn tess_sphere_face(
|
||||||
sphere: &SatSphereSurface,
|
sphere: &SatSphereSurface,
|
||||||
|
lod: LodConfig,
|
||||||
verts: &mut Vec<[f32; 3]>,
|
verts: &mut Vec<[f32; 3]>,
|
||||||
normals: &mut Vec<[f32; 3]>,
|
normals: &mut Vec<[f32; 3]>,
|
||||||
indices: &mut Vec<u32>,
|
indices: &mut Vec<u32>,
|
||||||
@@ -428,8 +506,8 @@ fn tess_sphere_face(
|
|||||||
let u_dir = norm3([ux, uy, uz]);
|
let u_dir = norm3([ux, uy, uz]);
|
||||||
let v_dir = cross3(pole, u_dir);
|
let v_dir = cross3(pole, u_dir);
|
||||||
|
|
||||||
let nu = GRID_U;
|
let nu = lod.grid_u.max(3);
|
||||||
let nv = GRID_V;
|
let nv = lod.grid_v.max(2);
|
||||||
|
|
||||||
for j in 0..nv {
|
for j in 0..nv {
|
||||||
let phi0 = std::f64::consts::PI * (j as f64 / nv as f64); // 0..π
|
let phi0 = std::f64::consts::PI * (j as f64 / nv as f64); // 0..π
|
||||||
@@ -481,6 +559,7 @@ fn sphere_dir(pole: [f64; 3], u_dir: [f64; 3], v_dir: [f64; 3], theta: f64, phi:
|
|||||||
|
|
||||||
fn tess_torus_face(
|
fn tess_torus_face(
|
||||||
torus: &SatTorusSurface,
|
torus: &SatTorusSurface,
|
||||||
|
lod: LodConfig,
|
||||||
verts: &mut Vec<[f32; 3]>,
|
verts: &mut Vec<[f32; 3]>,
|
||||||
normals: &mut Vec<[f32; 3]>,
|
normals: &mut Vec<[f32; 3]>,
|
||||||
indices: &mut Vec<u32>,
|
indices: &mut Vec<u32>,
|
||||||
@@ -494,8 +573,8 @@ fn tess_torus_face(
|
|||||||
let major_r = torus.major_radius();
|
let major_r = torus.major_radius();
|
||||||
let minor_r = torus.minor_radius();
|
let minor_r = torus.minor_radius();
|
||||||
|
|
||||||
let nu = GRID_U; // around the tube
|
let nu = lod.grid_u.max(3); // around the tube
|
||||||
let nv = GRID_V; // around the torus
|
let nv = lod.grid_v.max(3); // around the torus
|
||||||
|
|
||||||
for j in 0..nv {
|
for j in 0..nv {
|
||||||
let phi0 = TAU * (j as f64 / nv as f64);
|
let phi0 = TAU * (j as f64 / nv as f64);
|
||||||
|
|||||||
Reference in new issue
Block a user