use std::sync::Arc;
use glam::{Vec3, Vec3A, uvec3, vec3};
use hexasphere::{BaseShape, Subdivided};
use itertools::Itertools as _;
use macaw::MeshGen;
use ordered_float::NotNan;
use re_byte_size::SizeBytes as _;
use re_chunk_store::external::re_chunk::external::re_byte_size;
use re_renderer::RenderContext;
use re_renderer::mesh::{self, GpuMesh, MeshError};
use re_viewer_context::Cache;
use smallvec::smallvec;
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum ProcMeshKey {
Cube,
Sphere {
subdivisions: usize,
axes_only: bool,
},
Capsule {
length: NotNan<f32>,
subdivisions: usize,
axes_only: bool,
},
Cylinder {
subdivisions: usize,
axes_only: bool,
},
}
impl re_byte_size::SizeBytes for ProcMeshKey {
fn heap_size_bytes(&self) -> u64 {
0
}
}
impl ProcMeshKey {
pub fn simple_bounding_box(&self) -> macaw::BoundingBox {
match self {
Self::Sphere {
subdivisions: _,
axes_only: _,
} => {
macaw::BoundingBox::from_center_size(Vec3::splat(0.0), Vec3::splat(2.0))
}
Self::Cube => macaw::BoundingBox::from_center_size(Vec3::splat(0.0), Vec3::splat(1.0)),
Self::Capsule {
subdivisions: _,
axes_only: _,
length,
} => macaw::BoundingBox::from_min_max(
Vec3::new(-1.0, -1.0, -1.0),
Vec3::new(1.0, 1.0, 1.0 + length.into_inner()),
),
Self::Cylinder {
subdivisions: _,
axes_only: _,
} => {
macaw::BoundingBox::from_center_size(Vec3::splat(0.0), Vec3::splat(2.0))
}
}
}
}
#[derive(Debug)]
pub struct WireframeMesh {
#[expect(unused)]
pub bbox: macaw::BoundingBox,
#[expect(unused)]
pub vertex_count: usize,
pub line_strips: Vec<Vec<Vec3>>,
}
impl re_byte_size::SizeBytes for WireframeMesh {
fn heap_size_bytes(&self) -> u64 {
let Self {
bbox: _,
vertex_count: _,
line_strips,
} = self;
line_strips
.iter()
.map(|strip| strip.len() * std::mem::size_of::<Vec3>())
.sum::<usize>() as _
}
}
#[derive(Clone)]
pub struct SolidMesh {
#[expect(unused)]
pub bbox: macaw::BoundingBox,
pub gpu_mesh: Arc<GpuMesh>,
}
impl re_byte_size::SizeBytes for SolidMesh {
fn heap_size_bytes(&self) -> u64 {
0 }
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
enum GenError {
#[error(transparent)]
MeshProcessing(#[from] MeshError),
}
#[derive(Default)]
pub struct WireframeCache(ahash::HashMap<ProcMeshKey, Option<Arc<WireframeMesh>>>);
impl WireframeCache {
pub fn entry(
&mut self,
key: ProcMeshKey,
render_ctx: &RenderContext,
) -> Option<Arc<WireframeMesh>> {
self.0
.entry(key)
.or_insert_with(|| {
re_tracing::profile_scope!("proc_mesh::WireframeCache(miss)", format!("{key:?}"));
re_log::trace!("Generating wireframe mesh {key:?}…");
Some(Arc::new(generate_wireframe(&key, render_ctx)))
})
.clone()
}
}
impl Cache for WireframeCache {
fn name(&self) -> &'static str {
"WireframeCache"
}
fn purge_memory(&mut self) {
self.0.clear();
}
}
impl re_byte_size::MemUsageTreeCapture for WireframeCache {
fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree {
re_byte_size::MemUsageTree::Bytes(self.0.total_size_bytes())
}
}
fn generate_wireframe(key: &ProcMeshKey, render_ctx: &RenderContext) -> WireframeMesh {
re_tracing::profile_function!();
_ = render_ctx;
match *key {
ProcMeshKey::Cube => {
let corners = [
vec3(-0.5, -0.5, -0.5),
vec3(-0.5, -0.5, 0.5),
vec3(-0.5, 0.5, -0.5),
vec3(-0.5, 0.5, 0.5),
vec3(0.5, -0.5, -0.5),
vec3(0.5, -0.5, 0.5),
vec3(0.5, 0.5, -0.5),
vec3(0.5, 0.5, 0.5),
];
let line_strips: Vec<Vec<Vec3>> = vec![
vec![
corners[0b000],
corners[0b001],
corners[0b011],
corners[0b010],
corners[0b000],
corners[0b100],
corners[0b101],
corners[0b111],
corners[0b110],
corners[0b100],
],
vec![corners[0b001], corners[0b101]],
vec![corners[0b010], corners[0b110]],
vec![corners[0b011], corners[0b111]],
];
WireframeMesh {
bbox: key.simple_bounding_box(),
vertex_count: line_strips.iter().map(|v| v.len()).sum(),
line_strips,
}
}
ProcMeshKey::Sphere {
subdivisions,
axes_only,
} => {
let subdiv: hexasphere::Subdivided<(), OctahedronBase> =
hexasphere::Subdivided::new(subdivisions, |_| ());
let sphere_points = subdiv.raw_points();
let line_strips: Vec<Vec<Vec3>> = if axes_only {
let mut buffer: Vec<u32> = Vec::new();
subdiv.get_major_edges_line_indices(&mut buffer, 1, |v| v.push(0));
buffer
.split(|&i| i == 0)
.map(|strip| -> Vec<Vec3> {
strip
.iter()
.map(|&i| sphere_points[i as usize - 1].into())
.collect()
})
.collect()
} else {
subdiv
.get_all_line_indices(1, |v| v.push(0))
.split(|&i| i == 0)
.map(|strip| -> Vec<Vec3> {
strip
.iter()
.map(|&i| sphere_points[i as usize - 1].into())
.collect()
})
.collect()
};
WireframeMesh {
bbox: key.simple_bounding_box(),
vertex_count: line_strips.iter().map(|v| v.len()).sum(),
line_strips,
}
}
ProcMeshKey::Capsule {
length,
subdivisions,
axes_only,
} => {
let line_strips = capsule_wireframe_lines(length.into_inner(), subdivisions, axes_only);
WireframeMesh {
bbox: key.simple_bounding_box(),
vertex_count: line_strips.iter().map(|s| s.len()).sum(),
line_strips,
}
}
ProcMeshKey::Cylinder {
subdivisions,
axes_only,
} => {
let n = ((subdivisions + 1) * 4).max(3);
let delta = std::f32::consts::TAU / (n as f32);
let half_height = 1.0;
let mut line_strips: Vec<Vec<Vec3>> = Vec::new();
let mut bottom_loop = Vec::with_capacity(n + 1);
for i in 0..n {
let theta = i as f32 * delta;
let x = theta.cos(); let y = theta.sin();
bottom_loop.push(Vec3::new(x, y, -half_height));
}
bottom_loop.push(bottom_loop[0]);
line_strips.push(bottom_loop);
let mut top_loop = Vec::with_capacity(n + 1);
for i in 0..n {
let theta = i as f32 * delta;
let x = theta.cos();
let y = theta.sin();
top_loop.push(Vec3::new(x, y, half_height));
}
top_loop.push(top_loop[0]);
line_strips.push(top_loop);
let num_spokes = if axes_only { 6 } else { n };
let delta = std::f32::consts::TAU / (num_spokes as f32);
let bottom_center = Vec3::new(0.0, 0.0, -half_height);
let top_center = Vec3::new(0.0, 0.0, half_height);
for i in 0..num_spokes {
let theta = (i as f32) * delta;
let x = theta.cos();
let y = theta.sin();
line_strips.push(vec![
Vec3::new(x, y, -half_height),
Vec3::new(x, y, half_height),
]);
if !axes_only {
line_strips.push(vec![Vec3::new(x, y, -half_height), bottom_center]);
line_strips.push(vec![Vec3::new(x, y, half_height), top_center]);
}
}
WireframeMesh {
bbox: key.simple_bounding_box(),
vertex_count: line_strips.iter().map(|strip| strip.len()).sum(),
line_strips,
}
}
}
}
fn capsule_wireframe_lines(length: f32, subdiv: usize, axes_only: bool) -> Vec<Vec<Vec3>> {
let mut line_strips = Vec::new();
let n = ((subdiv + 1) * 4).max(3);
let delta = std::f32::consts::TAU / n as f32;
let z_top = length;
let z_bot = 0.0;
let mut top_loop = Vec::with_capacity(n + 1);
let mut bottom_loop = Vec::with_capacity(n + 1);
for i in 0..=n {
let theta = i as f32 * delta;
top_loop.push(Vec3::new(theta.cos(), theta.sin(), z_top));
bottom_loop.push(Vec3::new(theta.cos(), theta.sin(), z_bot));
}
line_strips.push(top_loop);
line_strips.push(bottom_loop);
let num_spokes = if axes_only { 4 } else { n };
let delta_spoke = std::f32::consts::TAU / num_spokes as f32;
for i in 0..num_spokes {
let theta = i as f32 * delta_spoke;
let rim = Vec3::new(theta.cos(), theta.sin(), 0.0);
line_strips.push(vec![rim.with_z(z_bot), rim.with_z(z_top)]);
}
let sphere: Subdivided<(), OctahedronBase> = Subdivided::new(subdiv, |_| ());
let mut tmp = Vec::new();
let indices: Vec<u32> = if axes_only {
sphere.get_major_edges_line_indices(&mut tmp, 1, |v| v.push(0));
tmp
} else {
sphere.get_all_line_indices(1, |v| v.push(0))
};
let pts = sphere.raw_points();
for strip in indices.split(|&i| i == 0) {
let mut prev_top: Option<Vec3> = None;
let mut prev_bottom: Option<Vec3> = None;
for &idx in strip {
let p = pts[idx as usize - 1];
let v_top = Vec3::new(p.x, p.y, p.z + z_top);
let v_bottom = Vec3::new(p.x, p.y, p.z);
if let Some(p0) = prev_top {
if p0.z >= z_top && v_top.z >= z_top {
line_strips.push(vec![p0, v_top]);
}
}
if let Some(p0) = prev_bottom {
if p0.z <= z_bot && v_bottom.z <= z_bot {
line_strips.push(vec![p0, v_bottom]);
}
}
prev_top = Some(v_top);
prev_bottom = Some(v_bottom);
}
}
line_strips
}
#[derive(Default)]
pub struct SolidCache(ahash::HashMap<ProcMeshKey, Option<SolidMesh>>);
impl SolidCache {
pub fn entry(&mut self, key: ProcMeshKey, render_ctx: &RenderContext) -> Option<SolidMesh> {
self.0
.entry(key)
.or_insert_with(|| {
re_tracing::profile_scope!("proc_mesh::SolidCache(miss)", format!("{key:?}"));
re_log::trace!("Generating solid mesh {key:?}…");
match generate_solid(&key, render_ctx) {
Ok(mesh) => Some(mesh),
Err(err) => {
re_log::warn!(
"Failed to generate mesh {key:?}: {}",
re_error::format_ref(&err)
);
None
}
}
})
.clone()
}
}
impl Cache for SolidCache {
fn purge_memory(&mut self) {
self.0.clear();
}
fn name(&self) -> &'static str {
"SolidCache"
}
fn vram_usage(&self) -> re_byte_size::MemUsageTree {
let mut node = re_byte_size::MemUsageNode::new();
let mut items: Vec<_> = self
.0
.iter()
.map(|(key, mesh)| {
let bytes_gpu = mesh.as_ref().map_or(0, |m| m.gpu_mesh.gpu_byte_size());
(format!("{key:?}"), bytes_gpu)
})
.collect();
items.sort_by(|a, b| a.0.cmp(&b.0));
for (item_name, bytes_gpu) in items {
node.add(item_name, re_byte_size::MemUsageTree::Bytes(bytes_gpu));
}
node.into_tree()
}
}
impl re_byte_size::MemUsageTreeCapture for SolidCache {
fn capture_mem_usage_tree(&self) -> re_byte_size::MemUsageTree {
re_byte_size::MemUsageTree::Bytes(self.0.total_size_bytes())
}
}
fn generate_solid(key: &ProcMeshKey, render_ctx: &RenderContext) -> Result<SolidMesh, GenError> {
re_tracing::profile_function!();
let bbox = key.simple_bounding_box();
let mesh: mesh::CpuMesh = match *key {
ProcMeshKey::Cube => {
let mut mg = macaw::MeshGen::new();
mg.push_cube(Vec3::splat(0.5), macaw::IsoTransform::IDENTITY);
mesh_from_mesh_gen(format!("{key:?}").into(), mg, render_ctx, bbox)
}
ProcMeshKey::Sphere {
subdivisions,
axes_only: _, } => {
let subdiv: hexasphere::Subdivided<(), OctahedronBase> =
hexasphere::Subdivided::new(subdivisions, |_| ());
let vertex_positions: Vec<Vec3> =
subdiv.raw_points().iter().map(|&p| p.into()).collect();
let vertex_normals = vertex_positions.clone();
let num_vertices = vertex_positions.len();
let triangle_indices = subdiv.get_all_indices();
let triangle_indices: Vec<glam::UVec3> = triangle_indices
.into_iter()
.tuples()
.map(|(i1, i2, i3)| glam::uvec3(i1, i2, i3))
.collect();
let materials = materials_for_uncolored_mesh(render_ctx, triangle_indices.len());
mesh::CpuMesh {
label: format!("{key:?}").into(),
triangle_indices,
vertex_positions,
vertex_normals,
vertex_colors: vec![re_renderer::Rgba32Unmul::BLACK; num_vertices],
vertex_texcoords: vec![glam::Vec2::ZERO; num_vertices],
materials,
bbox,
}
}
ProcMeshKey::Capsule {
length,
subdivisions,
axes_only: _, } => {
let mg_subdivisions = (subdivisions + 1) * 4;
let mut mg = macaw::MeshGen::new();
mg.push_capsule(
1.0,
length.into_inner(),
mg_subdivisions,
mg_subdivisions,
macaw::IsoTransform::from_quat(glam::Quat::from_rotation_x(
std::f32::consts::FRAC_PI_2,
)),
);
mesh_from_mesh_gen(format!("{key:?}").into(), mg, render_ctx, bbox)
}
ProcMeshKey::Cylinder {
subdivisions,
axes_only: _, } => {
let mg_subdivisions = (subdivisions + 1) * 4;
let mut mg = macaw::MeshGen::new();
push_cylinder_solid(&mut mg, 1.0, 2.0, mg_subdivisions);
mesh_from_mesh_gen(format!("{key:?}").into(), mg, render_ctx, bbox)
}
};
mesh.sanity_check()?;
Ok(SolidMesh {
bbox: key.simple_bounding_box(),
gpu_mesh: Arc::new(GpuMesh::new(render_ctx, &mesh)?),
})
}
fn push_cylinder_solid(mesh_gen: &mut MeshGen, radius: f32, height: f32, subdivisions: usize) {
let index_offset = mesh_gen.positions.len() as u32;
let n = subdivisions.max(3) as u32;
let half_height = height * 0.5;
let delta = 2.0 * std::f32::consts::PI / n as f32;
mesh_gen.positions.push(Vec3::new(0.0, 0.0, half_height));
mesh_gen.normals.push(Vec3::new(0.0, 0.0, 1.0));
mesh_gen.positions.push(Vec3::new(0.0, 0.0, -half_height));
mesh_gen.normals.push(Vec3::new(0.0, 0.0, -1.0));
for i in 0..n {
let theta = i as f32 * delta;
let (cos_theta, sin_theta) = (theta.cos(), theta.sin());
let x = radius * cos_theta;
let y = radius * sin_theta;
mesh_gen.positions.push(Vec3::new(x, y, half_height));
mesh_gen.normals.push(Vec3::new(0.0, 0.0, 1.0));
mesh_gen.positions.push(Vec3::new(x, y, -half_height));
mesh_gen.normals.push(Vec3::new(0.0, 0.0, -1.0));
mesh_gen.positions.push(Vec3::new(x, y, half_height));
mesh_gen.normals.push(Vec3::new(cos_theta, sin_theta, 0.0));
mesh_gen.positions.push(Vec3::new(x, y, -half_height));
mesh_gen.normals.push(Vec3::new(cos_theta, sin_theta, 0.0));
}
for i in 0..n {
let top_center = index_offset;
let bot_center = index_offset + 1;
let top_rim = index_offset + 2 + i * 4;
let next_top = index_offset + 2 + ((i + 1) % n) * 4;
mesh_gen
.indices
.extend_from_slice(&[top_center, top_rim, next_top]);
let bot_rim = index_offset + 3 + i * 4;
let next_bot = index_offset + 3 + ((i + 1) % n) * 4;
mesh_gen
.indices
.extend_from_slice(&[bot_center, next_bot, bot_rim]);
}
let side_base = index_offset + 4;
for i in 0..n {
let top = side_base + i * 4;
let bot = side_base + i * 4 + 1;
let next_top = side_base + ((i + 1) % n) * 4;
let next_bot = side_base + ((i + 1) % n) * 4 + 1;
mesh_gen.indices.extend_from_slice(&[top, bot, next_top]);
mesh_gen
.indices
.extend_from_slice(&[next_top, bot, next_bot]);
}
}
fn mesh_from_mesh_gen(
label: re_renderer::Label,
mg: MeshGen,
render_ctx: &RenderContext,
bbox: macaw::BoundingBox,
) -> mesh::CpuMesh {
let num_vertices = mg.positions.len();
let triangle_indices: Vec<glam::UVec3> = mg
.indices
.into_iter()
.tuples()
.map(|(i1, i2, i3)| uvec3(i1, i2, i3))
.collect();
let materials = materials_for_uncolored_mesh(render_ctx, triangle_indices.len());
mesh::CpuMesh {
label,
materials,
triangle_indices,
vertex_positions: mg.positions,
vertex_normals: mg.normals,
vertex_colors: vec![re_renderer::Rgba32Unmul::BLACK; num_vertices],
vertex_texcoords: vec![glam::Vec2::ZERO; num_vertices],
bbox,
}
}
fn materials_for_uncolored_mesh(
render_ctx: &RenderContext,
num_triangles: usize,
) -> smallvec::SmallVec<[mesh::Material; 1]> {
smallvec![mesh::Material {
label: "default material".into(),
index_range: 0..(num_triangles * 3) as u32,
albedo: render_ctx
.texture_manager_2d
.white_texture_unorm_handle()
.clone(),
albedo_factor: re_renderer::Rgba::BLACK,
}]
}
#[derive(Clone, Copy, Debug, Default)]
struct OctahedronBase;
impl BaseShape for OctahedronBase {
fn initial_points(&self) -> Vec<Vec3A> {
vec![
Vec3A::NEG_X,
Vec3A::NEG_Y,
Vec3A::NEG_Z,
Vec3A::X,
Vec3A::Y,
Vec3A::Z,
]
}
fn triangles(&self) -> Box<[hexasphere::Triangle]> {
use hexasphere::Triangle;
const TRIANGLES: [Triangle; 8] = [
Triangle::new(0, 2, 1, 1, 4, 0), Triangle::new(0, 1, 5, 0, 6, 3), Triangle::new(0, 4, 2, 2, 5, 1), Triangle::new(0, 5, 4, 3, 7, 2), Triangle::new(3, 1, 2, 8, 4, 9), Triangle::new(3, 5, 1, 11, 6, 8), Triangle::new(3, 2, 4, 9, 5, 10), Triangle::new(3, 4, 5, 10, 7, 11), ];
Box::new(TRIANGLES)
}
const EDGES: usize = 12;
fn interpolate(&self, a: Vec3A, b: Vec3A, p: f32) -> Vec3A {
hexasphere::interpolation::geometric_slerp(a, b, p)
}
}