Skip to content

Commit aa626e4

Browse files
authored
Per-meshlet compressed vertex data (#15643)
# Objective - Prepare for streaming by storing vertex data per-meshlet, rather than per-mesh (this means duplicating vertices per-meshlet) - Compress vertex data to reduce the cost of this ## Solution The important parts are in from_mesh.rs, the changes to the Meshlet type in asset.rs, and the changes in meshlet_bindings.wgsl. Everything else is pretty secondary/boilerplate/straightforward changes. - Positions are quantized in centimeters with a user-provided power of 2 factor (ideally auto-determined, but that's a TODO for the future), encoded as an offset relative to the minimum value within the meshlet, and then stored as a packed list of bits using the minimum number of bits needed for each vertex position channel for that meshlet - E.g. quantize positions (lossly, throws away precision that's not needed leading to using less bits in the bitstream encoding) - Get the min/max quantized value of each X/Y/Z channel of the quantized positions within a meshlet - Encode values relative to the min value of the meshlet. E.g. convert from [min, max] to [0, max - min] - The new max value in the meshlet is (max - min), which only takes N bits, so we only need N bits to store each channel within the meshlet (lossless) - We can store the min value and that it takes N bits per channel in the meshlet metadata, and reconstruct the position from the bitstream - Normals are octahedral encoded and than snorm2x16 packed and stored as a single u32. - Would be better to implement the precise variant of octhedral encoding for extra precision (no extra decode cost), but decided to keep it simple for now and leave that as a followup - Tried doing a quantizing and bitstream encoding scheme like I did for positions, but struggled to get it smaller. Decided to go with this for simplicity for now - UVs are uncompressed and take a full 64bits per vertex which is expensive - In the future this should be improved - Tangents, as of the previous PR, are not explicitly stored and are instead derived from screen space gradients - While I'm here, split up MeshletMeshSaverLoader into two separate types Other future changes include implementing a smaller encoding of triangle data (3 u8 indices = 24 bits per triangle currently), and more disk-oriented compression schemes. References: * "A Deep Dive into UE5's Nanite Virtualized Geometry" https://advances.realtimerendering.com/s2021/Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf#page=128 (also available on youtube) * "Towards Practical Meshlet Compression" https://arxiv.org/pdf/2404.06359 * "Vertex quantization in Omniforce Game Engine" https://daniilvinn.github.io/2024/05/04/omniforce-vertex-quantization.html ## Testing - Did you test these changes? If so, how? - Converted the stanford bunny, and rendered it with a debug material showing normals, and confirmed that it's identical to what's on main. EDIT: See additional testing in the comments below. - Are there any parts that need more testing? - Could use some more size comparisons on various meshes, and testing different quantization factors. Not sure if 4 is a good default. EDIT: See additional testing in the comments below. - Also did not test runtime performance of the shaders. EDIT: See additional testing in the comments below. - How can other people (reviewers) test your changes? Is there anything specific they need to know? - Use my unholy script, replacing the meshlet example https://paste.rs/7xQHk.rs (must make MeshletMesh fields pub instead of pub crate, must add lz4_flex as a dev-dependency) (must compile with meshlet and meshlet_processor features, mesh must have only positions, normals, and UVs, no vertex colors or tangents) --- ## Migration Guide - TBD by JMS55 at the end of the release
1 parent f6cd6a4 commit aa626e4

14 files changed

+465
-170
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1209,7 +1209,7 @@ setup = [
12091209
"curl",
12101210
"-o",
12111211
"assets/models/bunny.meshlet_mesh",
1212-
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/854eb98353ad94aea1104f355fc24dbe4fda679d/bunny.meshlet_mesh",
1212+
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/8443bbdee0bf517e6c297dede7f6a46ab712ee4c/bunny.meshlet_mesh",
12131213
],
12141214
]
12151215

crates/bevy_pbr/Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ ios_simulator = ["bevy_render/ios_simulator"]
2020
# Enables the meshlet renderer for dense high-poly scenes (experimental)
2121
meshlet = ["dep:lz4_flex", "dep:thiserror", "dep:range-alloc", "dep:bevy_tasks"]
2222
# Enables processing meshes into meshlet meshes
23-
meshlet_processor = ["meshlet", "dep:meshopt", "dep:metis", "dep:itertools"]
23+
meshlet_processor = [
24+
"meshlet",
25+
"dep:meshopt",
26+
"dep:metis",
27+
"dep:itertools",
28+
"dep:bitvec",
29+
]
2430

2531
[dependencies]
2632
# bevy
@@ -53,6 +59,7 @@ range-alloc = { version = "0.1.3", optional = true }
5359
meshopt = { version = "0.3.0", optional = true }
5460
metis = { version = "0.2", optional = true }
5561
itertools = { version = "0.13", optional = true }
62+
bitvec = { version = "1", optional = true }
5663
# direct dependency required for derive macro
5764
bytemuck = { version = "1", features = ["derive", "must_cast"] }
5865
radsort = "0.1"

crates/bevy_pbr/src/meshlet/asset.rs

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use bevy_asset::{
44
saver::{AssetSaver, SavedAsset},
55
Asset, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext,
66
};
7-
use bevy_math::Vec3;
7+
use bevy_math::{Vec2, Vec3};
88
use bevy_reflect::TypePath;
99
use bevy_tasks::block_on;
1010
use bytemuck::{Pod, Zeroable};
@@ -38,30 +38,51 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 1;
3838
/// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`].
3939
#[derive(Asset, TypePath, Clone)]
4040
pub struct MeshletMesh {
41-
/// Raw vertex data bytes for the overall mesh.
42-
pub(crate) vertex_data: Arc<[u8]>,
43-
/// Indices into `vertex_data`.
44-
pub(crate) vertex_ids: Arc<[u32]>,
45-
/// Indices into `vertex_ids`.
41+
/// Quantized and bitstream-packed vertex positions for meshlet vertices.
42+
pub(crate) vertex_positions: Arc<[u32]>,
43+
/// Octahedral-encoded and 2x16snorm packed normals for meshlet vertices.
44+
pub(crate) vertex_normals: Arc<[u32]>,
45+
/// Uncompressed vertex texture coordinates for meshlet vertices.
46+
pub(crate) vertex_uvs: Arc<[Vec2]>,
47+
/// Triangle indices for meshlets.
4648
pub(crate) indices: Arc<[u8]>,
4749
/// The list of meshlets making up this mesh.
4850
pub(crate) meshlets: Arc<[Meshlet]>,
4951
/// Spherical bounding volumes.
50-
pub(crate) bounding_spheres: Arc<[MeshletBoundingSpheres]>,
52+
pub(crate) meshlet_bounding_spheres: Arc<[MeshletBoundingSpheres]>,
5153
}
5254

5355
/// A single meshlet within a [`MeshletMesh`].
5456
#[derive(Copy, Clone, Pod, Zeroable)]
5557
#[repr(C)]
5658
pub struct Meshlet {
57-
/// The offset within the parent mesh's [`MeshletMesh::vertex_ids`] buffer where the indices for this meshlet begin.
58-
pub start_vertex_id: u32,
59+
/// The bit offset within the parent mesh's [`MeshletMesh::vertex_positions`] buffer where the vertex positions for this meshlet begin.
60+
pub start_vertex_position_bit: u32,
61+
/// The offset within the parent mesh's [`MeshletMesh::vertex_normals`] and [`MeshletMesh::vertex_uvs`] buffers
62+
/// where non-position vertex attributes for this meshlet begin.
63+
pub start_vertex_attribute_id: u32,
5964
/// The offset within the parent mesh's [`MeshletMesh::indices`] buffer where the indices for this meshlet begin.
6065
pub start_index_id: u32,
6166
/// The amount of vertices in this meshlet.
62-
pub vertex_count: u32,
67+
pub vertex_count: u8,
6368
/// The amount of triangles in this meshlet.
64-
pub triangle_count: u32,
69+
pub triangle_count: u8,
70+
/// Unused.
71+
pub padding: u16,
72+
/// Number of bits used to to store the X channel of vertex positions within this meshlet.
73+
pub bits_per_vertex_position_channel_x: u8,
74+
/// Number of bits used to to store the Y channel of vertex positions within this meshlet.
75+
pub bits_per_vertex_position_channel_y: u8,
76+
/// Number of bits used to to store the Z channel of vertex positions within this meshlet.
77+
pub bits_per_vertex_position_channel_z: u8,
78+
/// Power of 2 factor used to quantize vertex positions within this meshlet.
79+
pub vertex_position_quantization_factor: u8,
80+
/// Minimum quantized X channel value of vertex positions within this meshlet.
81+
pub min_vertex_position_channel_x: f32,
82+
/// Minimum quantized Y channel value of vertex positions within this meshlet.
83+
pub min_vertex_position_channel_y: f32,
84+
/// Minimum quantized Z channel value of vertex positions within this meshlet.
85+
pub min_vertex_position_channel_z: f32,
6586
}
6687

6788
/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`].
@@ -84,13 +105,13 @@ pub struct MeshletBoundingSphere {
84105
pub radius: f32,
85106
}
86107

87-
/// An [`AssetLoader`] and [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
88-
pub struct MeshletMeshSaverLoader;
108+
/// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
109+
pub struct MeshletMeshSaver;
89110

90-
impl AssetSaver for MeshletMeshSaverLoader {
111+
impl AssetSaver for MeshletMeshSaver {
91112
type Asset = MeshletMesh;
92113
type Settings = ();
93-
type OutputLoader = Self;
114+
type OutputLoader = MeshletMeshLoader;
94115
type Error = MeshletMeshSaveOrLoadError;
95116

96117
async fn save(
@@ -111,18 +132,22 @@ impl AssetSaver for MeshletMeshSaverLoader {
111132

112133
// Compress and write asset data
113134
let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer));
114-
write_slice(&asset.vertex_data, &mut writer)?;
115-
write_slice(&asset.vertex_ids, &mut writer)?;
135+
write_slice(&asset.vertex_positions, &mut writer)?;
136+
write_slice(&asset.vertex_normals, &mut writer)?;
137+
write_slice(&asset.vertex_uvs, &mut writer)?;
116138
write_slice(&asset.indices, &mut writer)?;
117139
write_slice(&asset.meshlets, &mut writer)?;
118-
write_slice(&asset.bounding_spheres, &mut writer)?;
140+
write_slice(&asset.meshlet_bounding_spheres, &mut writer)?;
119141
writer.finish()?;
120142

121143
Ok(())
122144
}
123145
}
124146

125-
impl AssetLoader for MeshletMeshSaverLoader {
147+
/// An [`AssetLoader`] for `.meshlet_mesh` [`MeshletMesh`] assets.
148+
pub struct MeshletMeshLoader;
149+
150+
impl AssetLoader for MeshletMeshLoader {
126151
type Asset = MeshletMesh;
127152
type Settings = ();
128153
type Error = MeshletMeshSaveOrLoadError;
@@ -147,18 +172,20 @@ impl AssetLoader for MeshletMeshSaverLoader {
147172

148173
// Load and decompress asset data
149174
let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader));
150-
let vertex_data = read_slice(reader)?;
151-
let vertex_ids = read_slice(reader)?;
175+
let vertex_positions = read_slice(reader)?;
176+
let vertex_normals = read_slice(reader)?;
177+
let vertex_uvs = read_slice(reader)?;
152178
let indices = read_slice(reader)?;
153179
let meshlets = read_slice(reader)?;
154-
let bounding_spheres = read_slice(reader)?;
180+
let meshlet_bounding_spheres = read_slice(reader)?;
155181

156182
Ok(MeshletMesh {
157-
vertex_data,
158-
vertex_ids,
183+
vertex_positions,
184+
vertex_normals,
185+
vertex_uvs,
159186
indices,
160187
meshlets,
161-
bounding_spheres,
188+
meshlet_bounding_spheres,
162189
})
163190
}
164191

0 commit comments

Comments
 (0)