Skip to content

Conversation

@RJ
Copy link
Contributor

@RJ RJ commented Sep 3, 2024

Objective

FixedJoints currently apply entity1's Rotation to entity2.

This change allows you set a fixed rotation offset to entity2's Rotation, relative to entity1.

Here's the output difference in the fixed_joint_2d.rs example, if you make this change:

-    commands.spawn(FixedJoint::new(anchor, object).with_local_anchor_1(Vector::X * 100.0));
+    commands.spawn(
+        FixedJoint::new(anchor, object)
+            .with_local_anchor_1(Vector::X * 100.0)
+            .with_rotation_offset(PI / 4.), // <--- NEW
+    );

Original output

example.without.rotation.offset.mov

New output with rotation offset applied

example.with.rotation.offset.of.quarter-pi.mov

Caveat

I've only visually confirmed this looks right in 2d.
Hopefully the 3d version of my get_rotation_difference is correct.


Changelog

  • Add FixedJoint::with_rotation_offset

@Jondolf Jondolf added C-Feature A new feature, making something new possible A-Dynamics Relates to rigid body dynamics: motion, mass, constraint solving, joints, CCD, and so on labels Sep 4, 2024
@RJ
Copy link
Contributor Author

RJ commented Sep 4, 2024

this change is so i can use a fixed joint to land on asteroids like this, maintaining the upright ship rotation:

landing-on-asteroid.mov

Jondolf added a commit that referenced this pull request Aug 20, 2025
# Objective

Closes #198.
Closes #199.
Closes #254.
Supersedes #507.
Supersedes #511.
Partially addresses #440.

Avian's joints are in need of a rework. Some problems include:

- Full local frames are not supported (anchor + basis, see #254)
- Accessing joint forces in a generic way is non-trivial, making joint breakage (#199) trickier to implement
- Joint components store implementation details like Lagrange multipliers and pre-step data
- Joints are strictly tied to XPBD, which has patenting concerns (#440), and makes it difficult to implement custom joint solvers
- Debug rendering is the same for each joint type, with no support for rendering limits or other relevant data
- Documentation is somewhat poor, with no images to visualize the different joint types
- A lot of the APIs are not very polished

A future goal is to also replace the XPBD joint solver, but in the meanwhile, we can improve the API and code organization by fixing the above problems.

This PR is massive (sorry!) and could be split into smaller chunks, but I wanted to do a fairly comprehensive pass to fix all the low-hanging fruit, and a lot of the changes are somewhat entangled, so doing (most of) it at once felt the easiest / most productive here.

## Solution

### Joint Frames

`FixedJoint`, `PrismaticJoint`, `RevoluteJoint`, and `SphericalJoint` now support a full `JointFrame` that contains a `JointAnchor` and `JointBasis`. They are enums with `Local` and `FromGlobal` variants, allowing the initialization of local frames using world-space values.

```rust
// Use a world-space anchor of (5, 2), and rotate the second body's local frame by 45 degrees.
commands.spawn((
    RevoluteJoint::new(body1, body2)
        .with_anchor(Vec2::new(5.0, 2.0))
        .with_local_basis2(Rot2::degrees(45.0))
));
```

While the basis is now configurable, it is still also possible to configure axes like the "hinge axis" for a 3D `RevoluteJoint` or the "twist axis" for a `SphericalJoint`. This is unlike some engines where e.g. the x-axis is the de-facto slider axis for prismatic joints, and the local frames *must* be rotated to get other behavior. The motivation behind my approach is (1) user-friendliness, (2) minimizing implicit defaults, and (3) being more agnostic to different joint setups and coordinate systems.

### Joint Damping

Previously, each joint type stored its own damping coefficients. However, the actual damping logic is not joint-specific. and not all joints need damping. Thus, it is now handled by a separate `JointDamping` component with `linear` and `angular` properties.

```rust
commands.spawn((
    DistanceJoint::new(body1, body2),
    JointDamping {
        linear: 0.1,  // Linear damping
        angular: 0.1, // Angular damping
    },
));
```

### Joint Forces

The details of joint forces are solver-specific. However, ultimately users will tend to want to read a force vector and torque. This has now been generalized as a `JointForces` component that the constraint solver writes to and users can read. It is *not* added automatically and must be added manually for the desired joint entities.

```rust
commands.spawn((
    RevoluteJoint::new(body1, body2),
    JointForces::new(),
));
```

An example of where this may be useful is breaking joints when their forces or torques exceed some threshold:

```rust
fn break_joints(
    mut commands: Commands,
    query: Query<(Entity, &JointForces), Without<JointDisabled>>,
) {
    for (entity, joint_forces) in &query {
        if joint_forces.force().length() > BREAK_THRESHOLD {
            // Break the joint by adding the `JointDisabled` component.
            // Alternatively, you could simply remove the joint component or despawn the entity.
            commands.entity(entity).insert(JointDisabled);
        }
    }
}
```

### Joint Debug Rendering

Joints now use a `ConstraintDebugRendering` trait for their debug rendering. This makes custom rendering logic for each joint type more doable.

For now, the old debug rendering is still used, but the infrastructure is there to e.g. visualize limits for each joint type.

### Solver Reorganization and XPBD

All XPBD logic is now contained within `dynamics::solver::xpbd`, gated behind the `xpbd_joints` feature. The actual joint API has been extracted out into `dynamics::joints`, and the solver internal data has been moved out into separate solver data components. This makes Avian's joints much more solver agnostic, and allows usage without XPBD!

This involved some broader restructuring to do cleanly. Some of the big changes include:

- There is a new `SolverPlugins` plugin group that adds the default solver's plugins.
- XPBD system sets from `SubstepSolverSet` have been extracted to a separate `XpbdSolverSet` enum.
- XPBD systems are now initialized by an `XpbdSolverPlugin`.

The joint traits and XPBD helpers were also changed a bit, the `Joint` trait was removed, and `Dominance` is now stored for `SolverBodyInertia` and used for computing relative dominance for constraints.

### Polish and Documentation

I did a lot of work on polishing up the joint APIs and documentation some more. Notably:

- Renamed `entity1` and `entity2` to `body1` and `body2`
- Renamed `free_axis` to `slider_axis` for `PrismaticJoint` (more accurate and matches some other engines)
- Renamed `aligned_axis` to `hinge_axis` for `RevoluteJoint` (maybe clearer and matches some other engines)
- Made more methods `const` where possible
- Vastly improved joint documentation, added more code examples, and added SVGs for illustration
- Miscellaneous other improvements

---

## Migration Guide

### Joints

- Joint APIs are now in `dynamics::joints` instead of `dynamics::solver::joints`
- The `Joint` trait has been removed in favor of the `EntityConstraint` trait and helper methods on the joint types themselves
- Renamed `entity1` and `entity2` to `body1` and `body2`
- Renamed `free_axis` to `slider_axis` for `PrismaticJoint`
- Renamed `aligned_axis` to `hinge_axis` for `RevoluteJoint`
- Renamed `with_local_anchor_1`, `with_local_anchor_2`, `local_anchor_1`, and `local_anchor_2` to `with_local_anchor1`, `with_local_anchor2`, `local_anchor1`, and `local_anchor2`
- The `local_anchor1` and `local_anchor2` methods now return an `Option`
- The `FixedJoint`, `PrismaticJoint`, `RevoluteJoint`, and `SphericalJoint` now store a full `JointFrame` (anchor + basis) for each body instead of just local anchors
- Removed `swing_axis` from `SphericalJoint`; just set the `twist_axis`, and the swing limit cone will be oriented accordingly
- Removed damping properties and methods from joint types in favor of the `JointDamping` component
- Removed force properties and methods from joint types in favor of the `JointForces` component

### Solver Reorganization and XPBD

- XPBD logic is now contained within `dynamics::solver::xpbd`, gated behind the `xpbd_joints` feature
- XPBD system sets from `SubstepSolverSet` have been extracted to a separate `XpbdSolverSet` enum
- XPBD systems are now initialized by an `XpbdSolverPlugin`
- `SupstepSolverSet` has a new `Damping` system set for constraint velocity damping

### Custom XPBD Constraints

- `XpbdConstraint` now has a `SolverData` associated type for a solver data component implementing the `XpbdConstraintSolverData` trait. This is taken by `prepare` and `solve`.
- `apply_positional_lagrange_update` has been removed. Use `apply_positional_impulse` instead.
- Most methods that previously returned forces or torques now return Lagrange multiplier updates.
- See the `custom_constraint` example for a functional demonstration of implementing a custom constraint.
@Jondolf
Copy link
Member

Jondolf commented Aug 20, 2025

Hi, thanks, and sorry that I've neglected this for so long 😅

Instead of supporting this for just the FixedJoint, I wanted to do this consistently for all joints that should support a "rotation offset", including revolute and prismatic joints. A lot of engines do this with a local frame (anchor + basis) for each body, see for example Rapier, Box2D (main branch), Bullet, PhysX...

I've implemented that in #803, so I'm closing this now. In follow-ups, we can consider more APIs like a helper for a rotation offset or even automatically computing the local frames based on the current transforms of the bodies.

@Jondolf Jondolf closed this Aug 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Dynamics Relates to rigid body dynamics: motion, mass, constraint solving, joints, CCD, and so on C-Feature A new feature, making something new possible

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants