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 <email@atlasdostal.com>
This commit is contained in:
Jan Hohenheim 2025-06-24 02:32:34 +02:00 committed by GitHub
parent f3d94f3958
commit 7aed3e411d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 252 additions and 6 deletions

View File

@ -100,6 +100,7 @@ serialize = [
"bevy_window?/serialize",
"bevy_winit?/serialize",
"bevy_platform/serialize",
"bevy_render/serialize",
]
multi_threaded = [
"std",

View File

@ -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

View File

@ -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<u16>),
U32(Vec<u32>),

View File

@ -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<Mesh> 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<Indices>,
}
#[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<Box<str>, 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::<Vec<Triangle3d>>()
);
}
#[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);
}
}

View File

@ -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<Box<str>, MeshVertexAttribute>,
) -> Option<MeshVertexAttribute> {
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<MeshVertexAttribute> 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<Box<str>, MeshVertexAttribute>,
) -> Option<MeshAttributeData> {
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<f32>),
Sint32(Vec<i32>),

View File

@ -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

View File

@ -15,6 +15,7 @@ serialize = [
"uuid/serde",
"bevy_ecs/serialize",
"bevy_platform/serialize",
"bevy_render?/serialize",
]
[dependencies]

View File

@ -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 = []