From 2a7969f2dc94c6fa28b29e096ac4650668c60daf Mon Sep 17 00:00:00 2001 From: Trim Bresilla Date: Fri, 1 May 2026 23:38:57 +0200 Subject: [PATCH 1/4] fix(usdc): reorder quat bytes to match USDA convention --- src/usdc/mod.rs | 24 ++++++++++++++---------- src/usdc/reader.rs | 45 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/usdc/mod.rs b/src/usdc/mod.rs index 553354d..d989346 100644 --- a/src/usdc/mod.rs +++ b/src/usdc/mod.rs @@ -277,7 +277,9 @@ mod tests { .try_as_quatf() .unwrap(); - assert_eq!(quat, [2.9, 8.5, 4.6, 1.4]); + // USDC bytes are `[x, y, z, w]` (Pixar GfQuat layout); the + // reader reorders to `(w, x, y, z)` to match USDA convention. + assert_eq!(quat, [1.4, 2.9, 8.5, 4.6]); let quat = data .get(&sdf::path("/World.quatfArr")?, "default")? @@ -288,9 +290,9 @@ mod tests { assert_eq!( quat, vec![ - [3.5, 2.6, 3.6, 4.2], // 1 - [5.3, 6.3, 5.2, 2.4], // 2 - [4.3, 2.4, 6.4, 7.1], // 3 + [4.2, 3.5, 2.6, 3.6], // 1 + [2.4, 5.3, 6.3, 5.2], // 2 + [7.1, 4.3, 2.4, 6.4], // 3 ] ); @@ -307,7 +309,8 @@ mod tests { .try_as_quatd() .unwrap(); - assert_eq!(quat, [5.3, 6.3, 5.2, 2.4]); + // USDC bytes are `[x, y, z, w]`; reader returns `(w, x, y, z)`. + assert_eq!(quat, [2.4, 5.3, 6.3, 5.2]); let quat = data .get(&sdf::path("/World.quatdArr")?, "default")? @@ -318,8 +321,8 @@ mod tests { assert_eq!( quat, vec![ - [3.5, 2.6, 3.6, 4.2], // 1 - [4.3, 2.4, 6.4, 7.1], // 2 + [4.2, 3.5, 2.6, 3.6], // 1 + [7.1, 4.3, 2.4, 6.4], // 2 ] ); @@ -336,7 +339,8 @@ mod tests { .try_as_quath() .unwrap(); - assert_eq!(quat, [4.6, 2.5, 7.6, 3.5].map(f16::from_f32)); + // USDC bytes are `[x, y, z, w]`; reader returns `(w, x, y, z)`. + assert_eq!(quat, [3.5, 4.6, 2.5, 7.6].map(f16::from_f32)); let quat = data .get(&sdf::path("/World.quathArr")?, "default")? @@ -347,8 +351,8 @@ mod tests { assert_eq!( quat, vec![ - [2.4, 7.8, 8.5, 4.7].map(f16::from_f32), // 1 - [6.7, 5.6, 5.3, 4.6].map(f16::from_f32), // 2 + [4.7, 2.4, 7.8, 8.5].map(f16::from_f32), // 1 + [4.6, 6.7, 5.6, 5.3].map(f16::from_f32), // 2 ] ); diff --git a/src/usdc/reader.rs b/src/usdc/reader.rs index 195886d..000d4c5 100644 --- a/src/usdc/reader.rs +++ b/src/usdc/reader.rs @@ -1051,14 +1051,47 @@ impl CrateFile { // // Quats // - Type::Quath if value.is_array() => Value::QuathVec(self.read_vec_array::(value)?), - Type::Quath => sdf::Value::Quath(self.unpack_value::<[f16; 4]>(value)?), + // Pixar's GfQuat declares `_imaginary: GfVec3` then + // `_real: T`, so on-disk bytes are `[imag_x, imag_y, imag_z, + // real]` = `[x, y, z, w]`. The USDA textual form is + // `(real, i, j, k)` = `(w, x, y, z)`, which the USDA parser + // stores verbatim. Reorder USDC bytes here so `Value::Quat*` + // values are consistently `(w, x, y, z)` regardless of source + // — without this, binary USDC quats from real production + // assets (Isaac Sim Agilebot, Omniverse robotics scenes) + // come out with axes scrambled. + Type::Quath if value.is_array() => Value::QuathVec( + self.read_vec_array::(value)? + .into_iter() + .map(|q| [q[3], q[0], q[1], q[2]]) + .collect(), + ), + Type::Quath => { + let raw = self.unpack_value::<[f16; 4]>(value)?; + sdf::Value::Quath([raw[3], raw[0], raw[1], raw[2]]) + } - Type::Quatf if value.is_array() => Value::QuatfVec(self.read_vec_array::(value)?), - Type::Quatf => sdf::Value::Quatf(self.unpack_value::<[f32; 4]>(value)?), + Type::Quatf if value.is_array() => Value::QuatfVec( + self.read_vec_array::(value)? + .into_iter() + .map(|q| [q[3], q[0], q[1], q[2]]) + .collect(), + ), + Type::Quatf => { + let raw = self.unpack_value::<[f32; 4]>(value)?; + sdf::Value::Quatf([raw[3], raw[0], raw[1], raw[2]]) + } - Type::Quatd if value.is_array() => Value::QuatdVec(self.read_vec_array::(value)?), - Type::Quatd => sdf::Value::Quatd(self.unpack_value::<[f64; 4]>(value)?), + Type::Quatd if value.is_array() => Value::QuatdVec( + self.read_vec_array::(value)? + .into_iter() + .map(|q| [q[3], q[0], q[1], q[2]]) + .collect(), + ), + Type::Quatd => { + let raw = self.unpack_value::<[f64; 4]>(value)?; + sdf::Value::Quatd([raw[3], raw[0], raw[1], raw[2]]) + } // // ListOp From 3527f0f2ebc8f4d45d58fd39d33cdc286dd6b0a1 Mon Sep 17 00:00:00 2001 From: Trim Bresilla Date: Fri, 1 May 2026 23:54:39 +0200 Subject: [PATCH 2/4] fix(usdc): apply matching quat reorder in writer for round-trip --- src/usdc/writer.rs | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/usdc/writer.rs b/src/usdc/writer.rs index bd9a36e..bc8c542 100644 --- a/src/usdc/writer.rs +++ b/src/usdc/writer.rs @@ -480,17 +480,31 @@ impl<'w, W: Write + Seek> Packer<'w, W> { Value::Vec2h(a) => self.write_pod_out(Type::Vec2h, a), Value::Vec3h(a) => self.write_pod_out(Type::Vec3h, a), Value::Vec4h(a) => self.write_pod_out(Type::Vec4h, a), - Value::Quath(a) => self.write_pod_out(Type::Quath, a), + // openusd-rs stores `Value::Quat*` as `(w, x, y, z)`; Pixar's + // `GfQuat` lays out `(x, y, z, w)` on disk (see USDC reader + // for the symmetric reorder). Reorder before writing so + // round-trips with Pixar tools remain semantically correct + // and `write → read` round-trips here are identity. + Value::Quath(a) => { + let pixar = [a[1], a[2], a[3], a[0]]; + self.write_pod_out(Type::Quath, &pixar) + } Value::Vec2f(a) => self.write_pod_out(Type::Vec2f, a), Value::Vec3f(a) => self.write_pod_out(Type::Vec3f, a), Value::Vec4f(a) => self.write_pod_out(Type::Vec4f, a), - Value::Quatf(a) => self.write_pod_out(Type::Quatf, a), + Value::Quatf(a) => { + let pixar = [a[1], a[2], a[3], a[0]]; + self.write_pod_out(Type::Quatf, &pixar) + } Value::Vec2d(a) => self.write_pod_out(Type::Vec2d, a), Value::Vec3d(a) => self.write_pod_out(Type::Vec3d, a), Value::Vec4d(a) => self.write_pod_out(Type::Vec4d, a), - Value::Quatd(a) => self.write_pod_out(Type::Quatd, a), + Value::Quatd(a) => { + let pixar = [a[1], a[2], a[3], a[0]]; + self.write_pod_out(Type::Quatd, &pixar) + } Value::Vec2i(a) => self.write_pod_out(Type::Vec2i, a), Value::Vec3i(a) => self.write_pod_out(Type::Vec3i, a), @@ -517,15 +531,24 @@ impl<'w, W: Write + Seek> Packer<'w, W> { Value::Vec2hVec(v) => self.write_array_arr_half::<2>(Type::Vec2h, v), Value::Vec3hVec(v) => self.write_array_arr_half::<3>(Type::Vec3h, v), Value::Vec4hVec(v) => self.write_array_arr_half::<4>(Type::Vec4h, v), - Value::QuathVec(v) => self.write_array_arr_half::<4>(Type::Quath, v), + Value::QuathVec(v) => { + let reordered: Vec<[f16; 4]> = v.iter().map(|q| [q[1], q[2], q[3], q[0]]).collect(); + self.write_array_arr_half::<4>(Type::Quath, &reordered) + } Value::Vec2fVec(v) => self.write_array_arr_f32::<2>(Type::Vec2f, v), Value::Vec3fVec(v) => self.write_array_arr_f32::<3>(Type::Vec3f, v), Value::Vec4fVec(v) => self.write_array_arr_f32::<4>(Type::Vec4f, v), - Value::QuatfVec(v) => self.write_array_arr_f32::<4>(Type::Quatf, v), + Value::QuatfVec(v) => { + let reordered: Vec<[f32; 4]> = v.iter().map(|q| [q[1], q[2], q[3], q[0]]).collect(); + self.write_array_arr_f32::<4>(Type::Quatf, &reordered) + } Value::Vec2dVec(v) => self.write_array_arr_f64::<2>(Type::Vec2d, v), Value::Vec3dVec(v) => self.write_array_arr_f64::<3>(Type::Vec3d, v), Value::Vec4dVec(v) => self.write_array_arr_f64::<4>(Type::Vec4d, v), - Value::QuatdVec(v) => self.write_array_arr_f64::<4>(Type::Quatd, v), + Value::QuatdVec(v) => { + let reordered: Vec<[f64; 4]> = v.iter().map(|q| [q[1], q[2], q[3], q[0]]).collect(); + self.write_array_arr_f64::<4>(Type::Quatd, &reordered) + } Value::Vec2iVec(v) => self.write_array_arr_i32::<2>(Type::Vec2i, v), Value::Vec3iVec(v) => self.write_array_arr_i32::<3>(Type::Vec3i, v), Value::Vec4iVec(v) => self.write_array_arr_i32::<4>(Type::Vec4i, v), From 20aa4580404fde8f1b6fd51c894898f08cbf8837 Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Sun, 3 May 2026 09:57:48 -0700 Subject: [PATCH 3/4] Add unit test Signed-off-by: Maksym Pavlenko --- tests/binary_writer_roundtrip.rs | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/binary_writer_roundtrip.rs b/tests/binary_writer_roundtrip.rs index ec10513..d514143 100644 --- a/tests/binary_writer_roundtrip.rs +++ b/tests/binary_writer_roundtrip.rs @@ -220,6 +220,41 @@ fn vendor_ball_maya() { assert_roundtrip(Path::new(&format!("{VENDOR}/ball.maya.usdc"))); } +#[test] +fn quat_write_read_preserves_wxyz_convention() { + // Regression: Pixar's GfQuat stores components on disk as [x, y, z, w]. + // The reader must reorder to (w, x, y, z), and the writer must apply the + // inverse so that written files remain Pixar-compatible and a second read + // yields the same value. + // + // fields.usdc is generated by Pixar's usdcat from fields.usda, which + // specifies quats in (w, x, y, z) text form. We write it back to USDC + // and assert the re-read values still match the USDA-defined semantics. + let file = std::fs::File::open("fixtures/fields.usdc").expect("open fields.usdc"); + let original = CrateData::open(file, true).expect("parse fields.usdc"); + + let mut buf = Vec::new(); + CrateWriter::write(&original as &dyn AbstractData, &mut Cursor::new(&mut buf)).expect("write"); + let round = CrateData::open(Cursor::new(&buf), true).expect("re-parse"); + let round: &dyn AbstractData = &round; + + // fields.usda: quatf quatfSingle = (1.4, 2.9, 8.5, 4.6) + let scalar = round.get(&sdf::path("/World.quatfSingle").unwrap(), "default").unwrap().into_owned(); + assert_eq!(scalar, Value::Quatf([1.4, 2.9, 8.5, 4.6])); + + // fields.usda: quatf[] quatfArr = [(4.2, 3.5, 2.6, 3.6), (2.4, 5.3, 6.3, 5.2), (7.1, 4.3, 2.4, 6.4)] + let arr = round.get(&sdf::path("/World.quatfArr").unwrap(), "default").unwrap().into_owned(); + assert_eq!(arr, Value::QuatfVec(vec![[4.2, 3.5, 2.6, 3.6], [2.4, 5.3, 6.3, 5.2], [7.1, 4.3, 2.4, 6.4]])); + + // fields.usda: quatd quatdSingle = (2.4, 5.3, 6.3, 5.2) + let scalar = round.get(&sdf::path("/World.quatdSingle").unwrap(), "default").unwrap().into_owned(); + assert_eq!(scalar, Value::Quatd([2.4, 5.3, 6.3, 5.2])); + + // fields.usda: quatd[] quatdArr = [(4.2, 3.5, 2.6, 3.6), (7.1, 4.3, 2.4, 6.4)] + let arr = round.get(&sdf::path("/World.quatdArr").unwrap(), "default").unwrap().into_owned(); + assert_eq!(arr, Value::QuatdVec(vec![[4.2, 3.5, 2.6, 3.6], [7.1, 4.3, 2.4, 6.4]])); +} + // Skipped fixtures — these fail during the *read* step, not during writing, // and are therefore pre-existing reader-side limitations: // From 020ea24a7bd3cb07579765d13ca645d5dde258bb Mon Sep 17 00:00:00 2001 From: Maksym Pavlenko Date: Sun, 3 May 2026 10:05:55 -0700 Subject: [PATCH 4/4] Avoid allocations when changing quat order Signed-off-by: Maksym Pavlenko --- src/usdc/reader.rs | 32 ++++++++++++++------------------ src/usdc/writer.rs | 29 +++++++++++++++++------------ tests/binary_writer_roundtrip.rs | 25 ++++++++++++++++++++----- 3 files changed, 51 insertions(+), 35 deletions(-) diff --git a/src/usdc/reader.rs b/src/usdc/reader.rs index 000d4c5..ba4c4ab 100644 --- a/src/usdc/reader.rs +++ b/src/usdc/reader.rs @@ -1060,34 +1060,19 @@ impl CrateFile { // — without this, binary USDC quats from real production // assets (Isaac Sim Agilebot, Omniverse robotics scenes) // come out with axes scrambled. - Type::Quath if value.is_array() => Value::QuathVec( - self.read_vec_array::(value)? - .into_iter() - .map(|q| [q[3], q[0], q[1], q[2]]) - .collect(), - ), + Type::Quath if value.is_array() => Value::QuathVec(xyzw_to_wxyz(self.read_vec_array::(value)?)), Type::Quath => { let raw = self.unpack_value::<[f16; 4]>(value)?; sdf::Value::Quath([raw[3], raw[0], raw[1], raw[2]]) } - Type::Quatf if value.is_array() => Value::QuatfVec( - self.read_vec_array::(value)? - .into_iter() - .map(|q| [q[3], q[0], q[1], q[2]]) - .collect(), - ), + Type::Quatf if value.is_array() => Value::QuatfVec(xyzw_to_wxyz(self.read_vec_array::(value)?)), Type::Quatf => { let raw = self.unpack_value::<[f32; 4]>(value)?; sdf::Value::Quatf([raw[3], raw[0], raw[1], raw[2]]) } - Type::Quatd if value.is_array() => Value::QuatdVec( - self.read_vec_array::(value)? - .into_iter() - .map(|q| [q[3], q[0], q[1], q[2]]) - .collect(), - ), + Type::Quatd if value.is_array() => Value::QuatdVec(xyzw_to_wxyz(self.read_vec_array::(value)?)), Type::Quatd => { let raw = self.unpack_value::<[f64; 4]>(value)?; sdf::Value::Quatd([raw[3], raw[0], raw[1], raw[2]]) @@ -1358,6 +1343,17 @@ enum ArrayKind { Other, } +/// Pixar's `GfQuat` stores components on disk as `[x, y, z, w]` (imaginary fields first, +/// then real), because `GfVec3 _imaginary` is declared before `T _real` in the struct. +/// Reorder each element to `[w, x, y, z]` in place so `Value::Quat*` is always +/// `(real, i, j, k)` regardless of whether the value came from USDC or USDA. +fn xyzw_to_wxyz(mut v: Vec<[T; 4]>) -> Vec<[T; 4]> { + for q in &mut v { + *q = [q[3], q[0], q[1], q[2]]; + } + v +} + fn to_vec, const N: usize>(data: [i8; N]) -> [T; N] { data.map(T::from) } diff --git a/src/usdc/writer.rs b/src/usdc/writer.rs index bc8c542..40b40cf 100644 --- a/src/usdc/writer.rs +++ b/src/usdc/writer.rs @@ -531,24 +531,15 @@ impl<'w, W: Write + Seek> Packer<'w, W> { Value::Vec2hVec(v) => self.write_array_arr_half::<2>(Type::Vec2h, v), Value::Vec3hVec(v) => self.write_array_arr_half::<3>(Type::Vec3h, v), Value::Vec4hVec(v) => self.write_array_arr_half::<4>(Type::Vec4h, v), - Value::QuathVec(v) => { - let reordered: Vec<[f16; 4]> = v.iter().map(|q| [q[1], q[2], q[3], q[0]]).collect(); - self.write_array_arr_half::<4>(Type::Quath, &reordered) - } + Value::QuathVec(v) => self.write_array_quat_wxyz(Type::Quath, v), Value::Vec2fVec(v) => self.write_array_arr_f32::<2>(Type::Vec2f, v), Value::Vec3fVec(v) => self.write_array_arr_f32::<3>(Type::Vec3f, v), Value::Vec4fVec(v) => self.write_array_arr_f32::<4>(Type::Vec4f, v), - Value::QuatfVec(v) => { - let reordered: Vec<[f32; 4]> = v.iter().map(|q| [q[1], q[2], q[3], q[0]]).collect(); - self.write_array_arr_f32::<4>(Type::Quatf, &reordered) - } + Value::QuatfVec(v) => self.write_array_quat_wxyz(Type::Quatf, v), Value::Vec2dVec(v) => self.write_array_arr_f64::<2>(Type::Vec2d, v), Value::Vec3dVec(v) => self.write_array_arr_f64::<3>(Type::Vec3d, v), Value::Vec4dVec(v) => self.write_array_arr_f64::<4>(Type::Vec4d, v), - Value::QuatdVec(v) => { - let reordered: Vec<[f64; 4]> = v.iter().map(|q| [q[1], q[2], q[3], q[0]]).collect(); - self.write_array_arr_f64::<4>(Type::Quatd, &reordered) - } + Value::QuatdVec(v) => self.write_array_quat_wxyz(Type::Quatd, v), Value::Vec2iVec(v) => self.write_array_arr_i32::<2>(Type::Vec2i, v), Value::Vec3iVec(v) => self.write_array_arr_i32::<3>(Type::Vec3i, v), Value::Vec4iVec(v) => self.write_array_arr_i32::<4>(Type::Vec4i, v), @@ -738,6 +729,20 @@ impl<'w, W: Write + Seek> Packer<'w, W> { Ok(rep_heap(ty, off, true)) } + /// Write a quat array, reordering each element from internal `(w, x, y, z)` to Pixar's + /// on-disk `[x, y, z, w]` layout (GfQuat stores imaginary before real). + fn write_array_quat_wxyz(&mut self, ty: Type, v: &[[T; 4]]) -> Result { + let off = self.pos()?; + self.write_count(v.len() as u64)?; + for q in v { + self.write_pod(&q[1])?; + self.write_pod(&q[2])?; + self.write_pod(&q[3])?; + self.write_pod(&q[0])?; + } + Ok(rep_heap(ty, off, true)) + } + fn write_array_arr_half(&mut self, ty: Type, v: &[[f16; N]]) -> Result { let off = self.pos()?; self.write_count(v.len() as u64)?; diff --git a/tests/binary_writer_roundtrip.rs b/tests/binary_writer_roundtrip.rs index d514143..7a07dfa 100644 --- a/tests/binary_writer_roundtrip.rs +++ b/tests/binary_writer_roundtrip.rs @@ -239,19 +239,34 @@ fn quat_write_read_preserves_wxyz_convention() { let round: &dyn AbstractData = &round; // fields.usda: quatf quatfSingle = (1.4, 2.9, 8.5, 4.6) - let scalar = round.get(&sdf::path("/World.quatfSingle").unwrap(), "default").unwrap().into_owned(); + let scalar = round + .get(&sdf::path("/World.quatfSingle").unwrap(), "default") + .unwrap() + .into_owned(); assert_eq!(scalar, Value::Quatf([1.4, 2.9, 8.5, 4.6])); // fields.usda: quatf[] quatfArr = [(4.2, 3.5, 2.6, 3.6), (2.4, 5.3, 6.3, 5.2), (7.1, 4.3, 2.4, 6.4)] - let arr = round.get(&sdf::path("/World.quatfArr").unwrap(), "default").unwrap().into_owned(); - assert_eq!(arr, Value::QuatfVec(vec![[4.2, 3.5, 2.6, 3.6], [2.4, 5.3, 6.3, 5.2], [7.1, 4.3, 2.4, 6.4]])); + let arr = round + .get(&sdf::path("/World.quatfArr").unwrap(), "default") + .unwrap() + .into_owned(); + assert_eq!( + arr, + Value::QuatfVec(vec![[4.2, 3.5, 2.6, 3.6], [2.4, 5.3, 6.3, 5.2], [7.1, 4.3, 2.4, 6.4]]) + ); // fields.usda: quatd quatdSingle = (2.4, 5.3, 6.3, 5.2) - let scalar = round.get(&sdf::path("/World.quatdSingle").unwrap(), "default").unwrap().into_owned(); + let scalar = round + .get(&sdf::path("/World.quatdSingle").unwrap(), "default") + .unwrap() + .into_owned(); assert_eq!(scalar, Value::Quatd([2.4, 5.3, 6.3, 5.2])); // fields.usda: quatd[] quatdArr = [(4.2, 3.5, 2.6, 3.6), (7.1, 4.3, 2.4, 6.4)] - let arr = round.get(&sdf::path("/World.quatdArr").unwrap(), "default").unwrap().into_owned(); + let arr = round + .get(&sdf::path("/World.quatdArr").unwrap(), "default") + .unwrap() + .into_owned(); assert_eq!(arr, Value::QuatdVec(vec![[4.2, 3.5, 2.6, 3.6], [7.1, 4.3, 2.4, 6.4]])); }