From 7aed3e411d27e6313d93c34f49cdf54cf1bd49ac Mon Sep 17 00:00:00 2001 From: Jan Hohenheim Date: Tue, 24 Jun 2025 02:32:34 +0200 Subject: [PATCH] Implement serializable mesh (#19743) # Objective - Alternative to and closes #19545 - Resolves #9790 by providing an alternative - `Mesh` is meant as format optimized for the renderer. There are no guarantees about how it looks, and breaking changes are expected - This makes it not feasible to implement `Reflect` for all its fields or `Serialize` it. - However, (de)serializing a mesh has an important use case: send a mesh over BRP to another process, like an editor! - In my case, I'm making a navmesh editor and need to copy the level that is running in the game into the editor process - Assets don't solve this because - They don't work over BRP #19709 and - The meshes may be procedural - So, we need a way to (de)serialize a mesh for short-term transmissions. ## Solution - Like `SerializedAnimationGraph` before, let's make a `SerializedMesh`! - This type's fields are all `private` because we want to keep the internals of `Mesh` hidden, and exposing them through this secondary struct would be counter-productive to that - All this struct can do is be serialized, be deserialized, and be converted to and from a mesh - It's not a lossless transmission: the handle for morph targets is ignored, and things like the render usages make no sense to be transmitted imo ## Future Work The same song and dance needs to happen for `Image`, but I can live with completely white meshes for the moment lol ## Testing - Added a simple test --------- Co-authored-by: atlv --- crates/bevy_internal/Cargo.toml | 1 + crates/bevy_mesh/Cargo.toml | 12 ++- crates/bevy_mesh/src/index.rs | 5 +- crates/bevy_mesh/src/mesh.rs | 163 +++++++++++++++++++++++++++++++- crates/bevy_mesh/src/vertex.rs | 73 +++++++++++++- crates/bevy_render/Cargo.toml | 2 + crates/bevy_scene/Cargo.toml | 1 + crates/bevy_ui/Cargo.toml | 1 + 8 files changed, 252 insertions(+), 6 deletions(-) diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 4692fe9d15..35c37077c6 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -100,6 +100,7 @@ serialize = [ "bevy_window?/serialize", "bevy_winit?/serialize", "bevy_platform/serialize", + "bevy_render/serialize", ] multi_threaded = [ "std", diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index c65c648cfd..0b77bdb619 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -28,11 +28,21 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5" } wgpu-types = { version = "24", default-features = false } -serde = { version = "1", features = ["derive"] } +serde = { version = "1", default-features = false, features = [ + "derive", +], optional = true } hexasphere = "15.0" thiserror = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } +[dev-dependencies] +serde_json = "1.0.140" + +[features] +default = [] +## Adds serialization support through `serde`. +serialize = ["dep:serde", "wgpu-types/serde"] + [lints] workspace = true diff --git a/crates/bevy_mesh/src/index.rs b/crates/bevy_mesh/src/index.rs index ca84d63bfb..87d81cc3f2 100644 --- a/crates/bevy_mesh/src/index.rs +++ b/crates/bevy_mesh/src/index.rs @@ -1,6 +1,8 @@ use bevy_reflect::Reflect; use core::iter; use core::iter::FusedIterator; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::IndexFormat; @@ -69,8 +71,9 @@ pub enum MeshTrianglesError { /// An array of indices into the [`VertexAttributeValues`](super::VertexAttributeValues) for a mesh. /// /// It describes the order in which the vertex attributes should be joined into faces. -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, PartialEq)] #[reflect(Clone)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub enum Indices { U16(Vec), U32(Vec), diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 893c84ecc5..3492788c4b 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -7,12 +7,18 @@ use super::{ MeshVertexAttributeId, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, }; +#[cfg(feature = "serialize")] +use crate::SerializedMeshAttributeData; use alloc::collections::BTreeMap; use bevy_asset::{Asset, Handle, RenderAssetUsages}; use bevy_image::Image; use bevy_math::{primitives::Triangle3d, *}; +#[cfg(feature = "serialize")] +use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; use bytemuck::cast_slice; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::warn; use wgpu_types::{VertexAttribute, VertexFormat, VertexStepMode}; @@ -104,7 +110,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// - Vertex winding order: by default, `StandardMaterial.cull_mode` is `Some(Face::Back)`, /// which means that Bevy would *only* render the "front" of each triangle, which /// is the side of the triangle from where the vertices appear in a *counter-clockwise* order. -#[derive(Asset, Debug, Clone, Reflect)] +#[derive(Asset, Debug, Clone, Reflect, PartialEq)] #[reflect(Clone)] pub struct Mesh { #[reflect(ignore, clone)] @@ -207,6 +213,10 @@ impl Mesh { pub const ATTRIBUTE_JOINT_INDEX: MeshVertexAttribute = MeshVertexAttribute::new("Vertex_JointIndex", 7, VertexFormat::Uint16x4); + /// The first index that can be used for custom vertex attributes. + /// Only the attributes with an index below this are used by Bevy. + pub const FIRST_AVAILABLE_CUSTOM_ATTRIBUTE: u64 = 8; + /// Construct a new mesh. You need to provide a [`PrimitiveTopology`] so that the /// renderer knows how to treat the vertex data. Most of the time this will be /// [`PrimitiveTopology::TriangleList`]. @@ -1252,6 +1262,133 @@ impl core::ops::Mul for Transform { } } +/// A version of [`Mesh`] suitable for serializing for short-term transfer. +/// +/// [`Mesh`] does not implement [`Serialize`] / [`Deserialize`] because it is made with the renderer in mind. +/// It is not a general-purpose mesh implementation, and its internals are subject to frequent change. +/// As such, storing a [`Mesh`] on disk is highly discouraged. +/// +/// But there are still some valid use cases for serializing a [`Mesh`], namely transferring meshes between processes. +/// To support this, you can create a [`SerializedMesh`] from a [`Mesh`] with [`SerializedMesh::from_mesh`], +/// and then deserialize it with [`SerializedMesh::deserialize`]. The caveats are: +/// - The mesh representation is not valid across different versions of Bevy. +/// - This conversion is lossy. Only the following information is preserved: +/// - Primitive topology +/// - Vertex attributes +/// - Indices +/// - Custom attributes that were not specified with [`MeshDeserializer::add_custom_vertex_attribute`] will be ignored while deserializing. +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializedMesh { + primitive_topology: PrimitiveTopology, + attributes: Vec<(MeshVertexAttributeId, SerializedMeshAttributeData)>, + indices: Option, +} + +#[cfg(feature = "serialize")] +impl SerializedMesh { + /// Create a [`SerializedMesh`] from a [`Mesh`]. See the documentation for [`SerializedMesh`] for caveats. + pub fn from_mesh(mesh: Mesh) -> Self { + Self { + primitive_topology: mesh.primitive_topology, + attributes: mesh + .attributes + .into_iter() + .map(|(id, data)| { + ( + id, + SerializedMeshAttributeData::from_mesh_attribute_data(data), + ) + }) + .collect(), + indices: mesh.indices, + } + } + + /// Create a [`Mesh`] from a [`SerializedMesh`]. See the documentation for [`SerializedMesh`] for caveats. + /// + /// Use [`MeshDeserializer`] if you need to pass extra options to the deserialization process, such as specifying custom vertex attributes. + pub fn into_mesh(self) -> Mesh { + MeshDeserializer::default().deserialize(self) + } +} + +/// Use to specify extra options when deserializing a [`SerializedMesh`] into a [`Mesh`]. +#[cfg(feature = "serialize")] +pub struct MeshDeserializer { + custom_vertex_attributes: HashMap, MeshVertexAttribute>, +} + +#[cfg(feature = "serialize")] +impl Default for MeshDeserializer { + fn default() -> Self { + // Written like this so that the compiler can validate that we use all the built-in attributes. + // If you just added a new attribute and got a compile error, please add it to this list :) + const BUILTINS: [MeshVertexAttribute; Mesh::FIRST_AVAILABLE_CUSTOM_ATTRIBUTE as usize] = [ + Mesh::ATTRIBUTE_POSITION, + Mesh::ATTRIBUTE_NORMAL, + Mesh::ATTRIBUTE_UV_0, + Mesh::ATTRIBUTE_UV_1, + Mesh::ATTRIBUTE_TANGENT, + Mesh::ATTRIBUTE_COLOR, + Mesh::ATTRIBUTE_JOINT_WEIGHT, + Mesh::ATTRIBUTE_JOINT_INDEX, + ]; + Self { + custom_vertex_attributes: BUILTINS + .into_iter() + .map(|attribute| (attribute.name.into(), attribute)) + .collect(), + } + } +} + +#[cfg(feature = "serialize")] +impl MeshDeserializer { + /// Create a new [`MeshDeserializer`]. + pub fn new() -> Self { + Self::default() + } + + /// Register a custom vertex attribute to the deserializer. Custom vertex attributes that were not added with this method will be ignored while deserializing. + pub fn add_custom_vertex_attribute( + &mut self, + name: &str, + attribute: MeshVertexAttribute, + ) -> &mut Self { + self.custom_vertex_attributes.insert(name.into(), attribute); + self + } + + /// Deserialize a [`SerializedMesh`] into a [`Mesh`]. + /// + /// See the documentation for [`SerializedMesh`] for caveats. + pub fn deserialize(&self, serialized_mesh: SerializedMesh) -> Mesh { + Mesh { + attributes: + serialized_mesh + .attributes + .into_iter() + .filter_map(|(id, data)| { + let attribute = data.attribute.clone(); + let Some(data) = + data.try_into_mesh_attribute_data(&self.custom_vertex_attributes) + else { + warn!( + "Deserialized mesh contains custom vertex attribute {attribute:?} that \ + was not specified with `MeshDeserializer::add_custom_vertex_attribute`. Ignoring." + ); + return None; + }; + Some((id, data)) + }) + .collect(), + indices: serialized_mesh.indices, + ..Mesh::new(serialized_mesh.primitive_topology, RenderAssetUsages::default()) + } + } +} + /// Error that can occur when calling [`Mesh::merge`]. #[derive(Error, Debug, Clone)] #[error("Incompatible vertex attribute types {} and {}", self_attribute.name, other_attribute.map(|a| a.name).unwrap_or("None"))] @@ -1263,6 +1400,8 @@ pub struct MergeMeshError { #[cfg(test)] mod tests { use super::Mesh; + #[cfg(feature = "serialize")] + use super::SerializedMesh; use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; use crate::PrimitiveTopology; use bevy_asset::RenderAssetUsages; @@ -1567,4 +1706,26 @@ mod tests { mesh.triangles().unwrap().collect::>() ); } + + #[cfg(feature = "serialize")] + #[test] + fn serialize_deserialize_mesh() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + + mesh.insert_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![[0., 0., 0.], [2., 0., 0.], [0., 1., 0.], [0., 0., 1.]], + ); + mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3])); + + let serialized_mesh = SerializedMesh::from_mesh(mesh.clone()); + let serialized_string = serde_json::to_string(&serialized_mesh).unwrap(); + let serialized_mesh_from_string: SerializedMesh = + serde_json::from_str(&serialized_string).unwrap(); + let deserialized_mesh = serialized_mesh_from_string.into_mesh(); + assert_eq!(mesh, deserialized_mesh); + } } diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 949e355b4c..fd683ef60d 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -2,13 +2,17 @@ use alloc::sync::Arc; use bevy_derive::EnumVariantMeta; use bevy_ecs::resource::Resource; use bevy_math::Vec3; +#[cfg(feature = "serialize")] +use bevy_platform::collections::HashMap; use bevy_platform::collections::HashSet; use bytemuck::cast_slice; use core::hash::{Hash, Hasher}; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::{BufferAddress, VertexAttribute, VertexFormat, VertexStepMode}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct MeshVertexAttribute { /// The friendly name of the vertex attribute pub name: &'static str, @@ -22,6 +26,37 @@ pub struct MeshVertexAttribute { pub format: VertexFormat, } +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct SerializedMeshVertexAttribute { + pub(crate) name: String, + pub(crate) id: MeshVertexAttributeId, + pub(crate) format: VertexFormat, +} + +#[cfg(feature = "serialize")] +impl SerializedMeshVertexAttribute { + pub(crate) fn from_mesh_vertex_attribute(attribute: MeshVertexAttribute) -> Self { + Self { + name: attribute.name.to_string(), + id: attribute.id, + format: attribute.format, + } + } + + pub(crate) fn try_into_mesh_vertex_attribute( + self, + possible_attributes: &HashMap, MeshVertexAttribute>, + ) -> Option { + let attr = possible_attributes.get(self.name.as_str())?; + if attr.id == self.id { + Some(*attr) + } else { + None + } + } +} + impl MeshVertexAttribute { pub const fn new(name: &'static str, id: u64, format: VertexFormat) -> Self { Self { @@ -37,6 +72,7 @@ impl MeshVertexAttribute { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct MeshVertexAttributeId(u64); impl From for MeshVertexAttributeId { @@ -132,12 +168,42 @@ impl VertexAttributeDescriptor { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct MeshAttributeData { pub(crate) attribute: MeshVertexAttribute, pub(crate) values: VertexAttributeValues, } +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct SerializedMeshAttributeData { + pub(crate) attribute: SerializedMeshVertexAttribute, + pub(crate) values: VertexAttributeValues, +} + +#[cfg(feature = "serialize")] +impl SerializedMeshAttributeData { + pub(crate) fn from_mesh_attribute_data(data: MeshAttributeData) -> Self { + Self { + attribute: SerializedMeshVertexAttribute::from_mesh_vertex_attribute(data.attribute), + values: data.values, + } + } + + pub(crate) fn try_into_mesh_attribute_data( + self, + possible_attributes: &HashMap, MeshVertexAttribute>, + ) -> Option { + let attribute = self + .attribute + .try_into_mesh_vertex_attribute(possible_attributes)?; + Some(MeshAttributeData { + attribute, + values: self.values, + }) + } +} + /// Compute a vector whose direction is the normal of the triangle formed by /// points a, b, c, and whose magnitude is double the area of the triangle. This /// is useful for computing smooth normals where the contributing normals are @@ -167,7 +233,8 @@ pub fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { /// Contains an array where each entry describes a property of a single vertex. /// Matches the [`VertexFormats`](VertexFormat). -#[derive(Clone, Debug, EnumVariantMeta)] +#[derive(Clone, Debug, EnumVariantMeta, PartialEq)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub enum VertexAttributeValues { Float32(Vec), Sint32(Vec), diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 143a1f0270..b6186267a2 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -46,6 +46,8 @@ ci_limits = [] webgl = ["wgpu/webgl"] webgpu = ["wgpu/webgpu"] detailed_trace = [] +## Adds serialization support through `serde`. +serialize = ["bevy_mesh/serialize"] [dependencies] # bevy diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 78de17e26f..919a10620c 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -15,6 +15,7 @@ serialize = [ "uuid/serde", "bevy_ecs/serialize", "bevy_platform/serialize", + "bevy_render?/serialize", ] [dependencies] diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 1f05d7a53b..b944053698 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -51,6 +51,7 @@ serialize = [ "smallvec/serde", "bevy_math/serialize", "bevy_platform/serialize", + "bevy_render/serialize", ] bevy_ui_picking_backend = ["bevy_picking", "dep:uuid"] bevy_ui_debug = []