Skip to content

Commit 951b232

Browse files
committed
Add option to compress vertex normal tangent to axis angle
1 parent 457b918 commit 951b232

13 files changed

Lines changed: 626 additions & 130 deletions

File tree

crates/bevy_mesh/src/mesh.rs

Lines changed: 280 additions & 76 deletions
Large diffs are not rendered by default.

crates/bevy_mesh/src/vertex.rs

Lines changed: 129 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use bevy_derive::EnumVariantMeta;
33
use bevy_ecs::resource::Resource;
44
use bevy_math::{
55
bounding::{Aabb2d, Aabb3d, BoundingVolume},
6-
vec2, Vec2, Vec3, Vec3A, Vec3Swizzles,
6+
ops, vec2, vec3, vec4, Mat3, Quat, Vec2, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles,
77
};
88
#[cfg(feature = "serialize")]
99
use bevy_platform::collections::HashMap;
@@ -1155,8 +1155,10 @@ pub fn octahedral_encode_signed(v: Vec3) -> Vec2 {
11551155

11561156
/// Encode tangent vectors as octahedral coordinates with range [-1, 1]. The sign is encoded in y component. Use [`octahedral_decode_tangent`] to decode.
11571157
pub fn octahedral_encode_tangent(v: Vec3, sign: f32) -> Vec2 {
1158-
// Bias to ensure that encoding as unorm16 preserves the sign. See https://github.com/godotengine/godot/pull/73265
1159-
let bias = 1.0 / 32767.0;
1158+
// Bias to ensure that encoding as snorm16 preserves the sign.
1159+
let bits = 16.;
1160+
let bias = 1. / (ops::powf(2.0, bits - 1.) - 1.);
1161+
11601162
let mut n_xy = octahedral_encode_signed(v);
11611163
// Map y to always be positive.
11621164
n_xy.y = n_xy.y * 0.5 + 0.5;
@@ -1184,12 +1186,60 @@ pub fn octahedral_decode_tangent(v: Vec2) -> (Vec3, f32) {
11841186
(octahedral_decode_signed(f), sign)
11851187
}
11861188

1189+
/// Convert the normal and tangent to the equivalent axis-angle representation.
1190+
/// The range of angle is [-2pi, 2pi], where the sign represents the handedness of the tangent.
1191+
pub fn normal_tangent_to_axis_angle(normal: Vec3, tangent_signed: Vec4) -> (Vec3, f32) {
1192+
// Bias to ensure that encoding as snorm16 preserves the sign.
1193+
let bits = 16.;
1194+
let bias = 1. / (ops::powf(2.0, bits - 1.) - 1.);
1195+
1196+
let tangent = tangent_signed.xyz();
1197+
let bitangent = normal.cross(tangent);
1198+
let (axis, angle) =
1199+
Quat::from_mat3(&Mat3::from_cols(tangent, bitangent, normal)).to_axis_angle();
1200+
let angle = angle
1201+
.rem_euclid(2.0 * core::f32::consts::PI)
1202+
.max(bias * 2.0 * core::f32::consts::PI);
1203+
1204+
(axis, angle * tangent_signed.w)
1205+
}
1206+
1207+
/// Convert the axis-angle representation back to normal and tangent.
1208+
/// The range of angle is [-2pi, 2pi], where the sign represents the handedness of the tangent.
1209+
pub fn axis_angle_to_normal_tangent(axis: Vec3, angle: f32) -> (Vec3, Vec4) {
1210+
let sign = if angle >= 0.0 { 1.0 } else { -1.0 };
1211+
// References the source code of `Mat3::from_quat(Quat::from_axis_angle(axis, angle))`
1212+
let angle = angle * 0.5 * sign;
1213+
let c = ops::cos(angle);
1214+
let s = ops::sin(angle);
1215+
let v = axis * s;
1216+
let rotation = vec4(v.x, v.y, v.z, c);
1217+
let x2 = rotation.x + rotation.x;
1218+
let y2 = rotation.y + rotation.y;
1219+
let z2 = rotation.z + rotation.z;
1220+
let xx = rotation.x * x2;
1221+
let xy = rotation.x * y2;
1222+
let xz = rotation.x * z2;
1223+
let yy = rotation.y * y2;
1224+
let yz = rotation.y * z2;
1225+
let zz = rotation.z * z2;
1226+
let wx = rotation.w * x2;
1227+
let wy = rotation.w * y2;
1228+
let wz = rotation.w * z2;
1229+
1230+
let tangent = vec3(1.0 - (yy + zz), xy + wz, xz - wy);
1231+
let normal = vec3(xz + wy, yz - wx, 1.0 - (xx + yy));
1232+
1233+
(normal, tangent.extend(sign))
1234+
}
1235+
11871236
#[cfg(test)]
11881237
mod tests {
1189-
use bevy_math::{vec2, vec3, Vec4Swizzles};
1238+
use bevy_math::{vec2, vec3, Mat3, Quat, Vec3, Vec4, Vec4Swizzles};
11901239

11911240
use crate::{
1192-
octahedral_decode_signed, octahedral_decode_tangent,
1241+
axis_angle_to_normal_tangent, normal_tangent_to_axis_angle, octahedral_decode_signed,
1242+
octahedral_decode_tangent,
11931243
vertex::{octahedral_encode_signed, octahedral_encode_tangent},
11941244
};
11951245

@@ -1198,18 +1248,21 @@ mod tests {
11981248
let vectors = [
11991249
vec3(1.0, 2.0, 3.0).normalize().extend(1.0),
12001250
vec3(1.0, 0.0, 0.0).extend(-1.0),
1251+
vec3(0.0, 1.0, 0.0).extend(-1.0),
12011252
vec3(0.0, 0.0, -1.0).extend(1.0),
12021253
vec3(0.0, 0.0, -1.0).extend(-1.0),
12031254
];
12041255
let expected_encoded_normals = [
12051256
vec2(0.16666667, 0.33333334),
12061257
vec2(1.0, 0.0),
1258+
vec2(0.0, 1.0),
12071259
vec2(-1.0, -1.0),
12081260
vec2(-1.0, -1.0),
12091261
];
12101262
let expected_encoded_tangents = [
12111263
vec2(0.16666667, 0.6666667),
12121264
vec2(1.0, -0.5),
1265+
vec2(0.0, -1.0),
12131266
vec2(-1.0, 3.051851e-5),
12141267
vec2(-1.0, -3.051851e-5),
12151268
];
@@ -1226,4 +1279,75 @@ mod tests {
12261279
assert!(decoded_tangent.distance(v.xyz()) < 1e-4);
12271280
}
12281281
}
1282+
1283+
pub fn axis_angle_to_normal_tangent_glam(axis: Vec3, angle: f32) -> (Vec3, Vec4) {
1284+
let sign = if angle >= 0.0 { 1.0 } else { -1.0 };
1285+
let tbn = Mat3::from_quat(Quat::from_axis_angle(axis, angle * sign));
1286+
(tbn.col(2), tbn.col(0).extend(sign))
1287+
}
1288+
1289+
#[test]
1290+
fn normal_tangent_axis_angle_encode_decode() {
1291+
let normal_tangent = [
1292+
(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0).extend(1.0)),
1293+
(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0).extend(-1.0)),
1294+
(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0).extend(1.0)),
1295+
(vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0).extend(-1.0)),
1296+
(vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0).extend(1.0)),
1297+
(vec3(1.0, 0.0, 0.0), vec3(0.0, -1.0, 0.0).extend(1.0)),
1298+
(
1299+
vec3(1.0, 1.0, 1.0).normalize(),
1300+
vec3(1.0, -1.0, 0.0).normalize().extend(1.0),
1301+
),
1302+
(
1303+
vec3(1.0, 2.0, 0.0).normalize(),
1304+
vec3(0.0, 0.0, 1.0).extend(-1.0),
1305+
),
1306+
(
1307+
vec3(0.0, 1.0, 1.0).normalize(),
1308+
vec3(1.0, 0.0, 0.0).extend(1.0),
1309+
),
1310+
(
1311+
vec3(-1.0, 1.0, 1.0).normalize(),
1312+
vec3(1.0, 1.0, 0.0).normalize().extend(-1.0),
1313+
),
1314+
(
1315+
vec3(3.0, 1.0, 2.0).normalize(),
1316+
vec3(0.0, 1.0, -0.5).normalize().extend(1.0),
1317+
),
1318+
];
1319+
1320+
#[expect(
1321+
clippy::approx_constant,
1322+
reason = "The values are taken from the test results"
1323+
)]
1324+
let expected_axis_angle = [
1325+
(vec3(0.7071068, 0.0, 0.7071068), 3.1415927),
1326+
(vec3(0.7071068, 0.0, 0.7071068), -3.1415927),
1327+
(vec3(1.0, 0.0, 0.0), 3.051851e-5),
1328+
(vec3(1.0, 0.0, 0.0), -3.051851e-5),
1329+
(vec3(0.57735026, 0.57735026, 0.57735026), 4.1887903),
1330+
(vec3(0.57735026, -0.57735026, 0.57735026), 4.1887903),
1331+
(vec3(-0.7429061, 0.3077218, -0.5944728), 1.2171159),
1332+
(vec3(0.64793617, 0.40044653, 0.64793617), -3.9033751),
1333+
(vec3(-1.0, 0.0, 0.0), 0.7853981),
1334+
(vec3(-0.7429061, -0.3077218, 0.5944728), -1.2171159),
1335+
(vec3(0.22525999, 0.6253928, 0.7470889), 1.6242763),
1336+
];
1337+
1338+
for (i, &(normal, tangent)) in normal_tangent.iter().enumerate() {
1339+
let (axis, angle) = normal_tangent_to_axis_angle(normal, tangent);
1340+
assert_eq!(angle.signum(), tangent.w.signum());
1341+
assert!(axis.distance(expected_axis_angle[i].0) < 1e-8);
1342+
1343+
let (decoded_normal, decoded_tangent) = axis_angle_to_normal_tangent(axis, angle);
1344+
let (decoded_normal_glam, decoded_tangent_glam) =
1345+
axis_angle_to_normal_tangent_glam(axis, angle);
1346+
1347+
assert!(decoded_normal.distance(normal) < 1e-3);
1348+
assert!(decoded_tangent.distance(tangent) < 1e-3);
1349+
assert!(decoded_normal_glam.distance(normal) < 1e-3);
1350+
assert!(decoded_tangent_glam.distance(tangent) < 1e-3);
1351+
}
1352+
}
12291353
}

crates/bevy_pbr/src/prepass/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,14 @@ impl PrepassPipeline {
535535
}
536536
vertex_attributes.push(Mesh::ATTRIBUTE_TANGENT.at_shader_location(4));
537537
}
538+
if layout
539+
.0
540+
.get_attribute_compression()
541+
.contains(MeshAttributeCompressionFlags::PACKED_AXIS_ANGLE_TBN)
542+
{
543+
shader_defs.push("VERTEX_TANGENTS".into());
544+
shader_defs.push("VERTEX_PACKED_AXIS_ANGLE_TBN".into());
545+
}
538546
}
539547
if mesh_key
540548
.intersects(MeshPipelineKey::MOTION_VECTOR_PREPASS | MeshPipelineKey::DEFERRED_PREPASS)

crates/bevy_pbr/src/prepass/prepass_io.wgsl

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@ struct UncompressedVertex {
1818
#ifdef VERTEX_NORMALS
1919
@location(3) normal: vec3<f32>,
2020
#endif
21+
2122
#ifdef VERTEX_TANGENTS
2223
@location(4) tangent: vec4<f32>,
24+
#else ifdef VERTEX_PACKED_AXIS_ANGLE_TBN
25+
@location(4) tangent: vec4<f32>,
2326
#endif
27+
2428
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
2529

2630
#ifdef SKINNED
@@ -39,26 +43,20 @@ struct UncompressedVertex {
3943

4044
struct Vertex {
4145
@builtin(instance_index) instance_index: u32,
46+
47+
#ifdef VERTEX_PACKED_AXIS_ANGLE_TBN
48+
@location(0) compressed_position_angle: vec4<f32>,
49+
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
50+
@location(3) compressed_axis: vec2<f32>,
51+
#endif
52+
53+
#else // VERTEX_PACKED_AXIS_ANGLE_TBN
54+
4255
#ifdef VERTEX_POSITIONS_COMPRESSED
4356
@location(0) compressed_position: vec4<f32>,
4457
#else
4558
@location(0) position: vec3<f32>,
4659
#endif
47-
#ifdef VERTEX_UVS_A
48-
#ifdef VERTEX_UVS_A_COMPRESSED
49-
@location(1) compressed_uv: vec2<f32>,
50-
#else
51-
@location(1) uv: vec2<f32>,
52-
#endif
53-
#endif
54-
#ifdef VERTEX_UVS_B
55-
#ifdef VERTEX_UVS_B_COMPRESSED
56-
@location(2) compressed_uv_b: vec2<f32>,
57-
#else
58-
@location(2) uv_b: vec2<f32>,
59-
#endif
60-
#endif
61-
6260
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
6361
#ifdef VERTEX_NORMALS
6462
#ifdef VERTEX_NORMALS_COMPRESSED
@@ -76,6 +74,23 @@ struct Vertex {
7674
#endif
7775
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
7876

77+
#endif // VERTEX_PACKED_AXIS_ANGLE_TBN
78+
79+
#ifdef VERTEX_UVS_A
80+
#ifdef VERTEX_UVS_A_COMPRESSED
81+
@location(1) compressed_uv: vec2<f32>,
82+
#else
83+
@location(1) uv: vec2<f32>,
84+
#endif
85+
#endif
86+
#ifdef VERTEX_UVS_B
87+
#ifdef VERTEX_UVS_B_COMPRESSED
88+
@location(2) compressed_uv_b: vec2<f32>,
89+
#else
90+
@location(2) uv_b: vec2<f32>,
91+
#endif
92+
#endif
93+
7994
#ifdef SKINNED
8095
@location(5) joint_indices: vec4<u32>,
8196
@location(6) joint_weights: vec4<f32>,
@@ -96,6 +111,24 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert
96111
let mesh_metadata = bevy_pbr::mesh_functions::get_metadata(instance_index);
97112
var uncompressed_vertex: UncompressedVertex;
98113
uncompressed_vertex.instance_index = instance_index;
114+
115+
#ifdef VERTEX_PACKED_AXIS_ANGLE_TBN
116+
uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position_angle, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents);
117+
#ifdef NORMAL_PREPASS_OR_DEFERRED_PREPASS
118+
var normal: vec3f;
119+
var tangent: vec4f;
120+
bevy_render::utils::decompress_vertex_axis_angle_to_normal_tangent(
121+
vertex_in.compressed_axis,
122+
vertex_in.compressed_position_angle.w,
123+
&normal,
124+
&tangent,
125+
);
126+
uncompressed_vertex.normal = normal;
127+
uncompressed_vertex.tangent = tangent;
128+
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
129+
130+
#else // VERTEX_PACKED_AXIS_ANGLE_TBN
131+
99132
#ifdef VERTEX_POSITIONS_COMPRESSED
100133
uncompressed_vertex.position = bevy_render::utils::decompress_vertex_position(vertex_in.compressed_position, mesh_metadata.aabb_center, mesh_metadata.aabb_half_extents);
101134
#else
@@ -117,6 +150,9 @@ fn decompress_vertex(vertex_in: Vertex, instance_index: u32) -> UncompressedVert
117150
#endif
118151
#endif
119152
#endif // NORMAL_PREPASS_OR_DEFERRED_PREPASS
153+
154+
#endif // VERTEX_PACKED_AXIS_ANGLE_TBN
155+
120156
#ifdef VERTEX_UVS_A
121157
#ifdef VERTEX_UVS_A_COMPRESSED
122158
let uv_min_and_extents_a = mesh_metadata.uv_channels_min_and_extents[0];

0 commit comments

Comments
 (0)