From e2590f7278c8c580a143483872d2ab3063666e41 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Sat, 13 Jun 2026 11:38:39 +0800 Subject: [PATCH 1/7] Refactor mesh compression methods --- crates/bevy_gltf/src/lib.rs | 15 +- crates/bevy_gltf/src/loader/mod.rs | 26 +- crates/bevy_mesh/Cargo.toml | 2 +- crates/bevy_mesh/src/mesh.rs | 957 +++++++++++------- crates/bevy_mesh/src/vertex.rs | 164 +-- 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 | 14 +- examples/stress_tests/many_foxes.rs | 13 +- examples/stress_tests/many_morph_targets.rs | 13 +- examples/tools/scene_viewer/main.rs | 17 +- 12 files changed, 763 insertions(+), 494 deletions(-) diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index ead576cb1992d..75c103bc40704 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.clone(), }); } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 14554608931d3..ec64479da0bb6 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,9 @@ 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 + .clone() + .unwrap_or(loader.default_mesh_compression.clone()), ), ); 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..b71e92b4e4680 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -8,11 +8,12 @@ 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::borrow::Cow; use alloc::collections::BTreeMap; use bevy_asset::{Asset, RenderAssetUsages}; use bevy_math::{ @@ -262,21 +263,63 @@ 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 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 calling [`Mesh::compressed_mesh`]. + /// This will be set when compressing UVs. pub final_uv_ranges: [Option; 2], skinned_mesh_bounds: Option, } +/// Mesh compression arguments used in [`Mesh::compressed_mesh`]. +#[derive(Debug, Clone)] +#[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: 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, +} + +impl MeshCompressionArgs { + /// None of compression/quantization is enabled. + pub fn none() -> Self { + Self { + compress_indices: false, + quantize_attributes: (&[]).into(), + compress_attributes: MeshAttributeCompressionFlags::empty(), + } + } + + /// 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: (&[ + // 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(), + } + } +} + 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). - /// - 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. + /// - 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 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 #[repr(transparent)] #[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] @@ -288,33 +331,9 @@ 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`] /// or [`Mesh::with_inserted_attribute`]. @@ -1025,244 +1044,251 @@ 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. + /// + /// 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 { + return Err(MeshVertexCompressionError::MissingAttribute(attr.id)); + }; + if values.is_empty() { + return Err(MeshVertexCompressionError::EmptyAttribute(attr)); } + let Some(aabb) = Self::compute_aabb(values) else { + 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()); + 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.id)); + }; + if values.is_empty() { + return Err(MeshVertexCompressionError::EmptyAttribute(attr)); } + let Some(uv_range) = Self::compute_uv_range(values) else { + 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()); + 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. + /// 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. /// - /// See [`MeshAttributeCompressionFlags`] for more context. - /// if vertex attributes are already compressed, they are unchanged and won't decompress. + /// # 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); + mesh.attribute_compression |= MeshAttributeCompressionFlags::COMPRESS_UV0; + }) + } + + /// Compress UV1 and apply [`MeshAttributeCompressionFlags::COMPRESS_UV1`]. + /// See [`MeshAttributeCompressionFlags`] for the details. /// - /// If `index_compression` is true and indices are u32 and vertex count <= 65535, indices will be converted to u16, otherwise it does nothing. + /// 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 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_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. + /// + /// 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 { + return Err(MeshVertexCompressionError::MissingAttribute(attr.id)); + }; + attr.format = VertexFormat::Snorm16x2; + let Some(values) = values.create_octahedral_encode_normals() else { + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + 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. + /// + /// 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 { + return Err(MeshVertexCompressionError::MissingAttribute(attr.id)); + }; + attr.format = VertexFormat::Snorm16x2; + let Some(values) = values.create_octahedral_encode_tangents() else { + 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 `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, + quantization: AttributeQuantization, + ) -> Result<&mut Mesh, MeshVertexCompressionError> { + 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(quantized_values) = values.create_quantized_values(quantization) else { + return Err( + MeshVertexCompressionError::UnsupportedAttributeForQuantizing { + attr: *attribute, + provided: (&*values).into(), + }, + ); + }; + attribute.format = (&quantized_values).into(); + *values = quantized_values; + Ok(self) + } + + /// 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, + quantization: AttributeQuantization, + ) -> Result<&mut Mesh, MeshVertexCompressionError> { + self.quantize_float32_attribute(Mesh::ATTRIBUTE_COLOR, 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, + quantization: AttributeQuantization, + ) -> Result<&mut Mesh, MeshVertexCompressionError> { + 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. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. + 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. + /// + /// # 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 + .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(); + } + for (attr, quantization) in args.quantize_attributes.iter().copied() { + let _ = self.quantize_float32_attribute(attr, quantization); + } + if args.compress_indices { + self.compress_indices(); } - self } @@ -3010,6 +3036,26 @@ 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(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:?}")] + 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`] #[derive(Error, Debug, Clone)] pub enum MeshMergeDuplicateVerticesError { @@ -3046,11 +3092,14 @@ mod tests { #[cfg(feature = "serialize")] use super::SerializedMesh; use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; - use crate::{MeshAttributeCompressionFlags, MeshVertexAttribute, PrimitiveTopology}; + use crate::{ + AttributeQuantization, MeshAttributeCompressionFlags, MeshVertexAttribute, + 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 +3559,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 +3572,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 +3594,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_compressed_all.attribute(Mesh::ATTRIBUTE_NORMAL), + 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.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 +3758,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 quantized to Float16x4") }; assert!(color_f16 .iter() @@ -3677,4 +3786,138 @@ 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])) + ); + } + + #[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()))); + } } diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index c638247e423c6..c83a2d52cabae 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,73 @@ 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(crate) 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!(), + } + } + } + } +} + /// Contains an array where each entry describes a property of a single vertex. /// Matches the [`VertexFormats`](VertexFormat). #[derive(Clone, Debug, EnumVariantMeta, PartialEq)] @@ -521,50 +588,21 @@ 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)) + /// 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, + ) -> 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 +641,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 +1160,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 +1185,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 +1198,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 +1222,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 +1243,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 +1271,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..0da4db9043e46 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::{ @@ -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,12 +430,8 @@ 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, - ) + if args.mesh_compression { + 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 2dfd082a6770c..f1ddf180e86f5 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}, @@ -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,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.mesh_compression { + MeshCompressionArgs::regular() } 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..0b7db8f94637b 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}, @@ -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,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.mesh_compression { + MeshCompressionArgs::regular() } else { - MeshAttributeCompressionFlags::empty() + MeshCompressionArgs::none() }, ..default() }), diff --git a/examples/tools/scene_viewer/main.rs b/examples/tools/scene_viewer/main.rs index e515623ea584d..4078e17de231f 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::regular() } 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 7f956a51eaa6308508f9c1cf649b7fd0896a3173 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Sat, 13 Jun 2026 15:09:29 +0800 Subject: [PATCH 2/7] Add tangent angle encoding to mesh compression --- crates/bevy_light/src/spot_light.rs | 4 +- crates/bevy_mesh/src/mesh.rs | 106 +++++++++++++++++- crates/bevy_mesh/src/vertex.rs | 90 ++++++++++++++- crates/bevy_pbr/src/prepass/mod.rs | 9 ++ crates/bevy_pbr/src/prepass/prepass_io.wgsl | 68 +++++++---- crates/bevy_pbr/src/render/forward_io.wgsl | 54 ++++++--- crates/bevy_pbr/src/render/mesh.rs | 10 ++ crates/bevy_render/src/maths.wgsl | 24 ++-- crates/bevy_render/src/utils.wgsl | 9 ++ crates/bevy_sprite_render/src/mesh2d/mesh.rs | 10 ++ .../src/mesh2d/mesh2d_vertex_input.wgsl | 55 ++++++--- 11 files changed, 362 insertions(+), 77 deletions(-) diff --git a/crates/bevy_light/src/spot_light.rs b/crates/bevy_light/src/spot_light.rs index f770e696ca788..0be319558da17 100644 --- a/crates/bevy_light/src/spot_light.rs +++ b/crates/bevy_light/src/spot_light.rs @@ -177,7 +177,7 @@ impl Default for SpotLight { // so we reproduce it here to avoid a mismatch if glam changes. // See bevy_render/maths.wgsl:orthonormalize pub fn orthonormalize(z_basis: Dir3) -> Mat3 { - let sign = 1f32.copysign(z_basis.z); + let sign = if z_basis.z >= 0.0 { 1.0 } else { -1.0 }; let a = -1.0 / (sign + z_basis.z); let b = z_basis.x * z_basis.y * a; let x_basis = Vec3::new( @@ -206,7 +206,7 @@ pub fn spot_light_clip_from_view(angle: f32, near_z: f32) -> Mat4 { } /// Add to a [`SpotLight`] to add a light texture effect. -/// A texture mask is applied to the light source to modulate its intensity, +/// A texture mask is applied to the light source to modulate its intensity, /// simulating patterns like window shadows, gobo/cookie effects, or soft falloffs. #[derive(Clone, Component, Debug, Reflect, FromTemplate)] #[reflect(Component, Debug)] diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index b71e92b4e4680..f87946c9972be 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -10,9 +10,9 @@ use super::{ }; #[cfg(feature = "morph")] use crate::morph::MorphAttributes; -use crate::AttributeQuantization; #[cfg(feature = "serialize")] use crate::SerializedMeshAttributeData; +use crate::{arr_f32_to_snorm16, encode_tangent_angle, AttributeQuantization}; use alloc::borrow::Cow; use alloc::collections::BTreeMap; use bevy_asset::{Asset, RenderAssetUsages}; @@ -326,11 +326,12 @@ bitflags::bitflags! { #[reflect(opaque)] #[reflect(Hash, Clone, PartialEq, Debug)] pub struct MeshAttributeCompressionFlags: u8 { - const COMPRESS_POSITION = 1 << 0; - const COMPRESS_NORMAL = 1 << 1; - const COMPRESS_TANGENT = 1 << 2; - const COMPRESS_UV0 = 1 << 3; - const COMPRESS_UV1 = 1 << 4; + const COMPRESS_POSITION = 1 << 0; + const COMPRESS_NORMAL = 1 << 1; + const COMPRESS_TANGENT = 1 << 2; + const COMPRESS_UV0 = 1 << 3; + const COMPRESS_UV1 = 1 << 4; + const PACKED_TANGENT_ANGLE = 1 << 5; } } @@ -1183,6 +1184,90 @@ impl Mesh { Ok(self) } + /// Compress tangents and apply [`MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE`]. + /// See [`MeshAttributeCompressionFlags`] for the details. + /// + /// Return an error if: + /// - [`Mesh::ATTRIBUTE_POSITION`] is missing or is not already compressed to Snorm16x4. + /// - [`Mesh::ATTRIBUTE_NORMAL`] is missing or is not Float32x3. + /// - [`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_to_angles(&mut self) -> Result<&mut Mesh, MeshVertexCompressionError> { + let vertex_count = self.count_vertices(); + let Some(positions) = self.attribute_mut(Mesh::ATTRIBUTE_POSITION) else { + return Err(MeshVertexCompressionError::MissingAttribute( + Mesh::ATTRIBUTE_POSITION.id, + )); + }; + let VertexAttributeValues::Snorm16x4(positions) = positions else { + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + attr: Mesh::ATTRIBUTE_POSITION, + expected: VertexFormat::Snorm16x4, + provided: (&*positions).into(), + }, + ); + }; + let mut positions = core::mem::take(positions); + fn return_pos(mesh: &mut Mesh, positions: Vec<[i16; 4]>) { + let mut attr = Mesh::ATTRIBUTE_POSITION; + attr.format = VertexFormat::Snorm16x4; + mesh.insert_attribute(attr, VertexAttributeValues::Snorm16x4(positions)); + } + + let Some(tangents) = self.attribute(Mesh::ATTRIBUTE_TANGENT) else { + return_pos(self, positions); + return Err(MeshVertexCompressionError::MissingAttribute( + Mesh::ATTRIBUTE_TANGENT.id, + )); + }; + + let VertexAttributeValues::Float32x4(tangents) = tangents else { + let provided = tangents.into(); + return_pos(self, positions); + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + attr: Mesh::ATTRIBUTE_TANGENT, + expected: Mesh::ATTRIBUTE_TANGENT.format, + provided, + }, + ); + }; + + let Some(normals) = self.attribute(Mesh::ATTRIBUTE_NORMAL) else { + return_pos(self, positions); + return Err(MeshVertexCompressionError::MissingAttribute( + Mesh::ATTRIBUTE_NORMAL.id, + )); + }; + + let VertexAttributeValues::Float32x3(normals) = normals else { + let provided = (&*normals).into(); + return_pos(self, positions); + return Err( + MeshVertexCompressionError::UnsupportedAttributeForCompression { + attr: Mesh::ATTRIBUTE_NORMAL, + expected: Mesh::ATTRIBUTE_NORMAL.format, + provided, + }, + ); + }; + + for i in 0..vertex_count { + let tangent = tangents[i]; + let normal = normals[i]; + let angle = + encode_tangent_angle(tangent.into(), normal.into()) / core::f32::consts::TAU; + positions[i][3] = arr_f32_to_snorm16([angle])[0]; + } + return_pos(self, positions); + self.remove_attribute(Mesh::ATTRIBUTE_TANGENT); + self.attribute_compression |= MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE; + Ok(self) + } + /// 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`. @@ -1259,6 +1344,12 @@ impl Mesh { { let _ = self.compress_positions(); } + if args + .compress_attributes + .contains(MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE) + { + let _ = self.compress_tangents_to_angles(); + } if args .compress_attributes .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) @@ -1268,6 +1359,9 @@ impl Mesh { if args .compress_attributes .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) + && !args + .compress_attributes + .contains(MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE) { let _ = self.compress_tangents(); } diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index c83a2d52cabae..85887605d479b 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}, - ops, vec2, Vec2, Vec3, Vec3A, Vec3Swizzles, + ops, vec2, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, }; #[cfg(feature = "serialize")] use bevy_platform::collections::HashMap; @@ -1229,12 +1229,52 @@ pub fn octahedral_decode_tangent(v: Vec2) -> (Vec3, f32) { (octahedral_decode_signed(f), sign) } +/// Matches bevy_render/maths.wgsl `orthonormal_y_axis` +fn orthonormal_y_axis(z_basis: Vec3) -> Vec3 { + let sign = if z_basis.z >= 0.0 { 1.0 } else { -1.0 }; + let a = -1.0 / (sign + z_basis.z); + let b = z_basis.x * z_basis.y * a; + // let x_basis = vec3f(1.0 + sign * z_basis.x * z_basis.x * a, sign * b, -sign * z_basis.x); + let y_basis = Vec3::new(b, sign + z_basis.y * z_basis.y * a, -z_basis.y); + return y_basis; +} + +/// Encode tangent to angle. The angle is [-2pi, 2pi], where the sign represents the orientation of the tangent. +pub fn encode_tangent_angle(tangent: Vec4, normal: Vec3) -> f32 { + // Bias to ensure that encoding as snorm16 preserves the sign. + let bits = 16.; + let bias = 1. / (ops::powf(2.0, bits - 1.) - 1.); + + let orientation = tangent.w.signum(); + let t0 = orthonormal_y_axis(normal); + let tangent = tangent.xyz(); + let angle = t0.angle_between(tangent).max(bias * core::f32::consts::TAU); + + orientation * { + if t0.cross(tangent).dot(normal) >= 0.0 { + angle + } else { + core::f32::consts::TAU - angle + } + } +} + +/// Decode angle to tangent. The angle is [-2pi, 2pi], where the sign represents the orientation of the tangent. +pub fn decode_tangent_angle(tangent_angle: f32, normal: Vec3) -> Vec4 { + let orientation = tangent_angle.signum(); + let angle = tangent_angle * orientation; + let t0 = orthonormal_y_axis(normal); + let tangent = t0 * angle.cos() + normal.cross(t0) * angle.sin(); + tangent.extend(orientation) +} + #[cfg(test)] mod tests { use bevy_math::{vec2, vec3, Vec4Swizzles}; use crate::{ - octahedral_decode_signed, octahedral_decode_tangent, + decode_tangent_angle, encode_tangent_angle, octahedral_decode_signed, + octahedral_decode_tangent, vertex::{octahedral_encode_signed, octahedral_encode_tangent}, }; @@ -1274,4 +1314,50 @@ mod tests { assert!(decoded_tangent.distance(v.xyz()) < 1e-7); } } + + #[test] + fn tangent_angle_encode_decode() { + let normal_tangent = [ + (vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0).extend(1.0)), + (vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0).extend(-1.0)), + (vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0).extend(1.0)), + (vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0).extend(-1.0)), + (vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0).extend(1.0)), + (vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0).extend(1.0)), + ( + vec3(1.0, 1.0, 1.0).normalize(), + vec3(1.0, -1.0, 0.0).normalize().extend(1.0), + ), + ( + vec3(1.0, 2.0, 0.0).normalize(), + vec3(0.0, 0.0, 1.0).extend(-1.0), + ), + ( + vec3(0.0, 1.0, 1.0).normalize(), + vec3(1.0, 0.0, 0.0).extend(1.0), + ), + ( + vec3(-1.0, 1.0, 1.0).normalize(), + vec3(1.0, 1.0, 0.0).normalize().extend(-1.0), + ), + ( + vec3(3.0, 1.0, 2.0).normalize(), + vec3(0.0, 1.0, -0.5).normalize().extend(1.0), + ), + ]; + + let expected_angle = [ + 1.5707963, -1.5707963, 4.712389, -4.712389, 3.1415927, 3.1415927, 3.926991, -2.6779451, + 4.712389, -5.4977875, 6.0473375, + ]; + + for (i, &(normal, tangent)) in normal_tangent.iter().enumerate() { + let angle = encode_tangent_angle(tangent, normal); + assert_eq!(angle.signum(), tangent.w.signum()); + approx::assert_relative_eq!(angle, expected_angle[i]); + + let decoded_tangent = decode_tangent_angle(angle, normal); + assert!(decoded_tangent.distance(tangent) < 1e-6); + } + } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 85a7f813ba448..2409e7bb2061c 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -535,6 +535,15 @@ impl PrepassPipeline { } vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); } + if layout + .0 + .get_attribute_compression() + .contains(MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE) + { + // The mesh has tangents but doesn't have `Mesh::ATTRIBUTE_TANGENT` + shader_defs.push("VERTEX_TANGENTS".into()); + shader_defs.push("VERTEX_PACKED_TANGENT_ANGLE".into()); + } } if mesh_key .intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS | MeshPipelineKey::DEFERRED_PREPASS) diff --git a/crates/bevy_pbr/src/prepass/prepass_io.wgsl b/crates/bevy_pbr/src/prepass/prepass_io.wgsl index bf01e84d45c4e..5cb0487d90309 100644 --- a/crates/bevy_pbr/src/prepass/prepass_io.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -39,25 +39,27 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, + +#ifdef VERTEX_PACKED_TANGENT_ANGLE + @location(0) compressed_position_tangent_angle: vec4, +#else // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_POSITIONS_COMPRESSED @location(0) compressed_position: vec4, #else @location(0) position: vec3, #endif -#ifdef VERTEX_UVS_A -#ifdef VERTEX_UVS_A_COMPRESSED - @location(1) compressed_uv: vec2, -#else - @location(1) uv: vec2, -#endif -#endif -#ifdef VERTEX_UVS_B -#ifdef VERTEX_UVS_B_COMPRESSED - @location(2) compressed_uv_b: vec2, +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + @location(4) compressed_tangent: vec2, #else - @location(2) uv_b: vec2, + @location(4) tangent: vec4, #endif #endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#endif // VERTEX_PACKED_TANGENT_ANGLE #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef VERTEX_NORMALS @@ -67,14 +69,22 @@ struct Vertex { @location(3) normal: vec3, #endif #endif -#ifdef VERTEX_TANGENTS -#ifdef VERTEX_TANGENTS_COMPRESSED - @location(4) compressed_tangent: vec2, +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef VERTEX_UVS_A +#ifdef VERTEX_UVS_A_COMPRESSED + @location(1) compressed_uv: vec2, #else - @location(4) tangent: vec4, + @location(1) uv: vec2, +#endif +#endif +#ifdef VERTEX_UVS_B +#ifdef VERTEX_UVS_B_COMPRESSED + @location(2) compressed_uv_b: vec2, +#else + @location(2) uv_b: vec2, #endif #endif -#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef SKINNED @location(5) joint_indices: vec4, @@ -96,11 +106,8 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert let mesh_metadata = bevy_pbr::mesh_functions::get_metadata(instance_index); var uncompressed_vertex: UncompressedVertex; uncompressed_vertex.instance_index = instance_index; -#ifdef VERTEX_POSITIONS_COMPRESSED - uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); -#else - uncompressed_vertex.position = vertex_in.position; -#endif + +// Decompress normal before tangent angle #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED @@ -109,6 +116,22 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.normal = vertex_in.normal; #endif #endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef VERTEX_PACKED_TANGENT_ANGLE + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position_tangent_angle, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + uncompressed_vertex.tangent = bevy_render::utils::decode_tangent_angle(vertex_in.compressed_position_tangent_angle.w, uncompressed_vertex.normal); +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#else // VERTEX_PACKED_TANGENT_ANGLE + +#ifdef VERTEX_POSITIONS_COMPRESSED + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); +#else + uncompressed_vertex.position = vertex_in.position; +#endif +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED uncompressed_vertex.tangent = bevy_render::utils::decompress_vertex_tangent(vertex_in.compressed_tangent); @@ -117,6 +140,9 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert #endif #endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#endif // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_UVS_A #ifdef VERTEX_UVS_A_COMPRESSED let uv_min_and_extents_a = mesh_metadata.uv_channels_min_and_extents[0]; diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 5567a63eda085..5619452d90911 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -31,6 +31,11 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, + +#ifdef VERTEX_PACKED_TANGENT_ANGLE + @location(0) compressed_position_tangent_angle: vec4, +#else // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_POSITIONS #ifdef VERTEX_POSITIONS_COMPRESSED @location(0) compressed_position: vec4, @@ -38,6 +43,15 @@ struct Vertex { @location(0) position: vec3, #endif #endif +#ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + @location(4) compressed_tangent: vec2, +#else + @location(4) tangent: vec4, +#endif +#endif +#endif // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED @location(1) compressed_normal: vec2, @@ -59,13 +73,6 @@ struct Vertex { @location(3) uv_b: vec2, #endif #endif -#ifdef VERTEX_TANGENTS -#ifdef VERTEX_TANGENTS_COMPRESSED - @location(4) compressed_tangent: vec2, -#else - @location(4) tangent: vec4, -#endif -#endif #ifdef VERTEX_COLORS @location(5) color: vec4, #endif @@ -86,6 +93,21 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert let mesh_metadata = bevy_pbr::mesh_functions::get_metadata(instance_index); var uncompressed_vertex: UncompressedVertex; uncompressed_vertex.instance_index = instance_index; + +// Decompress normal before tangent angle +#ifdef VERTEX_NORMALS +#ifdef VERTEX_NORMALS_COMPRESSED + uncompressed_vertex.normal = bevy_render::utils::decompress_vertex_normal(vertex_in.compressed_normal); +#else + uncompressed_vertex.normal = vertex_in.normal; +#endif +#endif + +#ifdef VERTEX_PACKED_TANGENT_ANGLE + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position_tangent_angle, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); + uncompressed_vertex.tangent = bevy_render::utils::decode_tangent_angle(vertex_in.compressed_position_tangent_angle.w, uncompressed_vertex.normal); +#else // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_POSITIONS #ifdef VERTEX_POSITIONS_COMPRESSED uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); @@ -93,13 +115,16 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.position = vertex_in.position; #endif #endif -#ifdef VERTEX_NORMALS -#ifdef VERTEX_NORMALS_COMPRESSED - uncompressed_vertex.normal = bevy_render::utils::decompress_vertex_normal(vertex_in.compressed_normal); +#ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + uncompressed_vertex.tangent = bevy_render::utils::decompress_vertex_tangent(vertex_in.compressed_tangent); #else - uncompressed_vertex.normal = vertex_in.normal; + uncompressed_vertex.tangent = vertex_in.tangent; #endif #endif + +#endif // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_UVS_A #ifdef VERTEX_UVS_A_COMPRESSED let uv_min_and_extents_a = mesh_metadata.uv_channels_min_and_extents[0]; @@ -116,13 +141,6 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.uv_b = vertex_in.uv_b; #endif #endif -#ifdef VERTEX_TANGENTS -#ifdef VERTEX_TANGENTS_COMPRESSED - uncompressed_vertex.tangent = bevy_render::utils::decompress_vertex_tangent(vertex_in.compressed_tangent); -#else - uncompressed_vertex.tangent = vertex_in.tangent; -#endif -#endif #ifdef VERTEX_COLORS uncompressed_vertex.color = vertex_in.color; #endif diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 6a90c9793f4f1..27b85ca087bdb 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -3393,6 +3393,16 @@ impl SpecializedMeshPipeline for MeshPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(5)); } + if layout + .0 + .get_attribute_compression() + .contains(MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE) + { + // The mesh has tangents but doesn't have `Mesh::ATTRIBUTE_TANGENT` + shader_defs.push("VERTEX_TANGENTS".into()); + shader_defs.push("VERTEX_PACKED_TANGENT_ANGLE".into()); + } + if cfg!(feature = "pbr_transmission_textures") { shader_defs.push("PBR_TRANSMISSION_TEXTURES_SUPPORTED".into()); } diff --git a/crates/bevy_render/src/maths.wgsl b/crates/bevy_render/src/maths.wgsl index 40dfebf892d54..f512f21defbb8 100644 --- a/crates/bevy_render/src/maths.wgsl +++ b/crates/bevy_render/src/maths.wgsl @@ -63,12 +63,6 @@ fn mat4x4_to_mat3x3(m: mat4x4) -> mat3x3 { return mat3x3(m[0].xyz, m[1].xyz, m[2].xyz); } -// Copy the sign bit from B onto A. -// copysign allows proper handling of negative zero to match the rust implementation of orthonormalize -fn copysign(a: f32, b: f32) -> f32 { - return bitcast((bitcast(a) & 0x7FFFFFFF) | (bitcast(b) & 0x80000000)); -} - // Constructs a right-handed orthonormal basis from a given unit Z vector. // // NOTE: requires unit-length (normalized) input to function properly. @@ -78,7 +72,7 @@ fn copysign(a: f32, b: f32) -> f32 { // the construction of the orthonormal basis up and right vectors here needs to precisely match the rust // implementation in bevy_light/spot_light.rs:spot_light_world_from_view fn orthonormalize(z_basis: vec3) -> mat3x3 { - let sign = copysign(1.0, z_basis.z); + let sign = select(-1.0, 1.0, z_basis.z >= 0.0); let a = -1.0 / (sign + z_basis.z); let b = z_basis.x * z_basis.y * a; let x_basis = vec3(1.0 + sign * z_basis.x * z_basis.x * a, sign * b, -sign * z_basis.x); @@ -86,6 +80,16 @@ fn orthonormalize(z_basis: vec3) -> mat3x3 { return mat3x3(x_basis, y_basis, z_basis); } +// Like `orthonormalize`, but only returns the orthonormal y axis. +fn orthonormal_y_axis(z_basis: vec3f) -> vec3f { + let sign = select(-1.0, 1.0, z_basis.z >= 0.0); + let a = -1.0 / (sign + z_basis.z); + let b = z_basis.x * z_basis.y * a; + // let x_basis = vec3f(1.0 + sign * z_basis.x * z_basis.x * a, sign * b, -sign * z_basis.x); + let y_basis = vec3f(b, sign + z_basis.y * z_basis.y * a, -z_basis.y); + return y_basis; +} + // Returns true if any part of a sphere is on the positive side of a plane. // // `sphere_center.w` should be 1.0. @@ -109,15 +113,15 @@ fn sphere_intersects_plane_half_space( // Returns vec2(t0, t1). If there is no intersection, returns vec2(-1.0). fn ray_sphere_intersect(r: f32, mu: f32, sphere_radius: f32) -> vec2 { let discriminant = r * r * (mu * mu - 1.0) + sphere_radius * sphere_radius; - + // No intersection if discriminant < 0.0 { return vec2(-1.0); } - + let q = -r * mu; let sqrt_discriminant = sqrt(discriminant); - + // Return both intersection distances return vec2( q - sqrt_discriminant, diff --git a/crates/bevy_render/src/utils.wgsl b/crates/bevy_render/src/utils.wgsl index ab1ecfa870991..b0f7028947d9c 100644 --- a/crates/bevy_render/src/utils.wgsl +++ b/crates/bevy_render/src/utils.wgsl @@ -49,3 +49,12 @@ fn octahedral_decode(v: vec2) -> vec3 { let f = v * 2.0 - 1.0; return octahedral_decode_signed(f); } + +/// Decode angle to tangent. The angle is [-2pi, 2pi], where the sign represents the orientation of the tangent. +fn decode_tangent_angle(tangent_angle: f32, normal: vec3f) -> vec4f { + let orientation = sign(tangent_angle); + let angle = tangent_angle * orientation * bevy_render::maths::PI_2; + let t0 = bevy_render::maths::orthonormal_y_axis(normal); + let tangent = t0 * cos(angle) + cross(normal, t0) * sin(angle); + return vec4f(tangent, orientation); +} diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh.rs b/crates/bevy_sprite_render/src/mesh2d/mesh.rs index ac2ea4f7aff5c..60e02440e18c6 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite_render/src/mesh2d/mesh.rs @@ -675,6 +675,16 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4)); } + if layout + .0 + .get_attribute_compression() + .contains(MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE) + { + // The mesh has tangents but doesn't have `Mesh::ATTRIBUTE_TANGENT` + shader_defs.push("VERTEX_TANGENTS".into()); + shader_defs.push("VERTEX_PACKED_TANGENT_ANGLE".into()); + } + if key.contains(Mesh2dPipelineKey::TONEMAP_IN_SHADER) { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_input.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_input.wgsl index 9a1e079a345db..d38b254e99bc4 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_input.wgsl +++ b/crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_input.wgsl @@ -23,6 +23,11 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, + +#ifdef VERTEX_PACKED_TANGENT_ANGLE + @location(0) compressed_position_tangent_angle: vec4, +#else // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_POSITIONS #ifdef VERTEX_POSITIONS_COMPRESSED @location(0) compressed_position: vec4, @@ -30,6 +35,16 @@ struct Vertex { @location(0) position: vec3, #endif #endif +#ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + @location(3) compressed_tangent: vec2, +#else + @location(3) tangent: vec4, +#endif +#endif + +#endif // VERTEX_PACKED_TANGENT_ANGLE + #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED @location(1) compressed_normal: vec2, @@ -44,13 +59,6 @@ struct Vertex { @location(2) uv: vec2, #endif #endif -#ifdef VERTEX_TANGENTS -#ifdef VERTEX_TANGENTS_COMPRESSED - @location(3) compressed_tangent: vec2, -#else - @location(3) tangent: vec4, -#endif -#endif #ifdef VERTEX_COLORS @location(4) color: vec4, #endif @@ -62,11 +70,8 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert let mesh_metadata = mesh_functions::get_metadata(instance_index); var uncompressed_vertex: UncompressedVertex; uncompressed_vertex.instance_index = instance_index; -#ifdef VERTEX_POSITIONS_COMPRESSED - uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); -#else - uncompressed_vertex.position = vertex_in.position; -#endif + +// Decompress normal before tangent angle #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED uncompressed_vertex.normal = bevy_render::utils::decompress_vertex_normal(vertex_in.compressed_normal); @@ -74,13 +79,16 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.normal = vertex_in.normal; #endif #endif -#ifdef VERTEX_UVS -#ifdef VERTEX_UVS_COMPRESSED - let uv_min_and_extents = mesh_metadata.uv_channels_min_and_extents[0]; - uncompressed_vertex.uv = bevy_render::utils::decompress_vertex_uv(vertex_in.compressed_uv, uv_min_and_extents); + +#ifdef VERTEX_PACKED_TANGENT_ANGLE + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position_tangent_angle, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); + uncompressed_vertex.tangent = bevy_render::utils::decode_tangent_angle(vertex_in.compressed_position_tangent_angle.w, uncompressed_vertex.normal); +#else // VERTEX_PACKED_TANGENT_ANGLE + +#ifdef VERTEX_POSITIONS_COMPRESSED + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); #else - uncompressed_vertex.uv = vertex_in.uv; -#endif + uncompressed_vertex.position = vertex_in.position; #endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED @@ -89,6 +97,17 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.tangent = vertex_in.tangent; #endif #endif + +#endif // VERTEX_PACKED_TANGENT_ANGLE + +#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_COMPRESSED + let uv_min_and_extents = mesh_metadata.uv_channels_min_and_extents[0]; + uncompressed_vertex.uv = bevy_render::utils::decompress_vertex_uv(vertex_in.compressed_uv, uv_min_and_extents); +#else + uncompressed_vertex.uv = vertex_in.uv; +#endif +#endif #ifdef VERTEX_COLORS uncompressed_vertex.color = vertex_in.color; #endif From 85ef428b04e1f43165796d0e5ef00182e51e3f08 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Sat, 13 Jun 2026 16:27:45 +0800 Subject: [PATCH 3/7] Clippy --- crates/bevy_mesh/src/mesh.rs | 9 +++++++-- crates/bevy_mesh/src/vertex.rs | 9 +++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index f87946c9972be..3b028b415c99a 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -318,8 +318,12 @@ bitflags::bitflags! { /// - 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 they can go beyond [0, 1], though a larger range will reduce precision. /// + /// If [`MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE`] is enabled, normal must exist and position must be compressed, + /// and tangent will be compressed to angle using [`encode_tangent_angle`] and stored in the w component of position. + /// /// [`octahedral_encode_signed`]: crate::vertex::octahedral_encode_signed /// [`octahedral_encode_tangent`]: crate::vertex::octahedral_encode_tangent + /// [`encode_tangent_angle`]: crate::vertex::encode_tangent_angle #[repr(transparent)] #[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] @@ -1185,10 +1189,11 @@ impl Mesh { } /// Compress tangents and apply [`MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE`]. + /// This should be called after compressing positions and before compressing normals. /// See [`MeshAttributeCompressionFlags`] for the details. /// /// Return an error if: - /// - [`Mesh::ATTRIBUTE_POSITION`] is missing or is not already compressed to Snorm16x4. + /// - [`Mesh::ATTRIBUTE_POSITION`] is missing or is not compressed to Snorm16x4. /// - [`Mesh::ATTRIBUTE_NORMAL`] is missing or is not Float32x3. /// - [`Mesh::ATTRIBUTE_TANGENT`] is missing or is not Float32x4. /// @@ -1244,7 +1249,7 @@ impl Mesh { }; let VertexAttributeValues::Float32x3(normals) = normals else { - let provided = (&*normals).into(); + let provided = normals.into(); return_pos(self, positions); return Err( MeshVertexCompressionError::UnsupportedAttributeForCompression { diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 85887605d479b..b5b0c0038879a 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -1229,14 +1229,14 @@ pub fn octahedral_decode_tangent(v: Vec2) -> (Vec3, f32) { (octahedral_decode_signed(f), sign) } -/// Matches bevy_render/maths.wgsl `orthonormal_y_axis` +/// Matches `bevy_render/maths.wgsl:orthonormal_y_axis` fn orthonormal_y_axis(z_basis: Vec3) -> Vec3 { let sign = if z_basis.z >= 0.0 { 1.0 } else { -1.0 }; let a = -1.0 / (sign + z_basis.z); let b = z_basis.x * z_basis.y * a; // let x_basis = vec3f(1.0 + sign * z_basis.x * z_basis.x * a, sign * b, -sign * z_basis.x); - let y_basis = Vec3::new(b, sign + z_basis.y * z_basis.y * a, -z_basis.y); - return y_basis; + // let y_basis = + Vec3::new(b, sign + z_basis.y * z_basis.y * a, -z_basis.y) } /// Encode tangent to angle. The angle is [-2pi, 2pi], where the sign represents the orientation of the tangent. @@ -1264,7 +1264,7 @@ pub fn decode_tangent_angle(tangent_angle: f32, normal: Vec3) -> Vec4 { let orientation = tangent_angle.signum(); let angle = tangent_angle * orientation; let t0 = orthonormal_y_axis(normal); - let tangent = t0 * angle.cos() + normal.cross(t0) * angle.sin(); + let tangent = t0 * ops::cos(angle) + normal.cross(t0) * ops::sin(angle); tangent.extend(orientation) } @@ -1346,6 +1346,7 @@ mod tests { ), ]; + #[expect(clippy::approx_constant, reason = "These values are from test results")] let expected_angle = [ 1.5707963, -1.5707963, 4.712389, -4.712389, 3.1415927, 3.1415927, 3.926991, -2.6779451, 4.712389, -5.4977875, 6.0473375, From 7988e45bb25349f3e60766d59791109c9ebca7c1 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sat, 13 Jun 2026 17:21:07 +0800 Subject: [PATCH 4/7] Update decode_tangent_angle comments in the shader --- crates/bevy_render/src/utils.wgsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_render/src/utils.wgsl b/crates/bevy_render/src/utils.wgsl index b0f7028947d9c..6a4f0bd0529a9 100644 --- a/crates/bevy_render/src/utils.wgsl +++ b/crates/bevy_render/src/utils.wgsl @@ -50,7 +50,7 @@ fn octahedral_decode(v: vec2) -> vec3 { return octahedral_decode_signed(f); } -/// Decode angle to tangent. The angle is [-2pi, 2pi], where the sign represents the orientation of the tangent. +/// Decode angle to tangent. The angle is [-1, 1] normalized from [-2pi, 2pi], where the sign represents the orientation of the tangent. fn decode_tangent_angle(tangent_angle: f32, normal: vec3f) -> vec4f { let orientation = sign(tangent_angle); let angle = tangent_angle * orientation * bevy_render::maths::PI_2; From 2fb2f4e11d5521484d5c621b32d4b481378852cf Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Sat, 13 Jun 2026 19:31:58 +0800 Subject: [PATCH 5/7] normalize en/decode_tangent_angle arg to [-1, 1] --- crates/bevy_mesh/src/mesh.rs | 3 +-- crates/bevy_mesh/src/vertex.rs | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 3b028b415c99a..58ecf99c6f52e 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -1263,8 +1263,7 @@ impl Mesh { for i in 0..vertex_count { let tangent = tangents[i]; let normal = normals[i]; - let angle = - encode_tangent_angle(tangent.into(), normal.into()) / core::f32::consts::TAU; + let angle = encode_tangent_angle(tangent.into(), normal.into()); positions[i][3] = arr_f32_to_snorm16([angle])[0]; } return_pos(self, positions); diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index b5b0c0038879a..5c6448e04af32 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -1239,7 +1239,7 @@ fn orthonormal_y_axis(z_basis: Vec3) -> Vec3 { Vec3::new(b, sign + z_basis.y * z_basis.y * a, -z_basis.y) } -/// Encode tangent to angle. The angle is [-2pi, 2pi], where the sign represents the orientation of the tangent. +/// Encode tangent to angle. The angle is [-1, 1] normalized from [-2pi, 2pi], where the sign represents the orientation of the tangent. pub fn encode_tangent_angle(tangent: Vec4, normal: Vec3) -> f32 { // Bias to ensure that encoding as snorm16 preserves the sign. let bits = 16.; @@ -1248,24 +1248,24 @@ pub fn encode_tangent_angle(tangent: Vec4, normal: Vec3) -> f32 { let orientation = tangent.w.signum(); let t0 = orthonormal_y_axis(normal); let tangent = tangent.xyz(); - let angle = t0.angle_between(tangent).max(bias * core::f32::consts::TAU); + let angle = (t0.angle_between(tangent) / core::f32::consts::TAU).max(bias); orientation * { if t0.cross(tangent).dot(normal) >= 0.0 { angle } else { - core::f32::consts::TAU - angle + 1.0 - angle } } } -/// Decode angle to tangent. The angle is [-2pi, 2pi], where the sign represents the orientation of the tangent. +/// Decode angle to tangent. The angle is [-1, 1] normalized from [-2pi, 2pi], where the sign represents the orientation of the tangent. pub fn decode_tangent_angle(tangent_angle: f32, normal: Vec3) -> Vec4 { - let orientation = tangent_angle.signum(); - let angle = tangent_angle * orientation; + let sign = tangent_angle.signum(); + let angle = tangent_angle * sign * core::f32::consts::TAU; let t0 = orthonormal_y_axis(normal); let tangent = t0 * ops::cos(angle) + normal.cross(t0) * ops::sin(angle); - tangent.extend(orientation) + tangent.extend(sign) } #[cfg(test)] @@ -1346,10 +1346,18 @@ mod tests { ), ]; - #[expect(clippy::approx_constant, reason = "These values are from test results")] let expected_angle = [ - 1.5707963, -1.5707963, 4.712389, -4.712389, 3.1415927, 3.1415927, 3.926991, -2.6779451, - 4.712389, -5.4977875, 6.0473375, + 0.24999999, + -0.24999999, + 0.75, + -0.75, + 0.5, + 0.5, + 0.625, + -0.4262082, + 0.75, + -0.875, + 0.9624636, ]; for (i, &(normal, tangent)) in normal_tangent.iter().enumerate() { From 70011415dba8fa6b1431a51ac28ad220a13f21a1 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Sat, 13 Jun 2026 19:45:25 +0800 Subject: [PATCH 6/7] Test compress_mesh_tangent_angles --- crates/bevy_mesh/src/mesh.rs | 73 ++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 58ecf99c6f52e..9841b9e1184d7 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -3825,6 +3825,79 @@ mod tests { ); } + #[test] + fn compress_mesh_tangent_angles() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![ + [0.0, 1.0, -1.0], + [1.0, -0.5, -1.0], + [-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], + ], + ); + mesh.compress_positions().unwrap(); + assert_eq!( + 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.attribute_compression, + MeshAttributeCompressionFlags::COMPRESS_POSITION + ); + assert_eq!( + mesh.attribute(Mesh::ATTRIBUTE_POSITION), + Some(&VertexAttributeValues::Snorm16x4(vec![ + [0, 32767, -32767, 0], + [32767, -32767, -32767, 0], + [-32767, -32767, -32767, 0], + [0, -32767, 32767, 0], + ])) + ); + + mesh.compress_tangents_to_angles().unwrap(); + assert_eq!( + mesh.attribute_compression, + MeshAttributeCompressionFlags::COMPRESS_POSITION + | MeshAttributeCompressionFlags::PACKED_TANGENT_ANGLE + ); + assert_eq!(mesh.attribute(Mesh::ATTRIBUTE_TANGENT), None); + assert_eq!( + mesh.attribute(Mesh::ATTRIBUTE_POSITION), + Some(&VertexAttributeValues::Snorm16x4(vec![ + [0, 32767, -32767, 16384], + [32767, -32767, -32767, -24575], + [-32767, -32767, -32767, 8192], + [0, -32767, 32767, 24575] + ])) + ); + } + #[test] fn quantize_mesh_colors() { let mut mesh = Mesh::new( From 367f2e6a880886b50cc476bbef02db5b6b323570 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Sat, 13 Jun 2026 19:56:28 +0800 Subject: [PATCH 7/7] Use `abs` instead of `* sign` --- crates/bevy_mesh/src/vertex.rs | 2 +- crates/bevy_render/src/utils.wgsl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 5c6448e04af32..7ed8b87d27ad4 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -1262,7 +1262,7 @@ pub fn encode_tangent_angle(tangent: Vec4, normal: Vec3) -> f32 { /// Decode angle to tangent. The angle is [-1, 1] normalized from [-2pi, 2pi], where the sign represents the orientation of the tangent. pub fn decode_tangent_angle(tangent_angle: f32, normal: Vec3) -> Vec4 { let sign = tangent_angle.signum(); - let angle = tangent_angle * sign * core::f32::consts::TAU; + let angle = tangent_angle.abs() * core::f32::consts::TAU; let t0 = orthonormal_y_axis(normal); let tangent = t0 * ops::cos(angle) + normal.cross(t0) * ops::sin(angle); tangent.extend(sign) diff --git a/crates/bevy_render/src/utils.wgsl b/crates/bevy_render/src/utils.wgsl index 6a4f0bd0529a9..ab37de3e1f724 100644 --- a/crates/bevy_render/src/utils.wgsl +++ b/crates/bevy_render/src/utils.wgsl @@ -52,9 +52,9 @@ fn octahedral_decode(v: vec2) -> vec3 { /// Decode angle to tangent. The angle is [-1, 1] normalized from [-2pi, 2pi], where the sign represents the orientation of the tangent. fn decode_tangent_angle(tangent_angle: f32, normal: vec3f) -> vec4f { - let orientation = sign(tangent_angle); - let angle = tangent_angle * orientation * bevy_render::maths::PI_2; + let sign = sign(tangent_angle); + let angle = abs(tangent_angle) * bevy_render::maths::PI_2; let t0 = bevy_render::maths::orthonormal_y_axis(normal); let tangent = t0 * cos(angle) + cross(normal, t0) * sin(angle); - return vec4f(tangent, orientation); + return vec4f(tangent, sign); }