Skip to content

fix(usd): declare stage units, disable subdivision, add usdz output - #312

Open
gipert wants to merge 2 commits into
g4edge:mainfrom
gipert:usd-export-fixes
Open

gipert wants to merge 2 commits into
g4edge:mainfrom
gipert:usd-export-fixes

Conversation

@gipert

@gipert gipert commented Sep 13, 2026

Copy link
Copy Markdown
Member
  • I have reviewed all the code in this pull request and deem it of the standard expected for this project.

Parts of this pull request were drafted with AI assistance.

What this fixes

mesh2Prim divides lengths by 1000, so an exported file holds metres, but the stage never recorded that. USD falls back to its default of centimetres, so every geometry pyg4ometry has exported to USD loads 100 times too small. metersPerUnit is now declared, along with the z up axis Geant4 uses, which was likewise left at the USD default of y.

Meshes were written without a subdivision scheme, so USD treats each solid as the control cage of a smooth surface. Edges get rounded off and the surface is pulled inside the volume the solid describes. subdivisionScheme is now set to none.

Materials were defined at /Materials, beside the geometry root rather than below it, leaving the stage with two root prims. They now go under the geometry root, and that root is recorded as the default prim so viewers know what to place.

MaterialBindingAPI is applied before binding. Without it USD warns on every load that bindings were found on a prim that does not have the schema applied.

What this adds

A .usdz suffix now writes a usdz package rather than a plain layer:

v = pyg4ometry.visualisation.UsdViewer("lht.usdz")
v.traverseHierarchy(l)
v.save()

usdz holds the layer and any textures in one file and is the format viewers on phones and tablets read, so this is what you need to show a geometry in augmented reality. Note that Apple's AR Quick Look assumes y is up and will lay a z up geometry on its side; the docs say so.

Behaviour change

Files written by this version are no longer the same size as before in viewers that honour metersPerUnit. This is the point of the change: they are now the size the geometry says they are, where previously they were 100 times too small. Anything downstream that compensated for the old behaviour by scaling up will need that compensation removed.

How it was tested

By exporting a box with a cylinder inside it and reading the file back: metersPerUnit is 1, upAxis is Z, there is a single root prim with the default prim set, every mesh has subdivisionScheme = none and a bound material, a 100 mm cylinder reads back as 0.1, and the usdz package contains one uncompressed, 64 byte aligned entry with the intermediate layer cleaned up. The repository's pre-commit hooks pass.

Not tested: the existing test suite has no USD coverage, and I have not added any, so this rests on the manual check above. The change has not been tried against a large real geometry through this code path, nor in usdview or Blender.

Left alone

scaleFactor = 0.9999, applied cumulatively at each level of the hierarchy to avoid z-fighting, shrinks geometry progressively with nesting depth. That sits awkwardly with a file that now claims accurate units, but changing it alters output for existing users, so it seemed worth raising separately.

Two further gaps, larger than this change: no normals are authored, so hard edges shade as though smooth, and deep hierarchies produce one mesh prim and one material per placement. The second matters for real time viewers, where the prim and material count, rather than the triangle count, is what stalls them.

Happy to split this into separate commits for the fixes and the usdz addition if you would prefer that.

mesh2Prim divides lengths by 1000, so an exported file holds metres, but the
stage never recorded that. USD then falls back to its default of centimetres and
the geometry loads 100 times too small in every viewer. Declare metersPerUnit,
and the z up axis that Geant4 uses, which was likewise left at the USD default
of y.

Meshes were written without a subdivision scheme. USD therefore treats each
solid as the control cage of a smooth surface, rounding off its edges and
pulling the surface inside the volume the solid describes. Set subdivisionScheme
to none.

Materials were defined at /Materials, beside the geometry root rather than below
it, leaving the stage with two root prims. Move them under the geometry root and
record that root as the default prim, so viewers know what to place.

A ".usdz" suffix now writes a usdz package rather than a plain layer. usdz holds
the layer and any textures in one file and is what viewers on phones and tablets
read.

Also apply MaterialBindingAPI before binding, which USD warns about otherwise,
and remove two debug prints and a reshape whose result was discarded.

Assisted-by: Generative AI
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.24561% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 74.73%. Comparing base (4887da6) to head (8580111).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/pyg4ometry/visualisation/UsdViewer.py 98.24% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #312      +/-   ##
==========================================
+ Coverage   74.32%   74.73%   +0.41%     
==========================================
  Files         157      157              
  Lines       22957    23001      +44     
==========================================
+ Hits        17062    17190     +128     
+ Misses       5895     5811      -84     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gipert
gipert marked this pull request as draft September 13, 2026 14:41
A geometry written volume by volume gives a viewer one mesh and one material per
volume. Viewers on phones and tablets compile a shader for every material they
are handed, so a geometry of a few thousand volumes takes minutes to appear, or
never does, even though the triangle count is one a phone draws easily.

UsdViewer(mergeByMaterial=True) combines the volumes into one mesh per material
when the file is saved. The meshes are read back from the stage, so placements
are already resolved and the merged points are in world coordinates, instancing
included. Volumes are grouped by the values their shaders carry rather than by
material name: a material is defined per volume here, so grouping by name would
merge nothing.

The hierarchy, the volume names and the placements do not survive, so this is
for viewing rather than for exchanging a geometry.

The tests cover the stage metadata and the subdivision scheme, the usdz package,
and that merging leaves fewer meshes holding the same faces in the same place.
They need usd-core, so the test extra now pulls in the usd one, which the test
workflow installs. Without that they would skip everywhere in CI.

Assisted-by: Generative AI
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.

1 participant