Skip to content

Commit 9d54fe0

Browse files
authored
Meshlet new error projection (bevyengine#15846)
* New error projection code taken from @zeux's meshoptimizer nanite.cpp demo for determining LOD (thanks zeux!) * Builder: `compute_lod_group_data()` * Runtime: `lod_error_is_imperceptible()`
1 parent 9930df8 commit 9d54fe0

File tree

11 files changed

+228
-132
lines changed

11 files changed

+228
-132
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1213,7 +1213,7 @@ setup = [
12131213
"curl",
12141214
"-o",
12151215
"assets/models/bunny.meshlet_mesh",
1216-
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/8443bbdee0bf517e6c297dede7f6a46ab712ee4c/bunny.meshlet_mesh",
1216+
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/167cdaf0b08f89fb747b83b94c27755f116cd408/bunny.meshlet_mesh",
12171217
],
12181218
]
12191219

crates/bevy_pbr/Cargo.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ shader_format_glsl = ["bevy_render/shader_format_glsl"]
1818
trace = ["bevy_render/trace"]
1919
ios_simulator = ["bevy_render/ios_simulator"]
2020
# Enables the meshlet renderer for dense high-poly scenes (experimental)
21-
meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:bevy_tasks"]
21+
meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:half", "dep:bevy_tasks"]
2222
# Enables processing meshes into meshlet meshes
2323
meshlet_processor = [
2424
"meshlet",
@@ -50,16 +50,17 @@ bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
5050
# other
5151
bitflags = "2.3"
5252
fixedbitset = "0.5"
53-
# meshlet
54-
lz4_flex = { version = "0.11", default-features = false, features = [
55-
"frame",
56-
], optional = true }
5753
derive_more = { version = "1", default-features = false, features = [
5854
"error",
5955
"from",
6056
"display",
6157
] }
58+
# meshlet
59+
lz4_flex = { version = "0.11", default-features = false, features = [
60+
"frame",
61+
], optional = true }
6262
range-alloc = { version = "0.1.3", optional = true }
63+
half = { version = "2", features = ["bytemuck"], optional = true }
6364
meshopt = { version = "0.3.0", optional = true }
6465
metis = { version = "0.2", optional = true }
6566
itertools = { version = "0.13", optional = true }

crates/bevy_pbr/src/meshlet/asset.rs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use bevy_reflect::TypePath;
99
use bevy_tasks::block_on;
1010
use bytemuck::{Pod, Zeroable};
1111
use derive_more::derive::{Display, Error, From};
12+
use half::f16;
1213
use lz4_flex::frame::{FrameDecoder, FrameEncoder};
1314
use std::io::{Read, Write};
1415

@@ -51,6 +52,8 @@ pub struct MeshletMesh {
5152
pub(crate) meshlets: Arc<[Meshlet]>,
5253
/// Spherical bounding volumes.
5354
pub(crate) meshlet_bounding_spheres: Arc<[MeshletBoundingSpheres]>,
55+
/// Meshlet group and parent group simplification errors.
56+
pub(crate) meshlet_simplification_errors: Arc<[MeshletSimplificationError]>,
5457
}
5558

5659
/// A single meshlet within a [`MeshletMesh`].
@@ -90,12 +93,12 @@ pub struct Meshlet {
9093
#[derive(Copy, Clone, Pod, Zeroable)]
9194
#[repr(C)]
9295
pub struct MeshletBoundingSpheres {
93-
/// The bounding sphere used for frustum and occlusion culling for this meshlet.
94-
pub self_culling: MeshletBoundingSphere,
95-
/// The bounding sphere used for determining if this meshlet is at the correct level of detail for a given view.
96-
pub self_lod: MeshletBoundingSphere,
97-
/// The bounding sphere used for determining if this meshlet's parent is at the correct level of detail for a given view.
98-
pub parent_lod: MeshletBoundingSphere,
96+
/// Bounding sphere used for frustum and occlusion culling for this meshlet.
97+
pub culling_sphere: MeshletBoundingSphere,
98+
/// Bounding sphere used for determining if this meshlet's group is at the correct level of detail for a given view.
99+
pub lod_group_sphere: MeshletBoundingSphere,
100+
/// Bounding sphere used for determining if this meshlet's parent group is at the correct level of detail for a given view.
101+
pub lod_parent_group_sphere: MeshletBoundingSphere,
99102
}
100103

101104
/// A spherical bounding volume used for a [`Meshlet`].
@@ -106,6 +109,16 @@ pub struct MeshletBoundingSphere {
106109
pub radius: f32,
107110
}
108111

112+
/// Simplification error used for choosing level of detail for a [`Meshlet`].
113+
#[derive(Copy, Clone, Pod, Zeroable)]
114+
#[repr(C)]
115+
pub struct MeshletSimplificationError {
116+
/// Simplification error used for determining if this meshlet's group is at the correct level of detail for a given view.
117+
pub group_error: f16,
118+
/// Simplification error used for determining if this meshlet's parent group is at the correct level of detail for a given view.
119+
pub parent_group_error: f16,
120+
}
121+
109122
/// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
110123
pub struct MeshletMeshSaver;
111124

@@ -139,6 +152,7 @@ impl AssetSaver for MeshletMeshSaver {
139152
write_slice(&asset.indices, &mut writer)?;
140153
write_slice(&asset.meshlets, &mut writer)?;
141154
write_slice(&asset.meshlet_bounding_spheres, &mut writer)?;
155+
write_slice(&asset.meshlet_simplification_errors, &mut writer)?;
142156
writer.finish()?;
143157

144158
Ok(())
@@ -179,6 +193,7 @@ impl AssetLoader for MeshletMeshLoader {
179193
let indices = read_slice(reader)?;
180194
let meshlets = read_slice(reader)?;
181195
let meshlet_bounding_spheres = read_slice(reader)?;
196+
let meshlet_simplification_errors = read_slice(reader)?;
182197

183198
Ok(MeshletMesh {
184199
vertex_positions,
@@ -187,6 +202,7 @@ impl AssetLoader for MeshletMeshLoader {
187202
indices,
188203
meshlets,
189204
meshlet_bounding_spheres,
205+
meshlet_simplification_errors,
190206
})
191207
}
192208

crates/bevy_pbr/src/meshlet/cull_clusters.wgsl

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#import bevy_pbr::meshlet_bindings::{
22
meshlet_cluster_meshlet_ids,
33
meshlet_bounding_spheres,
4+
meshlet_simplification_errors,
45
meshlet_cluster_instance_ids,
56
meshlet_instance_uniforms,
67
meshlet_second_pass_candidates,
@@ -13,6 +14,7 @@
1314
meshlet_hardware_raster_indirect_args,
1415
meshlet_raster_clusters,
1516
meshlet_raster_cluster_rightmost_slot,
17+
MeshletBoundingSphere,
1618
}
1719
#import bevy_render::maths::affine3_to_square
1820

@@ -48,8 +50,8 @@ fn cull_clusters(
4850
let world_from_local = affine3_to_square(instance_uniform.world_from_local);
4951
let world_scale = max(length(world_from_local[0]), max(length(world_from_local[1]), length(world_from_local[2])));
5052
let bounding_spheres = meshlet_bounding_spheres[meshlet_id];
51-
let culling_bounding_sphere_center = world_from_local * vec4(bounding_spheres.self_culling.center, 1.0);
52-
let culling_bounding_sphere_radius = world_scale * bounding_spheres.self_culling.radius;
53+
let culling_bounding_sphere_center = world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0);
54+
let culling_bounding_sphere_radius = world_scale * bounding_spheres.culling_sphere.radius;
5355

5456
#ifdef MESHLET_FIRST_CULLING_PASS
5557
// Frustum culling
@@ -60,28 +62,19 @@ fn cull_clusters(
6062
}
6163
}
6264

63-
// Calculate view-space LOD bounding sphere for the cluster
64-
let lod_bounding_sphere_center = world_from_local * vec4(bounding_spheres.self_lod.center, 1.0);
65-
let lod_bounding_sphere_radius = world_scale * bounding_spheres.self_lod.radius;
66-
let lod_bounding_sphere_center_view_space = (view.view_from_world * vec4(lod_bounding_sphere_center.xyz, 1.0)).xyz;
67-
68-
// Calculate view-space LOD bounding sphere for the cluster's parent
69-
let parent_lod_bounding_sphere_center = world_from_local * vec4(bounding_spheres.parent_lod.center, 1.0);
70-
let parent_lod_bounding_sphere_radius = world_scale * bounding_spheres.parent_lod.radius;
71-
let parent_lod_bounding_sphere_center_view_space = (view.view_from_world * vec4(parent_lod_bounding_sphere_center.xyz, 1.0)).xyz;
72-
73-
// Check LOD cut (cluster error imperceptible, and parent error not imperceptible)
74-
let lod_is_ok = lod_error_is_imperceptible(lod_bounding_sphere_center_view_space, lod_bounding_sphere_radius);
75-
let parent_lod_is_ok = lod_error_is_imperceptible(parent_lod_bounding_sphere_center_view_space, parent_lod_bounding_sphere_radius);
65+
// Check LOD cut (cluster group error imperceptible, and parent group error not imperceptible)
66+
let simplification_errors = unpack2x16float(meshlet_simplification_errors[meshlet_id]);
67+
let lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_group_sphere, simplification_errors.x, world_from_local, world_scale);
68+
let parent_lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_parent_group_sphere, simplification_errors.y, world_from_local, world_scale);
7669
if !lod_is_ok || parent_lod_is_ok { return; }
7770
#endif
7871

7972
// Project the culling bounding sphere to view-space for occlusion culling
8073
#ifdef MESHLET_FIRST_CULLING_PASS
8174
let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local);
8275
let previous_world_from_local_scale = max(length(previous_world_from_local[0]), max(length(previous_world_from_local[1]), length(previous_world_from_local[2])));
83-
let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.self_culling.center, 1.0);
84-
let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.self_culling.radius;
76+
let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0);
77+
let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.culling_sphere.radius;
8578
let occlusion_culling_bounding_sphere_center_view_space = (previous_view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz;
8679
#else
8780
let occlusion_culling_bounding_sphere_center = culling_bounding_sphere_center;
@@ -148,14 +141,23 @@ fn cull_clusters(
148141
meshlet_raster_clusters[buffer_slot] = cluster_id;
149142
}
150143

151-
// https://stackoverflow.com/questions/21648630/radius-of-projected-sphere-in-screen-space/21649403#21649403
152-
fn lod_error_is_imperceptible(cp: vec3<f32>, r: f32) -> bool {
153-
let d2 = dot(cp, cp);
154-
let r2 = r * r;
155-
let sphere_diameter_uv = view.clip_from_view[0][0] * r / sqrt(d2 - r2);
156-
let view_size = f32(max(view.viewport.z, view.viewport.w));
157-
let sphere_diameter_pixels = sphere_diameter_uv * view_size;
158-
return sphere_diameter_pixels < 1.0;
144+
// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115
145+
fn lod_error_is_imperceptible(lod_sphere: MeshletBoundingSphere, simplification_error: f32, world_from_local: mat4x4<f32>, world_scale: f32) -> bool {
146+
let sphere_world_space = (world_from_local * vec4(lod_sphere.center, 1.0)).xyz;
147+
let radius_world_space = world_scale * lod_sphere.radius;
148+
let error_world_space = world_scale * simplification_error;
149+
150+
var projected_error = error_world_space;
151+
if view.clip_from_view[3][3] != 1.0 {
152+
// Perspective
153+
let distance_to_closest_point_on_sphere = distance(sphere_world_space, view.world_position) - radius_world_space;
154+
let distance_to_closest_point_on_sphere_clamped_to_znear = max(distance_to_closest_point_on_sphere, view.clip_from_view[3][2]);
155+
projected_error /= distance_to_closest_point_on_sphere_clamped_to_znear;
156+
}
157+
projected_error *= view.clip_from_view[1][1] * 0.5;
158+
projected_error *= view.viewport.w;
159+
160+
return projected_error < 1.0;
159161
}
160162

161163
// https://zeux.io/2023/01/12/approximate-projected-bounds

0 commit comments

Comments
 (0)