-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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 continuous LOD #12755
Meshlet continuous LOD #12755
Changes from 250 commits
20083fa
2283864
3f403bb
a9f1d9f
3bfa80b
c16c3f2
fe3dacb
3c9b361
92639e3
8a4b9aa
0bd667a
cda73dc
58391d1
1d4d89b
ee2278c
737d42d
b04244a
ebc68e1
bb1490b
25d2943
5487522
dec0dda
60f2c91
e1049d7
0f446fa
b48daeb
109bb4f
11b96e3
1637b6a
f9a8032
f4cb938
2b665db
684a39f
6bb52d5
d471820
23207c5
4ddd312
18412d7
a03a37e
5f632f1
f75dc7a
932b34f
91fd217
ec11c7a
9db1a96
ea38623
07bac3c
d2903ac
2a6e022
62d8063
3d96a40
54ac8bb
3d6a7d3
a0f5c6b
a5cafe3
7979367
2a21ed9
e6faae6
e0b7bf2
726370a
d0789ae
4dcc424
2a5c800
ddfbe8a
eb4967b
fd26948
7831a97
e11b9b0
633137d
3284621
c3b4d34
76a9a62
54070b9
683a4e1
6e20432
f380cb4
ac5ce53
086ed77
1250949
19e46c3
6c8b8bf
dd0ec04
67a0bc5
6e72360
dc5c57e
ded44f3
ea39e34
fa75dd2
6bbb428
93fed23
ac89611
2249e00
1cdd044
16ff589
0463f47
084b1a2
48d5c89
a0ba8d1
6328d03
98d375d
c8711bf
6fd9dc2
a22848e
d961c94
baa8318
6a845d7
7a4f6ca
4f8cbda
83debc4
d33aa85
9e7655a
52eedf2
d7eb81c
81ce951
3ef551a
474bae2
21786e2
a7e7930
afbd441
8004df0
4a69f8a
82cb1d8
69184bf
0e7874d
d7fd5d4
096ea24
97e2a50
8db797f
182fb8f
b08e45f
12e982e
a903873
2916dd3
2f1b399
24fe534
c687e59
79cbf4c
3e32004
a3187da
7d0eb4a
bfa4754
4fc46b9
328edcd
99b18d5
0c06cd9
cf236fe
7d67997
928d809
25b3abc
8f9d54a
27773a1
c768471
be03224
f146345
de4cf86
9680e1b
0f4de29
60af7da
8ac2e94
0a7f76c
6be2f94
a6f8f62
3fc104c
bb77e5a
edccb65
cb58dde
c7521da
c1605c0
0fa362d
c3c774f
36787dc
bdc87f1
8510640
7eb151c
b13b324
a0b2bbf
c5a0565
6cbad7d
bc8d2ca
f21f38b
9a1e927
3090eba
a7055ee
cc24dea
3655e68
da5b4b4
b22d53d
e6299f2
68a04b0
9c58cf8
e13cc32
9d3a4a9
cdd83cb
78dec88
8636e82
f27af6e
ec4d06c
898f527
0c2275c
c96f454
975345c
f0af112
57ace17
f5b7f53
c7eaa58
a0de18a
37a0967
6f89d65
044e330
880677b
26bb38c
1b7cec2
6fc29ec
a52f9e7
3a3d861
32d1016
113f52c
20c28ac
e910274
f59bcbe
7580149
8533e82
9725908
6978cc7
971d941
d8f57c2
8f20363
3f31d62
0037c70
529e630
dbc1edd
f774cbf
504e5a4
9973a77
a5626b0
6443474
98a87c5
646c5c6
caad9c8
e57d12c
e456412
7f2fd53
cf7d3a9
48026f2
73fe48e
865c9d3
f3dde54
0e5ee9f
fb0a669
81bda63
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,17 @@ | ||
use bevy_asset::{ | ||
io::{Reader, Writer}, | ||
io::{AsyncReadAndSeek, Reader, Writer}, | ||
saver::{AssetSaver, SavedAsset}, | ||
Asset, AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext, | ||
}; | ||
use bevy_math::Vec3; | ||
use bevy_reflect::TypePath; | ||
use bytemuck::{Pod, Zeroable}; | ||
use lz4_flex::frame::{FrameDecoder, FrameEncoder}; | ||
use serde::{Deserialize, Serialize}; | ||
use std::sync::Arc; | ||
use std::{io::Cursor, sync::Arc}; | ||
|
||
/// The current version of the [`MeshletMesh`] asset format. | ||
pub const MESHLET_MESH_ASSET_VERSION: u64 = 0; | ||
|
||
/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets. | ||
/// | ||
|
@@ -25,8 +29,8 @@ use std::sync::Arc; | |
/// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`]. | ||
#[derive(Asset, TypePath, Serialize, Deserialize, Clone)] | ||
pub struct MeshletMesh { | ||
/// The total amount of triangles summed across all meshlets in the mesh. | ||
pub total_meshlet_triangles: u64, | ||
/// The total amount of triangles summed across all LOD 0 meshlets in the mesh. | ||
pub worst_case_meshlet_triangles: u64, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I personally feel that that's less clear. Up to maintainers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i prefer keeping it as is |
||
/// Raw vertex data bytes for the overall mesh. | ||
pub vertex_data: Arc<[u8]>, | ||
/// Indices into `vertex_data`. | ||
|
@@ -35,8 +39,8 @@ pub struct MeshletMesh { | |
pub indices: Arc<[u8]>, | ||
/// The list of meshlets making up this mesh. | ||
pub meshlets: Arc<[Meshlet]>, | ||
/// A list of spherical bounding volumes, 1 per meshlet. | ||
pub meshlet_bounding_spheres: Arc<[MeshletBoundingSphere]>, | ||
/// Spherical bounding volumes. | ||
pub bounding_spheres: Arc<[MeshletBoundingSpheres]>, | ||
} | ||
|
||
/// A single meshlet within a [`MeshletMesh`]. | ||
|
@@ -51,7 +55,19 @@ pub struct Meshlet { | |
pub triangle_count: u32, | ||
} | ||
|
||
/// A spherical bounding volume used for culling a [`Meshlet`]. | ||
/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`]. | ||
#[derive(Serialize, Deserialize, 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, | ||
} | ||
|
||
/// A spherical bounding volume used for a [`Meshlet`]. | ||
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] | ||
#[repr(C)] | ||
pub struct MeshletBoundingSphere { | ||
|
@@ -65,17 +81,24 @@ pub struct MeshletMeshSaverLoad; | |
impl AssetLoader for MeshletMeshSaverLoad { | ||
type Asset = MeshletMesh; | ||
type Settings = (); | ||
type Error = bincode::Error; | ||
type Error = MeshletMeshSaveOrLoadError; | ||
|
||
async fn load<'a>( | ||
&'a self, | ||
reader: &'a mut Reader<'_>, | ||
_settings: &'a Self::Settings, | ||
_load_context: &'a mut LoadContext<'_>, | ||
) -> Result<Self::Asset, Self::Error> { | ||
let version = read_u64(reader).await?; | ||
if version != MESHLET_MESH_ASSET_VERSION { | ||
return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version }); | ||
} | ||
|
||
let mut bytes = Vec::new(); | ||
reader.read_to_end(&mut bytes).await?; | ||
bincode::deserialize(&bytes) | ||
let asset = bincode::deserialize_from(FrameDecoder::new(Cursor::new(bytes)))?; | ||
|
||
Ok(asset) | ||
} | ||
|
||
fn extensions(&self) -> &[&str] { | ||
|
@@ -87,16 +110,44 @@ impl AssetSaver for MeshletMeshSaverLoad { | |
type Asset = MeshletMesh; | ||
type Settings = (); | ||
type OutputLoader = Self; | ||
type Error = bincode::Error; | ||
type Error = MeshletMeshSaveOrLoadError; | ||
|
||
async fn save<'a>( | ||
&'a self, | ||
writer: &'a mut Writer, | ||
asset: SavedAsset<'a, Self::Asset>, | ||
_settings: &'a Self::Settings, | ||
) -> Result<(), Self::Error> { | ||
let bytes = bincode::serialize(asset.get())?; | ||
writer | ||
.write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes()) | ||
.await?; | ||
|
||
let mut bytes = Vec::new(); | ||
let mut sync_writer = FrameEncoder::new(&mut bytes); | ||
bincode::serialize_into(&mut sync_writer, asset.get())?; | ||
sync_writer.finish()?; | ||
writer.write_all(&bytes).await?; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
#[derive(thiserror::Error, Debug)] | ||
pub enum MeshletMeshSaveOrLoadError { | ||
JMS55 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
#[error("expected asset version {MESHLET_MESH_ASSET_VERSION} but found version {found}")] | ||
WrongVersion { found: u64 }, | ||
#[error("failed to serialize or deserialize asset data")] | ||
SerializationOrDeserialization(#[from] bincode::Error), | ||
#[error("failed to compress or decompress asset data")] | ||
CompressionOrDecompression(#[from] lz4_flex::frame::Error), | ||
#[error("failed to read or write asset data")] | ||
Io(#[from] std::io::Error), | ||
} | ||
|
||
async fn read_u64( | ||
reader: &mut (dyn AsyncReadAndSeek + Sync + Send + Unpin), | ||
) -> Result<u64, bincode::Error> { | ||
let mut bytes = [0u8; 8]; | ||
reader.read_exact(&mut bytes).await?; | ||
Ok(u64::from_le_bytes(bytes)) | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -20,19 +20,34 @@ | |||||
@compute | ||||||
@workgroup_size(128, 1, 1) // 128 threads per workgroup, 1 instanced meshlet per thread | ||||||
fn cull_meshlets(@builtin(global_invocation_id) cluster_id: vec3<u32>) { | ||||||
// Fetch the instanced meshlet data | ||||||
// Fetch the instance data and check for instance culling | ||||||
if cluster_id.x >= arrayLength(&meshlet_thread_meshlet_ids) { return; } | ||||||
let instance_id = meshlet_thread_instance_ids[cluster_id.x]; | ||||||
if should_cull_instance(instance_id) { | ||||||
return; | ||||||
} | ||||||
|
||||||
// Fetch other meshlet data | ||||||
let meshlet_id = meshlet_thread_meshlet_ids[cluster_id.x]; | ||||||
let bounding_sphere = meshlet_bounding_spheres[meshlet_id]; | ||||||
let instance_uniform = meshlet_instance_uniforms[instance_id]; | ||||||
let model = affine3_to_square(instance_uniform.model); | ||||||
let model_scale = max(length(model[0]), max(length(model[1]), length(model[2]))); | ||||||
let bounding_sphere_center = model * vec4(bounding_sphere.center, 1.0); | ||||||
let bounding_sphere_radius = model_scale * bounding_sphere.radius; | ||||||
let bounding_spheres = meshlet_bounding_spheres[meshlet_id]; | ||||||
|
||||||
// Calculate view-space LOD bounding sphere for the meshlet | ||||||
let lod_bounding_sphere_center = model * vec4(bounding_spheres.self_lod.center, 1.0); | ||||||
let lod_bounding_sphere_radius = model_scale * bounding_spheres.self_lod.radius; | ||||||
let lod_bounding_sphere_center_view_space = (view.inverse_view * vec4(lod_bounding_sphere_center.xyz, 1.0)).xyz; | ||||||
|
||||||
// Calculate view-space LOD bounding sphere for the meshlet's parent | ||||||
let parent_lod_bounding_sphere_center = model * vec4(bounding_spheres.parent_lod.center, 1.0); | ||||||
let parent_lod_bounding_sphere_radius = model_scale * bounding_spheres.parent_lod.radius; | ||||||
let parent_lod_bounding_sphere_center_view_space = (view.inverse_view * vec4(parent_lod_bounding_sphere_center.xyz, 1.0)).xyz; | ||||||
|
||||||
// Check LOD cut (meshlet 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); | ||||||
if !lod_is_ok || parent_lod_is_ok { return; } | ||||||
|
||||||
// In the first pass, operate only on the clusters visible last frame. In the second pass, operate on all clusters. | ||||||
#ifdef MESHLET_SECOND_CULLING_PASS | ||||||
|
@@ -42,18 +57,22 @@ fn cull_meshlets(@builtin(global_invocation_id) cluster_id: vec3<u32>) { | |||||
if !meshlet_visible { return; } | ||||||
#endif | ||||||
|
||||||
// Calculate world-space culling bounding sphere for the cluster | ||||||
let culling_bounding_sphere_center = model * vec4(bounding_spheres.self_culling.center, 1.0); | ||||||
let culling_bounding_sphere_radius = model_scale * bounding_spheres.self_culling.radius; | ||||||
|
||||||
// Frustum culling | ||||||
// TODO: Faster method from https://vkguide.dev/docs/gpudriven/compute_culling/#frustum-culling-function | ||||||
for (var i = 0u; i < 6u; i++) { | ||||||
if !meshlet_visible { break; } | ||||||
meshlet_visible &= dot(view.frustum[i], bounding_sphere_center) > -bounding_sphere_radius; | ||||||
meshlet_visible &= dot(view.frustum[i], culling_bounding_sphere_center) > -culling_bounding_sphere_radius; | ||||||
} | ||||||
|
||||||
#ifdef MESHLET_SECOND_CULLING_PASS | ||||||
// In the second culling pass, cull against the depth pyramid generated from the first pass | ||||||
if meshlet_visible { | ||||||
let bounding_sphere_center_view_space = (view.inverse_view * vec4(bounding_sphere_center.xyz, 1.0)).xyz; | ||||||
let aabb = project_view_space_sphere_to_screen_space_aabb(bounding_sphere_center_view_space, bounding_sphere_radius); | ||||||
let culling_bounding_sphere_center_view_space = (view.inverse_view * vec4(culling_bounding_sphere_center.xyz, 1.0)).xyz; | ||||||
let aabb = project_view_space_sphere_to_screen_space_aabb(culling_bounding_sphere_center_view_space, culling_bounding_sphere_radius); | ||||||
|
||||||
// Halve the AABB size because the first depth mip resampling pass cut the full screen resolution into a power of two conservatively | ||||||
let depth_pyramid_size_mip_0 = vec2<f32>(textureDimensions(depth_pyramid, 0)) * 0.5; | ||||||
|
@@ -71,11 +90,11 @@ fn cull_meshlets(@builtin(global_invocation_id) cluster_id: vec3<u32>) { | |||||
let occluder_depth = min(min(depth_quad_a, depth_quad_b), min(depth_quad_c, depth_quad_d)); | ||||||
if view.projection[3][3] == 1.0 { | ||||||
// Orthographic | ||||||
let sphere_depth = view.projection[3][2] + (bounding_sphere_center_view_space.z + bounding_sphere_radius) * view.projection[2][2]; | ||||||
let sphere_depth = view.projection[3][2] + (culling_bounding_sphere_center_view_space.z + culling_bounding_sphere_radius) * view.projection[2][2]; | ||||||
meshlet_visible &= sphere_depth >= occluder_depth; | ||||||
} else { | ||||||
// Perspective | ||||||
let sphere_depth = -view.projection[3][2] / (bounding_sphere_center_view_space.z + bounding_sphere_radius); | ||||||
let sphere_depth = -view.projection[3][2] / (culling_bounding_sphere_center_view_space.z + culling_bounding_sphere_radius); | ||||||
meshlet_visible &= sphere_depth >= occluder_depth; | ||||||
} | ||||||
} | ||||||
|
@@ -86,6 +105,16 @@ fn cull_meshlets(@builtin(global_invocation_id) cluster_id: vec3<u32>) { | |||||
atomicOr(&meshlet_occlusion[cluster_id.x / 32u], occlusion_bit); | ||||||
} | ||||||
|
||||||
// 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.projection[0][0] * r / sqrt(d2 - r2); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The spec says inverseSqrt() does not work if e <= 0. Need to think if this is an issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i dont think sqrt is guaranteed to work for e <= 0 either. I'd just slab an abs or max 0 on it if its a worry, being explicit about edge case behavior intent is valuable and saturating/clamping is often free in gpus |
||||||
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://zeux.io/2023/01/12/approximate-projected-bounds | ||||||
fn project_view_space_sphere_to_screen_space_aabb(cp: vec3<f32>, r: f32) -> vec4<f32> { | ||||||
let inv_width = view.projection[0][0] * 0.5; | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think you will need to change this more? Storing binary files in git and having history of them isn't the nicest thing so if we could avoid it, that would be good. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, it will definitely change a couple more times... Storing it externally also sucks though as then you can't run the example easily... Idk what we should do.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will almost certainly change again, the format is not bit packed yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I get Gltf->MeshletMesh asset preprocessing in, we can ship the repo with the GLTF file, and just have users convert it as part of the example. That way we won't have to worry about the MeshletMesh asset changing. But that's a good bit off, I don't want to block on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bevyengine/maintainer-team any thoughts on this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to note as it happened on Discord - it was decided that hosting the file out of tree would be better, and prompting the user to download it. The reason is that binary files that are modified and committed to the repository will have all versions in git history and the only way to remove them is to rewrite the git history.”, which isn’t nice. But also, rewriting git history breaks all open PRs and branches.