diff --git a/CHANGELOG.md b/CHANGELOG.md index ec412d5..a122352 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,9 +30,14 @@ Stage-contract version: **1**, authored since the Phase 0 importer. also forwards the host-resolved Python development paths into the clean installed-consumer configure, provisions Python for Windows runtime validation, and recognizes the complete macOS OpenUSD foundation closure. - `mmdMotionBinding` now preserves the VMD model name - as provenance. End-to-end retarget and `UsdSkelAnimation` authoring remains - Phase 9 work. (`MOTION_CONTRACT.md` §10, §12.) + `mmdSkeletonAdapter` now also states `motionRetarget`'s pinned `motionCore` + artifact closure so its standalone build is complete. `mmdMotionBinding` + now preserves the VMD model name as provenance. A deterministic skeletal + acceptance now carries VMD-derived, IK-evaluated legs through `motionUsd`, + retargets them with `motionRetarget` onto both a PMX-derived stage skeleton + and a non-MMD skeleton, and binds the PMX target's `UsdSkelAnimation`; + `motionUsd` v0.5.0 is digest-pinned for that test only. + (`MOTION_CONTRACT.md` §10, §12; report 2026-09-22.) - **Physics runtime integration direction.** A proposed focused contract now fixes the future boundary: the existing `/Asset/physics` stage remains the diff --git a/docs/README.md b/docs/README.md index e8bf964..767ac0a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,7 +4,7 @@ Documentation is organized by responsibility: each category answers one class of question. The layout is the one `usd-vrm-plugins`, `open-strata` and `hydra-merlin` use, so the repositories read the same way. -**The tree holds Phases 0–7 and most of Phase 9 (2026-09-21):** the PMX structural parser reads +**The tree holds Phases 0–7 and most of Phase 9 (2026-09-22):** the PMX structural parser reads every table of a PMX 2.0 or 2.1 file, `mmd_inspect` reports on it, and `.pmx` opens as the canonical stage — mesh, UVs, material prims and subsets, skeleton and skinning, Y-up, in meters. Phase 3 adds canonical MMD material @@ -17,8 +17,9 @@ where it matches, simulating nothing. Phase 7 reads VMD motion without a model (`motionVmd`, `vmd_inspect`) and binds it to one by MMD's name rule (`mmdMotionBinding`), baking nothing. Phase 9 evaluates MMD's control rig over a bound motion (`mmdControl`) and hands the result to `usd-motion-plugins` -through `mmdMotionAdapter` and `mmdSkeletonAdapter`. End-to-end retarget and -animation authoring acceptance remains. Future +through `mmdMotionAdapter` and `mmdSkeletonAdapter`. Skeletal end-to-end +retarget and animation authoring acceptance is covered; expression +interoperability and rest-direction evidence remain. Future physics execution consumes the existing static stage through `usd-physics-plugins`, with MMD coupling kept here and simulation outside the importer. Everything @@ -34,7 +35,7 @@ place that says what is implemented. | [roadmap/](roadmap/) | What is planned next (incomplete work only), and which release carries it. | [README.md](roadmap/README.md) · [current.md](roadmap/current.md) | | [contributing/](contributing/) | How to maintain these documents. | [documentation.md](contributing/documentation.md) | | [releases/](releases/) | Immutable per-version release records, and how a release is cut. | [README.md](releases/README.md) · [v0.1.0.md](releases/v0.1.0.md) | -| [reports/](reports/) | Dated evidence from real runs; append-only. | [2026-09-15-phase2-local-models.md](reports/2026-09-15-phase2-local-models.md) · [2026-09-16-phase4-local-models.md](reports/2026-09-16-phase4-local-models.md) · [2026-09-16-phase5-local-models.md](reports/2026-09-16-phase5-local-models.md) · [2026-09-17-phase6-local-models.md](reports/2026-09-17-phase6-local-models.md) · [2026-09-17-phase7-local-motions.md](reports/2026-09-17-phase7-local-motions.md) | +| [reports/](reports/) | Dated evidence from real runs; append-only. | [2026-09-15-phase2-local-models.md](reports/2026-09-15-phase2-local-models.md) · [2026-09-16-phase4-local-models.md](reports/2026-09-16-phase4-local-models.md) · [2026-09-16-phase5-local-models.md](reports/2026-09-16-phase5-local-models.md) · [2026-09-17-phase6-local-models.md](reports/2026-09-17-phase6-local-models.md) · [2026-09-17-phase7-local-motions.md](reports/2026-09-17-phase7-local-motions.md) · [2026-09-22-phase9-motion-acceptance.md](reports/2026-09-22-phase9-motion-acceptance.md) | ## Canonical documents diff --git a/docs/architecture/DEPENDENCIES.md b/docs/architecture/DEPENDENCIES.md index 5ae00d2..d701641 100644 --- a/docs/architecture/DEPENDENCIES.md +++ b/docs/architecture/DEPENDENCIES.md @@ -96,13 +96,13 @@ exact version, and listed in `THIRD_PARTY_NOTICES.md`. The shared motion core: vendor- and avatar-format-neutral poses and clips, humanoid joint semantics, sampling, retargeting, recording and the `UsdSkelAnimation` bridge. v0.5.0 was published on 2026-09-20 with installable -`motionCore` and `motionRetarget`; the two adapters consume their per-target, -digest-pinned OpenStrata artifacts. +`motionCore`, `motionRetarget` and `motionUsd`; the adapters and their skeletal +acceptance test consume per-target, digest-pinned OpenStrata artifacts. | | | | --- | --- | -| Packages | `motionCore` (`HumanJoint`, `MotionPose`, `RootMotion`, `MotionClip`) and `motionRetarget` (`SkeletonDescriptor`, `RetargetMap`, `SourceRestPose`), each by `find_package( CONFIG)` and linked as `::`; `motionUsd` only where [WORKSPACE.md §2.4](WORKSPACE.md#24-edges-out-of-this-repository) allows it | -| Used by | `mmdMotionAdapter` (`motionCore`) and `mmdSkeletonAdapter` (`motionRetarget`); later perhaps `usdVmdFileFormat` (MOT-O2) | +| Packages | `motionCore` (`HumanJoint`, `MotionPose`, `RootMotion`, `MotionClip`), `motionRetarget` (`SkeletonDescriptor`, `RetargetMap`, `SourceRestPose`) and `motionUsd` (the standalone motion-stage writer/reader), each by `find_package( CONFIG)` and linked as `::` | +| Used by | `mmdMotionAdapter` (`motionCore`), `mmdSkeletonAdapter` (`motionRetarget`, plus its required `motionCore` artifact closure) and the Phase 9 acceptance test (`motionRetarget`, `motionUsd`); later perhaps `usdVmdFileFormat` (MOT-O2) | | Consumed as | an installed package, by `find_package` with a version range admitting the release it was verified against, the way siblings are ([WORKSPACE.md §5](WORKSPACE.md#5-build-modes)) | | Version | `>=0.5,<0.6`, verified against v0.5.0 | | OpenUSD | the same exact pin as §1 | @@ -114,6 +114,11 @@ retarget and one USD mapping; a private MMD copy would be the permanent duplication that policy forbids (its §37). What stays here is what needs MMD to be understood ([MOTION_CONTRACT.md §10](../design/MOTION_CONTRACT.md#10-normalizing-into-the-shared-motion-core)). +`motionUsd` is test-only today. Its pin lives on `mmdMotionAdapter`'s manifest +so a standalone or workspace test has the same artifact CI uses; the exported +`mmdMotionAdapter::mmdMotionAdapter` target and installed package do not link +or require it. + ## 7. usd-physics-plugins The future backend-neutral physics layer: scene construction from authored diff --git a/docs/architecture/PACKAGE_CONTRACT.md b/docs/architecture/PACKAGE_CONTRACT.md index 4d4a026..70e7412 100644 --- a/docs/architecture/PACKAGE_CONTRACT.md +++ b/docs/architecture/PACKAGE_CONTRACT.md @@ -122,9 +122,10 @@ five packages alone. The installed-consumer lane's `control_probe` finds | Version compatibility | `SameMinorVersion`, as `mmdPmx` | | Installed files | `${CMAKE_INSTALL_LIBDIR}/` (the archive), `${CMAKE_INSTALL_LIBDIR}/cmake/mmdSkeletonAdapter/`, and `include/mmdSkeletonAdapter/` | -Its manifest pins `motionRetarget` by archive and OCI digest for each supported -target. The installed-consumer lane verifies the package from outside the -source tree against that external package. +Its manifest pins `motionRetarget` and that package's `motionCore` dependency +by archive and OCI digest for each supported target, so its isolated artifact +closure is complete. The installed-consumer lane verifies the package from +outside the source tree against those external packages. ## `mmdMotionAdapter` @@ -138,7 +139,9 @@ source tree against that external package. | Version compatibility | `SameMinorVersion`, as `mmdPmx` | | Installed files | `${CMAKE_INSTALL_LIBDIR}/` (the archive), `${CMAKE_INSTALL_LIBDIR}/cmake/mmdMotionAdapter/`, and `include/mmdMotionAdapter/` | -Its manifest pins `motionCore` per target. The installed-consumer lane binds a +Its manifest pins `motionCore` per target, and pins `motionUsd` only to supply +the Phase 9 acceptance test; `motionUsd` is not in the exported target or +installed package contract. The installed-consumer lane binds a generated VMD to a generated PMX, evaluates it, and builds a shared `MotionClip` through both installed adapters. diff --git a/docs/architecture/WORKSPACE.md b/docs/architecture/WORKSPACE.md index b7e641d..3d980e3 100644 --- a/docs/architecture/WORKSPACE.md +++ b/docs/architecture/WORKSPACE.md @@ -103,6 +103,8 @@ mmdSkeletonAdapter ──→ mmdModel, mmdMotionAdapter ────→ mmdControl, mmdModel, mmdSkeletonAdapter, usd-motion-plugins motionCore (OpenUSD foundation types only, through it) +mmdMotionAdapter acceptance test + ────→ usd-motion-plugins motionRetarget, motionUsd usdVmdFileFormat ────→ motionVmd, OpenUSD; usd-motion-plugins motionUsd if MOT-O2 says so mmd_convert ─────────→ usdMmdFileFormat's public entry point, OpenUSD @@ -173,6 +175,9 @@ over `libs/mmdControl` with `mmdMotionBinding::mmdMotionBinding` and `mmdSkeletonAdapter_boundaries` runs with that library, allowing `motionRetarget`; `mmdMotionAdapter_boundaries` allows `motionCore` and the skeleton adapter. Those are the two narrow external adapter edges (§2.4). +The separate `mmdMotionAdapter_acceptance` executable intentionally links +`motionRetarget` and stage-level `motionUsd`; it is not the library target the +boundary test inspects. The five OpenUSD-free libraries are added before OpenUSD is resolved; the adapters follow it because their shared packages expose OpenUSD foundation types and reuse the root's already-resolved targets. The binary-import part @@ -197,13 +202,17 @@ usd-stage-runner ────→ usd-physics-plugins | Rule | Detail | | --- | --- | -| Narrow crossings | `mmdMotionAdapter` depends on `motionCore`; `mmdSkeletonAdapter` depends on `motionRetarget`, which owns `SkeletonDescriptor`, `RetargetMap` and `SourceRestPose` ([MOTION_CONTRACT.md §10.4](../design/MOTION_CONTRACT.md#104-skeleton-and-humanoid-map)). `usdVmdFileFormat` may add `motionUsd` if MOT-O2 says so. No parser, canonical model, evaluator or importer crosses. | +| Narrow crossings | `mmdMotionAdapter` depends on `motionCore`; `mmdSkeletonAdapter` depends on `motionRetarget`, which owns `SkeletonDescriptor`, `RetargetMap` and `SourceRestPose` ([MOTION_CONTRACT.md §10.4](../design/MOTION_CONTRACT.md#104-skeleton-and-humanoid-map)). The Phase 9 acceptance executable, not either exported target, uses `motionRetarget` and `motionUsd`. `usdVmdFileFormat` may add `motionUsd` if MOT-O2 says so. No parser, canonical model, evaluator or importer crosses. | | Installed packages only | The edge is a `find_package` on an installed package with a declared version range, never a sibling checkout, a submodule or a vendored copy (§5, [DEPENDENCIES.md §6](DEPENDENCIES.md#6-usd-motion-plugins)). | | Never the reverse | `usd-motion-plugins` never depends on any component here, and nothing here is designed to be moved there: VMD is MMD's format (the motion policy's §26). | -| Same OpenUSD | `motionCore` and `motionRetarget` are built against the OpenUSD release this repository pins ([DEPENDENCIES.md §1](DEPENDENCIES.md#1-openusd)); a mismatch is a configure error, not a warning. | +| Same OpenUSD | `motionCore`, `motionRetarget` and test-only `motionUsd` are built against the OpenUSD release this repository pins ([DEPENDENCIES.md §1](DEPENDENCIES.md#1-openusd)); a mismatch is a configure error, not a warning. | The two adapter edges are active since `usd-motion-plugins` v0.5.0: their -manifests pin `motionCore` and `motionRetarget` artifacts by target and digest. +manifests pin `motionCore` and `motionRetarget` artifacts by target and digest; +the skeleton adapter states `motionRetarget`'s `motionCore` artifact closure +explicitly so it can build in isolation. +The `mmdMotionAdapter` manifest additionally pins `motionUsd` for its test-only +acceptance edge; the adapter's link interface remains unchanged. A future MMD-specific physics adapter may similarly consume `usd-physics-plugins`; its identity and edge are added here only when the first runtime consumer makes them concrete @@ -348,6 +357,7 @@ target, header root and required packages — is | pyramid | the bundle manifest's `tests:` | `ost plugin test` L0–L5, from the build tree and from the package | — (`ost`) | | baseline | the bundle's `tests/fixtures/` | compact goldens do not change silently | the L5 goldens of `minimal.pmx` and `recoverable/unsafe-texture-paths.pmx` | | installed consumer | `tests/installed_consumer/` | installed packages work from a clean prefix outside the repository | `workspace_installed_consumer` | +| shared-motion acceptance | `libs/mmdMotionAdapter/tests/` | VMD-derived IK motion survives `motionUsd`, poses a PMX-derived stage skeleton, and retargets through an MMD-free translation unit to a non-MMD skeleton | `mmdMotionAdapter_acceptance` | | fuzz | `libs/mmdPmx/fuzz/`, `libs/motionVmd/fuzz/` | malformed input never crashes or over-reads, under ASan and UBSan | `mmdPmx_fuzz` and `motionVmd_fuzz` in [parser-sanitizers.yml](../../.github/workflows/parser-sanitizers.yml), which also runs every plain library's unit and robustness tests instrumented | Fixtures are generated by committed code, never copied from distributed diff --git a/docs/design/MOTION_CONTRACT.md b/docs/design/MOTION_CONTRACT.md index 0b8881b..27407da 100644 --- a/docs/design/MOTION_CONTRACT.md +++ b/docs/design/MOTION_CONTRACT.md @@ -289,7 +289,7 @@ as a whole. | MOT-O2 | What a directly opened `.vmd` stage looks like. `usd-motion-plugins` fixes the frame (`/Animation`, the body as `UsdSkelAnimation`, `customData.motion`); what remains is that a VMD without a model has only control-rig tracks, which no evaluator can turn into body motion (§10.2) — so either the stage carries source tracks outside `Body`, or no such stage exists | `usd-motion-plugins`' `motionUsd` contract, then a consumer | | MOT-O4 | Camera and light tracks: any USD mapping at all | a consumer that needs one | | MOT-O8 | Whether `mmdControl` must also evaluate from a stage alone — `/Asset/rig` and `/Asset/morph` — for a runtime that holds no `CanonicalDocument` (§10.3) | a consumer that holds only the stage | -| MOT-O10 | The rest a clip from MMD states. Every MMD bone rests at identity rotation, so §12.3's rotations are relative to the model's modelled pose, in which every local character's upper arms point 37–42° below horizontal ([report](../reports/2026-09-19-phase9-roles-and-root.md)) — not the level arms a VRM's identity rest describes. Whether `mmdSkeletonAdapter` states a `SourceRestPose` measured from the rest bone directions (as the shared core's BVH profiles state `rest-offsets`), and against which reference directions, or leaves the difference to a retarget option | the adapters' first retarget onto a non-MMD skeleton | +| MOT-O10 | The rest a clip from MMD states. Every MMD bone rests at identity rotation, so §12.3's rotations are relative to the model's modelled pose, in which every local character's upper arms point 37–42° below horizontal ([report](../reports/2026-09-19-phase9-roles-and-root.md)) — not the level arms a VRM's identity rest describes. Whether `mmdSkeletonAdapter` states a `SourceRestPose` measured from the rest bone directions (as the shared core's BVH profiles state `rest-offsets`), and against which reference directions, or leaves the difference to a retarget option | a measured A-pose-to-level-arm retarget comparison; the first generic skeletal acceptance used identity rest rotations and could not answer it ([report](../reports/2026-09-22-phase9-motion-acceptance.md)) | | MOT-O11 | Whether a plane link starts from its keyed rotation. §11.7 starts an enabled chain's plane angles at zero, so a knee's keyed rotation never reaches the pose and the knee is solved from straight; three.js r168 starts from the keyed rotation. On a motion that keys its legs' rotations alongside their goals, that start alone leaves a median 0.05 mm where §11.7 leaves 2.4 mm, and moves the knees a median 4.6 mm and at most 66 mm; on an IK-authored motion it changes little ([report](../reports/2026-09-19-phase9-ik-reference.md)). Which MMD does | MMD's output on a motion that keys its IK links | Resolved: diff --git a/docs/guides/building.md b/docs/guides/building.md index 8128669..98782cd 100644 --- a/docs/guides/building.md +++ b/docs/guides/building.md @@ -60,6 +60,7 @@ bundle's `plugInfo.json` expects it, `mmd_inspect` into | `mmdControl_robustness` | 20,000 generated rigs and motions — appends and IK chains naming any joint, wild loop counts, limits and keys, cyclic group morphs — each evaluated at five times, twice: no crash, the same bits both times, and finite unit-rotation poses from the tame half | | `mmdControl_boundaries` | `mmdControl`'s sources include no OpenUSD and no `motionCore/` header, it links `mmdMotionBinding` and `mmdModel` and nothing else, and a binary linking it imports no OpenUSD library | | `mmdSkeletonAdapter_boundaries`, `mmdMotionAdapter_boundaries` | each adapter links only its declared local and shared-motion packages; its test binary may import the OpenUSD foundation closure those packages expose, including the private `usd_boost`/`usd_python` support libraries on macOS, but no stage, schema or imaging library | +| `mmdMotionAdapter_acceptance` | a synthetic VMD leg-IK motion is bound and evaluated, round-tripped through `motionUsd`, retargeted onto a PMX-derived `UsdSkelSkeleton` and authored as its animation, then retargeted again onto a non-MMD skeleton through a source file containing no MMD dependency | | `mmd_inspect_fixtures` | `mmd_inspect` reads every generated fixture as `fixtures.json` says, from an ASCII and a non-ASCII directory | | `mmd_inspect_boundaries` | `mmd_inspect` links `mmdPmx` and nothing else, and imports no OpenUSD library | | `vmd_inspect_fixtures` | `vmd_inspect` reads every generated VMD fixture as its `fixtures.json` says, from an ASCII and a non-ASCII directory | diff --git a/docs/reference/CAPABILITY_MATRIX.md b/docs/reference/CAPABILITY_MATRIX.md index 3c7ba50..2e73f76 100644 --- a/docs/reference/CAPABILITY_MATRIX.md +++ b/docs/reference/CAPABILITY_MATRIX.md @@ -4,7 +4,7 @@ What the current code supports, feature by feature. This page states **facts about the tree**, not plans; a status here changes only in the change that adds the fixture proving it. -**As of 2026-09-21 the tree holds Phases 0–7 and most of Phase 9:** `.pmx` is registered, every +**As of 2026-09-22 the tree holds Phases 0–7 and most of Phase 9:** `.pmx` is registered, every table of a PMX 2.0 or 2.1 file is parsed and validated (by `mmdPmx`, reported by `mmd_inspect`), canonicalized (by `mmdModel`), and authored as the canonical stage — mesh, UVs, material prims and subsets, skeleton and @@ -126,6 +126,7 @@ lane against digest-pinned `usd-motion-plugins` v0.5.0 packages. | Non-bone morph weights under `mmd:morph:` channels | supported | [MOTION §10.7](../design/MOTION_CONTRACT.md#107-morphs-as-channels) | | Evaluated model visibility under the reserved `mmd:model:visibility` channel | supported | [MOTION §10.7](../design/MOTION_CONTRACT.md#107-morphs-as-channels) | | Non-finite evaluated shared values | rejected, never replaced | [MOTION §10.8](../design/MOTION_CONTRACT.md#108-diagnostics) | +| VMD-derived, IK-evaluated legs through a `motionUsd` round trip and `motionRetarget`, onto a PMX-derived stage skeleton and a non-MMD synthetic skeleton | supported | [Phase 9 acceptance report](../reports/2026-09-22-phase9-motion-acceptance.md) | | Rest-direction correction for an A-pose source (MOT-O10) | unverified | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | ## PMX model import diff --git a/docs/reports/2026-09-22-phase9-motion-acceptance.md b/docs/reports/2026-09-22-phase9-motion-acceptance.md new file mode 100644 index 0000000..fb3b244 --- /dev/null +++ b/docs/reports/2026-09-22-phase9-motion-acceptance.md @@ -0,0 +1,66 @@ +# Phase 9 skeletal motion acceptance (2026-09-22) + +Dated evidence from real runs; append-only +([contributing/documentation.md](../contributing/documentation.md)). + +## Question + +Can a VMD-derived clip be evaluated with MMD leg IK, cross the shared +`motionUsd` representation, pose the skeleton of a PMX target through +`motionRetarget`, and also retarget onto a skeleton that has no MMD naming or +code on its generic path? + +## Deterministic acceptance test + +`mmdMotionAdapter_acceptance` constructs its inputs in memory, so it needs no +distributed asset and is repeatable in every CI cell: + +1. A VMD document moves `左足IK` by 0.2 m upward and 0.05 m forward and + enables the chain. It is grouped by `motionVmd::BuildMotion`, bound by + `mmdMotionBinding`, and evaluated by `mmdControl` on a generated PMX + humanoid. +2. `mmdMotionAdapter` samples two frames. The test verifies that both the + upper-leg and knee humanoid rotations are non-identity even though the VMD + keys only the IK goal: the control result, not the goal bone, crossed the + boundary. +3. `motionUsd` v0.5.0 authors that `MotionClip` to an in-memory stage and reads + it back. The sample count, source format and absence of read warnings are + asserted. +4. A second generated PMX model, with different proportions, is adapted. Its + exact joint tokens and rest transforms are authored as + `/Asset/skel/Skeleton`, read back into a `SkeletonDescriptor`, and compared + with the adapter's descriptor. `motionRetarget` produces one full target + array per sample; the retargeted leg and knee are non-identity, diagnostics + are empty, and a bound `/Asset/skel/RetargetedAnimation` names the target + skeleton's joint order. +5. The same round-tripped clip is passed to `generic_retarget.cpp`. That source + file and its header include only shared `motionRetarget` types. It retargets + cleanly onto an English-named synthetic rig (`World/Pelvis`, `Leg_L`, + `Shin_L`, and so on), and the leg remains driven. + +The test passed on Windows 11 with OpenUSD 26.08 and the digest-pinned +`motionCore`, `motionRetarget` and `motionUsd` v0.5.0 artifacts. The workspace +graph check also passed with all three external edges declared. + +## Local asset trial + +A separate one-off run used a distributed Miku PMX as the VMD's source model, +`partial_lamb.vmd`, and `尼可.pmx` as the target. None of those inputs is a +fixture, and none is committed or redistributed. + +The resulting flattened USDA contained 1,201 samples at 30 fps over frames +0–1,200, targeted the PMX stage's 539 joints, and reported zero retarget +diagnostics. It had no references or sublayers after flattening, passed +`usdchecker`, and was opened in `usdview`; the user observed the target moving. +The generated USDA remains local and is not part of the repository. + +## Result and limits + +The Phase 9 skeletal end-to-end acceptance item passes. It proves the +MMD-specific work ends at evaluated shared values, because the second +retarget call is compiled in a translation unit with no MMD dependency. + +This does not resolve MOT-O10. The deterministic source and generic target use +identity rest rotations, while the local visual check established motion but +did not measure A-pose-to-target rest-direction correction. Expression-channel +interoperability is also a separate Phase 9 item. diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md index 9f65317..c951a44 100644 --- a/docs/roadmap/README.md +++ b/docs/roadmap/README.md @@ -37,7 +37,7 @@ in.** No other document states a version for a Phase. | 6 | physics preservation | ✅ done | [v0.1.0](../releases/v0.1.0.md) | | 7 | VMD | ✅ done | [v0.1.0](../releases/v0.1.0.md) | | 8 | avatar runtime composition, including optional physics coupling | ⬜ | unassigned, and owned mostly outside this repository | -| 9 | shared motion core adoption — runs before Phase 8 | 🚧 evaluator and adapters done; end-to-end acceptance remains | unassigned | +| 9 | shared motion core adoption — runs before Phase 8 | 🚧 skeletal evaluator, adapters and end-to-end acceptance done; expression interoperability and open rest-pose evidence remain | unassigned | Phases 0–7 ship together in v0.1.0, the first release, decided on 2026-09-17: it is the one that meets @@ -114,8 +114,12 @@ Where things stand, as of 2026-09-21: [report](../reports/2026-09-19-phase9-roles-and-root.md)). `mmdMotionAdapter` and `mmdSkeletonAdapter` consume the released, digest-pinned `motionCore` and `motionRetarget` v0.5.0 packages and are - covered by unit, boundary and installed-consumer tests. End-to-end retarget - and `UsdSkelAnimation` authoring acceptance remains. + covered by unit, boundary and installed-consumer tests. A deterministic + acceptance test now carries VMD-derived, IK-evaluated legs through + digest-pinned `motionUsd` and `motionRetarget`, onto both a PMX-derived + stage skeleton and a non-MMD skeleton + ([report](../reports/2026-09-22-phase9-motion-acceptance.md)). Expression + interoperability and MOT-O10 remain. - Phase 8, avatar runtime composition, follows Phase 9 and is owned mostly outside this repository. Its MMD physics work consumes the already-authored stage through `usd-physics-plugins`; the importer never gains a solver @@ -141,7 +145,7 @@ schedules them. | MOT-O5 | Which MMD bones feed `RootMotion` | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | resolved in Phase 9 | | MOT-O6 | The humanoid role table and its version | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | resolved in Phase 9 | | MOT-O9 | MMD's own IK distance at a model's loop count | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | resolved in Phase 9 — §11.7 kept | -| MOT-O10 | The rest a clip from MMD states | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | the adapters' first retarget onto a non-MMD skeleton | +| MOT-O10 | The rest a clip from MMD states | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | measured A-pose-to-level-arm retarget comparison; the first generic acceptance used identity rest rotations | | MOT-O2 | What a directly opened `.vmd` stage looks like | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | `usdVmdFileFormat` | | MOT-O3 | Which runtime owns MMD IK and append evaluation for baking | [MOTION §9](../design/MOTION_CONTRACT.md#9-open-questions) | resolved in Phase 7; superseded 2026-09-17 — `mmdControl`, here | | PMX-O2 | QDEF verification | [PMX §16](../design/PMX_CONTRACT.md#16-open-questions) | a consumer | diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md index c8fc192..41fa231 100644 --- a/docs/roadmap/current.md +++ b/docs/roadmap/current.md @@ -1,7 +1,8 @@ # Phase 9, then Phase 8 — shared motion, avatar and physics composition Status: 🚧 Phase 9 in progress — `mmdControl`, both shared-motion adapters, -MOT-O5, MOT-O6 and MOT-O9 are done; Phase 8 not started. +skeletal end-to-end acceptance, MOT-O5, MOT-O6 and MOT-O9 are done; Phase 8 +not started. Two Phases remain, and they run in this order although they are numbered the other way ([DESIGN_POLICY.md §14](../design/DESIGN_POLICY.md#14-phases)): @@ -24,8 +25,9 @@ the runtime consumes from here today is [v0.1.0](../releases/v0.1.0.md). As of 2026-09-21 `usd-motion-plugins` v0.5.0 is published with installable `motionCore`, `motionRetarget` and `motionUsd` ([DEPENDENCIES.md §6](../architecture/DEPENDENCIES.md#6-usd-motion-plugins)). -The first two are consumed by digest-pinned artifacts, and -`mmdMotionAdapter` and `mmdSkeletonAdapter` are implemented. +All three are consumed by digest-pinned artifacts: `motionCore` and +`motionRetarget` by the adapters, and `motionUsd` by the skeletal acceptance +test. `mmdMotionAdapter` and `mmdSkeletonAdapter` are implemented. ## Outcome @@ -69,16 +71,22 @@ usd-avatar-runtime: composes the above per frame and coordinates rendering states, is opened with them. - ⬜ **MOT-O10**: whether `mmdSkeletonAdapter` states a `SourceRestPose` measured from the rest bone directions — MMD's arms rest in an A — decided - with the adapters' first retarget onto a non-MMD skeleton. + by a measured A-pose-to-level-arm retarget comparison. The first generic + skeletal acceptance used identity rest rotations and therefore could not + answer it. - ✅ **`mmdMotionAdapter` and `mmdSkeletonAdapter`** (2026-09-21): digest-pinned `motionCore` and `motionRetarget` v0.5.0 packages; role-table version 1; stage-token `SkeletonDescriptor`, source rest and target `RetargetMap`; evaluated world rotations normalized into `MotionClip`, root motion and namespaced morph channels; unit, boundary and installed-consumer tests. -- ⬜ **Acceptance end to end**: through `motionRetarget` and `motionUsd`, a - VMD-derived clip poses the PMX stage's +- ✅ **Acceptance end to end** (2026-09-22): through `motionRetarget` and + `motionUsd`, a VMD-derived clip poses the PMX stage's skeleton with legs driven by IK, and retargets to a non-MMD synthetic - skeleton with no MMD code on that path. + skeleton through a translation unit whose interface and implementation name + no MMD type. The deterministic test uses generated source and target rigs; + a separate local run retargeted a 1,201-sample dance onto a 539-joint PMX + stage, passed `usdchecker`, and was observed moving in `usdview` + ([report](../reports/2026-09-22-phase9-motion-acceptance.md)). - ⬜ **Expression interoperability** follows the skeletal adapter path. Keep every original `mmd:morph:` channel, then optionally emit only explicit, versioned, high-confidence semantic mappings such as blink and diff --git a/libs/mmdMotionAdapter/openstrata.library.yaml b/libs/mmdMotionAdapter/openstrata.library.yaml index f86dc8f..ff079d8 100644 --- a/libs/mmdMotionAdapter/openstrata.library.yaml +++ b/libs/mmdMotionAdapter/openstrata.library.yaml @@ -25,6 +25,21 @@ requires: cy2026-windows-x86_64-py313-usd: digest: sha256:f06fef790e19371c3d085c667e44805d738b63e8af77df2d8fafa6a41d5969a5 source: oci://ghcr.io/animu-sphere/usd-motion-plugins@sha256:7e5c67b9ae5defa40887b7dbaccea0bb5258ae3e86b1e2d14e7c82ae13ce0564 + # Test-only: the exported adapter target still links motionCore alone. + # Phase 9's acceptance test round-trips its MotionClip through motionUsd. + - id: motionUsd + version: ">=0.5,<0.6" + artifact: + targets: + cy2026-linux-x86_64-py313-usd: + digest: sha256:9cfc2a8985a5d30a20d61603f6fd773fdb767ba328079778b7890d32827445ed + source: oci://ghcr.io/animu-sphere/usd-motion-plugins@sha256:0b5d8c4a7bbfbc159a4b48fdece7c1549686ab03ab57a7ef5f5a7b6ece9562e4 + cy2026-macos-arm64-py313-usd: + digest: sha256:2261c7187e0f8c3dae2be077889a7b45d247d6f94df5ac665c214312d65dc0fd + source: oci://ghcr.io/animu-sphere/usd-motion-plugins@sha256:333a32c1b0adc67264cef18f4154eed4eb23cd409898f244c80e081ddd236ff5 + cy2026-windows-x86_64-py313-usd: + digest: sha256:f11545538fa3a72d9efa6dd108f155123af66b7ee1f52d227f0b345b2ceac5bc + source: oci://ghcr.io/animu-sphere/usd-motion-plugins@sha256:5a7c9982828aacc3b00435f1dd2c2dee9710bc12961ad8b59f64dadfd77d45ee cmake: package: mmdMotionAdapter target: mmdMotionAdapter::mmdMotionAdapter diff --git a/libs/mmdMotionAdapter/tests/CMakeLists.txt b/libs/mmdMotionAdapter/tests/CMakeLists.txt index fe431da..204d63e 100644 --- a/libs/mmdMotionAdapter/tests/CMakeLists.txt +++ b/libs/mmdMotionAdapter/tests/CMakeLists.txt @@ -7,6 +7,22 @@ target_compile_options(mmdMotionAdapter_tests PRIVATE $,/UNDEBUG,-UNDEBUG>) add_test(NAME mmdMotionAdapter_unit COMMAND mmdMotionAdapter_tests) +if(NOT TARGET motionUsd::motionUsd) + find_package(motionUsd 0.5 CONFIG REQUIRED) +endif() + +add_executable(mmdMotionAdapter_acceptance + test_acceptance.cpp + generic_retarget.cpp) +target_link_libraries(mmdMotionAdapter_acceptance PRIVATE + mmdMotionAdapter::mmdMotionAdapter + motionRetarget::motionRetarget + motionUsd::motionUsd) +usdmmd_target_defaults(mmdMotionAdapter_acceptance) +target_compile_options(mmdMotionAdapter_acceptance + PRIVATE $,/UNDEBUG,-UNDEBUG>) +add_test(NAME mmdMotionAdapter_acceptance COMMAND mmdMotionAdapter_acceptance) + file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/mmdMotionAdapter_link_$.txt" CONTENT "LINK_LIBRARIES=$ diff --git a/libs/mmdMotionAdapter/tests/generic_retarget.cpp b/libs/mmdMotionAdapter/tests/generic_retarget.cpp new file mode 100644 index 0000000..63357b8 --- /dev/null +++ b/libs/mmdMotionAdapter/tests/generic_retarget.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "generic_retarget.h" + +openstrata::motion::RetargetedAnimation +RetargetGeneric(const openstrata::motion::MotionClip& clip, + const openstrata::motion::SkeletonDescriptor& skeleton, + const openstrata::motion::RetargetMap& map, + const openstrata::motion::SourceRestPose& sourceRest, + const std::vector& required, + openstrata::motion::RetargetDiagnostics* diagnostics) +{ + openstrata::motion::RetargetOptions options; + options.requiredBones = required; + return openstrata::motion::PoseRetargeter(skeleton, map, sourceRest, options) + .Retarget(clip, diagnostics); +} diff --git a/libs/mmdMotionAdapter/tests/generic_retarget.h b/libs/mmdMotionAdapter/tests/generic_retarget.h new file mode 100644 index 0000000..ef6d693 --- /dev/null +++ b/libs/mmdMotionAdapter/tests/generic_retarget.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Deliberately generic side of the Phase 9 acceptance path. This translation +// unit's interface names only shared-motion values: no MMD type crosses it. +#pragma once + +#include + +#include + +openstrata::motion::RetargetedAnimation +RetargetGeneric(const openstrata::motion::MotionClip& clip, + const openstrata::motion::SkeletonDescriptor& skeleton, + const openstrata::motion::RetargetMap& map, + const openstrata::motion::SourceRestPose& sourceRest, + const std::vector& required, + openstrata::motion::RetargetDiagnostics* diagnostics); diff --git a/libs/mmdMotionAdapter/tests/test_acceptance.cpp b/libs/mmdMotionAdapter/tests/test_acceptance.cpp new file mode 100644 index 0000000..cce3770 --- /dev/null +++ b/libs/mmdMotionAdapter/tests/test_acceptance.cpp @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Phase 9 acceptance, entirely synthetic and deterministic: +// VMD records -> binding -> MMD control evaluation (including leg IK) -> +// MotionClip -> motionUsd round trip -> motionRetarget -> a PMX-derived stage +// skeleton and a separately named, non-MMD skeleton. + +#include "generic_retarget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +namespace pmx = mmd::pmx; +namespace vmd = motionVmd; +using J = openstrata::motion::HumanJoint; + +pmx::Bone +Bone(std::string name, pmx::Vec3 position, std::int32_t parent, std::uint16_t flags = 0) +{ + pmx::Bone bone; + bone.name = std::move(name); + bone.position = position; + bone.parent = parent; + bone.flags = flags; + return bone; +} + +pmx::Document +Humanoid(bool withIk, float scale) +{ + const auto p = [scale](float x, float y, float z = 0.0f) { + return pmx::Vec3{x * scale, y * scale, z * scale}; + }; + pmx::Document doc; + doc.model.name = withIk ? "synthetic VMD source" : "synthetic PMX target"; + doc.bones = { + Bone("全ての親", p(0, 0), pmx::kNoIndex), // 0 + Bone("腰", p(0, 0), 0), // 1 + Bone("下半身", p(0, 12.5f), 1), // 2 + Bone("上半身", p(0, 15.0f), 1), // 3 + Bone("首", p(0, 20.0f), 3), // 4 + Bone("頭", p(0, 22.5f), 4), // 5 + Bone("左足", p(2.0f, 12.5f), 2), // 6 + Bone("左ひざ", p(2.0f, 6.25f), 6), // 7 + Bone("左足首", p(2.0f, 0), 7), // 8 + Bone("右足", p(-2.0f, 12.5f), 2), // 9 + Bone("右ひざ", p(-2.0f, 6.25f), 9), // 10 + Bone("右足首", p(-2.0f, 0), 10), // 11 + Bone("左腕", p(3.0f, 18.0f), 3), // 12 + Bone("左ひじ", p(6.0f, 18.0f), 12), // 13 + Bone("左手首", p(9.0f, 18.0f), 13), // 14 + Bone("右腕", p(-3.0f, 18.0f), 3), // 15 + Bone("右ひじ", p(-6.0f, 18.0f), 15), // 16 + Bone("右手首", p(-9.0f, 18.0f), 16), // 17 + }; + if (withIk) { + doc.bones.push_back(Bone("左足IK", p(2.0f, 0), 0, pmx::BoneFlag::Ik)); + pmx::Ik& ik = doc.bones.back().ik; + ik.target = 8; + ik.loopCount = 40; + ik.limitAngle = 2.0f; + ik.links = { + pmx::IkLink{7, + true, + {static_cast(-std::numbers::pi), 0.0f, 0.0f}, + {-0.00872665f, 0.0f, 0.0f}}, + pmx::IkLink{6, false, {}, {}}, + }; + } + return doc; +} + +mmd::CanonicalDocument +Canonical(const pmx::Document& document) +{ + auto result = mmd::Canonicalize(document); + assert(result); + return std::move(result).value(); +} + +vmd::Name +Name(const std::string& utf8, std::size_t width) +{ + std::string bytes = vmd::EncodeCp932(utf8).value(); + bytes.resize(std::min(bytes.size(), width)); + const auto decoded = vmd::DecodeCp932( + std::span(reinterpret_cast(bytes.data()), bytes.size())); + return {bytes, decoded.text}; +} + +vmd::Document +IkMotion() +{ + vmd::Document document; + document.header.modelName = Name("synthetic source", 20); + vmd::BoneKeyframe goal; + goal.bone = Name("左足IK", 15); + // Relative to the IK bone: +0.2 m Y and +0.05 m Z after basis conversion. + goal.translation = {0.0f, 2.5f, -0.625f}; + goal.interpolation.fill(20); + document.boneKeyframes.push_back(goal); + + vmd::IkKeyframe enabled; + enabled.visible = 1; + enabled.ik.push_back({Name("左足IK", 20), 1}); + document.ikKeyframes.push_back(enabled); + document.sectionsPresent = vmd::kSectionCount; + return document; +} + +bool +IsIdentity(const pxr::GfQuatf& rotation) +{ + return std::fabs(pxr::GfDot(rotation.GetNormalized(), pxr::GfQuatf(1.0f))) > 1.0f - 1.0e-5f; +} + +openstrata::motion::MotionClip +RoundTrip(const openstrata::motion::MotionClip& clip, + const mmd::skeleton::AdaptedSkeleton& source) +{ + openstrata::motion::MotionStageRest rest; + rest.localRotations = source.sourceRest.localRotations; + rest.localTranslations = source.sourceRest.localTranslations; + rest.present = source.sourcePresent; + + openstrata::motion::MotionStageOptions options; + options.sourceFormat = "vmd"; + options.rootMotionSource = "mapped hips world transform"; + options.rest = rest; + const pxr::UsdStageRefPtr stage = pxr::UsdStage::CreateInMemory(); + openstrata::motion::MotionStageReport report; + std::string error; + assert(openstrata::motion::AuthorMotionStage(stage, clip, options, &report, &error)); + assert(report.sampleCount == clip.samples.size()); + + openstrata::motion::MotionStageRead read; + assert(openstrata::motion::ReadMotionStage(stage, {}, &read, &error)); + assert(read.warnings.empty()); + assert(read.metadata.sourceFormat == "vmd"); + assert(read.clip.samples.size() == clip.samples.size()); + return std::move(read.clip); +} + +pxr::UsdStageRefPtr +PmxSkeletonStage(const mmd::skeleton::AdaptedSkeleton& target) +{ + const pxr::UsdStageRefPtr stage = pxr::UsdStage::CreateInMemory(); + pxr::UsdSkelRoot::Define(stage, pxr::SdfPath("/Asset")); + const pxr::UsdSkelSkeleton skeleton = + pxr::UsdSkelSkeleton::Define(stage, pxr::SdfPath("/Asset/skel/Skeleton")); + pxr::VtTokenArray tokens; + pxr::VtMatrix4dArray rests; + for (const auto& joint : target.skeleton.GetJoints()) { + tokens.push_back(pxr::TfToken(joint.token)); + pxr::GfMatrix4d matrix(1.0); + matrix.SetTranslateOnly(pxr::GfVec3d(joint.restTranslation)); + rests.push_back(matrix); + } + skeleton.CreateJointsAttr().Set(tokens); + skeleton.CreateRestTransformsAttr().Set(rests); + return stage; +} + +openstrata::motion::SkeletonDescriptor +ReadTargetSkeleton(const pxr::UsdStageRefPtr& stage) +{ + const pxr::UsdSkelSkeleton skeleton( + stage->GetPrimAtPath(pxr::SdfPath("/Asset/skel/Skeleton"))); + pxr::VtTokenArray tokens; + pxr::VtMatrix4dArray rests; + assert(skeleton.GetJointsAttr().Get(&tokens)); + assert(skeleton.GetRestTransformsAttr().Get(&rests)); + std::vector names; + names.reserve(tokens.size()); + for (const auto& token : tokens) { + names.push_back(token.GetString()); + } + const auto built = openstrata::motion::BuildSkeletonDescriptor( + names, std::vector(rests.begin(), rests.end())); + assert(built.skeleton); + return *built.skeleton; +} + +void +BindAnimation(const pxr::UsdStageRefPtr& stage, + const openstrata::motion::SkeletonDescriptor& skeleton, + const openstrata::motion::RetargetedAnimation& animation) +{ + const pxr::SdfPath path("/Asset/skel/RetargetedAnimation"); + const pxr::UsdSkelAnimation authored = pxr::UsdSkelAnimation::Define(stage, path); + pxr::VtTokenArray joints; + for (const std::string& joint : animation.joints) { + joints.push_back(pxr::TfToken(joint)); + } + authored.CreateJointsAttr().Set(joints); + pxr::VtVec3hArray scales; + for (const auto& joint : skeleton.GetJoints()) { + scales.push_back(pxr::GfVec3h(joint.restScale)); + } + authored.CreateScalesAttr().Set(scales); + for (const auto& sample : animation.samples) { + authored.CreateRotationsAttr().Set( + pxr::VtQuatfArray(sample.rotations.begin(), sample.rotations.end()), + pxr::UsdTimeCode(sample.timestamp * 30.0)); + authored.CreateTranslationsAttr().Set( + pxr::VtVec3fArray(sample.translations.begin(), sample.translations.end()), + pxr::UsdTimeCode(sample.timestamp * 30.0)); + } + const pxr::UsdSkelSkeleton target( + stage->GetPrimAtPath(pxr::SdfPath("/Asset/skel/Skeleton"))); + const pxr::UsdSkelBindingAPI binding = pxr::UsdSkelBindingAPI::Apply(target.GetPrim()); + assert(binding.CreateAnimationSourceRel().SetTargets({path})); +} + +struct GenericRig { + openstrata::motion::SkeletonDescriptor skeleton; + openstrata::motion::RetargetMap map; + std::vector required; +}; + +GenericRig +MakeGenericRig() +{ + GenericRig rig; + const auto add = [&](J role, std::string token, pxr::GfVec3f rest) { + openstrata::motion::SkeletonJoint joint; + joint.token = std::move(token); + joint.restTranslation = rest; + rig.skeleton.AddJoint(joint); + rig.required.push_back(role); + }; + add(J::Hips, "World/Pelvis", {0.0f, 1.1f, 0.0f}); + add(J::Spine, "World/Pelvis/Torso", {0.0f, 0.35f, 0.0f}); + add(J::Neck, "World/Pelvis/Torso/NeckBase", {0.0f, 0.35f, 0.0f}); + add(J::Head, "World/Pelvis/Torso/NeckBase/Cranium", {0.0f, 0.2f, 0.0f}); + add(J::LeftUpperLeg, "World/Pelvis/Leg_L", {0.14f, -0.08f, 0.0f}); + add(J::LeftLowerLeg, "World/Pelvis/Leg_L/Shin_L", {0.0f, -0.55f, 0.0f}); + add(J::LeftFoot, "World/Pelvis/Leg_L/Shin_L/Foot_L", {0.0f, -0.5f, 0.0f}); + add(J::RightUpperLeg, "World/Pelvis/Leg_R", {-0.14f, -0.08f, 0.0f}); + add(J::RightLowerLeg, "World/Pelvis/Leg_R/Shin_R", {0.0f, -0.55f, 0.0f}); + add(J::RightFoot, "World/Pelvis/Leg_R/Shin_R/Foot_R", {0.0f, -0.5f, 0.0f}); + add(J::LeftUpperArm, "World/Pelvis/Torso/Arm_L", {0.25f, 0.3f, 0.0f}); + add(J::LeftLowerArm, "World/Pelvis/Torso/Arm_L/Forearm_L", {0.35f, 0.0f, 0.0f}); + add(J::LeftHand, "World/Pelvis/Torso/Arm_L/Forearm_L/Palm_L", {0.3f, 0.0f, 0.0f}); + add(J::RightUpperArm, "World/Pelvis/Torso/Arm_R", {-0.25f, 0.3f, 0.0f}); + add(J::RightLowerArm, "World/Pelvis/Torso/Arm_R/Forearm_R", {-0.35f, 0.0f, 0.0f}); + add(J::RightHand, "World/Pelvis/Torso/Arm_R/Forearm_R/Palm_R", {-0.3f, 0.0f, 0.0f}); + rig.skeleton.ResolveParentsFromTokens(); + for (std::size_t i = 0; i < rig.required.size(); ++i) { + assert(rig.map.SetJointIndex(rig.required[i], static_cast(i), rig.skeleton.GetSize())); + } + return rig; +} + +void +TestEndToEnd() +{ + const mmd::CanonicalDocument sourceModel = Canonical(Humanoid(true, 1.0f)); + const auto motion = vmd::BuildMotion(IkMotion()); + assert(motion); + const auto bound = mmd::binding::Bind(motion.value(), sourceModel); + assert(bound && bound.value().bones.size() == 1 && bound.value().ik.size() == 1); + const auto evaluator = mmd::control::Evaluator::Prepare(sourceModel); + assert(evaluator); + const mmd::skeleton::AdaptedSkeleton source = mmd::skeleton::Adapt(sourceModel); + const auto built = mmd::motion::BuildClip( + sourceModel, bound.value(), evaluator.value(), source, + mmd::motion::ClipOptions{0.0, 1.0 / 30.0, 30.0}); + assert(built && built.diagnostics().empty()); + assert(built.value().samples.size() == 2); + + const std::size_t leftLeg = static_cast(J::LeftUpperLeg); + const std::size_t leftKnee = static_cast(J::LeftLowerLeg); + assert(built.value().samples[0].validRotations.test(leftLeg)); + assert(built.value().samples[0].validRotations.test(leftKnee)); + assert(!IsIdentity(built.value().samples[0].localRotations[leftLeg])); + assert(!IsIdentity(built.value().samples[0].localRotations[leftKnee])); + + const openstrata::motion::MotionClip clip = RoundTrip(built.value(), source); + + const mmd::CanonicalDocument targetModel = Canonical(Humanoid(false, 1.2f)); + const mmd::skeleton::AdaptedSkeleton target = mmd::skeleton::Adapt(targetModel); + const pxr::UsdStageRefPtr targetStage = PmxSkeletonStage(target); + const openstrata::motion::SkeletonDescriptor stageSkeleton = ReadTargetSkeleton(targetStage); + assert(stageSkeleton == target.skeleton); + + openstrata::motion::RetargetDiagnostics pmxDiagnostics; + const openstrata::motion::RetargetedAnimation pmxAnimation = RetargetGeneric( + clip, stageSkeleton, target.targetMap, source.sourceRest, + target.requiredJoints, &pmxDiagnostics); + assert(pmxDiagnostics.IsClean()); + assert(pmxAnimation.samples.size() == clip.samples.size()); + assert(pmxAnimation.joints.size() == stageSkeleton.GetSize()); + const int pmxLeg = target.targetMap.GetJointIndex(J::LeftUpperLeg); + const int pmxKnee = target.targetMap.GetJointIndex(J::LeftLowerLeg); + assert(pmxLeg >= 0 && pmxKnee >= 0); + assert(!IsIdentity(pmxAnimation.samples[0].rotations[static_cast(pmxLeg)])); + assert(!IsIdentity(pmxAnimation.samples[0].rotations[static_cast(pmxKnee)])); + BindAnimation(targetStage, stageSkeleton, pmxAnimation); + std::vector targets; + const pxr::UsdSkelBindingAPI binding( + targetStage->GetPrimAtPath(pxr::SdfPath("/Asset/skel/Skeleton"))); + assert(binding.GetAnimationSourceRel().GetTargets(&targets)); + assert(targets == std::vector{pxr::SdfPath("/Asset/skel/RetargetedAnimation")}); + + const GenericRig generic = MakeGenericRig(); + openstrata::motion::RetargetDiagnostics genericDiagnostics; + const openstrata::motion::RetargetedAnimation genericAnimation = RetargetGeneric( + clip, generic.skeleton, generic.map, source.sourceRest, + generic.required, &genericDiagnostics); + assert(genericDiagnostics.IsClean()); + assert(genericAnimation.samples.size() == clip.samples.size()); + assert(genericAnimation.joints.front() == "World/Pelvis"); + const int genericLeg = generic.map.GetJointIndex(J::LeftUpperLeg); + assert(!IsIdentity( + genericAnimation.samples[0].rotations[static_cast(genericLeg)])); + + std::puts("ok VMD + MMD IK -> motionUsd -> PMX and generic retarget targets"); +} + +} // namespace + +int +main() +{ + TestEndToEnd(); + return 0; +} diff --git a/libs/mmdSkeletonAdapter/openstrata.library.yaml b/libs/mmdSkeletonAdapter/openstrata.library.yaml index 0c4f3a6..5a16709 100644 --- a/libs/mmdSkeletonAdapter/openstrata.library.yaml +++ b/libs/mmdSkeletonAdapter/openstrata.library.yaml @@ -8,6 +8,21 @@ requires: libraries: - id: mmdModel version: ">=0.1,<0.2" + # motionRetarget's installed package requires motionCore; pin the complete + # external artifact closure so this library also builds standalone. + - id: motionCore + version: ">=0.5,<0.6" + artifact: + targets: + cy2026-linux-x86_64-py313-usd: + digest: sha256:2aa549d1d764ef99527f31b71533133ac1c98e48843c63feccf6c69cc551219d + source: oci://ghcr.io/animu-sphere/usd-motion-plugins@sha256:32b6ba1bc4b155278fbc388e24a0e9130f19540be9a980cb48bf049637bfe9e7 + cy2026-macos-arm64-py313-usd: + digest: sha256:0a2c796e1929abcca6b4bba75509b8139a1fd757cb1c4fe6f9a165eee55e8d49 + source: oci://ghcr.io/animu-sphere/usd-motion-plugins@sha256:54f62ff034b02babe2470a78d78aa18c44b4c2245d09d53ec7d7af0ccc6888e0 + cy2026-windows-x86_64-py313-usd: + digest: sha256:f06fef790e19371c3d085c667e44805d738b63e8af77df2d8fafa6a41d5969a5 + source: oci://ghcr.io/animu-sphere/usd-motion-plugins@sha256:7e5c67b9ae5defa40887b7dbaccea0bb5258ae3e86b1e2d14e7c82ae13ce0564 - id: motionRetarget version: ">=0.5,<0.6" artifact: