From 951b232537512dde771488248bebff2e651f4931 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Fri, 5 Jun 2026 17:32:36 +0800 Subject: [PATCH 1/2] Add option to compress vertex normal tangent to axis angle --- crates/bevy_mesh/src/mesh.rs | 356 ++++++++++++++---- crates/bevy_mesh/src/vertex.rs | 134 ++++++- crates/bevy_pbr/src/prepass/mod.rs | 8 + crates/bevy_pbr/src/prepass/prepass_io.wgsl | 66 +++- crates/bevy_pbr/src/render/forward_io.wgsl | 61 ++- crates/bevy_pbr/src/render/mesh.rs | 9 + crates/bevy_render/src/utils.wgsl | 39 ++ crates/bevy_sprite_render/src/mesh2d/mesh.rs | 9 + .../src/mesh2d/mesh2d_vertex_input.wgsl | 60 ++- examples/3d/deferred_rendering.rs | 5 + examples/stress_tests/many_cubes.rs | 3 +- examples/stress_tests/many_foxes.rs | 3 +- examples/stress_tests/many_morph_targets.rs | 3 +- 13 files changed, 626 insertions(+), 130 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 1f14a8e1a25b3..0d98bddf479e7 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -8,11 +8,13 @@ use super::{ MeshVertexBufferLayoutRef, MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, }; -use crate::arr_f32_to_unorm8; #[cfg(feature = "morph")] use crate::morph::MorphAttributes; #[cfg(feature = "serialize")] use crate::SerializedMeshAttributeData; +use crate::{ + arr_f32_to_snorm16, arr_f32_to_unorm8, normal_tangent_to_axis_angle, octahedral_encode_signed, +}; use alloc::collections::BTreeMap; use bevy_asset::{Asset, RenderAssetUsages}; use bevy_math::{ @@ -272,17 +274,27 @@ pub struct Mesh { 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). + /// - Position will be Snorm16x4 relative to the mesh's AABB. The w component is unused unless [`PACKED_AXIS_ANGLE_TBN`] is enabled. + /// - Normal and tangent will be Snorm16x2 with octahedral encoding, using [`octahedral_encode_signed`] and [`octahedral_encode_tangent`]. /// - UV0 and UV1 will be Unorm16x2. UVs are remapped based on their min/max values so them can go beyond [0, 1], though a larger range will reduce precision. - /// - Joint weight will be Unorm16x4. - /// - Color will be Float16x4 or Unorm8x4. + /// - Joint weight will be quantized to Unorm16x4. No change is needed in shaders. + /// - Color will be quantized to Float16x4/Unorm8x4. No change is needed in shaders. + /// + /// In addition, if [`PACKED_AXIS_ANGLE_TBN`] is enabled: + /// - Position, normal and tangent compression flags must be enabled, otherwise it won't be applied. + /// - Normal and tangent will be compressed to axis-angle representation. Axis is octahedral encoded Snorm16x2 in normal location (Note the axis isn't equal to normal). Angle is a Snorm16 in the w component of position. See also [`normal_tangent_to_axis_angle`] and [`normal_tangent_to_axis_angle`]. The tangent attribute will be removed with its location in shaders left empty. + /// + /// [`octahedral_encode_signed`]: crate::vertex::octahedral_encode_signed + /// [`octahedral_encode_tangent`]: crate::vertex::octahedral_encode_tangent + /// [`PACKED_AXIS_ANGLE_TBN`]: MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN + /// [`normal_tangent_to_axis_angle`]: crate::vertex::normal_tangent_to_axis_angle + /// [`axis_angle_to_normal_tangent`]: crate::vertex::axis_angle_to_normal_tangent #[repr(transparent)] #[derive(Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] #[reflect(opaque)] #[reflect(Hash, Clone, PartialEq, Debug)] - pub struct MeshAttributeCompressionFlags: u8 { + pub struct MeshAttributeCompressionFlags: u16 { const COMPRESS_POSITION = 1 << 0; const COMPRESS_NORMAL = 1 << 1; const COMPRESS_TANGENT = 1 << 2; @@ -293,17 +305,32 @@ bitflags::bitflags! { 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; + + const PACKED_AXIS_ANGLE_TBN = 1 << Self::PACKED_AXIS_ANGLE_TBN_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; + const COMPRESS_COLOR_MASK_BIT: u16 = 0b11; + const COMPRESS_COLOR_SHIFT_BIT: u16 = + Self::COMPRESS_JOINT_WEIGHT.bits().trailing_zeros() as u16 + 1; + + const PACKED_AXIS_ANGLE_TBN_SHIFT_BIT: u16 = + Self::COMPRESS_COLOR_MASK_BIT.count_ones() as u16 + Self::COMPRESS_COLOR_SHIFT_BIT; /// Helper function to set color flag. pub fn with_color(self, color_flag: Self) -> Self { self & !Self::COMPRESS_COLOR_RESERVED_BIT | color_flag } + + /// All compression with color quantized to unorm8 + pub fn all_with_color_unorm8() -> Self { + Self::all().with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + } + + /// All compression with color quantized to float16 + pub fn all_with_color_float16() -> Self { + Self::all().with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16) + } } // Constants to work around const expression can't be used in match pattern. @@ -1100,7 +1127,7 @@ impl Mesh { /// /// 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_compression: MeshAttributeCompressionFlags, attribute_id: MeshVertexAttributeId, attribute_values: &VertexAttributeValues, aabb: Option, @@ -1108,52 +1135,42 @@ impl Mesh { ) -> Option { match attribute_id { ATTRIBUTE_POSITION_ID - if self - .attribute_compression + if attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION) => { attribute_values.create_compressed_positions(aabb.unwrap()) } ATTRIBUTE_NORMAL_ID - if self - .attribute_compression + if attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) => { attribute_values.create_octahedral_encode_normals() } ATTRIBUTE_UV_0_ID - if self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => + if 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) => + if attribute_compression.contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => { attribute_values.create_compressed_uvs(uv_ranges[1].unwrap()) } ATTRIBUTE_TANGENT_ID - if self - .attribute_compression + if attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) => { attribute_values.create_octahedral_encode_tangents() } ATTRIBUTE_COLOR_ID - if self - .attribute_compression + if attribute_compression .intersects(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT) => { - if self - .attribute_compression + if attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16) { attribute_values.create_f16_values() - } else if self - .attribute_compression + } else if attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) { // Create Unorm8x4 color @@ -1171,8 +1188,7 @@ impl Mesh { } } ATTRIBUTE_JOINT_WEIGHT_ID - if self - .attribute_compression + if attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_JOINT_WEIGHT) => { attribute_values.create_unorm16_values() @@ -1181,35 +1197,71 @@ impl Mesh { } } + fn create_compressed_axis_angle_attribute_values( + vertex_count: usize, + normals: &VertexAttributeValues, + tangents: &VertexAttributeValues, + out_angles: &mut [[i16; 4]], + ) -> Option { + let (VertexAttributeValues::Float32x3(normals), VertexAttributeValues::Float32x4(tangents)) = + (normals, tangents) + else { + return None; + }; + + let mut compressed_axis = Vec::new(); + for i in 0..vertex_count { + let normal = Vec3::from_array(normals[i]).normalize(); + let tangent_signed = Vec3::new(tangents[i][0], tangents[i][1], tangents[i][2]) + .normalize() + .extend(tangents[i][3]); + let (axis, angle) = normal_tangent_to_axis_angle(normal, tangent_signed); + let oct_axis = octahedral_encode_signed(axis); + + compressed_axis.push(arr_f32_to_snorm16(oct_axis.to_array())); + out_angles[i][3] = arr_f32_to_snorm16([angle / 2.0 / core::f32::consts::PI])[0]; + } + Some(VertexAttributeValues::Snorm16x2(compressed_axis)) + } + /// Create a [`Mesh`] with the given compression flags to reduce memory bandwidth on GPU, with the tradeoff of reduced precision of vertex attributes. /// /// See [`MeshAttributeCompressionFlags`] for more context. - /// if vertex attributes are already compressed, they are unchanged and won't decompress. - /// - /// If `index_compression` is true and indices are u32 and vertex count <= 65535, indices will be converted to u16, otherwise it does nothing. + /// if vertex attributes are already compressed, they are unchanged and won't be decompressed. /// /// # 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 { + pub fn compress_vertex(&mut self, mut new_compression: MeshAttributeCompressionFlags) { if self .attribute_compression .intersects(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT) { // Don't change color flag if it's already compressed. - attribute_compression - .remove(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT); + new_compression.remove(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT); + } + let normal_tangent_already_compressed = self.attribute_compression.intersects( + MeshAttributeCompressionFlags::COMPRESS_NORMAL + | MeshAttributeCompressionFlags::COMPRESS_TANGENT, + ); + let tbn_in_axis_angle = !normal_tangent_already_compressed + && (self.attribute_compression | new_compression).contains( + MeshAttributeCompressionFlags::COMPRESS_POSITION + | MeshAttributeCompressionFlags::COMPRESS_NORMAL + | MeshAttributeCompressionFlags::COMPRESS_TANGENT + | MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN, + ) + && self.contains_attribute(ATTRIBUTE_POSITION_ID) + && self.contains_attribute(ATTRIBUTE_NORMAL_ID) + && self.contains_attribute(ATTRIBUTE_TANGENT_ID); + if !tbn_in_axis_angle { + new_compression.remove(MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN); } - self.attribute_compression |= attribute_compression; + self.attribute_compression |= new_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, ] { @@ -1218,51 +1270,135 @@ impl Mesh { { // 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. + let (mut final_aabb, mut final_uv_ranges) = (None, [None; 2]); match attr.id { ATTRIBUTE_POSITION_ID => { let Some(aabb) = Self::compute_aabb(values) else { continue; }; - self.final_aabb = Some(aabb); + 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); + 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); + final_uv_ranges[1] = Some(uv_range); } _ => {} } - let values = self.create_compressed_attribute_values( + let compressed_values = Self::create_compressed_attribute_values( + self.attribute_compression, attr.id, - self.attribute(attr.id).unwrap(), - self.final_aabb, - self.final_uv_ranges, + values, + final_aabb, + final_uv_ranges, ); - if let Some(values) = values { + match attr.id { + ATTRIBUTE_POSITION_ID => { + self.final_aabb = final_aabb; + } + ATTRIBUTE_UV_0_ID => { + self.final_uv_ranges[0] = final_uv_ranges[0]; + } + ATTRIBUTE_UV_1_ID => { + self.final_uv_ranges[1] = final_uv_ranges[1]; + } + _ => {} + } + if let Some(values) = compressed_values { attr.format = compressed_format; self.insert_attribute(attr, values); } } } + if tbn_in_axis_angle { + 'b: { + let vertex_count = self.count_vertices(); - 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(), - )); + let Some(positions) = self.attribute_mut(ATTRIBUTE_POSITION_ID) else { + unreachable!() + }; + let VertexAttributeValues::Snorm16x4(positions) = positions else { + break 'b; + }; + let mut compressed_positions = core::mem::take(positions); + + let compressed_axis = Self::create_compressed_axis_angle_attribute_values( + vertex_count, + self.attribute(ATTRIBUTE_NORMAL_ID).unwrap(), + self.attribute(ATTRIBUTE_TANGENT_ID).unwrap(), + &mut compressed_positions, + ); + // Re-insert the positions. + let mut pos_attr = Self::ATTRIBUTE_POSITION; + pos_attr.format = VertexFormat::Snorm16x4; + self.insert_attribute( + pos_attr, + VertexAttributeValues::Snorm16x4(compressed_positions), + ); + if let Some(compressed_axis) = compressed_axis { + let mut normal_attr = Mesh::ATTRIBUTE_NORMAL; + normal_attr.format = VertexFormat::Snorm16x2; + self.insert_attribute(normal_attr, compressed_axis); + self.remove_attribute(ATTRIBUTE_TANGENT_ID); + } + } + } else { + for mut attr in [Mesh::ATTRIBUTE_NORMAL, Mesh::ATTRIBUTE_TANGENT] { + if let Some(compressed_format) = self.get_compressed_vertex_format(attr.id) { + let Some(values) = self.attribute(attr.id) else { + continue; + }; + let compressed_values = Self::create_compressed_attribute_values( + self.attribute_compression, + attr.id, + values, + self.final_aabb, + self.final_uv_ranges, + ); + if let Some(values) = compressed_values { + attr.format = compressed_format; + self.insert_attribute(attr, values); + } + } } } + } + /// 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_index(&mut self) { + // 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(), + )); + } + } + + /// Consumes the mesh and returns a mesh that is compressed by [`Self::compress_vertex`] and [`Self::compress_index`]. + /// + /// # Panics + /// Panics when the mesh data has already been extracted to `RenderWorld`. + pub fn compressed_mesh( + mut self, + compress_vertex: MeshAttributeCompressionFlags, + compress_index: bool, + ) -> Mesh { + self.compress_vertex(compress_vertex); + if compress_index { + self.compress_index(); + } self } @@ -3048,9 +3184,9 @@ mod tests { use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; use crate::{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] @@ -3542,7 +3678,7 @@ mod tests { 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(), Vec3::new(-1.0, 0.0, 1.0).normalize().extend(1.0).to_array(), [1.0, 0.0, 0.0, 1.0], ], @@ -3551,6 +3687,10 @@ mod tests { 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]], + ) .with_inserted_attribute( Mesh::ATTRIBUTE_COLOR, vec![ @@ -3581,20 +3721,35 @@ mod tests { .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, - ); + let mut mesh_compressed = mesh + .clone() + .compressed_mesh(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8, true); + // Test compressing with flag progressively. `MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN` isn't applied. + for flag in MeshAttributeCompressionFlags::all().iter() { + mesh_compressed = mesh_compressed.compressed_mesh(flag, true); + } assert_eq!( - mesh_compressed_all.final_aabb, + mesh_compressed.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_compressed.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_compressed.attribute(Mesh::ATTRIBUTE_POSITION), Some(&VertexAttributeValues::Snorm16x4(vec![ [0, 32767, -32767, 0], [32767, -32767, -32767, 0], @@ -3603,7 +3758,7 @@ mod tests { ])) ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_NORMAL), + mesh_compressed.attribute(Mesh::ATTRIBUTE_NORMAL), Some(&VertexAttributeValues::Snorm16x2(vec![ [-16384, 32767], [32767, -16384], @@ -3612,16 +3767,16 @@ mod tests { ])) ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_TANGENT), + mesh_compressed.attribute(Mesh::ATTRIBUTE_TANGENT), Some(&VertexAttributeValues::Snorm16x2(vec![ [0, 24575], - [16384, 16384], + [16384, -16384], [-16384, 16384], [32767, 16384], ])) ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_UV_0), + mesh_compressed.attribute(Mesh::ATTRIBUTE_UV_0), Some(&VertexAttributeValues::Unorm16x2(vec![ [65535, 32571], [65535, 65535], @@ -3630,7 +3785,16 @@ mod tests { ])) ); assert_eq!( - mesh_compressed_all.attribute(Mesh::ATTRIBUTE_COLOR), + mesh_compressed.attribute(Mesh::ATTRIBUTE_UV_1), + Some(&VertexAttributeValues::Unorm16x2(vec![ + [65535, 43559], + [16437, 65535], + [0, 21845], + [19877, 0] + ])) + ); + assert_eq!( + mesh_compressed.attribute(Mesh::ATTRIBUTE_COLOR), Some(&VertexAttributeValues::Unorm8x4(vec![ [13, 255, 38, 255], [191, 18, 255, 255], @@ -3639,16 +3803,17 @@ mod tests { ])) ); assert_eq!( - mesh_compressed_all.indices(), + mesh_compressed.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), + mesh_compressed.attribute(custom_attr), Some(&VertexAttributeValues::Uint32(vec![0, 1, 2, 3])) ); let mesh_compressed_color_f16 = mesh .clone() .compressed_mesh(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16, false); + // Uncompressed attributes should be equal to mesh's. assert!(mesh .attributes() .filter(|(attr, _values)| attr.id != Mesh::ATTRIBUTE_COLOR.id) @@ -3676,5 +3841,44 @@ mod tests { .flatten() ) .all(|(a, b)| approx::relative_eq!(a.to_f32(), b.to_f32()))); + + // Test `MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN` + let mesh_compressed_axis_angle_tbn = mesh.clone().compressed_mesh( + MeshAttributeCompressionFlags::COMPRESS_POSITION + | MeshAttributeCompressionFlags::COMPRESS_NORMAL + | MeshAttributeCompressionFlags::COMPRESS_TANGENT + | MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN, + false, + ); + // Uncompressed attributes should be equal to mesh's. + assert!(mesh + .attributes() + .filter(|(attr, _values)| ![ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_TANGENT.id + ] + .contains(&attr.id)) + .eq(mesh_compressed_axis_angle_tbn + .attributes() + .filter(|(attr, _values)| ![ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_TANGENT.id + ] + .contains(&attr.id)))); + assert_eq!( + mesh_compressed.final_aabb, + mesh_compressed_axis_angle_tbn.final_aabb + ); + assert_eq!( + mesh_compressed_axis_angle_tbn.attribute(Mesh::ATTRIBUTE_POSITION), + Some(&VertexAttributeValues::Snorm16x4(vec![ + [0, 32767, -32767, 19241], + [32767, -32767, -32767, -16384], + [-32767, -32767, -32767, 20479], + [0, -32767, 32767, 1], + ])) + ); } } diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index c638247e423c6..1924b73c8f3a1 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, vec3, vec4, Mat3, Quat, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, }; #[cfg(feature = "serialize")] use bevy_platform::collections::HashMap; @@ -1155,8 +1155,10 @@ 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; @@ -1184,12 +1186,60 @@ pub fn octahedral_decode_tangent(v: Vec2) -> (Vec3, f32) { (octahedral_decode_signed(f), sign) } +/// Convert the normal and tangent to the equivalent axis-angle representation. +/// The range of angle is [-2pi, 2pi], where the sign represents the handedness of the tangent. +pub fn normal_tangent_to_axis_angle(normal: Vec3, tangent_signed: Vec4) -> (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 tangent = tangent_signed.xyz(); + let bitangent = normal.cross(tangent); + let (axis, angle) = + Quat::from_mat3(&Mat3::from_cols(tangent, bitangent, normal)).to_axis_angle(); + let angle = angle + .rem_euclid(2.0 * core::f32::consts::PI) + .max(bias * 2.0 * core::f32::consts::PI); + + (axis, angle * tangent_signed.w) +} + +/// Convert the axis-angle representation back to normal and tangent. +/// The range of angle is [-2pi, 2pi], where the sign represents the handedness of the tangent. +pub fn axis_angle_to_normal_tangent(axis: Vec3, angle: f32) -> (Vec3, Vec4) { + let sign = if angle >= 0.0 { 1.0 } else { -1.0 }; + // References the source code of `Mat3::from_quat(Quat::from_axis_angle(axis, angle))` + let angle = angle * 0.5 * sign; + let c = ops::cos(angle); + let s = ops::sin(angle); + let v = axis * s; + let rotation = vec4(v.x, v.y, v.z, c); + let x2 = rotation.x + rotation.x; + let y2 = rotation.y + rotation.y; + let z2 = rotation.z + rotation.z; + let xx = rotation.x * x2; + let xy = rotation.x * y2; + let xz = rotation.x * z2; + let yy = rotation.y * y2; + let yz = rotation.y * z2; + let zz = rotation.z * z2; + let wx = rotation.w * x2; + let wy = rotation.w * y2; + let wz = rotation.w * z2; + + let tangent = vec3(1.0 - (yy + zz), xy + wz, xz - wy); + let normal = vec3(xz + wy, yz - wx, 1.0 - (xx + yy)); + + (normal, tangent.extend(sign)) +} + #[cfg(test)] mod tests { - use bevy_math::{vec2, vec3, Vec4Swizzles}; + use bevy_math::{vec2, vec3, Mat3, Quat, Vec3, Vec4, Vec4Swizzles}; use crate::{ - octahedral_decode_signed, octahedral_decode_tangent, + axis_angle_to_normal_tangent, normal_tangent_to_axis_angle, octahedral_decode_signed, + octahedral_decode_tangent, vertex::{octahedral_encode_signed, octahedral_encode_tangent}, }; @@ -1198,18 +1248,21 @@ 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(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(0.0, -1.0), vec2(-1.0, 3.051851e-5), vec2(-1.0, -3.051851e-5), ]; @@ -1226,4 +1279,75 @@ mod tests { assert!(decoded_tangent.distance(v.xyz()) < 1e-4); } } + + pub fn axis_angle_to_normal_tangent_glam(axis: Vec3, angle: f32) -> (Vec3, Vec4) { + let sign = if angle >= 0.0 { 1.0 } else { -1.0 }; + let tbn = Mat3::from_quat(Quat::from_axis_angle(axis, angle * sign)); + (tbn.col(2), tbn.col(0).extend(sign)) + } + + #[test] + fn normal_tangent_axis_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), + ), + ]; + + #[expect( + clippy::approx_constant, + reason = "The values are taken from the test results" + )] + let expected_axis_angle = [ + (vec3(0.7071068, 0.0, 0.7071068), 3.1415927), + (vec3(0.7071068, 0.0, 0.7071068), -3.1415927), + (vec3(1.0, 0.0, 0.0), 3.051851e-5), + (vec3(1.0, 0.0, 0.0), -3.051851e-5), + (vec3(0.57735026, 0.57735026, 0.57735026), 4.1887903), + (vec3(0.57735026, -0.57735026, 0.57735026), 4.1887903), + (vec3(-0.7429061, 0.3077218, -0.5944728), 1.2171159), + (vec3(0.64793617, 0.40044653, 0.64793617), -3.9033751), + (vec3(-1.0, 0.0, 0.0), 0.7853981), + (vec3(-0.7429061, -0.3077218, 0.5944728), -1.2171159), + (vec3(0.22525999, 0.6253928, 0.7470889), 1.6242763), + ]; + + for (i, &(normal, tangent)) in normal_tangent.iter().enumerate() { + let (axis, angle) = normal_tangent_to_axis_angle(normal, tangent); + assert_eq!(angle.signum(), tangent.w.signum()); + assert!(axis.distance(expected_axis_angle[i].0) < 1e-8); + + let (decoded_normal, decoded_tangent) = axis_angle_to_normal_tangent(axis, angle); + let (decoded_normal_glam, decoded_tangent_glam) = + axis_angle_to_normal_tangent_glam(axis, angle); + + assert!(decoded_normal.distance(normal) < 1e-3); + assert!(decoded_tangent.distance(tangent) < 1e-3); + assert!(decoded_normal_glam.distance(normal) < 1e-3); + assert!(decoded_tangent_glam.distance(tangent) < 1e-3); + } + } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 8dfb1f8f5b661..be661b2806d97 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -535,6 +535,14 @@ impl PrepassPipeline { } vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); } + if layout + .0 + .get_attribute_compression() + .contains(MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN) + { + shader_defs.push("VERTEX_TANGENTS".into()); + shader_defs.push("VERTEX_PACKED_AXIS_ANGLE_TBN".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..15f26c1e88e9e 100644 --- a/crates/bevy_pbr/src/prepass/prepass_io.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -18,9 +18,13 @@ struct UncompressedVertex { #ifdef VERTEX_NORMALS @location(3) normal: vec3, #endif + #ifdef VERTEX_TANGENTS @location(4) tangent: vec4, +#else ifdef VERTEX_PACKED_AXIS_ANGLE_TBN + @location(4) tangent: vec4, #endif + #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef SKINNED @@ -39,26 +43,20 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, + +#ifdef VERTEX_PACKED_AXIS_ANGLE_TBN + @location(0) compressed_position_angle: vec4, +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + @location(3) compressed_axis: vec2, +#endif + +#else // VERTEX_PACKED_AXIS_ANGLE_TBN + #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, -#else - @location(2) uv_b: vec2, -#endif -#endif - #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED @@ -76,6 +74,23 @@ struct Vertex { #endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS +#endif // VERTEX_PACKED_AXIS_ANGLE_TBN + +#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, +#else + @location(2) uv_b: vec2, +#endif +#endif + #ifdef SKINNED @location(5) joint_indices: vec4, @location(6) joint_weights: vec4, @@ -96,6 +111,24 @@ 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_PACKED_AXIS_ANGLE_TBN + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position_angle, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS + var normal: vec3f; + var tangent: vec4f; + bevy_render::utils::decompress_vertex_axis_angle_to_normal_tangent( + vertex_in.compressed_axis, + vertex_in.compressed_position_angle.w, + &normal, + &tangent, + ); + uncompressed_vertex.normal = normal; + uncompressed_vertex.tangent = tangent; +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#else // VERTEX_PACKED_AXIS_ANGLE_TBN + #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 @@ -117,6 +150,9 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert #endif #endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#endif // VERTEX_PACKED_AXIS_ANGLE_TBN + #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..c69613b0bce57 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -14,9 +14,13 @@ struct UncompressedVertex { #ifdef VERTEX_UVS_B @location(3) uv_b: vec2, #endif + #ifdef VERTEX_TANGENTS @location(4) tangent: vec4, +#else ifdef VERTEX_PACKED_AXIS_ANGLE_TBN + @location(4) tangent: vec4, #endif + #ifdef VERTEX_COLORS @location(5) color: vec4, #endif @@ -31,6 +35,13 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, + +#ifdef VERTEX_PACKED_AXIS_ANGLE_TBN + @location(0) compressed_position_angle: vec4, + @location(1) compressed_axis: vec2, + +#else // VERTEX_PACKED_AXIS_ANGLE_TBN + #ifdef VERTEX_POSITIONS #ifdef VERTEX_POSITIONS_COMPRESSED @location(0) compressed_position: vec4, @@ -45,6 +56,16 @@ struct Vertex { @location(1) normal: 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_AXIS_ANGLE_TBN + #ifdef VERTEX_UVS_A #ifdef VERTEX_UVS_A_COMPRESSED @location(2) compressed_uv: vec2, @@ -59,13 +80,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 +100,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; + +#ifdef VERTEX_PACKED_AXIS_ANGLE_TBN + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position_angle, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); + var normal: vec3f; + var tangent: vec4f; + bevy_render::utils::decompress_vertex_axis_angle_to_normal_tangent( + vertex_in.compressed_axis, + vertex_in.compressed_position_angle.w, + &normal, + &tangent, + ); + uncompressed_vertex.normal = normal; + uncompressed_vertex.tangent = tangent; +#else // VERTEX_PACKED_AXIS_ANGLE_TBN + #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); @@ -100,6 +129,16 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.normal = vertex_in.normal; #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 + +#endif // VERTEX_PACKED_AXIS_ANGLE_TBN + #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 +155,7 @@ 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..7056247889b6e 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -3393,6 +3393,15 @@ impl SpecializedMeshPipeline for MeshPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(5)); } + if layout + .0 + .get_attribute_compression() + .contains(MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN) + { + shader_defs.push("VERTEX_TANGENTS".into()); + shader_defs.push("VERTEX_PACKED_AXIS_ANGLE_TBN".into()); + } + if cfg!(feature = "pbr_transmission_textures") { shader_defs.push("PBR_TRANSMISSION_TEXTURES_SUPPORTED".into()); } diff --git a/crates/bevy_render/src/utils.wgsl b/crates/bevy_render/src/utils.wgsl index 2d6fc4cd97f6b..d8a3942dea6e4 100644 --- a/crates/bevy_render/src/utils.wgsl +++ b/crates/bevy_render/src/utils.wgsl @@ -17,6 +17,16 @@ fn decompress_vertex_uv(compressed_uv: vec2, uv_min_and_extents: vec4) return uv_min_and_extents.xy + uv_min_and_extents.zw * compressed_uv; } +fn decompress_vertex_axis_angle_to_normal_tangent( + octahedral_axis: vec2f, + angle: f32, + out_normal: ptr, + out_tangent: ptr, +) { + let axis = octahedral_decode_signed(octahedral_axis); + axis_angle_to_normal_tangent(axis, 2.0 * bevy_render::maths::PI * angle, out_normal, out_tangent); +} + // For decoding normals or unit direction vectors from octahedral coordinates. Input is [-1, 1]. fn octahedral_decode_signed(v: vec2) -> vec3 { var n = vec3(v.xy, 1.0 - abs(v.x) - abs(v.y)); @@ -49,3 +59,32 @@ fn octahedral_decode(v: vec2) -> vec3 { let f = v * 2.0 - 1.0; return octahedral_decode_signed(f); } + +fn axis_angle_to_normal_tangent( + axis: vec3f, + angle: f32, + out_normal: ptr, + out_tangent: ptr, +) { + let sign = select(-1.0, 1.0, angle >= 0.0); + let angle_h = angle * 0.5 * sign; + let c = cos(angle_h); + let s = sin(angle_h); + let v = axis * s; + let rotation = vec4f(v.x, v.y, v.z, c); + let x2 = rotation.x + rotation.x; + let y2 = rotation.y + rotation.y; + let z2 = rotation.z + rotation.z; + let xx = rotation.x * x2; + let xy = rotation.x * y2; + let xz = rotation.x * z2; + let yy = rotation.y * y2; + let yz = rotation.y * z2; + let zz = rotation.z * z2; + let wx = rotation.w * x2; + let wy = rotation.w * y2; + let wz = rotation.w * z2; + + *out_tangent = vec4f(1.0 - (yy + zz), xy + wz, xz - wy, sign); + *out_normal = vec3f(xz + wy, yz - wx, 1.0 - (xx + yy)); +} diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh.rs b/crates/bevy_sprite_render/src/mesh2d/mesh.rs index ac2ea4f7aff5c..360395faf8756 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite_render/src/mesh2d/mesh.rs @@ -675,6 +675,15 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { vertex_attributes.push(Mesh::ATTRIBUTE_COLOR.at_shader_location(4)); } + if layout + .0 + .get_attribute_compression() + .contains(MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN) + { + shader_defs.push("VERTEX_TANGENTS".into()); + shader_defs.push("VERTEX_PACKED_AXIS_ANGLE_TBN".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..eebb94d184baa 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_input.wgsl +++ b/crates/bevy_sprite_render/src/mesh2d/mesh2d_vertex_input.wgsl @@ -13,9 +13,13 @@ struct UncompressedVertex { #ifdef VERTEX_UVS @location(2) uv: vec2, #endif + #ifdef VERTEX_TANGENTS @location(3) tangent: vec4, +#else ifdef VERTEX_PACKED_AXIS_ANGLE_TBN + @location(3) tangent: vec4, #endif + #ifdef VERTEX_COLORS @location(4) color: vec4, #endif @@ -23,6 +27,13 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, + +#ifdef VERTEX_PACKED_AXIS_ANGLE_TBN + @location(0) compressed_position_angle: vec4, + @location(1) compressed_axis: vec2, + +#else // VERTEX_PACKED_AXIS_ANGLE_TBN + #ifdef VERTEX_POSITIONS #ifdef VERTEX_POSITIONS_COMPRESSED @location(0) compressed_position: vec4, @@ -37,13 +48,6 @@ struct Vertex { @location(1) normal: vec3, #endif #endif -#ifdef VERTEX_UVS -#ifdef VERTEX_UVS_COMPRESSED - @location(2) compressed_uv: vec2, -#else - @location(2) uv: vec2, -#endif -#endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED @location(3) compressed_tangent: vec2, @@ -51,6 +55,16 @@ struct Vertex { @location(3) tangent: vec4, #endif #endif + +#endif // VERTEX_PACKED_AXIS_ANGLE_TBN + +#ifdef VERTEX_UVS +#ifdef VERTEX_UVS_COMPRESSED + @location(2) compressed_uv: vec2, +#else + @location(2) uv: vec2, +#endif +#endif #ifdef VERTEX_COLORS @location(4) color: vec4, #endif @@ -62,6 +76,21 @@ 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_PACKED_AXIS_ANGLE_TBN + uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position_angle, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents); + var normal: vec3f; + var tangent: vec4f; + bevy_render::utils::decompress_vertex_axis_angle_to_normal_tangent( + vertex_in.compressed_axis, + vertex_in.compressed_position_angle.w, + &normal, + &tangent, + ); + uncompressed_vertex.normal = normal; + uncompressed_vertex.tangent = tangent; +#else // VERTEX_PACKED_AXIS_ANGLE_TBN + #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 @@ -74,6 +103,16 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.normal = vertex_in.normal; #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 + +#endif // VERTEX_PACKED_AXIS_ANGLE_TBN + #ifdef VERTEX_UVS #ifdef VERTEX_UVS_COMPRESSED let uv_min_and_extents = mesh_metadata.uv_channels_min_and_extents[0]; @@ -82,13 +121,6 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert uncompressed_vertex.uv = vertex_in.uv; #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/examples/3d/deferred_rendering.rs b/examples/3d/deferred_rendering.rs index 87fea95aa39d0..d2c47db2f79d3 100644 --- a/examples/3d/deferred_rendering.rs +++ b/examples/3d/deferred_rendering.rs @@ -11,6 +11,7 @@ use bevy::{ }, material::OpaqueRendererMethod, math::ops, + mesh::MeshAttributeCompressionFlags, pbr::DefaultOpaqueRendererMethod, prelude::*, }; @@ -232,6 +233,10 @@ fn setup_parallax( // NOTE: for normal maps and depth maps to work, the mesh // needs tangents generated. cube.generate_tangents().unwrap(); + let cube = cube.compressed_mesh( + MeshAttributeCompressionFlags::all_with_color_float16(), + true, + ); let parallax_material = materials.add(StandardMaterial { perceptual_roughness: 0.4, diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index 1f31f26477d71..c3373662ea405 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -432,8 +432,7 @@ fn init_materials( fn compress_mesh(args: &Args, mesh: impl Into) -> Mesh { if args.vertex_compression { mesh.into().compressed_mesh( - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16), + MeshAttributeCompressionFlags::all_with_color_float16(), true, ) } else { diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index 2dfd082a6770c..79857695f0647 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -66,8 +66,7 @@ fn main() { }) .set(GltfPlugin { mesh_attribute_compression: if args.vertex_compression { - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + MeshAttributeCompressionFlags::all_with_color_unorm8() } else { MeshAttributeCompressionFlags::empty() }, diff --git a/examples/stress_tests/many_morph_targets.rs b/examples/stress_tests/many_morph_targets.rs index 94c1cc2935e29..869db3117f0a9 100644 --- a/examples/stress_tests/many_morph_targets.rs +++ b/examples/stress_tests/many_morph_targets.rs @@ -159,8 +159,7 @@ fn main() { }) .set(GltfPlugin { mesh_attribute_compression: if args.vertex_compression { - MeshAttributeCompressionFlags::all() - .with_color(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + MeshAttributeCompressionFlags::all_with_color_unorm8() } else { MeshAttributeCompressionFlags::empty() }, From 23893419e04d14d4a273850ad2d6c5917e6e5af5 Mon Sep 17 00:00:00 2001 From: Luo Zhiaho Date: Mon, 8 Jun 2026 14:24:05 +0800 Subject: [PATCH 2/2] Simplify `axis_angle_to_normal_tangent` --- crates/bevy_mesh/src/vertex.rs | 30 +++++++++--------------------- crates/bevy_render/src/utils.wgsl | 28 ++++++++++------------------ 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 1924b73c8f3a1..a66255b546139 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, vec3, vec4, Mat3, Quat, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, + ops, vec2, vec3, Mat3, Quat, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles, }; #[cfg(feature = "serialize")] use bevy_platform::collections::HashMap; @@ -1208,27 +1208,15 @@ pub fn normal_tangent_to_axis_angle(normal: Vec3, tangent_signed: Vec4) -> (Vec3 /// The range of angle is [-2pi, 2pi], where the sign represents the handedness of the tangent. pub fn axis_angle_to_normal_tangent(axis: Vec3, angle: f32) -> (Vec3, Vec4) { let sign = if angle >= 0.0 { 1.0 } else { -1.0 }; - // References the source code of `Mat3::from_quat(Quat::from_axis_angle(axis, angle))` - let angle = angle * 0.5 * sign; - let c = ops::cos(angle); - let s = ops::sin(angle); + let angle_abs = angle * sign; + let c = ops::cos(angle_abs); + let s = ops::sin(angle_abs); let v = axis * s; - let rotation = vec4(v.x, v.y, v.z, c); - let x2 = rotation.x + rotation.x; - let y2 = rotation.y + rotation.y; - let z2 = rotation.z + rotation.z; - let xx = rotation.x * x2; - let xy = rotation.x * y2; - let xz = rotation.x * z2; - let yy = rotation.y * y2; - let yz = rotation.y * z2; - let zz = rotation.z * z2; - let wx = rotation.w * x2; - let wy = rotation.w * y2; - let wz = rotation.w * z2; - - let tangent = vec3(1.0 - (yy + zz), xy + wz, xz - wy); - let normal = vec3(xz + wy, yz - wx, 1.0 - (xx + yy)); + let omc = axis * (1.0 - c); + + let tangent = omc.xxx() * axis + vec3(c, v.z, -v.y); + // let bitangent = omc.yyy() * axis + vec3(-v.z, c, v.x); + let normal = omc.zzz() * axis + vec3(v.y, -v.x, c); (normal, tangent.extend(sign)) } diff --git a/crates/bevy_render/src/utils.wgsl b/crates/bevy_render/src/utils.wgsl index d8a3942dea6e4..67a4b7434f579 100644 --- a/crates/bevy_render/src/utils.wgsl +++ b/crates/bevy_render/src/utils.wgsl @@ -67,24 +67,16 @@ fn axis_angle_to_normal_tangent( out_tangent: ptr, ) { let sign = select(-1.0, 1.0, angle >= 0.0); - let angle_h = angle * 0.5 * sign; - let c = cos(angle_h); - let s = sin(angle_h); + let angle_abs = angle * sign; + let c = cos(angle_abs); + let s = sin(angle_abs); let v = axis * s; - let rotation = vec4f(v.x, v.y, v.z, c); - let x2 = rotation.x + rotation.x; - let y2 = rotation.y + rotation.y; - let z2 = rotation.z + rotation.z; - let xx = rotation.x * x2; - let xy = rotation.x * y2; - let xz = rotation.x * z2; - let yy = rotation.y * y2; - let yz = rotation.y * z2; - let zz = rotation.z * z2; - let wx = rotation.w * x2; - let wy = rotation.w * y2; - let wz = rotation.w * z2; + let omc = axis * (1.0 - c); - *out_tangent = vec4f(1.0 - (yy + zz), xy + wz, xz - wy, sign); - *out_normal = vec3f(xz + wy, yz - wx, 1.0 - (xx + yy)); + let tangent = omc.xxx * axis + vec3f(c, v.z, -v.y); + // let bitangent = omc.yyy * axis + vec3f(-v.z, c, v.x); + let normal = omc.zzz * axis + vec3f(v.y, -v.x, c); + + *out_tangent = vec4f(tangent, sign); + *out_normal = normal; }