use bevy_asset::RenderAssetUsages;
use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
use isomesh::{MeshBuffer, MeshSink};
#[derive(Clone, Debug)]
pub struct MeshBuilder {
positions: Vec<[f32; 3]>,
normals: Vec<[f32; 3]>,
uvs: Vec<[f32; 2]>,
colors: Vec<[f32; 4]>,
indices: Vec<u32>,
uv_scale: f32,
}
impl MeshBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
positions: Vec::new(),
normals: Vec::new(),
uvs: Vec::new(),
colors: Vec::new(),
indices: Vec::new(),
uv_scale: 1.0,
}
}
#[must_use]
pub const fn with_uv_scale(mut self, scale: f32) -> Self {
self.uv_scale = scale;
self
}
pub fn reset(&mut self) {
self.positions.clear();
self.normals.clear();
self.uvs.clear();
self.colors.clear();
self.indices.clear();
}
#[must_use]
pub fn vertex_count(&self) -> usize {
self.positions.len()
}
#[must_use]
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
#[must_use]
pub fn positions(&self) -> &[[f32; 3]] {
&self.positions
}
#[must_use]
pub fn normals(&self) -> &[[f32; 3]] {
&self.normals
}
#[must_use]
pub fn indices(&self) -> &[u32] {
&self.indices
}
pub fn colors_mut(&mut self) -> &mut Vec<[f32; 4]> {
&mut self.colors
}
pub fn positions_and_colors_mut(&mut self) -> (&[[f32; 3]], &mut Vec<[f32; 4]>) {
(&self.positions, &mut self.colors)
}
#[must_use]
pub fn colors(&self) -> &[[f32; 4]] {
&self.colors
}
#[must_use]
pub fn into_mesh(self) -> Mesh {
assert!(
self.colors.is_empty() || self.colors.len() == self.positions.len(),
"colours must be one per vertex: {} colours against {} vertices -- \
bevy_mesh would warn and truncate every attribute to the shorter \
length while the indices keep addressing the longer one",
self.colors.len(),
self.positions.len(),
);
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
usages_for(self.positions.len()),
);
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, self.positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, self.normals);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, self.uvs);
if !self.colors.is_empty() {
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, self.colors);
}
mesh.insert_indices(Indices::U32(self.indices));
mesh
}
}
impl Default for MeshBuilder {
fn default() -> Self {
Self::new()
}
}
impl MeshSink for MeshBuilder {
type Scalar = f32;
#[inline]
fn vertex(&mut self, position: [f32; 3], normal: [f32; 3]) -> u32 {
let index = self.positions.len();
assert!(
index < u32::MAX as usize,
"MeshBuilder exceeded the u32 index space at {index} vertices"
);
self.uvs.push(triplanar_uv(position, normal, self.uv_scale));
self.positions.push(position);
self.normals.push(normal);
index as u32
}
#[inline]
fn triangle(&mut self, a: u32, b: u32, c: u32) {
self.indices.push(a);
self.indices.push(b);
self.indices.push(c);
}
#[inline]
fn reserve(&mut self, vertices: usize, triangles: usize) {
self.positions.reserve(vertices);
self.normals.reserve(vertices);
self.uvs.reserve(vertices);
self.indices.reserve(triangles * 3);
}
}
#[inline]
fn triplanar_uv(position: [f32; 3], normal: [f32; 3], scale: f32) -> [f32; 2] {
let n = [normal[0].abs(), normal[1].abs(), normal[2].abs()];
let (u, v) = if n[0] >= n[1] && n[0] >= n[2] {
(position[1], position[2])
} else if n[1] >= n[2] {
(position[2], position[0])
} else {
(position[0], position[1])
};
[u / scale, v / scale]
}
fn usages_for(vertices: usize) -> RenderAssetUsages {
if vertices == 0 {
RenderAssetUsages::MAIN_WORLD
} else {
RenderAssetUsages::default()
}
}
#[must_use]
pub fn to_bevy_mesh(buffer: &MeshBuffer<f32>) -> Mesh {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
usages_for(buffer.positions.len()),
);
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, buffer.positions.clone());
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, buffer.normals.clone());
mesh.insert_indices(Indices::U32(buffer.indices.clone()));
mesh
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum SoupError {
NotTriangleList(PrimitiveTopology),
NoPositions,
NoIndices,
RaggedIndices(usize),
IndexOutOfRange {
index: u32,
vertices: usize,
},
}
impl core::fmt::Display for SoupError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::NotTriangleList(topology) => {
write!(f, "topology is {topology:?}, not TriangleList")
}
Self::NoPositions => write!(f, "no Float32x3 ATTRIBUTE_POSITION"),
Self::NoIndices => write!(f, "no index buffer"),
Self::RaggedIndices(n) => write!(f, "{n} indices is not a multiple of three"),
Self::IndexOutOfRange { index, vertices } => {
write!(f, "index {index} but only {vertices} vertices")
}
}
}
}
impl core::error::Error for SoupError {}
pub fn from_bevy_mesh(mesh: &Mesh) -> Result<(Vec<[f32; 3]>, Vec<u32>), SoupError> {
let topology = mesh.primitive_topology();
if topology != PrimitiveTopology::TriangleList {
return Err(SoupError::NotTriangleList(topology));
}
let positions = mesh
.attribute(Mesh::ATTRIBUTE_POSITION)
.and_then(|values| values.as_float3())
.ok_or(SoupError::NoPositions)?;
let indices: Vec<u32> = match mesh.indices().ok_or(SoupError::NoIndices)? {
Indices::U16(values) => values.iter().map(|&i| u32::from(i)).collect(),
Indices::U32(values) => values.clone(),
};
if !indices.len().is_multiple_of(3) {
return Err(SoupError::RaggedIndices(indices.len()));
}
if let Some(&index) = indices.iter().find(|&&i| i as usize >= positions.len()) {
return Err(SoupError::IndexOutOfRange {
index,
vertices: positions.len(),
});
}
Ok((positions.to_vec(), indices))
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WeldKeyConfig {
pub normal_steps: f32,
pub uv_steps: f32,
pub include_uv: bool,
}
impl Default for WeldKeyConfig {
fn default() -> Self {
Self {
normal_steps: 16.0,
uv_steps: 4096.0,
include_uv: false,
}
}
}
fn fnv1a(bytes: &[u8]) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325u64;
for &b in bytes {
hash ^= u64::from(b);
hash = hash.wrapping_mul(0x1000_0000_01b3);
}
hash
}
#[must_use]
pub fn weld_keys(mesh: &Mesh, config: WeldKeyConfig) -> Vec<u64> {
let count = mesh.count_vertices();
let normals = mesh
.attribute(Mesh::ATTRIBUTE_NORMAL)
.and_then(|values| values.as_float3());
let uvs = if config.include_uv {
match mesh.attribute(Mesh::ATTRIBUTE_UV_0) {
Some(VertexAttributeValues::Float32x2(values)) => Some(values.as_slice()),
_ => None,
}
} else {
None
};
(0..count)
.map(|i| {
let mut bytes = [0u8; 20];
let mut len = 0usize;
let push = |v: i32, bytes: &mut [u8; 20], len: &mut usize| {
bytes[*len..*len + 4].copy_from_slice(&v.to_le_bytes());
*len += 4;
};
if let Some(n) = normals.and_then(|n| n.get(i)) {
for c in n {
push(
(c * config.normal_steps).round() as i32,
&mut bytes,
&mut len,
);
}
}
if let Some(uv) = uvs.and_then(|u| u.get(i)) {
for c in uv {
push((c * config.uv_steps).round() as i32, &mut bytes, &mut len);
}
}
fnv1a(&bytes[..len])
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use isomesh::RuntimeShape3;
use isomesh::fields::Sphere;
use isomesh::marching_cubes::MarchingCubes;
fn sphere_builder() -> MeshBuilder {
let mut builder = MeshBuilder::new();
let mut mc = MarchingCubes::<f32>::new();
let shape = RuntimeShape3::new([17; 3]).expect("valid shape");
mc.extract(
&Sphere::<f32>::canonical(),
&shape,
[-2.0; 3],
4.0 / 16.0,
&mut builder,
)
.expect("extraction");
builder
}
use bevy_shape::Cuboid;
#[test]
fn the_key_preserves_a_cuboids_creases_and_without_it_they_collapse() {
use isomesh::MeshBuffer;
use isomesh::weld::{Welder, epsilon_for};
let mesh = Mesh::from(Cuboid::new(1.0, 1.0, 1.0));
let (positions, indices) = from_bevy_mesh(&mesh).expect("triangle list");
let normals: Vec<[f32; 3]> = mesh
.attribute(Mesh::ATTRIBUTE_NORMAL)
.and_then(|v| v.as_float3())
.expect("a cuboid has normals")
.to_vec();
let keys = weld_keys(&mesh, WeldKeyConfig::default());
let build = || {
let mut buffer = MeshBuffer::<f32>::new();
buffer.positions.extend_from_slice(&positions);
buffer.normals.extend_from_slice(&normals);
buffer.indices.extend_from_slice(&indices);
buffer
};
let mut plain = build();
Welder::default()
.weld(&mut plain, epsilon_for(1.0f32))
.expect("weld");
assert_eq!(
plain.positions.len(),
8,
"without the key a cube is eight corners and no creases"
);
let mut kept = build();
Welder::default()
.weld_split_by(&mut kept, epsilon_for(1.0f32), &keys)
.expect("weld");
assert_eq!(
kept.positions.len(),
24,
"the key is what keeps the crease that makes a cube look like a cube"
);
}
#[test]
fn one_key_per_face_of_a_cuboid() {
let mesh = Mesh::from(Cuboid::new(2.0, 3.0, 4.0));
let keys = weld_keys(&mesh, WeldKeyConfig::default());
assert_eq!(keys.len(), 24);
let mut distinct = keys.clone();
distinct.sort_unstable();
distinct.dedup();
assert_eq!(distinct.len(), 6, "six faces, six normals, six keys");
}
#[test]
fn the_key_is_stable_across_runs() {
let mesh = Mesh::from(Cuboid::new(1.0, 1.0, 1.0));
let first = weld_keys(&mesh, WeldKeyConfig::default());
for _ in 0..32 {
assert_eq!(weld_keys(&mesh, WeldKeyConfig::default()), first);
}
}
#[test]
fn a_mesh_with_no_normals_keys_every_vertex_the_same() {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
);
let keys = weld_keys(&mesh, WeldKeyConfig::default());
assert_eq!(keys.len(), 3);
assert!(keys.windows(2).all(|w| w[0] == w[1]));
}
#[test]
fn a_coarser_quantum_merges_what_a_finer_one_splits() {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_NORMAL,
vec![[0.0f32, 0.0, 1.0], [0.05, 0.0, 0.998], [0.10, 0.0, 0.995]],
);
let fine = weld_keys(
&mesh,
WeldKeyConfig {
normal_steps: 256.0,
..WeldKeyConfig::default()
},
);
let coarse = weld_keys(
&mesh,
WeldKeyConfig {
normal_steps: 1.0,
..WeldKeyConfig::default()
},
);
assert!(fine[0] != fine[1] && fine[1] != fine[2], "fine splits them");
assert!(
coarse[0] == coarse[1] && coarse[1] == coarse[2],
"coarse buckets them together"
);
}
#[test]
fn a_cuboid_keeps_its_twenty_four_vertices() {
let mesh = Mesh::from(Cuboid::new(2.0, 3.0, 4.0));
let (positions, indices) = from_bevy_mesh(&mesh).expect("cuboid is a triangle list");
assert_eq!(
positions.len(),
24,
"welding is the caller's decision, not this function's"
);
assert_eq!(indices.len(), 36, "12 triangles");
assert!(indices.iter().all(|&i| (i as usize) < positions.len()));
let mut distinct: Vec<[u32; 3]> = positions
.iter()
.map(|p| [p[0].to_bits(), p[1].to_bits(), p[2].to_bits()])
.collect();
distinct.sort_unstable();
distinct.dedup();
assert_eq!(distinct.len(), 8, "a box has eight corners");
}
#[test]
fn an_extracted_sphere_survives_the_round_trip() {
let builder = sphere_builder();
let (want_positions, want_indices) =
(builder.positions().to_vec(), builder.indices().to_vec());
let mesh = builder.into_mesh();
let (got_positions, got_indices) = from_bevy_mesh(&mesh).expect("round trip");
assert_eq!(got_positions, want_positions);
assert_eq!(got_indices, want_indices);
}
#[test]
fn a_u16_index_buffer_widens_rather_than_being_refused() {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
);
mesh.insert_indices(Indices::U16(vec![0, 1, 2]));
let (positions, indices) = from_bevy_mesh(&mesh).expect("u16 is a valid index type");
assert_eq!(positions.len(), 3);
assert_eq!(indices, vec![0u32, 1, 2]);
}
#[test]
fn a_strip_is_refused_rather_than_expanded() {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleStrip,
RenderAssetUsages::default(),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
);
mesh.insert_indices(Indices::U32(vec![0, 1, 2]));
assert_eq!(
from_bevy_mesh(&mesh),
Err(SoupError::NotTriangleList(PrimitiveTopology::TriangleStrip))
);
}
#[test]
fn missing_positions_and_missing_indices_are_distinguished() {
let mut no_positions = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
);
no_positions.insert_indices(Indices::U32(vec![0, 1, 2]));
assert_eq!(from_bevy_mesh(&no_positions), Err(SoupError::NoPositions));
let mut no_indices = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
);
no_indices.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
);
assert_eq!(from_bevy_mesh(&no_indices), Err(SoupError::NoIndices));
}
#[test]
fn a_ragged_or_out_of_range_index_buffer_is_refused() {
let make = |indices: Vec<u32>| {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
);
mesh.insert_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![[0.0f32, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
);
mesh.insert_indices(Indices::U32(indices));
mesh
};
assert_eq!(
from_bevy_mesh(&make(vec![0, 1, 2, 0])),
Err(SoupError::RaggedIndices(4))
);
assert_eq!(
from_bevy_mesh(&make(vec![0, 1, 7])),
Err(SoupError::IndexOutOfRange {
index: 7,
vertices: 3
})
);
}
#[test]
fn extraction_writes_straight_into_the_mesh_arrays() {
let builder = sphere_builder();
let vertices = builder.vertex_count();
let triangles = builder.triangle_count();
assert!(vertices > 0 && triangles > 0);
let mesh = builder.into_mesh();
assert_eq!(mesh.count_vertices(), vertices);
assert_eq!(
mesh.indices().map(bevy_mesh::Indices::len),
Some(triangles * 3)
);
assert_eq!(mesh.primitive_topology(), PrimitiveTopology::TriangleList);
assert!(mesh.attribute(Mesh::ATTRIBUTE_POSITION).is_some());
assert!(mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some());
assert!(mesh.attribute(Mesh::ATTRIBUTE_UV_0).is_some());
}
#[test]
fn indices_are_u32() {
let mesh = sphere_builder().into_mesh();
assert!(matches!(mesh.indices(), Some(Indices::U32(_))));
}
#[test]
fn an_unpainted_mesh_has_no_color_attribute() {
let mesh = sphere_builder().into_mesh();
assert!(mesh.attribute(Mesh::ATTRIBUTE_COLOR).is_none());
}
#[test]
fn shading_the_builder_puts_colors_on_the_mesh() {
use isomesh::paint::{Edit, PaintStack, Splat};
let mut builder = sphere_builder();
let vertices = builder.vertex_count();
let log: [Edit<Sphere<f32>, Sphere<f32>, f32>; 1] = [Edit::Spray(Splat {
shape: Sphere {
center: [0.0, 1.0, 0.0],
radius: 0.6,
},
color: [1.0, 0.0, 0.0, 1.0],
softness: 0.1,
depth: 0.1,
})];
let world = PaintStack {
base: Sphere::<f32>::canonical(),
edits: &log,
background: [0.5, 0.5, 0.5, 1.0],
};
let (positions, colors) = builder.positions_and_colors_mut();
isomesh::paint::shade(positions, &world, colors);
assert_eq!(builder.colors().len(), vertices);
let mesh = builder.into_mesh();
let attribute = mesh
.attribute(Mesh::ATTRIBUTE_COLOR)
.expect("colour attribute");
assert_eq!(attribute.len(), vertices);
}
#[test]
fn the_bridged_mesh_is_still_a_closed_surface() {
let builder = sphere_builder();
let positions: Vec<[f32; 3]> = builder.positions.clone();
let indices = builder.indices.clone();
let report = isomesh::validate::validate_indexed(
&positions,
&indices,
&isomesh::validate::ValidateConfig::from_cell_size(f64::from(4.0f32 / 16.0))
.expect("valid cell size"),
);
assert!(report.is_closed(), "{report}");
assert_eq!(report.euler_characteristic, 2, "{report}");
}
#[test]
fn reset_keeps_capacity() {
let mut builder = sphere_builder();
let capacity = builder.positions.capacity();
assert!(capacity > 0);
builder.reset();
assert_eq!(builder.vertex_count(), 0);
assert_eq!(builder.positions.capacity(), capacity);
}
#[test]
fn buffer_conversion_matches_the_builder() {
let mut buffer = MeshBuffer::<f32>::new();
let mut mc = MarchingCubes::<f32>::new();
let shape = RuntimeShape3::new([17; 3]).expect("valid shape");
mc.extract(
&Sphere::<f32>::canonical(),
&shape,
[-2.0; 3],
4.0 / 16.0,
&mut buffer,
)
.expect("extraction");
let mesh = to_bevy_mesh(&buffer);
assert_eq!(mesh.count_vertices(), buffer.vertex_count());
assert_eq!(
mesh.indices().map(bevy_mesh::Indices::len),
Some(buffer.indices.len())
);
}
fn extract_nothing<S: MeshSink<Scalar = f32>>(sink: &mut S) {
let shape = RuntimeShape3::new([17; 3]).expect("valid shape");
MarchingCubes::<f32>::new()
.extract(
&Sphere::<f32>::canonical(),
&shape,
[10.0; 3],
4.0 / 16.0,
sink,
)
.expect("extraction");
}
#[test]
fn into_mesh_keeps_an_empty_mesh_out_of_the_render_world_and_readable() {
let mut builder = MeshBuilder::new();
extract_nothing(&mut builder);
assert_eq!(
builder.vertex_count(),
0,
"the sphere is not in this region"
);
let empty = builder.into_mesh();
assert_eq!(
empty.asset_usage,
RenderAssetUsages::MAIN_WORLD,
"Bevy's mesh allocator warns for every zero-vertex mesh it is handed"
);
assert_eq!(empty.count_vertices(), 0);
assert!(
empty.try_attributes().is_ok(),
"the main world still reads it"
);
assert_eq!(from_bevy_mesh(&empty), Ok((Vec::new(), Vec::new())));
let surface = sphere_builder().into_mesh();
assert!(surface.count_vertices() > 0);
assert_eq!(
surface.asset_usage,
RenderAssetUsages::default(),
"a mesh with vertices is drawn and still readable"
);
}
#[test]
fn to_bevy_mesh_keeps_an_empty_mesh_out_of_the_render_world_and_readable() {
let mut empty = MeshBuffer::<f32>::new();
extract_nothing(&mut empty);
assert_eq!(empty.vertex_count(), 0, "the sphere is not in this region");
let mesh = to_bevy_mesh(&empty);
assert_eq!(mesh.asset_usage, RenderAssetUsages::MAIN_WORLD);
assert_eq!(mesh.count_vertices(), 0);
assert!(
mesh.try_attributes().is_ok(),
"the main world still reads it"
);
assert_eq!(from_bevy_mesh(&mesh), Ok((Vec::new(), Vec::new())));
let mut surface = MeshBuffer::<f32>::new();
MarchingCubes::<f32>::new()
.extract(
&Sphere::<f32>::canonical(),
&RuntimeShape3::new([17; 3]).expect("valid shape"),
[-2.0; 3],
4.0 / 16.0,
&mut surface,
)
.expect("extraction");
assert!(surface.vertex_count() > 0);
assert_eq!(
to_bevy_mesh(&surface).asset_usage,
RenderAssetUsages::default()
);
}
#[test]
fn uvs_follow_the_dominant_axis() {
assert_eq!(
triplanar_uv([1.0, 2.0, 3.0], [1.0, 0.0, 0.0], 1.0),
[2.0, 3.0]
);
assert_eq!(
triplanar_uv([1.0, 2.0, 3.0], [0.0, 1.0, 0.0], 1.0),
[3.0, 1.0]
);
assert_eq!(
triplanar_uv([1.0, 2.0, 3.0], [0.0, 0.0, 1.0], 1.0),
[1.0, 2.0]
);
assert_eq!(
triplanar_uv([1.0, 2.0, 3.0], [0.0, 0.0, 1.0], 2.0),
[0.5, 1.0]
);
}
}