Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_pbr/src/meshlet/from_mesh.rs
6600 views
1
use crate::meshlet::asset::{MeshletAabb, MeshletAabbErrorOffset, MeshletCullData};
2
3
use super::asset::{BvhNode, Meshlet, MeshletBoundingSphere, MeshletMesh};
4
use alloc::borrow::Cow;
5
use bevy_math::{
6
bounding::{Aabb3d, BoundingSphere, BoundingVolume},
7
ops::log2,
8
IVec3, Isometry3d, Vec2, Vec3, Vec3A, Vec3Swizzles,
9
};
10
use bevy_mesh::{Indices, Mesh};
11
use bevy_platform::collections::HashMap;
12
use bevy_render::render_resource::PrimitiveTopology;
13
use bevy_tasks::{AsyncComputeTaskPool, ParallelSlice};
14
use bitvec::{order::Lsb0, vec::BitVec, view::BitView};
15
use core::{f32, ops::Range};
16
use itertools::Itertools;
17
use meshopt::{
18
build_meshlets, ffi::meshopt_Meshlet, generate_vertex_remap_multi,
19
simplify_with_attributes_and_locks, Meshlets, SimplifyOptions, VertexDataAdapter, VertexStream,
20
};
21
use metis::{option::Opt, Graph};
22
use smallvec::SmallVec;
23
use thiserror::Error;
24
use tracing::debug_span;
25
26
// Aim to have 8 meshlets per group
27
const TARGET_MESHLETS_PER_GROUP: usize = 8;
28
// Reject groups that keep over 60% of their original triangles. We'd much rather render a few
29
// extra triangles than create too many meshlets, increasing cull overhead.
30
const SIMPLIFICATION_FAILURE_PERCENTAGE: f32 = 0.60;
31
32
/// Default vertex position quantization factor for use with [`MeshletMesh::from_mesh`].
33
///
34
/// Snaps vertices to the nearest 1/16th of a centimeter (1/2^4).
35
pub const MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR: u8 = 4;
36
37
const CENTIMETERS_PER_METER: f32 = 100.0;
38
39
impl MeshletMesh {
40
/// Process a [`Mesh`] to generate a [`MeshletMesh`].
41
///
42
/// This process is very slow, and should be done ahead of time, and not at runtime.
43
///
44
/// # Requirements
45
///
46
/// This function requires the `meshlet_processor` cargo feature.
47
///
48
/// The input mesh must:
49
/// 1. Use [`PrimitiveTopology::TriangleList`]
50
/// 2. Use indices
51
/// 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)
52
///
53
/// # Vertex precision
54
///
55
/// `vertex_position_quantization_factor` is the amount of precision to use when quantizing vertex positions.
56
///
57
/// Vertices are snapped to the nearest (1/2^x)th of a centimeter, where x = `vertex_position_quantization_factor`.
58
/// E.g. if x = 4, then vertices are snapped to the nearest 1/2^4 = 1/16th of a centimeter.
59
///
60
/// 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.
61
///
62
/// To ensure that two different meshes do not have cracks between them when placed directly next to each other:
63
/// * Use the same quantization factor when converting each mesh to a meshlet mesh
64
/// * Ensure that their [`bevy_transform::components::Transform::translation`]s are a multiple of 1/2^x centimeters (note that translations are in meters)
65
/// * Ensure that their [`bevy_transform::components::Transform::scale`]s are the same
66
/// * Ensure that their [`bevy_transform::components::Transform::rotation`]s are a multiple of 90 degrees
67
pub fn from_mesh(
68
mesh: &Mesh,
69
vertex_position_quantization_factor: u8,
70
) -> Result<Self, MeshToMeshletMeshConversionError> {
71
let s = debug_span!("build meshlet mesh");
72
let _e = s.enter();
73
74
// Validate mesh format
75
let indices = validate_input_mesh(mesh)?;
76
77
// Get meshlet vertices
78
let vertex_buffer = mesh.create_packed_vertex_buffer_data();
79
let vertex_stride = mesh.get_vertex_size() as usize;
80
let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap();
81
let vertex_normals = bytemuck::cast_slice(&vertex_buffer[12..16]);
82
83
// Generate a position-only vertex buffer for determining triangle/meshlet connectivity
84
let (position_only_vertex_count, position_only_vertex_remap) = generate_vertex_remap_multi(
85
vertices.vertex_count,
86
&[VertexStream::new_with_stride::<Vec3, _>(
87
vertex_buffer.as_ptr(),
88
vertex_stride,
89
)],
90
Some(&indices),
91
);
92
93
// Split the mesh into an initial list of meshlets (LOD 0)
94
let (mut meshlets, mut cull_data) = compute_meshlets(
95
&indices,
96
&vertices,
97
&position_only_vertex_remap,
98
position_only_vertex_count,
99
None,
100
);
101
102
let mut vertex_locks = vec![false; vertices.vertex_count];
103
104
// Build further LODs
105
let mut bvh = BvhBuilder::default();
106
let mut all_groups = Vec::new();
107
let mut simplification_queue: Vec<_> = (0..meshlets.len() as u32).collect();
108
let mut stuck = Vec::new();
109
while !simplification_queue.is_empty() {
110
let s = debug_span!("simplify lod", meshlets = simplification_queue.len());
111
let _e = s.enter();
112
113
// For each meshlet build a list of connected meshlets (meshlets that share a vertex)
114
let connected_meshlets_per_meshlet = find_connected_meshlets(
115
&simplification_queue,
116
&meshlets,
117
&position_only_vertex_remap,
118
position_only_vertex_count,
119
);
120
121
// Group meshlets into roughly groups of size TARGET_MESHLETS_PER_GROUP,
122
// grouping meshlets with a high number of shared vertices
123
let groups = group_meshlets(
124
&simplification_queue,
125
&cull_data,
126
&connected_meshlets_per_meshlet,
127
);
128
simplification_queue.clear();
129
130
// Lock borders between groups to prevent cracks when simplifying
131
lock_group_borders(
132
&mut vertex_locks,
133
&groups,
134
&meshlets,
135
&position_only_vertex_remap,
136
position_only_vertex_count,
137
);
138
139
let simplified = groups.par_chunk_map(AsyncComputeTaskPool::get(), 1, |_, groups| {
140
let mut group = groups[0].clone();
141
142
// If the group only has a single meshlet we can't simplify it
143
if group.meshlets.len() == 1 {
144
return Err(group);
145
}
146
147
let s = debug_span!("simplify group", meshlets = group.meshlets.len());
148
let _e = s.enter();
149
150
// Simplify the group to ~50% triangle count
151
let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_group(
152
&group,
153
&meshlets,
154
&vertices,
155
vertex_normals,
156
vertex_stride,
157
&vertex_locks,
158
) else {
159
// Couldn't simplify the group enough
160
return Err(group);
161
};
162
163
// Force the group error to be atleast as large as all of its constituent meshlet's
164
// individual errors.
165
for &id in group.meshlets.iter() {
166
group_error = group_error.max(cull_data[id as usize].error);
167
}
168
group.parent_error = group_error;
169
170
// Build new meshlets using the simplified group
171
let new_meshlets = compute_meshlets(
172
&simplified_group_indices,
173
&vertices,
174
&position_only_vertex_remap,
175
position_only_vertex_count,
176
Some((group.lod_bounds, group.parent_error)),
177
);
178
179
Ok((group, new_meshlets))
180
});
181
182
let first_group = all_groups.len() as u32;
183
let mut passed_tris = 0;
184
let mut stuck_tris = 0;
185
for group in simplified {
186
match group {
187
Ok((group, (new_meshlets, new_cull_data))) => {
188
let start = meshlets.len();
189
merge_meshlets(&mut meshlets, new_meshlets);
190
cull_data.extend(new_cull_data);
191
let end = meshlets.len();
192
let new_meshlet_ids = start as u32..end as u32;
193
194
passed_tris += triangles_in_meshlets(&meshlets, new_meshlet_ids.clone());
195
simplification_queue.extend(new_meshlet_ids);
196
all_groups.push(group);
197
}
198
Err(group) => {
199
stuck_tris +=
200
triangles_in_meshlets(&meshlets, group.meshlets.iter().copied());
201
stuck.push(group);
202
}
203
}
204
}
205
206
// If we have enough triangles that passed, we can retry simplifying the stuck
207
// meshlets.
208
if passed_tris > stuck_tris / 3 {
209
simplification_queue.extend(stuck.drain(..).flat_map(|group| group.meshlets));
210
}
211
212
bvh.add_lod(first_group, &all_groups);
213
}
214
215
// If there's any stuck meshlets left, add another LOD level with only them
216
if !stuck.is_empty() {
217
let first_group = all_groups.len() as u32;
218
all_groups.extend(stuck);
219
bvh.add_lod(first_group, &all_groups);
220
}
221
222
let (bvh, aabb, depth) = bvh.build(&mut meshlets, all_groups, &mut cull_data);
223
224
// Copy vertex attributes per meshlet and compress
225
let mut vertex_positions = BitVec::<u32, Lsb0>::new();
226
let mut vertex_normals = Vec::new();
227
let mut vertex_uvs = Vec::new();
228
let mut bevy_meshlets = Vec::with_capacity(meshlets.len());
229
for (i, meshlet) in meshlets.meshlets.iter().enumerate() {
230
build_and_compress_per_meshlet_vertex_data(
231
meshlet,
232
meshlets.get(i).vertices,
233
&vertex_buffer,
234
vertex_stride,
235
&mut vertex_positions,
236
&mut vertex_normals,
237
&mut vertex_uvs,
238
&mut bevy_meshlets,
239
vertex_position_quantization_factor,
240
);
241
}
242
vertex_positions.set_uninitialized(false);
243
244
Ok(Self {
245
vertex_positions: vertex_positions.into_vec().into(),
246
vertex_normals: vertex_normals.into(),
247
vertex_uvs: vertex_uvs.into(),
248
indices: meshlets.triangles.into(),
249
bvh: bvh.into(),
250
meshlets: bevy_meshlets.into(),
251
meshlet_cull_data: cull_data
252
.into_iter()
253
.map(|cull_data| MeshletCullData {
254
aabb: aabb_to_meshlet(cull_data.aabb, cull_data.error, 0),
255
lod_group_sphere: sphere_to_meshlet(cull_data.lod_group_sphere),
256
})
257
.collect(),
258
aabb,
259
bvh_depth: depth,
260
})
261
}
262
}
263
264
fn validate_input_mesh(mesh: &Mesh) -> Result<Cow<'_, [u32]>, MeshToMeshletMeshConversionError> {
265
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
266
return Err(MeshToMeshletMeshConversionError::WrongMeshPrimitiveTopology);
267
}
268
269
if mesh.attributes().map(|(attribute, _)| attribute.id).ne([
270
Mesh::ATTRIBUTE_POSITION.id,
271
Mesh::ATTRIBUTE_NORMAL.id,
272
Mesh::ATTRIBUTE_UV_0.id,
273
]) {
274
return Err(MeshToMeshletMeshConversionError::WrongMeshVertexAttributes(
275
mesh.attributes()
276
.map(|(attribute, _)| format!("{attribute:?}"))
277
.collect(),
278
));
279
}
280
281
match mesh.indices() {
282
Some(Indices::U32(indices)) => Ok(Cow::Borrowed(indices.as_slice())),
283
Some(Indices::U16(indices)) => Ok(indices.iter().map(|i| *i as u32).collect()),
284
_ => Err(MeshToMeshletMeshConversionError::MeshMissingIndices),
285
}
286
}
287
288
fn triangles_in_meshlets(meshlets: &Meshlets, ids: impl IntoIterator<Item = u32>) -> u32 {
289
ids.into_iter()
290
.map(|id| meshlets.get(id as _).triangles.len() as u32 / 3)
291
.sum()
292
}
293
294
fn compute_meshlets(
295
indices: &[u32],
296
vertices: &VertexDataAdapter,
297
position_only_vertex_remap: &[u32],
298
position_only_vertex_count: usize,
299
prev_lod_data: Option<(BoundingSphere, f32)>,
300
) -> (Meshlets, Vec<TempMeshletCullData>) {
301
// For each vertex, build a list of all triangles that use it
302
let mut vertices_to_triangles = vec![Vec::new(); position_only_vertex_count];
303
for (i, index) in indices.iter().enumerate() {
304
let vertex_id = position_only_vertex_remap[*index as usize];
305
let vertex_to_triangles = &mut vertices_to_triangles[vertex_id as usize];
306
vertex_to_triangles.push(i / 3);
307
}
308
309
// For each triangle pair, count how many vertices they share
310
let mut triangle_pair_to_shared_vertex_count = <HashMap<_, _>>::default();
311
for vertex_triangle_ids in vertices_to_triangles {
312
for (triangle_id1, triangle_id2) in vertex_triangle_ids.into_iter().tuple_combinations() {
313
let count = triangle_pair_to_shared_vertex_count
314
.entry((
315
triangle_id1.min(triangle_id2),
316
triangle_id1.max(triangle_id2),
317
))
318
.or_insert(0);
319
*count += 1;
320
}
321
}
322
323
// For each triangle, gather all other triangles that share at least one vertex along with their shared vertex count
324
let triangle_count = indices.len() / 3;
325
let mut connected_triangles_per_triangle = vec![Vec::new(); triangle_count];
326
for ((triangle_id1, triangle_id2), shared_vertex_count) in triangle_pair_to_shared_vertex_count
327
{
328
// We record both id1->id2 and id2->id1 as adjacency is symmetrical
329
connected_triangles_per_triangle[triangle_id1].push((triangle_id2, shared_vertex_count));
330
connected_triangles_per_triangle[triangle_id2].push((triangle_id1, shared_vertex_count));
331
}
332
333
// The order of triangles depends on hash traversal order; to produce deterministic results, sort them
334
// TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting?
335
for list in connected_triangles_per_triangle.iter_mut() {
336
list.sort_unstable();
337
}
338
339
let mut xadj = Vec::with_capacity(triangle_count + 1);
340
let mut adjncy = Vec::new();
341
let mut adjwgt = Vec::new();
342
for connected_triangles in connected_triangles_per_triangle {
343
xadj.push(adjncy.len() as i32);
344
for (connected_triangle_id, shared_vertex_count) in connected_triangles {
345
adjncy.push(connected_triangle_id as i32);
346
adjwgt.push(shared_vertex_count);
347
// TODO: Additional weight based on triangle center spatial proximity?
348
}
349
}
350
xadj.push(adjncy.len() as i32);
351
352
let mut options = [-1; metis::NOPTIONS];
353
options[metis::option::Seed::INDEX] = 17;
354
options[metis::option::UFactor::INDEX] = 1; // Important that there's very little imbalance between partitions
355
356
let mut meshlet_per_triangle = vec![0; triangle_count];
357
let partition_count = triangle_count.div_ceil(126); // Need to undershoot to prevent METIS from going over 128 triangles per meshlet
358
Graph::new(1, partition_count as i32, &xadj, &adjncy)
359
.unwrap()
360
.set_options(&options)
361
.set_adjwgt(&adjwgt)
362
.part_recursive(&mut meshlet_per_triangle)
363
.unwrap();
364
365
let mut indices_per_meshlet = vec![Vec::new(); partition_count];
366
for (triangle_id, meshlet) in meshlet_per_triangle.into_iter().enumerate() {
367
let meshlet_indices = &mut indices_per_meshlet[meshlet as usize];
368
let base_index = triangle_id * 3;
369
meshlet_indices.extend_from_slice(&indices[base_index..(base_index + 3)]);
370
}
371
372
// Use meshopt to build meshlets from the sets of triangles
373
let mut meshlets = Meshlets {
374
meshlets: Vec::new(),
375
vertices: Vec::new(),
376
triangles: Vec::new(),
377
};
378
let mut cull_data = Vec::new();
379
let get_vertex = |&v: &u32| {
380
*bytemuck::from_bytes::<Vec3>(
381
&vertices.reader.get_ref()
382
[vertices.position_offset + v as usize * vertices.vertex_stride..][..12],
383
)
384
};
385
for meshlet_indices in &indices_per_meshlet {
386
let meshlet = build_meshlets(meshlet_indices, vertices, 255, 128, 0.0);
387
for meshlet in meshlet.iter() {
388
let (lod_group_sphere, error) = prev_lod_data.unwrap_or_else(|| {
389
let bounds = meshopt::compute_meshlet_bounds(meshlet, vertices);
390
(BoundingSphere::new(bounds.center, bounds.radius), 0.0)
391
});
392
393
cull_data.push(TempMeshletCullData {
394
aabb: Aabb3d::from_point_cloud(
395
Isometry3d::IDENTITY,
396
meshlet.vertices.iter().map(get_vertex),
397
),
398
lod_group_sphere,
399
error,
400
});
401
}
402
merge_meshlets(&mut meshlets, meshlet);
403
}
404
(meshlets, cull_data)
405
}
406
407
fn find_connected_meshlets(
408
simplification_queue: &[u32],
409
meshlets: &Meshlets,
410
position_only_vertex_remap: &[u32],
411
position_only_vertex_count: usize,
412
) -> Vec<Vec<(usize, usize)>> {
413
// For each vertex, build a list of all meshlets that use it
414
let mut vertices_to_meshlets = vec![Vec::new(); position_only_vertex_count];
415
for (id_index, &meshlet_id) in simplification_queue.iter().enumerate() {
416
let meshlet = meshlets.get(meshlet_id as _);
417
for index in meshlet.triangles {
418
let vertex_id = position_only_vertex_remap[meshlet.vertices[*index as usize] as usize];
419
let vertex_to_meshlets = &mut vertices_to_meshlets[vertex_id as usize];
420
// Meshlets are added in order, so we can just check the last element to deduplicate,
421
// in the case of two triangles sharing the same vertex within a single meshlet
422
if vertex_to_meshlets.last() != Some(&id_index) {
423
vertex_to_meshlets.push(id_index);
424
}
425
}
426
}
427
428
// For each meshlet pair, count how many vertices they share
429
let mut meshlet_pair_to_shared_vertex_count = <HashMap<_, _>>::default();
430
for vertex_meshlet_ids in vertices_to_meshlets {
431
for (meshlet_id1, meshlet_id2) in vertex_meshlet_ids.into_iter().tuple_combinations() {
432
let count = meshlet_pair_to_shared_vertex_count
433
.entry((meshlet_id1.min(meshlet_id2), meshlet_id1.max(meshlet_id2)))
434
.or_insert(0);
435
*count += 1;
436
}
437
}
438
439
// For each meshlet, gather all other meshlets that share at least one vertex along with their shared vertex count
440
let mut connected_meshlets_per_meshlet = vec![Vec::new(); simplification_queue.len()];
441
for ((meshlet_id1, meshlet_id2), shared_vertex_count) in meshlet_pair_to_shared_vertex_count {
442
// We record both id1->id2 and id2->id1 as adjacency is symmetrical
443
connected_meshlets_per_meshlet[meshlet_id1].push((meshlet_id2, shared_vertex_count));
444
connected_meshlets_per_meshlet[meshlet_id2].push((meshlet_id1, shared_vertex_count));
445
}
446
447
// The order of meshlets depends on hash traversal order; to produce deterministic results, sort them
448
// TODO: Wouldn't it be faster to use a `BTreeMap` above instead of `HashMap` + sorting?
449
for list in connected_meshlets_per_meshlet.iter_mut() {
450
list.sort_unstable();
451
}
452
453
connected_meshlets_per_meshlet
454
}
455
456
// METIS manual: https://github.com/KarypisLab/METIS/blob/e0f1b88b8efcb24ffa0ec55eabb78fbe61e58ae7/manual/manual.pdf
457
fn group_meshlets(
458
simplification_queue: &[u32],
459
meshlet_cull_data: &[TempMeshletCullData],
460
connected_meshlets_per_meshlet: &[Vec<(usize, usize)>],
461
) -> Vec<TempMeshletGroup> {
462
let mut xadj = Vec::with_capacity(simplification_queue.len() + 1);
463
let mut adjncy = Vec::new();
464
let mut adjwgt = Vec::new();
465
for connected_meshlets in connected_meshlets_per_meshlet {
466
xadj.push(adjncy.len() as i32);
467
for (connected_meshlet_id, shared_vertex_count) in connected_meshlets {
468
adjncy.push(*connected_meshlet_id as i32);
469
adjwgt.push(*shared_vertex_count as i32);
470
// TODO: Additional weight based on meshlet spatial proximity
471
}
472
}
473
xadj.push(adjncy.len() as i32);
474
475
let mut options = [-1; metis::NOPTIONS];
476
options[metis::option::Seed::INDEX] = 17;
477
options[metis::option::UFactor::INDEX] = 200;
478
479
let mut group_per_meshlet = vec![0; simplification_queue.len()];
480
let partition_count = simplification_queue
481
.len()
482
.div_ceil(TARGET_MESHLETS_PER_GROUP); // TODO: Nanite uses groups of 8-32, probably based on some kind of heuristic
483
Graph::new(1, partition_count as i32, &xadj, &adjncy)
484
.unwrap()
485
.set_options(&options)
486
.set_adjwgt(&adjwgt)
487
.part_recursive(&mut group_per_meshlet)
488
.unwrap();
489
490
let mut groups = vec![TempMeshletGroup::default(); partition_count];
491
for (i, meshlet_group) in group_per_meshlet.into_iter().enumerate() {
492
let group = &mut groups[meshlet_group as usize];
493
let meshlet_id = simplification_queue[i];
494
495
group.meshlets.push(meshlet_id);
496
let data = &meshlet_cull_data[meshlet_id as usize];
497
group.aabb = group.aabb.merge(&data.aabb);
498
group.lod_bounds = merge_spheres(group.lod_bounds, data.lod_group_sphere);
499
}
500
groups
501
}
502
503
fn lock_group_borders(
504
vertex_locks: &mut [bool],
505
groups: &[TempMeshletGroup],
506
meshlets: &Meshlets,
507
position_only_vertex_remap: &[u32],
508
position_only_vertex_count: usize,
509
) {
510
let mut position_only_locks = vec![-1; position_only_vertex_count];
511
512
// Iterate over position-only based vertices of all meshlets in all groups
513
for (group_id, group) in groups.iter().enumerate() {
514
for &meshlet_id in group.meshlets.iter() {
515
let meshlet = meshlets.get(meshlet_id as usize);
516
for index in meshlet.triangles {
517
let vertex_id =
518
position_only_vertex_remap[meshlet.vertices[*index as usize] as usize] as usize;
519
520
// If the vertex is not yet claimed by any group, or was already claimed by this group
521
if position_only_locks[vertex_id] == -1
522
|| position_only_locks[vertex_id] == group_id as i32
523
{
524
position_only_locks[vertex_id] = group_id as i32; // Then claim the vertex for this group
525
} else {
526
position_only_locks[vertex_id] = -2; // Else vertex was already claimed by another group or was already locked, lock it
527
}
528
}
529
}
530
}
531
532
// Lock vertices used by more than 1 group
533
for i in 0..vertex_locks.len() {
534
let vertex_id = position_only_vertex_remap[i] as usize;
535
vertex_locks[i] = position_only_locks[vertex_id] == -2;
536
}
537
}
538
539
fn simplify_meshlet_group(
540
group: &TempMeshletGroup,
541
meshlets: &Meshlets,
542
vertices: &VertexDataAdapter<'_>,
543
vertex_normals: &[f32],
544
vertex_stride: usize,
545
vertex_locks: &[bool],
546
) -> Option<(Vec<u32>, f32)> {
547
// Build a new index buffer into the mesh vertex data by combining all meshlet data in the group
548
let group_indices = group
549
.meshlets
550
.iter()
551
.flat_map(|&meshlet_id| {
552
let meshlet = meshlets.get(meshlet_id as _);
553
meshlet
554
.triangles
555
.iter()
556
.map(|&meshlet_index| meshlet.vertices[meshlet_index as usize])
557
})
558
.collect::<Vec<_>>();
559
560
// Simplify the group to ~50% triangle count
561
let mut error = 0.0;
562
let simplified_group_indices = simplify_with_attributes_and_locks(
563
&group_indices,
564
vertices,
565
vertex_normals,
566
&[0.5; 3],
567
vertex_stride,
568
vertex_locks,
569
group_indices.len() / 2,
570
f32::MAX,
571
SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute,
572
Some(&mut error),
573
);
574
575
// Check if we were able to simplify
576
if simplified_group_indices.len() as f32 / group_indices.len() as f32
577
> SIMPLIFICATION_FAILURE_PERCENTAGE
578
{
579
return None;
580
}
581
582
Some((simplified_group_indices, error))
583
}
584
585
fn merge_meshlets(meshlets: &mut Meshlets, merge: Meshlets) {
586
let vertex_offset = meshlets.vertices.len() as u32;
587
let triangle_offset = meshlets.triangles.len() as u32;
588
meshlets.vertices.extend_from_slice(&merge.vertices);
589
meshlets.triangles.extend_from_slice(&merge.triangles);
590
meshlets
591
.meshlets
592
.extend(merge.meshlets.into_iter().map(|mut meshlet| {
593
meshlet.vertex_offset += vertex_offset;
594
meshlet.triangle_offset += triangle_offset;
595
meshlet
596
}));
597
}
598
599
fn build_and_compress_per_meshlet_vertex_data(
600
meshlet: &meshopt_Meshlet,
601
meshlet_vertex_ids: &[u32],
602
vertex_buffer: &[u8],
603
vertex_stride: usize,
604
vertex_positions: &mut BitVec<u32, Lsb0>,
605
vertex_normals: &mut Vec<u32>,
606
vertex_uvs: &mut Vec<Vec2>,
607
meshlets: &mut Vec<Meshlet>,
608
vertex_position_quantization_factor: u8,
609
) {
610
let start_vertex_position_bit = vertex_positions.len() as u32;
611
let start_vertex_attribute_id = vertex_normals.len() as u32;
612
613
let quantization_factor =
614
(1 << vertex_position_quantization_factor) as f32 * CENTIMETERS_PER_METER;
615
616
let mut min_quantized_position_channels = IVec3::MAX;
617
let mut max_quantized_position_channels = IVec3::MIN;
618
619
// Lossy vertex compression
620
let mut quantized_positions = [IVec3::ZERO; 255];
621
for (i, vertex_id) in meshlet_vertex_ids.iter().enumerate() {
622
// Load source vertex attributes
623
let vertex_id_byte = *vertex_id as usize * vertex_stride;
624
let vertex_data = &vertex_buffer[vertex_id_byte..(vertex_id_byte + vertex_stride)];
625
let position = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[0..12]));
626
let normal = Vec3::from_slice(bytemuck::cast_slice(&vertex_data[12..24]));
627
let uv = Vec2::from_slice(bytemuck::cast_slice(&vertex_data[24..32]));
628
629
// Copy uncompressed UV
630
vertex_uvs.push(uv);
631
632
// Compress normal
633
vertex_normals.push(pack2x16snorm(octahedral_encode(normal)));
634
635
// Quantize position to a fixed-point IVec3
636
let quantized_position = (position * quantization_factor + 0.5).as_ivec3();
637
quantized_positions[i] = quantized_position;
638
639
// Compute per X/Y/Z-channel quantized position min/max for this meshlet
640
min_quantized_position_channels = min_quantized_position_channels.min(quantized_position);
641
max_quantized_position_channels = max_quantized_position_channels.max(quantized_position);
642
}
643
644
// Calculate bits needed to encode each quantized vertex position channel based on the range of each channel
645
let range = max_quantized_position_channels - min_quantized_position_channels + 1;
646
let bits_per_vertex_position_channel_x = log2(range.x as f32).ceil() as u8;
647
let bits_per_vertex_position_channel_y = log2(range.y as f32).ceil() as u8;
648
let bits_per_vertex_position_channel_z = log2(range.z as f32).ceil() as u8;
649
650
// Lossless encoding of vertex positions in the minimum number of bits per channel
651
for quantized_position in quantized_positions.iter().take(meshlet_vertex_ids.len()) {
652
// Remap [range_min, range_max] IVec3 to [0, range_max - range_min] UVec3
653
let position = (quantized_position - min_quantized_position_channels).as_uvec3();
654
655
// Store as a packed bitstream
656
vertex_positions.extend_from_bitslice(
657
&position.x.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_x as usize],
658
);
659
vertex_positions.extend_from_bitslice(
660
&position.y.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_y as usize],
661
);
662
vertex_positions.extend_from_bitslice(
663
&position.z.view_bits::<Lsb0>()[..bits_per_vertex_position_channel_z as usize],
664
);
665
}
666
667
meshlets.push(Meshlet {
668
start_vertex_position_bit,
669
start_vertex_attribute_id,
670
start_index_id: meshlet.triangle_offset,
671
vertex_count: meshlet.vertex_count as u8,
672
triangle_count: meshlet.triangle_count as u8,
673
padding: 0,
674
bits_per_vertex_position_channel_x,
675
bits_per_vertex_position_channel_y,
676
bits_per_vertex_position_channel_z,
677
vertex_position_quantization_factor,
678
min_vertex_position_channel_x: min_quantized_position_channels.x as f32,
679
min_vertex_position_channel_y: min_quantized_position_channels.y as f32,
680
min_vertex_position_channel_z: min_quantized_position_channels.z as f32,
681
});
682
}
683
684
fn merge_spheres(a: BoundingSphere, b: BoundingSphere) -> BoundingSphere {
685
let sr = a.radius().min(b.radius());
686
let br = a.radius().max(b.radius());
687
let len = a.center.distance(b.center);
688
if len + sr <= br || sr == 0.0 || len == 0.0 {
689
if a.radius() > b.radius() {
690
a
691
} else {
692
b
693
}
694
} else {
695
let radius = (sr + br + len) / 2.0;
696
let center =
697
(a.center + b.center + (a.radius() - b.radius()) * (a.center - b.center) / len) / 2.0;
698
BoundingSphere::new(center, radius)
699
}
700
}
701
702
#[derive(Copy, Clone)]
703
struct TempMeshletCullData {
704
aabb: Aabb3d,
705
lod_group_sphere: BoundingSphere,
706
error: f32,
707
}
708
709
#[derive(Clone)]
710
struct TempMeshletGroup {
711
aabb: Aabb3d,
712
lod_bounds: BoundingSphere,
713
parent_error: f32,
714
meshlets: SmallVec<[u32; TARGET_MESHLETS_PER_GROUP]>,
715
}
716
717
impl Default for TempMeshletGroup {
718
fn default() -> Self {
719
Self {
720
aabb: aabb_default(), // Default AABB to merge into
721
lod_bounds: BoundingSphere::new(Vec3A::ZERO, 0.0),
722
parent_error: f32::MAX,
723
meshlets: SmallVec::new(),
724
}
725
}
726
}
727
728
// All the BVH build code was stolen from https://github.com/SparkyPotato/radiance/blob/4aa17a3a5be7a0466dc69713e249bbcee9f46057/crates/rad-renderer/src/assets/mesh/virtual_mesh.rs because it works and I'm lazy and don't want to reimplement it
729
struct TempBvhNode {
730
group: u32,
731
aabb: Aabb3d,
732
children: SmallVec<[u32; 8]>,
733
}
734
735
#[derive(Default)]
736
struct BvhBuilder {
737
nodes: Vec<TempBvhNode>,
738
lods: Vec<Range<u32>>,
739
}
740
741
impl BvhBuilder {
742
fn add_lod(&mut self, offset: u32, all_groups: &[TempMeshletGroup]) {
743
let first = self.nodes.len() as u32;
744
self.nodes.extend(
745
all_groups
746
.iter()
747
.enumerate()
748
.skip(offset as _)
749
.map(|(i, group)| TempBvhNode {
750
group: i as u32,
751
aabb: group.aabb,
752
children: SmallVec::new(),
753
}),
754
);
755
let end = self.nodes.len() as u32;
756
if first != end {
757
self.lods.push(first..end);
758
}
759
}
760
761
fn surface_area(&self, nodes: &[u32]) -> f32 {
762
nodes
763
.iter()
764
.map(|&x| self.nodes[x as usize].aabb)
765
.reduce(|a, b| a.merge(&b))
766
.expect("cannot find surface area of zero nodes")
767
.visible_area()
768
}
769
770
fn sort_nodes_by_sah(&self, nodes: &mut [u32], splits: [usize; 8]) {
771
// We use a BVH8, so just recursively binary split 3 times for near-optimal SAH
772
for i in 0..3 {
773
let parts = 1 << i; // 2^i
774
let nodes_per_split = 8 >> i; // 8 / 2^i
775
let half_count = nodes_per_split / 2;
776
let mut offset = 0;
777
for p in 0..parts {
778
let first = p * nodes_per_split;
779
let mut s0 = 0;
780
let mut s1 = 0;
781
for i in 0..half_count {
782
s0 += splits[first + i];
783
s1 += splits[first + half_count + i];
784
}
785
let c = s0 + s1;
786
let nodes = &mut nodes[offset..(offset + c)];
787
offset += c;
788
789
let mut cost = f32::MAX;
790
let mut axis = 0;
791
let key = |x, ax| self.nodes[x as usize].aabb.center()[ax];
792
for ax in 0..3 {
793
nodes.sort_unstable_by(|&x, &y| key(x, ax).partial_cmp(&key(y, ax)).unwrap());
794
let (left, right) = nodes.split_at(s0);
795
let c = self.surface_area(left) + self.surface_area(right);
796
if c < cost {
797
axis = ax;
798
cost = c;
799
}
800
}
801
if axis != 2 {
802
nodes.sort_unstable_by(|&x, &y| {
803
key(x, axis).partial_cmp(&key(y, axis)).unwrap()
804
});
805
}
806
}
807
}
808
}
809
810
fn build_temp_inner(&mut self, nodes: &mut [u32], optimize: bool) -> u32 {
811
let count = nodes.len();
812
if count == 1 {
813
nodes[0]
814
} else if count <= 8 {
815
let i = self.nodes.len();
816
self.nodes.push(TempBvhNode {
817
group: u32::MAX,
818
aabb: aabb_default(),
819
children: nodes.iter().copied().collect(),
820
});
821
i as _
822
} else {
823
// We need to split the nodes into 8 groups, with the smallest possible tree depth.
824
// Additionally, no child should be more than one level deeper than the others.
825
// At `l` levels, we can fit upto 8^l nodes.
826
// The `max_child_size` is the largest power of 8 <= `count` (any larger and we'd have
827
// unfilled nodes).
828
// The `min_child_size` is thus 1 level (8 times) smaller.
829
// After distributing `min_child_size` to all children, we have distributed
830
// `min_child_size * 8` nodes (== `max_child_size`).
831
// The remaining nodes are then distributed left to right.
832
let max_child_size = 1 << ((count.ilog2() / 3) * 3);
833
let min_child_size = max_child_size >> 3;
834
let max_extra_per_node = max_child_size - min_child_size;
835
let mut extra = count - max_child_size; // 8 * min_child_size
836
let splits = core::array::from_fn(|_| {
837
let size = extra.min(max_extra_per_node);
838
extra -= size;
839
min_child_size + size
840
});
841
842
if optimize {
843
self.sort_nodes_by_sah(nodes, splits);
844
}
845
846
let mut offset = 0;
847
let children = splits
848
.into_iter()
849
.map(|size| {
850
let i = self.build_temp_inner(&mut nodes[offset..(offset + size)], optimize);
851
offset += size;
852
i
853
})
854
.collect();
855
856
let i = self.nodes.len();
857
self.nodes.push(TempBvhNode {
858
group: u32::MAX,
859
aabb: aabb_default(),
860
children,
861
});
862
i as _
863
}
864
}
865
866
fn build_temp(&mut self) -> u32 {
867
let mut lods = Vec::with_capacity(self.lods.len());
868
for lod in core::mem::take(&mut self.lods) {
869
let mut lod: Vec<_> = lod.collect();
870
let root = self.build_temp_inner(&mut lod, true);
871
let node = &self.nodes[root as usize];
872
if node.group != u32::MAX || node.children.len() == 8 {
873
lods.push(root);
874
} else {
875
lods.extend(node.children.iter().copied());
876
}
877
}
878
self.build_temp_inner(&mut lods, false)
879
}
880
881
fn build_inner(
882
&self,
883
groups: &[TempMeshletGroup],
884
out: &mut Vec<BvhNode>,
885
max_depth: &mut u32,
886
node: u32,
887
depth: u32,
888
) -> u32 {
889
*max_depth = depth.max(*max_depth);
890
let node = &self.nodes[node as usize];
891
let onode = out.len();
892
out.push(BvhNode::default());
893
894
for (i, &child_id) in node.children.iter().enumerate() {
895
let child = &self.nodes[child_id as usize];
896
if child.group != u32::MAX {
897
let group = &groups[child.group as usize];
898
let out = &mut out[onode];
899
out.aabbs[i] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]);
900
out.lod_bounds[i] = sphere_to_meshlet(group.lod_bounds);
901
out.child_counts[i] = group.meshlets[1] as _;
902
} else {
903
let child_id = self.build_inner(groups, out, max_depth, child_id, depth + 1);
904
let child = &out[child_id as usize];
905
let mut aabb = aabb_default();
906
let mut parent_error = 0.0f32;
907
let mut lod_bounds = BoundingSphere::new(Vec3A::ZERO, 0.0);
908
for i in 0..8 {
909
if child.child_counts[i] == 0 {
910
break;
911
}
912
913
aabb = aabb.merge(&Aabb3d::new(
914
child.aabbs[i].center,
915
child.aabbs[i].half_extent,
916
));
917
lod_bounds = merge_spheres(
918
lod_bounds,
919
BoundingSphere::new(child.lod_bounds[i].center, child.lod_bounds[i].radius),
920
);
921
parent_error = parent_error.max(child.aabbs[i].error);
922
}
923
924
let out = &mut out[onode];
925
out.aabbs[i] = aabb_to_meshlet(aabb, parent_error, child_id);
926
out.lod_bounds[i] = sphere_to_meshlet(lod_bounds);
927
out.child_counts[i] = u8::MAX;
928
}
929
}
930
931
onode as _
932
}
933
934
fn build(
935
mut self,
936
meshlets: &mut Meshlets,
937
mut groups: Vec<TempMeshletGroup>,
938
cull_data: &mut Vec<TempMeshletCullData>,
939
) -> (Vec<BvhNode>, MeshletAabb, u32) {
940
// The BVH requires group meshlets to be contiguous, so remap them first.
941
let mut remap = Vec::with_capacity(meshlets.meshlets.len());
942
let mut remapped_cull_data = Vec::with_capacity(cull_data.len());
943
for group in groups.iter_mut() {
944
let first = remap.len() as u32;
945
let count = group.meshlets.len() as u32;
946
remap.extend(
947
group
948
.meshlets
949
.iter()
950
.map(|&m| meshlets.meshlets[m as usize]),
951
);
952
remapped_cull_data.extend(group.meshlets.iter().map(|&m| cull_data[m as usize]));
953
group.meshlets.resize(2, 0);
954
group.meshlets[0] = first;
955
group.meshlets[1] = count;
956
}
957
meshlets.meshlets = remap;
958
*cull_data = remapped_cull_data;
959
960
let mut out = vec![];
961
let mut aabb = aabb_default();
962
let mut max_depth = 0;
963
964
if self.nodes.len() == 1 {
965
let mut o = BvhNode::default();
966
let group = &groups[0];
967
o.aabbs[0] = aabb_to_meshlet(group.aabb, group.parent_error, group.meshlets[0]);
968
o.lod_bounds[0] = sphere_to_meshlet(group.lod_bounds);
969
o.child_counts[0] = group.meshlets[1] as _;
970
out.push(o);
971
aabb = group.aabb;
972
max_depth = 1;
973
} else {
974
let root = self.build_temp();
975
let root = self.build_inner(&groups, &mut out, &mut max_depth, root, 1);
976
assert_eq!(root, 0, "root must be 0");
977
978
let root = &out[0];
979
for i in 0..8 {
980
if root.child_counts[i] == 0 {
981
break;
982
}
983
984
aabb = aabb.merge(&Aabb3d::new(
985
root.aabbs[i].center,
986
root.aabbs[i].half_extent,
987
));
988
}
989
}
990
991
let mut reachable = vec![false; meshlets.meshlets.len()];
992
verify_bvh(&out, cull_data, &mut reachable, 0);
993
assert!(
994
reachable.iter().all(|&x| x),
995
"all meshlets must be reachable"
996
);
997
998
(
999
out,
1000
MeshletAabb {
1001
center: aabb.center().into(),
1002
half_extent: aabb.half_size().into(),
1003
},
1004
max_depth,
1005
)
1006
}
1007
}
1008
1009
fn verify_bvh(
1010
out: &[BvhNode],
1011
cull_data: &[TempMeshletCullData],
1012
reachable: &mut [bool],
1013
node: u32,
1014
) {
1015
let node = &out[node as usize];
1016
for i in 0..8 {
1017
let sphere = node.lod_bounds[i];
1018
let error = node.aabbs[i].error;
1019
if node.child_counts[i] == u8::MAX {
1020
let child = &out[node.aabbs[i].child_offset as usize];
1021
for i in 0..8 {
1022
if child.child_counts[i] == 0 {
1023
break;
1024
}
1025
assert!(
1026
child.aabbs[i].error <= error,
1027
"BVH errors are not monotonic"
1028
);
1029
let sphere_error = (sphere.center - child.lod_bounds[i].center).length()
1030
- (sphere.radius - child.lod_bounds[i].radius);
1031
assert!(
1032
sphere_error <= 0.0001,
1033
"BVH lod spheres are not monotonic ({sphere_error})"
1034
);
1035
}
1036
verify_bvh(out, cull_data, reachable, node.aabbs[i].child_offset);
1037
} else {
1038
for m in 0..node.child_counts[i] as u32 {
1039
let mid = (m + node.aabbs[i].child_offset) as usize;
1040
let meshlet = &cull_data[mid];
1041
assert!(meshlet.error <= error, "meshlet errors are not monotonic");
1042
let sphere_error = (Vec3A::from(sphere.center) - meshlet.lod_group_sphere.center)
1043
.length()
1044
- (sphere.radius - meshlet.lod_group_sphere.radius());
1045
assert!(
1046
sphere_error <= 0.0001,
1047
"meshlet lod spheres are not monotonic: ({sphere_error})"
1048
);
1049
reachable[mid] = true;
1050
}
1051
}
1052
}
1053
}
1054
1055
fn aabb_default() -> Aabb3d {
1056
Aabb3d {
1057
min: Vec3A::INFINITY,
1058
max: Vec3A::NEG_INFINITY,
1059
}
1060
}
1061
1062
fn aabb_to_meshlet(aabb: Aabb3d, error: f32, child_offset: u32) -> MeshletAabbErrorOffset {
1063
MeshletAabbErrorOffset {
1064
center: aabb.center().into(),
1065
error,
1066
half_extent: aabb.half_size().into(),
1067
child_offset,
1068
}
1069
}
1070
1071
fn sphere_to_meshlet(sphere: BoundingSphere) -> MeshletBoundingSphere {
1072
MeshletBoundingSphere {
1073
center: sphere.center.into(),
1074
radius: sphere.radius(),
1075
}
1076
}
1077
1078
// TODO: Precise encode variant
1079
fn octahedral_encode(v: Vec3) -> Vec2 {
1080
let n = v / (v.x.abs() + v.y.abs() + v.z.abs());
1081
let octahedral_wrap = (1.0 - n.yx().abs())
1082
* Vec2::new(
1083
if n.x >= 0.0 { 1.0 } else { -1.0 },
1084
if n.y >= 0.0 { 1.0 } else { -1.0 },
1085
);
1086
if n.z >= 0.0 {
1087
n.xy()
1088
} else {
1089
octahedral_wrap
1090
}
1091
}
1092
1093
// https://www.w3.org/TR/WGSL/#pack2x16snorm-builtin
1094
fn pack2x16snorm(v: Vec2) -> u32 {
1095
let v = v.clamp(Vec2::NEG_ONE, Vec2::ONE);
1096
let v = (v * 32767.0 + 0.5).floor().as_i16vec2();
1097
bytemuck::cast(v)
1098
}
1099
1100
/// An error produced by [`MeshletMesh::from_mesh`].
1101
#[derive(Error, Debug)]
1102
pub enum MeshToMeshletMeshConversionError {
1103
#[error("Mesh primitive topology is not TriangleList")]
1104
WrongMeshPrimitiveTopology,
1105
#[error("Mesh vertex attributes are not {{POSITION, NORMAL, UV_0}}: {0:?}")]
1106
WrongMeshVertexAttributes(Vec<String>),
1107
#[error("Mesh has no indices")]
1108
MeshMissingIndices,
1109
}
1110
1111