Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
612d65b
docs(usd): specify USD Stage import rework
jeffamstutz Jul 24, 2026
b5bab5c
build(usd): add imaging + OpenSubdiv deps and make TSD_USE_USD public
jeffamstutz Jul 24, 2026
70b8120
feat(usd): import Stages through a Hydra scene index
jeffamstutz Jul 24, 2026
3d96fa1
fix(usd): correct clocks, sharing, and refinement gaps found in review
jeffamstutz Jul 24, 2026
2aacbc5
feat(usd): emit native MaterialX materials when OpenUSD can convert them
jeffamstutz Jul 25, 2026
6275e18
feat(usd): add a USD_MATX Importer Type for native MaterialX import
jeffamstutz Jul 26, 2026
febd2a0
fix(usd): anchor MaterialX texture paths before the document leaves TSD
jeffamstutz Jul 26, 2026
16b390c
feat(usd): bind samplers to MaterialX texture inputs
jeffamstutz Jul 26, 2026
ce96a38
feat(io): decode TIFF textures via optional OpenImageIO
jeffamstutz Jul 26, 2026
253118a
fix(core): teach tsd::Any that std::string is ANARI_STRING
jeffamstutz Jul 26, 2026
d6ae939
docs(adr): record why UDIM tile sets are reported, not approximated
jeffamstutz Jul 26, 2026
19904db
fix(io): match stb's texel contract in the OpenImageIO path
jeffamstutz Jul 26, 2026
274304a
docs(usd): record why mdlResourceSearchPaths cannot silence the MDL n…
jeffamstutz Jul 27, 2026
404a8e2
fix(usd): reject MaterialX documents the device cannot compile
jeffamstutz Jul 27, 2026
56ac4b0
fix(usd): give GeomSubsets the face-varying attributes they draw
jeffamstutz Jul 28, 2026
25df87e
fix(usd): keep face-varying primvars on already-triangulated meshes
jeffamstutz Jul 28, 2026
fe8eaec
docs(style): require non-owning members be pointers, not references
jeffamstutz Jul 28, 2026
bb15951
docs(io): propose one image-import path with a stated orientation
jeffamstutz Jul 28, 2026
f9ebb0d
test(io): characterize image orientation across the importers
jeffamstutz Jul 28, 2026
98b536a
refactor(io): put every image decode behind one ImageCache
jeffamstutz Jul 28, 2026
82d68e1
fix(io): store decoded images in ANARI orientation
jeffamstutz Jul 28, 2026
e321b4d
refactor(io): route HDRI radiance through the image cache
jeffamstutz Jul 28, 2026
9d15b55
fix(io): address code review of the image import path
jeffamstutz Jul 28, 2026
56a9dd8
docs(io): record what landed and what is left of the image import
jeffamstutz Jul 28, 2026
86a5501
fix(io): scale default curve/point radius to prim bounds on USD import
jeffamstutz Aug 14, 2026
b57afb8
rename matx --> mtlx
jeffamstutz Aug 14, 2026
dcd0fbe
fix(io): store decoded images top-down, as ANARI addresses them
jeffamstutz Aug 14, 2026
6bfabc9
fix(tests): read image texels through helium instead of dataAs<uint8_t>
jeffamstutz Aug 18, 2026
0835572
fix(tsd): compile error in optionally compiled region
jeffamstutz Aug 18, 2026
e342ec1
feat(io): support animated USD scenes via a shared Stage Session
jeffamstutz Aug 18, 2026
0ccece9
fix(io): address code review of the animated USD import
jeffamstutz Aug 18, 2026
45ef85a
refactor(io): split USD geometry conversion into resolve and build
jeffamstutz Aug 18, 2026
1cc6b15
fix(io): address code review of the converter split
jeffamstutz Aug 18, 2026
463cd80
fix(rendering): re-copy instance transforms when a transform Array is…
jeffamstutz Aug 18, 2026
9595315
feat(io): map OmniPBR materials onto the portable material again
jeffamstutz Aug 18, 2026
6ae6afa
refactor(io): make resolveMaterial a dispatcher over one material cache
jeffamstutz Aug 18, 2026
9010894
fix(io): keep the Import's Animation reachable when another one is added
jeffamstutz Aug 18, 2026
a4eb07c
refactor(io): store ImportContext's non-owning members as pointers
jeffamstutz Aug 19, 2026
0ef1079
refactor(io): store Traversal's non-owning members as pointers
jeffamstutz Aug 19, 2026
6ab95b7
refactor(io): store the subdivision refiners' TopologyRefiner as a po…
jeffamstutz Aug 19, 2026
bf03c59
test(io): split the USD import suite by subject
jeffamstutz Aug 19, 2026
f13521d
test(io): collapse the USD import per-scenario preamble
jeffamstutz Aug 19, 2026
cebdf59
test(io): address code review of the USD import suite split
jeffamstutz Aug 19, 2026
6a82579
refactor(io): move UsdVol conversion onto the shared spatial field di…
jeffamstutz Aug 19, 2026
cfeb64f
refactor(io): teach the spatial field dispatcher '.vdb'
jeffamstutz Aug 19, 2026
473bdba
fix(io): address code review of the USD volume split
jeffamstutz Aug 19, 2026
95d53ed
fix(io): convert the materials EnSight carrier prims bind
jeffamstutz Aug 19, 2026
586cb41
docs(io): drop the restated "what" above boundMaterialOf
jeffamstutz Aug 19, 2026
3a55998
fix(io): correct why a Claimed carrier's material is still reachable
jeffamstutz Aug 19, 2026
9e4c0df
refactor(io): report every skipped USD prim from one place
jeffamstutz Aug 19, 2026
ee0598b
test(io): cover a UsdVol Volume whose field file is missing
jeffamstutz Aug 19, 2026
d0f99ac
refactor(io): let an ImageCache alone say which Scene a Sampler joins
jeffamstutz Aug 19, 2026
0dcf19d
docs(io): record that the texture import shims stay
jeffamstutz Aug 19, 2026
0caf746
refactor(io): stop building a one-call ImageCache for HDRI radiance
jeffamstutz Aug 19, 2026
ece64a1
test(io): pin the compressed sampler's format and size parameters
jeffamstutz Aug 19, 2026
baa79a0
refactor(io): let an Image carry its own format and dimensions
jeffamstutz Aug 19, 2026
a0a72e9
refactor(io): let NetworkWalker read a string-or-token shader input
jeffamstutz Aug 19, 2026
904e3dd
refactor(io): build every non-dome light through one helper
jeffamstutz Aug 19, 2026
5825c98
docs(io): reword makeLight's comment around why, not what
jeffamstutz Aug 19, 2026
dcacba4
refactor(io): record only the nodes native instancing places on
jeffamstutz Aug 19, 2026
b422918
build(io): regroup the source list by directory again
jeffamstutz Aug 19, 2026
8a72d3d
refactor(io): find the native instancers through one shared walk
jeffamstutz Aug 19, 2026
6e0c1a0
test(io): compare the empty-mesh material count against an empty Scene
jeffamstutz Aug 19, 2026
eb55470
style(io): clang-format the split USD import test files
jeffamstutz Aug 19, 2026
1fccb4a
fix(io): check the magic before handing a file to the NanoVDB reader
jeffamstutz Aug 19, 2026
7bcc7f0
test(io): cover the HDF5 half of the shared spatial-field dispatcher
jeffamstutz Aug 19, 2026
53a3daf
fix(io): let fileOf() name a file that was given no directory
jeffamstutz Aug 19, 2026
a41fbc3
refactor(io): stop guarding importers on a filename being non-empty
jeffamstutz Aug 19, 2026
b681fd1
test(io): pin what an EnSight carrier's material outranks
jeffamstutz Aug 19, 2026
3e49027
fix(io): let pathOf() keep the separator the path already used
jeffamstutz Aug 19, 2026
728d48f
refactor(io): make the image cache the only way to name a Sampler's S…
jeffamstutz Aug 19, 2026
15cc0b3
refactor(io): stop handing PBRT's light converter a cache it never uses
jeffamstutz Aug 19, 2026
007459b
refactor(io): stop ImportContext from being copyable
jeffamstutz Aug 19, 2026
4b18512
docs(io): correct the shim call-site count and the cache inventory
jeffamstutz Aug 19, 2026
175ee2b
docs(io): say which images the orientation contract owns
jeffamstutz Aug 19, 2026
04338ab
test(io): pin where each USD light type puts its brightness
jeffamstutz Aug 19, 2026
062f1c7
style(io): clang-format one call in UsdInstancing
jeffamstutz Aug 19, 2026
6e164e0
refactor(io): declare InstancerRegistry's copy and move intent
jeffamstutz Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tsd/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Requires ANARI-SDK 0.15.0+ (`find_package(anari)` must succeed).
cmake -DVISRTX_BUILD_TSD=ON -DTSD_BUILD_APPS=ON ...
```

Key optional CMake flags: `TSD_USE_LUA`, `TSD_USE_ASSIMP`, `TSD_USE_HDF5`, `TSD_USE_MPI`, `TSD_USE_NETWORKING`, `TSD_USE_VTK`, `TSD_USE_SILO`, `TSD_USE_USD`.
Key optional CMake flags: `TSD_USE_LUA`, `TSD_USE_ASSIMP`, `TSD_USE_HDF5`, `TSD_USE_MPI`, `TSD_USE_NETWORKING`, `TSD_USE_VTK`, `TSD_USE_SILO`, `TSD_USE_USD`, `TSD_USE_OIIO`.

## Tests

Expand Down
2 changes: 2 additions & 0 deletions tsd/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ option(TSD_USE_USD "Use OpenUSD where relevant" OFF)
option(TSD_USE_TORCH "Use PyTorch for importing neural geometries" OFF)
option(TSD_USE_VTK "Use VTK for importing VTK file formats" OFF)
option(TSD_USE_SILO "Use Silo for importing Silo file formats" OFF)
option(TSD_USE_OIIO
"Use OpenImageIO to decode texture formats stb cannot (e.g. TIFF)" OFF)
option(TSD_USE_MPI "Enable MPI support" OFF)
option(TSD_USE_NETWORKING "Enable networking support via boost.asio" OFF)
option(TSD_NANOVDB_SKIP_INVALID_VOLUMES
Expand Down
37 changes: 37 additions & 0 deletions tsd/STYLEGUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,43 @@ scratch, check whether a TSD primitive already fits:

---

## Store Non-Owning Members as Pointers, Never References

Parent §6 allows raw pointers or ref wrappers for non-owning references; in TSD
a class member that refers to something it does not own is **always a raw
pointer**, default-initialized to `nullptr`:

```cpp
class ImageCache
{
public:
ImageCache(Scene *scene);
Scene *scene() const;

private:
Scene *m_scene{nullptr}; // not Scene &
};
```

This is the established shape — `Layer::m_scene`, `AnariHandleCache::m_scene`,
`AnyObjectUsePtr::m_scene`, and every `tsd/network/messages/` type store the
scene this way. A reference member silently deletes assignment and forces the
binding at construction, which breaks the movable-not-copyable lifetime that
`TSD_DEFAULT_MOVEABLE` and `DECLARE_OBJECT_DEFAULT_LIFETIME` declare
everywhere else.

The cost is that null becomes representable. Handle it at the boundary rather
than pushing the check onto callers: return the type's existing empty/failure
value (see Fallible Returns in parent §8), the way `self()` does in the object
skeleton below.

**This rule is about stored members only.** Function parameters stay
references where the argument is required and non-null — the importer and
exporter signatures in File I/O below take `Scene &` deliberately, and that
does not change.

---

## Scene Mutation and Notification

- Subclass `BaseUpdateDelegate` for any consumer that needs to react to scene
Expand Down
6 changes: 6 additions & 0 deletions tsd/apps/interactive/scivisStudio/ProjectContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1795,6 +1795,8 @@ const char *toString(tsd::io::ImporterType importerType)
return "OBJ";
case tsd::io::ImporterType::PDB:
return "PDB";
case tsd::io::ImporterType::PBRT:
return "PBRT";
case tsd::io::ImporterType::PLY:
return "PLY";
case tsd::io::ImporterType::POINTSBIN_MULTIFILE:
Expand All @@ -1809,10 +1811,14 @@ const char *toString(tsd::io::ImporterType importerType)
return "SMESH_ANIMATION";
case tsd::io::ImporterType::SWC:
return "SWC";
case tsd::io::ImporterType::SWC_SDF:
return "SWC_SDF";
case tsd::io::ImporterType::TRK:
return "TRK";
case tsd::io::ImporterType::USD:
return "USD";
case tsd::io::ImporterType::USD_MTLX:
return "USD_MTLX";
case tsd::io::ImporterType::VTP:
return "VTP";
case tsd::io::ImporterType::VTU:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct DatasetSourceChoice
bool subtree = false;
};

constexpr std::array<DatasetSourceChoice, 27> SOURCES = {{
constexpr std::array<DatasetSourceChoice, 28> SOURCES = {{
{"AGX", tsd::io::ImporterType::AGX},
{"ASSIMP", tsd::io::ImporterType::ASSIMP},
{"ASSIMP_FLAT", tsd::io::ImporterType::ASSIMP_FLAT},
Expand All @@ -49,6 +49,7 @@ constexpr std::array<DatasetSourceChoice, 27> SOURCES = {{
{"SWC", tsd::io::ImporterType::SWC},
{"TRK", tsd::io::ImporterType::TRK},
{"USD", tsd::io::ImporterType::USD},
{"USD_MTLX", tsd::io::ImporterType::USD_MTLX},
{"VTP", tsd::io::ImporterType::VTP},
{"VTU", tsd::io::ImporterType::VTU},
{"XYZDP", tsd::io::ImporterType::XYZDP},
Expand Down
2 changes: 2 additions & 0 deletions tsd/apps/interactive/viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ Use one of these flags before filenames to select how those files are loaded:
- `-swc`
- `-trk`
- `-usd`
- `-usd_mtlx` (USD, emitting native MaterialX materials rather than a portable
mapping; falls back per material where no MaterialX network converts)
- `-vtp`
- `-vtu`
- `-xyzdp`
Expand Down
1 change: 1 addition & 0 deletions tsd/apps/tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ override flags (`--campos`, `--lookpos`, `--upvec`, `--fovy`).

Use `-tsd <file>` to load a Scene Archive. Foreign-format importer flags
include `-gltf`, `-obj`, `-ply`, `-volume`, `-hdri`, `-silo`, `-usd`,
`-usd_mtlx`,
`-assimp`, `-axyz`, `-e57xyz`, `-pdb`, `-swc`, `-trk`, `-nbody`, and
`-l`/`--layer`.

Expand Down
2 changes: 2 additions & 0 deletions tsd/apps/tools/tsdOffline.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ static void printUsage(const char *programName)
std::cout << " -hdri <file> Set HDRI environment map\n";
std::cout << " -silo <file...> Import Silo files\n";
std::cout << " -usd <file...> Import USD files\n";
std::cout
<< " -usd_mtlx <file...> Import USD files as native MaterialX\n";
std::cout
<< " -l, --layer <name> Specify layer name for following imports\n";
std::cout
Expand Down
3 changes: 2 additions & 1 deletion tsd/apps/tutorial/tsdTutorialLoadUSD.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ int main(int argc, char *argv[])
// Populate spheres //

tsd::animation::AnimationManager anim(&scene);
tsd::io::import_USD(scene, anim, g_filename.c_str());
tsd::io::widenAnimationClock(
anim, tsd::io::import_USD(scene, anim, g_filename.c_str()));

// Setup ANARI device //

Expand Down
37 changes: 37 additions & 0 deletions tsd/docs/adr/0014-store-images-in-anari-orientation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Store images in ANARI orientation

A decoded image resident in a TSD Scene is stored in ANARI orientation: the
array's row 0 is the top row of the picture, because ANARI addresses texture
coordinate `(0, 0)` at the image's upper-left corner. Importers hand ANARI
texture coordinates in ANARI's convention, converting from the source format's
where they differ — glTF's `v` already runs down the image and is passed
through, while OBJ, USD, PBRT, and ASSIMP are all v-up and have their `v`
reversed. A format that also carries a uv transform of its own has it
conjugated by that reversal rather than flipped twice: PBRT's `vscale`/`vdelta`
and USD's `UsdTransform2d` both become `vs*v + (1 - vs - vd)`.

Decoders declare the row order their library produced and `ImageCache`
normalizes; no importer flips texels itself. Previously each of seven decode
paths carried its own unstated assumption, and the assumptions cancelled for
glTF, ASSIMP, and PBRT but not for OBJ and USD, whose textures rendered
mirrored. Two consequences follow from the contract: a cached image bound
somewhere that is not an image sampler asks `ImageCache` for the order that
consumer wants — a USD dome light's `radiance` runs bottom-up, so it says so
on its `ImageSource`; and block-compressed DDS, whose 4×4 blocks cannot be
row-reversed without decoding and re-encoding, would stay as authored and
instead get a `v`-flip composed into its sampler's `inTransform`/`inOffset`
— which is why `makeImageSampler` owns those two parameters outright and takes
the importer's own uv transform through `SamplerSettings`. Neither reversal
runs today: every decoder in `io/images/detail/decoders.hpp` emits top-down
rows, which is what a sampler asks for, so both paths are dormant rather than
dead.
The one loader that produces bottom-up rows, `importers/detail/HDRImage.h`, is
not one of those decoders, and its only cached consumer asks for bottom-up
delivery — so there is nothing to normalize there either.

This contract governs images the Image Cache owns. A radiance array bound to an
`hdri` light may be built without the cache and is not covered: `import_HDRI`
decodes exactly one image per call, so a cache scoped to the call can never be
hit, and PBRT's infinite light resamples equal-area to equirectangular, so what
it binds is not the decoded image and could not be keyed as one. Both keep the
bottom-up rows `HDRImage` produced, which is the order an `hdri` light wants.
17 changes: 17 additions & 0 deletions tsd/docs/adr/0015-import-usd-through-a-hydra-scene-index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Import USD through a Hydra scene index

TSD imports USD stages by consuming a UsdImaging scene index chain rather than
traversing UsdGeom and UsdShade schemas directly. Composition, purpose and
visibility resolution, native and point instancing, material binding and render
context selection, primvar interpolation, implicit-shape conversion, NURBS
approximation, and skinning are all resolved by OpenUSD's own filtering scene
indices; the importer contributes one converter per Hydra prim type and nothing
more. The stage stays open alongside the scene index, so TSD-specific data that
Hydra does not model — `customData` carriers, `anari:` and `tsd:io:` attributes,
render settings — is still read directly from prims by path. This costs links
against `hd`, `usdImaging`, `hio`, and `hdsi`, and requires working in
data-source idioms rather than schema APIs, but it removes the class of silent
omissions that a hand-rolled traversal accumulates one unhandled prim type at a
time. A Hydra render delegate was rejected for the same job: it is a push-based
sync architecture for repeated frame updates, not a one-shot conversion into a
scene the user then edits by hand.
17 changes: 17 additions & 0 deletions tsd/docs/adr/0016-bake-prototype-internal-transforms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Bake prototype-internal transforms when importing instanced USD content

`TransformsToAnariVisitor` never pushes a transform-array node's matrices onto
the transform stack, so a transform node nested beneath one composes against
that array node's ancestors instead: its subtree renders once, un-instanced,
rather than once per instance. Transform-array nodes are therefore leaf-only
instancing, while USD prototypes are typically `Xform` subtrees holding several
gprims at their own local transforms. The importer resolves this by importing
each prototype exactly once into shared TSD objects and baking each gprim's
prototype-root-relative transform into its vertex data, leaving a flat set of
Surfaces that a transform-array node may legally instance; a point instancer
becomes one transform-array node and a native instance becomes one mat4 node
reusing the same objects. Baking is cheap precisely because a prototype is
imported once regardless of instance count. Prototypes whose internal transforms
are themselves animated cannot be baked and fall back to expanded per-instance
transform nodes. Do not "correct" the baked vertex data without first changing
how the render index composes transform-array nodes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Deviate from usdview defaults for purpose and subdivision

USD import is judged by visual parity with a reference Hydra render, and two
import defaults deliberately differ from stock usdview anyway. usdview defaults
to `showProxy(true)` and `showRender(false)`, so it displays proxy stand-in
geometry; TSD imports `default` + `render` because it renders with a path tracer
for which proxy assets are the wrong input, and because an asset whose real
content sits behind `purpose=render` would otherwise import as its bounding-box
card. usdview defaults to complexity 1.0, which maps to refinement level 0 and
draws subdivision meshes as their unrefined control cage; TSD refines with
OpenSubdiv by default so silhouettes are correct. Both are configurable through
`UsdImportOptions`, and any parity comparison must set matching purpose and
Complexity on the reference render before treating a difference as a defect.
Neither deviation is a bug, and neither should be "aligned" with usdview without
revisiting this decision.
20 changes: 20 additions & 0 deletions tsd/docs/adr/0018-let-imported-scenes-retain-an-open-usd-stage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Let imported scenes retain an open UsdStage

Time-varying geometry is imported as one eager first frame plus a `FileBinding`
that re-pulls point and index arrays from the still-open stage and scene index
at time `t`, rather than baking every sampled frame into TSD Arrays. Baking a
hundred-frame, million-vertex mesh costs on the order of a gigabyte per mesh,
while the lazy binding keeps memory flat regardless of frame count and follows
the pattern `EnSightFileBinding` and `SpatialFieldFileBinding` already
establish. The consequence is that import is not a fully detached operation for
animated content: an imported scene's lifetime holds a `UsdStage` and its scene
index, serialization records file and prim paths and reconstructs by re-opening
rather than by copying data, scrubbing performs real work per frame, and stage
access must be accounted for when a scene is used across threads. Static content
carries no such dependency.

Amended by
[ADR 0021](0021-share-one-usd-stage-session-across-import-and-animation.md): the
retained stage is now a Stage Session shared with the import rather than one the
binding opens for itself, and the scene index it holds really is retained, which
the code this ADR described did not do.
44 changes: 44 additions & 0 deletions tsd/docs/adr/0019-report-udim-tile-sets-as-unsupported.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Report UDIM tile sets as unsupported rather than approximate them

A MaterialX `filename` input whose path contains a `<UDIM>` marker names a set
Comment thread
jeffamstutz marked this conversation as resolved.
of tiles, not a file. `UsdMaterials.cpp` reports such an input as
`TEXTURE_LOAD_FAILED` with `(tiled texture sets are not supported)` and binds no
sampler, while still writing the anchored absolute path into the generated
document. On the OpenPBR Shader Playground reference asset that accounts for 89
of the importer's 100 skipped prims -- every remaining texture failure after
TIFF decoding landed.

Two routes were investigated and both are closed at the level TSD can reach.

The device cannot resolve the tile set on TSD's behalf. VisRTX does load MDL
texture resources from disk -- `SamplerRegistry::loadFromImage` reads
`textureDesc.url` with stb -- but `libmdl::Core::resolveResource` returns
`get_element(0)->get_filename(0)`, a single filename. MDL's entity resolver
returns one element per tile for a `<UDIM>` resource; VisRTX discards all but
the first, and `loadFromImage` then builds exactly one `Image2D`. Nothing in
`devices/` carries a UDIM concept. So MDL's native `<UDIM>` support, which is
real, is not plumbed through this device at all, and setting
`mdlResourceSearchPaths` -- the seam that would fix the cosmetic
`Failed to resolve texture resource` log noise -- would not change the outcome.
Enabling it properly means multi-tile resolution in libmdl, a tile-indexed
texture in the MDL runtime PTX, and a representation for tiled textures in
ANARI, which has none: an ANARI sampler is a single image.

TSD cannot expand the tiles either, for the same reason. Rewriting the
MaterialX network into per-tile branches is a large amount of machinery, and
the obvious shortcut -- binding tile 1001 and dropping the rest -- is wrong for
every mesh that actually spans more than one tile, and worse, it converts a
reported gap into a silently incorrect render.

The gap is therefore left reported rather than approximated. Two properties
make that a deliberate stance and not neglect: the skip is counted in the
import report with a reason, so the cost is visible, and the anchored absolute
path is still written into the document, so a consumer that gains UDIM support
finds a well-formed path waiting. `SdfAssetPath::GetResolvedPath()` is empty
for every UDIM path -- a `<UDIM>` path names no file, so no resolver resolves
it -- which is why that path comes from `UsdMaterials.cpp`'s Stage-directory
fallback anchor rather than from USD's own resolution.

Revisit this when ANARI gains a tiled-texture or texture-array sampler, or when
VisRTX's MDL runtime grows tile-indexed lookup. Until one of those exists,
"supporting UDIM in TSD" has nowhere to send the texels.
57 changes: 57 additions & 0 deletions tsd/docs/adr/0020-bind-mesh-attributes-per-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Bind mesh attributes per Surface, not once per mesh

A USD Mesh with `GeomSubset` children converts to one TSD Surface per subset.
`UsdGeometry.cpp` used to build the attribute set once, on the parent mesh, and
let each subset borrow it through a hard-coded list of `vertex.normal`,
`vertex.attribute0`, and `vertex.color`. That works only for vertex-interpolated
data, which is indexed by the same vertex indices the subset already carries.

Face-varying data is not. It is indexed by `3 * triangle + corner` against the
mesh's full triangulation, so a subset -- which draws a chosen subset of those
triangles -- cannot point at the parent array; the corners it wants are not
contiguous and not at the offsets its own primitives imply. The same is true of
uniform data, which the importer expands to one value per triangle. Sharing was
therefore not merely incomplete, it was unavailable: assets authoring
`interpolation = "faceVarying"` texture coordinates -- which is what USD assets
overwhelmingly do -- rendered their subsets with no UVs at all.

Attributes are now expanded onto the triangulation once per mesh
(`triangulatePrimvars`) and then *gathered* per Surface for the triangles that
Surface draws (`buildTriangleGeometry`). Vertex-interpolated primvars still
create a single Array shared by every Surface, because for them the gather is
the identity; only per-triangle and per-corner data is copied per subset. The
cost of the copy is the price of correctness, and it is bounded by the size of
the mesh however many subsets divide it.

Two consequences follow from moving the binding to the Surface.

A subset can resolve its own material's UV primvar name. `ResolvedMaterial`
carries the primvar its texture reader asked for, and that answer now reaches
the geometry the material is bound to, instead of the mesh-level binding
deciding `attribute0` for subsets it knows nothing about. A subset without its
own answer falls back to the mesh's, then to the conventional `st`.

Faces that no subset claims become their own Surface under the mesh's material.
Previously the parent geometry was built, populated, and then never surfaced
when subsets existed -- unassigned faces were invisible and the objects were
dead weight in the scene. USD's own model is that a face outside every
`materialBind` subset keeps the mesh's binding, so that is what it gets.

One older gap is now easier to see and is deliberately left alone. `GeomSubset`
indices name coarse faces, but when a mesh is refined (ADR 0017 leaves
`refinementLevel` at 2) the triangulation this code selects from describes
*refined* faces, so a subset over coarse face 1 of a `catmullClark` quad pair
picks up refined face 1 -- 2 triangles rather than the 32 that face became.
Correcting it means carrying OpenSubdiv's refined-to-coarse face mapping out of
`refineMesh`, which is its own change; it is called out here so the leftover
Surface's suddenly-visible triangle count is not read as a regression from this
one.

The appeal to `materialBind` above is approximate in one respect, also unchanged
by this work: the importer treats every `geomSubset` child as a material subset,
because `HdGeomSubsetSchema` exposes only `type` and `indices` -- the scene
index does not carry `familyName`. A face claimed by a subset from some other
family is therefore counted as claimed and stays out of the leftover Surface.
Assets in the wild author `materialBind` subsets under a Mesh; if one appears
that does not, the family will have to be recovered from the Stage rather than
from the scene index.
Loading
Loading