Case study · Atomic Rig · SimReady integration
From robot CAD to avatar bits
How I built the pipeline that turns NVIDIA SimReady robot packages, simulation assets published by robot makers, into game-ready parts for Atomic Rig: discovered, cut into parts, decimated, deduplicated by shape, license-tracked and published from a single admin page.
Independent work. Neither Atomic Rig nor I am affiliated with or endorsed by NVIDIA, Hugging Face or any robot maker named here.
The problem
Why SimReady
Atomic Rig builds avatars from bits: rigid parts placed on a humanoid skeleton and exported as one character. A builder like that is only as good as its library, and I wanted a large, credible robot library quickly without modeling every part by hand.
SimReady is NVIDIA's OpenUSD packaging standard for simulation-ready assets. On October 21, 2026, NVIDIA announced SimReady Central, more than 2,000 of these assets on Hugging Face, each in its owner's own dataset under the owner's own license. Robot makers including FANUC (Apache 2.0), Franka (CC BY 4.0), Robotiq (CC BY 4.0), Comau (CC BY 4.0) and AgiBot (Apache 2.0) publish engineering-accurate models of their arms, grippers and humanoids there under licenses that allow commercial reuse. That is a great source, but simulation assets are not game assets:
- A single link can be 155,000 triangles of CAD tessellation, with hidden interior faces and inverted patches.
- One part is often a dozen flat CAD materials. A bit needs one material with real PBR maps.
- Packages are organized for physics (links, joints, colliders), not for the slots an avatar needs.
- They carry logos and brand colors, and each one has its own license and attribution terms.
- Some packages are several gigabytes, and the same robot often appears in more than one repo.
So the job was a pipeline, not a script: one that a person could steer in minutes per package, and that would never publish a part it couldn't credit.
End to end
The pipeline
Machine steps run from one CLI, simready-ingest. Every human decision happens on one admin page. The database only ever changes through that page or through SQL a person runs, never as a side effect of a script.
The first version had six CLI commands, three SQL pastes, two upload approvals and two separate review pages for every package. Collapsing that to one command and one page with a Publish button was the single biggest improvement in the project.
Stage 1–3
Discovery and selection
Discovery searches the Hugging Face dataset API and keeps only repos whose file tree contains a SimReady packaging file, which separates real packages from the many repos that only use the word. Of the roughly 150 datasets the search returned, the robot repos in that format were listed, 18 so far, holding 186 packages between them. list then reads each package's small metadata files (packaging, validation, benchmark, license and NOTICE text) and downloads no asset bytes at all.
Auto-reject rules
- License must be on the allow-list: CC0, CC BY 4.0, Apache 2.0 or BSD 3-Clause, read per package, not per repo. NC is out because we sell, ND because we decimate, SA because it would spread to every avatar a part is used in.
- Validation and benchmark results must exist with no blocking failure. Physics benchmark failures are ignored: they say nothing about the mesh.
- A size cap keeps one package from filling the disk.
Anything ambiguous is held, not rejected: a failed visual benchmark, or a second license file inside the package. NVIDIA's packages, for example, ship a BSD and Apache license for their shader library alongside the model's own license. Both are compatible, but a person confirms that once rather than code guessing.
Selection also means saying no. One repo turned out to be a rehost of other people's assets under the original publisher's package ids, which would have made the credit wrong. It was excluded on sight.
Stage 4
Segmentation
preview fetches a kept package and flattens it with OpenUSD into rigid bodies, joints and extents. Rules, not a model, turn those into proposed bits. That was a deliberate choice: the same package always segments the same way, every rule has a unit test, and a curator can see exactly why a part was proposed.
The result is a parts map: the assembled robot rendered with every proposed bit in its own color and discards in gray. The curator ticks the parts worth baking right on the card.


Stage 6
Decimation and baking
Every bit has a budget. Small or simple parts start at S (400 triangles), the rest at M (1,000), and a part that can't hold its shape moves up to L (2,500). The budgets are about the library, not any one platform: a SimReady link arrives at tens of thousands of triangles, and an avatar is dozens of bits edited live in a browser. Fixed tiers keep every bit's cost predictable, so the editor stays real-time and downloads stay small. Platform limits are a separate job, handled at export (see Composite avatars).
The modeling work runs headless and deterministic. Blender does the cleanup, the voxel remesh behind retry cages, and the PBR bake; meshoptimizer simplifies; glTF-Transform handles the files; and Three.js in headless Chromium, driven by Playwright, renders the comparisons. The first thing the loop does is clean up CAD data, because most bake artifacts come from the source, not the decimator.
The visual check
A triangle count says nothing about whether a part still looks right. Each bake is rendered next to its source from ten orthographic views and compared on two measures: how far the silhouette moved (on the front, top and side views), and how much of the bake is dark where the source isn't, averaged over all ten (the signature of inverted normals and failed bakes). A failing part is retried from a remeshed cage, then moved up a tier. If it still fails at L, it isn't silently dropped or shipped. It is flagged on the admin card for a person to judge.
Cleanup carries most of the weight. Welding, dropping fasteners under 3% of the part's size, and removing faces no camera can see from 42 directions routinely halve a part before simplification starts. The first link of the OpenArm went from 114,829 triangles to 54,686 after cleanup, then to 2,494 at L.

Stage 7 · Matching by shape
A topology-independent duplicate check
Robot makers reuse parts across a product line, and NVIDIA republishes some of the same robots under its own account. Eighty near-identical industrial arms are not eighty useful bits, and a marketplace full of lightly-renamed copies is worse than a small one. Duplicates had to be caught automatically, across every package and against everything already on the site.
The obvious tool, a hash of the mesh, catches almost nothing here. The same bracket arrives re-tessellated by a different exporter, at a different scale, rotated, offset or mirrored for the other side, and every one of those has a different hash. So the check ignores topology entirely and compares shapes: each part becomes a fingerprint of its surface that doesn't care how the surface was triangulated.
How it works
- Sample, don't read the mesh. 2,000 points scattered over the surface, weighted by triangle area, with a fixed seed so the same mesh always gives the same fingerprint. A cylinder with 12 segments and one with 48 produce nearly the same cloud.
- Remove pose and size. Center on the centroid, scale to unit RMS radius (uniform only, so a stretched part still looks different), and rotate onto the principal axes.
- A cheap summary for filtering. A 64-bin histogram of distances between 20,000 random point pairs (the D2 shape distribution). Pairs whose histograms are far apart are scored 0 without any search, which keeps whole-library matching fast.
- An exact score for the rest. PCA leaves the axis signs ambiguous, so all 8 sign flips are tried, which also catches mirrored left/right copies. When two axes tie (a square plate) or all three do (a cube), it sweeps rotations as well. The best orientation is polished with small rotations and scored by symmetric Chamfer distance, mapped to a 0–1 similarity.
Calibrated, not guessed
The thresholds come from unit tests on procedural meshes: pairs that must match (resampled, re-segmented, spun, mirrored) and pairs that must not (a 2:1 plate against a square one, a box against a cylinder). Every duplicate scores 0.975 or higher, every non-duplicate 0.891 or lower, and the line sits at 0.9. A read-only calibration script reruns the same report against a real baked package before any threshold changes.
From score to decision
Each baked part is compared with three pools, in order of precedence: every bit already on the site, its sibling parts in the same package, and parts of other packages waiting for review. A match at 90% or more drops the part by default, with the reason on the card ("97% similar to link-3 in CRX-10iA"). Matches from 70% are shown without dropping anything. Precedence makes the outcome stable: of two identical twins only one is ever dropped, and of two packages carrying the same part, the one that sorts first keeps it.
The curator can override any default. Publish recomputes the same rule on the server from the same inputs and refuses (409) if the result no longer matches what the page showed, so nothing is published from a stale view.
Human in the loop
The admin
One admin curation page, with one card per package. A card only ever moves forward through a small state machine, and each state shows exactly the decision it needs.
- Publish is transactional. It writes the bits, their provenance records and the rejected parts (with why) in one function call.
- The server never trusts the page's view. If the shape matches changed since the page loaded, Publish answers 409 and the curator reviews again.
- Grippers become collections. A gripper's base and fingers are saved as one set rooted on the hand bone and listed on the marketplace for free.
- Dropped parts stay recoverable. A part dropped at Publish can be published later on its own, under the package's existing credit.
Data
Storage
The rule is to keep what proves where a part came from and to discard everything that can be fetched again. There is no source archive: Hugging Face, pinned to a commit, is the source of truth.
Every upload goes to a content-hashed path, so an upload never overwrites anything. If a path exists, it already holds that exact file. That made re-runs safe by construction: baking a package twice, or retrying after a dropped connection, costs time but can't corrupt a published bit. It is also what let me give the agent standing approval to upload to that one folder.
Attribution
License tracking
CC BY only allows reuse if you credit the creator in the way they ask, link the license and say what you changed. Get that wrong and the license can end, which would make every sale of that part an infringement. So attribution is part of the data model, not a footer.
- Credits are a union, computed on demand: the deduplicated sources behind every bit in an avatar, collection or download.
- They travel with the file: in GLB metadata, USD layer metadata, a CREDITS.txt next to FBX and Roblox exports, and on a public credits page linked from each file.
- They are frozen per export. A download keeps the credits it was made with, even if a source record is corrected later, which is why credit lines are agreed before anything is baked.
- Credit is set deliberately, per vendor. A missing vendor entry stops the run. For repos that publish many makers' robots under one account, credit and branding resolve per package, never from the repo owner's name.
- Logos are always stripped. A permissive copyright license still grants no trademark rights, so materials carrying lettering or brand colors can be repainted before baking.
The payoff
Composite avatars
A published part is an ordinary bit. In the editor it sits alongside starter bits, user uploads, image-to-3D generations and marketplace collections, bound rigidly to a Mixamo-compatible skeleton. Export bakes the live scene into the target format.
The FBX path uses my own binary FBX 7.4 writer rather than a library, because each target (Unity, Blender, Roblox Studio) needs a slightly different node layout and every one was verified by importing the file in that tool. Platform budgets live here, not at ingest: the Roblox target decimates the whole composed avatar to 10,742 triangles, so the library never has to be baked down for the strictest platform.
How it was built
Automation and skills
I built this with Claude Code as a pair, and treated the agent like a new teammate who needs a runbook and clear permissions, not a magic box.
Trust boundaries
The agent may, without asking
- Read the database and Hugging Face metadata
- Run list, preview, run and clean
- Upload to content-hashed simready/ paths
- Open PRs with tests, written first
A person decides
- Every SQL write and migration
- Keep, skip, confirm a license
- Which parts to bake, and Publish
- Credit lines, brand paint-outs, merges
The boundaries are enforced in the system, not just in the prompt: the CLI never writes the database, the admin routes are admin-only, Publish is a single database function, and uploads can't overwrite.
A skill as the runbook
The pipeline's knowledge lives in a project skill: a short document an agent loads whenever it touches ingest. It covers who does what, the nine stages, the checks before baking and the common mistakes. I tested it the way you'd test code, with one trial each, so treat it as a smoke test rather than a benchmark. Without the skill, a fresh agent given a realistic ingest task rediscovered the pipeline from source and missed the last two stages. With the skill, a second fresh agent on the same task reused a listing from that morning, skipped known duplicates, and caught that preview without a package filter downloads every candidate. That gap went straight back into the skill.
Tests as the contract
Every rule (auto-reject, segmentation, tier choice, credit resolution, the kept/dropped logic) is pure and unit-tested against recorded package metadata. Bug fixes start with a test that reproduces the bug. The pipeline lives in Atomic Rig's private repository, which runs over 1,200 unit tests on every push. The public atomic-rig repository is a snapshot from August 2026, before this pipeline existed, and has fewer tests.
Honest notes
What broke
- A metadata format nobody else used. NVIDIA's own robot repo writes its list of root files in a different shape. The code fell back to a path from NVIDIA's validation run that doesn't exist in the published repo, and every package in that repo failed to download. One failing test and a short fix later, all 134 of its packages that had passed the license check could be fetched.
- The wrong name on the credit. Resolving credit by repo owner would have credited and branded every robot in that repo, from a dozen makers, as one company. Credit now resolves per package for repos like that.
- A quality bar has to be enforced, not remembered. An early bulk re-bake replaced full-quality starter bits with budget bakes, guarded only by a darkness check. Content-hashed storage meant the originals were untouched and the fix was a rollback. The rule since: the library stays full quality, budgets belong to the bake and export, and no bake replaces a bit without a person comparing before and after.
- CAD surfaces bake black. Inverted patches and hidden interior faces were the main source of failed bakes, which is why the 42-direction visibility pass and the dark-pixel check exist.
- Networks drop. A package failed mid-download with nothing more than "terminated". Hash-verified, resumable steps meant the retry just worked.
Roadmap
What's next
- Per-package logo paint-outs for the large multi-maker batch, chosen from the parts maps.
- Roblox smoke tests: composed SimReady avatars imported into Studio and play-tested.
- Solid fitting: turning a part's mesh into a signed distance field and fitting clean solids, for bits that can be edited, not just placed.
- Making Confirm license a recorded notice instead of a hold when the second license is already on the allow-list.
Work with me
I build 3D products end to end: real-time rendering, asset pipelines, avatar systems and the tooling around them, with twenty years of production 3D underneath. If you're working on something like this, I'd like to hear about it.




