Differentiable XPBD (Extended Position-Based Dynamics) for cloth/chain. Two implementations: C++/Eigen (forward + hand-written adjoint) and JAX (autodiff via grad). Same simulation, same config, comparable output.
| cloth(10, 10) - Free Fall | Chain(10) - Collision | cloth(5, 5) - Collision |
|---|---|---|
![]() |
![]() |
![]() |
| chain(10) - Collision | cloth(8, 8)- Collision | cloth(8, 8) - Free Fall |
|---|---|---|
![]() |
![]() |
![]() |
src/main.cpp C++ sim + adjoint (gravity, ground collider, distance constraints, Jacobi 1-iter)
src/param.conf parameters (sim_rate, gravity, object, compliance, ...)
jax_impl.py same solver in JAX, automatic gradient w.r.t. compliance
tester.py launches a given configuration on both implementation to check if they match results
gui.py GUI app for configuring and launching experiments (C++, JAX, tester.py)
animation/ per-frame .obj output (target_*.obj, guess_*.obj) — loadable in animator.blend
external/eigen header-only, vendored
CMakeLists.txt C++ build
docs/ theory notes (PDF)
docs/InversePhysics.pdf — notes covering the implicit BDF1 simulator, the adjoint method for gradients, Baraff-Witkin cloth, descent / primal-dual methods, and the XPBD adjoint derivation this repo implements.
Requires CMake ≥ 3.16 and a C++17 compiler (MSVC / clang / gcc).
cmake -S . -B build
cmake --build build --config Release
./build/bin/xpbd # Linux/macOS
build\bin\Release\xpbd.exe # Windowspip install jax numpy
python jax_impl.pyPrints target/guess final positions, loss, and dL/dcompliance.
Launch the interactive config and experiment UI:
python gui.pyOpens a window with:
- Left panel — scrollable form with typed inputs for all config parameters.
- Top right — three action buttons: Exec C++ (run C++ exe), Exec JAX (run JAX impl), Compare (run tester.py to cross-validate).
- Bottom right — terminal pane showing live stdout from the running process.
On startup, loads defaults from src/param.conf.
One key = value per line; #, ;, // start comments. Both impls read the same file (pass a path as the first CLI arg, relative to the project root, otherwise the standard path src/param.conf is used).
sim_rate = 312 # integration substeps per second
n_seconds = 4 # simulated duration in integer seconds
gravity = (0.0, -9.81, 0.0)
fps = 24 # .obj export rate
target_compliance = 0.0005 # compliance of the ground-truth "target" sim
compliance = 0.0001 # compliance of the "guess" sim
target_offset = (0.0, 0.0, 0.0) # initial position offset of the target object
offset = (0.0, 0.0, 0.0) # initial position offset of the guess object
obj = cloth(10, 10, corners, stretch | shear | bending)
collision_mode = projection # projection | constraints(compliance)
colliders = [ halfspace((0.0, -5.0, 0.0), (0.0, 1.0, 0.0)) ]
export_obj = true # write per-frame target_*.obj / guess_*.obj into animation/
experiment = compliance_optimization(50)
optimizer = momentum(1e-8, 0.8)
loss = mse_frames_trajectory(24)
Field notes:
- obj — object specification:
chain(N [, pin_mode])— a chain of N particles in a line. Pin mode (default:corners):none(free fall),corners/row(both pin the top particle).cloth(W, H, pin_mode [, constraints])— W×H grid. Pin mode (default:corners):none(free fall),corners(two top corners),row(entire first row). Optionalconstraintsfilters which types to include (default: all). Options:stretch,shear,bending, separated by|(e.g.,stretch | bending). Stretch constraints are always required.
- collision_mode — how to handle collisions (default:
projection):projection— post-step geometric correction via colliderproject().constraints(c)— integrated into the Jacobi solver as collision constraints with compliancec;
- colliders — a list
[ ... ]of collision primitives. An empty list[](or omitting the field) means no collisions. Each entry isname(args):halfspace((ox, oy, oz), (nx, ny, nz))— a plane through point(ox, oy, oz)with outward normal(nx, ny, nz); particles are kept on the normal side.sphere((cx, cy, cz), r)— a sphere centered at(cx, cy, cz)with radiusr; particles are kept outside.
- experiment — what to run:
forward_simulation— just runs the target and guess forward sims (writes the.objframes if exported enabled).compliance_gradient—dL/dcompliancex0_gradient—dL/d(initial positions).single_step_jacobian(step)— the per-step Jacobiandx⁺/dx⁻at updatestep.compliance_optimization(iters)— gradient-descent fit of compliance to the target foriterssteps (usesoptimizer).loss_scan_compliance(min_compl, max_compl, sub_steps)— sweeps compliance uniformly frommin_compltomax_complinsub_stepsincrements, printing the loss at each value (useful for visualizing the loss landscape).
- optimizer — descent rule, only for optimization experiments:
GD(lr)momentum(lr, beta)ADAM(lr, beta1, beta2, epsilon)
- loss — trajectory-matching error:
mse_final_position(only final frame)mse_full_trajectory(every single step)mse_frames_trajectory(fps)(frames sampled atfps)
The per-frame .obj files written to animation/ (when export_obj = true) are viewed via animation/animator.blend.
Requirements
- Blender 4.3 — the file is saved in 4.3 (EEVEE Next).
- Stop-motion-OBJ add-on, installed and enabled. The import script calls its
loadSequenceFromMeshFiles(Python modulemesh_sequence_controller).
Steps
- Run a sim with
export_obj = truesoanimation/holds the frames (guess_*.obj,target_*.obj). - Open
animation/animator.blend. - The file embeds a script that registers a DiffXPBD panel in the 3D Viewport sidebar (press
N). - In the DiffXPBD panel click Import sequences (operator
diffxpbd.import_sequences): it loads theguess_*andtarget_*.objsequences fromanimation/as Stop-motion-OBJ mesh sequences. Scrub the timeline to play.





