Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions crates/bevy_pbr/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +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:serde",
"dep:bincode",
"dep:thiserror",
"dep:range-alloc",
]
meshlet = ["dep:lz4_flex", "dep:thiserror", "dep:range-alloc", "dep:bevy_tasks"]
# Enables processing meshes into meshlet meshes
meshlet_processor = ["meshlet", "dep:meshopt", "dep:metis", "dep:itertools"]

Expand All @@ -34,16 +28,17 @@ bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.15.0-dev" }
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
"bevy",
] }
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", optional = true }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }


# other
Expand All @@ -53,8 +48,6 @@ fixedbitset = "0.5"
lz4_flex = { version = "0.11", default-features = false, features = [
"frame",
], optional = true }
serde = { version = "1", features = ["derive", "rc"], optional = true }
bincode = { version = "1", optional = true }
thiserror = { version = "1", optional = true }
range-alloc = { version = "0.1", optional = true }
meshopt = { version = "0.3.0", optional = true }
Expand Down
184 changes: 134 additions & 50 deletions crates/bevy_pbr/src/meshlet/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ use bevy_asset::{
};
use bevy_math::Vec3;
use bevy_reflect::TypePath;
use bevy_tasks::block_on;
use bytemuck::{Pod, Zeroable};
use lz4_flex::frame::{FrameDecoder, FrameEncoder};
use serde::{Deserialize, Serialize};
use std::{io::Cursor, sync::Arc};
use std::{
io::{Read, Write},
sync::Arc,
};

/// Unique identifier for the [`MeshletMesh`] asset format.
const MESHLET_MESH_ASSET_MAGIC: u64 = 1717551717668;

/// The current version of the [`MeshletMesh`] asset format.
pub const MESHLET_MESH_ASSET_VERSION: u64 = 0;
pub const MESHLET_MESH_ASSET_VERSION: u64 = 1;

/// A mesh that has been pre-processed into multiple small clusters of triangles called meshlets.
///
Expand All @@ -27,24 +33,24 @@ pub const MESHLET_MESH_ASSET_VERSION: u64 = 0;
/// * Limited control over [`bevy_render::render_resource::RenderPipelineDescriptor`] attributes.
///
/// See also [`super::MaterialMeshletMeshBundle`] and [`super::MeshletPlugin`].
#[derive(Asset, TypePath, Serialize, Deserialize, Clone)]
#[derive(Asset, TypePath, 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]>,
}

/// A single meshlet within a [`MeshletMesh`].
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct Meshlet {
/// The offset within the parent mesh's [`MeshletMesh::vertex_ids`] buffer where the indices for this meshlet begin.
Expand All @@ -56,7 +62,7 @@ pub struct Meshlet {
}

/// Bounding spheres used for culling and choosing level of detail for a [`Meshlet`].
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletBoundingSpheres {
/// The bounding sphere used for frustum and occlusion culling for this meshlet.
Expand All @@ -68,84 +74,162 @@ pub struct MeshletBoundingSpheres {
}

/// A spherical bounding volume used for a [`Meshlet`].
#[derive(Serialize, Deserialize, Copy, Clone, Pod, Zeroable)]
#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct MeshletBoundingSphere {
pub center: Vec3,
pub radius: f32,
}

/// An [`AssetLoader`] and [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets.
pub struct MeshletMeshSaverLoad;
pub struct MeshletMeshSaverLoader;

impl AssetLoader for MeshletMeshSaverLoad {
impl AssetSaver for MeshletMeshSaverLoader {
type Asset = MeshletMesh;
type Settings = ();
type OutputLoader = Self;
type Error = MeshletMeshSaveOrLoadError;

async fn load<'a>(
async fn save<'a>(
&'a self,
reader: &'a mut dyn 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 });
}
writer: &'a mut Writer,
asset: SavedAsset<'a, MeshletMesh>,
_settings: &'a (),
) -> Result<(), MeshletMeshSaveOrLoadError> {
// Write asset magic number
writer
.write_all(&MESHLET_MESH_ASSET_MAGIC.to_le_bytes())
.await?;

let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let asset = bincode::deserialize_from(FrameDecoder::new(Cursor::new(bytes)))?;
// Write asset version
writer
.write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes())
.await?;

Ok(asset)
}
// Compress and write asset data
writer
.write_all(&asset.worst_case_meshlet_triangles.to_le_bytes())
.await?;
let mut writer = FrameEncoder::new(AsyncWriteSyncAdapter(writer));
write_slice(&asset.vertex_data, &mut writer)?;
write_slice(&asset.vertex_ids, &mut writer)?;
write_slice(&asset.indices, &mut writer)?;
write_slice(&asset.meshlets, &mut writer)?;
write_slice(&asset.bounding_spheres, &mut writer)?;
writer.finish()?;

fn extensions(&self) -> &[&str] {
&["meshlet_mesh"]
Ok(())
}
}

impl AssetSaver for MeshletMeshSaverLoad {
impl AssetLoader for MeshletMeshSaverLoader {
type Asset = MeshletMesh;
type Settings = ();
type OutputLoader = Self;
type Error = MeshletMeshSaveOrLoadError;

async fn save<'a>(
async fn load<'a>(
&'a self,
writer: &'a mut Writer,
asset: SavedAsset<'a, Self::Asset>,
_settings: &'a Self::Settings,
) -> Result<(), Self::Error> {
writer
.write_all(&MESHLET_MESH_ASSET_VERSION.to_le_bytes())
.await?;
reader: &'a mut dyn Reader,
_settings: &'a (),
_load_context: &'a mut LoadContext<'_>,
) -> Result<MeshletMesh, MeshletMeshSaveOrLoadError> {
// Load and check magic number
let magic = async_read_u64(reader).await?;
if magic != MESHLET_MESH_ASSET_MAGIC {
return Err(MeshletMeshSaveOrLoadError::WrongFileType);
}

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?;
// Load and check asset version
let version = async_read_u64(reader).await?;
if version != MESHLET_MESH_ASSET_VERSION {
return Err(MeshletMeshSaveOrLoadError::WrongVersion { found: version });
}

Ok(())
// Load and decompress asset data
let worst_case_meshlet_triangles = async_read_u64(reader).await?;
let reader = &mut FrameDecoder::new(AsyncReadSyncAdapter(reader));
let vertex_data = read_slice(reader)?;
let vertex_ids = read_slice(reader)?;
let indices = read_slice(reader)?;
let meshlets = read_slice(reader)?;
let bounding_spheres = read_slice(reader)?;

Ok(MeshletMesh {
worst_case_meshlet_triangles,
vertex_data,
vertex_ids,
indices,
meshlets,
bounding_spheres,
})
}

fn extensions(&self) -> &[&str] {
&["meshlet_mesh"]
}
}

#[derive(thiserror::Error, Debug)]
pub enum MeshletMeshSaveOrLoadError {
#[error("file was not a MeshletMesh asset")]
WrongFileType,
#[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 Reader) -> Result<u64, bincode::Error> {
async fn async_read_u64(reader: &mut dyn Reader) -> Result<u64, std::io::Error> {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes).await?;
Ok(u64::from_le_bytes(bytes))
}

fn read_u64(reader: &mut dyn Read) -> Result<u64, std::io::Error> {
let mut bytes = [0u8; 8];
reader.read_exact(&mut bytes)?;
Ok(u64::from_le_bytes(bytes))
}

fn write_slice<T: Pod>(
field: &[T],
writer: &mut dyn Write,
) -> Result<(), MeshletMeshSaveOrLoadError> {
writer.write_all(&(field.len() as u64).to_le_bytes())?;
writer.write_all(bytemuck::cast_slice(field))?;
Ok(())
}

fn read_slice<T: Pod>(reader: &mut dyn Read) -> Result<Arc<[T]>, std::io::Error> {
let len = read_u64(reader)? as usize;

let mut data: Arc<[T]> = std::iter::repeat_with(T::zeroed).take(len).collect();
let slice = Arc::get_mut(&mut data).unwrap();
reader.read_exact(bytemuck::cast_slice_mut(slice))?;

Ok(data)
}

// TODO: Use async for everything and get rid of this adapter
struct AsyncWriteSyncAdapter<'a>(&'a mut Writer);

impl Write for AsyncWriteSyncAdapter<'_> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
block_on(self.0.write(buf))
}

fn flush(&mut self) -> std::io::Result<()> {
block_on(self.0.flush())
}
}

// TODO: Use async for everything and get rid of this adapter
struct AsyncReadSyncAdapter<'a>(&'a mut dyn Reader);

impl Read for AsyncReadSyncAdapter<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
block_on(self.0.read(buf))
}
}
1 change: 0 additions & 1 deletion crates/bevy_pbr/src/meshlet/from_mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,6 @@ fn simplify_meshlet_groups(
let target_error = target_error_relative * mesh_scale;

// Simplify the group to ~50% triangle count
// TODO: Use simplify_with_locks()
let mut error = 0.0;
let simplified_group_indices = simplify(
&group_indices,
Expand Down
7 changes: 5 additions & 2 deletions crates/bevy_pbr/src/meshlet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ pub(crate) use self::{
},
};

pub use self::asset::*;
pub use self::asset::{MeshletMesh, MeshletMeshSaverLoader};
#[cfg(feature = "meshlet_processor")]
pub use self::from_mesh::MeshToMeshletMeshConversionError;

Expand Down Expand Up @@ -118,6 +118,9 @@ pub struct MeshletPlugin;

impl Plugin for MeshletPlugin {
fn build(&self, app: &mut App) {
#[cfg(target_endian = "big")]
compile_error!("MeshletPlugin is only supported on little-endian processors.");

load_internal_asset!(
app,
MESHLET_BINDINGS_SHADER_HANDLE,
Expand Down Expand Up @@ -168,7 +171,7 @@ impl Plugin for MeshletPlugin {
);

app.init_asset::<MeshletMesh>()
.register_asset_loader(MeshletMeshSaverLoad)
.register_asset_loader(MeshletMeshSaverLoader)
.insert_resource(Msaa::Off)
.add_systems(
PostUpdate,
Expand Down
3 changes: 2 additions & 1 deletion examples/3d/meshlet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ use bevy::{
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";
const ASSET_URL: &str =
"https://raw.githubusercontent.com/JMS55/bevy_meshlet_asset/b6c712cfc87c65de419f856845401aba336a7bcd/bunny.meshlet_mesh";

fn main() -> ExitCode {
if !Path::new("./assets/models/bunny.meshlet_mesh").exists() {
Expand Down