More triangles/vertices per meshlet (#15023)

### Builder changes
- Increased meshlet max vertices/triangles from 64v/64t to 255v/128t
(meshoptimizer won't allow 256v sadly). This gives us a much greater
percentage of meshlets with max triangle count (128). Still not perfect,
we still end up with some tiny <=10 triangle meshlets that never really
get simplified, but it's progress.
- Removed the error target limit. Now we allow meshoptimizer to simplify
as much as possible. No reason to cap this out, as the cluster culling
code will choose a good LOD level anyways. Again leads to higher quality
LOD trees.
- After some discussion and consulting the Nanite slides again, changed
meshlet group error from _adding_ the max child's error to the group
error, to doing `group_error = max(group_error, max_child_error)`. Error
is already cumulative between LODs as the edges we're collapsing during
simplification get longer each time.
- Bumped the 65% simplification threshold to allow up to 95% of the
original geometry (e.g. accept simplification as valid even if we only
simplified 5% of the triangles). This gives us closer to
log2(initial_meshlet_count) LOD levels, and fewer meshlet roots in the
DAG.

Still more work to be done in the future here. Maybe trying METIS for
meshlet building instead of meshoptimizer.

Using ~8 clusters per group instead of ~4 might also make a big
difference. The Nanite slides say that they have 8-32 meshlets per
group, suggesting some kind of heuristic. Unfortunately meshopt's
compute_cluster_bounds won't work with large groups atm
(https://github.com/zeux/meshoptimizer/discussions/750#discussioncomment-10562641)
so hard to test.

Based on discussion from
https://github.com/bevyengine/bevy/discussions/14998,
https://github.com/zeux/meshoptimizer/discussions/750, and discord.

### Runtime changes
- cluster:triangle packed IDs are now stored 25:7 instead of 26:6 bits,
as max triangles per cluster are now 128 instead of 64
- Hardware raster now spawns 128 * 3 vertices instead of 64 * 3 vertices
to account for the new max triangles limit
- Hardware raster now outputs NaN triangles (0 / 0) instead of
zero-positioned triangles for extra vertex invocations over the cluster
triangle count. Shouldn't really be a difference idt, but I did it
anyways.
- Software raster now does 128 threads per workgroup instead of 64
threads. Each thread now loads, projects, and caches a vertex (vertices
0-127), and then if needed does so again (vertices 128-254). Each thread
then rasterizes one of 128 triangles.
- Fixed a bug with `needs_dispatch_remap`. I had the condition backwards
in my last PR, I probably committed it by accident after testing the
non-default code path on my GPU.
This commit is contained in:
JMS55 2024-09-08 10:55:57 -07:00 committed by GitHub
parent 8fb69dcbf0
commit a0faf9cd01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 52 additions and 60 deletions

View file

@ -1075,7 +1075,7 @@ setup = [
"curl",
"-o",
"assets/models/bunny.meshlet_mesh",
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh",
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/e3da1533b4c69fb967f233c817e9b0921134d317/bunny.meshlet_mesh",
],
]

View file

@ -7,7 +7,7 @@ use bevy_utils::HashMap;
use itertools::Itertools;
use meshopt::{
build_meshlets, compute_cluster_bounds, compute_meshlet_bounds, ffi::meshopt_Bounds, simplify,
simplify_scale, Meshlets, SimplifyOptions, VertexDataAdapter,
Meshlets, SimplifyOptions, VertexDataAdapter,
};
use metis::Graph;
use smallvec::SmallVec;
@ -49,11 +49,9 @@ impl MeshletMesh {
},
})
.collect::<Vec<_>>();
let mesh_scale = simplify_scale(&vertices);
// Build further LODs
let mut simplification_queue = 0..meshlets.len();
let mut lod_level = 1;
while simplification_queue.len() > 1 {
// For each meshlet build a list of connected meshlets (meshlets that share a triangle edge)
let connected_meshlets_per_meshlet =
@ -70,19 +68,14 @@ impl MeshletMesh {
for group_meshlets in groups.into_iter().filter(|group| group.len() > 1) {
// Simplify the group to ~50% triangle count
let Some((simplified_group_indices, mut group_error)) = simplify_meshlet_groups(
&group_meshlets,
&meshlets,
&vertices,
lod_level,
mesh_scale,
) else {
let Some((simplified_group_indices, mut group_error)) =
simplify_meshlet_group(&group_meshlets, &meshlets, &vertices)
else {
continue;
};
// Add the maximum child error to the parent error to make parent error cumulative from LOD 0
// (we're currently building the parent from its children)
group_error += group_meshlets.iter().fold(0.0f32, |acc, meshlet_id| {
// Force parent error to be >= child error (we're currently building the parent from its children)
group_error = group_meshlets.iter().fold(group_error, |acc, meshlet_id| {
acc.max(bounding_spheres[*meshlet_id].self_lod.radius)
});
@ -99,7 +92,7 @@ impl MeshletMesh {
}
// Build new meshlets using the simplified group
let new_meshlets_count = split_simplified_groups_into_new_meshlets(
let new_meshlets_count = split_simplified_group_into_new_meshlets(
&simplified_group_indices,
&vertices,
&mut meshlets,
@ -125,7 +118,6 @@ impl MeshletMesh {
}
simplification_queue = next_lod_start..meshlets.len();
lod_level += 1;
}
// Convert meshopt_Meshlet data to a custom format
@ -172,7 +164,7 @@ fn validate_input_mesh(mesh: &Mesh) -> Result<Cow<'_, [u32]>, MeshToMeshletMeshC
}
fn compute_meshlets(indices: &[u32], vertices: &VertexDataAdapter) -> Meshlets {
build_meshlets(indices, vertices, 64, 64, 0.0)
build_meshlets(indices, vertices, 255, 128, 0.0) // Meshoptimizer won't currently let us do 256 vertices
}
fn find_connected_meshlets(
@ -252,7 +244,7 @@ fn group_meshlets(
xadj.push(adjncy.len() as i32);
let mut group_per_meshlet = vec![0; simplification_queue.len()];
let partition_count = simplification_queue.len().div_ceil(4);
let partition_count = simplification_queue.len().div_ceil(4); // 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_adjwgt(&adjwgt)
@ -267,12 +259,10 @@ fn group_meshlets(
groups
}
fn simplify_meshlet_groups(
fn simplify_meshlet_group(
group_meshlets: &[usize],
meshlets: &Meshlets,
vertices: &VertexDataAdapter<'_>,
lod_level: u32,
mesh_scale: f32,
) -> Option<(Vec<u32>, f32)> {
// Build a new index buffer into the mesh vertex data by combining all meshlet data in the group
let mut group_indices = Vec::new();
@ -283,11 +273,6 @@ fn simplify_meshlet_groups(
}
}
// Allow more deformation for high LOD levels (1% at LOD 1, 10% at LOD 20+)
let t = (lod_level - 1) as f32 / 19.0;
let target_error_relative = 0.1 * t + 0.01 * (1.0 - t);
let target_error = target_error_relative * mesh_scale;
// Simplify the group to ~50% triangle count
// TODO: Simplify using vertex attributes
let mut error = 0.0;
@ -295,13 +280,13 @@ fn simplify_meshlet_groups(
&group_indices,
vertices,
group_indices.len() / 2,
target_error,
SimplifyOptions::LockBorder | SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute,
f32::MAX,
SimplifyOptions::LockBorder | SimplifyOptions::Sparse | SimplifyOptions::ErrorAbsolute, // TODO: Specify manual vertex locks instead of meshopt's overly-strict locks
Some(&mut error),
);
// Check if we were able to simplify to at least 65% triangle count
if simplified_group_indices.len() as f32 / group_indices.len() as f32 > 0.65 {
// Check if we were able to simplify at least a little (95% of the original triangle count)
if simplified_group_indices.len() as f32 / group_indices.len() as f32 > 0.95 {
return None;
}
@ -311,7 +296,7 @@ fn simplify_meshlet_groups(
Some((simplified_group_indices, error))
}
fn split_simplified_groups_into_new_meshlets(
fn split_simplified_group_into_new_meshlets(
simplified_group_indices: &[u32],
vertices: &VertexDataAdapter<'_>,
meshlets: &mut Meshlets,

View file

@ -31,7 +31,7 @@ fn resolve_material_depth(in: FullscreenVertexOutput) -> @builtin(frag_depth) f3
let depth = visibility >> 32u;
if depth == 0lu { return 0.0; }
let cluster_id = u32(visibility) >> 6u;
let cluster_id = u32(visibility) >> 7u;
let instance_id = meshlet_cluster_instance_ids[cluster_id];
let material_id = meshlet_instance_material_ids[instance_id];
return f32(material_id) / 65535.0;

View file

@ -63,7 +63,7 @@ pub struct ResourceManager {
impl ResourceManager {
pub fn new(cluster_buffer_slots: u32, render_device: &RenderDevice) -> Self {
let needs_dispatch_remap =
cluster_buffer_slots < render_device.limits().max_compute_workgroups_per_dimension;
cluster_buffer_slots > render_device.limits().max_compute_workgroups_per_dimension;
Self {
visibility_buffer_raster_clusters: render_device.create_buffer(&BufferDescriptor {
@ -472,7 +472,7 @@ pub fn prepare_meshlet_per_frame_resources(
.create_buffer_with_data(&BufferInitDescriptor {
label: Some("meshlet_visibility_buffer_hardware_raster_indirect_args_first"),
contents: DrawIndirectArgs {
vertex_count: 64 * 3,
vertex_count: 128 * 3,
instance_count: 0,
first_vertex: 0,
first_instance: 0,
@ -484,7 +484,7 @@ pub fn prepare_meshlet_per_frame_resources(
.create_buffer_with_data(&BufferInitDescriptor {
label: Some("visibility_buffer_hardware_raster_indirect_args_second"),
contents: DrawIndirectArgs {
vertex_count: 64 * 3,
vertex_count: 128 * 3,
instance_count: 0,
first_vertex: 0,
first_instance: 0,

View file

@ -56,7 +56,7 @@ fn vertex(@builtin(instance_index) instance_index: u32, @builtin(vertex_index) v
return VertexOutput(
clip_position,
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
(cluster_id << 6u) | triangle_id,
(cluster_id << 7u) | triangle_id,
#endif
#ifdef DEPTH_CLAMP_ORTHO
unclamped_clip_depth,
@ -83,7 +83,7 @@ fn fragment(vertex_output: VertexOutput) {
fn dummy_vertex() -> VertexOutput {
return VertexOutput(
vec4(0.0),
vec4(divide(0.0, 0.0)), // NaN vertex position
#ifdef MESHLET_VISIBILITY_BUFFER_RASTER_PASS_OUTPUT
0u,
#endif
@ -92,3 +92,8 @@ fn dummy_vertex() -> VertexOutput {
#endif
);
}
// Naga doesn't allow divide by zero literals, but this lets us work around it
fn divide(a: f32, b: f32) -> f32 {
return a / b;
}

View file

@ -97,11 +97,11 @@ struct VertexOutput {
fn resolve_vertex_output(frag_coord: vec4<f32>) -> VertexOutput {
let frag_coord_1d = u32(frag_coord.y) * u32(view.viewport.z) + u32(frag_coord.x);
let packed_ids = u32(meshlet_visibility_buffer[frag_coord_1d]); // TODO: Might be faster to load the correct u32 directly
let cluster_id = packed_ids >> 6u;
let cluster_id = packed_ids >> 7u;
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
let meshlet = meshlets[meshlet_id];
let triangle_id = extractBits(packed_ids, 0u, 6u);
let triangle_id = extractBits(packed_ids, 0u, 7u);
let index_ids = meshlet.start_index_id + (triangle_id * 3u) + vec3(0u, 1u, 2u);
let indices = meshlet.start_vertex_id + vec3(get_meshlet_index(index_ids.x), get_meshlet_index(index_ids.y), get_meshlet_index(index_ids.z));
let vertex_ids = vec3(meshlet_vertex_ids[indices.x], meshlet_vertex_ids[indices.y], meshlet_vertex_ids[indices.z]);

View file

@ -22,10 +22,10 @@
// TODO: Subpixel precision and top-left rule
var<workgroup> viewport_vertices: array<vec3f, 64>;
var<workgroup> viewport_vertices: array<vec3f, 255>;
@compute
@workgroup_size(64, 1, 1) // 64 threads per workgroup, 1 vertex/triangle per thread, 1 cluster per workgroup
@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1-2 vertices per thread, 1 triangle per thread, 1 cluster per workgroup
fn rasterize_cluster(
@builtin(workgroup_id) workgroup_id: vec3<u32>,
@builtin(local_invocation_index) local_invocation_index: u32,
@ -44,28 +44,30 @@ fn rasterize_cluster(
let meshlet_id = meshlet_cluster_meshlet_ids[cluster_id];
let meshlet = meshlets[meshlet_id];
// Load and project 1 vertex per thread
let vertex_id = local_invocation_index;
if vertex_id < meshlet.vertex_count {
let meshlet_vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + vertex_id];
let vertex = unpack_meshlet_vertex(meshlet_vertex_data[meshlet_vertex_id]);
let instance_id = meshlet_cluster_instance_ids[cluster_id];
let instance_uniform = meshlet_instance_uniforms[instance_id];
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
// Project vertex to viewport space
let instance_id = meshlet_cluster_instance_ids[cluster_id];
let instance_uniform = meshlet_instance_uniforms[instance_id];
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0));
var clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0);
var ndc_position = clip_position.xyz / clip_position.w;
// Load and project 1 vertex per thread, and then again if there are more than 128 vertices in the meshlet
for (var i = 0u; i <= 128u; i += 128u) {
let vertex_id = local_invocation_index + i;
if vertex_id < meshlet.vertex_count {
let meshlet_vertex_id = meshlet_vertex_ids[meshlet.start_vertex_id + vertex_id];
let vertex = unpack_meshlet_vertex(meshlet_vertex_data[meshlet_vertex_id]);
// Project vertex to viewport space
let world_position = mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0));
let clip_position = view.clip_from_world * vec4(world_position.xyz, 1.0);
var ndc_position = clip_position.xyz / clip_position.w;
#ifdef DEPTH_CLAMP_ORTHO
ndc_position.z = 1.0 / clip_position.z;
ndc_position.z = 1.0 / clip_position.z;
#endif
let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw;
let viewport_position_xy = ndc_to_uv(ndc_position.xy) * view.viewport.zw;
// Write vertex to workgroup shared memory
viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z);
// Write vertex to workgroup shared memory
viewport_vertices[vertex_id] = vec3(viewport_position_xy, ndc_position.z);
}
}
workgroupBarrier();
// Load 1 triangle's worth of vertex data per thread
@ -76,7 +78,7 @@ fn rasterize_cluster(
let vertex_0 = viewport_vertices[vertex_ids[2]];
let vertex_1 = viewport_vertices[vertex_ids[1]];
let vertex_2 = viewport_vertices[vertex_ids[0]];
let packed_ids = (cluster_id << 6u) | triangle_id;
let packed_ids = (cluster_id << 7u) | triangle_id;
// Compute triangle bounding box
let min_x = u32(min3(vertex_0.x, vertex_1.x, vertex_2.x));

View file

@ -17,7 +17,7 @@ use camera_controller::{CameraController, CameraControllerPlugin};
use std::{f32::consts::PI, path::Path, process::ExitCode};
const ASSET_URL: &str =
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/10bb5471c7beedfe63ad1cf269599c92b0f10aa2/bunny.meshlet_mesh";
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/e3da1533b4c69fb967f233c817e9b0921134d317/bunny.meshlet_mesh";
fn main() -> ExitCode {
if !Path::new("./assets/models/bunny.meshlet_mesh").exists() {