Skip to content

Commit 50dc772

Browse files
Implement Serialized Image (#20328)
# Objective Following #19743, we can now send meshes over BRP to an editor, yay! https://github.com/user-attachments/assets/84ce4fa7-c402-4738-9414-a446cd0472fe But oh no, the editor look a bit pale! No wonder; we cannot send `StandardMaterial` over the wire yet because `Image` is not serializable! ## Solution - Do the same as for `SerializedMesh` and `SerialzedAnimationGraph`: create a `SerializedImage` meant for transmission. - ~~I don't think making a `SerializedStandardMaterial` is necessary for now, that part is trivial to do by hand if `Image` is serializable.~~ - I take that back, it's about 500 LOC and a can of worms I don't want to open upstream. I'll leave serializing standard materials for userland for now :P ## Testing - Added a roundtrip serialization - Ported the navmesh editor: https://github.com/user-attachments/assets/1ccbad6e-5beb-4846-8e18-c55c85709481 ## Additional Info Added it to the milestone with Alice's consent :) --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent e08c78b commit 50dc772

File tree

5 files changed

+210
-2
lines changed

5 files changed

+210
-2
lines changed

crates/bevy_image/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ qoi = ["image/qoi"]
3232
tga = ["image/tga"]
3333
tiff = ["image/tiff"]
3434
webp = ["image/webp"]
35-
serialize = ["bevy_reflect", "bevy_platform/serialize"]
35+
serialize = ["bevy_reflect", "bevy_platform/serialize", "wgpu-types/serde"]
3636

3737
# For ktx2 supercompression
3838
zlib = ["flate2"]
@@ -89,6 +89,7 @@ half = { version = "2.4.1" }
8989

9090
[dev-dependencies]
9191
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
92+
serde_json = "1.0.140"
9293

9394
[lints]
9495
workspace = true

crates/bevy_image/src/image.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,13 @@ impl ToExtents for UVec3 {
359359
}
360360
}
361361

362-
#[derive(Asset, Debug, Clone)]
362+
/// An image, optimized for usage in rendering.
363+
///
364+
/// ## Remote Inspection
365+
///
366+
/// To transmit an [`Image`] between two running Bevy apps, e.g. through BRP, use [`SerializedImage`](crate::SerializedImage).
367+
/// This type is only meant for short-term transmission between same versions and should not be stored anywhere.
368+
#[derive(Asset, Debug, Clone, PartialEq)]
363369
#[cfg_attr(
364370
feature = "bevy_reflect",
365371
derive(Reflect),
@@ -377,9 +383,23 @@ pub struct Image {
377383
/// Use [`TextureDataOrder::default()`] for all other cases.
378384
pub data_order: TextureDataOrder,
379385
// TODO: this nesting makes accessing Image metadata verbose. Either flatten out descriptor or add accessors.
386+
/// Describes the data layout of the GPU texture.\
387+
/// For example, whether a texture contains 1D/2D/3D data, and what the format of the texture data is.
388+
///
389+
/// ## Field Usage Notes
390+
/// - [`TextureDescriptor::label`] is used for caching purposes when not using `Asset<Image>`.\
391+
/// If you use assets, the label is purely a debugging aid.
392+
/// - [`TextureDescriptor::view_formats`] is currently unused by Bevy.
380393
pub texture_descriptor: TextureDescriptor<Option<&'static str>, &'static [TextureFormat]>,
381394
/// The [`ImageSampler`] to use during rendering.
382395
pub sampler: ImageSampler,
396+
/// Describes how the GPU texture should be interpreted.\
397+
/// For example, 2D image data could be read as plain 2D, an array texture of layers of 2D with the same dimensions (and the number of layers in that case),
398+
/// a cube map, an array of cube maps, etc.
399+
///
400+
/// ## Field Usage Notes
401+
/// - [`TextureViewDescriptor::label`] is used for caching purposes when not using `Asset<Image>`.\
402+
/// If you use assets, the label is purely a debugging aid.
383403
pub texture_view_descriptor: Option<TextureViewDescriptor<Option<&'static str>>>,
384404
pub asset_usage: RenderAssetUsages,
385405
/// Whether this image should be copied on the GPU when resized.

crates/bevy_image/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ compile_error!(
1717

1818
mod image;
1919
pub use self::image::*;
20+
#[cfg(feature = "serialize")]
21+
mod serialized_image;
22+
#[cfg(feature = "serialize")]
23+
pub use self::serialized_image::*;
2024
#[cfg(feature = "basis-universal")]
2125
mod basis;
2226
#[cfg(feature = "compressed_image_saver")]
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
use crate::{Image, ImageSampler};
2+
use bevy_asset::RenderAssetUsages;
3+
use core::fmt::Debug;
4+
use serde::{Deserialize, Serialize};
5+
use wgpu_types::{
6+
TextureAspect, TextureDataOrder, TextureDescriptor, TextureFormat, TextureUsages,
7+
TextureViewDescriptor, TextureViewDimension,
8+
};
9+
10+
/// A version of [`Image`] suitable for serializing for short-term transfer.
11+
///
12+
/// [`Image`] does not implement [`Serialize`] / [`Deserialize`] because it is made with the renderer in mind.
13+
/// It is not a general-purpose image implementation, and its internals are subject to frequent change.
14+
/// As such, storing an [`Image`] on disk is highly discouraged.
15+
/// Use an existing image asset format such as `.png` instead!
16+
///
17+
/// But there are still some valid use cases for serializing an [`Image`], namely transferring images between processes.
18+
/// To support this, you can create a [`SerializedImage`] from an [`Image`] with [`SerializedImage::from_image`],
19+
/// and then deserialize it with [`SerializedImage::into_image`].
20+
///
21+
/// The caveats are:
22+
/// - The image representation is not valid across different versions of Bevy.
23+
/// - This conversion is lossy. The following information is not preserved:
24+
/// - texture descriptor and texture view descriptor labels
25+
/// - texture descriptor view formats
26+
#[derive(Debug, Clone, Serialize, Deserialize)]
27+
pub struct SerializedImage {
28+
data: Option<Vec<u8>>,
29+
data_order: SerializedTextureDataOrder,
30+
texture_descriptor: TextureDescriptor<(), ()>,
31+
sampler: ImageSampler,
32+
texture_view_descriptor: Option<SerializedTextureViewDescriptor>,
33+
}
34+
35+
#[derive(Debug, Clone, Serialize, Deserialize)]
36+
struct SerializedTextureViewDescriptor {
37+
format: Option<TextureFormat>,
38+
dimension: Option<TextureViewDimension>,
39+
usage: Option<TextureUsages>,
40+
aspect: TextureAspect,
41+
base_mip_level: u32,
42+
mip_level_count: Option<u32>,
43+
base_array_layer: u32,
44+
array_layer_count: Option<u32>,
45+
}
46+
47+
impl SerializedTextureViewDescriptor {
48+
fn from_texture_view_descriptor(
49+
descriptor: TextureViewDescriptor<Option<&'static str>>,
50+
) -> Self {
51+
Self {
52+
format: descriptor.format,
53+
dimension: descriptor.dimension,
54+
usage: descriptor.usage,
55+
aspect: descriptor.aspect,
56+
base_mip_level: descriptor.base_mip_level,
57+
mip_level_count: descriptor.mip_level_count,
58+
base_array_layer: descriptor.base_array_layer,
59+
array_layer_count: descriptor.array_layer_count,
60+
}
61+
}
62+
63+
fn into_texture_view_descriptor(self) -> TextureViewDescriptor<Option<&'static str>> {
64+
TextureViewDescriptor {
65+
// Not used for asset-based images other than debugging
66+
label: None,
67+
format: self.format,
68+
dimension: self.dimension,
69+
usage: self.usage,
70+
aspect: self.aspect,
71+
base_mip_level: self.base_mip_level,
72+
mip_level_count: self.mip_level_count,
73+
base_array_layer: self.base_array_layer,
74+
array_layer_count: self.array_layer_count,
75+
}
76+
}
77+
}
78+
79+
#[derive(Debug, Clone, Serialize, Deserialize)]
80+
enum SerializedTextureDataOrder {
81+
LayerMajor,
82+
MipMajor,
83+
}
84+
85+
impl SerializedTextureDataOrder {
86+
fn from_texture_data_order(order: TextureDataOrder) -> Self {
87+
match order {
88+
TextureDataOrder::LayerMajor => SerializedTextureDataOrder::LayerMajor,
89+
TextureDataOrder::MipMajor => SerializedTextureDataOrder::MipMajor,
90+
}
91+
}
92+
93+
fn into_texture_data_order(self) -> TextureDataOrder {
94+
match self {
95+
SerializedTextureDataOrder::LayerMajor => TextureDataOrder::LayerMajor,
96+
SerializedTextureDataOrder::MipMajor => TextureDataOrder::MipMajor,
97+
}
98+
}
99+
}
100+
101+
impl SerializedImage {
102+
/// Creates a new [`SerializedImage`] from an [`Image`].
103+
pub fn from_image(image: Image) -> Self {
104+
Self {
105+
data: image.data,
106+
data_order: SerializedTextureDataOrder::from_texture_data_order(image.data_order),
107+
texture_descriptor: TextureDescriptor {
108+
label: (),
109+
size: image.texture_descriptor.size,
110+
mip_level_count: image.texture_descriptor.mip_level_count,
111+
sample_count: image.texture_descriptor.sample_count,
112+
dimension: image.texture_descriptor.dimension,
113+
format: image.texture_descriptor.format,
114+
usage: image.texture_descriptor.usage,
115+
view_formats: (),
116+
},
117+
sampler: image.sampler,
118+
texture_view_descriptor: image.texture_view_descriptor.map(|descriptor| {
119+
SerializedTextureViewDescriptor::from_texture_view_descriptor(descriptor)
120+
}),
121+
}
122+
}
123+
124+
/// Create an [`Image`] from a [`SerializedImage`].
125+
pub fn into_image(self) -> Image {
126+
Image {
127+
data: self.data,
128+
data_order: self.data_order.into_texture_data_order(),
129+
texture_descriptor: TextureDescriptor {
130+
// Not used for asset-based images other than debugging
131+
label: None,
132+
size: self.texture_descriptor.size,
133+
mip_level_count: self.texture_descriptor.mip_level_count,
134+
sample_count: self.texture_descriptor.sample_count,
135+
dimension: self.texture_descriptor.dimension,
136+
format: self.texture_descriptor.format,
137+
usage: self.texture_descriptor.usage,
138+
// Not used for asset-based images
139+
view_formats: &[],
140+
},
141+
sampler: self.sampler,
142+
texture_view_descriptor: self
143+
.texture_view_descriptor
144+
.map(SerializedTextureViewDescriptor::into_texture_view_descriptor),
145+
asset_usage: RenderAssetUsages::RENDER_WORLD,
146+
copy_on_resize: false,
147+
}
148+
}
149+
}
150+
151+
#[cfg(test)]
152+
mod tests {
153+
use wgpu_types::{Extent3d, TextureDimension};
154+
155+
use super::*;
156+
157+
#[test]
158+
fn serialize_deserialize_image() {
159+
let image = Image::new(
160+
Extent3d {
161+
width: 3,
162+
height: 1,
163+
depth_or_array_layers: 1,
164+
},
165+
TextureDimension::D2,
166+
vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255],
167+
TextureFormat::Rgba8UnormSrgb,
168+
RenderAssetUsages::RENDER_WORLD,
169+
);
170+
171+
let serialized_image = SerializedImage::from_image(image.clone());
172+
let serialized_string = serde_json::to_string(&serialized_image).unwrap();
173+
let serialized_image_from_string: SerializedImage =
174+
serde_json::from_str(&serialized_string).unwrap();
175+
let deserialized_image = serialized_image_from_string.into_image();
176+
assert_eq!(image, deserialized_image);
177+
}
178+
}

crates/bevy_mesh/src/mesh.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10;
110110
/// - Vertex winding order: by default, `StandardMaterial.cull_mode` is `Some(Face::Back)`,
111111
/// which means that Bevy would *only* render the "front" of each triangle, which
112112
/// is the side of the triangle from where the vertices appear in a *counter-clockwise* order.
113+
///
114+
/// ## Remote Inspection
115+
///
116+
/// To transmit a [`Mesh`] between two running Bevy apps, e.g. through BRP, use [`SerializedMesh`].
117+
/// This type is only meant for short-term transmission between same versions and should not be stored anywhere.
113118
#[derive(Asset, Debug, Clone, Reflect, PartialEq)]
114119
#[reflect(Clone)]
115120
pub struct Mesh {

0 commit comments

Comments
 (0)