Skip to content

Rigid and Affine Body Dynamics - #184

Open
zfergus wants to merge 110 commits into
mainfrom
dynamics
Open

zfergus wants to merge 110 commits into
mainfrom
dynamics

Conversation

@zfergus

@zfergus zfergus commented Aug 18, 2025 •

Copy link
Copy Markdown
Member

Description

Adds rigid and affine body dynamics as reusable library components (src/ipc/dynamics/), glTF scene I/O (src/ipc/io/), and a demo simulator (demo/) that assembles them, touching CCD, the LBVH broad phase, and the Python bindings along the way.

Mechanism

  • ipc::rigid — Pose, RigidBody, and RigidBodies (a CollisionMesh of all bodies, built from per-body meshes). Each body is re-centered on its center of mass and expressed in its principal frame. This follows Rigid IPC (Ferguson et al. 2021).
  • ipc::rigid::RigidCandidates — finds body pairs with a bounding-sphere broad phase, then queries each pair with two-tree LBVH queries in one body's rest frame. Narrow-phase CCD follows the curved rigid trajectories through NonlinearCCD, with a conservative bound on how far each piece strays from its chord.
  • ipc::affine — the body terms are written once in affine coordinates y = [p; vec(A)]: InertialTerm, BodyForces, OrthogonalityPotential, an AugmentedLagrangian that drives kinematic bodies, and linear JointConstraints. This follows ABD (Lan et al. 2022), with joints as in Chen et al. 2022, §4.1.
  • ipc::dynamics — a ToAffine change of variables lets the same terms serve both models. RigidToAffine maps [p; θ] to A = R(θ); AffineToAffine is the identity. BodyPotentials sums the terms and applies the chain rule once, and BDF provides orders 1–6.
  • demo/ (ipc_toolkit_demo, not part of the library) — a polysolve-based Simulator with adaptive barrier stiffness, lagged friction, and static/kinematic bodies. It is exposed in Python as ipctk.demo.

Impact

  • Downstream simulators can build rigid or affine body dynamics from library parts. The core library still depends on no solver; only the demo links polysolve.
  • NonlinearCCD can now use any NarrowPhaseCCD for its per-piece linear CCD; the demo uses AdditiveCCD.
  • Scenes load from and animations save to glTF when IPC_TOOLKIT_WITH_GLTF=ON.
  • Top-level builds now build the demo by default, which fetches polysolve. Projects that include the toolkit as a subproject are unaffected.

API changes

  • New CMake options: IPC_TOOLKIT_BUILD_DEMO (ON at top level, OFF as a subproject) and IPC_TOOLKIT_WITH_GLTF (OFF).
  • NarrowPhaseCCD: every query takes a trailing NarrowPhaseCCDMode mode = STANDALONE. COMPONENT and COMPONENT_NO_ZERO_TOI tell a method it is the per-piece CCD inside a piecewise driver, so it returns the raw time of impact. The mode is passed per call, so a shared narrow-phase object is never modified.
  • NonlinearCCD: the constructor is now NonlinearCCD(narrow_phase_ccd = nullptr, conservative_rescaling); null selects a TightInclusionCCD. Also new: set_narrow_phase_ccd() and narrow_phase_ccd().
  • LBVH: new two-tree queries detect_*_candidates(const LBVH& other, can_collide, candidates) between two separately built trees.
  • CollisionMesh: the destructor and vertices(full_positions) are now virtual, so RigidBodies can map poses to vertices.
  • Smaller additions: nearest_rotation, sinc (scalar and interval), ipc::PI (<ipc/math/constants.hpp>), and a 2D cross_product_matrix(double).
  • Logger: the default ipctk logger is no longer registered in spdlog's global registry. Previously, two Python modules that each embed the toolkit failed on the duplicate name.
  • Python:
    • Bindings for RigidBodies, RigidBody, Pose/Poses, AffinePose, JointConstraints, read_gltf/write_gltf, and NarrowPhaseCCDMode.
    • The CCD classes are now held by std::shared_ptr.
    • Python NarrowPhaseCCD subclasses written without a mode parameter keep working.

💥 Breaking changes

  • C++ subclasses of NarrowPhaseCCD must add the mode parameter to their four overrides.
  • NonlinearCCD::tolerance and NonlinearCCD::max_iterations are removed. Configure the TightInclusionCCD you pass in instead.
  • Deprecated, but still working with a warning:
    • NonlinearCCD(tolerance, max_iterations, conservative_rescaling) in C++ and Python. It builds the equivalent TightInclusionCCD.
    • Candidates::save_obj, renamed to write_obj, in C++ and Python.
    • <ipc/utils/save_obj.hpp>, which forwards to <ipc/io/write_candidates_obj.hpp>.

New dependencies

  • polysolve (MIT), fetched with CPM at a pinned commit.
    • Linked only by ipc_toolkit_demo, never by ipc_toolkit.
    • Its optional backends (CHOLMOD, MKL, AMGCL, …) are disabled, so it uses only Eigen's solvers and adds no transitive system dependencies.
  • TinyGLTF 2.9.6 (MIT), fetched with CPM.
    • Compiled as one translation unit and linked PRIVATE.
    • Fetched only when IPC_TOOLKIT_WITH_GLTF=ON.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which improves existing functionality)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • ipc_toolkit_tests "~[!benchmark]": all 518 test cases pass. This includes 86 new cases in tests/src/tests/{dynamics,demo,io}:
    • Finite-difference checks of every body term, the ToAffine chain rule, and rigid pose derivatives.
    • Analytic checks of mass properties, BDF exactness and derivatives, and external torques applied at arbitrary orientations.
    • Rigid candidates and CCD along curved trajectories.
    • glTF round-trips, including node hierarchies, multi-primitive meshes, and faceless bodies.
    • Simulator scenes: free flight, friction stick/slip and deceleration, static and kinematic bodies, joints, 2D, and codimensional contact.
  • python -m nose2 in python/tests: all 106 tests pass. These include the new NarrowPhaseCCDMode and backward-compatibility tests.
  • A compile-only check of src/ipc/math/sinc.cpp with IPC_TOOLKIT_WITH_FILIB off.

Test Configuration:

  • OS and Version: macOS 26 (Apple Silicon)
  • Compiler and Version: AppleClang 21.0, Release, Python 3.13

Checklist

  • I have followed the project style guide
  • My code follows the clang-format style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

zfergus and others added 17 commits May 31, 2024 00:41
- Added InertialTerm class for computing energy, gradient, and Hessian of rigid bodies.
- Implemented mass properties computation for 2D and 3D meshes, including point clouds.
- Created pose representation with rotation vector and matrix conversions.
- Developed RigidBodies class to manage multiple rigid bodies from mesh data.
- Implemented ImplicitEuler time integrator for updating positions and velocities.
- Added unit tests for mass properties and pose transformations using Catch2 framework.
- Updated finite-diff package from version 1.0.1 to 1.0.3 in CMake configuration.
- Remove template from Pose.
- Modified RigidBody and related classes to use the updated Pose structure.
- Added unit tests for RigidBody construction and transformations.
- Update CPM to 0.42.0
- Add TinyGLTF dependency
- Rename save_obj to write_candidates_obj
- Add write_gltf
@zfergus zfergus added the enhancement New feature or request label Aug 18, 2025
@zfergus zfergus added this to the v1.5.0 milestone Aug 18, 2025
@codecov

codecov Bot commented Aug 18, 2025 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.84834% with 315 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.24%. Comparing base (33fc60f) to head (2082fba).

Files with missing lines Patch % Lines
src/ipc/io/write_gltf.cpp 65.84% 97 Missing ⚠️
src/ipc/io/read_gltf.cpp 76.96% 47 Missing ⚠️
src/ipc/dynamics/rigid/rigid_candidates.cpp 87.01% 37 Missing ⚠️
demo/src/simulator.cpp 94.38% 31 Missing ⚠️
.../ipc/dynamics/time_integration/time_integrator.cpp 77.94% 15 Missing ⚠️
demo/src/kinematics.cpp 85.86% 13 Missing ⚠️
src/ipc/dynamics/affine/joints.cpp 94.25% 10 Missing ⚠️
src/ipc/dynamics/rigid/mass.cpp 93.67% 10 Missing ⚠️
src/ipc/candidates/candidates.cpp 0.00% 9 Missing ⚠️
src/ipc/dynamics/affine/affine_dof.cpp 82.92% 7 Missing ⚠️
... and 12 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #184      +/-   ##
==========================================
- Coverage   96.97%   96.24%   -0.73%     
==========================================
  Files         221      264      +43     
  Lines       22426    25787    +3361     
  Branches     1540     1965     +425     
==========================================
+ Hits        21747    24818    +3071     
- Misses        679      969     +290     
Flag Coverage Δ
unittests 96.24% <90.84%> (-0.73%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- Introduce new overloads for build methods across various classes, allowing for the use of precomputed AABBs.
- Update related documentation and ensure consistent handling of vertex boxes and edges.
- Improve voxel size suggestion logic based on bounding box dimensions.
zfergus and others added 29 commits July 7, 2026 16:34
Finish the Rigid IPC / ABD port on the `dynamics` branch with the remaining
algorithmic features, modernize the incremental potential, and extract the
kinematic driver into its own class.

Objective units:
- Keep the objective uniformly in kg·m²: inertia is unscaled and every other
  term (body forces, orthogonality, barrier, friction, augmented Lagrangian)
  is multiplied by the integrator's (βΔt)² acceleration scaling at summation.
  The library energy terms are now pure, unscaled potentials; the demo
  Simulator applies the scaling. Barrier stiffness κ lives only in the
  BarrierPotential.

Friction (velocity-based, lagged) [Ferguson et al. 2021]:
- Wire TangentialCollisions + FrictionPotential into the demo Simulator.
  Friction is evaluated on velocities recovered from the BDF history (correct
  at order ≥ 2), lagged per solve, with a single outer loop and
  momentum-balance convergence. New settings: friction_coefficient,
  static_friction_speed_bound, friction_iterations.

Static / kinematic bodies (augmented Lagrangian + DOF masking):
- RigidBody gains Type (STATIC/KINEMATIC/DYNAMIC) and per-DOF is_dof_fixed.
- New rigid/affine AugmentedLagrangian energy terms (manual derivatives) drive
  kinematic bodies to per-step targets; DOF masking pins static/fixed/AL-
  satisfied DOFs, composed with the joint reduction.
- Kinematic driving is encapsulated in a demo KinematicDriver owned by the
  Simulator (set_kinematic_driver); RigidBody keeps only its classification.

2D simulation (rigid + affine):
- Dimension-generic mass properties (2×2 second moment; edge/point-cloud
  lumped masses), explicit time-integrator layout, dim-aware kinematics and
  simulator, 2D affine terms and joints (add_hinge throws in 2D).

Codimensional contact:
- Rigid body-pair broad phase emits vertex-vertex (and 3D codim edge-vertex)
  candidates via a new two-tree LBVH query; point clouds and segment nets get
  valid lumped mass and BVHs.

Tests: friction (FD + stick/slip + deceleration), static/kinematic (both
modes), 2D, codim, and augmented-Lagrangian finite-difference checks. Full
suite green (358 cases) + hidden bunny (115 assertions). Python bindings and
demo/README.md updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prep for merging the RBD/ABD inertial and body-force terms behind a single
change of variables.

Stage 0: extract rotation_to_matrix_jacobian/_hessian into pose.{hpp,cpp},
dispatching 2D (so(2) exp map, 4x1) vs 3D (Rodrigues, 9x3 / 9x9), and refactor
Pose::transform_vertices_jacobian/_hessian to use them (behavior-preserving).

Stage 1: move rigid::AffinePose into a new ipc::affine::Pose (affine/pose.{hpp,cpp})
as the shared affine-coordinate representation. Update all references across the
integrator, affine_dof, body_forces, inertial_term, demo kinematics/simulator,
Python bindings (Python-facing name kept as "AffinePose"), and tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stage 2 of unifying the RBD/ABD body terms behind one to-affine map. Nothing is
wired into the simulator yet; the existing terms remain in use.

- ipc::dynamics::ToAffine (RigidToAffine / AffineToAffine): maps the optimization
  DOFs to affine pose coordinates y=[p; vec(A)] and pulls affine-space
  gradients/Hessians back to the DOFs (block-diagonal per body; rotation block
  PSD-projected once). RigidToAffine uses A=Q(θ); AffineToAffine is the identity.

- ipc::dynamics::BodyPotentials: owns the affine-coordinate terms (inertia, body
  forces, SO(dim) orthogonality penalty for ABD, augmented Lagrangian), evaluates
  each at y=to_affine(x), sums with the incremental-potential scaling, and applies
  the to-affine chain rule once. AL is separable via include_al for barrier-
  stiffness seeding.

- Fix a latent 2D bug in affine::BodyForces::update (gravity().head(dim)).

- Tests: test_to_affine (FD chain rule vs a synthetic affine quadratic, 2D+3D,
  rigid+affine) and test_body_potentials (aggregator FD gradient/Hessian).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…old terms

Stage 3 (final) of the merge. The demo simulator now drives the body (non-
contact) physics through a single dynamics::BodyPotentials behind the
parameterization's dynamics::ToAffine map, replacing the parallel rigid/affine
term set.

- Integrator state is affine-shaped [p; vec(Q)] per body in every dimension
  (rot_ndof = dim*dim); the 2D rigid scalar-angle special cases are removed
  (integrator decode, state init, ω extraction). The rigid optimization DOF
  stays reduced [p; θ]; only the state/affine coords/AL use the matrix form.

- Kinematics delegates poses()/to_integrator_state()/from_integrator_state() to
  a held ToAffine (single source of the θ→A map); candidates/CCD and the
  vertex-space maps stay in Kinematics.

- Simulator: the five term optionals + two AL optionals collapse to one
  BodyPotentials. energy/gradient/full_hessian drop their is_affine branches
  (inertia + body forces + orthogonality + AL, chain rule applied once); barrier
  and friction keep the vertex-space chain rule. AL is separable via include_al
  for barrier-stiffness seeding; DOF pinning reads the merged AL's satisfied
  flags. (βΔt)² scaling is unchanged. BodyPotentials gains AL params + satisfied
  accessors.

- Delete rigid::{InertialTerm, BodyForces, AugmentedLagrangian} and
  RigidPotential; their behavior is now the affine term + ToAffine pullback. The
  affine terms are the merged implementations and are retained.

- Tests: drop the rigid-term unit tests (covered by [to_affine]/[body_potentials]);
  retarget the AL test to the affine AL; drop the tautological rigid-vs-affine
  inertial cross-check. Re-baseline the 2D ballistic-rotation angle tolerance:
  2D rotation now integrates in matrix space (as in 3D), differing from the
  linear-in-angle value by O((Δtω)³).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Document the merged formulation (body terms in affine coordinates aggregated by
dynamics::BodyPotentials behind the dynamics::ToAffine map) and refresh the
architecture, augmented-Lagrangian, and tests sections. Add a "Rigid and affine
dynamics" section spelling out the main difference from Rigid IPC: rigid bodies
optimize [p; θ] but integrate the rotation in matrix space (as in ABD), versus
Rigid IPC's rotation-vector dynamics throughout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
/Wall enables off-by-default warnings (C4711/C4710/C4514/C4820/C5045)
that flooded CI with ~80k messages. /W4 is the MSVC equivalent of
-Wall -Wextra.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
M_PI is not defined by <cmath> on MSVC unless _USE_MATH_DEFINES is set
before the include, which broke the CI build. Switch to igl::PI (the
convention already used throughout the test suite) and include <igl/PI.h>.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pose::rotation_vector() asserted A.isUnitary(1e-9), a tolerance carried
over from the rigid case where A = exp(θ) is orthogonal to machine
precision. For affine bodies A is only softly held in SO(dim) by the
orthogonality potential and drifts ~1e-5 from unitary, so the assert
fired in debug builds (release compiles it out), aborting 8 simulator
tests. Relax to isUnitary(1e-3): comfortably above the observed ~2.8e-5
worst-case drift while still catching genuine non-rotations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hook overrode the mirror's default entry (clang-format -i) with a
bare `clang-format` and no -i flag, so clang-format printed to stdout,
left files unchanged, and always exited 0 -- the hook silently passed on
everything and never reformatted or blocked a commit.

Drop the redundant name/description/entry overrides and add -i to args
so
the hook reformats staged files in place.
Add MSVC /wd flags to cmake/ipc_toolkit/ipc_toolkit_warnings.cmake to silence warnings that don't have GCC/Clang equivalents (e.g. /wd4244, /wd4267, /wd4250, /wd4201, /wd4068, /wd4100), keeping MSVC output consistent with other compilers.

In src/ipc/math/math.cpp switch Eigen::JacobiSVD to take compute flags as a template parameter and add a clarifying comment (ComputeFullUV). This matches Eigen's preferred compile-time option and avoids runtime-deprecation/behavior issues.
Remove experimental /experimental:external and /external:W1 compiler flags for MSVC. These flags were used to suppress warnings from external files but are no longer needed.
- rigid.py: apply the original Rigid IPC 04-bolt.json fixture settings
  (dt=0.01, dhat=1e-4 < thread clearance of 1.69e-4, static nut, solver
  iteration budget, continue on non-convergence)
- rigid.py: disable contact area weighting (Rigid IPC parity). The barrier
  stiffness initialization's minimum-stiffness fallback (used whenever a
  step starts with no active contacts) does not account for the Voronoi
  area weights (~1e-6 m^2 on the fine thread mesh), leaving the barrier
  orders of magnitude too soft: the screw wedged nanometers into the
  threads and Newton ground to the iteration limit
- demo: use the IPC collision set type when area weighting is off (the
  improved max approximator assumes area weights)
- demo: log the actual (non-squared) minimum distance at stiffness init
- python: RigidBodies.__getitem__ returns a live reference; add
  vertex_to_body

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eCCD

Replace the direct Tight-Inclusion calls in the piecewise-linear driver
with calls through a stored NarrowPhaseCCD (set_narrow_phase_ccd; default
TightInclusionCCD). A new NarrowPhaseCCD::component_mode flag, set by
NonlinearCCD, tells the narrow phase it is a component of the piecewise
driver, which supplies its own conservative margins:

- TightInclusionCCD bypasses its standalone ccd_strategy (no minimum-
  separation inflation, small-TOI rerun, or rescaling) and forwards the
  driver's per-piece cm_no_zero_toi hint (true on the first piece), a new
  trailing parameter on the NarrowPhaseCCD interface
- AdditiveCCD uses COMPONENT_CONSERVATIVE_RESCALING = 0.99 (a raw TOI
  never terminates since its advancement gap is (1-cr) x initial gap),
  honors cm_no_zero_toi by halving the minimum separation until the
  initial distance clears it, and logs expected within-separation
  returns at trace level
- NonlinearCCD's tolerance/max_iterations move to the TightInclusionCCD
  component; conservative_rescaling stays (used by the driver and the
  plane-vertex CCD)
- demo: rigid mode uses AdditiveCCD per piece (bolt scene: 0.50 s vs
  0.65 s with Tight Inclusion vs 8.45 s original Rigid IPC, 20 steps;
  identical trajectories, intersection-free)
- python: NarrowPhaseCCD hierarchy switches to shared_ptr holders;
  NonlinearCCD.narrow_phase_ccd is a property whose setter enforces
  component_mode

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add IPC_TOOLKIT_BUILD_DEMO using main's unforced option pattern
- Keep the dynamics/demo Python bindings alongside the deprecated
  GCP-name __getattr__
- Keep AdditiveCCD::handle_initial_proximity with the templated
  additive_ccd
- Take main's MSVC warning variable and newer tests-data commit
- Drop unordered_map_and_set.cpp (header-only on main) and save_obj
  (replaced by write_candidates_obj on dynamics)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Project each contact's rigid-DOF Hessian block to PSD, as in Rigid IPC
- Add fixed-size single-vertex Pose Jacobian and Hessian
- Restore position-rotation coupling in RigidToAffine::apply_hessian
- Add Simulator minimum_separation_distance; default
  use_area_weighting to false
- Skip candidates between bodies that are both non-dynamic
- Add unique rigid body names with name lookup
- Expose num_contacts, last_momentum_balance, and a writable
  external_force in Python

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Trim read_gltf/write_gltf to geometry and node names
- Take per-body DOF masks in read_gltf, keyed by unique body name
- Add RigidBody::set_density and RigidBodies::make_unique_names
- Load a settings .py with configure(scene) in rigid.py
- Enable glTF and CUDA in the python preset and find its module

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use a double pi in Pose::clamp_rotation_to_pi (-Werror=float-conversion)
- Cast glTF buffer values to float explicitly
- Keep polysolve's unused CuSolverDN.cu and bare /utf-8 away from nvcc
- Raise the CTest per-test timeout for the slow ESP test on Windows Debug

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Set the second moment of bodies with two rotation DOFs fixed
- Apply external torques as the world torque at any orientation
- Pass the CCD component mode per call instead of mutating the
  shared narrow phase
- Keep the old NonlinearCCD constructor and save_obj as deprecated
  overloads, and old Python CCD overrides working
- Read glTF node hierarchies, matrix transforms, and multiple or
  unindexed primitives; write faceless bodies as lines or points
- Make the joint reduction sparse and reject invalid or late joints
- Fix the BDF::dvdx history index and the FILIB-off sinc build
- Add a shared ipc::PI and rename getters to lower_case
- Fix the CPM OPTIONS syntax in the polysolve recipe

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zfergus
zfergus marked this pull request as ready for review October 25, 2026 16:18

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant