From 6425e01421bacaa68fcfad57703b6db4f4a3edc7 Mon Sep 17 00:00:00 2001 From: snkmcb Date: Sun, 27 Sep 2026 22:07:36 +0900 Subject: [PATCH] feat: add optional standard USD box declaration package --- CMakeLists.txt | 10 + README.md | 11 +- docs/architecture/DEPENDENCIES.md | 2 +- docs/architecture/PACKAGE_CONTRACT.md | 10 +- docs/architecture/WORKSPACE.md | 2 +- docs/design/USD_BRIDGE_CONTRACT.md | 27 ++- docs/reference/CAPABILITY_MATRIX.md | 28 ++- docs/roadmap/README.md | 6 +- docs/roadmap/current.md | 108 +++------- libs/physicsUsd/CMakeLists.txt | 56 ++++++ .../cmake/physicsUsdConfig.cmake.in | 5 + .../include/usd_physics/usd/box_scene.h | 45 +++++ libs/physicsUsd/openstrata.library.yaml | 21 ++ libs/physicsUsd/src/box_scene.cpp | 187 ++++++++++++++++++ libs/physicsUsd/tests/CMakeLists.txt | 7 + libs/physicsUsd/tests/box_scene_test.cpp | 124 ++++++++++++ openstrata.toml | 6 + tests/CMakeLists.txt | 2 + tests/check_boundaries.cmake | 10 +- tests/check_versions.cmake | 1 + tests/installed_consumer/CMakeLists.txt | 10 + tests/installed_consumer/usd_main.cpp | 16 ++ tests/run_installed_consumer.cmake | 7 +- 23 files changed, 604 insertions(+), 97 deletions(-) create mode 100644 libs/physicsUsd/CMakeLists.txt create mode 100644 libs/physicsUsd/cmake/physicsUsdConfig.cmake.in create mode 100644 libs/physicsUsd/include/usd_physics/usd/box_scene.h create mode 100644 libs/physicsUsd/openstrata.library.yaml create mode 100644 libs/physicsUsd/src/box_scene.cpp create mode 100644 libs/physicsUsd/tests/CMakeLists.txt create mode 100644 libs/physicsUsd/tests/box_scene_test.cpp create mode 100644 tests/installed_consumer/usd_main.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b4d8359..414f525 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,8 +27,18 @@ endif() add_subdirectory(libs/physicsCore) add_subdirectory(backends/physicsJolt) +option(USDPHYSICS_BUILD_USD "Build the optional standard OpenUSD declaration adapter" OFF) +if(USDPHYSICS_BUILD_USD) + add_subdirectory(libs/physicsUsd) +endif() include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/UsdPhysicsBoundaryChecks.cmake") +if(TARGET physicsUsd) + usdphysics_assert_target_dependencies( + TARGET physicsUsd + ALLOWED_PUBLIC physicsCore::physicsCore usd usdGeom usdPhysics pxr::usd pxr::usdGeom pxr::usdPhysics + ALLOWED_PRIVATE physicsCore::physicsCore usd usdGeom usdPhysics pxr::usd pxr::usdGeom pxr::usdPhysics) +endif() usdphysics_assert_target_dependencies( TARGET physicsCore ALLOWED_PUBLIC) diff --git a/README.md b/README.md index 7587a4e..44b3cc5 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ Backend-neutral physics runtime components for OpenUSD-based applications. -> **Status (2026-09-23): Phase 2 backend extraction in progress.** The +> **Status (2026-09-27): core/backend extraction complete; Box USD foundation in progress.** The > repository builds and installs `physicsCore` and `physicsJolt`; the neutral > core contract and a Jolt-backed world with fixed stepping, changed state, > segment queries, and ground queries are implemented. The Jolt-required > OpenStrata intent passes -> locally; hosted Phase 2 evidence and an OpenUSD bridge remain. The +> locally and in hosted CI. An optional `physicsUsd` package now reads standard +> Box declarations; wider bridge support and artifact rollout remain. The > [capability matrix](docs/reference/CAPABILITY_MATRIX.md) is the only page > that states what exists, and the [current roadmap](docs/roadmap/current.md) > states what comes next. @@ -20,6 +21,12 @@ validate it with ## The central rule +The optional Box adapter builds with `-DUSDPHYSICS_BUILD_USD=ON` and an OpenUSD +26.08 install on `CMAKE_PREFIX_PATH`. It installs `physicsUsd::physicsUsd` and +`usd_physics/usd/box_scene.h`. The core/backend-only root build remains the +default. See the [bounded Box contract](docs/design/USD_BRIDGE_CONTRACT.md) +for supported declarations and explicit rejection behavior. + > **OpenUSD describes the physical world; `usd-physics-plugins` makes that > world executable without exposing a backend SDK to its consumers.** diff --git a/docs/architecture/DEPENDENCIES.md b/docs/architecture/DEPENDENCIES.md index 0a836a3..4a2e408 100644 --- a/docs/architecture/DEPENDENCIES.md +++ b/docs/architecture/DEPENDENCIES.md @@ -15,7 +15,7 @@ claims. | C++ | C++17 | all C++ targets | verified with GCC 15.2.0 and MSVC 19.51 | | OpenStrata | 0.23.2 | workspace composition and CI generation | Windows root build/test, isolated library tests, consumer verification, and packaging pass | | OpenStrata platform | `cy2026`, `usd` profile | workspace composition | digest-pinned Windows runtime materialized and validated; scaffold code does not link OpenUSD | -| OpenUSD | 26.08 exact for the first ecosystem integration | future `physicsUsd`, `physicsSchema`, USD tests | aligned and CI runtime pinned; no target currently links it | +| OpenUSD | 26.08 exact for the first ecosystem integration | `physicsUsd` and USD tests; future `physicsSchema` | Box adapter and clean-prefix consumer verified locally on Windows; hosted bridge evidence remains open | | Jolt Physics | 5.5.0, tag `v5.5.0`, commit `23dadd0e603f1b321142d4c74df07fce85064989` | private `physicsJolt` implementation | Jolt-backed plain-CMake tests pass locally on Windows and Linux and the `jolt` OpenStrata intent passes on Windows; hosted Phase 2 evidence remains open | | CTest | version shipped with CMake | tests | 11 Jolt-enabled root tests pass on Windows and Linux and through OpenStrata on Windows | diff --git a/docs/architecture/PACKAGE_CONTRACT.md b/docs/architecture/PACKAGE_CONTRACT.md index e42f238..705c3ed 100644 --- a/docs/architecture/PACKAGE_CONTRACT.md +++ b/docs/architecture/PACKAGE_CONTRACT.md @@ -19,7 +19,7 @@ not need this repository's source tree. | --- | --- | --- | --- | | `physicsCore` | `physicsCore::physicsCore` | Version surface plus neutral vector, quaternion, transform, typed handles, validated box/body/fixed-constraint descriptors, semantic collision filters, typed errors, world lifecycle/state, and optional segment and ground query interfaces | Phase 1 surface present | | `physicsJolt` | `physicsJolt::physicsJolt` | `physicsJolt_BACKEND_AVAILABLE` package metadata, `backendAvailable()`, and `createWorld()`; a Jolt-enabled build provides shapes, bodies, fixed constraints, commands, stepping, changed state, segment queries, and ground queries, while a no-Jolt build reports typed unavailability | Phase 2 complete | -| `physicsUsd` | `physicsUsd::physicsUsd` | USD translation, mappings, and synchronization records | 4 | +| `physicsUsd` | `physicsUsd::physicsUsd` | Validated standard Box snapshots; mappings and synchronization records remain planned | Phase 4 subset implemented | | `secondaryMotion` | `secondaryMotion::secondaryMotion` | generic secondary-motion contracts | 6, if admitted | | `secondaryMotionVerlet` | `secondaryMotionVerlet::secondaryMotionVerlet` | first CPU solver | 6, if admitted | | `physicsSchema` | bundle contract to be defined at admission | generated schema library and resources | only if admitted | @@ -29,6 +29,14 @@ The public include root and namespace are fixed by ## 3. Install-interface rules +The optional `physicsUsd` target builds with `USDPHYSICS_BUILD_USD=ON` in the +root or as a standalone `libs/physicsUsd` project. It exports public headers +under `usd_physics/usd/`, links only `physicsCore` and OpenUSD `usd`, `usdGeom`, +and `usdPhysics` targets (also accepting `pxr::` names), and resolves those +dependencies from its installed config. The clean-prefix consumer includes a +standard Cube import when this option is enabled. Its OpenStrata member is +declared but has not entered the published release/artifact set. + Every library package must: - install public headers under one repository-owned include root; diff --git a/docs/architecture/WORKSPACE.md b/docs/architecture/WORKSPACE.md index 820f364..09e3bfc 100644 --- a/docs/architecture/WORKSPACE.md +++ b/docs/architecture/WORKSPACE.md @@ -18,7 +18,7 @@ that contradicts it updates this document first, in a focused change. | --- | --- | --- | --- | --- | --- | --- | | `physicsCore` | plain static CMake library | `libs/physicsCore/` | `openstrata.library.yaml` | Solver-neutral value types, handles, descriptors, world lifecycle, state, validation, and optional query contracts. No OpenUSD or backend SDK. | Phase 0 scaffold; Phase 1 runtime | Phase 1 values, handles, descriptors, world contract, and segment/ground queries implemented | | `physicsJolt` | plain static CMake library | `backends/physicsJolt/` | `openstrata.library.yaml` | Jolt implementation of `physicsCore`; owns Jolt initialization, filters, jobs, resources, stepping, and queries. | Phase 0 scaffold; Phase 2 backend | backend implemented; Windows and Linux plain-CMake and installed-consumer evidence present | -| `physicsUsd` | plain static CMake library | `libs/physicsUsd/` | `openstrata.library.yaml` | Standard `UsdPhysics` to core descriptors, transform conversion, transient scene/resource mapping, and synchronization records. | Phase 4 | reserved | +| `physicsUsd` | plain static CMake library | `libs/physicsUsd/` | `openstrata.library.yaml` | Standard Box declarations to neutral snapshots; resource mapping and synchronization records remain planned. | Phase 4 | optional installable Box reader implemented | The Phase 0 targets established package and dependency boundaries. `physicsCore` has supported value, handle, descriptor, and world-contract diff --git a/docs/design/USD_BRIDGE_CONTRACT.md b/docs/design/USD_BRIDGE_CONTRACT.md index 6296f32..6ce44cc 100644 --- a/docs/design/USD_BRIDGE_CONTRACT.md +++ b/docs/design/USD_BRIDGE_CONTRACT.md @@ -1,7 +1,30 @@ # OpenUSD physics bridge contract -> **Status: proposed, 2026-09-21.** `physicsUsd` is planned for Phase 4 and is -> not present. See the [capability matrix](../reference/CAPABILITY_MATRIX.md). +> **Status: proposed, updated 2026-09-27.** A bounded Box snapshot reader is +> implemented; the wider bridge remains planned. See the +> [capability matrix](../reference/CAPABILITY_MATRIX.md). + +The initial implementation exposes `usd_physics::usd::readBoxScene()` and +`BoxBody` in `usd_physics/usd/box_scene.h`. It returns validated neutral shape, +motion, transform, and mass values sorted by absolute prim path. It owns no +world, handles, persistent mapping, or USD edits. Callers must keep the result +associated with its source Stage and discard it at reload; stage-generation +identities and synchronization records remain future work. + +The Box subset requires collision and body on the same Cube, Y-up meters/kg, +explicit positive mass for dynamic bodies, one translate op then scale ops, +and identity ancestors or resetXformStack. Static collision-only Cubes and +disabled rigid bodies are static; disabled collision-only Cubes are skipped. +A rigid body with disabled collision is rejected until collider-free bodies +are supported. Kinematic bodies, animated declarations, scenes, joints, +instances, nested bodies, inherited/density-based mass, materials, and all +other authored `physics:*` properties are rejected. No default mass is +substituted for unsupported USD mass inference. + +For this surface, `USD-O2` uses typed `ImportErrorCode` plus the offending path +on `ImportError`: `invalidStage`, `unsupportedUnits`, `unsupportedDeclaration`, +`unsupportedTransform`, and `invalidValue`. Consumers assert code/path, not +diagnostic prose. Validation completes before a snapshot is returned. ## 1. Purpose diff --git a/docs/reference/CAPABILITY_MATRIX.md b/docs/reference/CAPABILITY_MATRIX.md index 1a37837..f776586 100644 --- a/docs/reference/CAPABILITY_MATRIX.md +++ b/docs/reference/CAPABILITY_MATRIX.md @@ -4,7 +4,7 @@ This page is the only source of truth for what the current repository tree implements. Design documents describe intended behavior; they do not upgrade a capability on this page. -> **Tree status (2026-09-23): Phase 2 complete; Phase 3 migration in progress.** +> **Tree status (2026-09-27): Phase 2 complete; Phase 3 release follow-up and Phase 4 Box foundation in progress.** > Buildable and installable `physicsCore` and `physicsJolt` package boundaries > exist. > `physicsCore` provides neutral rigid-transform values, typed handles, and @@ -13,8 +13,9 @@ capability on this page. > query interfaces. `physicsJolt` implements those contracts when a compatible > Jolt package is present and retains a typed unavailable fallback otherwise. > The Jolt-required OpenStrata intent passes locally and in hosted Windows and -> Linux CI. Stage Runner has not migrated to the installed packages, and an -> OpenUSD plugin does not exist yet. +> Linux CI. Stage Runner has migrated to installed core/backend packages. An +> optional installable `physicsUsd` Box reader now exists; an OpenUSD plugin +> does not. Bridge artifact publication and hosted parity remain open. ## 1. Status vocabulary @@ -73,11 +74,12 @@ capability on this page. | Capability | Status | Evidence / note | | --- | --- | --- | -| `physicsUsd` package | not present | Planned Phase 4 | +| `physicsUsd` package | partial | Optional installed `physicsUsd::physicsUsd`; `physicsUsd.box_scene` and clean-prefix USD consumer test | | `UsdPhysicsScene` gravity | planned | No implementation | -| Box/sphere/capsule collision | planned | No implementation | -| `UsdPhysicsRigidBodyAPI` | planned | No implementation | -| `UsdPhysicsMassAPI` | planned | No implementation | +| Box collision | partial | Cube size and ordered scales; `physicsUsd.box_scene` covers conversion, disabled static colliders, and invalid dimensions | +| Sphere/capsule collision | planned | No implementation | +| `UsdPhysicsRigidBodyAPI` | partial | Enabled dynamic or disabled static body on the collision Cube; kinematic, collider-free, nested, and compound bodies rejected | +| `UsdPhysicsMassAPI` | partial | Explicit positive dynamic mass in kg; density inference, inherited mass, inertia and center-of-mass overrides rejected | | Fixed joints | planned | No implementation | | Other standard joints, limits, and drives | planned | Phase 5 expands from consumer fixtures | | Prim/resource mappings | planned | No implementation | @@ -85,6 +87,14 @@ capability on this page. | Incremental USD change processing | unsupported | Rebuild-first policy; no implementation | | USD state synchronization records | planned | `USD-O3` unresolved | +The [Box contract](../design/USD_BRIDGE_CONTRACT.md) documents the bounded +subset. It requires Y-up meters/kg, translate then scale, and identity +ancestors or resetXformStack. Animated values, instances, other standard +physics APIs/properties, scenes, and joints fail with typed code/path diagnostics. +Tests live in [box_scene_test.cpp](../../libs/physicsUsd/tests/box_scene_test.cpp). +The parser never edits USD or creates resources; returned paths are snapshot +identities belonging to the caller's Stage, not persistent runtime mappings. + ## 6. Secondary motion | Capability | Status | Evidence / note | @@ -106,8 +116,8 @@ capability on this page. | Scenario | Status | Evidence / note | | --- | --- | --- | -| Stage Runner falling body | planned | Phase 3 | -| Stage Runner character grounding and camera collision | planned | Phase 3 | +| Stage Runner falling body | supported | Installed core/backend migration verified in the [Phase 3 artifact report](../reports/2026-09-23-phase3-linux-artifacts.md); standard Box consumer remains opt-in | +| Stage Runner character grounding and camera collision | supported | Same installed-package consumer evidence; standard/compatibility parity is covered in Stage Runner's optional `stage_runtime.standard_physics_parity` test | | MMD rigid bodies and joints through standard USD | planned | Phase 5 | | VRM-derived secondary motion | planned | Phase 7 | | Vehicle primitives | planned | Phase 8 | diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md index 2a4d284..ef863d8 100644 --- a/docs/roadmap/README.md +++ b/docs/roadmap/README.md @@ -9,8 +9,8 @@ status and ordering. | Phase | Status | Current outcome | | --- | --- | --- | -| 3 — Stage Runner migration | in progress | Consume the installed Phase 1–2 packages without changing Stage Runner orchestration or compatibility behavior. | -| 4 — `physicsUsd` foundation | not started | Waits for the neutral rigid-body contract. | +| 3 — Stage Runner migration | in progress | Consumer migration verified; versioned release workflow remains. | +| 4 — `physicsUsd` foundation | in progress | Box snapshot parser exists; artifact rollout and wider declarations remain. | | 5 — MMD validation | not started | Waits for the USD bridge and a generated vertical-slice fixture. | | 6 — secondary-motion core | not started | Deferred until rigid-body extraction is stable and a VRM slice validates the descriptor. | | 7 — VRM validation | not started | Waits for Phase 6 and a format-owned adapter. | @@ -21,7 +21,7 @@ version plan exists; release numbers do not belong in the phase definitions. ## Current plan -[current.md](current.md) contains the ordered Phase 3 migration work. +[current.md](current.md) contains release follow-up and the next Phase 4 slice. Completed items leave that page and are reflected in the [capability matrix](../reference/CAPABILITY_MATRIX.md). diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md index 951687a..ed2d402 100644 --- a/docs/roadmap/current.md +++ b/docs/roadmap/current.md @@ -1,76 +1,32 @@ -# Current roadmap — Phase 3 Stage Runner migration - -> **Status: in progress, 2026-09-23.** Phase 2 is complete: the installed -> `physicsCore` and `physicsJolt` packages pass local and hosted Windows/Linux -> verification. The next slice migrates Stage Runner to those packages while -> preserving its orchestration and authored compatibility behavior. - -## 1. Outcome - -Phase 3 makes `usd-stage-runner` a consumer of the installed packages instead -of an owner of the neutral physics and Jolt backend implementations. - -```text -installed physicsCore + physicsJolt - -> Stage Runner composition boundary - -> fixed-step orchestration and gameplay capabilities - -> incremental runtime-layer synchronization -``` - -## 2. Available prerequisites - -- Phase 1 provides the installed neutral values, handles, descriptors, errors, - world contract, and optional segment and ground query capabilities. -- Phase 2 provides the installed Jolt-backed world factory with passing local - and hosted Windows/Linux evidence. -- The Stage Runner extraction inventory fixes the current public surface, - consumers, compatibility importer, fixed-step order, and deterministic parity - evidence at its recorded source revision. -- `StageSession::PhysicsWorldFactory` already gives the standalone and usdview - hosts one backend-selection seam. - -## 3. Completed locally - -- Replaced Stage Runner's repository-local `physicsCore` and `physicsJolt` - source edges with installed-package discovery and equivalent OpenStrata - requirements. -- Adapted Stage Runner includes, namespace usage, and math values explicitly at - the composition boundary. -- Moved prim/body mapping, dirty synchronization, and fixed-step delegation from - the local `PhysicsRuntime` into `stageRuntime` without moving `PrimId` or - `RuntimeWorld` into the external package. -- Kept the Runner physics schema importer in Stage Runner as a temporary - compatibility path that produces neutral external descriptors. -- Preserved `StageSession::PhysicsWorldFactory` and used the same external Jolt - factory in standalone and usdview hosts. -- Removed the repository-local physics libraries after the installed package - path passed all 48 local Windows parity tests. -- Published the pinned Windows packages as public, immutable OCI artifacts and - verified both OCI and content digests from a fresh cache. -- Published equivalent Linux packages and verified their content and OCI - digests from a fresh cache; see the - [Linux artifact report](../reports/2026-09-23-phase3-linux-artifacts.md). -- Stage Runner passed hosted Windows and Linux OpenStrata consumer builds and - tests with artifact caches disabled, plus the plain-CMake hosted jobs; - [the consumer run](https://github.com/animu-sphere/usd-stage-runner/actions/runs/35853380748) - records both platform cells. - -## 4. Remaining Phase 3 work - -- Implement and dry-run the versioned release workflow against the - [release schema](../architecture/RELEASE_SCHEMA.md) before creating a tag; - the currently published Windows and Linux artifacts are Phase 3 inputs, not - a full release. - -## 5. Phase 3 completion criteria - -- Existing falling-body, character grounding, jump-support, and camera - collision scenarios pass against installed `physicsCore` and `physicsJolt`. -- Stage Runner no longer owns the reusable neutral physics implementation or - the Jolt backend, and no Jolt type crosses a public Stage Runner boundary. -- Prim/body mapping, fixed-step ordering, changed-state synchronization, and - discardable runtime-layer behavior remain owned and tested by Stage Runner. -- Runner physics schema fixtures continue to work through the local - compatibility importer; no new `runner:physics:*` property is added. -- Plain CMake, OpenStrata, standalone, and usdview resolve the same package and - world-factory graph. +# Current roadmap — release follow-up and Phase 4 + +Status: in progress, 2026-09-27. The installed core/backend consumer migration +has passed hosted Windows/Linux verification. An optional standard Box reader +is implemented; the [capability matrix](../reference/CAPABILITY_MATRIX.md) +records the supported subset and tests. + +## 1. Release follow-up + +Implement and dry-run the versioned release workflow against the +[release schema](../architecture/RELEASE_SCHEMA.md) before creating a tag. +Existing Windows/Linux core/backend artifacts remain migration inputs rather +than a full release. + +## 2. Next Box delivery slice + +- Validate and publish Windows/Linux `physicsUsd` artifacts and pin them in + Stage Runner's OpenStrata requirements before enabling standard import by default. +- Run hosted standard/compatibility parity including standalone and usdview. +- Extend through working fixtures for scene gravity, additional shapes, + mass inference, fixed joints, and composed transforms. +- Resolve stage-generation identity and synchronization contracts before + introducing retained resource mappings in the package. + +## 3. Completion criteria + +Standard declarations become the primary authored representation in Stage +Runner while core/backend packages remain reusable and contain no gameplay +policy. The same package graph must work in plain CMake and OpenStrata, on +Windows and Linux. Keep Runner compatibility fixtures until all host and +runtime-layer parity gates pass. The wider intended bridge is defined in +[USD_BRIDGE_CONTRACT.md](../design/USD_BRIDGE_CONTRACT.md). diff --git a/libs/physicsUsd/CMakeLists.txt b/libs/physicsUsd/CMakeLists.txt new file mode 100644 index 0000000..e3d32d1 --- /dev/null +++ b/libs/physicsUsd/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 3.25) +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../VERSION") + file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/../../VERSION" _physics_usd_version LIMIT_COUNT 1) + string(STRIP "${_physics_usd_version}" _physics_usd_version) +else() + set(_physics_usd_version "0.1.0") +endif() +project(physicsUsd VERSION ${_physics_usd_version} LANGUAGES CXX) + +if(NOT TARGET physicsCore::physicsCore) + find_package(physicsCore 0.1 CONFIG REQUIRED) +endif() +find_package(pxr CONFIG REQUIRED) +foreach(component IN ITEMS usd usdGeom usdPhysics) + if(TARGET pxr::${component}) + list(APPEND _physics_usd_targets pxr::${component}) + elseif(TARGET ${component}) + list(APPEND _physics_usd_targets ${component}) + else() + message(FATAL_ERROR "physicsUsd requires the OpenUSD ${component} target") + endif() +endforeach() + +add_library(physicsUsd STATIC src/box_scene.cpp) +add_library(physicsUsd::physicsUsd ALIAS physicsUsd) +set_target_properties(physicsUsd PROPERTIES POSITION_INDEPENDENT_CODE ON) +target_compile_features(physicsUsd PUBLIC cxx_std_17) +target_include_directories(physicsUsd PUBLIC + "$" + "$") +target_link_libraries(physicsUsd PUBLIC physicsCore::physicsCore ${_physics_usd_targets}) + +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) +install(TARGETS physicsUsd EXPORT physicsUsdTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) +install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) +configure_package_config_file(cmake/physicsUsdConfig.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/physicsUsdConfig.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/physicsUsd) +write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/physicsUsdConfigVersion.cmake" + VERSION ${PROJECT_VERSION} COMPATIBILITY SameMinorVersion) +install(EXPORT physicsUsdTargets FILE physicsUsdTargets.cmake NAMESPACE physicsUsd:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/physicsUsd) +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/physicsUsdConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/physicsUsdConfigVersion.cmake" + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/physicsUsd) + +set(_physics_usd_tests_default ${PROJECT_IS_TOP_LEVEL}) +if(DEFINED USDPHYSICS_BUILD_TESTS) + set(_physics_usd_tests_default ${USDPHYSICS_BUILD_TESTS}) +endif() +option(PHYSICSUSD_BUILD_TESTS "Build standard USD declaration tests" ${_physics_usd_tests_default}) +if(PHYSICSUSD_BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/libs/physicsUsd/cmake/physicsUsdConfig.cmake.in b/libs/physicsUsd/cmake/physicsUsdConfig.cmake.in new file mode 100644 index 0000000..906f9c7 --- /dev/null +++ b/libs/physicsUsd/cmake/physicsUsdConfig.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ +include(CMakeFindDependencyMacro) +find_dependency(physicsCore 0.1 CONFIG) +find_dependency(pxr CONFIG) +include("${CMAKE_CURRENT_LIST_DIR}/physicsUsdTargets.cmake") diff --git a/libs/physicsUsd/include/usd_physics/usd/box_scene.h b/libs/physicsUsd/include/usd_physics/usd/box_scene.h new file mode 100644 index 0000000..571cecb --- /dev/null +++ b/libs/physicsUsd/include/usd_physics/usd/box_scene.h @@ -0,0 +1,45 @@ +#pragma once + +#include "usd_physics/core/descriptors.h" +#include +#include +#include +#include + +namespace usd_physics::usd { + +enum class ImportErrorCode { + invalidStage, + unsupportedUnits, + unsupportedDeclaration, + unsupportedTransform, + invalidValue, +}; + +class ImportError : public std::runtime_error { +public: + ImportError(ImportErrorCode code, std::string path, const std::string& detail); + [[nodiscard]] ImportErrorCode code() const noexcept { return code_; } + [[nodiscard]] const std::string& path() const noexcept { return path_; } +private: + ImportErrorCode code_; + std::string path_; +}; + +// Snapshot-only declarations. Paths belong to the supplied Stage; consumers +// own resource handles, Stage lifetime, scheduling, and writeback. +struct BoxBody { + std::string path; + core::ShapeDescriptor shape; + core::MotionType motionType{core::MotionType::staticBody}; + core::Transform transform; + double mass{1.0}; +}; + +// Validates the complete supported subset before returning any declarations. +// Requires Y-up meters/kg, Cube collision on the body prim, explicit positive +// dynamic mass, translate then scale, and identity ancestors (or reset stack). +// No USD edits or runtime resources are created. Results are sorted by path. +[[nodiscard]] std::vector readBoxScene(const pxr::UsdStageRefPtr& stage); + +} // namespace usd_physics::usd diff --git a/libs/physicsUsd/openstrata.library.yaml b/libs/physicsUsd/openstrata.library.yaml new file mode 100644 index 0000000..f176a7b --- /dev/null +++ b/libs/physicsUsd/openstrata.library.yaml @@ -0,0 +1,21 @@ +schema: openstrata.library/v1alpha1 +library: + id: physicsUsd + version: 0.1.0 +cmake: + package: physicsUsd + target: physicsUsd::physicsUsd +requires: + libraries: + - id: physicsCore + version: ">=0.1,<0.2" +package: + standalone: true + aggregate_member: true +package_contract: + package_name: physicsUsd + exported_targets: [physicsUsd::physicsUsd] + public_headers: [include/usd_physics/usd/**] + consumer: + include: usd_physics/usd/box_scene.h + symbol: usd_physics::usd::readBoxScene diff --git a/libs/physicsUsd/src/box_scene.cpp b/libs/physicsUsd/src/box_scene.cpp new file mode 100644 index 0000000..6d63baa --- /dev/null +++ b/libs/physicsUsd/src/box_scene.cpp @@ -0,0 +1,187 @@ +#include "usd_physics/usd/box_scene.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace usd_physics::usd { +namespace { +using Code = ImportErrorCode; + +[[noreturn]] void reject(Code code, const pxr::UsdPrim& prim, const char* detail) { + throw ImportError(code, prim.GetPath().GetString(), detail); +} + +template T read(const pxr::UsdAttribute& attribute, const pxr::UsdPrim& prim) { + T value{}; + if (!attribute || attribute.GetNumTimeSamples() != 0 || !attribute.Get(&value)) { + reject(Code::invalidValue, prim, "expected a readable, time-independent value"); + } + return value; +} + +void validateProperties(const pxr::UsdPrim& prim, bool rigid, bool collision, bool mass) { + for (const auto& schema : prim.GetAppliedSchemas()) { + const auto& name = schema.GetString(); + if (name.rfind("Physics", 0) == 0 && name != "PhysicsRigidBodyAPI" && + name != "PhysicsCollisionAPI" && name != "PhysicsMassAPI") { + reject(Code::unsupportedDeclaration, prim, "unsupported physics API"); + } + } + if (prim.GetTypeName().GetString().rfind("Physics", 0) == 0) { + reject(Code::unsupportedDeclaration, prim, "scenes and joints are not supported yet"); + } + for (const auto& property : prim.GetAuthoredProperties()) { + const auto& name = property.GetName().GetString(); + if (name.rfind("physics:", 0) != 0) continue; + const bool admitted = + (collision && name == "physics:collisionEnabled") || + (rigid && (name == "physics:rigidBodyEnabled" || name == "physics:kinematicEnabled")) || + (mass && name == "physics:mass"); + if (!admitted) { + reject(Code::unsupportedDeclaration, prim, "unsupported physics property or missing API"); + } + } +} + +void readTransform(const pxr::UsdPrim& prim, BoxBody& result) { + const pxr::UsdGeomXformable xform(prim); + bool reset = false; + const auto ops = xform.GetOrderedXformOps(&reset); + if (ops.empty() || ops.front().IsInverseOp() || + ops.front().GetOpType() != pxr::UsdGeomXformOp::TypeTranslate) { + reject(Code::unsupportedTransform, prim, "expected translate followed by scale ops"); + } + // GetAs supports the float and half translate ops used by host fixtures. + pxr::GfVec3d position; + if (ops.front().GetAttr().GetNumTimeSamples() != 0 || + !ops.front().GetAs(&position, pxr::UsdTimeCode::Default())) { + reject(Code::invalidValue, prim, "unreadable translation"); + } + result.transform.translation = {position[0], position[1], position[2]}; + pxr::GfVec3d scale(1.0); + for (std::size_t i = 1; i < ops.size(); ++i) { + if (ops[i].IsInverseOp() || ops[i].GetOpType() != pxr::UsdGeomXformOp::TypeScale) { + reject(Code::unsupportedTransform, prim, "expected translate followed by scale ops"); + } + pxr::GfVec3d value; + if (ops[i].GetAttr().GetNumTimeSamples() != 0 || + !ops[i].GetAs(&value, pxr::UsdTimeCode::Default())) { + reject(Code::invalidValue, prim, "unreadable or animated scale"); + } + for (int axis = 0; axis != 3; ++axis) scale[axis] *= value[axis]; + } + if (!reset) { + for (auto parent = prim.GetParent(); parent && !parent.IsPseudoRoot(); parent = parent.GetParent()) { + const pxr::UsdGeomXformable parentXform(parent); + if (parentXform && (parentXform.TransformMightBeTimeVarying() || + !pxr::GfIsClose(parentXform.ComputeLocalToWorldTransform(pxr::UsdTimeCode::Default()), + pxr::GfMatrix4d(1.0), 1e-9))) { + reject(Code::unsupportedTransform, prim, "expected identity ancestors or resetXformStack"); + } + } + } + const auto size = read(pxr::UsdGeomCube(prim).GetSizeAttr(), prim); + if (!std::isfinite(size) || size <= 0.0) { + reject(Code::invalidValue, prim, "cube size must be positive and finite"); + } + result.shape.halfExtents = {std::abs(scale[0]) * size / 2.0, + std::abs(scale[1]) * size / 2.0, + std::abs(scale[2]) * size / 2.0}; + try { + core::validateShapeDescriptor(result.shape); + } catch (const std::exception&) { + reject(Code::invalidValue, prim, "invalid scaled cube dimensions"); + } + if (!std::isfinite(position[0]) || !std::isfinite(position[1]) || !std::isfinite(position[2])) { + reject(Code::invalidValue, prim, "translation must be finite"); + } +} +} // namespace + +ImportError::ImportError(ImportErrorCode code, std::string path, const std::string& detail) + : std::runtime_error("physicsUsd: " + path + ": " + detail), code_(code), path_(std::move(path)) {} + +std::vector readBoxScene(const pxr::UsdStageRefPtr& stage) { + if (!stage) throw ImportError(Code::invalidStage, {}, "expected an open Stage"); + std::vector result; + bool unitsChecked = false; + for (const auto& prim : stage->Traverse(pxr::UsdTraverseInstanceProxies())) { + const bool rigid = prim.HasAPI(); + const bool collision = prim.HasAPI(); + const bool mass = prim.HasAPI(); + validateProperties(prim, rigid, collision, mass); + if (!rigid && !collision && !mass) continue; + if (!unitsChecked) { + if (pxr::UsdGeomGetStageUpAxis(stage) != pxr::UsdGeomTokens->y || + !pxr::UsdGeomLinearUnitsAre(pxr::UsdGeomGetStageMetersPerUnit(stage), 1.0) || + !pxr::UsdPhysicsMassUnitsAre(pxr::UsdPhysicsGetStageKilogramsPerUnit(stage), 1.0)) { + reject(Code::unsupportedUnits, prim, "expected Y-up meters and kilograms"); + } + unitsChecked = true; + } + if (prim.IsInstance() || prim.IsInstanceProxy()) { + reject(Code::unsupportedDeclaration, prim, "instanced physics is not supported"); + } + for (auto parent = prim.GetParent(); parent && !parent.IsPseudoRoot(); parent = parent.GetParent()) { + if (parent.HasAPI() || parent.HasAPI()) { + reject(Code::unsupportedDeclaration, prim, "nested bodies and inherited mass are not supported"); + } + } + if (!collision || !pxr::UsdGeomCube(prim)) { + reject(Code::unsupportedDeclaration, prim, "expected CollisionAPI on a Cube at the body prim"); + } + const bool enabled = read(pxr::UsdPhysicsCollisionAPI(prim).GetCollisionEnabledAttr(), prim); + if (!enabled) { + if (rigid) reject(Code::unsupportedDeclaration, prim, "a body without an enabled collider is unsupported"); + continue; + } + BoxBody body; + body.path = prim.GetPath().GetString(); + if (rigid) { + const pxr::UsdPhysicsRigidBodyAPI api(prim); + if (read(api.GetKinematicEnabledAttr(), prim)) { + reject(Code::unsupportedDeclaration, prim, "kinematic bodies are not supported"); + } + if (read(api.GetRigidBodyEnabledAttr(), prim)) body.motionType = core::MotionType::dynamicBody; + } + if (body.motionType == core::MotionType::dynamicBody) { + if (!mass) reject(Code::unsupportedDeclaration, prim, "dynamic boxes require explicit MassAPI mass"); + body.mass = read(pxr::UsdPhysicsMassAPI(prim).GetMassAttr(), prim); + if (!std::isfinite(body.mass) || body.mass <= 0.0) { + reject(Code::invalidValue, prim, "mass must be explicit, positive and finite; density inference is unsupported"); + } + } + readTransform(prim, body); + result.push_back(std::move(body)); + } + for (const auto& prim : stage->Traverse()) { + const pxr::UsdGeomPointInstancer instancer(prim); + if (!instancer) continue; + pxr::SdfPathVector prototypes; + if (!instancer.GetPrototypesRel().GetTargets(&prototypes)) { + reject(Code::unsupportedDeclaration, prim, "unreadable point-instancer prototypes"); + } + for (const auto& prototype : prototypes) { + for (const auto& body : result) { + if (pxr::SdfPath(body.path).HasPrefix(prototype)) { + reject(Code::unsupportedDeclaration, prim, "point-instanced physics is not supported"); + } + } + } + } + std::sort(result.begin(), result.end(), [](const auto& a, const auto& b) { return a.path < b.path; }); + return result; +} +} // namespace usd_physics::usd diff --git a/libs/physicsUsd/tests/CMakeLists.txt b/libs/physicsUsd/tests/CMakeLists.txt new file mode 100644 index 0000000..2c3e3c8 --- /dev/null +++ b/libs/physicsUsd/tests/CMakeLists.txt @@ -0,0 +1,7 @@ +add_executable(physics_usd_box_scene_test box_scene_test.cpp) +target_link_libraries(physics_usd_box_scene_test PRIVATE physicsUsd::physicsUsd) +add_test(NAME physicsUsd.box_scene COMMAND physics_usd_box_scene_test) +if(WIN32) + set_tests_properties(physicsUsd.box_scene PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${pxr_DIR}/bin;PATH=path_list_prepend:${pxr_DIR}/lib;PATH=path_list_prepend:${Python3_RUNTIME_LIBRARY_DIRS}") +endif() diff --git a/libs/physicsUsd/tests/box_scene_test.cpp b/libs/physicsUsd/tests/box_scene_test.cpp new file mode 100644 index 0000000..a2a78f5 --- /dev/null +++ b/libs/physicsUsd/tests/box_scene_test.cpp @@ -0,0 +1,124 @@ +#include "usd_physics/usd/box_scene.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace usd_physics; +using Code = usd::ImportErrorCode; + +pxr::UsdStageRefPtr fixture() { + auto stage = pxr::UsdStage::CreateInMemory(); + pxr::UsdGeomSetStageUpAxis(stage, pxr::UsdGeomTokens->y); + pxr::UsdGeomSetStageMetersPerUnit(stage, 1.0); + auto cube = pxr::UsdGeomCube::Define(stage, pxr::SdfPath("/World/Box")); + cube.AddTranslateOp(pxr::UsdGeomXformOp::PrecisionFloat).Set(pxr::GfVec3f(0, 3, 0)); + cube.AddScaleOp().Set(pxr::GfVec3f(-0.5, 1, 2)); + pxr::UsdPhysicsCollisionAPI::Apply(cube.GetPrim()); + pxr::UsdPhysicsRigidBodyAPI::Apply(cube.GetPrim()); + pxr::UsdPhysicsMassAPI::Apply(cube.GetPrim()).CreateMassAttr().Set(70.0f); + return stage; +} + +void check(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +int main() { + try { + auto stage = fixture(); + std::string before, after; + stage->GetRootLayer()->ExportToString(&before); + const auto boxes = usd::readBoxScene(stage); + stage->GetRootLayer()->ExportToString(&after); + check(before == after, "import must not edit USD"); + check(boxes.size() == 1 && boxes[0].path == "/World/Box" && boxes[0].mass == 70 && + boxes[0].motionType == core::MotionType::dynamicBody && + boxes[0].shape.halfExtents == core::Vector3{0.5, 1, 2} && + boxes[0].transform.translation == core::Vector3{0, 3, 0}, "box conversion"); + auto box = stage->GetPrimAtPath(pxr::SdfPath("/World/Box")); + pxr::UsdPhysicsRigidBodyAPI(box).CreateRigidBodyEnabledAttr().Set(false); + check(usd::readBoxScene(stage)[0].motionType == core::MotionType::staticBody, "disabled rigid body is static"); + box.RemoveAPI(); + box.RemoveProperty(pxr::TfToken("physics:rigidBodyEnabled")); + check(usd::readBoxScene(stage)[0].motionType == core::MotionType::staticBody, "collision-only is static"); + pxr::UsdPhysicsCollisionAPI(box).CreateCollisionEnabledAttr().Set(false); + check(usd::readBoxScene(stage).empty(), "disabled static collision is skipped"); + stage = fixture(); + auto floor = pxr::UsdGeomCube::Define(stage, pxr::SdfPath("/A")); + floor.AddTranslateOp().Set(pxr::GfVec3d(0, -1, 0)); + pxr::UsdPhysicsCollisionAPI::Apply(floor.GetPrim()); + check(usd::readBoxScene(stage)[0].path == "/A", "path order is deterministic"); + auto parent = pxr::UsdGeomXform::Define(stage, pxr::SdfPath("/World")); + parent.AddTranslateOp().Set(pxr::GfVec3d(4, 0, 0)); + pxr::UsdGeomXformable(stage->GetPrimAtPath(pxr::SdfPath("/World/Box"))).SetResetXformStack(true); + check(usd::readBoxScene(stage)[1].transform.translation.x == 0, "reset stack ignores ancestors"); + + using Edit = std::function; + const std::vector> invalid{ + {Code::unsupportedUnits, [](auto s, auto) { pxr::UsdGeomSetStageMetersPerUnit(s, .01); }}, + {Code::unsupportedUnits, [](auto s, auto) { pxr::UsdGeomSetStageUpAxis(s, pxr::UsdGeomTokens->z); }}, + {Code::unsupportedUnits, [](auto s, auto) { pxr::UsdPhysicsSetStageKilogramsPerUnit(s, .001); }}, + {Code::invalidValue, [](auto, auto p) { pxr::UsdPhysicsMassAPI(p).CreateMassAttr().Set(0.0f); }}, + {Code::invalidValue, [](auto, auto p) { pxr::UsdPhysicsMassAPI(p).CreateMassAttr().Set(-1.0f); }}, + {Code::invalidValue, [](auto, auto p) { pxr::UsdGeomCube(p).CreateSizeAttr().Set(std::numeric_limits::infinity()); }}, + {Code::invalidValue, [](auto, auto p) { p.GetAttribute(pxr::TfToken("xformOp:scale")).Set(pxr::GfVec3f(0)); }}, + {Code::invalidValue, [](auto, auto p) { p.GetAttribute(pxr::TfToken("xformOp:translate")).Set(pxr::GfVec3f(2), 1); }}, + {Code::unsupportedDeclaration, [](auto, auto p) { pxr::UsdPhysicsRigidBodyAPI(p).CreateKinematicEnabledAttr().Set(true); }}, + {Code::unsupportedDeclaration, [](auto, auto p) { pxr::UsdPhysicsRigidBodyAPI(p).CreateVelocityAttr().Set(pxr::GfVec3f(1)); }}, + {Code::unsupportedDeclaration, [](auto, auto p) { pxr::UsdPhysicsMassAPI(p).CreateDensityAttr().Set(1000.0f); }}, + {Code::unsupportedDeclaration, [](auto, auto p) { p.RemoveAPI(); }}, + {Code::unsupportedDeclaration, [](auto, auto p) { p.SetTypeName(pxr::TfToken("Sphere")); }}, + {Code::unsupportedDeclaration, [](auto, auto p) { pxr::UsdPhysicsCollisionAPI(p).CreateCollisionEnabledAttr().Set(false); }}, + {Code::unsupportedDeclaration, [](auto s, auto) { pxr::UsdPhysicsScene::Define(s, pxr::SdfPath("/Scene")); }}, + {Code::unsupportedDeclaration, [](auto s, auto) { + pxr::UsdGeomPointInstancer::Define(s, pxr::SdfPath("/Points")) + .CreatePrototypesRel().SetTargets({pxr::SdfPath("/World")}); + }}, + {Code::unsupportedDeclaration, [](auto s, auto) { pxr::UsdPhysicsRigidBodyAPI::Apply(s->GetPrimAtPath(pxr::SdfPath("/World"))); }}, + {Code::unsupportedTransform, [](auto, auto p) { pxr::UsdGeomXformable(p).AddRotateYOp().Set(30.0f); }}, + {Code::unsupportedTransform, [](auto, auto p) { pxr::UsdGeomXformable(p).ClearXformOpOrder(); }}, + {Code::unsupportedTransform, [](auto s, auto) { pxr::UsdGeomXform::Define(s, pxr::SdfPath("/World")).AddTranslateOp().Set(pxr::GfVec3d(1)); }}, + }; + for (std::size_t i = 0; i < invalid.size(); ++i) { + stage = fixture(); + invalid[i].second(stage, stage->GetPrimAtPath(pxr::SdfPath("/World/Box"))); + try { + (void)usd::readBoxScene(stage); + throw std::runtime_error("invalid fixture accepted: " + std::to_string(i)); + } catch (const usd::ImportError& error) { + check(error.code() == invalid[i].first && !error.path().empty(), "wrong diagnostic code/path"); + } + } + stage = fixture(); + auto instance = stage->DefinePrim(pxr::SdfPath("/Instance")); + instance.GetReferences().AddInternalReference(pxr::SdfPath("/World")); + instance.SetInstanceable(true); + try { + (void)usd::readBoxScene(stage); + throw std::runtime_error("instanced physics accepted"); + } catch (const usd::ImportError& error) { + check(error.code() == Code::unsupportedDeclaration, "instance diagnostic"); + } + try { + (void)usd::readBoxScene({}); + throw std::runtime_error("null Stage accepted"); + } catch (const usd::ImportError& error) { + check(error.code() == Code::invalidStage, "null Stage diagnostic"); + } + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/openstrata.toml b/openstrata.toml index 4df1866..0839766 100644 --- a/openstrata.toml +++ b/openstrata.toml @@ -11,10 +11,16 @@ cache = { PHYSICSJOLT_REQUIRE_JOLT = { type = "BOOL", value = true } } +[build.intents.usd] +cache = { + USDPHYSICS_BUILD_USD = { type = "BOOL", value = true } +} + [workspace] members = [ "libs/physicsCore", "backends/physicsJolt", + "libs/physicsUsd", ] release_members = [ "physicsCore", diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 46e29bb..a4316c0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -26,6 +26,8 @@ add_test(NAME workspace.installed_consumer "-DTEST_MT=${CMAKE_MT}" "-DTEST_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}" "-DTEST_JOLT_DIR=${Jolt_DIR}" + "-DTEST_USD=${USDPHYSICS_BUILD_USD}" + "-DTEST_PXR_DIR=${pxr_DIR}" "-DTEST_CONFIG=$" -P "${CMAKE_CURRENT_SOURCE_DIR}/run_installed_consumer.cmake") set_tests_properties(workspace.installed_consumer PROPERTIES diff --git a/tests/check_boundaries.cmake b/tests/check_boundaries.cmake index ba3da9a..14ca8de 100644 --- a/tests/check_boundaries.cmake +++ b/tests/check_boundaries.cmake @@ -47,4 +47,12 @@ foreach(_header IN LISTS _public_jolt_headers) endif() endforeach() -message(STATUS "physicsCore and physicsJolt source boundaries are clean") +assert_sources_exclude( + "${PROJECT_ROOT}/libs/physicsUsd" + "physicsUsd" + "[#]include[ \\t]*[<\"]Jolt/" + "usd_stage_runner" + "usd_mmd" + "usd_vrm") + +message(STATUS "physicsCore, physicsJolt, and physicsUsd source boundaries are clean") diff --git a/tests/check_versions.cmake b/tests/check_versions.cmake index ed8686b..53aa571 100644 --- a/tests/check_versions.cmake +++ b/tests/check_versions.cmake @@ -11,6 +11,7 @@ endif() foreach(_manifest IN ITEMS "${PROJECT_ROOT}/libs/physicsCore/openstrata.library.yaml" + "${PROJECT_ROOT}/libs/physicsUsd/openstrata.library.yaml" "${PROJECT_ROOT}/backends/physicsJolt/openstrata.library.yaml") file(READ "${_manifest}" _content) string(REGEX MATCH "version:[ \t]*\"?([^\" \t\r\n]+)" diff --git a/tests/installed_consumer/CMakeLists.txt b/tests/installed_consumer/CMakeLists.txt index 3b25640..ee5ef0d 100644 --- a/tests/installed_consumer/CMakeLists.txt +++ b/tests/installed_consumer/CMakeLists.txt @@ -26,3 +26,13 @@ target_link_libraries(usd_physics_installed_consumer PRIVATE enable_testing() add_test(NAME installed_consumer.runs COMMAND usd_physics_installed_consumer) +if(TEST_USD) + find_package(physicsUsd CONFIG REQUIRED) + add_executable(usd_physics_usd_consumer usd_main.cpp) + target_link_libraries(usd_physics_usd_consumer PRIVATE physicsUsd::physicsUsd) + add_test(NAME installed_consumer.usd COMMAND usd_physics_usd_consumer) + if(WIN32) + set_tests_properties(installed_consumer.usd PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${pxr_DIR}/bin;PATH=path_list_prepend:${pxr_DIR}/lib;PATH=path_list_prepend:${Python3_RUNTIME_LIBRARY_DIRS}") + endif() +endif() diff --git a/tests/installed_consumer/usd_main.cpp b/tests/installed_consumer/usd_main.cpp new file mode 100644 index 0000000..7f7812d --- /dev/null +++ b/tests/installed_consumer/usd_main.cpp @@ -0,0 +1,16 @@ +#include "usd_physics/usd/box_scene.h" +#include +#include +#include +#include + +int main() { + const auto stage = pxr::UsdStage::CreateInMemory(); + pxr::UsdGeomSetStageUpAxis(stage, pxr::UsdGeomTokens->y); + pxr::UsdGeomSetStageMetersPerUnit(stage, 1.0); + const auto cube = pxr::UsdGeomCube::Define(stage, pxr::SdfPath("/Box")); + cube.AddTranslateOp().Set(pxr::GfVec3d(0.0)); + pxr::UsdPhysicsCollisionAPI::Apply(cube.GetPrim()); + const auto boxes = usd_physics::usd::readBoxScene(stage); + return boxes.size() == 1 && boxes[0].shape.halfExtents.x == 1.0 ? 0 : 1; +} diff --git a/tests/run_installed_consumer.cmake b/tests/run_installed_consumer.cmake index d87586e..b15c334 100644 --- a/tests/run_installed_consumer.cmake +++ b/tests/run_installed_consumer.cmake @@ -28,7 +28,8 @@ foreach(_metadata IN LISTS _installed_metadata) file(READ "${_metadata}" _content) string(REPLACE "\\" "/" _content "${_content}") foreach(_forbidden IN ITEMS - "${_core_source}" "${_jolt_source}" "${_core_build}" "${_jolt_build}") + "${_core_source}" "${_jolt_source}" "${_core_build}" "${_jolt_build}" + "${PROJECT_SOURCE_ROOT}/libs/physicsUsd" "${PROJECT_BUILD_DIR}/libs/physicsUsd") string(FIND "${_content}" "${_forbidden}" _position) if(NOT _position EQUAL -1) message(FATAL_ERROR @@ -47,6 +48,10 @@ set(_configure_args if(TEST_GENERATOR_PLATFORM) list(APPEND _configure_args -A "${TEST_GENERATOR_PLATFORM}") endif() +if(TEST_USD) + list(APPEND _configure_args "-DTEST_USD=ON" "-Dpxr_DIR=${TEST_PXR_DIR}" + "-DCMAKE_PREFIX_PATH=${_prefix}\;${TEST_PXR_DIR}") +endif() if(TEST_MAKE_PROGRAM) list(APPEND _configure_args "-DCMAKE_MAKE_PROGRAM=${TEST_MAKE_PROGRAM}") endif()