bevy/crates/bevy_pbr/src/meshlet/from_mesh.rs
Zachary Harrold 5241e09671
Upgrade to Rust Edition 2024 (#17967)
# Objective

- Fixes #17960

## Solution

- Followed the [edition upgrade
guide](https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html)

## Testing

- CI

---

## Summary of Changes

### Documentation Indentation

When using lists in documentation, proper indentation is now linted for.
This means subsequent lines within the same list item must start at the
same indentation level as the item.

```rust
/* Valid */
/// - Item 1
///   Run-on sentence.
/// - Item 2
struct Foo;

/* Invalid */
/// - Item 1
///     Run-on sentence.
/// - Item 2
struct Foo;
```

### Implicit `!` to `()` Conversion

`!` (the never return type, returned by `panic!`, etc.) no longer
implicitly converts to `()`. This is particularly painful for systems
with `todo!` or `panic!` statements, as they will no longer be functions
returning `()` (or `Result<()>`), making them invalid systems for
functions like `add_systems`. The ideal fix would be to accept functions
returning `!` (or rather, _not_ returning), but this is blocked on the
[stabilisation of the `!` type
itself](https://doc.rust-lang.org/std/primitive.never.html), which is
not done.

The "simple" fix would be to add an explicit `-> ()` to system
signatures (e.g., `|| { todo!() }` becomes `|| -> () { todo!() }`).
However, this is _also_ banned, as there is an existing lint which (IMO,
incorrectly) marks this as an unnecessary annotation.

So, the "fix" (read: workaround) is to put these kinds of `|| -> ! { ...
}` closuers into variables and give the variable an explicit type (e.g.,
`fn()`).

```rust
// Valid
let system: fn() = || todo!("Not implemented yet!");
app.add_systems(..., system);

// Invalid
app.add_systems(..., || todo!("Not implemented yet!"));
```

### Temporary Variable Lifetimes

The order in which temporary variables are dropped has changed. The
simple fix here is _usually_ to just assign temporaries to a named
variable before use.

### `gen` is a keyword

We can no longer use the name `gen` as it is reserved for a future
generator syntax. This involved replacing uses of the name `gen` with
`r#gen` (the raw-identifier syntax).

### Formatting has changed

Use statements have had the order of imports changed, causing a
substantial +/-3,000 diff when applied. For now, I have opted-out of
this change by amending `rustfmt.toml`

```toml
style_edition = "2021"
```

This preserves the original formatting for now, reducing the size of
this PR. It would be a simple followup to update this to 2024 and run
`cargo fmt`.

### New `use<>` Opt-Out Syntax

Lifetimes are now implicitly included in RPIT types. There was a handful
of instances where it needed to be added to satisfy the borrow checker,
but there may be more cases where it _should_ be added to avoid
breakages in user code.

### `MyUnitStruct { .. }` is an invalid pattern

Previously, you could match against unit structs (and unit enum
variants) with a `{ .. }` destructuring. This is no longer valid.

### Pretty much every use of `ref` and `mut` are gone

Pattern binding has changed to the point where these terms are largely
unused now. They still serve a purpose, but it is far more niche now.

### `iter::repeat(...).take(...)` is bad

New lint recommends using the more explicit `iter::repeat_n(..., ...)`
instead.

## Migration Guide

The lifetimes of functions using return-position impl-trait (RPIT) are
likely _more_ conservative than they had been previously. If you
encounter lifetime issues with such a function, please create an issue
to investigate the addition of `+ use<...>`.

## Notes

- Check the individual commits for a clearer breakdown for what
_actually_ changed.

---------

Co-authored-by: François Mockers <francois.mockers@vleue.com>
2025-02-24 03:54:47 +00:00

734 lines
30 KiB
Rust

use super::asset::{
Meshlet, MeshletBoundingSphere, MeshletBoundingSpheres, MeshletMesh, MeshletSimplificationError,
};
use alloc::borrow::Cow;
use bevy_math::{ops::log2, IVec3, Vec2, Vec3, Vec3Swizzles};
use bevy_platform_support::collections::HashMap;
use bevy_render::{
mesh::{Indices, Mesh},
render_resource::PrimitiveTopology,
};
use bitvec::{order::Lsb0, vec::BitVec, view::BitView};
use core::{iter, ops::Range};
use half::f16;
use itertools::Itertools;
use meshopt::{
build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi,
simplify_with_attributes_and_locks, Meshlets, SimplifyOptions, VertexDataAdapter, VertexStream,
};
use metis::{option::Opt, Graph};
use smallvec::SmallVec;
use thiserror::Error;
// Aim to have 8 meshlets per group
const TARGET_MESHLETS_PER_GROUP: usize = 8;
// Reject groups that keep over 95% of their original triangles
const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.95;
/// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`].
///
/// Snaps vertices to the nearest 1/16th of a centimeter (1/2^4).
pub const MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR: u8 = 4;
const CENTIMETERS_PER_METER: f32 = 100.0;
impl MeshletMesh {
/// Process a [`Mesh`] to generate a [`MeshletMesh`].
///
/// This process is very slow, and should be done ahead of time, and not at runtime.
///
/// # Requirements
///
/// This function requires the `meshlet_processor` cargo feature.
///
/// The input mesh must:
/// 1. Use [`PrimitiveTopology::TriangleList`]
/// 2. Use indices
/// 3. Have the exact following set of vertex attributes: `{POSITION, NORMAL, UV_0}` (tangents can be used in material shaders, but are calculated at runtime and are not stored in the mesh)
///
/// # Vertex precision
///
/// `vertex_position_quantization_factor` is the amount of precision to use when quantizing vertex positions.
///
/// Vertices are snapped to the nearest (1/2^x)th of a centimeter, where x = `vertex_position_quantization_factor`.
/// E.g. if x = 4, then vertices are snapped to the nearest 1/2^4 = 1/16th of a centimeter.
///
/// Use [`MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR`] as a default, adjusting lower to save memory and disk space, and higher to prevent artifacts if needed.
///
/// To ensure that two different meshes do not have cracks between them when placed directly next to each other:
/// * Use the same quantization factor when converting each mesh to a meshlet mesh
/// * Ensure that their [`bevy_transform::components::Transform::translation`]s are a multiple of 1/2^x centimeters (note that translations are in meters)
/// * Ensure that their [`bevy_transform::components::Transform::scale`]s are the same
/// * Ensure that their [`bevy_transform::components::Transform::rotation`]s are a multiple of 90 degrees
pub fn from_mesh(
mesh: &Mesh,
vertex_position_quantization_factor: u8,
) -> Result<Self, MeshToMeshletMeshConversionError> {
// Validate mesh format
let indices = validate_input_mesh(mesh)?;
// Get meshlet vertices
let vertex_buffer = mesh.create_packed_vertex_buffer_data();
let vertex_stride = mesh.get_vertex_size() as usize;
let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap();
let vertex_normals = bytemuck::cast_slice(&vertex_buffer[12..16]);
// Generate a position-only vertex buffer for determining triangle/meshlet connectivity
let (position_only_vertex_count, position_only_vertex_remap) = generate_vertex_remap_multi(
vertices.vertex_count,
&[VertexStream::new_with_stride::<Vec3, _>(
vertex_buffer.as_ptr(),
vertex_stride,
)],
Some(&indices),
);
// Split the mesh into an initial list of meshlets (LOD 0)
let mut meshlets = compute_meshlets(
&indices,
&vertices,
&position_only_vertex_remap,
position_only_vertex_count,
);
let mut bounding_spheres = meshlets
.iter()
.map(|meshlet| compute_meshlet_bounds(meshlet, &vertices))
.map(|bounding_sphere| MeshletBoundingSpheres {
culling_sphere: bounding_sphere,
lod_group_sphere: bounding_sphere,
lod_parent_group_sphere: MeshletBoundingSphere {
center: Vec3::ZERO,
radius: 0.0,
},
})
.collect::<Vec<_>>();
let mut simplification_errors = iter::repeat_n(
MeshletSimplificationError {
group_error: f16::ZERO,
parent_group_error: f16::MAX,
},
meshlets.len(),
)
.collect::<Vec<_>>();
let mut vertex_locks = vec![false; vertices.vertex_count];
// Build further LODs
let mut simplification_queue = 0..meshlets.len();
while simplification_queue.len() > 1 {
// For each meshlet build a list of connected meshlets (meshlets that share a vertex)
let connected_meshlets_per_meshlet = find_connected_meshlets(
simplification_queue.clone(),
&meshlets,
&position_only_vertex_remap,
position_only_vertex_count,
);
// Group meshlets into roughly groups of size TARGET_MESHLETS_PER_GROUP,
// grouping meshlets with a high number of shared vertices
let groups = group_meshlets(
&connected_meshlets_per_meshlet,
simplification_queue.clone(),
);
// Lock borders between groups to prevent cracks when simplifying
lock_group_borders(
&mut vertex_locks,
&groups,
&meshlets,
&position_only_vertex_remap,
position_only_vertex_count,
);
let next_lod_start = meshlets.len();
for group_meshlets in groups.into_iter() {
// If the group only has a single meshlet we can't simplify it
if group_meshlets.len() == 1 {
continue;
}
// Simplify the group to ~50% triangle count
let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group(
&group_meshlets,
&meshlets,
&vertices,
vertex_normals,
vertex_stride,
&vertex_locks,
) else {
// Couldn't simplify the group enough
continue;
};
// Compute LOD data for the group
let group_bounding_sphere = compute_lod_group_data(
&group_meshlets,
&mut group_error,
&mut bounding_spheres,
&mut simplification_errors,
);
// Build new meshlets using the simplified group
let new_meshlets_count = split_simplified_group_into_new_meshlets(
&simplified_group_indices,
&vertices,
&position_only_vertex_remap,
position_only_vertex_count,
&mut meshlets,
);
// Calculate the culling bounding sphere for the new meshlets and set their LOD group data
let new_meshlet_ids = (meshlets.len() - new_meshlets_count)..meshlets.len();
bounding_spheres.extend(new_meshlet_ids.clone().map(|meshlet_id| {
MeshletBoundingSpheres {
culling_sphere: compute_meshlet_bounds(meshlets.get(meshlet_id), &vertices),
lod_group_sphere: group_bounding_sphere,
lod_parent_group_sphere: MeshletBoundingSphere {
center: Vec3::ZERO,
radius: 0.0,
},
}
}));
simplification_errors.extend(iter::repeat_n(
MeshletSimplificationError {
group_error,
parent_group_error: f16::MAX,
},
new_meshlet_ids.len(),
));
}
// Set simplification queue to the list of newly created meshlets
simplification_queue = next_lod_start..meshlets.len();
}
// Copy vertex attributes per meshlet and compress
let mut vertex_positions = BitVec::<u32, Lsb0>::new();
let mut vertex_normals = Vec::new();
let mut vertex_uvs = Vec::new();
let mut bevy_meshlets = Vec::with_capacity(meshlets.len());
for (i, meshlet) in meshlets.meshlets.iter().enumerate() {
build_and_compress_per_meshlet_vertex_data(
meshlet,
meshlets.get(i).vertices,
&vertex_buffer,
vertex_stride,
&mut vertex_positions,
&mut vertex_normals,
&mut vertex_uvs,
&mut bevy_meshlets,
vertex_position_quantization_factor,
);
}
vertex_positions.set_uninitialized(false);
Ok(Self {
vertex_positions: vertex_positions.into_vec().into(),
vertex_normals: vertex_normals.into(),
vertex_uvs: vertex_uvs.into(),
indices: meshlets.triangles.into(),
meshlets: bevy_meshlets.into(),
meshlet_bounding_spheres: bounding_spheres.into(),
meshlet_simplification_errors: simplification_errors.into(),
})
}
}
fn validate_input_mesh(mesh: &Mesh) -> Result<Cow<'_, [u32]>, MeshToMeshletMeshConversionError> {
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology);
}
if mesh.attributes().map(|(attribute, _)| attribute.id).ne([
Mesh::ATTRIBUTE_POSITION.id,
Mesh::ATTRIBUTE_NORMAL.id,
Mesh::ATTRIBUTE_UV_0.id,
]) {
return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes);
}
match mesh.indices() {
Some(Indices::U32(indices)) => Ok(Cow::Borrowed(indices.as_slice())),
Some(Indices::U16(indices)) => Ok(indices.iter().map(|i| *i as u32).collect()),
_ => Err(MeshToMeshletMeshConversionError::MeshMissingIndices),
}
}
fn compute_meshlets(
indices: &[u32],
vertices: &VertexDataAdapter,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
) -> Meshlets {
// For each vertex, build a list of all triangles that use it
let mut vertices_to_triangles = vec![Vec::new(); position_only_vertex_count];
for (i, index) in indices.iter().enumerate() {
let vertex_id = position_only_vertex_remap[*index as usize];
let vertex_to_triangles = &mut vertices_to_triangles[vertex_id as usize];
vertex_to_triangles.push(i / 3);
}
// For each triangle pair, count how many vertices they share
let mut triangle_pair_to_shared_vertex_count = <HashMap<_, _>>::default();
for vertex_triangle_ids in vertices_to_triangles {
for (triangle_id1, triangle_id2) in vertex_triangle_ids.into_iter().tuple_combinations() {
let count = triangle_pair_to_shared_vertex_count
.entry((
triangle_id1.min(triangle_id2),
triangle_id1.max(triangle_id2),
))
.or_insert(0);
*count += 1;
}
}
// For each triangle, gather all other triangles that share at least one vertex along with their shared vertex count
let triangle_count = indices.len() / 3;
let mut connected_triangles_per_triangle = vec![Vec::new(); triangle_count];
for ((triangle_id1, triangle_id2), shared_vertex_count) in triangle_pair_to_shared_vertex_count
{
// We record both id1->id2 and id2->id1 as adjacency is symmetrical
connected_triangles_per_triangle[triangle_id1].push((triangle_id2, shared_vertex_count));
connected_triangles_per_triangle[triangle_id2].push((triangle_id1, shared_vertex_count));
}
// The order of triangles depends on hash traversal order; to produce deterministic results, sort them
for list in connected_triangles_per_triangle.iter_mut() {
list.sort_unstable();
}
let mut xadj = Vec::with_capacity(triangle_count + 1);
let mut adjncy = Vec::new();
let mut adjwgt = Vec::new();
for connected_triangles in connected_triangles_per_triangle {
xadj.push(adjncy.len() as i32);
for (connected_triangle_id, shared_vertex_count) in connected_triangles {
adjncy.push(connected_triangle_id as i32);
adjwgt.push(shared_vertex_count);
// TODO: Additional weight based on triangle center spatial proximity?
}
}
xadj.push(adjncy.len() as i32);
let mut options = [-1; metis::NOPTIONS];
options[metis::option::Seed::INDEX] = 17;
options[metis::option::UFactor::INDEX] = 1; // Important that there's very little imbalance between partitions
let mut meshlet_per_triangle = vec![0; triangle_count];
let partition_count = triangle_count.div_ceil(126); // Need to undershoot to prevent METIS from going over 128 triangles per meshlet
Graph::new(1, partition_count as i32, &xadj, &adjncy)
.unwrap()
.set_options(&options)
.set_adjwgt(&adjwgt)
.part_recursive(&mut meshlet_per_triangle)
.unwrap();
let mut indices_per_meshlet = vec![Vec::new(); partition_count];
for (triangle_id, meshlet) in meshlet_per_triangle.into_iter().enumerate() {
let meshlet_indices = &mut indices_per_meshlet[meshlet as usize];
let base_index = triangle_id * 3;
meshlet_indices.extend_from_slice(&indices[base_index..(base_index + 3)]);
}
// Use meshopt to build meshlets from the sets of triangles
let mut meshlets = Meshlets {
meshlets: Vec::new(),
vertices: Vec::new(),
triangles: Vec::new(),
};
for meshlet_indices in &indices_per_meshlet {
let meshlet = build_meshlets(meshlet_indices, vertices, 255, 128, 0.0);
let vertex_offset = meshlets.vertices.len() as u32;
let triangle_offset = meshlets.triangles.len() as u32;
meshlets.vertices.extend_from_slice(&meshlet.vertices);
meshlets.triangles.extend_from_slice(&meshlet.triangles);
meshlets
.meshlets
.extend(meshlet.meshlets.into_iter().map(|mut meshlet| {
meshlet.vertex_offset += vertex_offset;
meshlet.triangle_offset += triangle_offset;
meshlet
}));
}
meshlets
}
fn find_connected_meshlets(
simplification_queue: Range<usize>,
meshlets: &Meshlets,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
) -> Vec<Vec<(usize, usize)>> {
// For each vertex, build a list of all meshlets that use it
let mut vertices_to_meshlets = vec![Vec::new(); position_only_vertex_count];
for meshlet_id in simplification_queue.clone() {
let meshlet = meshlets.get(meshlet_id);
for index in meshlet.triangles {
let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize];
let vertex_to_meshlets = &mut vertices_to_meshlets[vertex_id as usize];
// Meshlets are added in order, so we can just check the last element to deduplicate,
// in the case of two triangles sharing the same vertex within a single meshlet
if vertex_to_meshlets.last() != Some(&meshlet_id) {
vertex_to_meshlets.push(meshlet_id);
}
}
}
// For each meshlet pair, count how many vertices they share
let mut meshlet_pair_to_shared_vertex_count = <HashMap<_, _>>::default();
for vertex_meshlet_ids in vertices_to_meshlets {
for (meshlet_id1, meshlet_id2) in vertex_meshlet_ids.into_iter().tuple_combinations() {
let count = meshlet_pair_to_shared_vertex_count
.entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2)))
.or_insert(0);
*count += 1;
}
}
// For each meshlet, gather all other meshlets that share at least one vertex along with their shared vertex count
let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()];
for ((meshlet_id1, meshlet_id2), shared_vertex_count) in meshlet_pair_to_shared_vertex_count {
// We record both id1->id2 and id2->id1 as adjacency is symmetrical
connected_meshlets_per_meshlet[meshlet_id1 - simplification_queue.start]
.push((meshlet_id2, shared_vertex_count));
connected_meshlets_per_meshlet[meshlet_id2 - simplification_queue.start]
.push((meshlet_id1, shared_vertex_count));
}
// The order of meshlets depends on hash traversal order; to produce deterministic results, sort them
for list in connected_meshlets_per_meshlet.iter_mut() {
list.sort_unstable();
}
connected_meshlets_per_meshlet
}
// METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf
fn group_meshlets(
connected_meshlets_per_meshlet: &[Vec<(usize, usize)>],
simplification_queue: Range<usize>,
) -> Vec<SmallVec<[usize; TARGET_MESHLETS_PER_GROUP]>> {
let mut xadj = Vec::with_capacity(simplification_queue.len() + 1);
let mut adjncy = Vec::new();
let mut adjwgt = Vec::new();
for connected_meshlets in connected_meshlets_per_meshlet {
xadj.push(adjncy.len() as i32);
for (connected_meshlet_id, shared_vertex_count) in connected_meshlets {
adjncy.push((connected_meshlet_id - simplification_queue.start) as i32);
adjwgt.push(*shared_vertex_count as i32);
// TODO: Additional weight based on meshlet spatial proximity
}
}
xadj.push(adjncy.len() as i32);
let mut options = [-1; metis::NOPTIONS];
options[metis::option::Seed::INDEX] = 17;
options[metis::option::UFactor::INDEX] = 200;
let mut group_per_meshlet = vec![0; simplification_queue.len()];
let partition_count = simplification_queue
.len()
.div_ceil(TARGET_MESHLETS_PER_GROUP); // TODO: Nanite uses groups of 8-32, probably based on some kind of heuristic
Graph::new(1, partition_count as i32, &xadj, &adjncy)
.unwrap()
.set_options(&options)
.set_adjwgt(&adjwgt)
.part_recursive(&mut group_per_meshlet)
.unwrap();
let mut groups = vec![SmallVec::new(); partition_count];
for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() {
groups[meshlet_group as usize].push(i + simplification_queue.start);
}
groups
}
fn lock_group_borders(
vertex_locks: &mut [bool],
groups: &[SmallVec<[usize; TARGET_MESHLETS_PER_GROUP]>],
meshlets: &Meshlets,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
) {
let mut position_only_locks = vec![-1; position_only_vertex_count];
// Iterate over position-only based vertices of all meshlets in all groups
for (group_id, group_meshlets) in groups.iter().enumerate() {
for meshlet_id in group_meshlets {
let meshlet = meshlets.get(*meshlet_id);
for index in meshlet.triangles {
let vertex_id =
position_only_vertex_remap[meshlet.vertices[*index as usize] as usize] as usize;
// If the vertex is not yet claimed by any group, or was already claimed by this group
if position_only_locks[vertex_id] == -1
|| position_only_locks[vertex_id] == group_id as i32
{
position_only_locks[vertex_id] = group_id as i32; // Then claim the vertex for this group
} else {
position_only_locks[vertex_id] = -2; // Else vertex was already claimed by another group or was already locked, lock it
}
}
}
}
// Lock vertices used by more than 1 group
for i in 0..vertex_locks.len() {
let vertex_id = position_only_vertex_remap[i] as usize;
vertex_locks[i] = position_only_locks[vertex_id] == -2;
}
}
fn simplify_meshlet_group(
group_meshlets: &[usize],
meshlets: &Meshlets,
vertices: &VertexDataAdapter<'_>,
vertex_normals: &[f32],
vertex_stride: usize,
vertex_locks: &[bool],
) -> Option<(Vec<u32>, f16)> {
// Build a new index buffer into the mesh vertex data by combining all meshlet data in the group
let mut group_indices = Vec::new();
for meshlet_id in group_meshlets {
let meshlet = meshlets.get(*meshlet_id);
for meshlet_index in meshlet.triangles {
group_indices.push(meshlet.vertices[*meshlet_index as usize]);
}
}
// Simplify the group to ~50% triangle count
let mut error = 0.0;
let simplified_group_indices = simplify_with_attributes_and_locks(
&group_indices,
vertices,
vertex_normals,
&[0.5; 3],
vertex_stride,
vertex_locks,
group_indices.len() / 2,
f32::MAX,
SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute,
Some(&mut error),
);
// Check if we were able to simplify at least a little
if simplified_group_indices.len() as f32 / group_indices.len() as f32
> SIMPLIFICATION_FAILURE_PERCENTAGE
{
return None;
}
Some((simplified_group_indices, f16::from_f32(error)))
}
fn compute_lod_group_data(
group_meshlets: &[usize],
group_error: &mut f16,
bounding_spheres: &mut [MeshletBoundingSpheres],
simplification_errors: &mut [MeshletSimplificationError],
) -> MeshletBoundingSphere {
let mut group_bounding_sphere = MeshletBoundingSphere {
center: Vec3::ZERO,
radius: 0.0,
};
// Compute the lod group sphere center as a weighted average of the children spheres
let mut weight = 0.0;
for meshlet_id in group_meshlets {
let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere;
group_bounding_sphere.center +=
meshlet_lod_bounding_sphere.center * meshlet_lod_bounding_sphere.radius;
weight += meshlet_lod_bounding_sphere.radius;
}
group_bounding_sphere.center /= weight;
// Force parent group sphere to contain all child group spheres (we're currently building the parent from its children)
// TODO: This does not produce the absolute minimal bounding sphere. Doing so is non-trivial.
// "Smallest enclosing balls of balls" http://www.inf.ethz.ch/personal/emo/DoctThesisFiles/fischer05.pdf
for meshlet_id in group_meshlets {
let meshlet_lod_bounding_sphere = bounding_spheres[*meshlet_id].lod_group_sphere;
let d = meshlet_lod_bounding_sphere
.center
.distance(group_bounding_sphere.center);
group_bounding_sphere.radius = group_bounding_sphere
.radius
.max(meshlet_lod_bounding_sphere.radius + d);
}
// Force parent error to be >= child error (we're currently building the parent from its children)
for meshlet_id in group_meshlets {
*group_error = group_error.max(simplification_errors[*meshlet_id].group_error);
}
// Set the children's lod parent group data to the new lod group we just made
for meshlet_id in group_meshlets {
bounding_spheres[*meshlet_id].lod_parent_group_sphere = group_bounding_sphere;
simplification_errors[*meshlet_id].parent_group_error = *group_error;
}
group_bounding_sphere
}
fn split_simplified_group_into_new_meshlets(
simplified_group_indices: &[u32],
vertices: &VertexDataAdapter<'_>,
position_only_vertex_remap: &[u32],
position_only_vertex_count: usize,
meshlets: &mut Meshlets,
) -> usize {
let simplified_meshlets = compute_meshlets(
simplified_group_indices,
vertices,
position_only_vertex_remap,
position_only_vertex_count,
);
let new_meshlets_count = simplified_meshlets.len();
let vertex_offset = meshlets.vertices.len() as u32;
let triangle_offset = meshlets.triangles.len() as u32;
meshlets
.vertices
.extend_from_slice(&simplified_meshlets.vertices);
meshlets
.triangles
.extend_from_slice(&simplified_meshlets.triangles);
meshlets
.meshlets
.extend(simplified_meshlets.meshlets.into_iter().map(|mut meshlet| {
meshlet.vertex_offset += vertex_offset;
meshlet.triangle_offset += triangle_offset;
meshlet
}));
new_meshlets_count
}
fn build_and_compress_per_meshlet_vertex_data(
meshlet: &meshopt_Meshlet,
meshlet_vertex_ids: &[u32],
vertex_buffer: &[u8],
vertex_stride: usize,
vertex_positions: &mut BitVec<u32, Lsb0>,
vertex_normals: &mut Vec<u32>,
vertex_uvs: &mut Vec<Vec2>,
meshlets: &mut Vec<Meshlet>,
vertex_position_quantization_factor: u8,
) {
let start_vertex_position_bit = vertex_positions.len() as u32;
let start_vertex_attribute_id = vertex_normals.len() as u32;
let quantization_factor =
(1 << vertex_position_quantization_factor) as f32 * CENTIMETERS_PER_METER;
let mut min_quantized_position_channels = IVec3::MAX;
let mut max_quantized_position_channels = IVec3::MIN;
// Lossy vertex compression
let mut quantized_positions = [IVec3::ZERO; 255];
for (i, vertex_id) in meshlet_vertex_ids.iter().enumerate() {
// Load source vertex attributes
let vertex_id_byte = *vertex_id as usize * vertex_stride;
let vertex_data = &vertex_buffer[vertex_id_byte..(vertex_id_byte + vertex_stride)];
let position = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[0..12]));
let normal = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[12..24]));
let uv = Vec2::from_slice(bytemuck::cast_slice(&vertex_data[24..32]));
// Copy uncompressed UV
vertex_uvs.push(uv);
// Compress normal
vertex_normals.push(pack2x16snorm(octahedral_encode(normal)));
// Quantize position to a fixed-point IVec3
let quantized_position = (position * quantization_factor + 0.5).as_ivec3();
quantized_positions[i] = quantized_position;
// Compute per X/Y/Z-channel quantized position min/max for this meshlet
min_quantized_position_channels = min_quantized_position_channels.min(quantized_position);
max_quantized_position_channels = max_quantized_position_channels.max(quantized_position);
}
// Calculate bits needed to encode each quantized vertex position channel based on the range of each channel
let range = max_quantized_position_channels - min_quantized_position_channels + 1;
let bits_per_vertex_position_channel_x = log2(range.x as f32).ceil() as u8;
let bits_per_vertex_position_channel_y = log2(range.y as f32).ceil() as u8;
let bits_per_vertex_position_channel_z = log2(range.z as f32).ceil() as u8;
// Lossless encoding of vertex positions in the minimum number of bits per channel
for quantized_position in quantized_positions.iter().take(meshlet_vertex_ids.len()) {
// Remap [range_min, range_max] IVec3 to [0, range_max - range_min] UVec3
let position = (quantized_position - min_quantized_position_channels).as_uvec3();
// Store as a packed bitstream
vertex_positions.extend_from_bitslice(
&position.x.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_x as usize],
);
vertex_positions.extend_from_bitslice(
&position.y.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_y as usize],
);
vertex_positions.extend_from_bitslice(
&position.z.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_z as usize],
);
}
meshlets.push(Meshlet {
start_vertex_position_bit,
start_vertex_attribute_id,
start_index_id: meshlet.triangle_offset,
vertex_count: meshlet.vertex_count as u8,
triangle_count: meshlet.triangle_count as u8,
padding: 0,
bits_per_vertex_position_channel_x,
bits_per_vertex_position_channel_y,
bits_per_vertex_position_channel_z,
vertex_position_quantization_factor,
min_vertex_position_channel_x: min_quantized_position_channels.x as f32,
min_vertex_position_channel_y: min_quantized_position_channels.y as f32,
min_vertex_position_channel_z: min_quantized_position_channels.z as f32,
});
}
fn compute_meshlet_bounds(
meshlet: meshopt::Meshlet<'_>,
vertices: &VertexDataAdapter<'_>,
) -> MeshletBoundingSphere {
let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices);
MeshletBoundingSphere {
center: bounds.center.into(),
radius: bounds.radius,
}
}
// TODO: Precise encode variant
fn octahedral_encode(v: Vec3) -> Vec2 {
let n = v / (v.x.abs() + v.y.abs() + v.z.abs());
let octahedral_wrap = (1.0 - n.yx().abs())
* Vec2::new(
if n.x >= 0.0 { 1.0 } else { -1.0 },
if n.y >= 0.0 { 1.0 } else { -1.0 },
);
if n.z >= 0.0 {
n.xy()
} else {
octahedral_wrap
}
}
// https://www.w3.org/TR/WGSL/#pack2x16snorm-builtin
fn pack2x16snorm(v: Vec2) -> u32 {
let v = v.clamp(Vec2::NEG_ONE, Vec2::ONE);
let v = (v * 32767.0 + 0.5).floor().as_i16vec2();
bytemuck::cast(v)
}
/// An error produced by [`MeshletMesh::from_mesh`].
#[derive(Error, Debug)]
pub enum MeshToMeshletMeshConversionError {
#[error("Mesh primitive topology is not TriangleList")]
WrongMeshPrimitiveTopology,
#[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}")]
WrongMeshVertexAttributes,
#[error("Mesh has no indices")]
MeshMissingIndices,
}