Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Meshlet new error projection #15846

Merged
merged 14 commits into from
Oct 22, 2024
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1209,7 +1209,7 @@ setup = [
"curl",
"-o",
"assets/models/bunny.meshlet_mesh",
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/8443bbdee0bf517e6c297dede7f6a46ab712ee4c/bunny.meshlet_mesh",
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/167cdaf0b08f89fb747b83b94c27755f116cd408/bunny.meshlet_mesh",
],
]

11 changes: 6 additions & 5 deletions crates/bevy_pbr/Cargo.toml
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@ shader_format_glsl = ["bevy_render/shader_format_glsl"]
trace = ["bevy_render/trace"]
ios_simulator = ["bevy_render/ios_simulator"]
# Enables the meshlet renderer for dense high-poly scenes (experimental)
meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:bevy_tasks"]
meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:half", "dep:bevy_tasks"]
# Enables processing meshes into meshlet meshes
meshlet_processor = [
"meshlet",
@@ -50,16 +50,17 @@ bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
# other
bitflags = "2.3"
fixedbitset = "0.5"
# meshlet
lz4_flex = { version = "0.11", default-features = false, features = [
"frame",
], optional = true }
derive_more = { version = "1", default-features = false, features = [
"error",
"from",
"display",
] }
# meshlet
lz4_flex = { version = "0.11", default-features = false, features = [
"frame",
], optional = true }
range-alloc = { version = "0.1.3", optional = true }
half = { version = "2", features = ["bytemuck"], optional = true }
meshopt = { version = "0.3.0", optional = true }
metis = { version = "0.2", optional = true }
itertools = { version = "0.13", optional = true }
28 changes: 22 additions & 6 deletions crates/bevy_pbr/src/meshlet/asset.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ use bevy_reflect::TypePath;
use bevy_tasks::block_on;
use bytemuck::{Pod, Zeroable};
use derive_more::derive::{Display, Error, From};
use half::f16;
use lz4_flex::frame::{FrameDecoder, FrameEncoder};
use std::io::{Read, Write};

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

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

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

/// Simplification error used for choosing level of detail for a [`Meshlet`].
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletSimplificationError {
/// Simplification error used for determining if this meshlet's group is at the correct level of detail for a given view.
pub group_error: f16,
/// Simplification error used for determining if this meshlet's parent group is at the correct level of detail for a given view.
pub parent_group_error: f16,
}

/// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
pub struct MeshletMeshSaver;

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

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

Ok(MeshletMesh {
vertex_positions,
@@ -187,6 +202,7 @@ impl AssetLoader for MeshletMeshLoader {
indices,
meshlets,
meshlet_bounding_spheres,
meshlet_simplification_errors,
})
}

52 changes: 27 additions & 25 deletions crates/bevy_pbr/src/meshlet/cull_clusters.wgsl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import bevy_pbr::meshlet_bindings::{
meshlet_cluster_meshlet_ids,
meshlet_bounding_spheres,
meshlet_simplification_errors,
meshlet_cluster_instance_ids,
meshlet_instance_uniforms,
meshlet_second_pass_candidates,
@@ -13,6 +14,7 @@
meshlet_hardware_raster_indirect_args,
meshlet_raster_clusters,
meshlet_raster_cluster_rightmost_slot,
MeshletBoundingSphere,
}
#import bevy_render::maths::affine3_to_square

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

#ifdef MESHLET_FIRST_CULLING_PASS
// Frustum culling
@@ -60,28 +62,19 @@ fn cull_clusters(
}
}

// Calculate view-space LOD bounding sphere for the cluster
let lod_bounding_sphere_center = world_from_local * vec4(bounding_spheres.self_lod.center, 1.0);
let lod_bounding_sphere_radius = world_scale * bounding_spheres.self_lod.radius;
let lod_bounding_sphere_center_view_space = (view.view_from_world * vec4(lod_bounding_sphere_center.xyz, 1.0)).xyz;

// Calculate view-space LOD bounding sphere for the cluster's parent
let parent_lod_bounding_sphere_center = world_from_local * vec4(bounding_spheres.parent_lod.center, 1.0);
let parent_lod_bounding_sphere_radius = world_scale * bounding_spheres.parent_lod.radius;
let parent_lod_bounding_sphere_center_view_space = (view.view_from_world * vec4(parent_lod_bounding_sphere_center.xyz, 1.0)).xyz;

// Check LOD cut (cluster error imperceptible, and parent error not imperceptible)
let lod_is_ok = lod_error_is_imperceptible(lod_bounding_sphere_center_view_space, lod_bounding_sphere_radius);
let parent_lod_is_ok = lod_error_is_imperceptible(parent_lod_bounding_sphere_center_view_space, parent_lod_bounding_sphere_radius);
// Check LOD cut (cluster group error imperceptible, and parent group error not imperceptible)
let simplification_errors = unpack2x16float(meshlet_simplification_errors[meshlet_id]);
let lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_group_sphere, simplification_errors.x, world_from_local, world_scale);
let parent_lod_is_ok = lod_error_is_imperceptible(bounding_spheres.lod_parent_group_sphere, simplification_errors.y, world_from_local, world_scale);
if !lod_is_ok || parent_lod_is_ok { return; }
#endif

// Project the culling bounding sphere to view-space for occlusion culling
#ifdef MESHLET_FIRST_CULLING_PASS
let previous_world_from_local = affine3_to_square(instance_uniform.previous_world_from_local);
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])));
let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.self_culling.center, 1.0);
let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.self_culling.radius;
let occlusion_culling_bounding_sphere_center = previous_world_from_local * vec4(bounding_spheres.culling_sphere.center, 1.0);
let occlusion_culling_bounding_sphere_radius = previous_world_from_local_scale * bounding_spheres.culling_sphere.radius;
let occlusion_culling_bounding_sphere_center_view_space = (previous_view.view_from_world * vec4(occlusion_culling_bounding_sphere_center.xyz, 1.0)).xyz;
#else
let occlusion_culling_bounding_sphere_center = culling_bounding_sphere_center;
@@ -148,14 +141,23 @@ fn cull_clusters(
meshlet_raster_clusters[buffer_slot] = cluster_id;
}

// https://stackoverflow.com/questions/21648630/radius-of-projected-sphere-in-screen-space/21649403#21649403
fn lod_error_is_imperceptible(cp: vec3<f32>, r: f32) -> bool {
let d2 = dot(cp, cp);
let r2 = r * r;
let sphere_diameter_uv = view.clip_from_view[0][0] * r / sqrt(d2 - r2);
let view_size = f32(max(view.viewport.z, view.viewport.w));
let sphere_diameter_pixels = sphere_diameter_uv * view_size;
return sphere_diameter_pixels < 1.0;
// https://github.com/zeux/meshoptimizer/blob/1e48e96c7e8059321de492865165e9ef071bffba/demo/nanite.cpp#L115
fn lod_error_is_imperceptible(lod_sphere: MeshletBoundingSphere, simplification_error: f32, world_from_local: mat4x4<f32>, world_scale: f32) -> bool {
let sphere_world_space = (world_from_local * vec4(lod_sphere.center, 1.0)).xyz;
let radius_world_space = world_scale * lod_sphere.radius;
let error_world_space = world_scale * simplification_error;

var projected_error = error_world_space;
if view.clip_from_view[3][3] != 1.0 {
// Perspective
let distance_to_closest_point_on_sphere = distance(sphere_world_space, view.world_position) - radius_world_space;
let distance_to_closest_point_on_sphere_clamped_to_znear = max(distance_to_closest_point_on_sphere, view.clip_from_view[3][2]);
projected_error /= distance_to_closest_point_on_sphere_clamped_to_znear;
}
projected_error *= view.clip_from_view[1][1] * 0.5;
projected_error *= view.viewport.w;

return projected_error < 1.0;
}

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