Skip to content

feat(geom): UsdGeom schema reader behind geom feature#74

Merged
mxpv merged 24 commits into
mxpv:mainfrom
bresilla:feat/geom
May 27, 2026
Merged

feat(geom): UsdGeom schema reader behind geom feature#74
mxpv merged 24 commits into
mxpv:mainfrom
bresilla:feat/geom

Conversation

@bresilla
Copy link
Copy Markdown
Contributor

UsdGeom is the foundation every USD consumer needs — meshes, transforms, cameras, instancing. Without it the crate can parse a stage but can't surface its actual content to a renderer or DCC. This PR fills that gap behind a geom Cargo feature, in the same shape as skel / physics / lux.

The first commit extracts the matrix helpers from schemas::skel::skinning into a new top-level crate::math module so the xform evaluator can share them without geom having to depend on skel. anim_query + skel_reader test imports are updated in the same commit so every commit on the branch builds clean.

Read-only — no authoring helpers, same as the other schema readers. Defaults across every prim match Pixar's schema.usda exactly.

While implementing the xformOp:rotateXYZ evaluator I hit a discrepancy between Pixar's schema docs and their C++ source — opened PixarAnimationStudios/OpenUSD#4087 upstream. This PR follows the C++ behaviour (which is what consumers see in practice).

Covered by 58 integration tests against a fixture that exercises every prim type. fmt and clippy both come back clean.

Copilot AI review requested due to automatic review settings May 26, 2026 19:03
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a feature-gated UsdGeom schema reader (including xformOp evaluation and many concrete prim readers) and centralizes shared 4×4 matrix math so both UsdGeom and UsdSkel can reuse it.

Changes:

  • Introduces schemas::geom behind a new geom Cargo feature, plus a comprehensive integration fixture + tests.
  • Adds crate::math with shared row-major 4×4 helpers; refactors skel math call sites to use it.
  • Updates docs/roadmap to reflect UsdGeom support and wires up the new geom integration test target.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/skel_reader.rs Switches identity matrix constant to the new shared openusd::math location.
tests/geom_reader.rs Adds integration tests covering UsdGeom readers (Imageable/Boundable, xforms, shapes, mesh/primvars, curves, instancing).
src/schemas/skel/skinning_query.rs Uses crate::math::IDENTITY_MAT4 instead of the skel-local constant.
src/schemas/skel/skinning.rs Replaces in-module mat4 helpers with imports from crate::math; removes duplicated mat4 tests.
src/schemas/skel/anim_query.rs Switches mat4 imports to crate::math to share implementation.
src/schemas/mod.rs Registers geom schema module behind the geom feature gate.
src/schemas/geom/xform.rs Implements UsdGeomXformable xformOpOrder evaluation including !invert! and !resetXformStack!.
src/schemas/geom/types.rs Adds decoded structs/enums for UsdGeom return types and token conversions.
src/schemas/geom/tokens.rs Adds centralized UsdGeom token constants.
src/schemas/geom/shapes.rs Adds intrinsic shape readers (Cube/Sphere/Cylinder/Capsule/Cone/Plane).
src/schemas/geom/read.rs Adds cross-cutting readers (visibility/purpose/extent/proxyPrim/kind) and find_geom_prims.
src/schemas/geom/mod.rs Defines the public UsdGeom module surface and re-exports.
src/schemas/geom/mesh.rs Adds Mesh/GeomSubset/PrimvarsAPI readers.
src/schemas/geom/instancer.rs Adds PointInstancer reader including proto targets and prune lists.
src/schemas/geom/curves.rs Adds curve/patch/points/tet-mesh readers with defaults and fallbacks.
src/schemas/geom/camera.rs Adds Camera reader + reads all authored attrs with spec defaults.
src/math.rs Introduces shared row-major mat4 helpers + unit tests.
src/lib.rs Exposes the new math module in the crate root docs and module list.
fixtures/usdGeom_scene.usda Adds a dedicated fixture scene for geom integration tests.
ROADMAP.md Marks UsdGeom as implemented and documents the supported surface.
Cargo.toml Adds geom feature and wires geom_reader integration tests to it.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/schemas/geom/read.rs Outdated
Comment on lines +196 to +198
} else if let Ok(Some(_)) = stage.field::<Value>(path.clone(), A_VISIBILITY) {
out.imageables.push(p);
} else if let Ok(Some(_)) = stage.field::<Value>(path.clone(), A_PURPOSE) {
Comment thread src/schemas/geom/curves.rs Outdated
Comment on lines +75 to +83
let n = (*count as usize).max(0);
let p = order.get(i).copied().unwrap_or(4) as usize;
let nk = n + p;
if k_cursor + nk <= knots.len() && p > 0 && n > 0 {
out.push([knots[k_cursor + p - 1], knots[k_cursor + n]]);
} else {
out.push([0.0, 1.0]);
}
k_cursor += nk;
Comment thread src/schemas/geom/xform.rs
None => (false, op_name),
};

let attr = prim.append_property(base)?;
Comment thread src/schemas/geom/xform.rs Outdated
};

if inverted {
Ok(mat4_inverse(&m).unwrap_or(IDENTITY_MAT4))
Comment thread src/schemas/geom/curves.rs Outdated
Comment on lines +112 to +117
let u_knots = read_double_vec(stage, prim, A_U_KNOTS)?.unwrap_or_default();
let v_knots = read_double_vec(stage, prim, A_V_KNOTS)?.unwrap_or_default();
let u_range = read_vec2d_scalar(stage, prim, A_U_RANGE)?
.unwrap_or_else(|| clamped_inner_span(&u_knots, u_vertex_count as usize, u_order as usize));
let v_range = read_vec2d_scalar(stage, prim, A_V_RANGE)?
.unwrap_or_else(|| clamped_inner_span(&v_knots, v_vertex_count as usize, v_order as usize));
Comment thread tests/geom_reader.rs
Comment on lines +138 to +140
fn find_geom_prims_buckets_by_type() -> Result<()> {
let stage = open()?;
let prims = find_geom_prims(&stage)?;
mxpv added 13 commits May 27, 2026 12:42
Signed-off-by: Maksym Pavlenko <[email protected]>
Hermite tangent fallback now walks per-curve segments via curveVertexCounts so
a multi-curve prim's terminal tangents don't accidentally point into the next
curve. Primvar Interpolation::default() flipped from Vertex to Constant to
match the UsdGeomPrimvar spec default.
Pixar's PointInstancer spec mandates that authored orientationsf (quatf[])
wins over the legacy orientations (quath[]); the float-precision attribute
exists specifically to override the half-precision one. The reader had the
priority reversed.
Pixar's schema declares inactiveIds as int64listop prim metadata, so the
canonical authoring shape is a list-op (prepend/append/delete). The reader
previously only matched plain Int64Vec/IntVec, silently dropping every
list-op opinion and leaving masked instances visible.
Replace ReadMesh.interpolate_boundary / face_varying_linear_interpolation /
triangle_subdivision_rule (previously Option<String>) with typed token enums.
Unauthored opinions now substitute Pixar's documented defaults — edgeAndCorner,
cornersPlus1, catmullClark — instead of staying None.
The walker previously classified a prim as imageable if visibility or purpose
held any Value type, but read_visibility / read_purpose accept only Token /
String — a wrong-typed opinion like `bool visibility = false` would put the
prim into the imageables bucket while value resolution returned Inherited.
Match Token / String here so walker and reader agree.
compute_purpose previously walked past an authored-but-unrecognised purpose
token on the local prim and inherited the parent's value, while read_purpose
maps the same input to Purpose::Default. The two readers now agree: any
authored opinion stops the walk and falls back to Default on unknown tokens.

Both walks also drop the raw `cur.as_str() == "/"` check in favour of
Path::parent() returning None at the pseudo-root — the typed API is the
canonical termination signal and CLAUDE.md forbids raw path string compares.
Pixar's UsdGeomNurbsCurves carries an optional double[] pointWeights — when
authored, each control vertex's weight turns the curve rational (the only way
to represent shapes like exact circles). The reader previously dropped this
attribute entirely, silently flattening rational curves to polynomial.
Pixar's UsdGeomXformOp allows xformOp:rotate* to be authored as either float
or double, and UsdGeomCamera declares shutter:open / shutter:close as double.
The readers previously narrowed both to f32 before the math ran, silently
truncating mantissa bits a Pixar-compatible reader would have kept.

xform.rs now keeps rotation angles in f64 end-to-end (value extract,
to_radians, sin_cos, matrix). ReadCamera.shutter_open / shutter_close move to
f64. Other camera attributes stay f32 because their schema type is float.

Also tightens compute_local_to_parent_transform: a !resetXformStack! sentinel
is only valid at index 0 per the UsdGeomXformable spec; a non-leading
occurrence now surfaces an error instead of being silently dropped.
Primvar elementSize stored as Int64 was previously cast to i32 with a wrapping
`as` — an adversarial or buggy author could write a value above i32::MAX and
read it back as a negative or rolled-over i32 used unchecked as a stride.
Treat overflow as "no opinion" via i32::try_from.
UsdGeomPlane overrides Gprim's `doubleSided = false` default with `true` —
Plane is the one Gprim where the inherited default flips. ReadPlane now
exposes the field and read_plane substitutes the spec default when
`doubleSided` is unauthored.
translation, scale, axis rotation, and quaternion-to-matrix helpers were
duplicated between geom/xform.rs and skel/anim_query.rs (with one only
trivial naming difference). Move all six into crate::math under a consistent
mat4_ prefix — mat4_translation, mat4_scale, mat4_rotation_x/y/z,
mat4_from_quat — and drop the locals from both schema modules.

Also adjusts mat4_transform_vec's docstring: "e.g. a normal" was misleading
because normals under non-uniform scale need the inverse-transpose; mention
that caveat explicitly so callers don't get surprised.
UsdGeomXformOp can be authored with double precision translate, scale, and
quaternion values. The reader previously narrowed those values through f32
before building matrix4d output, losing mantissa bits for large coordinates
and high-precision transforms.

UsdGeomGprim also has schema fallbacks that should apply when opinions are
unauthored: doubleSided defaults to true, and UsdGeomPrimvar interpolation
defaults to constant. Match those fallbacks so composed meshes are not read
as single-sided or face-varying unless the layer says so.
@mxpv
Copy link
Copy Markdown
Owner

mxpv commented May 27, 2026

Thanks for contributing this! This is awesome.
I've added bunch of follow ups (see the commit history).
I also kind of thinking of revisiting how the API should look like (might follow up with some ideas).

@mxpv mxpv merged commit 1942faa into mxpv:main May 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants