From 91381d9e35050315ef75678416ec7e3dca874d3a Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Mon, 24 Nov 2025 09:56:10 +0800 Subject: [PATCH 1/8] Add options to lower precision/compressed vertex buffer --- crates/bevy_gltf/Cargo.toml | 2 +- crates/bevy_gltf/src/loader/mod.rs | 7 +- crates/bevy_mesh/Cargo.toml | 3 +- crates/bevy_mesh/src/mesh.rs | 207 +++++++++++++++++- crates/bevy_mesh/src/vertex.rs | 141 +++++++++++- crates/bevy_pbr/src/prepass/mod.rs | 6 + crates/bevy_pbr/src/prepass/prepass.wgsl | 14 +- crates/bevy_pbr/src/prepass/prepass_io.wgsl | 87 ++++++++ crates/bevy_pbr/src/render/forward_io.wgsl | 78 +++++++ crates/bevy_pbr/src/render/mesh.rs | 6 + crates/bevy_pbr/src/render/mesh.wgsl | 10 +- crates/bevy_sprite_render/src/mesh2d/mesh.rs | 6 + .../bevy_sprite_render/src/mesh2d/mesh2d.wgsl | 69 +++++- 13 files changed, 611 insertions(+), 25 deletions(-) diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index bd906090b96da..0835b0544a66e 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -29,7 +29,7 @@ bevy_image = { path = "../bevy_image", version = "0.18.0-dev" } bevy_light = { path = "../bevy_light", version = "0.18.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev" } bevy_math = { path = "../bevy_math", version = "0.18.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.18.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.18.0-dev", features = ["serialize"] } bevy_pbr = { path = "../bevy_pbr", version = "0.18.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" } bevy_render = { path = "../bevy_render", version = "0.18.0-dev" } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 7c6c0eaa56ca9..efd93c62ec805 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -30,7 +30,7 @@ use bevy_math::{Mat4, Vec3}; use bevy_mesh::{ morph::{MeshMorphWeights, MorphAttributes, MorphTargetImage, MorphWeights}, skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, Mesh, Mesh3d, MeshVertexAttribute, PrimitiveTopology, + Indices, Mesh, Mesh3d, MeshAttributeCompressionFlags, MeshVertexAttribute, PrimitiveTopology, }; #[cfg(feature = "pbr_transmission_textures")] use bevy_pbr::UvChannel; @@ -185,6 +185,8 @@ pub struct GltfLoaderSettings { /// /// Otherwise, nodes will be loaded and retained in RAM/VRAM according to the active flags. pub load_meshes: RenderAssetUsages, + /// Mesh attribute compression flags for the loaded meshes. + pub mesh_attribute_compression: MeshAttributeCompressionFlags, /// If empty, the gltf materials will be skipped. /// /// Otherwise, materials will be loaded and retained in RAM/VRAM according to the active flags. @@ -232,6 +234,7 @@ impl Default for GltfLoaderSettings { default_sampler: None, override_sampler: false, use_model_forward_direction: None, + mesh_attribute_compression: MeshAttributeCompressionFlags::default(), } } } @@ -678,7 +681,7 @@ impl GltfLoader { let primitive_topology = primitive_topology(primitive.mode())?; let mut mesh = Mesh::new(primitive_topology, settings.load_meshes); - + mesh.attribute_compression = settings.mesh_attribute_compression; // Read vertex attributes for (semantic, accessor) in primitive.attributes() { if [Semantic::Joints(0), Semantic::Weights(0)].contains(&semantic) { diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index eaa0dc2c574f0..c7bf0801ed3c4 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -35,6 +35,7 @@ hexasphere = "16.0" thiserror = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } derive_more = { version = "2", default-features = false, features = ["from"] } +half = { version = "2.4.1", features = ["bytemuck"] } [dev-dependencies] serde_json = "1.0.140" @@ -42,7 +43,7 @@ serde_json = "1.0.140" [features] default = [] ## Adds serialization support through `serde`. -serialize = ["dep:serde", "wgpu-types/serde"] +serialize = ["dep:serde", "wgpu-types/serde", "half/serde"] morph = ["dep:bevy_image"] [lints] diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 3ff416f89221d..1d56fac866c95 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -150,6 +150,37 @@ pub struct Mesh { /// Does nothing if not used with `bevy_solari`, or if the mesh is not compatible /// with `bevy_solari` (see `bevy_solari`'s docs). pub enable_raytracing: bool, + + pub attribute_compression: MeshAttributeCompressionFlags, +} + +bitflags::bitflags! { + #[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 { + const COMPRESS_NONE = 0; + const COMPRESS_NORMAL = 1 << 0; + const COMPRESS_TANGENT = 1 << 1; + const COMPRESS_UV0 = 1 << 2; + const COMPRESS_UV1 = 1 << 3; + const COMPRESS_JOINT_WEIGHT = 1 << 4; + const COMPRESS_COLOR_UNORM8 = 1 << 5; + const COMPRESS_COLOR_FLOAT16 = 1 << 6; + } +} + +impl Default for MeshAttributeCompressionFlags { + fn default() -> Self { + Self::COMPRESS_NORMAL + | Self::COMPRESS_TANGENT + | Self::COMPRESS_UV0 + | Self::COMPRESS_UV1 + | Self::COMPRESS_JOINT_WEIGHT + | Self::COMPRESS_COLOR_FLOAT16 + } } impl Mesh { @@ -241,6 +272,7 @@ impl Mesh { morph_target_names: None, asset_usage, enable_raytracing: true, + attribute_compression: MeshAttributeCompressionFlags::default(), } } @@ -417,7 +449,11 @@ impl Mesh { pub fn get_vertex_size(&self) -> u64 { self.attributes .values() - .map(|data| data.attribute.format.size()) + .map(|data| { + self.get_compressed_vertex_format(data.attribute.id) + .unwrap_or(data.attribute.format) + .size() + }) .sum() } @@ -447,12 +483,15 @@ impl Mesh { let mut accumulated_offset = 0; for (index, data) in self.attributes.values().enumerate() { attribute_ids.push(data.attribute.id); + let format = self + .get_compressed_vertex_format(data.attribute.id) + .unwrap_or(data.attribute.format); attributes.push(VertexAttribute { offset: accumulated_offset, - format: data.attribute.format, + format, shader_location: index as u32, }); - accumulated_offset += data.attribute.format.size(); + accumulated_offset += format.size(); } let layout = MeshVertexBufferLayout { @@ -462,6 +501,12 @@ impl Mesh { attributes, }, attribute_ids, + is_normal_compressed: self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL), + is_tangent_compressed: self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT), }; mesh_vertex_buffer_layouts.insert(layout) } @@ -493,6 +538,139 @@ impl Mesh { vertex_count.unwrap_or(0) } + /// Returns the compressed vertex format for the given attribute ID, or None if the attribute is not compressed. + pub fn get_compressed_vertex_format( + &self, + attribute_id: MeshVertexAttributeId, + ) -> Option { + match attribute_id { + id if id == Self::ATTRIBUTE_NORMAL.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) => + { + Some(VertexFormat::Float16x2) + } + id if id == Self::ATTRIBUTE_UV_0.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => + { + Some(VertexFormat::Float16x2) + } + id if id == Self::ATTRIBUTE_UV_1.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => + { + Some(VertexFormat::Float16x2) + } + id if id == Self::ATTRIBUTE_TANGENT.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) => + { + Some(VertexFormat::Float16x2) + } + id if id == Self::ATTRIBUTE_COLOR.id + && (self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + || self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16)) => + { + if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16) + { + Some(VertexFormat::Float16x4) + } else { + Some(VertexFormat::Unorm8x4) + } + } + id if id == Self::ATTRIBUTE_JOINT_WEIGHT.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_JOINT_WEIGHT) => + { + Some(VertexFormat::Float16x4) + } + _ => None, + } + } + + /// Create compressed attribute values for the given attribute ID and attribute values, or None if the attribute is not compressed. + pub fn create_compressed_attribute_values( + &self, + attribute_id: MeshVertexAttributeId, + attribute_values: &VertexAttributeValues, + ) -> Option { + match attribute_id { + id if id == Self::ATTRIBUTE_NORMAL.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) => + { + Some(attribute_values.create_octahedral_encode_f16x2()) + } + id if id == Self::ATTRIBUTE_UV_0.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => + { + Some(attribute_values.create_f16_values()) + } + id if id == Self::ATTRIBUTE_UV_1.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => + { + Some(attribute_values.create_f16_values()) + } + id if id == Self::ATTRIBUTE_TANGENT.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) => + { + Some(attribute_values.create_octahedral_encode_f16x2()) + } + id if id == Self::ATTRIBUTE_COLOR.id + && (self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + || self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16)) => + { + if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16) + { + Some(attribute_values.create_f16_values()) + } else { + let uncompressed_values = match attribute_values { + VertexAttributeValues::Float32x4(val) => val, + _ => unreachable!(), + }; + let mut values = Vec::<[u8; 4]>::with_capacity(uncompressed_values.len()); + for val in uncompressed_values { + values.push(val.map(|v| (v * u8::MAX as f32).round() as u8)); + } + Some(VertexAttributeValues::Unorm8x4(values)) + } + } + id if id == Self::ATTRIBUTE_JOINT_WEIGHT.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_JOINT_WEIGHT) => + { + Some(attribute_values.create_f16_values()) + } + _ => None, + } + } + /// Computes and returns the vertex data of the mesh as bytes. /// Therefore the attributes are located in the order of their [`MeshVertexAttribute::id`]. /// This is used to transform the vertex data into a GPU friendly format. @@ -520,6 +698,26 @@ impl Mesh { // bundle into interleaved buffers let mut attribute_offset = 0; for attribute_data in self.attributes.values() { + let compressed_values = self.create_compressed_attribute_values( + attribute_data.attribute.id, + &attribute_data.values, + ); + let compressed_format = self.get_compressed_vertex_format(attribute_data.attribute.id); + let compressed_attribute_data = if let Some(compressed_values) = compressed_values + && let Some(compressed_format) = compressed_format + { + Some(MeshAttributeData { + attribute: MeshVertexAttribute { + format: compressed_format, + ..attribute_data.attribute + }, + values: compressed_values, + }) + } else { + None + }; + + let attribute_data = compressed_attribute_data.as_ref().unwrap_or(attribute_data); let attribute_size = attribute_data.attribute.format.size() as usize; let attributes_bytes = attribute_data.values.get_bytes(); for (vertex_index, attribute_bytes) in attributes_bytes @@ -556,9 +754,11 @@ impl Mesh { )] match &mut attributes.values { VertexAttributeValues::Float32(vec) => *vec = duplicate(vec, indices), + VertexAttributeValues::Float16(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Sint32(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Uint32(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Float32x2(vec) => *vec = duplicate(vec, indices), + VertexAttributeValues::Float16x2(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Sint32x2(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Uint32x2(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Float32x3(vec) => *vec = duplicate(vec, indices), @@ -567,6 +767,7 @@ impl Mesh { VertexAttributeValues::Sint32x4(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Uint32x4(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Float32x4(vec) => *vec = duplicate(vec, indices), + VertexAttributeValues::Float16x4(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Sint16x2(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Snorm16x2(vec) => *vec = duplicate(vec, indices), VertexAttributeValues::Uint16x2(vec) => *vec = duplicate(vec, indices), diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 3e5471d51f9ab..1bd4db0bc5b4f 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -1,7 +1,7 @@ use alloc::sync::Arc; use bevy_derive::EnumVariantMeta; use bevy_ecs::resource::Resource; -use bevy_math::Vec3; +use bevy_math::{vec2, Vec2, Vec3, Vec3Swizzles}; #[cfg(feature = "serialize")] use bevy_platform::collections::HashMap; use bevy_platform::collections::HashSet; @@ -84,6 +84,8 @@ impl From for MeshVertexAttributeId { #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct MeshVertexBufferLayout { pub(crate) attribute_ids: Vec, + pub(crate) is_normal_compressed: bool, + pub(crate) is_tangent_compressed: bool, pub(crate) layout: VertexBufferLayout, } @@ -91,6 +93,8 @@ impl MeshVertexBufferLayout { pub fn new(attribute_ids: Vec, layout: VertexBufferLayout) -> Self { Self { attribute_ids, + is_normal_compressed: false, + is_tangent_compressed: false, layout, } } @@ -110,6 +114,14 @@ impl MeshVertexBufferLayout { &self.layout } + pub fn is_vertex_normal_compressed(&self) -> bool { + self.is_normal_compressed + } + + pub fn is_vertex_tangent_compressed(&self) -> bool { + self.is_tangent_compressed + } + pub fn get_layout( &self, attribute_descriptors: &[VertexAttributeDescriptor], @@ -237,15 +249,18 @@ pub fn triangle_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub enum VertexAttributeValues { Float32(Vec), + Float16(Vec), Sint32(Vec), Uint32(Vec), Float32x2(Vec<[f32; 2]>), + Float16x2(Vec<[half::f16; 2]>), Sint32x2(Vec<[i32; 2]>), Uint32x2(Vec<[u32; 2]>), Float32x3(Vec<[f32; 3]>), Sint32x3(Vec<[i32; 3]>), Uint32x3(Vec<[u32; 3]>), Float32x4(Vec<[f32; 4]>), + Float16x4(Vec<[half::f16; 4]>), Sint32x4(Vec<[i32; 4]>), Uint32x4(Vec<[u32; 4]>), Sint16x2(Vec<[i16; 2]>), @@ -276,15 +291,18 @@ impl VertexAttributeValues { pub fn len(&self) -> usize { match self { VertexAttributeValues::Float32(values) => values.len(), + VertexAttributeValues::Float16(values) => values.len(), VertexAttributeValues::Sint32(values) => values.len(), VertexAttributeValues::Uint32(values) => values.len(), VertexAttributeValues::Float32x2(values) => values.len(), + VertexAttributeValues::Float16x2(values) => values.len(), VertexAttributeValues::Sint32x2(values) => values.len(), VertexAttributeValues::Uint32x2(values) => values.len(), VertexAttributeValues::Float32x3(values) => values.len(), VertexAttributeValues::Sint32x3(values) => values.len(), VertexAttributeValues::Uint32x3(values) => values.len(), VertexAttributeValues::Float32x4(values) => values.len(), + VertexAttributeValues::Float16x4(values) => values.len(), VertexAttributeValues::Sint32x4(values) => values.len(), VertexAttributeValues::Uint32x4(values) => values.len(), VertexAttributeValues::Sint16x2(values) => values.len(), @@ -329,15 +347,18 @@ impl VertexAttributeValues { pub fn get_bytes(&self) -> &[u8] { match self { VertexAttributeValues::Float32(values) => cast_slice(values), + VertexAttributeValues::Float16(values) => cast_slice(values), VertexAttributeValues::Sint32(values) => cast_slice(values), VertexAttributeValues::Uint32(values) => cast_slice(values), VertexAttributeValues::Float32x2(values) => cast_slice(values), + VertexAttributeValues::Float16x2(values) => cast_slice(values), VertexAttributeValues::Sint32x2(values) => cast_slice(values), VertexAttributeValues::Uint32x2(values) => cast_slice(values), VertexAttributeValues::Float32x3(values) => cast_slice(values), VertexAttributeValues::Sint32x3(values) => cast_slice(values), VertexAttributeValues::Uint32x3(values) => cast_slice(values), VertexAttributeValues::Float32x4(values) => cast_slice(values), + VertexAttributeValues::Float16x4(values) => cast_slice(values), VertexAttributeValues::Sint32x4(values) => cast_slice(values), VertexAttributeValues::Uint32x4(values) => cast_slice(values), VertexAttributeValues::Sint16x2(values) => cast_slice(values), @@ -358,21 +379,88 @@ impl VertexAttributeValues { VertexAttributeValues::Unorm8x4(values) => cast_slice(values), } } + + /// Create a new VertexAttributeValues with the values converted from f32 to f16. Panic if the values are not Float32, Float32x2 or Float32x4. + pub fn create_f16_values(&self) -> VertexAttributeValues { + match &self { + VertexAttributeValues::Float32(uncompressed_values) => { + let mut values = Vec::::with_capacity(uncompressed_values.len()); + for value in uncompressed_values { + values.push(half::f16::from_f32(*value)); + } + 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([half::f16::from_f32(value[0]), half::f16::from_f32(value[1])]); + } + 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([ + half::f16::from_f32(value[0]), + half::f16::from_f32(value[1]), + half::f16::from_f32(value[2]), + half::f16::from_f32(value[3]), + ]); + } + VertexAttributeValues::Float16x4(values) + } + _ => panic!("Unsupported vertex attribute format"), + } + } + + /// Create a new VertexAttributeValues with Float32x3 or Float32x4 values converted to Float16x2 using octahedral encoding, assuming the values are vertex normal or tangent. Panic if the values are not Float32x3 or Float32x4. + pub fn create_octahedral_encode_f16x2(&self) -> VertexAttributeValues { + let values = match &self { + VertexAttributeValues::Float32x3(uncompressed_values) => { + let mut values = Vec::<[half::f16; 2]>::with_capacity(uncompressed_values.len()); + for value in uncompressed_values { + let val = octahedral_encode(Vec3::from_array(*value).normalize()); + values.push([half::f16::from_f32(val.x), half::f16::from_f32(val.y)]); + } + values + } + VertexAttributeValues::Float32x4(uncompressed_values) => { + let mut values = Vec::<[half::f16; 2]>::with_capacity(uncompressed_values.len()); + for value in uncompressed_values { + let mut encoded_value = octahedral_encode( + Vec3::from_array([value[0], value[1], value[2]]).normalize(), + ); + // encode binormal sign in y component + encoded_value.y *= value[3].signum(); + values.push([ + half::f16::from_f32(encoded_value.x), + half::f16::from_f32(encoded_value.y), + ]); + } + values + } + _ => panic!("Unsupported vertex attribute format"), + }; + VertexAttributeValues::Float16x2(values) + } } impl From<&VertexAttributeValues> for VertexFormat { fn from(values: &VertexAttributeValues) -> Self { match values { VertexAttributeValues::Float32(_) => VertexFormat::Float32, + VertexAttributeValues::Float16(_) => VertexFormat::Float16, VertexAttributeValues::Sint32(_) => VertexFormat::Sint32, VertexAttributeValues::Uint32(_) => VertexFormat::Uint32, VertexAttributeValues::Float32x2(_) => VertexFormat::Float32x2, + VertexAttributeValues::Float16x2(_) => VertexFormat::Float16x2, VertexAttributeValues::Sint32x2(_) => VertexFormat::Sint32x2, VertexAttributeValues::Uint32x2(_) => VertexFormat::Uint32x2, VertexAttributeValues::Float32x3(_) => VertexFormat::Float32x3, VertexAttributeValues::Sint32x3(_) => VertexFormat::Sint32x3, VertexAttributeValues::Uint32x3(_) => VertexFormat::Uint32x3, VertexAttributeValues::Float32x4(_) => VertexFormat::Float32x4, + VertexAttributeValues::Float16x4(_) => VertexFormat::Float16x4, VertexAttributeValues::Sint32x4(_) => VertexFormat::Sint32x4, VertexAttributeValues::Uint32x4(_) => VertexFormat::Uint32x4, VertexAttributeValues::Sint16x2(_) => VertexFormat::Sint16x2, @@ -491,3 +579,54 @@ impl Hash for MeshVertexBufferLayoutRef { (Arc::as_ptr(&self.0) as usize).hash(state); } } + +/// Encode normals or unit direction vectors as octahedral coordinates. +fn octahedral_encode(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)), + vec2(1.0, 1.0), + vec2(-1.0, -1.0), + ); + let n_xy = if n.z >= 0.0 { n.xy() } else { octahedral_wrap }; + return n_xy * 0.5 + 0.5; +} + +#[cfg(test)] +mod tests { + use bevy_math::{vec2, vec3, Vec3Swizzles as _}; + + use crate::vertex::octahedral_encode; + + /// Decode normals or unit direction vectors from octahedral coordinates. + fn octahedral_decode(v: Vec2) -> Vec3 { + let f = v * 2.0 - 1.0; + return octahedral_decode_signed(f); + } + + /// Like octahedral_decode, but for input in [-1, 1] instead of [0, 1]. + fn octahedral_decode_signed(v: Vec2) -> Vec3 { + let mut n = vec3(v.x, v.y, 1.0 - v.x.abs() - v.y.abs()); + let t = (-n.z).clamp(0.0, 1.0); + let w = Vec2::select(n.xy().cmpge(vec2(0.0, 0.0)), vec2(-t, -t), vec2(t, t)); + n = vec3(n.x + w.x, n.y + w.y, n.z); + return n.normalize(); + } + + #[test] + fn octahedral_encode_decode() { + let vs = [ + vec3(1.0, 2.0, 3.0).normalize(), + vec3(1.0, 0.0, 0.0), + vec3(0.0, 0.0, -1.0), + ]; + let vs_encoded = [vec2(0.5833333, 0.6666667), vec2(1.0, 0.5), vec2(0.0, 0.0)]; + for (i, &v) in vs.iter().enumerate() { + let encoded = octahedral_encode(v); + assert_eq!(encoded, vs_encoded[i]); + let decoded = octahedral_decode(encoded); + assert!(decoded.distance(v) < 1e-6); + } + } +} diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index d6e6ce69b49b0..8f7ae645cd6eb 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -458,6 +458,9 @@ impl PrepassPipeline { shader_defs.push("NORMAL_PREPASS_OR_DEFERRED_PREPASS".into()); if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { shader_defs.push("VERTEX_NORMALS".into()); + if layout.0.is_vertex_normal_compressed() { + shader_defs.push("VERTEX_NORMALS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(3)); } else if mesh_key.contains(MeshPipelineKey::NORMAL_PREPASS) { warn!( @@ -466,6 +469,9 @@ impl PrepassPipeline { } if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { shader_defs.push("VERTEX_TANGENTS".into()); + if layout.0.is_vertex_tangent_compressed() { + shader_defs.push("VERTEX_TANGENTS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); } } diff --git a/crates/bevy_pbr/src/prepass/prepass.wgsl b/crates/bevy_pbr/src/prepass/prepass.wgsl index 52dd9bf201568..6e4049c4de2f3 100644 --- a/crates/bevy_pbr/src/prepass/prepass.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass.wgsl @@ -2,7 +2,7 @@ prepass_bindings, mesh_bindings::mesh, mesh_functions, - prepass_io::{Vertex, VertexOutput, FragmentOutput}, + prepass_io::{Vertex, VertexOutput, FragmentOutput, decompress_vertex}, skinning, morph, mesh_view_bindings::view, @@ -59,11 +59,11 @@ fn morph_prev_vertex(vertex_in: Vertex) -> Vertex { @vertex fn vertex(vertex_no_morph: Vertex) -> VertexOutput { var out: VertexOutput; - + let uncompressed_vertex = decompress_vertex(vertex_no_morph); #ifdef MORPH_TARGETS - var vertex = morph_vertex(vertex_no_morph); + var vertex = morph_vertex(uncompressed_vertex); #else - var vertex = vertex_no_morph; + var vertex = uncompressed_vertex; #endif let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index); @@ -132,13 +132,13 @@ fn vertex(vertex_no_morph: Vertex) -> VertexOutput { #ifdef MORPH_TARGETS #ifdef HAS_PREVIOUS_MORPH - let prev_vertex = morph_prev_vertex(vertex_no_morph); + let prev_vertex = morph_prev_vertex(uncompressed_vertex); #else // HAS_PREVIOUS_MORPH - let prev_vertex = vertex_no_morph; + let prev_vertex = uncompressed_vertex; #endif // HAS_PREVIOUS_MORPH #else // MORPH_TARGETS - let prev_vertex = vertex_no_morph; + let prev_vertex = uncompressed_vertex; #endif // MORPH_TARGETS // Take skinning into account. diff --git a/crates/bevy_pbr/src/prepass/prepass_io.wgsl b/crates/bevy_pbr/src/prepass/prepass_io.wgsl index c3c0e55549906..882f5702e5ef4 100644 --- a/crates/bevy_pbr/src/prepass/prepass_io.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -1,7 +1,43 @@ #define_import_path bevy_pbr::prepass_io + // Most of these attributes are not used in the default prepass fragment shader, but they are still needed so we can // pass them to custom prepass shaders like pbr_prepass.wgsl. +struct UncompressedVertex { + @builtin(instance_index) instance_index: u32, + @location(0) position: vec3, + +#ifdef VERTEX_UVS_A + @location(1) uv: vec2, +#endif + +#ifdef VERTEX_UVS_B + @location(2) uv_b: vec2, +#endif + +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_NORMALS + @location(3) normal: vec3, +#endif +#ifdef VERTEX_TANGENTS + @location(4) tangent: vec4, +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS + +#ifdef SKINNED + @location(5) joint_indices: vec4, + @location(6) joint_weights: vec4, +#endif + +#ifdef VERTEX_COLORS + @location(7) color: vec4, +#endif + +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif // MORPH_TARGETS +}; + struct Vertex { @builtin(instance_index) instance_index: u32, @location(0) position: vec3, @@ -16,11 +52,19 @@ struct Vertex { #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef VERTEX_NORMALS +#ifdef VERTEX_NORMALS_COMPRESSED + @location(3) normal: vec2, +#else @location(3) normal: vec3, #endif +#endif #ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + @location(4) tangent: vec2, +#else @location(4) tangent: vec4, #endif +#endif #endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef SKINNED @@ -37,6 +81,49 @@ struct Vertex { #endif // MORPH_TARGETS } +fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { + var uncompressed_vertex: UncompressedVertex; + uncompressed_vertex.instance_index = vertex_in.instance_index; +#ifdef VERTEX_POSITIONS + uncompressed_vertex.position = vertex_in.position; +#endif +#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_NORMALS +#ifdef VERTEX_NORMALS_COMPRESSED + uncompressed_vertex.normal = bevy_pbr::utils::octahedral_decode(vertex_in.normal); +#else + uncompressed_vertex.normal = vertex_in.normal; +#endif +#endif +#ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + let binormal_sign = sign(vertex_in.tangent.y); + let tangent = vec4(bevy_pbr::utils::octahedral_decode(vec2(vertex_in.tangent.x, abs(vertex_in.tangent.y))), binormal_sign); + uncompressed_vertex.tangent = tangent; +#else + uncompressed_vertex.tangent = vertex_in.tangent; +#endif +#endif +#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS +#ifdef VERTEX_UVS_A + uncompressed_vertex.uv = vertex_in.uv; +#endif +#ifdef VERTEX_UVS_B + uncompressed_vertex.uv_b = vertex_in.uv_b; +#endif +#ifdef VERTEX_COLORS + uncompressed_vertex.color = vertex_in.color; +#endif +#ifdef SKINNED + uncompressed_vertex.joint_indices = vertex_in.joint_indices; + uncompressed_vertex.joint_weights = vertex_in.joint_weights; +#endif +#ifdef MORPH_TARGETS + uncompressed_vertex.index = vertex_in.index; +#endif + return uncompressed_vertex; +} + struct VertexOutput { // This is `clip position` when the struct is used as a vertex stage output // and `frag coord` when used as a fragment stage input diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 99f2ecced7628..8e4198480bdad 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -1,13 +1,46 @@ #define_import_path bevy_pbr::forward_io +struct UncompressedVertex { + @builtin(instance_index) instance_index: u32, +#ifdef VERTEX_POSITIONS + @location(0) position: vec3, +#endif +#ifdef VERTEX_NORMALS + @location(1) normal: vec3, +#endif +#ifdef VERTEX_UVS_A + @location(2) uv: vec2, +#endif +#ifdef VERTEX_UVS_B + @location(3) uv_b: vec2, +#endif +#ifdef VERTEX_TANGENTS + @location(4) tangent: vec4, +#endif +#ifdef VERTEX_COLORS + @location(5) color: vec4, +#endif +#ifdef SKINNED + @location(6) joint_indices: vec4, + @location(7) joint_weights: vec4, +#endif +#ifdef MORPH_TARGETS + @builtin(vertex_index) index: u32, +#endif +}; + struct Vertex { @builtin(instance_index) instance_index: u32, #ifdef VERTEX_POSITIONS @location(0) position: vec3, #endif #ifdef VERTEX_NORMALS +#ifdef VERTEX_NORMALS_COMPRESSED + @location(1) normal: vec2, +#else @location(1) normal: vec3, #endif +#endif #ifdef VERTEX_UVS_A @location(2) uv: vec2, #endif @@ -15,8 +48,12 @@ struct Vertex { @location(3) uv_b: vec2, #endif #ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + @location(4) tangent: vec2, +#else @location(4) tangent: vec4, #endif +#endif #ifdef VERTEX_COLORS @location(5) color: vec4, #endif @@ -29,6 +66,47 @@ struct Vertex { #endif }; +fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { + var uncompressed_vertex: UncompressedVertex; + uncompressed_vertex.instance_index = vertex_in.instance_index; +#ifdef VERTEX_POSITIONS + uncompressed_vertex.position = vertex_in.position; +#endif +#ifdef VERTEX_NORMALS +#ifdef VERTEX_NORMALS_COMPRESSED + uncompressed_vertex.normal = bevy_pbr::utils::octahedral_decode(vertex_in.normal); +#else + uncompressed_vertex.normal = vertex_in.normal; +#endif +#endif +#ifdef VERTEX_UVS_A + uncompressed_vertex.uv = vertex_in.uv; +#endif +#ifdef VERTEX_UVS_B + uncompressed_vertex.uv_b = vertex_in.uv_b; +#endif +#ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + let binormal_sign = sign(vertex_in.tangent.y); + let tangent = vec4(bevy_pbr::utils::octahedral_decode(vec2(vertex_in.tangent.x, abs(vertex_in.tangent.y))), binormal_sign); + uncompressed_vertex.tangent = tangent; +#else + uncompressed_vertex.tangent = vertex_in.tangent; +#endif +#endif +#ifdef VERTEX_COLORS + uncompressed_vertex.color = vertex_in.color; +#endif +#ifdef SKINNED + uncompressed_vertex.joint_indices = vertex_in.joint_indices; + uncompressed_vertex.joint_weights = vertex_in.joint_weights; +#endif +#ifdef MORPH_TARGETS + uncompressed_vertex.index = vertex_in.index; +#endif + return uncompressed_vertex; +} + struct VertexOutput { // This is `clip position` when the struct is used as a vertex stage output // and `frag coord` when used as a fragment stage input diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 6e4b3b8aebd8d..73966982b0548 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -2310,6 +2310,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { shader_defs.push("VERTEX_NORMALS".into()); + if layout.0.is_vertex_normal_compressed() { + shader_defs.push("VERTEX_NORMALS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1)); } @@ -2327,6 +2330,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { shader_defs.push("VERTEX_TANGENTS".into()); + if layout.0.is_vertex_tangent_compressed() { + shader_defs.push("VERTEX_TANGENTS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4)); } diff --git a/crates/bevy_pbr/src/render/mesh.wgsl b/crates/bevy_pbr/src/render/mesh.wgsl index 95684684f5140..19c8e36afb761 100644 --- a/crates/bevy_pbr/src/render/mesh.wgsl +++ b/crates/bevy_pbr/src/render/mesh.wgsl @@ -3,12 +3,12 @@ mesh_functions, skinning, morph::morph, - forward_io::{Vertex, VertexOutput}, + forward_io::{Vertex, UncompressedVertex, VertexOutput, decompress_vertex}, view_transformations::position_world_to_clip, } #ifdef MORPH_TARGETS -fn morph_vertex(vertex_in: Vertex) -> Vertex { +fn morph_vertex(vertex_in: UncompressedVertex) -> UncompressedVertex { var vertex = vertex_in; let first_vertex = mesh[vertex.instance_index].first_vertex_index; let vertex_index = vertex.index - first_vertex; @@ -34,11 +34,11 @@ fn morph_vertex(vertex_in: Vertex) -> Vertex { @vertex fn vertex(vertex_no_morph: Vertex) -> VertexOutput { var out: VertexOutput; - + let uncompressed_vertex = decompress_vertex(vertex_no_morph); #ifdef MORPH_TARGETS - var vertex = morph_vertex(vertex_no_morph); + var vertex = morph_vertex(uncompressed_vertex); #else - var vertex = vertex_no_morph; + var vertex = uncompressed_vertex; #endif let mesh_world_from_local = mesh_functions::get_world_from_local(vertex_no_morph.instance_index); diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh.rs b/crates/bevy_sprite_render/src/mesh2d/mesh.rs index a1f03b85acfa1..80786a7387b6c 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite_render/src/mesh2d/mesh.rs @@ -509,6 +509,9 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { if layout.0.contains(Mesh::ATTRIBUTE_NORMAL) { shader_defs.push("VERTEX_NORMALS".into()); + if layout.0.is_vertex_normal_compressed() { + shader_defs.push("VERTEX_NORMALS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_NORMAL.at_shader_location(1)); } @@ -519,6 +522,9 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { if layout.0.contains(Mesh::ATTRIBUTE_TANGENT) { shader_defs.push("VERTEX_TANGENTS".into()); + if layout.0.is_vertex_tangent_compressed() { + shader_defs.push("VERTEX_TANGENTS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(3)); } diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl index e909608be8ba1..c79e1d9a33080 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl +++ b/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl @@ -8,54 +8,113 @@ #import bevy_core_pipeline::tonemapping #endif +struct UncompressedVertex { + @builtin(instance_index) instance_index: u32, +#ifdef VERTEX_POSITIONS + @location(0) position: vec3, +#endif +#ifdef VERTEX_NORMALS + @location(1) normal: vec3, +#endif +#ifdef VERTEX_UVS + @location(2) uv: vec2, +#endif +#ifdef VERTEX_TANGENTS + @location(3) tangent: vec4, +#endif +#ifdef VERTEX_COLORS + @location(4) color: vec4, +#endif +}; + struct Vertex { @builtin(instance_index) instance_index: u32, #ifdef VERTEX_POSITIONS @location(0) position: vec3, #endif #ifdef VERTEX_NORMALS +#ifdef VERTEX_NORMALS_COMPRESSED + @location(1) normal: vec2, +#else @location(1) normal: vec3, #endif +#endif #ifdef VERTEX_UVS @location(2) uv: vec2, #endif #ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + @location(3) tangent: vec2, +#else @location(3) tangent: vec4, #endif +#endif #ifdef VERTEX_COLORS @location(4) color: vec4, #endif }; +fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { + var uncompressed_vertex: UncompressedVertex; + uncompressed_vertex.instance_index = vertex_in.instance_index; +#ifdef VERTEX_POSITIONS + uncompressed_vertex.position = vertex_in.position; +#endif +#ifdef VERTEX_NORMALS +#ifdef VERTEX_NORMALS_COMPRESSED + uncompressed_vertex.normal = bevy_pbr::utils::octahedral_decode(vertex_in.normal); +#else + uncompressed_vertex.normal = vertex_in.normal; +#endif +#endif +#ifdef VERTEX_UVS + uncompressed_vertex.uv = vertex_in.uv; +#endif +#ifdef VERTEX_TANGENTS +#ifdef VERTEX_TANGENTS_COMPRESSED + let binormal_sign = sign(vertex_in.tangent.y); + let tangent = vec4(bevy_pbr::utils::octahedral_decode(vec2(vertex_in.tangent.x, abs(vertex_in.tangent.y))), binormal_sign); + uncompressed_vertex.tangent = tangent; +#else + uncompressed_vertex.tangent = vertex_in.tangent; +#endif +#endif +#ifdef VERTEX_COLORS + uncompressed_vertex.color = vertex_in.color; +#endif + return uncompressed_vertex; +} + @vertex fn vertex(vertex: Vertex) -> VertexOutput { var out: VertexOutput; + let uncompressed_vertex = decompress_vertex(vertex); #ifdef VERTEX_UVS - out.uv = vertex.uv; + out.uv = uncompressed_vertex.uv; #endif #ifdef VERTEX_POSITIONS var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); out.world_position = mesh_functions::mesh2d_position_local_to_world( world_from_local, - vec4(vertex.position, 1.0) + vec4(uncompressed_vertex.position, 1.0) ); out.position = mesh_functions::mesh2d_position_world_to_clip(out.world_position); #endif #ifdef VERTEX_NORMALS - out.world_normal = mesh_functions::mesh2d_normal_local_to_world(vertex.normal, vertex.instance_index); + out.world_normal = mesh_functions::mesh2d_normal_local_to_world(uncompressed_vertex.normal, vertex.instance_index); #endif #ifdef VERTEX_TANGENTS out.world_tangent = mesh_functions::mesh2d_tangent_local_to_world( world_from_local, - vertex.tangent + uncompressed_vertex.tangent ); #endif #ifdef VERTEX_COLORS - out.color = vertex.color; + out.color = uncompressed_vertex.color; #endif return out; } From 9391ea4b208caf21b655c4ba76edf173270a53a5 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Mon, 24 Nov 2025 22:36:35 +0800 Subject: [PATCH 2/8] Change to Unorm16 --- crates/bevy_mesh/src/mesh.rs | 18 +-- crates/bevy_mesh/src/vertex.rs | 136 +++++++++++++----- crates/bevy_pbr/src/prepass/prepass_io.wgsl | 4 +- crates/bevy_pbr/src/render/forward_io.wgsl | 4 +- crates/bevy_pbr/src/render/utils.wgsl | 9 ++ .../bevy_sprite_render/src/mesh2d/mesh2d.wgsl | 4 +- 6 files changed, 120 insertions(+), 55 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 1d56fac866c95..0d75132f8ef7c 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -549,28 +549,28 @@ impl Mesh { .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) => { - Some(VertexFormat::Float16x2) + Some(VertexFormat::Unorm16x2) } id if id == Self::ATTRIBUTE_UV_0.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => { - Some(VertexFormat::Float16x2) + Some(VertexFormat::Unorm16x2) } id if id == Self::ATTRIBUTE_UV_1.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => { - Some(VertexFormat::Float16x2) + Some(VertexFormat::Unorm16x2) } id if id == Self::ATTRIBUTE_TANGENT.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) => { - Some(VertexFormat::Float16x2) + Some(VertexFormat::Unorm16x2) } id if id == Self::ATTRIBUTE_COLOR.id && (self @@ -594,7 +594,7 @@ impl Mesh { .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_JOINT_WEIGHT) => { - Some(VertexFormat::Float16x4) + Some(VertexFormat::Unorm16x4) } _ => None, } @@ -612,28 +612,28 @@ impl Mesh { .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL) => { - Some(attribute_values.create_octahedral_encode_f16x2()) + Some(attribute_values.create_octahedral_encode_unorm16()) } id if id == Self::ATTRIBUTE_UV_0.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => { - Some(attribute_values.create_f16_values()) + Some(attribute_values.create_unrom16_values()) } id if id == Self::ATTRIBUTE_UV_1.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => { - Some(attribute_values.create_f16_values()) + Some(attribute_values.create_unrom16_values()) } id if id == Self::ATTRIBUTE_TANGENT.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_TANGENT) => { - Some(attribute_values.create_octahedral_encode_f16x2()) + Some(attribute_values.create_octahedral_encode_unorm16()) } id if id == Self::ATTRIBUTE_COLOR.id && (self diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 1bd4db0bc5b4f..52569774c46ae 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -413,35 +413,65 @@ impl VertexAttributeValues { } } - /// Create a new VertexAttributeValues with Float32x3 or Float32x4 values converted to Float16x2 using octahedral encoding, assuming the values are vertex normal or tangent. Panic if the values are not Float32x3 or Float32x4. - pub fn create_octahedral_encode_f16x2(&self) -> VertexAttributeValues { - let values = match &self { + /// Create a new VertexAttributeValues with the values converted from f32 to unrom16. Panic if the values are not Float32, Float32x2 or Float32x4. + pub fn create_unrom16_values(&self) -> VertexAttributeValues { + match &self { + VertexAttributeValues::Float32x2(uncompressed_values) => { + let mut values = Vec::<[u16; 2]>::with_capacity(uncompressed_values.len()); + for value in uncompressed_values { + values.push([ + (value[0] * u16::MAX as f32).round() as u16, + (value[1] * u16::MAX as f32).round() as u16, + ]); + } + VertexAttributeValues::Unorm16x2(values) + } + VertexAttributeValues::Float32x4(uncompressed_values) => { + let mut values = Vec::<[u16; 4]>::with_capacity(uncompressed_values.len()); + for value in uncompressed_values { + values.push([ + (value[0] * u16::MAX as f32).round() as u16, + (value[1] * u16::MAX as f32).round() as u16, + (value[2] * u16::MAX as f32).round() as u16, + (value[3] * u16::MAX as f32).round() as u16, + ]); + } + VertexAttributeValues::Unorm16x4(values) + } + _ => panic!("Unsupported vertex attribute format"), + } + } + + /// Create a new `VertexAttributeValues` with Float32x3 or Float32x4 values converted to Unorm16x2 using octahedral encoding, assuming the values are vertex normal or tangent. Panic if the values are not Float32x3 or Float32x4. + pub fn create_octahedral_encode_unorm16(&self) -> VertexAttributeValues { + match &self { VertexAttributeValues::Float32x3(uncompressed_values) => { - let mut values = Vec::<[half::f16; 2]>::with_capacity(uncompressed_values.len()); + let mut values = Vec::<[u16; 2]>::with_capacity(uncompressed_values.len()); for value in uncompressed_values { let val = octahedral_encode(Vec3::from_array(*value).normalize()); - values.push([half::f16::from_f32(val.x), half::f16::from_f32(val.y)]); + values.push([ + (val.x * u16::MAX as f32).round() as u16, + (val.y * u16::MAX as f32).round() as u16, + ]); } - values + VertexAttributeValues::Unorm16x2(values) } VertexAttributeValues::Float32x4(uncompressed_values) => { - let mut values = Vec::<[half::f16; 2]>::with_capacity(uncompressed_values.len()); + let mut values = Vec::<[u16; 2]>::with_capacity(uncompressed_values.len()); for value in uncompressed_values { - let mut encoded_value = octahedral_encode( + let encoded_value = octahedral_encode_tangent( Vec3::from_array([value[0], value[1], value[2]]).normalize(), + value[3], ); - // encode binormal sign in y component - encoded_value.y *= value[3].signum(); values.push([ - half::f16::from_f32(encoded_value.x), - half::f16::from_f32(encoded_value.y), + (encoded_value.x * u16::MAX as f32).round() as u16, + (encoded_value.y * u16::MAX as f32).round() as u16, ]); } - values + VertexAttributeValues::Unorm16x2(values) } _ => panic!("Unsupported vertex attribute format"), - }; - VertexAttributeValues::Float16x2(values) + } } } @@ -589,20 +619,68 @@ fn octahedral_encode(v: Vec3) -> Vec2 { vec2(1.0, 1.0), vec2(-1.0, -1.0), ); - let n_xy = if n.z >= 0.0 { n.xy() } else { octahedral_wrap }; - return n_xy * 0.5 + 0.5; + let mut n_xy = if n.z >= 0.0 { n.xy() } else { octahedral_wrap }; + n_xy = n_xy * 0.5 + 0.5; + n_xy +} + +/// Encode tangent vectors as octahedral coordinates. sign is encoded in y component. +fn octahedral_encode_tangent(v: Vec3, sign: f32) -> Vec2 { + // Code from Godot, https://github.com/godotengine/godot/pull/73265 + let bias = 1.0 / 32767.0; + let mut n_xy = octahedral_encode(v); + n_xy.y = n_xy.y.max(bias); + n_xy.y = n_xy.y * 0.5 + 0.5; + n_xy.y = if sign >= 0.0 { n_xy.y } else { 1.0 - n_xy.y }; + n_xy } #[cfg(test)] mod tests { - use bevy_math::{vec2, vec3, Vec3Swizzles as _}; + use bevy_math::{vec2, vec3, Vec2, Vec3, Vec3Swizzles as _, Vec4Swizzles}; + + use crate::vertex::{octahedral_encode, octahedral_encode_tangent}; - use crate::vertex::octahedral_encode; + #[test] + fn octahedral_encode_decode() { + let vs = [ + vec3(1.0, 2.0, 3.0).normalize().extend(1.0), + vec3(1.0, 0.0, 0.0).extend(-1.0), + vec3(0.0, 0.0, -1.0).extend(1.0), + ]; + let vs_encoded = [vec2(0.5833333, 0.6666667), vec2(1.0, 0.5), vec2(0.0, 0.0)]; + let vs_encoded_tangent = [ + vec2(0.5833333, 0.8333334), + vec2(1.0, 0.25), + vec2(0.0, 0.50001526), + ]; + for (i, &v) in vs.iter().enumerate() { + let encoded_normal = octahedral_encode(v.xyz()); + let decoded_normal = octahedral_decode(encoded_normal); + assert_eq!(encoded_normal, vs_encoded[i]); + assert!(decoded_normal.distance(vs[i].xyz()) < 1e-6); + + let encoded_tangent = octahedral_encode_tangent(v.xyz(), v.w); + let (decoded_tangent, sign) = octahedral_decode_tangent(encoded_tangent); + assert_eq!(encoded_tangent, vs_encoded_tangent[i]); + assert_eq!(v.w, sign); + assert!(decoded_tangent.distance(v.xyz()) < 1e-4); + } + } /// Decode normals or unit direction vectors from octahedral coordinates. fn octahedral_decode(v: Vec2) -> Vec3 { let f = v * 2.0 - 1.0; - return octahedral_decode_signed(f); + octahedral_decode_signed(f) + } + + /// Decode tangent vectors from octahedral coordinates and return the sign. The sign should have been encoded in y component using corresponding `octahedral_encode_tangent`. + fn octahedral_decode_tangent(v: Vec2) -> (Vec3, f32) { + let mut f = v; + f.y = f.y * 2.0 - 1.0; + let sign = if f.y >= 0.0 { 1.0 } else { -1.0 }; + f.y = f.y.abs(); + (octahedral_decode(f), sign) } /// Like octahedral_decode, but for input in [-1, 1] instead of [0, 1]. @@ -611,22 +689,6 @@ mod tests { let t = (-n.z).clamp(0.0, 1.0); let w = Vec2::select(n.xy().cmpge(vec2(0.0, 0.0)), vec2(-t, -t), vec2(t, t)); n = vec3(n.x + w.x, n.y + w.y, n.z); - return n.normalize(); - } - - #[test] - fn octahedral_encode_decode() { - let vs = [ - vec3(1.0, 2.0, 3.0).normalize(), - vec3(1.0, 0.0, 0.0), - vec3(0.0, 0.0, -1.0), - ]; - let vs_encoded = [vec2(0.5833333, 0.6666667), vec2(1.0, 0.5), vec2(0.0, 0.0)]; - for (i, &v) in vs.iter().enumerate() { - let encoded = octahedral_encode(v); - assert_eq!(encoded, vs_encoded[i]); - let decoded = octahedral_decode(encoded); - assert!(decoded.distance(v) < 1e-6); - } + n.normalize() } } diff --git a/crates/bevy_pbr/src/prepass/prepass_io.wgsl b/crates/bevy_pbr/src/prepass/prepass_io.wgsl index 882f5702e5ef4..ef092bd40ee98 100644 --- a/crates/bevy_pbr/src/prepass/prepass_io.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -97,9 +97,7 @@ fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { #endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED - let binormal_sign = sign(vertex_in.tangent.y); - let tangent = vec4(bevy_pbr::utils::octahedral_decode(vec2(vertex_in.tangent.x, abs(vertex_in.tangent.y))), binormal_sign); - uncompressed_vertex.tangent = tangent; + uncompressed_vertex.tangent = bevy_pbr::utils::octahedral_decode_tangent(vertex_in.tangent); #else uncompressed_vertex.tangent = vertex_in.tangent; #endif diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 8e4198480bdad..9859176141e17 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -87,9 +87,7 @@ fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { #endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED - let binormal_sign = sign(vertex_in.tangent.y); - let tangent = vec4(bevy_pbr::utils::octahedral_decode(vec2(vertex_in.tangent.x, abs(vertex_in.tangent.y))), binormal_sign); - uncompressed_vertex.tangent = tangent; + uncompressed_vertex.tangent = bevy_pbr::utils::octahedral_decode_tangent(vertex_in.tangent); #else uncompressed_vertex.tangent = vertex_in.tangent; #endif diff --git a/crates/bevy_pbr/src/render/utils.wgsl b/crates/bevy_pbr/src/render/utils.wgsl index 8e91aeb9c0361..f0f712386b199 100644 --- a/crates/bevy_pbr/src/render/utils.wgsl +++ b/crates/bevy_pbr/src/render/utils.wgsl @@ -68,6 +68,15 @@ fn octahedral_decode_signed(v: vec2) -> vec3 { return normalize(n); } +// Decode tangent vectors from octahedral coordinates and return the sign. The sign should have been encoded in y component using corresponding `octahedral_encode_tangent`. +fn octahedral_decode_tangent(v: vec2) -> vec4 { + var f = v; + f.y = f.y * 2.0 - 1.0; + let sign = select(-1.0, 1.0, f.y >= 0.0); + f.y = abs(f.y); + return vec4(octahedral_decode(f), sign); +} + // https://blog.demofox.org/2022/01/01/interleaved-gradient-noise-a-different-kind-of-low-discrepancy-sequence fn interleaved_gradient_noise(pixel_coordinates: vec2, frame: u32) -> f32 { let xy = pixel_coordinates + 5.588238 * f32(frame % 64u); diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl index c79e1d9a33080..8c50d883f29a1 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl +++ b/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl @@ -72,9 +72,7 @@ fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { #endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED - let binormal_sign = sign(vertex_in.tangent.y); - let tangent = vec4(bevy_pbr::utils::octahedral_decode(vec2(vertex_in.tangent.x, abs(vertex_in.tangent.y))), binormal_sign); - uncompressed_vertex.tangent = tangent; + uncompressed_vertex.tangent = bevy_pbr::utils::octahedral_decode_tangent(vertex_in.tangent); #else uncompressed_vertex.tangent = vertex_in.tangent; #endif From 8050c838ad19546a10e992e88bf28dc9f32d1689 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Mon, 24 Nov 2025 22:43:26 +0800 Subject: [PATCH 3/8] format --- crates/bevy_gltf/Cargo.toml | 4 +++- crates/bevy_mesh/src/mesh.rs | 10 +++++----- crates/bevy_mesh/src/vertex.rs | 6 +++--- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 0835b0544a66e..40ed2a8c1689a 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -29,7 +29,9 @@ bevy_image = { path = "../bevy_image", version = "0.18.0-dev" } bevy_light = { path = "../bevy_light", version = "0.18.0-dev" } bevy_camera = { path = "../bevy_camera", version = "0.18.0-dev" } bevy_math = { path = "../bevy_math", version = "0.18.0-dev" } -bevy_mesh = { path = "../bevy_mesh", version = "0.18.0-dev", features = ["serialize"] } +bevy_mesh = { path = "../bevy_mesh", version = "0.18.0-dev", features = [ + "serialize", +] } bevy_pbr = { path = "../bevy_pbr", version = "0.18.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.18.0-dev" } bevy_render = { path = "../bevy_render", version = "0.18.0-dev" } diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 0d75132f8ef7c..5b56e49542040 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -619,14 +619,14 @@ impl Mesh { .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => { - Some(attribute_values.create_unrom16_values()) + Some(attribute_values.create_unorm16_values()) } id if id == Self::ATTRIBUTE_UV_1.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => { - Some(attribute_values.create_unrom16_values()) + Some(attribute_values.create_unorm16_values()) } id if id == Self::ATTRIBUTE_TANGENT.id && self @@ -649,9 +649,9 @@ impl Mesh { { Some(attribute_values.create_f16_values()) } else { - let uncompressed_values = match attribute_values { - VertexAttributeValues::Float32x4(val) => val, - _ => unreachable!(), + let VertexAttributeValues::Float32x4(uncompressed_values) = attribute_values + else { + unreachable!() }; let mut values = Vec::<[u8; 4]>::with_capacity(uncompressed_values.len()); for val in uncompressed_values { diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 52569774c46ae..ce5c80659984d 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -380,7 +380,7 @@ impl VertexAttributeValues { } } - /// Create a new VertexAttributeValues with the values converted from f32 to f16. Panic if the values are not Float32, Float32x2 or Float32x4. + /// Create a new `VertexAttributeValues` with the values converted from f32 to f16. Panic if the values are not Float32, Float32x2 or Float32x4. pub fn create_f16_values(&self) -> VertexAttributeValues { match &self { VertexAttributeValues::Float32(uncompressed_values) => { @@ -413,8 +413,8 @@ impl VertexAttributeValues { } } - /// Create a new VertexAttributeValues with the values converted from f32 to unrom16. Panic if the values are not Float32, Float32x2 or Float32x4. - pub fn create_unrom16_values(&self) -> VertexAttributeValues { + /// Create a new `VertexAttributeValues` with the values converted from f32 to unorm16. Panic if the values are not Float32, Float32x2 or Float32x4. + pub fn create_unorm16_values(&self) -> VertexAttributeValues { match &self { VertexAttributeValues::Float32x2(uncompressed_values) => { let mut values = Vec::<[u16; 2]>::with_capacity(uncompressed_values.len()); From dbdd06b2cda336a48eaac348b472ab3276385a44 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Mon, 24 Nov 2025 22:49:04 +0800 Subject: [PATCH 4/8] fix missing joint weight --- crates/bevy_mesh/src/mesh.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 5b56e49542040..140e963bc6eb6 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -665,7 +665,7 @@ impl Mesh { .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_JOINT_WEIGHT) => { - Some(attribute_values.create_f16_values()) + Some(attribute_values.create_unorm16_values()) } _ => None, } From 5de5484f1f801379a81d81ed1caefae63bc6c205 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 25 Nov 2025 13:59:56 +0800 Subject: [PATCH 5/8] docs and float16 for uv --- crates/bevy_mesh/src/mesh.rs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 140e963bc6eb6..88d4e1397436b 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -151,6 +151,12 @@ pub struct Mesh { /// with `bevy_solari` (see `bevy_solari`'s docs). pub enable_raytracing: bool, + /// Whether or not to compress vertex attributes when uploading to GPU buffer. + /// If the corresponding flag is enabled: + /// Normal and tangent will be Unorm16x2 using octahedral encoding. + /// UV0 and UV1 will be Float16x2. + /// Joint weight will be Unorm16x4. + /// Color will be Float16x4 or Unorm8x4 pub attribute_compression: MeshAttributeCompressionFlags, } @@ -446,6 +452,7 @@ impl Mesh { } /// Returns the size of a vertex in bytes. + /// This is affected by the [`Mesh::attribute_compression`]. pub fn get_vertex_size(&self) -> u64 { self.attributes .values() @@ -458,6 +465,7 @@ impl Mesh { } /// Returns the size required for the vertex buffer in bytes. + /// This is affected by the [`Mesh::attribute_compression`]. pub fn get_vertex_buffer_size(&self) -> usize { let vertex_size = self.get_vertex_size() as usize; let vertex_count = self.count_vertices(); @@ -474,6 +482,7 @@ impl Mesh { } /// Get this `Mesh`'s [`MeshVertexBufferLayout`], used in `SpecializedMeshPipeline`. + /// This is affected by the [`Mesh::attribute_compression`]. pub fn get_mesh_vertex_buffer_layout( &self, mesh_vertex_buffer_layouts: &mut MeshVertexBufferLayouts, @@ -556,14 +565,14 @@ impl Mesh { .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => { - Some(VertexFormat::Unorm16x2) + Some(VertexFormat::Float16x2) } id if id == Self::ATTRIBUTE_UV_1.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => { - Some(VertexFormat::Unorm16x2) + Some(VertexFormat::Float16x2) } id if id == Self::ATTRIBUTE_TANGENT.id && self @@ -619,14 +628,14 @@ impl Mesh { .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => { - Some(attribute_values.create_unorm16_values()) + Some(attribute_values.create_f16_values()) } id if id == Self::ATTRIBUTE_UV_1.id && self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => { - Some(attribute_values.create_unorm16_values()) + Some(attribute_values.create_f16_values()) } id if id == Self::ATTRIBUTE_TANGENT.id && self @@ -649,6 +658,7 @@ impl Mesh { { Some(attribute_values.create_f16_values()) } else { + // Create Unorm8x4 color let VertexAttributeValues::Float32x4(uncompressed_values) = attribute_values else { unreachable!() @@ -680,6 +690,8 @@ impl Mesh { /// /// This is a convenience method which allocates a Vec. /// Prefer pre-allocating and using [`Mesh::write_packed_vertex_buffer_data`] when possible. + /// + /// This will be compressed data if [`Mesh::attribute_compression`] isn't none. pub fn create_packed_vertex_buffer_data(&self) -> Vec { let mut attributes_interleaved_buffer = vec![0; self.get_vertex_buffer_size()]; self.write_packed_vertex_buffer_data(&mut attributes_interleaved_buffer); @@ -692,6 +704,8 @@ impl Mesh { /// /// If the vertex attributes have different lengths, they are all truncated to /// the length of the smallest. + /// + /// This will write compressed data if [`Mesh::attribute_compression`] isn't none. pub fn write_packed_vertex_buffer_data(&self, slice: &mut [u8]) { let vertex_size = self.get_vertex_size() as usize; let vertex_count = self.count_vertices(); From 8b85876fa9ea71418b7e606cd922bab2a0e62775 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 2 Dec 2025 09:26:09 +0800 Subject: [PATCH 6/8] Snorm16x4 compressed positions --- assets/shaders/custom_stencil.wgsl | 13 ++- assets/shaders/specialized_mesh_pipeline.wgsl | 11 ++- crates/bevy_mesh/src/mesh.rs | 82 ++++++++++++++++--- crates/bevy_mesh/src/vertex.rs | 6 ++ crates/bevy_pbr/src/prepass/mod.rs | 3 + crates/bevy_pbr/src/prepass/prepass_io.wgsl | 13 ++- crates/bevy_pbr/src/render/forward_io.wgsl | 12 ++- crates/bevy_pbr/src/render/mesh.rs | 49 ++++++++++- .../bevy_pbr/src/render/mesh_functions.wgsl | 16 +++- .../bevy_pbr/src/render/mesh_preprocess.wgsl | 2 + crates/bevy_pbr/src/render/mesh_types.wgsl | 5 ++ .../mesh_preprocess_types.wgsl | 6 ++ crates/bevy_render/src/mesh/mod.rs | 5 ++ crates/bevy_sprite_render/src/mesh2d/mesh.rs | 38 +++++++-- .../bevy_sprite_render/src/mesh2d/mesh2d.wgsl | 12 ++- .../src/mesh2d/mesh2d_functions.wgsl | 40 ++++++++- .../src/mesh2d/mesh2d_types.wgsl | 5 ++ .../shader_advanced/custom_render_phase.rs | 37 ++++----- .../specialized_mesh_pipeline.rs | 7 ++ 19 files changed, 310 insertions(+), 52 deletions(-) diff --git a/assets/shaders/custom_stencil.wgsl b/assets/shaders/custom_stencil.wgsl index 6f2fa2da4f977..a3e2be4ec5426 100644 --- a/assets/shaders/custom_stencil.wgsl +++ b/assets/shaders/custom_stencil.wgsl @@ -1,4 +1,4 @@ -//! A shader showing how to use the vertex position data to output the +//! A shader showing how to use the vertex position data to output the //! stencil in the right position // First we import everything we need from bevy_pbr @@ -14,7 +14,11 @@ struct Vertex { @builtin(instance_index) instance_index: u32, // Like we defined for the vertex layout // position is at location 0 +#ifdef VERTEX_POSITIONS_COMPRESSED + @location(0) position: vec4, +#else @location(0) position: vec3, +#endif }; // This is the output of the vertex shader and we also use it as the input for the fragment shader @@ -29,7 +33,12 @@ fn vertex(vertex: Vertex) -> VertexOutput { // This is how bevy computes the world position // The vertex.instance_index is very important. Especially if you are using batching and gpu preprocessing var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); - out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); +#ifdef VERTEX_POSITIONS_COMPRESSED + let vertex_position = mesh_functions::decompress_vertex_position(vertex.instance_index, vertex.position); +#else + let vertex_position = vertex.position; +#endif + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0)); out.clip_position = position_world_to_clip(out.world_position.xyz); return out; } diff --git a/assets/shaders/specialized_mesh_pipeline.wgsl b/assets/shaders/specialized_mesh_pipeline.wgsl index 29c9069ec88d7..700ea94d749da 100644 --- a/assets/shaders/specialized_mesh_pipeline.wgsl +++ b/assets/shaders/specialized_mesh_pipeline.wgsl @@ -14,7 +14,11 @@ struct Vertex { @builtin(instance_index) instance_index: u32, // Like we defined for the vertex layout // position is at location 0 +#ifdef VERTEX_POSITIONS_COMPRESSED + @location(0) position: vec4, +#else @location(0) position: vec3, +#endif // and color at location 1 @location(1) color: vec4, }; @@ -32,7 +36,12 @@ fn vertex(vertex: Vertex) -> VertexOutput { // This is how bevy computes the world position // The vertex.instance_index is very important. Especially if you are using batching and gpu preprocessing var world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); - out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex.position, 1.0)); +#ifdef VERTEX_POSITIONS_COMPRESSED + let vertex_position = mesh_functions::decompress_vertex_position(vertex.instance_index, vertex.position); +#else + let vertex_position = vertex.position; +#endif + out.world_position = mesh_functions::mesh_position_local_to_world(world_from_local, vec4(vertex_position, 1.0)); out.clip_position = position_world_to_clip(out.world_position.xyz); // We just use the raw vertex color diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 88d4e1397436b..6d732682b660b 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -15,7 +15,11 @@ use bevy_asset::Handle; use bevy_asset::{Asset, RenderAssetUsages}; #[cfg(feature = "morph")] use bevy_image::Image; -use bevy_math::{primitives::Triangle3d, *}; +use bevy_math::{ + bounding::{Aabb3d, BoundingVolume}, + primitives::Triangle3d, + *, +}; #[cfg(feature = "serialize")] use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; @@ -153,6 +157,7 @@ pub struct Mesh { /// Whether or not to compress vertex attributes when uploading to GPU buffer. /// If the corresponding flag is enabled: + /// Position will be Snorm16x4 relative to the mesh's AABB. The w component is unused. /// Normal and tangent will be Unorm16x2 using octahedral encoding. /// UV0 and UV1 will be Float16x2. /// Joint weight will be Unorm16x4. @@ -168,22 +173,28 @@ bitflags::bitflags! { #[reflect(Hash, Clone, PartialEq, Debug)] pub struct MeshAttributeCompressionFlags: u8 { const COMPRESS_NONE = 0; - const COMPRESS_NORMAL = 1 << 0; - const COMPRESS_TANGENT = 1 << 1; - const COMPRESS_UV0 = 1 << 2; - const COMPRESS_UV1 = 1 << 3; - const COMPRESS_JOINT_WEIGHT = 1 << 4; - const COMPRESS_COLOR_UNORM8 = 1 << 5; - const COMPRESS_COLOR_FLOAT16 = 1 << 6; + 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_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; +} impl Default for MeshAttributeCompressionFlags { fn default() -> Self { Self::COMPRESS_NORMAL | Self::COMPRESS_TANGENT - | Self::COMPRESS_UV0 - | Self::COMPRESS_UV1 | Self::COMPRESS_JOINT_WEIGHT | Self::COMPRESS_COLOR_FLOAT16 } @@ -510,6 +521,9 @@ impl Mesh { attributes, }, attribute_ids, + is_position_compressed: self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION), is_normal_compressed: self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_NORMAL), @@ -547,12 +561,37 @@ impl Mesh { vertex_count.unwrap_or(0) } + /// Calculates the axis-aligned bounding box of the mesh. Panics if the positions attribute is not Float32x3. + pub fn calculate_aabb(&self) -> Option { + let positions = self.attribute(Self::ATTRIBUTE_POSITION)?; + match positions { + VertexAttributeValues::Float32x3(val) => { + let mut iter = val.iter().map(|a| Vec3A::from_array(*a)); + let first = iter.next()?; + let (min, max) = iter.fold((first, first), |(prev_min, prev_max), point| { + (point.min(prev_min), point.max(prev_max)) + }); + Some(Aabb3d { min, max }) + } + _ => { + unreachable!() + } + } + } + /// Returns the compressed vertex format for the given attribute ID, or None if the attribute is not compressed. pub fn get_compressed_vertex_format( &self, attribute_id: MeshVertexAttributeId, ) -> Option { match attribute_id { + id if id == Self::ATTRIBUTE_POSITION.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION) => + { + Some(VertexFormat::Snorm16x4) + } id if id == Self::ATTRIBUTE_NORMAL.id && self .attribute_compression @@ -616,6 +655,29 @@ impl Mesh { attribute_values: &VertexAttributeValues, ) -> Option { match attribute_id { + id if id == Self::ATTRIBUTE_POSITION.id + && self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_POSITION) => + { + // Create Snorm16x4 position + let VertexAttributeValues::Float32x3(uncompressed_values) = attribute_values else { + unreachable!() + }; + let aabb = self.calculate_aabb().unwrap(); + let mut values = Vec::<[i16; 4]>::with_capacity(uncompressed_values.len()); + for val in uncompressed_values { + let mut val = Vec3A::from_array(*val); + val = (val - aabb.center()) / aabb.half_size(); + values.push([ + (val[0] * i16::MAX as f32).round() as i16, + (val[1] * i16::MAX as f32).round() as i16, + (val[2] * i16::MAX as f32).round() as i16, + 0, + ]); + } + Some(VertexAttributeValues::Snorm16x4(values)) + } id if id == Self::ATTRIBUTE_NORMAL.id && self .attribute_compression diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index ce5c80659984d..537efb434772c 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -84,6 +84,7 @@ impl From for MeshVertexAttributeId { #[derive(Debug, Clone, Hash, Eq, PartialEq)] pub struct MeshVertexBufferLayout { pub(crate) attribute_ids: Vec, + pub(crate) is_position_compressed: bool, pub(crate) is_normal_compressed: bool, pub(crate) is_tangent_compressed: bool, pub(crate) layout: VertexBufferLayout, @@ -93,6 +94,7 @@ impl MeshVertexBufferLayout { pub fn new(attribute_ids: Vec, layout: VertexBufferLayout) -> Self { Self { attribute_ids, + is_position_compressed: false, is_normal_compressed: false, is_tangent_compressed: false, layout, @@ -114,6 +116,10 @@ impl MeshVertexBufferLayout { &self.layout } + pub fn is_vertex_position_compressed(&self) -> bool { + self.is_position_compressed + } + pub fn is_vertex_normal_compressed(&self) -> bool { self.is_normal_compressed } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 8f7ae645cd6eb..cdfb74b1f7824 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -422,6 +422,9 @@ impl PrepassPipeline { } if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { shader_defs.push("VERTEX_POSITIONS".into()); + if layout.0.is_vertex_position_compressed() { + shader_defs.push("VERTEX_POSITIONS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); } // For directional light shadow map views, use unclipped depth via either the native GPU feature, diff --git a/crates/bevy_pbr/src/prepass/prepass_io.wgsl b/crates/bevy_pbr/src/prepass/prepass_io.wgsl index ef092bd40ee98..902e5e51a9ba8 100644 --- a/crates/bevy_pbr/src/prepass/prepass_io.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_io.wgsl @@ -40,8 +40,11 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, +#ifdef VERTEX_POSITIONS_COMPRESSED + @location(0) position: vec4, +#else @location(0) position: vec3, - +#endif #ifdef VERTEX_UVS_A @location(1) uv: vec2, #endif @@ -84,20 +87,22 @@ struct Vertex { fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { var uncompressed_vertex: UncompressedVertex; uncompressed_vertex.instance_index = vertex_in.instance_index; -#ifdef VERTEX_POSITIONS +#ifdef VERTEX_POSITIONS_COMPRESSED + uncompressed_vertex.position = bevy_pbr::mesh_functions::decompress_vertex_position(vertex_in.instance_index, vertex_in.position); +#else uncompressed_vertex.position = vertex_in.position; #endif #ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED - uncompressed_vertex.normal = bevy_pbr::utils::octahedral_decode(vertex_in.normal); + uncompressed_vertex.normal = bevy_pbr::mesh_functions::decompress_vertex_normal(vertex_in.normal); #else uncompressed_vertex.normal = vertex_in.normal; #endif #endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED - uncompressed_vertex.tangent = bevy_pbr::utils::octahedral_decode_tangent(vertex_in.tangent); + uncompressed_vertex.tangent = bevy_pbr::mesh_functions::decompress_vertex_tangent(vertex_in.tangent); #else uncompressed_vertex.tangent = vertex_in.tangent; #endif diff --git a/crates/bevy_pbr/src/render/forward_io.wgsl b/crates/bevy_pbr/src/render/forward_io.wgsl index 9859176141e17..bc709a2576074 100644 --- a/crates/bevy_pbr/src/render/forward_io.wgsl +++ b/crates/bevy_pbr/src/render/forward_io.wgsl @@ -32,8 +32,12 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, #ifdef VERTEX_POSITIONS +#ifdef VERTEX_POSITIONS_COMPRESSED + @location(0) position: vec4, +#else @location(0) position: vec3, #endif +#endif #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED @location(1) normal: vec2, @@ -70,11 +74,15 @@ fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { var uncompressed_vertex: UncompressedVertex; uncompressed_vertex.instance_index = vertex_in.instance_index; #ifdef VERTEX_POSITIONS +#ifdef VERTEX_POSITIONS_COMPRESSED + uncompressed_vertex.position = bevy_pbr::mesh_functions::decompress_vertex_position(vertex_in.instance_index, vertex_in.position); +#else uncompressed_vertex.position = vertex_in.position; #endif +#endif #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED - uncompressed_vertex.normal = bevy_pbr::utils::octahedral_decode(vertex_in.normal); + uncompressed_vertex.normal = bevy_pbr::mesh_functions::decompress_vertex_normal(vertex_in.normal); #else uncompressed_vertex.normal = vertex_in.normal; #endif @@ -87,7 +95,7 @@ fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { #endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED - uncompressed_vertex.tangent = bevy_pbr::utils::octahedral_decode_tangent(vertex_in.tangent); + uncompressed_vertex.tangent = bevy_pbr::mesh_functions::decompress_vertex_tangent(vertex_in.tangent); #else uncompressed_vertex.tangent = vertex_in.tangent; #endif diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 73966982b0548..3ca330f024367 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -26,7 +26,7 @@ use bevy_light::{ EnvironmentMapLight, IrradianceVolume, NotShadowCaster, NotShadowReceiver, ShadowFilteringMethod, TransmittedShadowReceiver, }; -use bevy_math::{Affine3, Rect, UVec2, Vec3, Vec4}; +use bevy_math::{bounding::BoundingVolume, Affine3, Rect, UVec2, Vec3, Vec4}; use bevy_mesh::{ skinning::SkinnedMesh, BaseMeshPipelineKey, Mesh, Mesh3d, MeshTag, MeshVertexBufferLayoutRef, VertexAttributeDescriptor, @@ -485,6 +485,12 @@ pub struct MeshUniform { pub tag: u32, /// Padding. pub pad: u32, + /// AABB center for decompressing vertex positions. + pub aabb_center: Vec3, + pub pad1: u32, + /// AABB half extents for decompressing vertex positions. + pub aabb_half_extents: Vec3, + pub pad2: u32, } /// Information that has to be transferred from CPU to GPU in order to produce @@ -549,8 +555,13 @@ pub struct MeshInputUniform { pub timestamp: u32, /// User supplied tag to identify this mesh instance. pub tag: u32, - /// Padding. pub pad: u32, + /// AABB for decompressing positions. + pub aabb_center: Vec3, + pub pad1: u32, + pub aabb_half_extents: Vec3, + pub pad2: u32, + pub pad3: [u32; 4], } /// Information about each mesh instance needed to cull it on GPU. @@ -584,6 +595,7 @@ impl MeshUniform { maybe_lightmap: Option<(LightmapSlotIndex, Rect)>, current_skin_index: Option, tag: Option, + mesh: Option<&RenderMesh>, ) -> Self { let (local_from_world_transpose_a, local_from_world_transpose_b) = mesh_transforms.world_from_local.inverse_transpose_3x3(); @@ -591,6 +603,7 @@ impl MeshUniform { None => u16::MAX, Some((slot_index, _)) => slot_index.into(), }; + let aabb = mesh.map(|m| m.aabb).unwrap_or_default(); Self { world_from_local: mesh_transforms.world_from_local.to_transpose(), @@ -604,7 +617,11 @@ impl MeshUniform { material_and_lightmap_bind_group_slot: u32::from(material_bind_group_slot) | ((lightmap_bind_group_slot as u32) << 16), tag: tag.unwrap_or(0), + aabb_center: aabb.map(|a| a.center().into()).unwrap_or_default(), + aabb_half_extents: aabb.map(|a| a.half_size().into()).unwrap_or_default(), pad: 0, + pad1: 0, + pad2: 0, } } } @@ -1115,6 +1132,7 @@ impl RenderMeshInstanceGpuBuilder { skin_uniforms: &SkinUniforms, timestamp: FrameCount, meshes_to_reextract_next_frame: &mut MeshesToReextractNextFrame, + meshes: &RenderAssets, ) -> Option { let (first_vertex_index, vertex_count) = match mesh_allocator.mesh_vertex_slice(&self.shared.mesh_asset_id) { @@ -1186,7 +1204,22 @@ impl RenderMeshInstanceGpuBuilder { self.shared.material_bindings_index.slot, ) | ((lightmap_slot as u32) << 16), tag: self.shared.tag, + aabb_center: meshes + .get(self.shared.mesh_asset_id) + .map(|a| a.aabb.map(|aabb| aabb.center().into()).unwrap_or_default()) + .unwrap_or_default(), + aabb_half_extents: meshes + .get(self.shared.mesh_asset_id) + .map(|a| { + a.aabb + .map(|aabb| aabb.half_size().into()) + .unwrap_or_default() + }) + .unwrap_or_default(), pad: 0, + pad1: 0, + pad2: 0, + pad3: [0; 4], }; // Did the last frame contain this entity as well? @@ -1670,6 +1703,7 @@ pub fn collect_meshes_for_gpu_building( skin_uniforms: Res, frame_count: Res, mut meshes_to_reextract_next_frame: ResMut, + meshes: Res>, ) { let RenderMeshInstances::GpuBuilding(render_mesh_instances) = render_mesh_instances.into_inner() @@ -1714,6 +1748,7 @@ pub fn collect_meshes_for_gpu_building( &skin_uniforms, *frame_count, &mut meshes_to_reextract_next_frame, + &meshes, ); } @@ -1743,6 +1778,7 @@ pub fn collect_meshes_for_gpu_building( &skin_uniforms, *frame_count, &mut meshes_to_reextract_next_frame, + &meshes, ) else { continue; }; @@ -1915,7 +1951,7 @@ impl GetBatchData for MeshPipeline { type BufferData = MeshUniform; fn get_batch_data( - (mesh_instances, lightmaps, _, mesh_allocator, skin_uniforms): &SystemParamItem< + (mesh_instances, lightmaps, meshes, mesh_allocator, skin_uniforms): &SystemParamItem< Self::Param, >, (_entity, main_entity): (Entity, MainEntity), @@ -1946,6 +1982,7 @@ impl GetBatchData for MeshPipeline { maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), current_skin_index, Some(mesh_instance.tag), + meshes.get(mesh_instance.mesh_asset_id), ), mesh_instance.should_batch().then_some(( material_bind_group_index.group, @@ -1986,7 +2023,7 @@ impl GetFullBatchData for MeshPipeline { } fn get_binned_batch_data( - (mesh_instances, lightmaps, _, mesh_allocator, skin_uniforms): &SystemParamItem< + (mesh_instances, lightmaps, meshes, mesh_allocator, skin_uniforms): &SystemParamItem< Self::Param, >, main_entity: MainEntity, @@ -2014,6 +2051,7 @@ impl GetFullBatchData for MeshPipeline { maybe_lightmap.map(|lightmap| (lightmap.slot_index, lightmap.uv_rect)), current_skin_index, Some(mesh_instance.tag), + meshes.get(mesh_instance.mesh_asset_id), )) } @@ -2305,6 +2343,9 @@ impl SpecializedMeshPipeline for MeshPipeline { if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { shader_defs.push("VERTEX_POSITIONS".into()); + if layout.0.is_vertex_position_compressed() { + shader_defs.push("VERTEX_POSITIONS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); } diff --git a/crates/bevy_pbr/src/render/mesh_functions.wgsl b/crates/bevy_pbr/src/render/mesh_functions.wgsl index 4514f1270c163..30d7ed37381a5 100644 --- a/crates/bevy_pbr/src/render/mesh_functions.wgsl +++ b/crates/bevy_pbr/src/render/mesh_functions.wgsl @@ -24,7 +24,7 @@ fn get_previous_world_from_local(instance_index: u32) -> mat4x4 { fn get_local_from_world(instance_index: u32) -> mat4x4 { // the model matrix is translation * rotation * scale - // the inverse is then scale^-1 * rotation ^-1 * translation^-1 + // the inverse is then scale^-1 * rotation ^-1 * translation^-1 // the 3x3 matrix only contains the information for the rotation and scale let inverse_model_3x3 = transpose(mat2x4_f32_to_mat3x3_unpack( mesh[instance_index].local_from_world_transpose_a, @@ -166,3 +166,17 @@ fn get_tag(instance_index: u32) -> u32 { return mesh[instance_index].tag; } #endif + +fn decompress_vertex_position(instance_index: u32, compressed_position: vec4) -> vec3 { + let aabb_center = bevy_pbr::mesh_bindings::mesh[instance_index].aabb_center; + let aabb_half_extents = bevy_pbr::mesh_bindings::mesh[instance_index].aabb_half_extents; + return aabb_center + aabb_half_extents * compressed_position.xyz; +} + +fn decompress_vertex_normal(compressed_normal: vec2) -> vec3 { + return bevy_pbr::utils::octahedral_decode(compressed_normal); +} + +fn decompress_vertex_tangent(compressed_tangent: vec2) -> vec4 { + return bevy_pbr::utils::octahedral_decode_tangent(compressed_tangent); +} diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 03986c0d0795b..872163c3d3a0f 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -373,4 +373,6 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { output[mesh_output_index].material_and_lightmap_bind_group_slot = current_input[input_index].material_and_lightmap_bind_group_slot; output[mesh_output_index].tag = current_input[input_index].tag; + output[mesh_output_index].aabb_center = current_input[input_index].aabb_center; + output[mesh_output_index].aabb_half_extents = current_input[input_index].aabb_half_extents; } diff --git a/crates/bevy_pbr/src/render/mesh_types.wgsl b/crates/bevy_pbr/src/render/mesh_types.wgsl index 4c85192ddd9a2..304b32c0315b4 100644 --- a/crates/bevy_pbr/src/render/mesh_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_types.wgsl @@ -24,6 +24,11 @@ struct Mesh { // User supplied index to identify the mesh instance tag: u32, pad: u32, + // AABB for decompressing positions. + aabb_center: vec3, + pad1_: u32, + aabb_half_extents: vec3, + pad2_: u32, }; #ifdef SKINNED diff --git a/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl b/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl index a597fb0537228..ef772a20be410 100644 --- a/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl +++ b/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl @@ -22,6 +22,12 @@ struct MeshInput { // User supplied index to identify the mesh instance tag: u32, pad: u32, + /// AABB for decompressing positions. + aabb_center: vec3, + pad1_: u32, + aabb_half_extents: vec3, + pad2_: u32, + pad3_: vec4, } // The `wgpu` indirect parameters structure. This is a union of two structures. diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 2c2f84a0a9e66..6745fbb623b09 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -14,6 +14,7 @@ use bevy_ecs::{ SystemParamItem, }, }; +use bevy_math::bounding::Aabb3d; #[cfg(feature = "morph")] use bevy_mesh::morph::{MeshMorphWeights, MorphWeights}; use bevy_mesh::*; @@ -92,6 +93,9 @@ pub struct RenderMesh { /// Combined with [`RenderMesh::buffer_info`], this specifies the complete /// layout of the buffers associated with this mesh. pub layout: MeshVertexBufferLayoutRef, + + /// AABB for decompressing vertex positions. None if the mesh doesn't compress positions. + pub aabb: Option, } impl RenderMesh { @@ -187,6 +191,7 @@ impl RenderAsset for RenderMesh { layout: mesh_vertex_buffer_layout, #[cfg(feature = "morph")] morph_targets, + aabb: mesh.calculate_aabb(), }) } } diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh.rs b/crates/bevy_sprite_render/src/mesh2d/mesh.rs index 80786a7387b6c..680ab12fd4a86 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite_render/src/mesh2d/mesh.rs @@ -21,7 +21,10 @@ use bevy_ecs::{ system::{lifetimeless::*, SystemParamItem}, }; use bevy_image::BevyDefault; -use bevy_math::{Affine3, Vec4}; +use bevy_math::{ + bounding::{Aabb3d, BoundingVolume}, + Affine3, Vec3, Vec4, +}; use bevy_mesh::{Mesh, Mesh2d, MeshTag, MeshVertexBufferLayoutRef}; use bevy_render::prelude::Msaa; use bevy_render::RenderSystems::PrepareAssets; @@ -207,10 +210,15 @@ pub struct Mesh2dUniform { pub local_from_world_transpose_b: f32, pub flags: u32, pub tag: u32, + /// AABB for decompressing positions. + pub aabb_center: Vec3, + pub pad1: u32, + pub aabb_half_extents: Vec3, + pub pad2: u32, } impl Mesh2dUniform { - fn from_components(mesh_transforms: &Mesh2dTransforms, tag: u32) -> Self { + fn from_components(mesh_transforms: &Mesh2dTransforms, tag: u32, aabb: Option) -> Self { let (local_from_world_transpose_a, local_from_world_transpose_b) = mesh_transforms.world_from_local.inverse_transpose_3x3(); Self { @@ -219,6 +227,12 @@ impl Mesh2dUniform { local_from_world_transpose_b, flags: mesh_transforms.flags, tag, + aabb_center: aabb.map(|aabb| aabb.center().into()).unwrap_or(Vec3::ZERO), + aabb_half_extents: aabb + .map(|aabb| aabb.half_size().into()) + .unwrap_or(Vec3::ZERO), + pad1: 0, + pad2: 0, } } } @@ -336,12 +350,19 @@ impl GetBatchData for Mesh2dPipeline { type BufferData = Mesh2dUniform; fn get_batch_data( - (mesh_instances, _, _): &SystemParamItem, + (mesh_instances, meshes, _): &SystemParamItem, (_entity, main_entity): (Entity, MainEntity), ) -> Option<(Self::BufferData, Option)> { let mesh_instance = mesh_instances.get(&main_entity)?; Some(( - Mesh2dUniform::from_components(&mesh_instance.transforms, mesh_instance.tag), + Mesh2dUniform::from_components( + &mesh_instance.transforms, + mesh_instance.tag, + meshes + .get(mesh_instance.mesh_asset_id) + .map(|m| m.aabb) + .unwrap_or(None), + ), mesh_instance.automatic_batching.then_some(( mesh_instance.material_bind_group_id, mesh_instance.mesh_asset_id, @@ -354,13 +375,17 @@ impl GetFullBatchData for Mesh2dPipeline { type BufferInputData = (); fn get_binned_batch_data( - (mesh_instances, _, _): &SystemParamItem, + (mesh_instances, meshes, _): &SystemParamItem, main_entity: MainEntity, ) -> Option { let mesh_instance = mesh_instances.get(&main_entity)?; Some(Mesh2dUniform::from_components( &mesh_instance.transforms, mesh_instance.tag, + meshes + .get(mesh_instance.mesh_asset_id) + .map(|m| m.aabb) + .unwrap_or(None), )) } @@ -504,6 +529,9 @@ impl SpecializedMeshPipeline for Mesh2dPipeline { if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { shader_defs.push("VERTEX_POSITIONS".into()); + if layout.0.is_vertex_position_compressed() { + shader_defs.push("VERTEX_POSITIONS_COMPRESSED".into()); + } vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); } diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl index 8c50d883f29a1..721343534810e 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl +++ b/crates/bevy_sprite_render/src/mesh2d/mesh2d.wgsl @@ -30,8 +30,12 @@ struct UncompressedVertex { struct Vertex { @builtin(instance_index) instance_index: u32, #ifdef VERTEX_POSITIONS +#ifdef VERTEX_POSITIONS_COMPRESSED + @location(0) position: vec4, +#else @location(0) position: vec3, #endif +#endif #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED @location(1) normal: vec2, @@ -57,12 +61,14 @@ struct Vertex { fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { var uncompressed_vertex: UncompressedVertex; uncompressed_vertex.instance_index = vertex_in.instance_index; -#ifdef VERTEX_POSITIONS +#ifdef VERTEX_POSITIONS_COMPRESSED + uncompressed_vertex.position = mesh_functions::decompress_vertex_position(vertex_in.instance_index, vertex_in.position); +#else uncompressed_vertex.position = vertex_in.position; #endif #ifdef VERTEX_NORMALS #ifdef VERTEX_NORMALS_COMPRESSED - uncompressed_vertex.normal = bevy_pbr::utils::octahedral_decode(vertex_in.normal); + uncompressed_vertex.normal = mesh_functions::decompress_vertex_normal(vertex_in.normal); #else uncompressed_vertex.normal = vertex_in.normal; #endif @@ -72,7 +78,7 @@ fn decompress_vertex(vertex_in: Vertex) -> UncompressedVertex { #endif #ifdef VERTEX_TANGENTS #ifdef VERTEX_TANGENTS_COMPRESSED - uncompressed_vertex.tangent = bevy_pbr::utils::octahedral_decode_tangent(vertex_in.tangent); + uncompressed_vertex.tangent = mesh_functions::decompress_vertex_tangent(vertex_in.tangent); #else uncompressed_vertex.tangent = vertex_in.tangent; #endif diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh2d_functions.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_functions.wgsl index dbd73fb171f3f..4abfd81ded7b9 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh2d_functions.wgsl +++ b/crates/bevy_sprite_render/src/mesh2d/mesh2d_functions.wgsl @@ -46,4 +46,42 @@ fn mesh2d_tangent_local_to_world(world_from_local: mat4x4, vertex_tangent: fn get_tag(instance_index: u32) -> u32 { return mesh[instance_index].tag; -} \ No newline at end of file +} + +fn decompress_vertex_position(instance_index: u32, compressed_position: vec4) -> vec3 { + let aabb_center = bevy_sprite::mesh2d_bindings::mesh[instance_index].aabb_center; + let aabb_half_extents = bevy_sprite::mesh2d_bindings::mesh[instance_index].aabb_half_extents; + return aabb_center + aabb_half_extents * compressed_position.xyz; +} + +fn decompress_vertex_normal(compressed_normal: vec2) -> vec3 { + return octahedral_decode(compressed_normal); +} + +fn decompress_vertex_tangent(compressed_tangent: vec2) -> vec4 { + return octahedral_decode_tangent(compressed_tangent); +} + +// For decoding normals or unit direction vectors from octahedral coordinates. +fn octahedral_decode(v: vec2) -> vec3 { + let f = v * 2.0 - 1.0; + return octahedral_decode_signed(f); +} + +// Like octahedral_decode, but for input in [-1, 1] instead of [0, 1]. +fn octahedral_decode_signed(v: vec2) -> vec3 { + var n = vec3(v.xy, 1.0 - abs(v.x) - abs(v.y)); + let t = saturate(-n.z); + let w = select(vec2(t), vec2(-t), n.xy >= vec2(0.0)); + n = vec3(n.xy + w, n.z); + return normalize(n); +} + +// Decode tangent vectors from octahedral coordinates and return the sign. The sign should have been encoded in y component using corresponding `octahedral_encode_tangent`. +fn octahedral_decode_tangent(v: vec2) -> vec4 { + var f = v; + f.y = f.y * 2.0 - 1.0; + let sign = select(-1.0, 1.0, f.y >= 0.0); + f.y = abs(f.y); + return vec4(octahedral_decode(f), sign); +} diff --git a/crates/bevy_sprite_render/src/mesh2d/mesh2d_types.wgsl b/crates/bevy_sprite_render/src/mesh2d/mesh2d_types.wgsl index e29264e0bf4f3..f0980745b9518 100644 --- a/crates/bevy_sprite_render/src/mesh2d/mesh2d_types.wgsl +++ b/crates/bevy_sprite_render/src/mesh2d/mesh2d_types.wgsl @@ -14,4 +14,9 @@ struct Mesh2d { // 'flags' is a bit field indicating various options. u32 is 32 bits so we have up to 32 options. flags: u32, tag: u32, + // AABB for decompressing positions. + aabb_center: vec3, + pad1_: u32, + aabb_half_extents: vec3, + pad2_: u32, }; diff --git a/examples/shader_advanced/custom_render_phase.rs b/examples/shader_advanced/custom_render_phase.rs index 866e96adca9da..8c1cc99b794ec 100644 --- a/examples/shader_advanced/custom_render_phase.rs +++ b/examples/shader_advanced/custom_render_phase.rs @@ -182,9 +182,14 @@ impl SpecializedMeshPipeline for StencilPipeline { key: Self::Key, layout: &MeshVertexBufferLayoutRef, ) -> Result { + let mut shader_defs = Vec::new(); // We will only use the position of the mesh in our shader so we only need to specify that let mut vertex_attributes = Vec::new(); if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + // Handle compressed vertex positions. + if layout.0.is_vertex_position_compressed() { + shader_defs.push("VERTEX_POSITIONS_COMPRESSED".into()); + } // Make sure this matches the shader location vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); } @@ -207,11 +212,13 @@ impl SpecializedMeshPipeline for StencilPipeline { ], vertex: VertexState { shader: self.shader_handle.clone(), + shader_defs: shader_defs.clone(), buffers: vec![vertex_buffer_layout], ..default() }, fragment: Some(FragmentState { shader: self.shader_handle.clone(), + shader_defs, targets: vec![Some(ColorTargetState { format: TextureFormat::bevy_default(), blend: None, @@ -340,7 +347,7 @@ impl GetBatchData for StencilPipeline { type BufferData = MeshUniform; fn get_batch_data( - (mesh_instances, _render_assets, mesh_allocator): &SystemParamItem, + (mesh_instances, meshes, mesh_allocator): &SystemParamItem, (_entity, main_entity): (Entity, MainEntity), ) -> Option<(Self::BufferData, Option)> { let RenderMeshInstances::CpuBuilding(ref mesh_instances) = **mesh_instances else { @@ -356,24 +363,15 @@ impl GetBatchData for StencilPipeline { Some(mesh_vertex_slice) => mesh_vertex_slice.range.start, None => 0, }; - let mesh_uniform = { - let mesh_transforms = &mesh_instance.transforms; - let (local_from_world_transpose_a, local_from_world_transpose_b) = - mesh_transforms.world_from_local.inverse_transpose_3x3(); - MeshUniform { - world_from_local: mesh_transforms.world_from_local.to_transpose(), - previous_world_from_local: mesh_transforms.previous_world_from_local.to_transpose(), - lightmap_uv_rect: UVec2::ZERO, - local_from_world_transpose_a, - local_from_world_transpose_b, - flags: mesh_transforms.flags, - first_vertex_index, - current_skin_index: u32::MAX, - material_and_lightmap_bind_group_slot: 0, - tag: 0, - pad: 0, - } - }; + let mesh_uniform = MeshUniform::new( + &mesh_instance.transforms, + first_vertex_index, + mesh_instance.material_bindings_index.slot, + None, + None, + Some(mesh_instance.tag), + meshes.get(mesh_instance.mesh_asset_id), + ); Some((mesh_uniform, None)) } } @@ -426,6 +424,7 @@ impl GetFullBatchData for StencilPipeline { None, None, None, + None, )) } diff --git a/examples/shader_advanced/specialized_mesh_pipeline.rs b/examples/shader_advanced/specialized_mesh_pipeline.rs index ee5abdb54d647..5815e8600adc6 100644 --- a/examples/shader_advanced/specialized_mesh_pipeline.rs +++ b/examples/shader_advanced/specialized_mesh_pipeline.rs @@ -188,9 +188,14 @@ impl SpecializedMeshPipeline for CustomMeshPipeline { mesh_key: Self::Key, layout: &MeshVertexBufferLayoutRef, ) -> Result { + let mut shader_defs = Vec::new(); // Define the vertex attributes based on a standard bevy [`Mesh`] let mut vertex_attributes = Vec::new(); if layout.0.contains(Mesh::ATTRIBUTE_POSITION) { + // Handle compressed vertex positions. + if layout.0.is_vertex_position_compressed() { + shader_defs.push("VERTEX_POSITIONS_COMPRESSED".into()); + } // Make sure this matches the shader location vertex_attributes.push(Mesh::ATTRIBUTE_POSITION.at_shader_location(0)); } @@ -214,12 +219,14 @@ impl SpecializedMeshPipeline for CustomMeshPipeline { ], vertex: VertexState { shader: self.shader_handle.clone(), + shader_defs: shader_defs.clone(), // Customize how to store the meshes' vertex attributes in the vertex buffer buffers: vec![vertex_buffer_layout], ..default() }, fragment: Some(FragmentState { shader: self.shader_handle.clone(), + shader_defs, targets: vec![Some(ColorTargetState { // This isn't required, but bevy supports HDR and non-HDR rendering // so it's generally recommended to specialize the pipeline for that From ad52d941164141a385b17119ca38e5cb6ee3c0a9 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 2 Dec 2025 10:55:10 +0800 Subject: [PATCH 7/8] re-add unorm16x2 UV option --- crates/bevy_mesh/src/mesh.rs | 119 ++++++++++++++---- .../bevy_pbr/src/meshlet/instance_manager.rs | 1 + .../mesh_preprocess_types.wgsl | 2 +- crates/bevy_render/src/mesh/mod.rs | 3 +- 4 files changed, 96 insertions(+), 29 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 6d732682b660b..fa9aec7097ae5 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -159,9 +159,11 @@ pub struct Mesh { /// If the corresponding flag is enabled: /// Position will be Snorm16x4 relative to the mesh's AABB. The w component is unused. /// Normal and tangent will be Unorm16x2 using octahedral encoding. - /// UV0 and UV1 will be Float16x2. + /// UV0 and UV1 will be Unorm16x2 or Float16x2. /// Joint weight will be Unorm16x4. - /// Color will be Float16x4 or Unorm8x4 + /// Color will be Float16x4 or Unorm8x4. + /// + /// For UVs compression, it's recommended to use Unorm16x2 for better precision if you don't need texture coordinates that go beyond the range of [0, 1]. The Float16x2 format may cause precision issues for textures larger than 1024x1024. pub attribute_compression: MeshAttributeCompressionFlags, } @@ -171,14 +173,20 @@ bitflags::bitflags! { #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] #[reflect(opaque)] #[reflect(Hash, Clone, PartialEq, Debug)] - pub struct MeshAttributeCompressionFlags: u8 { + pub struct MeshAttributeCompressionFlags: u16 { const COMPRESS_NONE = 0; 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_JOINT_WEIGHT = 1 << 5; + const COMPRESS_JOINT_WEIGHT = 1 << 3; + + const COMPRESS_UV0_RESERVED_BIT = Self::COMPRESS_UV0_MASK_BIT << Self::COMPRESS_UV0_SHIFT_BIT; + const COMPRESS_UV0_UNORM16 = 1 << Self::COMPRESS_UV0_SHIFT_BIT; + const COMPRESS_UV0_FLOAT16 = 2 << Self::COMPRESS_UV0_SHIFT_BIT; + + const COMPRESS_UV1_RESERVED_BIT = Self::COMPRESS_UV1_MASK_BIT << Self::COMPRESS_UV1_SHIFT_BIT; + const COMPRESS_UV1_UNORM16 = 1 << Self::COMPRESS_UV1_SHIFT_BIT; + const COMPRESS_UV1_FLOAT16 = 2 << Self::COMPRESS_UV1_SHIFT_BIT; 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; @@ -186,9 +194,17 @@ bitflags::bitflags! { } } 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_UV0_MASK_BIT: u16 = 0b11; + const COMPRESS_UV0_SHIFT_BIT: u16 = + Self::COMPRESS_JOINT_WEIGHT.bits().trailing_zeros() as u16 + 1; + + const COMPRESS_UV1_MASK_BIT: u16 = 0b11; + const COMPRESS_UV1_SHIFT_BIT: u16 = + Self::COMPRESS_UV0_MASK_BIT.count_ones() as u16 + Self::COMPRESS_UV0_SHIFT_BIT; + + const COMPRESS_COLOR_MASK_BIT: u16 = 0b11; + const COMPRESS_COLOR_SHIFT_BIT: u16 = + Self::COMPRESS_UV1_MASK_BIT.count_ones() as u16 + Self::COMPRESS_UV1_SHIFT_BIT; } impl Default for MeshAttributeCompressionFlags { @@ -561,7 +577,8 @@ impl Mesh { vertex_count.unwrap_or(0) } - /// Calculates the axis-aligned bounding box of the mesh. Panics if the positions attribute is not Float32x3. + /// Calculates the axis-aligned bounding box of the mesh. + /// Returns None if the position attribute is empty or the format isn't Float32x3. pub fn calculate_aabb(&self) -> Option { let positions = self.attribute(Self::ATTRIBUTE_POSITION)?; match positions { @@ -573,9 +590,7 @@ impl Mesh { }); Some(Aabb3d { min, max }) } - _ => { - unreachable!() - } + _ => None, } } @@ -602,16 +617,40 @@ impl Mesh { id if id == Self::ATTRIBUTE_UV_0.id && self .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => + .intersects(MeshAttributeCompressionFlags::COMPRESS_UV0_RESERVED_BIT) => { - Some(VertexFormat::Float16x2) + if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV0_UNORM16) + { + Some(VertexFormat::Unorm16x2) + } else if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV0_FLOAT16) + { + Some(VertexFormat::Float16x2) + } else { + unreachable!() + } } id if id == Self::ATTRIBUTE_UV_1.id && self .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => + .intersects(MeshAttributeCompressionFlags::COMPRESS_UV1_RESERVED_BIT) => { - Some(VertexFormat::Float16x2) + if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV1_UNORM16) + { + Some(VertexFormat::Unorm16x2) + } else if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV1_FLOAT16) + { + Some(VertexFormat::Float16x2) + } else { + unreachable!() + } } id if id == Self::ATTRIBUTE_TANGENT.id && self @@ -621,20 +660,22 @@ impl Mesh { Some(VertexFormat::Unorm16x2) } id if id == Self::ATTRIBUTE_COLOR.id - && (self + && self .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) - || self - .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16)) => + .intersects(MeshAttributeCompressionFlags::COMPRESS_COLOR_RESERVED_BIT) => { if self .attribute_compression .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_FLOAT16) { Some(VertexFormat::Float16x4) - } else { + } else if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_COLOR_UNORM8) + { Some(VertexFormat::Unorm8x4) + } else { + unreachable!() } } id if id == Self::ATTRIBUTE_JOINT_WEIGHT.id @@ -688,16 +729,40 @@ impl Mesh { id if id == Self::ATTRIBUTE_UV_0.id && self .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV0) => + .intersects(MeshAttributeCompressionFlags::COMPRESS_UV0_RESERVED_BIT) => { - Some(attribute_values.create_f16_values()) + if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV0_UNORM16) + { + Some(attribute_values.create_unorm16_values()) + } else if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV0_FLOAT16) + { + Some(attribute_values.create_f16_values()) + } else { + unreachable!() + } } id if id == Self::ATTRIBUTE_UV_1.id && self .attribute_compression - .contains(MeshAttributeCompressionFlags::COMPRESS_UV1) => + .intersects(MeshAttributeCompressionFlags::COMPRESS_UV1_RESERVED_BIT) => { - Some(attribute_values.create_f16_values()) + if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV1_UNORM16) + { + Some(attribute_values.create_unorm16_values()) + } else if self + .attribute_compression + .contains(MeshAttributeCompressionFlags::COMPRESS_UV1_FLOAT16) + { + Some(attribute_values.create_f16_values()) + } else { + unreachable!() + } } id if id == Self::ATTRIBUTE_TANGENT.id && self diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index 6747732f26c1d..cbfaccff12a80 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -133,6 +133,7 @@ impl InstanceManager { None, None, None, + None, ); // Append instance data diff --git a/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl b/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl index ef772a20be410..34bffdf077adf 100644 --- a/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl +++ b/crates/bevy_render/src/experimental/occlusion_culling/mesh_preprocess_types.wgsl @@ -22,7 +22,7 @@ struct MeshInput { // User supplied index to identify the mesh instance tag: u32, pad: u32, - /// AABB for decompressing positions. + // AABB for decompressing positions. aabb_center: vec3, pad1_: u32, aabb_half_extents: vec3, diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 6745fbb623b09..8bc4854ddfd8d 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -94,7 +94,8 @@ pub struct RenderMesh { /// layout of the buffers associated with this mesh. pub layout: MeshVertexBufferLayoutRef, - /// AABB for decompressing vertex positions. None if the mesh doesn't compress positions. + /// AABB used for decompressing vertex positions. + /// None if the positions of the mesh is empty or the format isn't Float32x3. pub aabb: Option, } From e8cf7200334146af5fae127af474d10c15639237 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 2 Dec 2025 11:15:01 +0800 Subject: [PATCH 8/8] docs --- crates/bevy_mesh/src/mesh.rs | 2 +- crates/bevy_mesh/src/vertex.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index fa9aec7097ae5..e60236a3e1176 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -163,7 +163,7 @@ pub struct Mesh { /// Joint weight will be Unorm16x4. /// Color will be Float16x4 or Unorm8x4. /// - /// For UVs compression, it's recommended to use Unorm16x2 for better precision if you don't need texture coordinates that go beyond the range of [0, 1]. The Float16x2 format may cause precision issues for textures larger than 1024x1024. + /// For UVs compression, it's recommended to use Unorm16x2 for better precision if you don't need texture coordinates that go beyond the range of [0, 1]. The Float16x2 format is only suitable for tiling small textures otherwise it may have precision issues. pub attribute_compression: MeshAttributeCompressionFlags, } diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 537efb434772c..6a8e1024ee52d 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -632,7 +632,7 @@ fn octahedral_encode(v: Vec3) -> Vec2 { /// Encode tangent vectors as octahedral coordinates. sign is encoded in y component. fn octahedral_encode_tangent(v: Vec3, sign: f32) -> Vec2 { - // Code from Godot, https://github.com/godotengine/godot/pull/73265 + // Code references Godot, the bias is from https://github.com/godotengine/godot/pull/73265 let bias = 1.0 / 32767.0; let mut n_xy = octahedral_encode(v); n_xy.y = n_xy.y.max(bias);