From 3376c3f1b7601fb64dcf367cfa00bf7264d77352 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 9 Jun 2026 20:58:51 +0800 Subject: [PATCH 1/9] Refactor mesh compression methods --- crates/bevy_gltf/src/lib.rs | 15 +- crates/bevy_gltf/src/loader/mod.rs | 25 +- crates/bevy_mesh/Cargo.toml | 2 +- crates/bevy_mesh/src/mesh.rs | 844 ++++++++++-------- crates/bevy_mesh/src/vertex.rs | 167 ++-- crates/bevy_render/src/mesh/mod.rs | 11 +- crates/bevy_render/src/utils.wgsl | 4 +- .../bevy_sprite_render/src/sprite_mesh/mod.rs | 21 +- examples/stress_tests/many_cubes.rs | 8 +- examples/stress_tests/many_foxes.rs | 9 +- examples/stress_tests/many_morph_targets.rs | 9 +- 11 files changed, 638 insertions(+), 477 deletions(-) diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index ead576cb1992d..211fcb47e190c 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -148,7 +148,7 @@ use bevy_app::prelude::*; use bevy_asset::AssetApp; use bevy_ecs::prelude::Resource; use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageSamplerDescriptor}; -use bevy_mesh::{MeshAttributeCompressionFlags, MeshVertexAttribute}; +use bevy_mesh::{MeshCompressionArgs, MeshVertexAttribute}; /// The glTF prelude. /// @@ -235,11 +235,8 @@ pub struct GltfPlugin { /// [`GltfLoaderSettings::skinned_mesh_bounds_policy`]. pub skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy, - /// Mesh attribute compression flags for the loaded meshes. - pub mesh_attribute_compression: MeshAttributeCompressionFlags, - - /// Whether to convert mesh indices to u16 if vertex count <= 65535 and indices are u32. - pub mesh_index_compression: bool, + /// Mesh attribute compression arguments applied when loading meshes. + pub mesh_compression: MeshCompressionArgs, } impl Default for GltfPlugin { @@ -249,8 +246,7 @@ impl Default for GltfPlugin { custom_vertex_attributes: HashMap::default(), convert_coordinates: GltfConvertCoordinates::default(), skinned_mesh_bounds_policy: Default::default(), - mesh_attribute_compression: MeshAttributeCompressionFlags::empty(), - mesh_index_compression: false, + mesh_compression: MeshCompressionArgs::none(), } } } @@ -307,8 +303,7 @@ impl Plugin for GltfPlugin { default_convert_coordinates: self.convert_coordinates, extensions: extensions.0.clone(), default_skinned_mesh_bounds_policy: self.skinned_mesh_bounds_policy, - default_mesh_attribute_compression: self.mesh_attribute_compression, - default_mesh_index_compression: self.mesh_index_compression, + default_mesh_compression: self.mesh_compression, }); } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 5781bfbf82d08..8e874d7c68db9 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -32,7 +32,7 @@ use bevy_mesh::UvChannel; use bevy_mesh::{ morph::{MeshMorphWeights, MorphAttributes, MorphWeights}, skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, Mesh, Mesh3d, MeshAttributeCompressionFlags, MeshVertexAttribute, PrimitiveTopology, + Indices, Mesh, Mesh3d, MeshCompressionArgs, MeshVertexAttribute, PrimitiveTopology, }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::TypePath; @@ -162,10 +162,8 @@ pub struct GltfLoader { /// The default policy for skinned mesh bounds. Can be overridden by /// [`GltfLoaderSettings::skinned_mesh_bounds_policy`]. pub default_skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy, - /// Default Mesh attribute compression flags for the loaded meshes. - pub default_mesh_attribute_compression: MeshAttributeCompressionFlags, - /// Whether to convert mesh indices to u16 if vertex count <= 65535 and indices are u32. - pub default_mesh_index_compression: bool, + /// Default Mesh compression arguments for the loaded meshes. + pub default_mesh_compression: MeshCompressionArgs, } /// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of @@ -220,11 +218,8 @@ pub struct GltfLoaderSettings { /// Optionally overrides [`GltfPlugin::skinned_mesh_bounds_policy`](crate::GltfPlugin). pub skinned_mesh_bounds_policy: Option, /// Mesh attribute compression flags for the loaded meshes. - /// If `None`, uses the global default set by [`GltfPlugin::mesh_attribute_compression`](crate::GltfPlugin::mesh_attribute_compression). - pub mesh_attribute_compression: Option, - /// Whether to convert mesh indices to u16 if vertex count <= 65535 and indices are u32. - /// If `None`, uses the global default set by [`GltfPlugin::mesh_index_compression`](crate::GltfPlugin::mesh_index_compression). - pub mesh_index_compression: Option, + /// If `None`, uses the global default set by [`GltfPlugin::mesh_compression`](crate::GltfPlugin::mesh_compression). + pub mesh_compression: Option, } impl Default for GltfLoaderSettings { @@ -241,8 +236,7 @@ impl Default for GltfLoaderSettings { validate: true, convert_coordinates: None, skinned_mesh_bounds_policy: None, - mesh_attribute_compression: None, - mesh_index_compression: None, + mesh_compression: None, } } } @@ -878,11 +872,8 @@ impl GltfLoader { primitive_label.to_string(), mesh.compressed_mesh( settings - .mesh_attribute_compression - .unwrap_or(loader.default_mesh_attribute_compression), - settings - .mesh_index_compression - .unwrap_or(loader.default_mesh_index_compression), + .mesh_compression + .unwrap_or(loader.default_mesh_compression), ), ); primitives.push(super::GltfPrimitive::new( diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index c226c44174d17..b6e54082e7573 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -26,7 +26,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.19.0-dev", default-fea # other bitflags = { version = "2.3", features = ["serde"] } -bytemuck = { version = "1.5" } +bytemuck = { version = "1.5", features = ["must_cast"] } wgpu-types = { version = "29.0.3", default-features = false } serde = { version = "1", default-features = false, features = [ "derive", diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 1f14a8e1a25b3..f18dac75b8f16 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -8,9 +8,9 @@ use super::{ MeshVertexBufferLayoutRef, MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, }; -use crate::arr_f32_to_unorm8; #[cfg(feature = "morph")] use crate::morph::MorphAttributes; +use crate::AttributeQuantization; #[cfg(feature = "serialize")] use crate::SerializedMeshAttributeData; use alloc::collections::BTreeMap; @@ -262,21 +262,70 @@ pub struct Mesh { /// Indicate whether vertex attributes are compressed. attribute_compression: MeshAttributeCompressionFlags, /// Precomputed min and max extents of the mesh position data. Used mainly for constructing `Aabb`s for frustum culling and decompressing vertex positions. - /// This data will be set if/when a mesh is extracted to the GPU or calling [`Mesh::compressed_mesh`]. + /// This data will be set if/when a mesh is extracted to the GPU or compressing positions. pub final_aabb: Option, /// Precomputed min and max extents of the mesh UV channels data. Used mainly for decompressing vertex UVs. - /// This will be set when calling [`Mesh::compressed_mesh`]. + /// This will be set when compressing UVs. pub final_uv_ranges: [Option; 2], skinned_mesh_bounds: Option, } +/// Quantization arguments of the mesh. +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub struct MeshAttributeQuantization { + pub color: Option, + pub joint_weight: Option, +} + +/// Mesh compression arguments used in [`Mesh::compressed_mesh`]. +#[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub struct MeshCompressionArgs { + /// Whether to compress indices to [`Indices::U16`] when possible. + pub compress_indices: bool, + /// Quantize Float32 to smaller types for some attributes. No change is needed in shaders. + pub quantize_attributes: MeshAttributeQuantization, + /// Compress some attributes to smaller representation. Shaders need change to decode them correctly. + /// See [`MeshAttributeCompressionFlags`] for the details. + pub compress_attributes: MeshAttributeCompressionFlags, +} + +impl MeshCompressionArgs { + /// None of compression/quantization is enabled. + pub const fn none() -> Self { + Self { + compress_indices: false, + quantize_attributes: MeshAttributeQuantization { + color: None, + joint_weight: None, + }, + compress_attributes: MeshAttributeCompressionFlags::empty(), + } + } + + /// All compression is enabled, with colors quantized to unorm8 and joint weights quantized to unorm16. + pub const fn all() -> Self { + Self { + compress_indices: true, + quantize_attributes: MeshAttributeQuantization { + // Unorm8 is chosen by default, as glTF vertex color is unorm8/unorm16 and HDR vertex color isn't common. + color: Some(AttributeQuantization::Unorm8), + joint_weight: Some(AttributeQuantization::Unorm16), + }, + compress_attributes: MeshAttributeCompressionFlags::all(), + } + } +} + bitflags::bitflags! { /// If the corresponding attribute compression is enabled: /// - Position will be Snorm16x4 relative to the mesh's AABB. The w component is unused. - /// - Normal and tangent will be Snorm16x2 with octahedral encoding, using [`octahedral_encode_signed`](crate::vertex::octahedral_encode_signed) and [`octahedral_encode_tangent`](crate::vertex::octahedral_encode_tangent). + /// - Normal and tangent will be Snorm16x2 with octahedral encoding, using [`octahedral_encode_signed`] and [`octahedral_encode_tangent`]. /// - UV0 and UV1 will be Unorm16x2. UVs are remapped based on their min/max values so them can go beyond [0, 1], though a larger range will reduce precision. - /// - Joint weight will be Unorm16x4. - /// - Color will be Float16x4 or Unorm8x4. + /// + /// [`octahedral_encode_signed`]: crate::vertex::octahedral_encode_signed + /// [`octahedral_encode_tangent`]: crate::vertex::octahedral_encode_tangent #[repr(transparent)] #[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] @@ -288,32 +337,8 @@ bitflags::bitflags! { const COMPRESS_TANGENT = 1 << 2; const COMPRESS_UV0 = 1 << 3; const COMPRESS_UV1 = 1 << 4; - const COMPRESS_JOINT_WEIGHT = 1 << 5; - - const COMPRESS_COLOR_RESERVED_BIT = Self::COMPRESS_COLOR_MASK_BIT << Self::COMPRESS_COLOR_SHIFT_BIT; - const COMPRESS_COLOR_UNORM8 = 1 << Self::COMPRESS_COLOR_SHIFT_BIT; - const COMPRESS_COLOR_FLOAT16 = 2 << Self::COMPRESS_COLOR_SHIFT_BIT; } } -impl MeshAttributeCompressionFlags { - const COMPRESS_COLOR_MASK_BIT: u8 = 0b11; - const COMPRESS_COLOR_SHIFT_BIT: u8 = - Self::COMPRESS_JOINT_WEIGHT.bits().trailing_zeros() as u8 + 1; - - /// Helper function to set color flag. - pub fn with_color(self, color_flag: Self) -> Self { - self & !Self::COMPRESS_COLOR_RESERVED_BIT | color_flag - } -} - -// Constants to work around const expression can't be used in match pattern. -const ATTRIBUTE_POSITION_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_POSITION.id; -const ATTRIBUTE_NORMAL_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_NORMAL.id; -const ATTRIBUTE_UV_0_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_UV_0.id; -const ATTRIBUTE_UV_1_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_UV_1.id; -const ATTRIBUTE_TANGENT_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_TANGENT.id; -const ATTRIBUTE_COLOR_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_COLOR.id; -const ATTRIBUTE_JOINT_WEIGHT_ID: MeshVertexAttributeId = Mesh::ATTRIBUTE_JOINT_WEIGHT.id; impl Mesh { /// Where the vertex is located in space. Use in conjunction with [`Mesh::insert_attribute`] @@ -1025,244 +1050,214 @@ impl Mesh { } } - /// Returns the compressed vertex format for the given attribute ID, or `None` if the compression flag is not set. - fn get_compressed_vertex_format( - &self, - attribute_id: MeshVertexAttributeId, - ) -> Option { - match attribute_id { - ATTRIBUTE_POSITION_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION) => - { - Some(VertexFormat::Snorm16x4) - } - ATTRIBUTE_NORMAL_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) => - { - Some(VertexFormat::Snorm16x2) - } - ATTRIBUTE_UV_0_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => - { - Some(VertexFormat::Unorm16x2) - } - ATTRIBUTE_UV_1_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => - { - Some(VertexFormat::Unorm16x2) - } - ATTRIBUTE_TANGENT_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) => - { - Some(VertexFormat::Snorm16x2) - } - ATTRIBUTE_COLOR_ID - if self - .attribute_compression - .intersects(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT) => - { - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16) - { - Some(VertexFormat::Float16x4) - } else if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) - { - Some(VertexFormat::Unorm8x4) - } else { - unreachable!("Color compression flag must be `COMPRESS_COLOR_FLOAT16` or `COMPRESS_COLOR_UNORM8`") - } - } - ATTRIBUTE_JOINT_WEIGHT_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_JOINT_WEIGHT) => - { - Some(VertexFormat::Unorm16x4) - } - _ => None, + /// Compress positions and apply [`MeshAttributeCompressionFlags::COMPRESS_POSITION`]. + /// See [`MeshAttributeCompressionFlags`] for the details. + pub fn compress_positions(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { + let mut attr = Mesh::ATTRIBUTE_POSITION; + let Some(values) = self.attribute(attr) else { + return Err(MeshVertexCompressionError::MissingAttribute(attr)); + }; + if values.is_empty() { + return Err(MeshVertexCompressionError::EmptyAttribute(attr)); } + let Some(aabb) = Self::compute_aabb(values) else { + return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { + attr, + expected: attr.format, + provided: values.into(), + }); + }; + attr.format = VertexFormat::Snorm16x4; + self.insert_attribute(attr, values.create_compressed_positions(aabb).unwrap()); + self.final_aabb = Some(aabb); + self.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_POSITION; + Ok(self) } - /// Create compressed attribute values for the given attribute ID and attribute values. Return `None` if the compression flag is not set or the attribute values can't be compressed. - /// - /// Panics when compressing positions but `aabb` is `None`, or when compressing UVs but corresponding `uv_ranges` is `None`. - fn create_compressed_attribute_values( - &self, - attribute_id: MeshVertexAttributeId, - attribute_values: &VertexAttributeValues, - aabb: Option, - uv_ranges: [Option; 2], - ) -> Option { - match attribute_id { - ATTRIBUTE_POSITION_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION) => - { - attribute_values.create_compressed_positions(aabb.unwrap()) - } - ATTRIBUTE_NORMAL_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) => - { - attribute_values.create_octahedral_encode_normals() - } - ATTRIBUTE_UV_0_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => - { - attribute_values.create_compressed_uvs(uv_ranges[0].unwrap()) - } - ATTRIBUTE_UV_1_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => - { - attribute_values.create_compressed_uvs(uv_ranges[1].unwrap()) - } - ATTRIBUTE_TANGENT_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) => - { - attribute_values.create_octahedral_encode_tangents() - } - ATTRIBUTE_COLOR_ID - if self - .attribute_compression - .intersects(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT) => - { - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16) - { - attribute_values.create_f16_values() - } else if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) - { - // Create Unorm8x4 color - let VertexAttributeValues::Float32x4(uncompressed_values) = attribute_values - else { - return None; - }; - let mut values = Vec::<[u8; 4]>::with_capacity(uncompressed_values.len()); - for val in uncompressed_values { - values.push(arr_f32_to_unorm8(*val)); - } - Some(VertexAttributeValues::Unorm8x4(values)) - } else { - unreachable!("Color compression flag must be `COMPRESS_COLOR_FLOAT16` or `COMPRESS_COLOR_UNORM8`") - } - } - ATTRIBUTE_JOINT_WEIGHT_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_JOINT_WEIGHT) => - { - attribute_values.create_unorm16_values() - } - _ => None, + fn compress_uvs( + &mut self, + mut attr: MeshVertexAttribute, + ok: impl Fn(&mut Mesh, Aabb2d), + ) -> Result<&mut Mesh, MeshVertexCompressionError> { + let Some(values) = self.attribute(attr) else { + return Err(MeshVertexCompressionError::MissingAttribute(attr)); + }; + if values.is_empty() { + return Err(MeshVertexCompressionError::EmptyAttribute(attr)); } + let Some(uv_range) = Self::compute_uv_range(values) else { + return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { + attr, + expected: attr.format, + provided: values.into(), + }); + }; + attr.format = VertexFormat::Unorm16x2; + self.insert_attribute(attr, values.create_compressed_uvs(uv_range).unwrap()); + ok(self, uv_range); + Ok(self) } - /// Create a [`Mesh`] with the given compression flags to reduce memory bandwidth on GPU, with the tradeoff of reduced precision of vertex attributes. - /// - /// See [`MeshAttributeCompressionFlags`] for more context. - /// if vertex attributes are already compressed, they are unchanged and won't decompress. - /// - /// If `index_compression` is true and indices are u32 and vertex count <= 65535, indices will be converted to u16, otherwise it does nothing. + /// Compress UV0 and apply [`MeshAttributeCompressionFlags::COMPRESS_UV0`]. + /// See [`MeshAttributeCompressionFlags`] for the details. + pub fn compress_uv0(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { + self.compress_uvs(Mesh::ATTRIBUTE_UV_0, |mesh, uv_range| { + mesh.final_uv_ranges[0] = Some(uv_range); + mesh.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_UV0; + }) + } + + /// Compress UV1 and apply [`MeshAttributeCompressionFlags::COMPRESS_UV1`]. + /// See [`MeshAttributeCompressionFlags`] for the details. + pub fn compress_uv1(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { + self.compress_uvs(Mesh::ATTRIBUTE_UV_1, |mesh, uv_range| { + mesh.final_uv_ranges[1] = Some(uv_range); + mesh.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_UV1; + }) + } + + /// Compress normals and apply [`MeshAttributeCompressionFlags::COMPRESS_NORMAL`]. + /// See [`MeshAttributeCompressionFlags`] for the details. + pub fn compress_normals(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { + let mut attr = Mesh::ATTRIBUTE_NORMAL; + let Some(values) = self.attribute(attr) else { + return Err(MeshVertexCompressionError::MissingAttribute(attr)); + }; + attr.format = VertexFormat::Snorm16x2; + let Some(values) = values.create_octahedral_encode_normals() else { + return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { + attr, + expected: attr.format, + provided: values.into(), + }); + }; + self.insert_attribute(attr, values); + self.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_NORMAL; + Ok(self) + } + + /// Compress tangents and apply [`MeshAttributeCompressionFlags::COMPRESS_TANGENT`]. + /// See [`MeshAttributeCompressionFlags`] for the details. + pub fn compress_tangents(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { + let mut attr = Mesh::ATTRIBUTE_TANGENT; + let Some(values) = self.attribute(attr) else { + return Err(MeshVertexCompressionError::MissingAttribute(attr)); + }; + attr.format = VertexFormat::Snorm16x2; + let Some(values) = values.create_octahedral_encode_tangents() else { + return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { + attr, + expected: attr.format, + provided: values.into(), + }); + }; + self.insert_attribute(attr, values); + self.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_TANGENT; + Ok(self) + } + + /// Quantize `Float32x4` colors to the type of `quantization`. + /// [`AttributeQuantization::Unorm8`] or [`AttributeQuantization::Float16`] (if you need HDR colors) is recommended. + pub fn quantize_colors( + &mut self, + quantization: AttributeQuantization, + ) -> Result<&mut Mesh, MeshVertexCompressionError> { + let mut attr = Mesh::ATTRIBUTE_COLOR; + let Some(values) = self.attribute(attr) else { + return Err(MeshVertexCompressionError::MissingAttribute(attr)); + }; + let Some(values) = values.create_quantized_values(quantization) else { + return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { + attr, + expected: attr.format, + provided: values.into(), + }); + }; + attr.format = quantization.vertex_format::<4>(); + self.insert_attribute(attr, values); + Ok(self) + } + + /// Quantize `Float32x4` joint weights to the type of `quantization`. + /// [`AttributeQuantization::Unorm16`] is recommended. + pub fn quantize_joint_weights( + &mut self, + quantization: AttributeQuantization, + ) -> Result<&mut Mesh, MeshVertexCompressionError> { + let mut attr = Mesh::ATTRIBUTE_JOINT_WEIGHT; + let Some(values) = self.attribute(attr) else { + return Err(MeshVertexCompressionError::MissingAttribute(attr)); + }; + let Some(values) = values.create_quantized_values(quantization) else { + return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { + attr, + expected: attr.format, + provided: values.into(), + }); + }; + attr.format = quantization.vertex_format::<4>(); + self.insert_attribute(attr, values); + Ok(self) + } + + /// If indices are u32 and vertex count <= 65535, indices will be converted to u16, otherwise this does nothing. /// /// # Panics /// Panics when the mesh data has already been extracted to `RenderWorld`. - pub fn compressed_mesh( - mut self, - mut attribute_compression: MeshAttributeCompressionFlags, - index_compression: bool, - ) -> Mesh { - if self - .attribute_compression - .intersects(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT) + pub fn compress_indices(&mut self) -> &mut Mesh { + // Vertex count should be <= 65535 (max index <= 65534), not 65536 because of primitive restart value. + if let Some(Indices::U32(indices)) = self.indices() + && self.count_vertices() <= 65535 { - // Don't change color flag if it's already compressed. - attribute_compression - .remove(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT); - } - self.attribute_compression |= attribute_compression; - for mut attr in [ - Mesh::ATTRIBUTE_POSITION, - Mesh::ATTRIBUTE_NORMAL, - Mesh::ATTRIBUTE_UV_0, - Mesh::ATTRIBUTE_UV_1, - Mesh::ATTRIBUTE_TANGENT, - Mesh::ATTRIBUTE_COLOR, - Mesh::ATTRIBUTE_JOINT_WEIGHT, - ] { - if let Some(compressed_format) = self.get_compressed_vertex_format(attr.id) - && let Some(values) = self.attribute(attr.id) - { - // Must compute aabb, uv0, uv1 before we insert the compressed attributes. After compressing them can't be computed. - // If computing fails, which means the format isn't expected uncompressed format or the values is empty, we skip this attribute. - match attr.id { - ATTRIBUTE_POSITION_ID => { - let Some(aabb) = Self::compute_aabb(values) else { - continue; - }; - self.final_aabb = Some(aabb); - } - ATTRIBUTE_UV_0_ID => { - let Some(uv_range) = Self::compute_uv_range(values) else { - continue; - }; - self.final_uv_ranges[0] = Some(uv_range); - } - ATTRIBUTE_UV_1_ID => { - let Some(uv_range) = Self::compute_uv_range(values) else { - continue; - }; - self.final_uv_ranges[1] = Some(uv_range); - } - _ => {} - } - let values = self.create_compressed_attribute_values( - attr.id, - self.attribute(attr.id).unwrap(), - self.final_aabb, - self.final_uv_ranges, - ); - if let Some(values) = values { - attr.format = compressed_format; - self.insert_attribute(attr, values); - } - } + self.insert_indices(Indices::U16( + indices.iter().map(|idx| *idx as u16).collect(), + )); } + self + } - if index_compression { - // Vertex count should be <= 65535 (max index <= 65534), not 65536 because of primitive restart value. - if let Some(Indices::U32(indices)) = self.indices() - && self.count_vertices() <= 65535 - { - self.insert_indices(Indices::U16( - indices.iter().map(|idx| *idx as u16).collect(), - )); - } + /// Compress the mesh with [`MeshCompressionArgs`] using `Mesh::compress_*` and `Mesh::quantize_*` methods. + /// Any failed compression is just ignored. + pub fn compressed_mesh(mut self, args: MeshCompressionArgs) -> Mesh { + if args + .compress_attributes + .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION) + { + let _ = self.compress_positions(); + } + if args + .compress_attributes + .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) + { + let _ = self.compress_normals(); + } + if args + .compress_attributes + .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) + { + let _ = self.compress_tangents(); + } + if args + .compress_attributes + .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) + { + let _ = self.compress_uv0(); + } + if args + .compress_attributes + .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) + { + let _ = self.compress_uv1(); + } + if let Some(quantization) = args.quantize_attributes.color { + let _ = self.quantize_colors(quantization); + } + if let Some(quantization) = args.quantize_attributes.joint_weight { + let _ = self.quantize_joint_weights(quantization); + } + if args.compress_indices { + self.compress_indices(); } - self } @@ -3010,6 +3005,21 @@ impl MeshDeserializer { } } +/// Error that can occur when compressing/quantizing mesh vertex attributes. +#[derive(Error, Debug, Clone)] +pub enum MeshVertexCompressionError { + #[error("Vertex attribute {0:?} doesn't exist")] + MissingAttribute(MeshVertexAttribute), + #[error("Vertex attribute {0:?} must not be empty")] + EmptyAttribute(MeshVertexAttribute), + #[error("Vertex attribute {attr:?} must have format {expected:?} before compressing/quantizing, but got {provided:?}")] + UnmatchedAttributeFormat { + attr: MeshVertexAttribute, + expected: VertexFormat, + provided: VertexFormat, + }, +} + /// Error that can occur when calling [`Mesh::merge_duplicate_vertices`] #[derive(Error, Debug, Clone)] pub enum MeshMergeDuplicateVerticesError { @@ -3046,11 +3056,11 @@ mod tests { #[cfg(feature = "serialize")] use super::SerializedMesh; use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; - use crate::{MeshAttributeCompressionFlags, MeshVertexAttribute, PrimitiveTopology}; + use crate::{AttributeQuantization, MeshAttributeCompressionFlags, PrimitiveTopology}; use bevy_asset::RenderAssetUsages; - use bevy_math::bounding::Aabb3d; + use bevy_math::bounding::{Aabb2d, Aabb3d}; use bevy_math::primitives::Triangle3d; - use bevy_math::{Vec3, Vec3A}; + use bevy_math::{Vec2, Vec3, Vec3A}; use bevy_transform::components::Transform; #[test] @@ -3510,13 +3520,8 @@ mod tests { } #[test] - fn compress_mesh() { - let custom_attr = MeshVertexAttribute::new( - "custom_attr", - Mesh::FIRST_AVAILABLE_CUSTOM_ATTRIBUTE, - wgpu_types::VertexFormat::Uint32, - ); - let mesh = Mesh::new( + fn compress_mesh_positions() { + let mut mesh = Mesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) @@ -3528,73 +3533,21 @@ mod tests { [-1.0, -0.5, -1.0], [0.0, -0.5, 1.0], ], - ) - .with_inserted_attribute( - Mesh::ATTRIBUTE_NORMAL, - vec![ - Vec3::new(0.0, 1.0, -1.0).normalize().to_array(), - Vec3::new(1.0, 0.0, -1.0).normalize().to_array(), - Vec3::new(-1.0, 0.0, -1.0).normalize().to_array(), - [0.0, 0.0, 1.0], - ], - ) - .with_inserted_attribute( - Mesh::ATTRIBUTE_TANGENT, - vec![ - Vec3::new(0.0, 1.0, 1.0).normalize().extend(1.0).to_array(), - Vec3::new(1.0, 0.0, 1.0).normalize().extend(1.0).to_array(), - Vec3::new(-1.0, 0.0, 1.0).normalize().extend(1.0).to_array(), - [1.0, 0.0, 0.0, 1.0], - ], - ) - .with_inserted_attribute( - Mesh::ATTRIBUTE_UV_0, - vec![[0.126, 0.497], [0.126, 1.0], [0.05, 0.0], [0.0, 0.5]], - ) - .with_inserted_attribute( - Mesh::ATTRIBUTE_COLOR, - vec![ - [0.05, 1.0, 0.15, 1.0], - [0.75, 0.07, 1.0, 1.0], - [0.04, 0.11, 1.0, 1.0], - [1.0, 0.11, 0.13, 1.0], - ], - ) - .with_inserted_attribute( - Mesh::ATTRIBUTE_JOINT_WEIGHT, - vec![ - [0.024, 0.0, 0.0, 0.0], - [0.007, 0.0, 0.0, 0.0], - [0.008, 0.0, 0.0, 0.0], - [1.0, 0.0, 0.0, 0.0], - ], - ) - .with_inserted_attribute( - Mesh::ATTRIBUTE_JOINT_INDEX, - VertexAttributeValues::Uint16x4(vec![ - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - [0, 0, 0, 0], - ]), - ) - .with_inserted_attribute(custom_attr, VertexAttributeValues::Uint32(vec![0, 1, 2, 3])) - .with_inserted_indices(Indices::U32(vec![0, 1, 2, 0, 3, 1, 0, 2, 3, 1, 3, 2])); - - let mesh_compressed_all = mesh.clone().compressed_mesh( - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8), - true, ); + mesh.compress_positions().unwrap(); assert_eq!( - mesh_compressed_all.final_aabb, + mesh.final_aabb, Some(Aabb3d::from_min_max( Vec3A::new(-1.0, -0.5, -1.0), Vec3A::new(1.0, 1.0, 1.0) )) ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_POSITION), + mesh.attribute_compression, + MeshAttributeCompressionFlags::COMPRESS_POSITION + ); + assert_eq!( + mesh.attribute(Mesh::ATTRIBUTE_POSITION), Some(&VertexAttributeValues::Snorm16x4(vec![ [0, 32767, -32767, 0], [32767, -32767, -32767, 0], @@ -3602,35 +3555,163 @@ mod tests { [0, -32767, 32767, 0], ])) ); + } + + #[test] + fn compress_mesh_uvs() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_UV_0, + vec![[0.126, 0.497], [0.126, 1.0], [0.05, 0.0], [0.0, 0.5]], + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_UV_1, + vec![[12.6, 0.497], [0.126, 1.0], [-4.05, 0.0], [1.0, -0.5]], + ); + mesh.compress_uv0().unwrap(); + assert_eq!( + mesh.final_uv_ranges, + [ + Some(Aabb2d { + min: Vec2::new(0.0, 0.0), + max: Vec2::new(0.126, 1.0) + }), + None + ] + ); + assert_eq!( + mesh.attribute_compression, + MeshAttributeCompressionFlags::COMPRESS_UV0 + ); + assert_eq!( + mesh.attribute(Mesh::ATTRIBUTE_UV_0), + Some(&VertexAttributeValues::Unorm16x2(vec![ + [65535, 32571], + [65535, 65535], + [26006, 0], + [0, 32768], + ])) + ); + + mesh.compress_uv1().unwrap(); + assert_eq!( + mesh.final_uv_ranges, + [ + Some(Aabb2d { + min: Vec2::new(0.0, 0.0), + max: Vec2::new(0.126, 1.0) + }), + Some(Aabb2d { + min: Vec2::new(-4.05, -0.5), + max: Vec2::new(12.6, 1.0) + }) + ] + ); + assert_eq!( + mesh.attribute_compression, + MeshAttributeCompressionFlags::COMPRESS_UV0 + | MeshAttributeCompressionFlags::COMPRESS_UV1 + ); + assert_eq!( + mesh.attribute(Mesh::ATTRIBUTE_UV_1), + Some(&VertexAttributeValues::Unorm16x2(vec![ + [65535, 43559], + [16437, 65535], + [0, 21845], + [19877, 0] + ])) + ); + } + + #[test] + fn compress_mesh_normals() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_NORMAL, + vec![ + Vec3::new(0.0, 1.0, -1.0).normalize().to_array(), + Vec3::new(1.0, 0.0, -1.0).normalize().to_array(), + Vec3::new(-1.0, 0.0, -1.0).normalize().to_array(), + [0.0, 0.0, 1.0], + ], + ); + mesh.compress_normals().unwrap(); + assert_eq!( + mesh.attribute_compression, + MeshAttributeCompressionFlags::COMPRESS_NORMAL + ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_NORMAL), + mesh.attribute(Mesh::ATTRIBUTE_NORMAL), Some(&VertexAttributeValues::Snorm16x2(vec![ - [-16384, 32767], - [32767, -16384], - [-32767, -16384], + [16384, 32767], + [32767, 16384], + [-32767, 16384], [0, 0], ])) ); + } + + #[test] + fn compress_mesh_tangents() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_TANGENT, + vec![ + Vec3::new(0.0, 1.0, 1.0).normalize().extend(1.0).to_array(), + Vec3::new(1.0, 0.0, 1.0).normalize().extend(-1.0).to_array(), + Vec3::new(-1.0, 0.0, 1.0).normalize().extend(1.0).to_array(), + [1.0, 0.0, 0.0, 1.0], + ], + ); + mesh.compress_tangents().unwrap(); + assert_eq!( + mesh.attribute_compression, + MeshAttributeCompressionFlags::COMPRESS_TANGENT + ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_TANGENT), + mesh.attribute(Mesh::ATTRIBUTE_TANGENT), Some(&VertexAttributeValues::Snorm16x2(vec![ [0, 24575], - [16384, 16384], + [16384, -16384], [-16384, 16384], [32767, 16384], ])) ); + } + + #[test] + fn quantize_mesh_colors() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_COLOR, + vec![ + [0.05, 1.0, 0.15, 1.0], + [0.75, 0.07, 1.0, 1.0], + [0.04, 0.11, 1.0, 1.0], + [1.0, 0.11, 0.13, 1.0], + ], + ); + let mut mesh_color_f16 = mesh.clone(); + + mesh.quantize_colors(AttributeQuantization::Unorm8).unwrap(); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_UV_0), - Some(&VertexAttributeValues::Unorm16x2(vec![ - [65535, 32571], - [65535, 65535], - [26006, 0], - [0, 32768], - ])) + mesh.attribute_compression, + MeshAttributeCompressionFlags::empty() ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_COLOR), + mesh.attribute(Mesh::ATTRIBUTE_COLOR), Some(&VertexAttributeValues::Unorm8x4(vec![ [13, 255, 38, 255], [191, 18, 255, 255], @@ -3638,29 +3719,18 @@ mod tests { [255, 28, 33, 255], ])) ); + + mesh_color_f16 + .quantize_colors(AttributeQuantization::Float16) + .unwrap(); assert_eq!( - mesh_compressed_all.indices(), - Some(&Indices::U16(vec![0, 1, 2, 0, 3, 1, 0, 2, 3, 1, 3, 2])) - ); - assert_eq!( - mesh_compressed_all.attribute(custom_attr), - Some(&VertexAttributeValues::Uint32(vec![0, 1, 2, 3])) + mesh_color_f16.attribute_compression, + MeshAttributeCompressionFlags::empty() ); - let mesh_compressed_color_f16 = mesh - .clone() - .compressed_mesh(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16, false); - assert!(mesh - .attributes() - .filter(|(attr, _values)| attr.id != Mesh::ATTRIBUTE_COLOR.id) - .eq(mesh_compressed_color_f16 - .attributes() - .filter(|(attr, _values)| attr.id != Mesh::ATTRIBUTE_COLOR.id))); - assert_eq!(mesh.indices(), mesh_compressed_color_f16.indices()); - let VertexAttributeValues::Float16x4(color_f16) = mesh_compressed_color_f16 - .attribute(Mesh::ATTRIBUTE_COLOR) - .unwrap() + let VertexAttributeValues::Float16x4(color_f16) = + mesh_color_f16.attribute(Mesh::ATTRIBUTE_COLOR).unwrap() else { - panic!("Color attribute is not Float16x4") + panic!("Color attribute is not compressed to Float16x4") }; assert!(color_f16 .iter() @@ -3677,4 +3747,62 @@ mod tests { ) .all(|(a, b)| approx::relative_eq!(a.to_f32(), b.to_f32()))); } + + #[test] + fn quantize_mesh_joint_weights() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_JOINT_WEIGHT, + vec![ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + [0.1, 0.9, 0.0, 0.0], + [0.1, 0.2, 0.7, 0.0], + [0.1, 0.2, 0.4, 0.3], + ], + ); + + mesh.quantize_joint_weights(AttributeQuantization::Unorm16) + .unwrap(); + assert_eq!( + mesh.attribute_compression, + MeshAttributeCompressionFlags::empty() + ); + assert_eq!( + mesh.attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT), + Some(&VertexAttributeValues::Unorm16x4(vec![ + [65535, 0, 0, 0], + [0, 65535, 0, 0], + [0, 0, 65535, 0], + [0, 0, 0, 65535], + [6554, 58982, 0, 0], + [6554, 13107, 45875, 0], + [6554, 13107, 26214, 19661] + ])) + ); + } + + #[test] + fn compress_mesh_indices() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_indices(Indices::U32(vec![0, 1, 2, 0, 3, 1, 0, 2, 3, 1, 3, 2])); + + mesh.compress_indices(); + assert_eq!( + mesh.attribute_compression, + MeshAttributeCompressionFlags::empty() + ); + assert_eq!( + mesh.indices(), + Some(&Indices::U16(vec![0, 1, 2, 0, 3, 1, 0, 2, 3, 1, 3, 2])) + ); + } } diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index c638247e423c6..cdff21b5cbacb 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -3,7 +3,7 @@ use bevy_derive::EnumVariantMeta; use bevy_ecs::resource::Resource; use bevy_math::{ bounding::{Aabb2d, Aabb3d, BoundingVolume}, - vec2, Vec2, Vec3, Vec3A, Vec3Swizzles, + ops, vec2, Vec2, Vec3, Vec3A, Vec3Swizzles, }; #[cfg(feature = "serialize")] use bevy_platform::collections::HashMap; @@ -246,6 +246,78 @@ pub fn triangle_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { (b - a).cross(c - a).normalize_or_zero().into() } +/// Vertex format that can be quantized from Float32. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +pub enum AttributeQuantization { + Unorm8, + Snorm8, + Unorm16, + Snorm16, + Float16, +} + +impl AttributeQuantization { + /// Create a [`VertexAttributeValues`] with `values` quantized to the format of this quantization. + pub fn quantize_f32_values( + &self, + values: &[[f32; N]], + ) -> VertexAttributeValues { + match self { + AttributeQuantization::Unorm8 => { + let values = values.iter().map(|v| arr_f32_to_unorm8(*v)).collect(); + match N { + 1 => VertexAttributeValues::Unorm8(bytemuck::cast_vec(values)), + 2 => VertexAttributeValues::Unorm8x2(bytemuck::cast_vec(values)), + 4 => VertexAttributeValues::Unorm8x4(bytemuck::cast_vec(values)), + _ => unreachable!(), + } + } + AttributeQuantization::Snorm8 => { + let values = values.iter().map(|v| arr_f32_to_snorm8(*v)).collect(); + match N { + 1 => VertexAttributeValues::Snorm8(bytemuck::cast_vec(values)), + 2 => VertexAttributeValues::Snorm8x2(bytemuck::cast_vec(values)), + 4 => VertexAttributeValues::Snorm8x4(bytemuck::cast_vec(values)), + _ => unreachable!(), + } + } + AttributeQuantization::Unorm16 => { + let values = values.iter().map(|v| arr_f32_to_unorm16(*v)).collect(); + match N { + 1 => VertexAttributeValues::Unorm16(bytemuck::cast_vec(values)), + 2 => VertexAttributeValues::Unorm16x2(bytemuck::cast_vec(values)), + 4 => VertexAttributeValues::Unorm16x4(bytemuck::cast_vec(values)), + _ => unreachable!(), + } + } + AttributeQuantization::Snorm16 => { + let values = values.iter().map(|v| arr_f32_to_snorm16(*v)).collect(); + match N { + 1 => VertexAttributeValues::Snorm16(bytemuck::cast_vec(values)), + 2 => VertexAttributeValues::Snorm16x2(bytemuck::cast_vec(values)), + 4 => VertexAttributeValues::Snorm16x4(bytemuck::cast_vec(values)), + _ => unreachable!(), + } + } + AttributeQuantization::Float16 => { + let values = values.iter().map(|v| arr_f32_to_float16(*v)).collect(); + match N { + 1 => VertexAttributeValues::Float16(bytemuck::cast_vec(values)), + 2 => VertexAttributeValues::Float16x2(bytemuck::cast_vec(values)), + 4 => VertexAttributeValues::Float16x4(bytemuck::cast_vec(values)), + _ => unreachable!(), + } + } + } + } + + /// Get the vertex format of this quantization with N components. + pub fn vertex_format(&self) -> VertexFormat { + (&self.quantize_f32_values::(&[])).into() + } +} + /// Contains an array where each entry describes a property of a single vertex. /// Matches the [`VertexFormats`](VertexFormat). #[derive(Clone, Debug, EnumVariantMeta, PartialEq)] @@ -521,50 +593,19 @@ impl VertexAttributeValues { } } - /// Create a new `VertexAttributeValues` with the values converted from f32 to f16. Return None if the values are not Float32, Float32x2 or Float32x4. - pub(crate) fn create_f16_values(&self) -> Option { - match &self { - VertexAttributeValues::Float32(uncompressed_values) => { - let mut values = Vec::::with_capacity(uncompressed_values.len()); - for value in uncompressed_values { - values.push(arr_f32_to_f16([*value])[0]); - } - Some(VertexAttributeValues::Float16(values)) - } - VertexAttributeValues::Float32x2(uncompressed_values) => { - let mut values = Vec::<[half::f16; 2]>::with_capacity(uncompressed_values.len()); - for value in uncompressed_values { - values.push(arr_f32_to_f16(*value)); - } - Some(VertexAttributeValues::Float16x2(values)) - } - VertexAttributeValues::Float32x4(uncompressed_values) => { - let mut values = Vec::<[half::f16; 4]>::with_capacity(uncompressed_values.len()); - for value in uncompressed_values { - values.push(arr_f32_to_f16(*value)); - } - Some(VertexAttributeValues::Float16x4(values)) + pub(crate) fn create_quantized_values( + &self, + quantization: AttributeQuantization, + ) -> Option { + match self { + VertexAttributeValues::Float32(values) => { + Some(quantization.quantize_f32_values::<1>(bytemuck::must_cast_slice(values))) } - _ => None, - } - } - - /// Create a new `VertexAttributeValues` with the values converted from f32 to unorm16. Return None if the values are not Float32, Float32x2 or Float32x4. - pub(crate) fn create_unorm16_values(&self) -> Option { - match &self { - VertexAttributeValues::Float32x2(uncompressed_values) => { - let mut values = Vec::<[u16; 2]>::with_capacity(uncompressed_values.len()); - for value in uncompressed_values { - values.push(arr_f32_to_unorm16(*value)); - } - Some(VertexAttributeValues::Unorm16x2(values)) + VertexAttributeValues::Float32x2(values) => { + Some(quantization.quantize_f32_values::<2>(values)) } - VertexAttributeValues::Float32x4(uncompressed_values) => { - let mut values = Vec::<[u16; 4]>::with_capacity(uncompressed_values.len()); - for value in uncompressed_values { - values.push(arr_f32_to_unorm16(*value)); - } - Some(VertexAttributeValues::Unorm16x4(values)) + VertexAttributeValues::Float32x4(values) => { + Some(quantization.quantize_f32_values::<4>(values)) } _ => None, } @@ -603,6 +644,7 @@ impl VertexAttributeValues { } } + /// Returns the compressed positions, or `None` if `self` is not [`Self::Float32x3`]. pub(crate) fn create_compressed_positions( &self, aabb: Aabb3d, @@ -1121,19 +1163,23 @@ impl Hash for MeshVertexBufferLayoutRef { } } -pub(crate) fn arr_f32_to_unorm16(value: [f32; N]) -> [u16; N] { - value.map(|v| (v.clamp(0.0, 1.0) * u16::MAX as f32).round() as u16) -} - pub(crate) fn arr_f32_to_unorm8(value: [f32; N]) -> [u8; N] { value.map(|v| (v.clamp(0.0, 1.0) * u8::MAX as f32).round() as u8) } +pub(crate) fn arr_f32_to_snorm8(value: [f32; N]) -> [i8; N] { + value.map(|v| (v.clamp(-1.0, 1.0) * i8::MAX as f32).round() as i8) +} + +pub(crate) fn arr_f32_to_unorm16(value: [f32; N]) -> [u16; N] { + value.map(|v| (v.clamp(0.0, 1.0) * u16::MAX as f32).round() as u16) +} + pub(crate) fn arr_f32_to_snorm16(value: [f32; N]) -> [i16; N] { value.map(|v| (v.clamp(-1.0, 1.0) * i16::MAX as f32).round() as i16) } -pub(crate) fn arr_f32_to_f16(value: [f32; N]) -> [half::f16; N] { +pub(crate) fn arr_f32_to_float16(value: [f32; N]) -> [half::f16; N] { value.map(half::f16::from_f32) } @@ -1142,7 +1188,7 @@ pub fn octahedral_encode_signed(v: Vec3) -> Vec2 { let n = v / (v.x.abs() + v.y.abs() + v.z.abs()); let octahedral_wrap = (1.0 - n.yx().abs()) * Vec2::select( - n.xy().cmpgt(vec2(0.0, 0.0)), + n.xy().cmpge(vec2(0.0, 0.0)), vec2(1.0, 1.0), vec2(-1.0, -1.0), ); @@ -1155,14 +1201,16 @@ pub fn octahedral_encode_signed(v: Vec3) -> Vec2 { /// Encode tangent vectors as octahedral coordinates with range [-1, 1]. The sign is encoded in y component. Use [`octahedral_decode_tangent`] to decode. pub fn octahedral_encode_tangent(v: Vec3, sign: f32) -> Vec2 { - // Bias to ensure that encoding as unorm16 preserves the sign. See https://github.com/godotengine/godot/pull/73265 - let bias = 1.0 / 32767.0; + // Bias to ensure that encoding as snorm16 preserves the sign. + let bits = 16.; + let bias = 1. / (ops::powf(2.0, bits - 1.) - 1.); + let mut n_xy = octahedral_encode_signed(v); // Map y to always be positive. n_xy.y = n_xy.y * 0.5 + 0.5; n_xy.y = n_xy.y.max(bias); // Encode the sign. - n_xy.y = if sign >= 0.0 { n_xy.y } else { -n_xy.y }; + n_xy.y *= sign.signum(); n_xy } @@ -1177,7 +1225,7 @@ pub fn octahedral_decode_signed(v: Vec2) -> Vec3 { /// Decode tangent vectors from octahedral coordinates and return the sign. Input is [-1, 1]. The y component should have been mapped to always be positive and then encoded the sign. pub fn octahedral_decode_tangent(v: Vec2) -> (Vec3, f32) { - let sign = if v.y >= 0.0 { 1.0 } else { -1.0 }; + let sign = v.y.signum(); let mut f = v; f.y = f.y.abs(); f.y = f.y * 2.0 - 1.0; @@ -1198,20 +1246,23 @@ mod tests { let vectors = [ vec3(1.0, 2.0, 3.0).normalize().extend(1.0), vec3(1.0, 0.0, 0.0).extend(-1.0), + vec3(0.0, 1.0, 0.0).extend(-1.0), vec3(0.0, 0.0, -1.0).extend(1.0), vec3(0.0, 0.0, -1.0).extend(-1.0), ]; let expected_encoded_normals = [ vec2(0.16666667, 0.33333334), vec2(1.0, 0.0), - vec2(-1.0, -1.0), - vec2(-1.0, -1.0), + vec2(0.0, 1.0), + vec2(1.0, 1.0), + vec2(1.0, 1.0), ]; let expected_encoded_tangents = [ vec2(0.16666667, 0.6666667), vec2(1.0, -0.5), - vec2(-1.0, 3.051851e-5), - vec2(-1.0, -3.051851e-5), + vec2(0.0, -1.0), + vec2(1.0, 1.0), + vec2(1.0, -1.0), ]; for (i, &v) in vectors.iter().enumerate() { let encoded_normal = octahedral_encode_signed(v.xyz()); @@ -1223,7 +1274,7 @@ mod tests { let (decoded_tangent, decoded_sign) = octahedral_decode_tangent(encoded_tangent); assert!(encoded_tangent.distance(expected_encoded_tangents[i]) < 1e-8); assert_eq!(v.w, decoded_sign); - assert!(decoded_tangent.distance(v.xyz()) < 1e-4); + assert!(decoded_tangent.distance(v.xyz()) < 1e-7); } } } diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index c176b52aee731..47544eb367a89 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -62,15 +62,16 @@ impl Plugin for MeshRenderAssetPlugin { fn finish(&self, app: &mut App) { let mut mesh_assets = app.world_mut().resource_mut::>(); - let handle = mesh_assets.add( - Mesh::new(PrimitiveTopology::PointList, RenderAssetUsages::all()) + let handle = mesh_assets.add({ + let mut mesh = Mesh::new(PrimitiveTopology::PointList, RenderAssetUsages::all()) .with_inserted_attribute( Mesh::ATTRIBUTE_POSITION, VertexAttributeValues::Float32x3(vec![[0.0; 3]]), ) - .with_inserted_indices(Indices::U16(vec![0])) - .compressed_mesh(MeshAttributeCompressionFlags::COMPRESS_POSITION, false), - ); + .with_inserted_indices(Indices::U16(vec![0])); + mesh.compress_positions().unwrap(); + mesh + }); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; diff --git a/crates/bevy_render/src/utils.wgsl b/crates/bevy_render/src/utils.wgsl index 2d6fc4cd97f6b..ab1ecfa870991 100644 --- a/crates/bevy_render/src/utils.wgsl +++ b/crates/bevy_render/src/utils.wgsl @@ -28,7 +28,7 @@ fn octahedral_decode_signed(v: vec2) -> vec3 { // Decode tangent vectors from octahedral coordinates and return the sign. Input is [-1, 1]. The y component should have been mapped to always be positive and then encoded the sign. fn octahedral_decode_tangent(v: vec2) -> vec4 { - let sign = select(-1.0, 1.0, v.y >= 0.0); + let sign = sign(v.y); var f = v; f.y = abs(f.y); f.y = f.y * 2.0 - 1.0; @@ -39,7 +39,7 @@ fn octahedral_decode_tangent(v: vec2) -> vec4 { // For encoding normals or unit direction vectors as octahedral coordinates. fn octahedral_encode(v: vec3) -> vec2 { var n = v / (abs(v.x) + abs(v.y) + abs(v.z)); - let octahedral_wrap = (1.0 - abs(n.yx)) * select(vec2(-1.0), vec2(1.0), n.xy > vec2f(0.0)); + let octahedral_wrap = (1.0 - abs(n.yx)) * select(vec2(-1.0), vec2(1.0), n.xy >= vec2f(0.0)); let n_xy = select(octahedral_wrap, n.xy, n.z >= 0.0); return n_xy * 0.5 + 0.5; } diff --git a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs index 671e568a83761..0e40ca2e7badd 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs +++ b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs @@ -10,7 +10,7 @@ use bevy_asset::{Assets, Handle}; use bevy_image::TextureAtlasLayout; use bevy_math::{primitives::Rectangle, vec2}; -use bevy_mesh::{Mesh, Mesh2d, MeshAttributeCompressionFlags, MeshBuilder, Meshable}; +use bevy_mesh::{Mesh, Mesh2d, MeshBuilder, Meshable}; use bevy_platform::collections::HashMap; use bevy_sprite::{prelude::SpriteMesh, Anchor}; @@ -44,17 +44,18 @@ fn add_mesh( mut commands: Commands, ) { let quad = quad.get_or_insert_with(|| { - meshes.add( - Rectangle::from_size(vec2(1.0, 1.0)) + meshes.add({ + let mut mesh = Rectangle::from_size(vec2(1.0, 1.0)) .mesh() .build() - .with_removed_attribute(Mesh::ATTRIBUTE_NORMAL) - .compressed_mesh( - MeshAttributeCompressionFlags::COMPRESS_POSITION - | MeshAttributeCompressionFlags::COMPRESS_UV0, - true, - ), - ) + .with_removed_attribute(Mesh::ATTRIBUTE_NORMAL); + mesh.compress_indices() + .compress_positions() + .unwrap() + .compress_uv0() + .unwrap(); + mesh + }) }); for entity in sprites { commands.entity(entity).insert(Mesh2d(quad.clone())); diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index 1f31f26477d71..bff6516d8d864 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -20,7 +20,7 @@ use bevy::{ ops::{cbrt, sqrt}, DVec2, DVec3, }, - mesh::MeshAttributeCompressionFlags, + mesh::MeshCompressionArgs, post_process::motion_blur::MotionBlur, prelude::*, render::{ @@ -431,11 +431,7 @@ fn init_materials( fn compress_mesh(args: &Args, mesh: impl Into) -> Mesh { if args.vertex_compression { - mesh.into().compressed_mesh( - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16), - true, - ) + mesh.into().compressed_mesh(MeshCompressionArgs::all()) } else { mesh.into() } diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index 2dfd082a6770c..e40843b5c6607 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -8,7 +8,7 @@ use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, gltf::GltfPlugin, light::CascadeShadowConfigBuilder, - mesh::MeshAttributeCompressionFlags, + mesh::MeshCompressionArgs, post_process::motion_blur::MotionBlur, prelude::*, window::{PresentMode, WindowResolution}, @@ -65,11 +65,10 @@ fn main() { ..default() }) .set(GltfPlugin { - mesh_attribute_compression: if args.vertex_compression { - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + mesh_compression: if args.vertex_compression { + MeshCompressionArgs::all() } else { - MeshAttributeCompressionFlags::empty() + MeshCompressionArgs::none() }, ..default() }), diff --git a/examples/stress_tests/many_morph_targets.rs b/examples/stress_tests/many_morph_targets.rs index 94c1cc2935e29..ad3fd156578a8 100644 --- a/examples/stress_tests/many_morph_targets.rs +++ b/examples/stress_tests/many_morph_targets.rs @@ -4,7 +4,7 @@ use argh::FromArgs; use bevy::{ diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, gltf::GltfPlugin, - mesh::MeshAttributeCompressionFlags, + mesh::MeshCompressionArgs, post_process::motion_blur::MotionBlur, prelude::*, window::{PresentMode, WindowResolution}, @@ -158,11 +158,10 @@ fn main() { ..Default::default() }) .set(GltfPlugin { - mesh_attribute_compression: if args.vertex_compression { - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + mesh_compression: if args.vertex_compression { + MeshCompressionArgs::all() } else { - MeshAttributeCompressionFlags::empty() + MeshCompressionArgs::none() }, ..default() }), From 18bdc86fa6f6eb85c405c79ac80b677d640df669 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 9 Jun 2026 21:28:33 +0800 Subject: [PATCH 2/9] Add `quantize_float32_attribute` --- crates/bevy_mesh/src/mesh.rs | 127 ++++++++++++++++++----------------- 1 file changed, 67 insertions(+), 60 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index f18dac75b8f16..43177b4a124f7 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -271,15 +271,14 @@ pub struct Mesh { } /// Quantization arguments of the mesh. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct MeshAttributeQuantization { - pub color: Option, - pub joint_weight: Option, + pub attributes: alloc::borrow::Cow<'static, [(MeshVertexAttribute, AttributeQuantization)]>, } /// Mesh compression arguments used in [`Mesh::compressed_mesh`]. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct MeshCompressionArgs { /// Whether to compress indices to [`Indices::U16`] when possible. @@ -293,25 +292,27 @@ pub struct MeshCompressionArgs { impl MeshCompressionArgs { /// None of compression/quantization is enabled. - pub const fn none() -> Self { + pub fn none() -> Self { Self { compress_indices: false, quantize_attributes: MeshAttributeQuantization { - color: None, - joint_weight: None, + attributes: (&[]).into(), }, compress_attributes: MeshAttributeCompressionFlags::empty(), } } /// All compression is enabled, with colors quantized to unorm8 and joint weights quantized to unorm16. - pub const fn all() -> Self { + pub fn all() -> Self { Self { compress_indices: true, quantize_attributes: MeshAttributeQuantization { - // Unorm8 is chosen by default, as glTF vertex color is unorm8/unorm16 and HDR vertex color isn't common. - color: Some(AttributeQuantization::Unorm8), - joint_weight: Some(AttributeQuantization::Unorm16), + attributes: (&[ + // Unorm8 is chosen by default, as glTF vertex color is unorm8/unorm16 and HDR vertex color isn't common. + (Mesh::ATTRIBUTE_COLOR, AttributeQuantization::Unorm8), + (Mesh::ATTRIBUTE_JOINT_WEIGHT, AttributeQuantization::Unorm16), + ]) + .into(), }, compress_attributes: MeshAttributeCompressionFlags::all(), } @@ -1061,11 +1062,13 @@ impl Mesh { return Err(MeshVertexCompressionError::EmptyAttribute(attr)); } let Some(aabb) = Self::compute_aabb(values) else { - return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { - attr, - expected: attr.format, - provided: values.into(), - }); + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + attr, + expected: attr.format, + provided: values.into(), + }, + ); }; attr.format = VertexFormat::Snorm16x4; self.insert_attribute(attr, values.create_compressed_positions(aabb).unwrap()); @@ -1086,11 +1089,13 @@ impl Mesh { return Err(MeshVertexCompressionError::EmptyAttribute(attr)); } let Some(uv_range) = Self::compute_uv_range(values) else { - return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { - attr, - expected: attr.format, - provided: values.into(), - }); + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + attr, + expected: attr.format, + provided: values.into(), + }, + ); }; attr.format = VertexFormat::Unorm16x2; self.insert_attribute(attr, values.create_compressed_uvs(uv_range).unwrap()); @@ -1125,11 +1130,13 @@ impl Mesh { }; attr.format = VertexFormat::Snorm16x2; let Some(values) = values.create_octahedral_encode_normals() else { - return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { - attr, - expected: attr.format, - provided: values.into(), - }); + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + attr, + expected: attr.format, + provided: values.into(), + }, + ); }; self.insert_attribute(attr, values); self.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_NORMAL; @@ -1145,59 +1152,57 @@ impl Mesh { }; attr.format = VertexFormat::Snorm16x2; let Some(values) = values.create_octahedral_encode_tangents() else { - return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { - attr, - expected: attr.format, - provided: values.into(), - }); + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + attr, + expected: attr.format, + provided: values.into(), + }, + ); }; self.insert_attribute(attr, values); self.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_TANGENT; Ok(self) } - /// Quantize `Float32x4` colors to the type of `quantization`. - /// [`AttributeQuantization::Unorm8`] or [`AttributeQuantization::Float16`] (if you need HDR colors) is recommended. - pub fn quantize_colors( + /// Quantize `Float32`, `Float32x2` or `Float32x4` vertex attribute to the type of `quantization`. + pub fn quantize_float32_attribute( &mut self, + mut attr: MeshVertexAttribute, quantization: AttributeQuantization, ) -> Result<&mut Mesh, MeshVertexCompressionError> { - let mut attr = Mesh::ATTRIBUTE_COLOR; let Some(values) = self.attribute(attr) else { return Err(MeshVertexCompressionError::MissingAttribute(attr)); }; let Some(values) = values.create_quantized_values(quantization) else { - return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { - attr, - expected: attr.format, - provided: values.into(), - }); + return Err( + MeshVertexCompressionError::UnsupportedAttributeForQuantizing { + attr, + provided: values.into(), + }, + ); }; attr.format = quantization.vertex_format::<4>(); self.insert_attribute(attr, values); Ok(self) } + /// Quantize `Float32x4` colors to the type of `quantization`. + /// [`AttributeQuantization::Unorm8`] or [`AttributeQuantization::Float16`] (if you need HDR colors) is recommended. + pub fn quantize_colors( + &mut self, + quantization: AttributeQuantization, + ) -> Result<&mut Mesh, MeshVertexCompressionError> { + self.quantize_float32_attribute(Mesh::ATTRIBUTE_COLOR, quantization) + } + /// Quantize `Float32x4` joint weights to the type of `quantization`. /// [`AttributeQuantization::Unorm16`] is recommended. pub fn quantize_joint_weights( &mut self, quantization: AttributeQuantization, ) -> Result<&mut Mesh, MeshVertexCompressionError> { - let mut attr = Mesh::ATTRIBUTE_JOINT_WEIGHT; - let Some(values) = self.attribute(attr) else { - return Err(MeshVertexCompressionError::MissingAttribute(attr)); - }; - let Some(values) = values.create_quantized_values(quantization) else { - return Err(MeshVertexCompressionError::UnmatchedAttributeFormat { - attr, - expected: attr.format, - provided: values.into(), - }); - }; - attr.format = quantization.vertex_format::<4>(); - self.insert_attribute(attr, values); - Ok(self) + self.quantize_float32_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, quantization) } /// If indices are u32 and vertex count <= 65535, indices will be converted to u16, otherwise this does nothing. @@ -1249,11 +1254,8 @@ impl Mesh { { let _ = self.compress_uv1(); } - if let Some(quantization) = args.quantize_attributes.color { - let _ = self.quantize_colors(quantization); - } - if let Some(quantization) = args.quantize_attributes.joint_weight { - let _ = self.quantize_joint_weights(quantization); + for (attr, quantization) in args.quantize_attributes.attributes.iter().copied() { + let _ = self.quantize_float32_attribute(attr, quantization); } if args.compress_indices { self.compress_indices(); @@ -3013,11 +3015,16 @@ pub enum MeshVertexCompressionError { #[error("Vertex attribute {0:?} must not be empty")] EmptyAttribute(MeshVertexAttribute), #[error("Vertex attribute {attr:?} must have format {expected:?} before compressing/quantizing, but got {provided:?}")] - UnmatchedAttributeFormat { + UnsupportedAttributeForCompression { attr: MeshVertexAttribute, expected: VertexFormat, provided: VertexFormat, }, + #[error("Vertex attribute {attr:?} must have format `Float32`, `Float32x2` or `Float32x4` before quantizing, but got {provided:?}")] + UnsupportedAttributeForQuantizing { + attr: MeshVertexAttribute, + provided: VertexFormat, + }, } /// Error that can occur when calling [`Mesh::merge_duplicate_vertices`] From ff3ffc89b4fab8d0660d5bb7973b97736bbf6c1f Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 9 Jun 2026 21:53:05 +0800 Subject: [PATCH 3/9] Fix serde --- crates/bevy_gltf/src/lib.rs | 2 +- crates/bevy_gltf/src/loader/mod.rs | 3 ++- crates/bevy_mesh/src/mesh.rs | 38 +++++++++++++++++------------- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 211fcb47e190c..75c103bc40704 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -303,7 +303,7 @@ impl Plugin for GltfPlugin { default_convert_coordinates: self.convert_coordinates, extensions: extensions.0.clone(), default_skinned_mesh_bounds_policy: self.skinned_mesh_bounds_policy, - default_mesh_compression: self.mesh_compression, + default_mesh_compression: self.mesh_compression.clone(), }); } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 8e874d7c68db9..3da8cf15345ac 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -873,7 +873,8 @@ impl GltfLoader { mesh.compressed_mesh( settings .mesh_compression - .unwrap_or(loader.default_mesh_compression), + .clone() + .unwrap_or(loader.default_mesh_compression.clone()), ), ); primitives.push(super::GltfPrimitive::new( diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 43177b4a124f7..03d06c4fd4341 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -274,7 +274,7 @@ pub struct Mesh { #[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct MeshAttributeQuantization { - pub attributes: alloc::borrow::Cow<'static, [(MeshVertexAttribute, AttributeQuantization)]>, + pub attributes: alloc::borrow::Cow<'static, [(MeshVertexAttributeId, AttributeQuantization)]>, } /// Mesh compression arguments used in [`Mesh::compressed_mesh`]. @@ -309,8 +309,11 @@ impl MeshCompressionArgs { quantize_attributes: MeshAttributeQuantization { attributes: (&[ // Unorm8 is chosen by default, as glTF vertex color is unorm8/unorm16 and HDR vertex color isn't common. - (Mesh::ATTRIBUTE_COLOR, AttributeQuantization::Unorm8), - (Mesh::ATTRIBUTE_JOINT_WEIGHT, AttributeQuantization::Unorm16), + (Mesh::ATTRIBUTE_COLOR.id, AttributeQuantization::Unorm8), + ( + Mesh::ATTRIBUTE_JOINT_WEIGHT.id, + AttributeQuantization::Unorm16, + ), ]) .into(), }, @@ -1056,7 +1059,7 @@ impl Mesh { pub fn compress_positions(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { let mut attr = Mesh::ATTRIBUTE_POSITION; let Some(values) = self.attribute(attr) else { - return Err(MeshVertexCompressionError::MissingAttribute(attr)); + return Err(MeshVertexCompressionError::MissingAttribute(attr.id)); }; if values.is_empty() { return Err(MeshVertexCompressionError::EmptyAttribute(attr)); @@ -1083,7 +1086,7 @@ impl Mesh { ok: impl Fn(&mut Mesh, Aabb2d), ) -> Result<&mut Mesh, MeshVertexCompressionError> { let Some(values) = self.attribute(attr) else { - return Err(MeshVertexCompressionError::MissingAttribute(attr)); + return Err(MeshVertexCompressionError::MissingAttribute(attr.id)); }; if values.is_empty() { return Err(MeshVertexCompressionError::EmptyAttribute(attr)); @@ -1126,7 +1129,7 @@ impl Mesh { pub fn compress_normals(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { let mut attr = Mesh::ATTRIBUTE_NORMAL; let Some(values) = self.attribute(attr) else { - return Err(MeshVertexCompressionError::MissingAttribute(attr)); + return Err(MeshVertexCompressionError::MissingAttribute(attr.id)); }; attr.format = VertexFormat::Snorm16x2; let Some(values) = values.create_octahedral_encode_normals() else { @@ -1148,7 +1151,7 @@ impl Mesh { pub fn compress_tangents(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { let mut attr = Mesh::ATTRIBUTE_TANGENT; let Some(values) = self.attribute(attr) else { - return Err(MeshVertexCompressionError::MissingAttribute(attr)); + return Err(MeshVertexCompressionError::MissingAttribute(attr.id)); }; attr.format = VertexFormat::Snorm16x2; let Some(values) = values.create_octahedral_encode_tangents() else { @@ -1168,22 +1171,25 @@ impl Mesh { /// Quantize `Float32`, `Float32x2` or `Float32x4` vertex attribute to the type of `quantization`. pub fn quantize_float32_attribute( &mut self, - mut attr: MeshVertexAttribute, + attr_id: impl Into, quantization: AttributeQuantization, ) -> Result<&mut Mesh, MeshVertexCompressionError> { - let Some(values) = self.attribute(attr) else { - return Err(MeshVertexCompressionError::MissingAttribute(attr)); + let attr_id = attr_id.into(); + let Some(MeshAttributeData { attribute, values }) = + self.attributes.as_mut().unwrap().get_mut(&attr_id) + else { + return Err(MeshVertexCompressionError::MissingAttribute(attr_id)); }; - let Some(values) = values.create_quantized_values(quantization) else { + let Some(quantized_values) = values.create_quantized_values(quantization) else { return Err( MeshVertexCompressionError::UnsupportedAttributeForQuantizing { - attr, - provided: values.into(), + attr: *attribute, + provided: (&*values).into(), }, ); }; - attr.format = quantization.vertex_format::<4>(); - self.insert_attribute(attr, values); + attribute.format = quantization.vertex_format::<4>(); + *values = quantized_values; Ok(self) } @@ -3011,7 +3017,7 @@ impl MeshDeserializer { #[derive(Error, Debug, Clone)] pub enum MeshVertexCompressionError { #[error("Vertex attribute {0:?} doesn't exist")] - MissingAttribute(MeshVertexAttribute), + MissingAttribute(MeshVertexAttributeId), #[error("Vertex attribute {0:?} must not be empty")] EmptyAttribute(MeshVertexAttribute), #[error("Vertex attribute {attr:?} must have format {expected:?} before compressing/quantizing, but got {provided:?}")] From 0c14cb56cd6f0a3dc5d3ecd866a888e2ffd2344c Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Tue, 9 Jun 2026 23:15:04 +0800 Subject: [PATCH 4/9] Update comments --- crates/bevy_mesh/src/mesh.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 03d06c4fd4341..8739ce36f9405 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -308,7 +308,7 @@ impl MeshCompressionArgs { compress_indices: true, quantize_attributes: MeshAttributeQuantization { attributes: (&[ - // Unorm8 is chosen by default, as glTF vertex color is unorm8/unorm16 and HDR vertex color isn't common. + // Unorm8 is chosen by default as glTF vertex color is clamped to [0, 1] and HDR vertex color isn't common. (Mesh::ATTRIBUTE_COLOR.id, AttributeQuantization::Unorm8), ( Mesh::ATTRIBUTE_JOINT_WEIGHT.id, @@ -1194,7 +1194,7 @@ impl Mesh { } /// Quantize `Float32x4` colors to the type of `quantization`. - /// [`AttributeQuantization::Unorm8`] or [`AttributeQuantization::Float16`] (if you need HDR colors) is recommended. + /// [`AttributeQuantization::Unorm8`] is recommended if you don't need higher precision or floating-point range. pub fn quantize_colors( &mut self, quantization: AttributeQuantization, From 0884253fe9fcad27e7b83e9160df6adfa679e8df Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 10 Jun 2026 12:01:27 +0800 Subject: [PATCH 5/9] Fix quantized format --- crates/bevy_mesh/src/mesh.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 8739ce36f9405..af9168473c7c8 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -1188,7 +1188,7 @@ impl Mesh { }, ); }; - attribute.format = quantization.vertex_format::<4>(); + attribute.format = (&quantized_values).into(); *values = quantized_values; Ok(self) } From 1399b251f41def89037633cb47e12281fb221df9 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 10 Jun 2026 12:01:50 +0800 Subject: [PATCH 6/9] Add comments --- crates/bevy_mesh/src/mesh.rs | 39 +++++++++++++++++++++++++++++++--- crates/bevy_mesh/src/vertex.rs | 9 +++----- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index af9168473c7c8..ad1287680d1e2 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -1056,6 +1056,11 @@ impl Mesh { /// Compress positions and apply [`MeshAttributeCompressionFlags::COMPRESS_POSITION`]. /// See [`MeshAttributeCompressionFlags`] for the details. + /// + /// Return an error if [`Mesh::ATTRIBUTE_POSITION`] is missing or is empty or is not Float32x3. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. pub fn compress_positions(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { let mut attr = Mesh::ATTRIBUTE_POSITION; let Some(values) = self.attribute(attr) else { @@ -1108,6 +1113,11 @@ impl Mesh { /// Compress UV0 and apply [`MeshAttributeCompressionFlags::COMPRESS_UV0`]. /// See [`MeshAttributeCompressionFlags`] for the details. + /// + /// Return an error if [`Mesh::ATTRIBUTE_UV_0`] is missing or is empty or is not Float32x2. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. pub fn compress_uv0(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { self.compress_uvs(Mesh::ATTRIBUTE_UV_0, |mesh, uv_range| { mesh.final_uv_ranges[0] = Some(uv_range); @@ -1117,6 +1127,11 @@ impl Mesh { /// Compress UV1 and apply [`MeshAttributeCompressionFlags::COMPRESS_UV1`]. /// See [`MeshAttributeCompressionFlags`] for the details. + /// + /// Return an error if [`Mesh::ATTRIBUTE_UV_1`] is missing or is empty or is not Float32x2. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. pub fn compress_uv1(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { self.compress_uvs(Mesh::ATTRIBUTE_UV_1, |mesh, uv_range| { mesh.final_uv_ranges[1] = Some(uv_range); @@ -1126,6 +1141,11 @@ impl Mesh { /// Compress normals and apply [`MeshAttributeCompressionFlags::COMPRESS_NORMAL`]. /// See [`MeshAttributeCompressionFlags`] for the details. + /// + /// Return an error if [`Mesh::ATTRIBUTE_NORMAL`] is missing or is not Float32x3. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. pub fn compress_normals(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { let mut attr = Mesh::ATTRIBUTE_NORMAL; let Some(values) = self.attribute(attr) else { @@ -1148,6 +1168,11 @@ impl Mesh { /// Compress tangents and apply [`MeshAttributeCompressionFlags::COMPRESS_TANGENT`]. /// See [`MeshAttributeCompressionFlags`] for the details. + /// + /// Return an error if [`Mesh::ATTRIBUTE_TANGENT`] is missing or is not Float32x4. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. pub fn compress_tangents(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { let mut attr = Mesh::ATTRIBUTE_TANGENT; let Some(values) = self.attribute(attr) else { @@ -1168,7 +1193,12 @@ impl Mesh { Ok(self) } - /// Quantize `Float32`, `Float32x2` or `Float32x4` vertex attribute to the type of `quantization`. + /// Quantize `Float32`, `Float32x2` or `Float32x4` vertex attribute to the format of `quantization`. + /// + /// Return an error if `attr_id` is missing or is not `Float32`, `Float32x2` or `Float32x4`. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. pub fn quantize_float32_attribute( &mut self, attr_id: impl Into, @@ -1193,7 +1223,7 @@ impl Mesh { Ok(self) } - /// Quantize `Float32x4` colors to the type of `quantization`. + /// Quantize `Float32x4` colors to the format of `quantization` using [`Mesh::quantize_float32_attribute`]. /// [`AttributeQuantization::Unorm8`] is recommended if you don't need higher precision or floating-point range. pub fn quantize_colors( &mut self, @@ -1202,7 +1232,7 @@ impl Mesh { self.quantize_float32_attribute(Mesh::ATTRIBUTE_COLOR, quantization) } - /// Quantize `Float32x4` joint weights to the type of `quantization`. + /// Quantize `Float32x4` joint weights to the format of `quantization` using [`Mesh::quantize_float32_attribute`]. /// [`AttributeQuantization::Unorm16`] is recommended. pub fn quantize_joint_weights( &mut self, @@ -1229,6 +1259,9 @@ impl Mesh { /// Compress the mesh with [`MeshCompressionArgs`] using `Mesh::compress_*` and `Mesh::quantize_*` methods. /// Any failed compression is just ignored. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. pub fn compressed_mesh(mut self, args: MeshCompressionArgs) -> Mesh { if args .compress_attributes diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index cdff21b5cbacb..c83a2d52cabae 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -259,7 +259,7 @@ pub enum AttributeQuantization { impl AttributeQuantization { /// Create a [`VertexAttributeValues`] with `values` quantized to the format of this quantization. - pub fn quantize_f32_values( + pub(crate) fn quantize_f32_values( &self, values: &[[f32; N]], ) -> VertexAttributeValues { @@ -311,11 +311,6 @@ impl AttributeQuantization { } } } - - /// Get the vertex format of this quantization with N components. - pub fn vertex_format(&self) -> VertexFormat { - (&self.quantize_f32_values::(&[])).into() - } } /// Contains an array where each entry describes a property of a single vertex. @@ -593,6 +588,8 @@ impl VertexAttributeValues { } } + /// Create a new `VertexAttributeValues` with Float32 values quantized to the format of `quantization`. + /// Return None if the values are not Float32, Float32x2 or Float32x4. pub(crate) fn create_quantized_values( &self, quantization: AttributeQuantization, From 8e119b041449414853d776ac8845dd023f3cc4c0 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 10 Jun 2026 12:20:33 +0800 Subject: [PATCH 7/9] Test quantize_float32_attribute --- crates/bevy_mesh/src/mesh.rs | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 2 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index ad1287680d1e2..fafe80e4fa4b5 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -3102,7 +3102,10 @@ mod tests { #[cfg(feature = "serialize")] use super::SerializedMesh; use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; - use crate::{AttributeQuantization, MeshAttributeCompressionFlags, PrimitiveTopology}; + use crate::{ + AttributeQuantization, MeshAttributeCompressionFlags, MeshVertexAttribute, + PrimitiveTopology, + }; use bevy_asset::RenderAssetUsages; use bevy_math::bounding::{Aabb2d, Aabb3d}; use bevy_math::primitives::Triangle3d; @@ -3776,7 +3779,7 @@ mod tests { let VertexAttributeValues::Float16x4(color_f16) = mesh_color_f16.attribute(Mesh::ATTRIBUTE_COLOR).unwrap() else { - panic!("Color attribute is not compressed to Float16x4") + panic!("Color attribute is not quantized to Float16x4") }; assert!(color_f16 .iter() @@ -3851,4 +3854,80 @@ mod tests { Some(&Indices::U16(vec![0, 1, 2, 0, 3, 1, 0, 2, 3, 1, 3, 2])) ); } + + #[test] + fn quantize_mesh_float32_attributes() { + let f4 = MeshVertexAttribute::new("f32x4", 10, wgpu_types::VertexFormat::Float32x4); + let f2 = MeshVertexAttribute::new("f32x2", 11, wgpu_types::VertexFormat::Float32x2); + let f1 = MeshVertexAttribute::new("f32x1", 12, wgpu_types::VertexFormat::Float32); + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + f4, + vec![ + [1.0, 0.0, -2.0, 0.0], + [0.0, 5.0, 0.0, 0.0], + [0.0, 0.0, -1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + [0.1, -0.9, 0.0, 0.0], + [0.1, 0.2, -0.7, 0.0], + [0.1, 0.2, 0.4, 0.3], + ], + ) + .with_inserted_attribute( + f2, + vec![ + [10.0, 0.0], + [0.0, 1.0], + [-2.0, 0.0], + [-0.5, 0.0], + [0.1, 0.9], + [0.1, 0.2], + [0.1, 0.2], + ], + ) + .with_inserted_attribute(f1, vec![10.0, 0.0, 2.0, 0.0, 0.1, 0.1, 0.1]); + + mesh.quantize_float32_attribute(f4, AttributeQuantization::Unorm16) + .unwrap(); + assert_eq!( + mesh.attribute(f4), + Some(&VertexAttributeValues::Unorm16x4(vec![ + [65535, 0, 0, 0], + [0, 65535, 0, 0], + [0, 0, 0, 0], + [0, 0, 0, 65535], + [6554, 0, 0, 0], + [6554, 13107, 0, 0], + [6554, 13107, 26214, 19661] + ])) + ); + + mesh.quantize_float32_attribute(f2, AttributeQuantization::Snorm8) + .unwrap(); + assert_eq!( + mesh.attribute(f2), + Some(&VertexAttributeValues::Snorm8x2(vec![ + [127, 0], + [0, 127], + [-127, 0], + [-64, 0], + [13, 114], + [13, 25], + [13, 25] + ])) + ); + + mesh.quantize_float32_attribute(f1, AttributeQuantization::Float16) + .unwrap(); + let VertexAttributeValues::Float16(f16_values) = mesh.attribute(f1).unwrap() else { + panic!("Attribute f1 is not quantized to Float16") + }; + assert!(f16_values + .iter() + .zip([10.0, 0.0, 2.0, 0.0, 0.1, 0.1, 0.1].map(half::f16::from_f32)) + .all(|(a, b)| approx::relative_eq!(a.to_f32(), b.to_f32()))); + } } From 673e804de58d0102ba2d5a97c3e821e8f477d047 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Wed, 10 Jun 2026 13:28:51 +0800 Subject: [PATCH 8/9] Fix scene viewer and rename --- examples/stress_tests/many_cubes.rs | 6 +++--- examples/stress_tests/many_foxes.rs | 6 +++--- examples/stress_tests/many_morph_targets.rs | 6 +++--- examples/tools/scene_viewer/main.rs | 17 ++++++----------- 4 files changed, 15 insertions(+), 20 deletions(-) diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index bff6516d8d864..75cebf429792d 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -93,9 +93,9 @@ struct Args { #[argh(switch)] motion_blur: bool, - /// whether to enable vertex compression. + /// whether to enable mesh compression. #[argh(switch)] - vertex_compression: bool, + mesh_compression: bool, } #[derive(Default, Clone, PartialEq)] @@ -430,7 +430,7 @@ fn init_materials( } fn compress_mesh(args: &Args, mesh: impl Into) -> Mesh { - if args.vertex_compression { + if args.mesh_compression { mesh.into().compressed_mesh(MeshCompressionArgs::all()) } else { mesh.into() diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index e40843b5c6607..80b9affef6897 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -31,9 +31,9 @@ struct Args { #[argh(switch)] motion_blur: bool, - /// whether to enable vertex compression. + /// whether to enable mesh compression. #[argh(switch)] - vertex_compression: bool, + mesh_compression: bool, } #[derive(Resource)] @@ -65,7 +65,7 @@ fn main() { ..default() }) .set(GltfPlugin { - mesh_compression: if args.vertex_compression { + mesh_compression: if args.mesh_compression { MeshCompressionArgs::all() } else { MeshCompressionArgs::none() diff --git a/examples/stress_tests/many_morph_targets.rs b/examples/stress_tests/many_morph_targets.rs index ad3fd156578a8..81370bfe3fa75 100644 --- a/examples/stress_tests/many_morph_targets.rs +++ b/examples/stress_tests/many_morph_targets.rs @@ -132,9 +132,9 @@ struct Args { #[argh(switch)] motion_blur: bool, - /// whether to enable vertex compression. + /// whether to enable mesh compression. #[argh(switch)] - vertex_compression: bool, + mesh_compression: bool, } fn main() { @@ -158,7 +158,7 @@ fn main() { ..Default::default() }) .set(GltfPlugin { - mesh_compression: if args.vertex_compression { + mesh_compression: if args.mesh_compression { MeshCompressionArgs::all() } else { MeshCompressionArgs::none() diff --git a/examples/tools/scene_viewer/main.rs b/examples/tools/scene_viewer/main.rs index e515623ea584d..ad7ba069d91a1 100644 --- a/examples/tools/scene_viewer/main.rs +++ b/examples/tools/scene_viewer/main.rs @@ -16,7 +16,7 @@ use bevy::{ core_pipeline::prepass::{DeferredPrepass, DepthPrepass}, dev_tools::infinite_grid::{InfiniteGrid, InfiniteGridPlugin}, gltf::{convert_coordinates::GltfConvertCoordinates, GltfPlugin}, - mesh::MeshAttributeCompressionFlags, + mesh::MeshCompressionArgs, pbr::DefaultOpaqueRendererMethod, post_process::motion_blur::MotionBlur, prelude::*, @@ -61,12 +61,9 @@ struct Args { /// disables the infinite grid #[argh(switch)] no_infinite_grid: bool, - /// enable mesh attribute compression + /// enable mesh compression #[argh(switch)] - mesh_attribute_compression: bool, - /// enable mesh index compression - #[argh(switch)] - mesh_index_compression: bool, + mesh_compression: bool, /// enable motion blur #[argh(switch)] motion_blur: bool, @@ -113,13 +110,11 @@ fn main() { ..default() }) .set(GltfPlugin { - mesh_attribute_compression: if args.mesh_attribute_compression { - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + mesh_compression: if args.mesh_compression { + MeshCompressionArgs::all() } else { - MeshAttributeCompressionFlags::empty() + MeshCompressionArgs::none() }, - mesh_index_compression: args.mesh_index_compression, convert_coordinates: GltfConvertCoordinates { rotate_scene_entity: args.convert_scene_coordinates == Some(true), rotate_meshes: args.convert_mesh_coordinates == Some(true), From 25fd116f26c780c44829b5c8ed3fa1caa1cb6c4d Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Sat, 13 Jun 2026 11:31:49 +0800 Subject: [PATCH 9/9] address suggestion --- crates/bevy_gltf/src/loader/mod.rs | 2 +- crates/bevy_mesh/src/mesh.rs | 44 ++++++++------------- examples/stress_tests/many_cubes.rs | 2 +- examples/stress_tests/many_foxes.rs | 2 +- examples/stress_tests/many_morph_targets.rs | 2 +- examples/tools/scene_viewer/main.rs | 2 +- 6 files changed, 22 insertions(+), 32 deletions(-) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 3da8cf15345ac..5a271d40d0813 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -162,7 +162,7 @@ pub struct GltfLoader { /// The default policy for skinned mesh bounds. Can be overridden by /// [`GltfLoaderSettings::skinned_mesh_bounds_policy`]. pub default_skinned_mesh_bounds_policy: GltfSkinnedMeshBoundsPolicy, - /// Default Mesh compression arguments for the loaded meshes. + /// Default mesh compression arguments for the loaded meshes. pub default_mesh_compression: MeshCompressionArgs, } diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index fafe80e4fa4b5..b71e92b4e4680 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -13,6 +13,7 @@ use crate::morph::MorphAttributes; use crate::AttributeQuantization; #[cfg(feature = "serialize")] use crate::SerializedMeshAttributeData; +use alloc::borrow::Cow; use alloc::collections::BTreeMap; use bevy_asset::{Asset, RenderAssetUsages}; use bevy_math::{ @@ -262,7 +263,7 @@ pub struct Mesh { /// Indicate whether vertex attributes are compressed. attribute_compression: MeshAttributeCompressionFlags, /// Precomputed min and max extents of the mesh position data. Used mainly for constructing `Aabb`s for frustum culling and decompressing vertex positions. - /// This data will be set if/when a mesh is extracted to the GPU or compressing positions. + /// This data will be set if/when a mesh is extracted to the GPU or when compressing positions. pub final_aabb: Option, /// Precomputed min and max extents of the mesh UV channels data. Used mainly for decompressing vertex UVs. /// This will be set when compressing UVs. @@ -270,13 +271,6 @@ pub struct Mesh { skinned_mesh_bounds: Option, } -/// Quantization arguments of the mesh. -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] -pub struct MeshAttributeQuantization { - pub attributes: alloc::borrow::Cow<'static, [(MeshVertexAttributeId, AttributeQuantization)]>, -} - /// Mesh compression arguments used in [`Mesh::compressed_mesh`]. #[derive(Debug, Clone)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] @@ -284,7 +278,7 @@ pub struct MeshCompressionArgs { /// Whether to compress indices to [`Indices::U16`] when possible. pub compress_indices: bool, /// Quantize Float32 to smaller types for some attributes. No change is needed in shaders. - pub quantize_attributes: MeshAttributeQuantization, + pub quantize_attributes: Cow<'static, [(MeshVertexAttributeId, AttributeQuantization)]>, /// Compress some attributes to smaller representation. Shaders need change to decode them correctly. /// See [`MeshAttributeCompressionFlags`] for the details. pub compress_attributes: MeshAttributeCompressionFlags, @@ -295,28 +289,24 @@ impl MeshCompressionArgs { pub fn none() -> Self { Self { compress_indices: false, - quantize_attributes: MeshAttributeQuantization { - attributes: (&[]).into(), - }, + quantize_attributes: (&[]).into(), compress_attributes: MeshAttributeCompressionFlags::empty(), } } - /// All compression is enabled, with colors quantized to unorm8 and joint weights quantized to unorm16. - pub fn all() -> Self { + /// Compression with all attributes compressed and with colors quantized to unorm8 and joint weights quantized to unorm16. + pub fn regular() -> Self { Self { compress_indices: true, - quantize_attributes: MeshAttributeQuantization { - attributes: (&[ - // Unorm8 is chosen by default as glTF vertex color is clamped to [0, 1] and HDR vertex color isn't common. - (Mesh::ATTRIBUTE_COLOR.id, AttributeQuantization::Unorm8), - ( - Mesh::ATTRIBUTE_JOINT_WEIGHT.id, - AttributeQuantization::Unorm16, - ), - ]) - .into(), - }, + quantize_attributes: (&[ + // Unorm8 is chosen by default as glTF vertex color is clamped to [0, 1] and HDR vertex color isn't common. + (Mesh::ATTRIBUTE_COLOR.id, AttributeQuantization::Unorm8), + ( + Mesh::ATTRIBUTE_JOINT_WEIGHT.id, + AttributeQuantization::Unorm16, + ), + ]) + .into(), compress_attributes: MeshAttributeCompressionFlags::all(), } } @@ -326,7 +316,7 @@ bitflags::bitflags! { /// If the corresponding attribute compression is enabled: /// - Position will be Snorm16x4 relative to the mesh's AABB. The w component is unused. /// - Normal and tangent will be Snorm16x2 with octahedral encoding, using [`octahedral_encode_signed`] and [`octahedral_encode_tangent`]. - /// - UV0 and UV1 will be Unorm16x2. UVs are remapped based on their min/max values so them can go beyond [0, 1], though a larger range will reduce precision. + /// - UV0 and UV1 will be Unorm16x2. UVs are remapped based on their min/max values so they can go beyond [0, 1], though a larger range will reduce precision. /// /// [`octahedral_encode_signed`]: crate::vertex::octahedral_encode_signed /// [`octahedral_encode_tangent`]: crate::vertex::octahedral_encode_tangent @@ -1293,7 +1283,7 @@ impl Mesh { { let _ = self.compress_uv1(); } - for (attr, quantization) in args.quantize_attributes.attributes.iter().copied() { + for (attr, quantization) in args.quantize_attributes.iter().copied() { let _ = self.quantize_float32_attribute(attr, quantization); } if args.compress_indices { diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index 75cebf429792d..0da4db9043e46 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -431,7 +431,7 @@ fn init_materials( fn compress_mesh(args: &Args, mesh: impl Into) -> Mesh { if args.mesh_compression { - mesh.into().compressed_mesh(MeshCompressionArgs::all()) + mesh.into().compressed_mesh(MeshCompressionArgs::regular()) } else { mesh.into() } diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index 80b9affef6897..f1ddf180e86f5 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -66,7 +66,7 @@ fn main() { }) .set(GltfPlugin { mesh_compression: if args.mesh_compression { - MeshCompressionArgs::all() + MeshCompressionArgs::regular() } else { MeshCompressionArgs::none() }, diff --git a/examples/stress_tests/many_morph_targets.rs b/examples/stress_tests/many_morph_targets.rs index 81370bfe3fa75..0b7db8f94637b 100644 --- a/examples/stress_tests/many_morph_targets.rs +++ b/examples/stress_tests/many_morph_targets.rs @@ -159,7 +159,7 @@ fn main() { }) .set(GltfPlugin { mesh_compression: if args.mesh_compression { - MeshCompressionArgs::all() + MeshCompressionArgs::regular() } else { MeshCompressionArgs::none() }, diff --git a/examples/tools/scene_viewer/main.rs b/examples/tools/scene_viewer/main.rs index ad7ba069d91a1..4078e17de231f 100644 --- a/examples/tools/scene_viewer/main.rs +++ b/examples/tools/scene_viewer/main.rs @@ -111,7 +111,7 @@ fn main() { }) .set(GltfPlugin { mesh_compression: if args.mesh_compression { - MeshCompressionArgs::all() + MeshCompressionArgs::regular() } else { MeshCompressionArgs::none() },