Skip to content

Mesh serialization #19545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions crates/bevy_asset/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,23 @@ pub enum Handle<A: Asset> {
Weak(AssetId<A>),
}

/// Serialize `Handle`s as [`AssetPath`]s. This means types containing handles cannot be
/// deserialized without special handling. See
/// [`ReflectDeserializerProcessor`](bevy_reflect::serde::ReflectDeserializerProcessor).
impl<A: Asset> serde::Serialize for Handle<A> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self.path() {
Some(asset_path) => asset_path.serialize(serializer),
None => Err(serde::ser::Error::custom(
"only handles with asset paths can be serialized",
)),
}
}
}

impl<T: Asset> Clone for Handle<T> {
fn clone(&self) -> Self {
match self {
Expand Down
12 changes: 10 additions & 2 deletions crates/bevy_mesh/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,17 @@ repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[features]
serialize = ["dep:serde", "wgpu-types/serde"]

[dependencies]
# bevy
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" }
bevy_image = { path = "../bevy_image", version = "0.16.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
"wgpu-types",
] }
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.16.0-dev" }
Expand All @@ -28,11 +33,14 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.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", features = ["derive"], optional = true }
hexasphere = "15.0"
thiserror = { version = "2", default-features = false }
tracing = { version = "0.1", default-features = false, features = ["std"] }

[dev-dependencies]
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" }

[lints]
workspace = true

Expand Down
9 changes: 8 additions & 1 deletion crates/bevy_mesh/src/index.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use bevy_reflect::Reflect;
#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use core::iter;
use core::iter::FusedIterator;
use thiserror::Error;
Expand Down Expand Up @@ -70,7 +72,12 @@ pub enum MeshTrianglesError {
///
/// It describes the order in which the vertex attributes should be joined into faces.
#[derive(Debug, Clone, Reflect)]
#[reflect(Clone)]
#[reflect(Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum Indices {
U16(Vec<u16>),
U32(Vec<u32>),
Expand Down
37 changes: 35 additions & 2 deletions crates/bevy_mesh/src/mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use bevy_asset::{Asset, Handle, RenderAssetUsages};
use bevy_image::Image;
use bevy_math::{primitives::Triangle3d, *};
use bevy_reflect::Reflect;
#[cfg(feature = "serialize")]
use bevy_reflect::ReflectSerialize;
use bytemuck::cast_slice;
use thiserror::Error;
use tracing::warn;
Expand Down Expand Up @@ -104,16 +106,22 @@ 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.
///
/// ## Serialization
///
/// Note that, when serialization is enabled, a `Mesh` cannot be deserialized without special
/// handling of `Handle`s. See
/// [`ReflectDeserializerProcessor`](bevy_reflect::serde::ReflectDeserializerProcessor). The
/// serialized format should not be considered stable, and may not be compatible between versions.
#[derive(Asset, Debug, Clone, Reflect)]
#[reflect(Clone)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize), reflect(Serialize))]
pub struct Mesh {
#[reflect(ignore, clone)]
primitive_topology: PrimitiveTopology,
/// `std::collections::BTreeMap` with all defined vertex attributes (Positions, Normals, ...)
/// for this mesh. Attribute ids to attribute values.
/// Uses a [`BTreeMap`] because, unlike `HashMap`, it has a defined iteration order,
/// which allows easy stable `VertexBuffers` (i.e. same buffer order)
#[reflect(ignore, clone)]
attributes: BTreeMap<MeshVertexAttributeId, MeshAttributeData>,
indices: Option<Indices>,
morph_targets: Option<Handle<Image>>,
Expand Down Expand Up @@ -1551,4 +1559,29 @@ mod tests {
mesh.triangles().unwrap().collect::<Vec<Triangle3d>>()
);
}

#[test]
#[cfg(feature = "serialize")]
fn mesh_serialization() {
use bevy_app::{App, TaskPoolPlugin};
use bevy_asset::{ron, AssetApp, AssetPlugin, AssetServer};
use bevy_image::Image;

let mut app = App::new();

app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
.init_asset::<Image>();

let image = app
.world()
.resource::<AssetServer>()
.load("pixel/bevy_pixel_dark.png");

let mesh = Mesh::from(Triangle3d::default()).with_morph_targets(image);

assert_eq!(
ron::to_string(&mesh).unwrap(),
r#"(primitive_topology:r#triangle-list,attributes:{0:(attribute:(id:0,format:float32x3),values:Float32x3([(0.0,0.5,0.0),(-0.5,-0.5,0.0),(0.5,-0.5,0.0)])),1:(attribute:(id:1,format:float32x3),values:Float32x3([(0.0,0.0,1.0),(0.0,0.0,1.0),(0.0,0.0,1.0)])),2:(attribute:(id:2,format:float32x2),values:Float32x2([(0.0,0.0),(1.0,0.0),(0.59999996,1.0)]))},indices:Some(U32([0,1,2])),morph_targets:Some("pixel/bevy_pixel_dark.png"),morph_target_names:None,asset_usage:("MAIN_WORLD | RENDER_WORLD"))"#
);
}
}
37 changes: 33 additions & 4 deletions crates/bevy_mesh/src/vertex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ use bevy_derive::EnumVariantMeta;
use bevy_ecs::resource::Resource;
use bevy_math::Vec3;
use bevy_platform::collections::HashSet;
use bevy_reflect::Reflect;
#[cfg(feature = "serialize")]
use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
use bytemuck::cast_slice;
use core::hash::{Hash, Hasher};
use thiserror::Error;
use wgpu_types::{BufferAddress, VertexAttribute, VertexFormat, VertexStepMode};

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, Reflect)]
#[reflect(Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct MeshVertexAttribute {
/// The friendly name of the vertex attribute
#[cfg_attr(feature = "serialize", serde(skip))]
pub name: &'static str,

/// The _unique_ id of the vertex attribute. This will also determine sort ordering
Expand All @@ -36,7 +46,14 @@ impl MeshVertexAttribute {
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash, Reflect)]
#[reflect(Debug, Clone, PartialEq, Hash)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize),
serde(transparent)
)]
pub struct MeshVertexAttributeId(u64);

impl From<MeshVertexAttribute> for MeshVertexAttributeId {
Expand Down Expand Up @@ -132,7 +149,13 @@ impl VertexAttributeDescriptor {
}
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Reflect)]
#[reflect(Debug, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub(crate) struct MeshAttributeData {
pub(crate) attribute: MeshVertexAttribute,
pub(crate) values: VertexAttributeValues,
Expand Down Expand Up @@ -167,7 +190,13 @@ 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, Reflect)]
#[reflect(Clone, Debug)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub enum VertexAttributeValues {
Float32(Vec<f32>),
Sint32(Vec<i32>),
Expand Down
22 changes: 21 additions & 1 deletion crates/bevy_reflect/src/impls/wgpu_types.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
use crate::{impl_reflect_opaque, ReflectDeserialize, ReflectSerialize};
use crate::{std_traits::ReflectDefault, ReflectDeserialize, ReflectSerialize};
use bevy_reflect_derive::impl_reflect_opaque;

impl_reflect_opaque!(::wgpu_types::PrimitiveTopology(
Clone,
Debug,
Default,
Hash,
PartialEq,
Deserialize,
Serialize,
));

impl_reflect_opaque!(::wgpu_types::TextureFormat(
Clone,
Expand All @@ -8,3 +19,12 @@ impl_reflect_opaque!(::wgpu_types::TextureFormat(
Deserialize,
Serialize,
));

impl_reflect_opaque!(::wgpu_types::VertexFormat(
Clone,
Debug,
Hash,
PartialEq,
Deserialize,
Serialize,
));