diff --git a/Cargo.toml b/Cargo.toml index fae29567b7bb4..9533e14c3027a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1022,21 +1022,13 @@ wasm = false name = "meshlet" path = "examples/3d/meshlet.rs" doc-scrape-examples = true -required-features = ["meshlet"] +required-features = ["meshlet_processor", "asset_processor"] [package.metadata.example.meshlet] name = "Meshlet" description = "Meshlet rendering for dense high-poly scenes (experimental)" category = "3D Rendering" wasm = false -setup = [ - [ - "curl", - "-o", - "assets/models/bunny.meshlet_mesh", - "https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/bd869887bc5c9c6e74e353f657d342bef84bacd8/bunny.meshlet_mesh", - ], -] [[example]] name = "lightmaps" diff --git a/assets/models/bunny.glb b/assets/models/bunny.glb new file mode 100644 index 0000000000000..6cf0f0bc57e10 Binary files /dev/null and b/assets/models/bunny.glb differ diff --git a/assets/models/bunny.glb.meta b/assets/models/bunny.glb.meta new file mode 100644 index 0000000000000..982f85504337d --- /dev/null +++ b/assets/models/bunny.glb.meta @@ -0,0 +1,10 @@ +( + meta_format_version: "1.0", + asset: Process( + processor: "bevy::gltf::MeshletMeshProcessor", + settings: ( + loader_settings: (), + saver_settings: (), + ), + ), +) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index d6db4b988a283..e390e19ea8f01 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -283,6 +283,14 @@ pub trait AssetApp { fn register_asset_loader(&mut self, loader: L) -> &mut Self; /// Registers the given `processor` in the [`App`]'s [`AssetProcessor`]. fn register_asset_processor(&mut self, processor: P) -> &mut Self; + /// Registers the given `processor` in the [`App`]'s [`AssetProcessor`] along with an extra alias. + /// + /// The alias can be used in meta files to refer to this asset processor instead of using the full type name. + fn register_asset_processor_with_alias( + &mut self, + processor: P, + alias: &'static str, + ) -> &mut Self; /// Registers the given [`AssetSourceBuilder`] with the given `id`. /// /// Note that asset sources must be registered before adding [`AssetPlugin`] to your application, @@ -331,6 +339,17 @@ impl AssetApp for App { self } + fn register_asset_processor_with_alias( + &mut self, + processor: P, + alias: &'static str, + ) -> &mut Self { + if let Some(asset_processor) = self.world().get_resource::() { + asset_processor.register_processor_with_alias(processor, alias); + } + self + } + fn register_asset_source( &mut self, id: impl Into>, diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index bd33f9bb15796..b0db82d1a388e 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -49,7 +49,7 @@ use crate::io::{AssetReader, AssetWriter}; /// [`AssetProcessor`] can be run in the background while a Bevy App is running. Changes to assets will be automatically detected and hot-reloaded. /// /// Assets will only be re-processed if they have been changed. A hash of each asset source is stored in the metadata of the processed version of the -/// asset, which is used to determine if the asset source has actually changed. +/// asset, which is used to determine if the asset source has actually changed. /// /// A [`ProcessorTransactionLog`] is produced, which uses "write-ahead logging" to make the [`AssetProcessor`] crash and failure resistant. If a failed/unfinished /// transaction from a previous run is detected, the affected asset(s) will be re-processed. @@ -489,6 +489,16 @@ impl AssetProcessor { process_plans.insert(std::any::type_name::

(), Arc::new(processor)); } + /// Register a new asset processor with an alias. + pub fn register_processor_with_alias(&self, processor: P, alias: &'static str) { + let mut process_plans = self.data.processors.write(); + #[cfg(feature = "trace")] + let processor = InstrumentedAssetProcessor(processor); + let processor = Arc::new(processor); + process_plans.insert(alias, processor.clone()); + process_plans.insert(std::any::type_name::

(), processor); + } + /// Set the default processor for the given `extension`. Make sure `P` is registered with [`AssetProcessor::register_processor`]. pub fn set_default_processor(&self, extension: &str) { let mut default_processors = self.data.default_processors.write(); diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index b74079b3866a8..3d1f3c9da2236 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -12,6 +12,10 @@ keywords = ["bevy"] dds = ["bevy_render/dds"] pbr_transmission_textures = ["bevy_pbr/pbr_transmission_textures"] pbr_multi_layer_material_textures = [] +# Enables the meshlet renderer for dense high-poly scenes (experimental) +meshlet = ["bevy_pbr/meshlet"] +# Enables processing meshes into meshlet meshes +meshlet_processor = ["meshlet", "bevy_pbr/meshlet_processor"] [dependencies] # bevy diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index f2bd5c014c6f3..f98062a4aafbb 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -96,7 +96,7 @@ //! - `Scene{}`: glTF Scene as a Bevy `Scene` //! - `Node{}`: glTF Node as a `GltfNode` //! - `Mesh{}`: glTF Mesh as a `GltfMesh` -//! - `Mesh{}/Primitive{}`: glTF Primitive as a Bevy `Mesh` +//! - `Mesh{}/Primitive{}`: glTF Primitive as a Bevy `Mesh` or `MeshletMesh` //! - `Mesh{}/Primitive{}/MorphTargets`: Morph target animation data for a glTF Primitive //! - `Texture{}`: glTF Texture as a Bevy `Image` //! - `Material{}`: glTF Material as a Bevy `StandardMaterial` @@ -109,6 +109,8 @@ use bevy_animation::AnimationClip; use bevy_utils::HashMap; mod loader; +#[cfg(feature = "meshlet")] +mod meshlet_saver; mod vertex_attributes; pub use loader::*; @@ -149,11 +151,18 @@ impl GltfPlugin { impl Plugin for GltfPlugin { fn build(&self, app: &mut App) { app.register_type::() + .init_asset::() .init_asset::() .init_asset::() .init_asset::() .init_asset::() .preregister_asset_loader::(&["gltf", "glb"]); + + #[cfg(feature = "meshlet")] + app.register_asset_processor_with_alias::>( + meshlet_saver::MeshletMeshGltfSaver.into(), + "bevy::gltf::MeshletMeshProcessor" + ); } fn finish(&self, app: &mut App) { @@ -161,6 +170,8 @@ impl Plugin for GltfPlugin { Some(render_device) => CompressedImageFormats::from_features(render_device.features()), None => CompressedImageFormats::NONE, }; + + app.register_asset_loader(RawGltfLoader); app.register_asset_loader(GltfLoader { supported_compressed_formats, custom_vertex_attributes: self.custom_vertex_attributes.clone(), @@ -168,7 +179,16 @@ impl Plugin for GltfPlugin { } } -/// Representation of a loaded glTF file. +/// Underlying JSON Representation of a loaded glTF file. +#[derive(Asset, Debug, TypePath)] +pub struct RawGltf { + /// The JSON section of a glTF file. + pub gltf: gltf::Gltf, + /// The buffers of a glTF file, whether from the GLB BIN section, or from external bin files. + pub buffer_data: Vec>, +} + +/// Bevy representation of a loaded glTF file. #[derive(Asset, Debug, TypePath)] pub struct Gltf { /// All scenes loaded from the glTF file. @@ -234,6 +254,11 @@ pub struct GltfMesh { pub struct GltfPrimitive { /// Topology to be rendered. pub mesh: Handle, + /// Meshlet topology to be rendered. + /// + /// If this is Some, then `mesh` is [`Handle::default()`]. + #[cfg(feature = "meshlet")] + pub meshlet_mesh: Option>, /// Material to apply to the `mesh`. pub material: Option>, /// Additional data. diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index bfdec7aaacdc8..4e637dbc60f1f 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -1,4 +1,4 @@ -use crate::{vertex_attributes::convert_attribute, Gltf, GltfExtras, GltfNode}; +use crate::{vertex_attributes::convert_attribute, Gltf, GltfExtras, GltfNode, RawGltf}; #[cfg(feature = "bevy_animation")] use bevy_animation::{AnimationTarget, AnimationTargetId}; use bevy_asset::{ @@ -11,6 +11,10 @@ use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{entity::Entity, world::World}; use bevy_hierarchy::{BuildWorldChildren, WorldChildBuilder}; use bevy_math::{Affine2, Mat4, Vec3}; +#[cfg(feature = "meshlet")] +use bevy_pbr::experimental::meshlet::{ + MaterialMeshletMeshBundle, MeshletMesh, MESHLET_MESH_ASSET_VERSION, +}; use bevy_pbr::{ DirectionalLight, DirectionalLightBundle, PbrBundle, PointLight, PointLightBundle, SpotLight, SpotLightBundle, StandardMaterial, UvChannel, MAX_JOINTS, @@ -91,7 +95,7 @@ pub enum GltfError { ReadAssetBytesError(#[from] ReadAssetBytesError), /// Failed to load asset from an asset path. #[error("failed to load asset from an asset path: {0}")] - AssetLoadError(#[from] AssetLoadError), + AssetLoadError(#[from] Box), /// Missing sampler for an animation. #[error("Missing sampler for animation {0}")] MissingAnimationSampler(usize), @@ -104,9 +108,21 @@ pub enum GltfError { /// Failed to load a file. #[error("failed to load file: {0}")] Io(#[from] std::io::Error), + /// Wrong meshlet mesh asset version. + #[cfg(feature = "meshlet")] + #[error( + "Encountered an invalid MeshletMesh. The asset format may have changed. \ + Delete the imported_assets folder to regenerate assets." + )] + MeshletMeshWrongVersion, } +/// Loads glTF files as a [`RawGltf`]. +pub struct RawGltfLoader; + /// Loads glTF files with all of their data as their corresponding bevy representations. +/// +/// This is the default asset loader for .gltf and .glb files. pub struct GltfLoader { /// List of compressed image formats handled by the loader. pub supported_compressed_formats: CompressedImageFormats, @@ -135,7 +151,8 @@ pub struct GltfLoader { /// } /// ); /// ``` -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] +#[serde(default)] pub struct GltfLoaderSettings { /// If empty, the gltf mesh nodes will be skipped. /// @@ -185,6 +202,26 @@ impl AssetLoader for GltfLoader { } } +impl AssetLoader for RawGltfLoader { + type Asset = RawGltf; + type Settings = GltfLoaderSettings; + type Error = GltfError; + async fn load<'a>( + &'a self, + reader: &'a mut Reader<'_>, + _settings: &'a GltfLoaderSettings, + load_context: &'a mut LoadContext<'_>, + ) -> Result { + let mut bytes = Vec::new(); + reader.read_to_end(&mut bytes).await?; + let gltf = gltf::Gltf::from_slice(&bytes)?; + + let buffer_data = load_buffers(&gltf, load_context).await?; + + Ok(RawGltf { gltf, buffer_data }) + } +} + /// Loads an entire glTF file. async fn load_gltf<'a, 'b, 'c>( loader: &GltfLoader, @@ -436,113 +473,72 @@ async fn load_gltf<'a, 'b, 'c>( let mut primitives = vec![]; for primitive in gltf_mesh.primitives() { let primitive_label = primitive_label(&gltf_mesh, &primitive); - let primitive_topology = get_primitive_topology(primitive.mode())?; - - let mut mesh = Mesh::new(primitive_topology, settings.load_meshes); - - // Read vertex attributes - for (semantic, accessor) in primitive.attributes() { - if [Semantic::Joints(0), Semantic::Weights(0)].contains(&semantic) { - if !meshes_on_skinned_nodes.contains(&gltf_mesh.index()) { - warn!( - "Ignoring attribute {:?} for skinned mesh {:?} used on non skinned nodes (NODE_SKINNED_MESH_WITHOUT_SKIN)", - semantic, - primitive_label - ); - continue; - } else if meshes_on_non_skinned_nodes.contains(&gltf_mesh.index()) { - error!("Skinned mesh {:?} used on both skinned and non skin nodes, this is likely to cause an error (NODE_SKINNED_MESH_WITHOUT_SKIN)", primitive_label); - } - } - match convert_attribute( - semantic, - accessor, - &buffer_data, - &loader.custom_vertex_attributes, - ) { - Ok((attribute, values)) => mesh.insert_attribute(attribute, values), - Err(err) => warn!("{}", err), - } - } - - // Read vertex indices - let reader = primitive.reader(|buffer| Some(buffer_data[buffer.index()].as_slice())); - if let Some(indices) = reader.read_indices() { - mesh.insert_indices(match indices { - ReadIndices::U8(is) => Indices::U16(is.map(|x| x as u16).collect()), - ReadIndices::U16(is) => Indices::U16(is.collect()), - ReadIndices::U32(is) => Indices::U32(is.collect()), - }); - }; + #[cfg(feature = "meshlet")] + let (mesh, meshlet_mesh) = match primitive + .extensions() + .and_then(|extensions| extensions.get("BEVY_meshlet_mesh")) { - let morph_target_reader = reader.read_morph_targets(); - if morph_target_reader.len() != 0 { - let morph_targets_label = morph_targets_label(&gltf_mesh, &primitive); - let morph_target_image = MorphTargetImage::new( - morph_target_reader.map(PrimitiveMorphAttributesIter), - mesh.count_vertices(), - RenderAssetUsages::default(), - )?; - let handle = - load_context.add_labeled_asset(morph_targets_label, morph_target_image.0); - - mesh.set_morph_targets(handle); - let extras = gltf_mesh.extras().as_ref(); - if let Option::::Some(names) = - extras.and_then(|extras| serde_json::from_str(extras.get()).ok()) - { - mesh.set_morph_target_names(names.target_names); + Some(bevy_meshlet_mesh_extension) => { + let version = bevy_meshlet_mesh_extension["version"].as_u64().unwrap(); + if version != MESHLET_MESH_ASSET_VERSION { + return Err(GltfError::MeshletMeshWrongVersion); } - } - } - if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none() - && matches!(mesh.primitive_topology(), PrimitiveTopology::TriangleList) - { - bevy_utils::tracing::debug!( - "Automatically calculating missing vertex normals for geometry." - ); - let vertex_count_before = mesh.count_vertices(); - mesh.duplicate_vertices(); - mesh.compute_flat_normals(); - let vertex_count_after = mesh.count_vertices(); - if vertex_count_before != vertex_count_after { - bevy_utils::tracing::debug!("Missing vertex normals in indexed geometry, computing them as flat. Vertex count increased from {} to {}", vertex_count_before, vertex_count_after); - } else { - bevy_utils::tracing::debug!( - "Missing vertex normals in indexed geometry, computing them as flat." - ); - } - } + let byte_range_start = bevy_meshlet_mesh_extension["byteRangeStart"] + .as_u64() + .unwrap() as usize; + let byte_range_end = bevy_meshlet_mesh_extension["byteRangeEnd"] + .as_u64() + .unwrap() as usize; + let byte_range = byte_range_start..byte_range_end; - if let Some(vertex_attribute) = reader - .read_tangents() - .map(|v| VertexAttributeValues::Float32x4(v.collect())) - { - mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); - } else if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some() - && material_needs_tangents(&primitive.material()) - { - bevy_utils::tracing::debug!( - "Missing vertex tangents for {}, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents.", file_name - ); + let meshlet_mesh = + MeshletMesh::from_bytes(&buffer_data[0][byte_range]).unwrap(); - let generate_tangents_span = info_span!("generate_tangents", name = file_name); + let meshlet_mesh = + Some(load_context.add_labeled_asset(primitive_label, meshlet_mesh)); + (Handle::default(), meshlet_mesh) + } + None => { + let mesh = load_mesh_primitive( + &primitive_label, + &gltf_mesh, + &primitive, + settings, + &meshes_on_skinned_nodes, + &meshes_on_non_skinned_nodes, + &buffer_data, + loader, + load_context, + &file_name, + )?; + let mesh = load_context.add_labeled_asset(primitive_label, mesh); + (mesh, None) + } + }; - generate_tangents_span.in_scope(|| { - if let Err(err) = mesh.generate_tangents() { - warn!( - "Failed to generate vertex tangents using the mikktspace algorithm: {:?}", - err - ); - } - }); - } + #[cfg(not(feature = "meshlet"))] + let mesh = { + let mesh = load_mesh_primitive( + &primitive_label, + &gltf_mesh, + &primitive, + settings, + &meshes_on_skinned_nodes, + &meshes_on_non_skinned_nodes, + &buffer_data, + loader, + load_context, + &file_name, + )?; + load_context.add_labeled_asset(primitive_label, mesh) + }; - let mesh = load_context.add_labeled_asset(primitive_label, mesh); primitives.push(super::GltfPrimitive { mesh, + #[cfg(feature = "meshlet")] + meshlet_mesh, material: primitive .material() .index() @@ -724,6 +720,116 @@ async fn load_gltf<'a, 'b, 'c>( }) } +#[allow(clippy::too_many_arguments)] +fn load_mesh_primitive( + primitive_label: &str, + gltf_mesh: &gltf::Mesh, + primitive: &Primitive, + settings: &GltfLoaderSettings, + meshes_on_skinned_nodes: &HashSet, + meshes_on_non_skinned_nodes: &HashSet, + buffer_data: &Vec>, + loader: &GltfLoader, + load_context: &mut LoadContext<'_>, + file_name: &String, +) -> Result { + let primitive_topology = get_primitive_topology(primitive.mode())?; + let mut mesh = Mesh::new(primitive_topology, settings.load_meshes); + for (semantic, accessor) in primitive.attributes() { + if [Semantic::Joints(0), Semantic::Weights(0)].contains(&semantic) { + if !meshes_on_skinned_nodes.contains(&gltf_mesh.index()) { + warn!( + "Ignoring attribute {:?} for skinned mesh {:?} used on non skinned nodes (NODE_SKINNED_MESH_WITHOUT_SKIN)", + semantic, + primitive_label + ); + continue; + } else if meshes_on_non_skinned_nodes.contains(&gltf_mesh.index()) { + error!("Skinned mesh {:?} used on both skinned and non skin nodes, this is likely to cause an error (NODE_SKINNED_MESH_WITHOUT_SKIN)", primitive_label); + } + } + match convert_attribute( + semantic, + accessor, + buffer_data, + &loader.custom_vertex_attributes, + ) { + Ok((attribute, values)) => mesh.insert_attribute(attribute, values), + Err(err) => warn!("{}", err), + } + } + let reader = primitive.reader(|buffer| Some(buffer_data[buffer.index()].as_slice())); + if let Some(indices) = reader.read_indices() { + mesh.insert_indices(match indices { + ReadIndices::U8(is) => Indices::U16(is.map(|x| x as u16).collect()), + ReadIndices::U16(is) => Indices::U16(is.collect()), + ReadIndices::U32(is) => Indices::U32(is.collect()), + }); + }; + { + let morph_target_reader = reader.read_morph_targets(); + if morph_target_reader.len() != 0 { + let morph_targets_label = morph_targets_label(gltf_mesh, primitive); + let morph_target_image = MorphTargetImage::new( + morph_target_reader.map(PrimitiveMorphAttributesIter), + mesh.count_vertices(), + RenderAssetUsages::default(), + )?; + let handle = load_context.add_labeled_asset(morph_targets_label, morph_target_image.0); + + mesh.set_morph_targets(handle); + let extras = gltf_mesh.extras().as_ref(); + if let Option::::Some(names) = + extras.and_then(|extras| serde_json::from_str(extras.get()).ok()) + { + mesh.set_morph_target_names(names.target_names); + } + } + } + if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none() + && matches!(mesh.primitive_topology(), PrimitiveTopology::TriangleList) + { + bevy_utils::tracing::debug!( + "Automatically calculating missing vertex normals for geometry." + ); + let vertex_count_before = mesh.count_vertices(); + mesh.duplicate_vertices(); + mesh.compute_flat_normals(); + let vertex_count_after = mesh.count_vertices(); + if vertex_count_before != vertex_count_after { + bevy_utils::tracing::debug!("Missing vertex normals in indexed geometry, computing them as flat. Vertex count increased from {} to {}", vertex_count_before, vertex_count_after); + } else { + bevy_utils::tracing::debug!( + "Missing vertex normals in indexed geometry, computing them as flat." + ); + } + } + if let Some(vertex_attribute) = reader + .read_tangents() + .map(|v| VertexAttributeValues::Float32x4(v.collect())) + { + mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); + } else if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some() + && material_needs_tangents(&primitive.material()) + { + bevy_utils::tracing::debug!( + "Missing vertex tangents for {}, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents.", file_name + ); + + let generate_tangents_span = info_span!("generate_tangents", name = file_name); + + generate_tangents_span.in_scope(|| { + if let Err(err) = mesh.generate_tangents() { + warn!( + "Failed to generate vertex tangents using the mikktspace algorithm: {:?}", + err + ); + } + }); + } + Ok(mesh) +} + fn get_gltf_extras(extras: &gltf::json::Extras) -> Option { extras.as_ref().map(|extras| GltfExtras { value: extras.get().to_string(), @@ -1250,12 +1356,33 @@ fn load_node( let primitive_label = primitive_label(&mesh, &primitive); let bounds = primitive.bounding_box(); - let mut mesh_entity = parent.spawn(PbrBundle { - // TODO: handle missing label handle errors here? - mesh: load_context.get_label_handle(&primitive_label), - material: load_context.get_label_handle(&material_label), - ..Default::default() - }); + let mut mesh_entity = if primitive + .extensions() + .map(|extensions| extensions.contains_key("BEVY_meshlet_mesh")) + .unwrap_or(false) + { + #[cfg(feature = "meshlet")] + { + parent.spawn(MaterialMeshletMeshBundle:: { + // TODO: handle missing label handle errors here? + meshlet_mesh: load_context.get_label_handle(&primitive_label), + material: load_context.get_label_handle(&material_label), + ..Default::default() + }) + } + #[cfg(not(feature = "meshlet"))] + { + panic!("glTF file contained a MeshletMesh, but the meshlet cargo feature is not enabled.") + } + } else { + parent.spawn(PbrBundle { + // TODO: handle missing label handle errors here? + mesh: load_context.get_label_handle(&primitive_label), + material: load_context.get_label_handle(&material_label), + ..Default::default() + }) + }; + let target_count = primitive.morph_targets().len(); if target_count != 0 { let weights = match mesh.weights() { @@ -1569,7 +1696,7 @@ fn texture_address_mode(gltf_address_mode: &WrappingMode) -> ImageAddressMode { /// Maps the `primitive_topology` form glTF to `wgpu`. #[allow(clippy::result_large_err)] -fn get_primitive_topology(mode: Mode) -> Result { +pub(crate) fn get_primitive_topology(mode: Mode) -> Result { match mode { Mode::Points => Ok(PrimitiveTopology::PointList), Mode::Lines => Ok(PrimitiveTopology::LineList), diff --git a/crates/bevy_gltf/src/meshlet_saver.rs b/crates/bevy_gltf/src/meshlet_saver.rs new file mode 100644 index 0000000000000..f522bb492f4c9 --- /dev/null +++ b/crates/bevy_gltf/src/meshlet_saver.rs @@ -0,0 +1,227 @@ +use crate::{ + get_primitive_topology, vertex_attributes::convert_attribute, GltfError, GltfLoader, + GltfLoaderSettings, RawGltf, +}; +use bevy_asset::{ + io::Writer, + saver::{AssetSaver, SavedAsset}, + AsyncWriteExt, +}; +use bevy_pbr::experimental::meshlet::{MeshletMesh, MESHLET_MESH_ASSET_VERSION}; +use bevy_render::{ + mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}, + render_asset::RenderAssetUsages, +}; +use bevy_utils::{ + tracing::{debug, warn}, + HashMap, +}; +use gltf::{ + json::{Buffer, Index}, + mesh::util::ReadIndices, + Primitive, +}; +use serde_json::json; +use std::{collections::VecDeque, iter, ops::Deref}; + +/// An [`AssetSaver`] that converts all mesh primitives in a glTF file into [`MeshletMesh`]s. +/// +/// Only certain types of meshes and materials are supported. See [`MeshletMesh`] and [`MeshletMesh::from_mesh`] +/// for more details. +/// +/// Using this asset saver requires enabling the `meshlet_processor` cargo feature in addition to `asset_processor`. +/// +/// Use only glTF Binary (.glb) or glTF Embedded (.gltf without additional .bin) files. +/// Using glTF Separate files (.gltf with additional .bin) will lead to unnecessary data in the final processed asset. +/// +/// Compiling in release mode is strongly recommended, as the conversion process is very slow when compiling +/// without optimizations. +/// +/// Example asset meta file: +/// ``` +/// ( +/// meta_format_version: "1.0", +/// asset: Process( +/// processor: "bevy::gltf::MeshletMeshProcessor", +/// settings: ( +/// loader_settings: (), +/// saver_settings: (), +/// ), +/// ), +/// ) +/// ``` +pub struct MeshletMeshGltfSaver; + +impl AssetSaver for MeshletMeshGltfSaver { + type Asset = RawGltf; + type Settings = GltfLoaderSettings; + type OutputLoader = GltfLoader; + type Error = GltfError; + + async fn save<'a>( + &'a self, + writer: &'a mut Writer, + asset: SavedAsset<'a, RawGltf>, + settings: &'a GltfLoaderSettings, + ) -> Result { + #[cfg(not(feature = "meshlet_processor"))] + panic!( + "Converting GLTF files to use MeshletMeshes requires cargo feature meshlet_processor." + ); + + // Convert each primitive to a meshlet mesh + let mut meshlet_meshes: VecDeque = VecDeque::new(); + for mesh in asset.gltf.meshes() { + for primitive in mesh.primitives() { + let mesh = load_mesh(&primitive, &asset.buffer_data)?; + + #[cfg(feature = "meshlet_processor")] + { + let meshlet_mesh = MeshletMesh::from_mesh(&mesh).expect("TODO"); + meshlet_meshes.push_back(meshlet_mesh); + } + } + } + + // Create a mutable copy of the gltf asset + let mut gltf = asset.gltf.deref().clone().into_json(); + + // Clone the GLB BIN buffer, if it exists, or make a new buffer + let mut glb_buffer = match gltf.buffers.first() { + Some(buffer) if buffer.uri.is_none() => asset.buffer_data[0].clone(), + _ => Vec::new(), + }; + + // If there was not an existing GLB BIN buffer, but there were other buffers, + // increment each buffer view's buffer index to account for the GLB BIN buffer + // that we're going to add at index 0 + if let Some(Buffer { uri: Some(_), .. }) = gltf.buffers.first() { + for buffer_view in &mut gltf.buffer_views { + buffer_view.buffer = Index::new(buffer_view.buffer.value() as u32 + 1); + } + } + + // For each primitive, append the serialized meshlet mesh to the GLB BIN buffer, + // and add a custom extension pointing to the newly added slice of the buffer + for mesh in &mut gltf.meshes { + for primitive in &mut mesh.primitives { + let meshlet_mesh = meshlet_meshes.pop_front().unwrap(); + let mut meshlet_mesh_bytes = meshlet_mesh.into_bytes().expect("TODO"); + + let extension = json!({ + "version": MESHLET_MESH_ASSET_VERSION, + "byteRangeStart": glb_buffer.len(), + "byteRangeEnd": glb_buffer.len() + meshlet_mesh_bytes.len(), + }); + + primitive + .extensions + .get_or_insert(Default::default()) + .others + .insert("BEVY_meshlet_mesh".to_owned(), extension); + + glb_buffer.append(&mut meshlet_mesh_bytes); + + // TODO: Remove primitive indices, attributes, buffer views, and buffers + } + } + + // Pad GLB BIN buffer if needed + glb_buffer.extend(iter::repeat(0x00u8).take(glb_buffer.len() % 4)); + + match gltf.buffers.get_mut(0) { + // If there was an existing GLB BIN buffer, update it's size to account + // for the newly added meshlet mesh data + Some(buffer) if buffer.uri.is_none() => { + buffer.byte_length = glb_buffer.len().into(); + } + // Else insert a new GLB BIN buffer + _ => { + let buffer = Buffer { + byte_length: glb_buffer.len().into(), + name: None, + uri: None, + extensions: None, + extras: None, + }; + gltf.buffers.insert(0, buffer); + } + } + + // Pad JSON buffer if needed + let mut gltf_bytes = gltf.to_vec().expect("TODO"); + gltf_bytes.extend(iter::repeat(0x20u8).take(gltf_bytes.len() % 4)); + + // Calculate total GLB file size (headers, including chunk headers, plus JSON and BIN chunk) + let json_len = gltf_bytes.len() as u32; + let bin_len = glb_buffer.len() as u32; + let file_size = 28 + json_len + bin_len; + + // Write file header + writer.write_all(&0x46546C67u32.to_le_bytes()).await?; + writer.write_all(&2u32.to_le_bytes()).await?; + writer.write_all(&file_size.to_le_bytes()).await?; + + // Write JSON chunk + writer.write_all(&json_len.to_le_bytes()).await?; + writer.write_all(&0x4E4F534Au32.to_le_bytes()).await?; + writer.write_all(&gltf_bytes).await?; + + // Write BIN chunk + writer.write_all(&bin_len.to_le_bytes()).await?; + writer.write_all(&0x004E4942u32.to_le_bytes()).await?; + writer.write_all(&glb_buffer).await?; + + Ok(settings.clone()) + } +} + +fn load_mesh(primitive: &Primitive<'_>, buffer_data: &Vec>) -> Result { + let primitive_topology = get_primitive_topology(primitive.mode())?; + + let mut mesh = Mesh::new(primitive_topology, RenderAssetUsages::default()); + + for (semantic, accessor) in primitive.attributes() { + match convert_attribute(semantic, accessor, buffer_data, &HashMap::new()) { + Ok((attribute, values)) => mesh.insert_attribute(attribute, values), + Err(err) => warn!("{}", err), + } + } + + let reader = primitive.reader(|buffer| Some(buffer_data[buffer.index()].as_slice())); + + if let Some(indices) = reader.read_indices() { + mesh.insert_indices(match indices { + ReadIndices::U8(is) => Indices::U16(is.map(|x| x as u16).collect()), + ReadIndices::U16(is) => Indices::U16(is.collect()), + ReadIndices::U32(is) => Indices::U32(is.collect()), + }); + }; + + if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none() + && matches!(mesh.primitive_topology(), PrimitiveTopology::TriangleList) + { + debug!("Automatically calculating missing vertex normals for geometry."); + let vertex_count_before = mesh.count_vertices(); + mesh.duplicate_vertices(); + mesh.compute_flat_normals(); + let vertex_count_after = mesh.count_vertices(); + if vertex_count_before != vertex_count_after { + debug!("Missing vertex normals in indexed geometry, computing them as flat. Vertex count increased from {} to {}", vertex_count_before, vertex_count_after); + } else { + debug!("Missing vertex normals in indexed geometry, computing them as flat."); + } + } + + if let Some(vertex_attribute) = reader + .read_tangents() + .map(|v| VertexAttributeValues::Float32x4(v.collect())) + { + mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); + } else { + debug!("Missing vertex tangents, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents."); + mesh.generate_tangents()?; + } + + Ok(mesh) +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index e6cc7b3da2e54..ec3f02afb99ab 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -171,10 +171,13 @@ bevy_debug_stepping = [ ] # Enables the meshlet renderer for dense high-poly scenes (experimental) -meshlet = ["bevy_pbr?/meshlet"] +meshlet = ["bevy_pbr?/meshlet", "bevy_gltf?/meshlet"] -# Enables processing meshes into meshlet meshes for bevy_pbr -meshlet_processor = ["bevy_pbr?/meshlet_processor"] +# Enables processing meshes into meshlet meshes +meshlet_processor = [ + "bevy_pbr?/meshlet_processor", + "bevy_gltf?/meshlet_processor", +] # Provides a collection of developer tools bevy_dev_tools = ["dep:bevy_dev_tools"] diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index d6a2740dbf7d4..7891b923d4ce6 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -19,6 +19,9 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 0; /// The conversion step is very slow, and is meant to be ran once ahead of time, and not during runtime. This type of mesh is not suitable for /// dynamically generated geometry. /// +/// In addition to converting individual meshes via code, asset processing can be used to convert whole glTF scenes. +/// See `MeshletMeshGltfSaver` for details. +/// /// There are restrictions on the [`crate::Material`] functionality that can be used with this type of mesh. /// * Materials have no control over the vertex shader or vertex attributes. /// * Materials must be opaque. Transparent, alpha masked, and transmissive materials are not supported. @@ -30,23 +33,45 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 0; #[derive(Asset, TypePath, Serialize, Deserialize, Clone)] pub struct MeshletMesh { /// The total amount of triangles summed across all LOD 0 meshlets in the mesh. - pub worst_case_meshlet_triangles: u64, + pub(crate) worst_case_meshlet_triangles: u64, /// Raw vertex data bytes for the overall mesh. - pub vertex_data: Arc<[u8]>, + pub(crate) vertex_data: Arc<[u8]>, /// Indices into `vertex_data`. - pub vertex_ids: Arc<[u32]>, + pub(crate) vertex_ids: Arc<[u32]>, /// Indices into `vertex_ids`. - pub indices: Arc<[u8]>, + pub(crate) indices: Arc<[u8]>, /// The list of meshlets making up this mesh. - pub meshlets: Arc<[Meshlet]>, + pub(crate) meshlets: Arc<[Meshlet]>, /// Spherical bounding volumes. - pub bounding_spheres: Arc<[MeshletBoundingSpheres]>, + pub(crate) bounding_spheres: Arc<[MeshletBoundingSpheres]>, +} + +impl MeshletMesh { + /// Convert a [`MeshletMesh`] into a byte array. + /// + /// The resulting byte array should be treated as an opaque blob that is only compatible + /// with the version of Bevy used to generate it. + pub fn into_bytes(&self) -> Result, MeshletMeshSaveOrLoadError> { + let mut bytes = Vec::new(); + let mut writer = FrameEncoder::new(&mut bytes); + bincode::serialize_into(&mut writer, &self)?; + writer.finish()?; + Ok(bytes) + } + + /// Create a [`MeshletMesh`] from a byte array. + /// + /// The byte array must have been generated via [`MeshletMesh::into_bytes`] using + /// the same version of Bevy that this function is called from. + pub fn from_bytes(bytes: &[u8]) -> Result { + bincode::deserialize_from(FrameDecoder::new(Cursor::new(bytes))) + } } /// A single meshlet within a [`MeshletMesh`]. #[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] #[repr(C)] -pub struct Meshlet { +pub(crate) struct Meshlet { /// The offset within the parent mesh's [`MeshletMesh::vertex_ids`] buffer where the indices for this meshlet begin. pub start_vertex_id: u32, /// The offset within the parent mesh's [`MeshletMesh::indices`] buffer where the indices for this meshlet begin. @@ -58,7 +83,7 @@ pub struct 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 { +pub(crate) 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. @@ -70,7 +95,7 @@ pub struct MeshletBoundingSpheres { /// A spherical bounding volume used for a [`Meshlet`]. #[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)] #[repr(C)] -pub struct MeshletBoundingSphere { +pub(crate) struct MeshletBoundingSphere { pub center: Vec3, pub radius: f32, } @@ -96,7 +121,7 @@ impl AssetLoader for MeshletMeshSaverLoad { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; - let asset = bincode::deserialize_from(FrameDecoder::new(Cursor::new(bytes)))?; + let asset = MeshletMesh::from_bytes(&bytes)?; Ok(asset) } @@ -122,11 +147,7 @@ impl AssetSaver for MeshletMeshSaverLoad { .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?; + writer.write_all(&asset.into_bytes()?).await?; Ok(()) } diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index bae7feb1177d9..a5ff00fad121e 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -20,6 +20,9 @@ impl MeshletMesh { /// /// This function requires the `meshlet_processor` cargo feature. /// + /// Compiling in release mode is strongly recommended, as the conversion process is very + /// slow when compiling without optimizations. + /// /// The input mesh must: /// 1. Use [`PrimitiveTopology::TriangleList`] /// 2. Use indices diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index ecd3201918198..ebba6a834440a 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -8,34 +8,29 @@ mod camera_controller; use bevy::{ pbr::{ experimental::meshlet::{MaterialMeshletMeshBundle, MeshletPlugin}, - CascadeShadowConfigBuilder, DirectionalLightShadowMap, + CascadeShadowConfig, CascadeShadowConfigBuilder, DirectionalLightShadowMap, }, prelude::*, render::render_resource::AsBindGroup, }; 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/bd869887bc5c9c6e74e353f657d342bef84bacd8/bunny.meshlet_mesh"; - -fn main() -> ExitCode { - if !Path::new("./assets/models/bunny.meshlet_mesh").exists() { - eprintln!("ERROR: Asset at path /assets/models/bunny.meshlet_mesh is missing. Please download it from {ASSET_URL}"); - return ExitCode::FAILURE; - } +use std::f32::consts::PI; +fn main() { App::new() .insert_resource(DirectionalLightShadowMap { size: 4096 }) .add_plugins(( - DefaultPlugins, + DefaultPlugins.set(AssetPlugin { + mode: AssetMode::Processed, + ..default() + }), MeshletPlugin, MaterialPlugin::::default(), CameraControllerPlugin, )) .add_systems(Startup, setup) + .add_systems(Update, set_shadow_map_config) .run(); - - ExitCode::SUCCESS } fn setup( @@ -65,12 +60,6 @@ fn setup( shadows_enabled: true, ..default() }, - cascade_shadow_config: CascadeShadowConfigBuilder { - num_cascades: 1, - maximum_distance: 15.0, - ..default() - } - .build(), transform: Transform::from_rotation(Quat::from_euler( EulerRot::ZYX, 0.0, @@ -80,11 +69,7 @@ fn setup( ..default() }); - // A custom file format storing a [`bevy_render::mesh::Mesh`] - // that has been converted to a [`bevy_pbr::meshlet::MeshletMesh`] - // using [`bevy_pbr::meshlet::MeshletMesh::from_mesh`], which is - // a function only available when the `meshlet_processor` cargo feature is enabled. - let meshlet_mesh_handle = asset_server.load("models/bunny.meshlet_mesh"); + let meshlet_mesh_handle = asset_server.load("models/bunny.glb#Mesh0/Primitive0"); let debug_material = debug_materials.add(MeshletDebugMaterial::default()); for x in -2..=2 { @@ -131,6 +116,20 @@ fn setup( }); } +fn set_shadow_map_config( + mut shadow_confg: Query<&mut CascadeShadowConfig>, + camera: Query<&Transform, With>, +) { + let camera_transform = camera.get_single().unwrap(); + let mut shadow_config = shadow_confg.get_single_mut().unwrap(); + *shadow_config = CascadeShadowConfigBuilder { + num_cascades: 1, + maximum_distance: camera_transform.translation.y + 3.0, + ..default() + } + .build(); +} + #[derive(Asset, TypePath, AsBindGroup, Clone, Default)] struct MeshletDebugMaterial { _dummy: (),