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:
parent
f3d94f3958
commit
7aed3e411d
@ -100,6 +100,7 @@ serialize = [
|
||||
"bevy_window?/serialize",
|
||||
"bevy_winit?/serialize",
|
||||
"bevy_platform/serialize",
|
||||
"bevy_render/serialize",
|
||||
]
|
||||
multi_threaded = [
|
||||
"std",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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>),
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>),
|
||||
|
@ -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
|
||||
|
@ -15,6 +15,7 @@ serialize = [
|
||||
"uuid/serde",
|
||||
"bevy_ecs/serialize",
|
||||
"bevy_platform/serialize",
|
||||
"bevy_render?/serialize",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
|
@ -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 = []
|
||||
|
Loading…
Reference in New Issue
Block a user