usdGeospatial: codeless CRS schema + two-runtime reference (co-submission preview)#10
Open
asluk wants to merge 83 commits into
Open
usdGeospatial: codeless CRS schema + two-runtime reference (co-submission preview)#10asluk wants to merge 83 commits into
asluk wants to merge 83 commits into
Conversation
mistafunk
reviewed
Jul 7, 2026
There was a problem hiding this comment.
If I understand correctly (the AI generated text/code is hard to read), you are proposing two related changes:
- using a codeless schema (CRS representation and binding), i.e. no stage modification at composition time
- specifically no injection of xform reset for bound Xformables, i.e. let the runtime (hydra) do this.
This looks elegant though I have these concerns/questions:
- Is
crs:positionreally needed? What is its authoring UX? Why not use the natural origin of the CRS and do the rest with the USD xform stack? - We should demonstrate this also works without hydra, e.g. when flattening layers into the same target CRS.
- I think the proposed binding mechanism with the local xform being composed onto the
crs:position(in target CRS) is NOT correct for the general case where the target CRS is spherical (for non-rendering use cases). - Trimble suggested to use the WKT
CONVERSIONparameters (the projection natural origin) for local origin definition, this would need to be reconciled/combined withcrs:position. - What is the purpose of 'requires-CRS'? I was expecting that the
crs:bindingrelationship is simply ignored by the runtime if an USD distro does not have support for this schema.
HdStRenderPassState conditionally includes alphaThreshold in the uniform BAR layout only when it is not zero. Hdx OIT expects alphaThreshold to be zero for both passes, so we expect to reuse a single UBO for both passes. We also never recompile the shaders. Both optimizations have performance benefits. Applications should never set alphaThreshold on Hdx's OIT task params. If an application does set something other than HtRenderPassState's default of zero, OIT would break due to mismatch between the UBO layout and the shader. To avoid that, OIT guards against non-zero alphaThreshold. Please note that this guard preserves the correct behavior of OIT, but applications that persist in sending erroneous non-zero alphaThreshold will experience performance loss due to frequent resizing of the uniform BAR. (Internal change: 2417781)
asluk
force-pushed
the
aluk/geospatial-crs-prototype
branch
from
July 7, 2026 21:32
af7bd5c to
576d187
Compare
(Internal change: 2418021)
(Internal change: 2418132)
asluk
force-pushed
the
aluk/geospatial-crs-prototype
branch
from
July 9, 2026 18:11
a1661c2 to
576d187
Compare
1. When camera guides were enabled, the back plates would disappear. This is because the prior _PrimsAdded call would remove the whole subtree but only add in the back plate children. Turning on the camera guides causes a re-sync more frequently thus causing this to bug to surface. The solution is to revise the _RemoveSubtrees function to remove only the back plate children but ensure that _PrimsRemoved still removes the entire subtree. 2. Not recieving dirty calls for timesampled dirtied backplate properties. Modifed _PrimsDirtied to keep track of dirtied prims for HdXformSchema locators as well as modifying the adapter to issue locators for timesamples. 3. The backplate schema is at camera:backplate:APPLIED_INSTANCE_NAME, so we cannot give it a meaningful GetDefaultLocator(), instead we rely on HdCameraSchema::GetBackPlateLocator() (Internal change: 2418144)
Checking this in for @klucknav who did the work. - update HdStMaterialXShaderGen test - update ShaderGen as they added an `AIRY_FRESNEL_ITERATIONS` define - some of the changes in v1.39.5 included updates to the prefiltered environment lighting AcademySoftwareFoundation/MaterialX#2808 so the Storm baselines in the UsdImagingGL tests and testMaterialX needed to be updated - also updated the native baslines in the Native v MaterialX test (Internal change: 2418247)
(Internal change: 2418262)
(Internal change: 2418307)
(Internal change: 2418334)
asluk
force-pushed
the
aluk/geospatial-crs-prototype
branch
from
July 11, 2026 21:11
409ec89 to
c1daef2
Compare
The `--no-build-apple-framework` flag was ignored on embedded
platforms because buildAppleFramework was computed as:
`args.build_apple_framework or MacOSTargetEmbedded(self)`
On embedded platforms MacOSTargetEmbedded() is True, so the 'or'
short-circuited to True even when the user explicitly passed
--no-build-apple-framework (which sets args.build_apple_framework
to False).
Give the flag a None default so an unspecified flag can be
distinguished from an explicit False, and only fall back to the
embedded platform default when the flag is unspecified. Embedded
platforms still default to building the framework, but can now
disable it.
Closes PixarAnimationStudios#4153
(Internal change: 2418817)
(Internal change: 2418850)
(Internal change: 2418958)
(Internal change: 2419075)
The PXR_PREFIX was set to pxr/usdIrImaging instead of pxr/usdImaging, causing usdIrImaging headers to install to include/pxr/usdIrImaging rather than include/pxr/usdImaging/usdIrImaging. This matches the convention of every other subdirectory under pxr/usdImaging and the include paths already used within the usdIrImaging sources (e.g. pxr/usdImaging/usdIrImaging/api.h). Closes PixarAnimationStudios#4154 (Internal change: 2419094)
This is built on top of a corresponding MaterialX PR, that fixes nested geomprop nodes: AcademySoftwareFoundation/MaterialX#2957 Rendering a UsdPreviewSurface network that reads a primvar through an ND_UsdPrimvarReader_* node (for example varname="st" feeding a UsdUVTexture) failed in Hydra Storm with: HdSt_ApplyMaterialXFilter -- Unable to create the Glslfx Shader. MxException: No "geomprop" value found for geompropvalue node "primvar". That crash exposed a few different issues this PR fixes, all in how hdMtlx/hdSt build the document and emit the glslfx. 1. Preserve the topology-affecting primvar name. The topological classification (_IsTopologicalShader) only recognized the native geompropvalue family, so a UsdPrimvarReader varname was stripped from the anonymized network. 2. Recognize primvar-reader nodes generically (hdMtlx). A node is treated as a primvar reader when its implementation nodegraph wraps a geompropvalue or geompropvalueuniform node. New helpers HdMtlxIsPrimvarReaderNodeDef, HdMtlxGetPrimvarNameInputName and HdMtlxGetPrimvarDefaultInputName discover this and the input names that carry the primvar name and fallback (read from the interfacename on the wrapped geompropvalue inputs, for example varname and fallback for UsdPrimvarReader). These drive three call sites that previously only understood the native geompropvalue node by category or by the hardcoded geomprop/default parameter names: - _IsTopologicalShader now treats any primvar-reader node as topological. - _AddMaterialXNode now registers primvar-reader nodes in hdPrimvarNodes so the primvar is declared and bound. - _UpdatePrimvarNodes now extracts the primvar name and fallback from the correct inputs instead of assuming geomprop/default. Detecting by implementation structure rather than by family name keeps this robust for any future node that wraps a geompropvalue. Over-preserving a parameter is always correctness-safe; the only cost is slightly reduced shader reuse for such nodes, matching how the native geompropvalue node is already handled. 3. Emit glslfx-compatible primvar types (hdSt/materialXShaderGen.cpp). The primvar attributes block of the generated glslfx took its type name from the target shader language syntax. On Metal that produced names like float2, which HioGlslfxConfig does not understand (it accepts float, double, int, vec2, vec3, vec4), so the primvar declaration was rejected, st fell back to a default, and texture coordinates were lost (surfaces rendered untextured). The attributes type is now mapped to a glslfx type independent of the target language. This is a Metal-specific defect that became reachable only once a primvar actually reached shader generation. Closes PixarAnimationStudios#4091 (Internal change: 2419116)
…ng the plugin. (Internal change: 2419120)
(Internal change: 2419139)
(Internal change: 2419243)
(Internal change: 2419253)
(Internal change: 2419352)
(Internal change: 2419353)
This is the first in a set of name shortenings. Long names were causing issues on Windows systems with the 260 character limit for paths. (Internal change: 2419625)
…horingCode This is the second in a set of changes to rename the directories associated with irExample. Here we rename the authoring lib, and update references to it in the usdview plugin. (Internal change: 2419724)
(Internal change: 2419740)
(Internal change: 2419742) (Internal change: 2419750)
…wPlugin This is the third in a set of changes to rename the directories associated with irExample. Here we rename the usdview plugin. (Internal change: 2419757)
Aaron pointed to the 'Gaussians work': Pixar's in-tree hdParticleField example
(extras/imaging/examples/hdParticleField, OpenUSD dev) -- a sample render
delegate for a particleField prim type, the reference for gaussian-splat
rendering. Update the plan with its real file layout (pxr_plugin CMake macro,
plugInfo.json, .py converters) and the key distinction:
- hdParticleField is a render DELEGATE; our work is a scene-index FILTER.
- TAKE from it: the in-tree plugin SCAFFOLDING/BUILD pattern (it tracks current
dev Hydra, so it's a better build template than the NVIDIA plugin), and the
fact that we build inside asluk/USD means a sibling in-tree example compiles
with the same pxr_plugin machinery -- given a from-source USD build.
- TAKE the RUNTIME model from omniGeoSceneIndex, ported to current Hydra.
Pointer is now resolved; the remaining gating input is a from-source USD build.
…r hdParticleField) Per Aaron: before any geospatial code, prove the full path end-to-end on Pixar's known-good example -- build USD from source with usdview + examples, convert a PUBLIC 3DGS .ply to USD via the example's own py3dgsPlyToUsd.py, render it in usdview with the hdParticleField renderer, and send a screenshot. This de-risks the from-source build / usdview / in-tree-plugin-load / converter path on a known-good target before our new scene-index plugin depends on all of it. Phase 0 steps 0a-0d added (build flags, PXR_PLUGINPATH_NAME, public .ply convert, render + screenshot incl. headless xvfb/EGL or usdrecord fallback). Deliverable: a usdview screenshot of a gsplat to Aaron. Flagged that this needs a from-source build + GL/display, so it's a resume-session task (not the codeless venv).
Compiled Hydra scene index that performs geospatial resolution at render
time -- the production form of usdGeospatial/src/resolve_runtime.py.
- crsEngine: PROJ-backed CRS engine (Reproject + ENU->ECEF LocalFrameToEcef);
WKT stays opaque to USD, PROJ is the only consumer (mirrors crs_engine.py).
- geoResolver: C++ port of the reference runtime (binding strength/purpose/
collection resolution, anchor_frame, resolve_with_injection).
- geospatialSceneIndex: HdSingleInputFilteringSceneIndexBase wrapping Xformable
prims that resolve to a geospatial anchor; overrides the HdXformSchema matrix
locator with the inject-don't-bake world transform; dirties descendants.
Scaffolded from hdParticleField's pxr_plugin/plugInfo pattern but registers an
HdSceneIndexPlugin; runtime ported from NVIDIA omniGeoSceneIndex, retargeted to
crs:binding/crs:position and updated to current Hydra (USD 0.26.8).
Parity (run_parity.sh), all 0.0 mm vs closed-form WGS84 geodesy AND the Python
reference runtime:
- engine: 9 datasets (geographic + 5 projected CRSs)
- resolver: + real NVIDIA Deutsche Bahn railway anchor+curve subtree + anchor
frames (inject-don't-bake orientation)
- Hydra scene index: same datasets pulled THROUGH the pipeline as a renderer
would consume them.
Non-circular (GT is closed-form, not parallel PROJ) and with teeth (negative
control: stock Hydra puts these prims at the origin).
…piled Hydra), with visual proof The codeless schema's whole point is that behavior is a data contract, not one implementation. Now that the compiled C++ Hydra scene-index plugin exists and matches the Python reference runtime, tell that story in the README and back it with figures generated by the build: - New 'Two runtimes, one schema' section: one authored stage resolved through the Python reference runtime AND the compiled Hydra scene index, agreeing to sub-mm. - fig_multi_runtime.py: architecture figure (one schema -> two runtimes -> same ECEF world; parity table; negative control). - fig_runtime_parity.py: VISUAL parity -- real NVIDIA Deutsche Bahn rails-on-tiles rendered side-by-side via both runtimes from the same stage; per-vertex disagreement median 0.40 mm / worst 0.68 mm across 3,526 verts; self-asserts and FAILS the build if >1 mm. - render_figures.py wires both into the figure suite (multi_runtime auto-runs the Hydra dumper when the binary is present, else skips with a clear note). - dumpHydraXforms.cpp: dumps HdXformSchema matrices from the scene index for the visual-parity figure. - Retire the three forward-looking 'Hydra form is future work' references; the compiled production form is done (run_parity.sh: crsEngine 9/9, geoResolver 30/30, Hydra SI 30/30, all 0.0 mm vs Python oracle + closed-form GT).
…st codes This README is shared with external collaborators (Esri). The internal one-pager/checklist shorthand (C-04, C-05) referenced a doc those readers can't see, so it read as a dangling reference. Replaced with self-contained phrasing; no technical content changed: - 'Float32 localization falls out for free (one-pager C-05)' -> drop the code - 'cuProj -- this is the C-04 insertion point' -> 'a GPU engine (e.g. cuProj)' - test_float32_localization.py '(C-05)' -> drop the code - 'the C-04 insertion point' -> 'the natural insertion point'
…k' objection nonclaw feedback: the compiled Hydra scene index forces resetXformStack=true on the xform it injects (geospatialSceneIndex.cpp), which can read as the very baking the design rejects. Add a note clarifying the distinction is WHERE the flag lives: here it is on a computed, transient Hydra data source (the resolver already composed ancestors and returns the full world/ECEF matrix, so the flag just stops Hydra re-composing under parents at flatten time), never authored into a layer. The authored stage stays coordinate-neutral -- verified: world_neutral_relbinding.usda has zero resetXformStack and zero xformOp, only crs: properties. Additive doc only.
…ract, runtime is plural) The README had accreted three eras of incremental edits and become internally inconsistent: its spine still claimed 'one codeless Python runtime' and 'every figure is generated by this codeless build, no external renderer' while its climax is now 'the schema is the contract; the behavior is plural' (Python reference runtime + compiled C++ Hydra scene index). That blanket figure claim was simply false -- two figures (multi_runtime, runtime_parity) come from the compiled Hydra path and render_figures.py skips them without the Hydra binary. Rewritten from scratch on the correct spine: - New thesis up front: codeless data-contract schema proven by TWO independent runtimes resolving the same authored scene to the same world, sub-mm. - Reproducibility claim split honestly: nine Python-reference figures (one command, no external renderer) vs two cross-runtime parity figures that also need the compiled C++ Hydra build (run_parity.sh, prebuilt USD). - Added a short Repository-layout map (this example + ../usdGeospatialSceneIndex/). - Merged the two separate resetXformStack discussions into one argument (rejects resetXformStack AUTHORED into a layer; the scene index sets it only on a transient computed Hydra data source -- authored scene stays neutral). - Tests section: internal test-ID shorthand (T1-T3, S1-S7, P1-P4, ...) replaced with plain one-phrase descriptions of what each openable test file checks. - Running block now two-tier (codeless Python path + full parity path). - No internal checklist codes remain; external-audience throughout. No technical claims changed. Numbers re-verified against live output before commit: parity median 0.40 / worst 0.68 mm over 3,526 verts; usd-core 26.5, pyproj 3.7.1 / PROJ 9.5.1. The differing PROJ/USD versions vs the C++ build are a deliberate two-toolchain difference, preserved (the parity proof is exactly that two different stacks agree).
…ower -> upper) The railway figures painted each tile's quadnode imagery with imshow(origin= 'lower'), which puts image row 0 (the TOP of the PNG) at the south edge of the tile -- flipping the texture north/south, so the basemap text rendered mirrored and the (correctly-resolved) rails appeared not to sit on the tiles. The tile quads carry st=[(0,0),(0,1),(1,1),(1,0)] with t=0 at the SOUTH corner, so image row 0 must map to NORTH: origin='upper'. Texture-only fix; geometry untouched -- parity is byte-for-byte unchanged (median 0.40 mm / worst 0.68 mm over 3,526 verts, exit 0). Fixed in both fig_railway_render (render_figures.py) and fig_runtime_parity, regenerated railway_render.png + runtime_parity.png.
Follow-up to the origin flip fix: the railway figures had been faking the tile texturing by stretching each quadnode PNG into the tile's E/N bounding box. Now they honor the asset's actual UsdUVTexture/st mapping -- a _draw_uv_textured_quad helper solves the affine UV->world-EN transform from the mesh's per-corner st ([(0,0),(0,1),(1,1),(1,0)], t=0 at the south corner) and places image pixel (s,t) at the world corner carrying that st. Exact for the (parallelogram) tiles here (corner residual ~1e-10 m) and correct for any quad orientation, where a bbox stretch would not be. Applied in both fig_railway_render and fig_runtime_parity. Texture-only; geometry/parity unchanged (median 0.40 / worst 0.68 mm, 3,526 verts). Lets the README drop the 'we fake the texture' caveat.
…t UVs Follow-up to commit 406123c (faithful UsdUVTexture/st rendering). Add a short line under the railway figure noting the tile imagery is placed via the asset's own st mapping (s->East, t->North, t=0 south; pixel (s,t) lands at the world corner with that UV) rather than stretched into a bounding box -- so the basemap reads north-up and the rails register against it. Keeps the README consistent with the implementation and drops the implicit 'approximate texture' framing.
Commit 406123c (faithful UsdUVTexture/st rendering) reintroduced the exact N/S texture flip that 56f5ea8 had fixed: the affine UV-space imshow used origin='lower', which places the picture's TOP row at t=0. But the st contract has t=0 at the SOUTH corner, so the image top landed in the south -> tiles mirror-reversed and the (correctly-resolved) rails appeared misplaced against the flipped basemap. Fix: origin='upper' in the UV-space imshow (image row 0 -> t=1 -> north), in both _draw_uv_textured_quad sites (render_figures.py and fig_runtime_parity.py). Regenerated railway_render.png + runtime_parity.png. Verified by PIXEL SAMPLING this time, not just the affine math: render a known top=red/bottom=blue test image -> north samples red (correct); and on the real asset, figure-north matches the source image's top rows. Geometry/parity unchanged (median 0.40 / worst 0.68 mm over 3,526 verts).
…, not production The compiled C++ Hydra scene index is modeled on the Gaussian-splat hdParticleField example: a second, illustrative runtime that proves the codeless schema resolves through Hydra and agrees with the Python reference to sub-mm. It is NOT a prescription of how a production renderer must consume the schema. Reword README/SI-README/TLDR/plan/figure labels + source header comments accordingly; regenerate docs/multi_runtime.png with the new label.
Promote the architectural positioning that had been living only in the PR review guide into the canonical README: a 'Where the Python reference runtime sits' subsection (executable spec / conformance oracle; not USD-core, not a runtime dep, no exact precedent by design) and an 'Open design questions for the working group' section (codeless-schema+reference-runtime shape; coexist vs hooking CRS into UsdGeomXformable). The README is now the single source the PR body and slide deck derive from.
Add invisible <!-- slide:* --> markers to the README and a build_deck.py that parses them into a 13-slide presenter deck (docs/usdGeospatial.pdf): title -> thesis -> design-call (coexist) -> anchor/UsdGeomXformable -> 3 hero proofs (railway, coherence, two-runtimes) -> where-the-Python-sits -> open questions. Dense sections carry a concise deck-authored body= summary so the long-form README prose stays in the README. README is now the one grounding doc the PR body and the deck both derive from. deck.html is gitignored (intermediate); the PDF is the committed artifact.
…uard-rail framing Add an adversarial head-to-head that rebuilds a multi-CRS scene (MoMA in NAD83/UTM-17N nested under a WGS84/UTM-30N anchor) both baked (resetXformStack + double3 translate) and neutral (crs:binding + crs:position), measuring each against independent closed-form pyproj geodesy. No approach is graded against the other; assertions are approach-vs-ground-truth only. The harness found a real defect: for a PROJECTED anchor, resolve_with_injection lifted grid-authored child offsets through the anchor's true-ENU basis, which differs from grid axes by convergence + point-scale (~4.86 m over a ~420 m lever). Fix: crs_engine gains is_projected() + project_grid_offset_to_target(); the resolver composes projected-anchor child offsets IN-PLANE (grid add + reproject) and keeps the true-ENU topocentric path for geographic/ECEF anchors. Both approaches now land the corner to 0.0 mm; the geographic NYC path is unchanged. README: add "Composition frame: projected vs geographic anchors" (the correctness rule + found/fixed story), a "Guard rails" section (anchor-vs-child, compose in the CRS-implied frame, a requires-CRS stage marker + detect-and-refuse, all validator-checkable), a "What we'd ask of Esri and co-collaborators" section, and reframe open-question #2 around the residual conformance surface. Rebuild the slide deck from the README markers (18 slides). - src/crs_engine.py: is_projected(), project_grid_offset_to_target() - src/resolve_runtime.py: projected-anchor in-plane composition branch - src/test_coexist_vs_baked.py: the head-to-head harness (new) - testenv/world_multicrs_{baked,neutral}.usda: re-authored from public EPSG WKT - README.md, docs/usdGeospatial.pdf: framing + derived deck
Make the "guard rails" concrete: for each of the three coexist invariants, show a deliberately ill-formed asset, the exact failure measured against independent closed-form geodesy, and the minimal authoring fix that resolves to truth. src/test_illformed_assets.py (new): - G1 anchor-vs-child: a corner authored as its OWN georef leaf and parented to be relative jumps to its own CRS point (418.9 m off); fix = plain Cartesian child (xformOp:translate) -> 0.000 mm. - G2 compose-in-CRS-frame: a projected-anchor child composed through true-ENU bends by grid convergence + point-scale (4.86 m off); fix = compose in the grid plane + reproject -> 0.000 mm. - G3 requires-CRS marker: an unmarked scene under a CRS-unaware consumer lands 6,369 km off, silently; fix = stamp customLayerData['crsResolutionRequired'] so a conformant consumer detects-and-refuses. verify.py: replace the blunt "any xformOp == baked" check with precise neutrality (A1: no authored resetXformStack; A2: no georef prim bakes an xformOp onto itself -- Cartesian children may), and add check G (a stage with crs:binding must declare customLayerData['crsResolutionRequired']). Neutral fixtures + the earth2 generator now stamp the marker; both baked scenes correctly fail A1 under the validator. README: add "What breaks, and the fix" under Guard rails (runnable before/after for each invariant) with a slide marker; rebuild the deck (19 slides).
…ope note Add src/render_illformed.py, which renders three before/after figures (and a stacked gallery) directly from the measured test_illformed_assets.py quantities: illformed_g1.png anchor-vs-child 417.8 m off -> 0.000 mm illformed_g2.png wrong frame 4.86 m off -> 0.000 mm illformed_g3.png no marker 6,369 km off -> detect-and-refuse Each plots where the object lands vs the closed-form-geodesy target (green star), with the error distance called out; nothing is hand-drawn for effect. README: embed the three figures under "What breaks, and the fix", add a gallery slide:image marker for the deck, and add a subsection anticipating the debate the requires-CRS marker will draw -- including the layer-metadata vs prim-schema question (customLayerData: cheap O(1) up-front detect-and-refuse but coarse and untyped; a prim applied schema: typed/validatable/local but needs traversal). Our lean: a stage/layer signal for the cheap check, optionally refined per-prim; a WG call. Rebuild the deck (21 slides). PR body (PR_REVIEW_GUIDE.md, uncommitted): embed the gallery + link the test.
…mbers; tidy seams Review pass over the README after the layered ill-formed/visuals/marker edits, to remove Frankenstein seams from multiple edits: - render_illformed.py: label each figure with the exact 3D ECEF distance the test measures against pyproj geodesy (via _localized_pair), instead of the figure's own 2D ENU-panel hypot. G1 now reads 418.9 m (was a 417.8 m reprojection artifact) so figures, slide, prose, and gallery caption all agree with test_illformed_assets.py (418.9 m / 4.86 m / 6,369 km). ENU coords still used for marker placement + zoom only. - README: add blank lines around the three inline break/fix images so the numbered list keeps its continuity across renderers. - Verified: all H2-H4 headings unique, no TODO/placeholder cruft, and every internal anchor link resolves against GitHub's slug rule (incl. the em-dash double-hyphen anchor). Deck rebuilt (21 slides). Full gate green.
…xform-cache thread-safety fix - renderStormGeo.cpp: offscreen Storm render of the georeferenced railway with the UsdGeospatialSceneIndex inserted in the Hydra chain (SetStage explicit); --si/--nosi toggle renders the same stage twice from an identically-framed camera. - Proof PNGs: usdview_storm_proof.png (SI on -> railway at ECEF world position, drawn by Storm) vs usdview_storm_noSI.png (SI off -> geometry at raw local origin, off-frame). Negative control confirms the scene index is what places the geometry; Storm draws both. - geoResolver: guard UsdGeomXformCache access with a mutex. Storm syncs rprims across TBB worker threads and pulls the xform data source in parallel; the cache is not thread-safe (concurrent mutation -> double-free). Required for any real Storm render. - probeHydra.cpp: diagnostic confirming crs: attrs are absent from the default Hydra data stream (motivates the APISchemaAdapter needed for usdview auto-insertion / Level 1). - README: label every figure as a Matplotlib plot of resolved coordinates (not a Hydra/Storm render), so the plots are not mistaken for renderer output.
…er (Level 1)
The scene index now resolves georeferenced geometry AUTOMATICALLY when a stage is
opened in usdview/usdrecord -- no SetStage, no hand-built chain -- matching how the
hdParticleField example just works.
- geospatialAPISchemaAdapter.{h,cpp}: a keyless UsdImaging API-schema adapter
(apiSchemaName "", the codeless-schema pattern; modeled on coordSysAPIAdapter)
that surfaces crs:binding / crs:position / crs:wkt into the Hydra data stream so a
downstream scene-index filter can see them. This was the missing piece: crs: attrs
are absent from the default Hydra stream (see probeHydra).
- geospatialSchema.{h,cpp}: shared crs: token/locator definitions.
- geospatialSceneIndex / geoResolver: added a stage-free resolve path that reads
entirely from Hydra data sources; the SetStage path is preserved as a fallback so
renderStormGeo.cpp and the parity harness still work.
- plugInfo.json: register the API-schema adapter (keyless, isInternal).
- testHydraAutoParity.cpp: parity through the Hydra auto path == closed-form geodesy
and the Python oracle, 30/30 rows 0.0 mm (matches the by-hand chain).
- Proof PNGs (usdview + usdrecord): auto-insert -> railway at ECEF world position vs
negative control (plugin path removed) -> geometry at origin / absent.
Scope note: the Hydra auto path resolves direct crs:binding (+ nearest/stronger);
collection-based and purpose strength remain stage-path only for now.
Rewrite the example README from the accreted 21-section/617-line form onto a designed ~8-section spine that leads with the proofs a reviewer can check, consolidates the recurring resolve-don't-bake / resetXformStack framing, and folds in the two landed proof points that were previously tracked separately: the real Hydra Storm render and the true auto-insert into usdview (keyless UsdImaging API-schema adapter, stage-free Hydra-auto path, 30/30 at 0.0 mm). All proofs/numbers/caveats preserved (418.9 m, 4.86 m, 6,369 km, 162 mm, 480,000x, 0.0 mm parity, NOAA NCAT, Deutsche Bahn no-overfit, thread-safety fix, guard rails G1/G2/G3, the two open WG questions). Slide markers re-storyboarded deliberately; deck re-derived from README (22 slides).
…rfit renders Visualize non-geometric georeferenced data (e.g. the earth2 GFS t2m field: 7,320 crs:position samples with no intrinsic shape) WITHOUT mutating or duplicating the dataset, via USD composition: visualize_field_glyphs.py writes a SEPARATE overlay layer that subLayers the pristine base and adds marker geometry as geometry-only 'over' prims (zero crs:* re-authored). The auto- inserted scene index resolves each glyph as a Cartesian child under its georef prim (the same anchor-injection path test_anchor_injection.py already proves), so the base data stays byte-identical and the overlay is fully removable. This reframes non-visual geospatial data as a first-class, common use case and demonstrates the codeless schema composes correctly across USD layers. Marker size is a stated display parameter (analogous to a scatter's point size), with a sparsity option to sparsely overlay geometry. Anti-overfit render set: the SAME script + the SAME unchanged scene index place glyphs correctly across 5 CRS families / both hemispheres / equator-to-78N (NYC UTM-18N, Sydney UTM-56S, Wellington NZTM2000, Quito ~equator, Svalbard ~78N), zero code changes between locales. Plus the earth2 globe cross-visualizer parity (matplotlib scatter vs Storm render) and refreshed railway/level1 renders. Negative controls: no-plugin renders are empty/origin (SI does the placement). Parity re-verified GREEN: run_parity.sh 30/30 0.0mm, testHydraAutoParity 30/30. Retires make_renderable_globe.py (baked-geometry approach) in favor of overlays.
…overlay section Add a section documenting non-geometric georeferenced data (scalar fields, sensor networks, benchmarks) as a first-class use case, visualized via a USD composition overlay: a base layer holds the pristine dataset; a separate overlay subLayers it and adds marker glyphs as geometry-only 'over' prims (zero crs:* re-authored, base byte-identical, overlay removable), placed by the existing anchor-injection path (no new code). Includes the same-data/ two-visualizer globe parity and the same-runtime/many-CRS anti-overfit render set (NYC/Sydney/Wellington/Quito/Svalbard; 5 CRS families, both hemispheres, ~equator-to-78N; zero code changes). Deck re-derived (26 slides).
The README hero pointed at usdview_level1_autoinsert.png, a 601x465 usdview window screenshot (UI chrome, low-res). Point it instead at the clean 1280x1280 offscreen Storm render, and drop the two low-res usdview screenshots. Rename the auto-insert renders off the internal 'level1' milestone label to durable names: usdrecord_level1_autoinsert.png -> railway_storm_autoinsert.png usdrecord_level1_noplugin.png -> railway_storm_noplugin.png Update README + deck + PR-guide refs; tighten the hero caption (drop the screenshot-coverage and internal SetStage-fallback wording). Deck re-derived. (First of two passes; the README voice/register cleanup follows separately.)
Register-only pass (no re-structure, no facts changed): strip
implementation-history narration a reviewer never needs while keeping every
caveat as a present-tense property of the design.
- Proof 4: drop the SetStage-fallback aside ('stage-free path added alongside
the SetStage path, preserved as fallback'); state the thread-safety as a
present-tense fact (mutex-guarded XformCache) instead of 'getting Storm to
render surfaced a bug'.
- Projected-vs-geographic rule: rewrite 'an earlier revision used the ENU lift
... the harness caught it' as a present-tense property (compose through
true-ENU instead of the grid plane -> 4.86 m off; the test asserts frame
selection). Same fact, no changelog.
- Guard rails / open questions: 'the adversarial testing surfaced' -> present
tense; drop 'now'/'no longer' temporal phrasing.
- Running: drop the 'Tier 1/Tier 2' plan-numbering labels (keep both run modes).
- Status: 'Done:' checklist -> 'It demonstrates:' present-tense properties.
All numbers/proofs/caveats preserved (418.9m/4.86m/6,369km/162mm/480,000x/
0.40mm/0.0mm/NCAT/DB no-overfit/thread-safety/9-9/30-30/5-CRS anti-overfit/
composition-overlay). Deck re-derived (26 slides, unchanged; markers untouched).
PR body kept in sync (same SetStage->'no application code' edit); all 8 README
anchors verified against current slugs.
…-> resolved-position plot)
The multiCRS_glyph_renders.png montage stacked five auto-insert renders that
were pixel-identical: each locale's camera centers its glyph, and the scene is
otherwise pure black, so a centered dot at NYC is byte-identical to one at
Svalbard. All five overfit_<locale>_autoinsert.png had the same md5. The figure
looked like five CRS families but conveyed one, and read as the opposite of the
anti-overfit point it captioned. (The inputs were correct -- distinct per-locale
bases + cameras + 0.95%-vs-0%-control coverage -- but nothing varied visibly.)
Replace with src/fig_multicrs_positions.py: a Matplotlib plot (consistent with
every other figure) of WHERE each authored point resolves in ECEF, through one
code path (lon/lat -> the locale's projected CRS -> ECEF via PROJ, the registered
default engine). The five land 2,200+ km apart -- NYC/Sydney/Wellington/Quito/
Svalbard, 5 CRS families, both hemispheres, equator->78N -- with the authored
EPSG the only thing that differs. Self-checks the points are distinct; verified
sub-mm vs closed-form geodesy in generalization_suite.py.
- README: retitle 'render set' -> 'position set'; rewrite prose + caption + alt
text as a resolved-position plot; add fig_multicrs_positions.py to the run list.
- Remove src/montage_overfit.py + the 6 identical overfit_*.png.
- Keep render_overfit_glyphs.sh + overfit_glyph_suite.py as render infra (noted)
for a future real-globe render montage (Option A).
- Deck re-derived (26 slides, new caption); PDF refreshed.
- PR body prose synced ('render set' -> 'position set'); all 8 README anchors
re-verified.
…nonclaw) - matplotlib framed as same-seat consumer of resolver output (not consuming a stage) - drop 'executable specification' -> conformance oracle; normative behavior lives in prose; cite AOUSD core-spec-supplemental precedent - soften two-runtime 0.0mm to robustness/first conformance pass; correctness carried by NCAT + closed-form geodesy - coexist: usdchecker validator plugin promoted to committed near-term deliverable; lost 'just works' stated as eyes-open tradeoff (loud+recoverable vs baking silent+unrecoverable) No design/facts changed, no headings moved (PR-body anchors intact). Deck re-derived (26 slides).
…ful about the claim') Two internal-review phrasings that read meta to a human reader; prose-only, no slide markers touched (deck unchanged), no facts/design changed.
…sdGeospatial), cross-platform Make the usdGeospatialSceneIndex example an opt-in, cross-platform build like hdParticleField -- one build_usd.py flag builds PROJ and the plugin, replacing the Linux-only shell scripts. Validated on Linux and Windows (MSVC). Build: - build_usd.py: --usdGeospatial builds PROJ (+ its SQLite3 dependency) as opt-in third-party deps, then passes -DPXR_ENABLE_GEOSPATIAL_SUPPORT to the USD build. - cmake/defaults: PXR_ENABLE_GEOSPATIAL_SUPPORT option + find_package(PROJ). - Register the example under extras/usd/examples (gated on the flag + imaging); fix its CMakeLists: add the missing geospatialAPISchemaAdapter/geospatialSchema sources, define USDGEOSPATIAL_SI_EXPORTS, DISABLE_PRECOMPILED_HEADERS, link PROJ::proj. Parity as a ctest: - run_parity.py + pxr_register_test(testUsdGeospatialParity): CRS engine 9/9, stage resolver 30/30, Hydra scene index 30/30, stage-free auto-insert 30/30 -- all 0.0 mm. dumpHydraXforms built for the runtime_parity figure. - dump_oracle.py auto-converts the committed railway asset so the oracle is the full 30-row set. Cross-platform figure/doc tooling: - fig_runtime_parity.py / render_figures.py: portable dumper + tsv paths (GEO_HYDRA_TSV / GEO_HYDRA_DUMPER, system temp, os.pathsep) -- no /tmp or /usr/share/proj hardcodes. - docs/build_deck.py: read the README as UTF-8 (Windows). - probe_autoinsert.py: drop dead Hd/HdGp imports. Docs (README is the base artifact; deck derives from it): - README build sections -> the cross-platform build + ctest; add a reproducibility slide; regenerate the deck PDF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
asluk
force-pushed
the
aluk/geospatial-crs-prototype
branch
from
July 22, 2026 14:47
c1daef2 to
6f276df
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
usdGeospatial — PR Review Guide
Hero proofs (the four most illustrative)
1 — Real third-party asset, no overfit. A genuinely external NVIDIA OpenUSD-plugin-samples
Deutsche Bahn railway (~1,470 curves) authored in the original Omniverse geospatial
schema, converted to
crs:binding/crs:positionand resolved onto three real geospatialtiles near Hamburg — no Hydra, no external renderer, no baking. (full-size)
2 — Multi-CRS co-registration, against an independent authority. The same monument
authored three independent ways (geographic EPSG:4979 / UTM 18N / NY State Plane) from
independent NOAA NCAT coordinates co-registers to the same ECEF point to ≤ ~0.5 mm —
verified against closed-form WGS84 geodesy, not a parallel PROJ call. (full-size)
3 — One schema, two independent runtimes (a first conformance pass). The Python reference
runtime and the compiled C++ Hydra scene index resolve the same authored stage to the same world,
agreeing to 0.0 mm — evidence the data contract is unambiguous enough to build twice the same
way (robustness), not on its own a correctness proof. Correctness is Proof 2 (vs NCAT +
closed-form geodesy, the independent authority). The schema is a contract, not one implementation. (full-size)
4 — It just works in usdview (real Hydra Storm render). With only the built plugins on
PXR_PLUGINPATH_NAME— no application code, no hand-built scene-index chain — opening thegeoref scene in usdview /
usdrecorddraws the railway at its correct ECEF position via Storm; akeyless
UsdImagingAPISchemaAdaptersurfacescrs:into Hydra and an auto-inserted scene indexresolves it. Negative control: plugin path removed → railway absent. The stage-free Hydra-auto path
matches the oracle 30/30 at 0.0 mm (
testHydraAutoParity). This is a real render, not a plot. (full-size)The two things that merit the most care
Both are covered in full in the README; flagged here so a reviewer reads them deliberately.
crs:resolution coexists withUsdGeomXformable— does georeferenced placementreplace, wrap, or coexist with the existing xform stack? This design answers
coexist: the authored scene stays coordinate-neutral (only
crs:properties, zeroxformOp/resetXformStack), ancestor transforms are composed not bypassed, and an anchor'ssubtree composes under a runtime-injected local-frame→ECEF basis as ordinary USD. →
The design call: resolve, don't bake
· Anchor injection — coexisting with
UsdGeomXformable· Composition frame: projected vs. geographic anchors
· Guard rails a validator can enforce
An adversarial head-to-head (
test_coexist_vs_baked.py)rebuilds Simon Haegler's multi-CRS POC scene both baked and neutral and, measured against
independent closed-form geodesy, the neutral design reproduces the baked result to 0.0 mm.
What breaks under an ill-formed asset (and the one-line fix), measured the same way:
test_illformed_assets.py.sensor networks, benchmarks) is a prim with
crs:position+ a value and no geometry. Aseparate overlay layer
subLayersthe pristine dataset and adds marker glyphs as geometry-onlyovers (zerocrs:*re-authored; base byte-identical; overlay removable) — placed by the sameanchor-injection path, no new code. Doubles as a proof the codeless schema composes across USD
layers. Same-data/two-visualizer parity + a same-runtime/many-CRS (5 families, both hemispheres,
~equator-to-78N, zero code changes) anti-overfit position set (each authored point resolves to a
distinct ECEF location through one code path).
→ Non-geometric data: visualize by composition
specification / conformance oracle, not USD-core code, a runtime dependency, or
Python-in-the-render-loop. No exact precedent; that placement is a deliberate design choice.
→ Where the Python reference runtime sits
Open design questions we'd like Esri's / the WG's read on are listed in the README:
→ Status, scope, and open questions
The submission core (what we'd actually propose)
The subset to treat as the proposal; everything else in the diff exists to validate it.
pxr/usd/usdGeospatial/schema.usda(+ generated
generatedSchema.usda/plugInfo.json, regenerated byregen-schema.sh).skipCodeGeneration = true⇒ codeless. The data contract.resolve_runtime.py,the projection-engine seam
crs_engine.py, the converterreencode_georef.py, and the README.usdGeospatialSceneIndex/— a C++ Hydra scene index modeled on the Gaussian-splathdParticleFieldexample (illustrative, not a prescribed production renderer). Builds opt-in and cross-platform:build_usd.py --usdGeospatialfetches and builds PROJ, then the plugin and its parity tests, exactly thehdParticleFieldpattern; parity is actest(testUsdGeospatialParity); validated on Linux and Windows. Proves the schema is a contract, not one implementation.Guided reading order
Schema → author → resolve → engine → second runtime → proofs.
schema.usda: the twoclassdecls,skipCodeGeneration = true, binding is arelnot a baked transform.world_neutral_relbinding.usda(onlycrs:props, zeroxformOp) vs the baked Esri formworld_baked_resetxformstack.usda.resolve_runtime.py:crs_of_prim(binding precedence),resolve_*, thenresolve_with_injection(anchors).crs_engine.py: reprojection + local-frame basis as a registry; WKT never reaches USD.geospatialSceneIndex.cpp(HdXformSchemaoverride) +geoResolver.cpp/crsEngine.cpp.ctest -R testUsdGeospatialParity(cross-platform; orrun_parity.sh) (9/9, 30/30, 30/30 — 0.0 mm; negative control: no SI ⇒ origin),generalization_suite.py(7 datasets),test_anchor_injection.py.Full evidence narrative + figures: The proofs ·
docs/gallery.What's in this PR but NOT in the eventual submission (scaffolding)
The PR is the full prototype branch, unreorganized. The following is evidence and tooling,
trimmed/relocated before a real co-submission:
src/test_*.py,multi_crs_example.py,testenv_equivalence.py,verify.py, SItest*.cpp) — keep a focused subset as the example's test; rest → validation harness.src/render_*.py,fig_*.py,docs/*.png, ~3.5 MB) — keep one or two key figures + regen scripts; drop/host the rest.docs/{TLDR,codex-review,…}.md,HYDRA_PLUGIN_PLAN.md, the untrackedRESUME.md, and thisPR_REVIEW_GUIDE.md) — fold durable bits into the README; drop the rest.data/gfs_t2m.nc8 MB,data/thirdparty/Deutsche Bahn asset) — confirm redistribution licensing; fetch-on-demand or a smaller slice.build_standalone.sh,dump*,out/,__pycache__) —.gitignore'd / dropped.Next steps to submission shape: extract the core to a clean branch; relocate the test
battery; trim docs to one README; resolve data-fixture licensing; confirm in-tree
--examplescompile; re-green the suite; coordinate the joint submission with Esri.