Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Docs/APIChanges.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Changes that make some state saved through SaveBinaryState from a prior version

## Changes between v5.5.0 and latest

* 20260417 - `SixDOFConstraint` velocity motors with `MotorSettings::mSpringSettings` damping > 0 and no stiffness/frequency now act as soft acceleration-mode velocity drives (`a = -damping * v_err`) rather than hard velocity constraints (where the damping was previously ignored). Motors with damping == 0 are unchanged.
* 20260410 - Fixed contact callbacks for body with motion quality LinearCast vs a soft body. Previously, the contacts would be reported accidentally through the regular ContactListener. Now they're properly reported through the SoftBodyContactListener. (63765d19bae439ea4a9f93d186d6f1d94029229b)
* 20260307 - *SBS* - Added support for HeightFieldShapeSettings::mBitsPerSample > 8 which adds 1 byte to the binary serialization format and renders it incompatible with previous saved data. (449b645b71a7a47aa0d7bdcb5f9c197f1ddff5b0)
* 20253012 - Added interface to run compute shaders on the GPU with implementations for DX12, Vulkan and Metal. These interfaces can be disabled by setting JPH_USE_DX12, JPH_USE_VK and JPH_USE_MTL to OFF. To build on macOS, you'll need to have dxc and spirv-cross installed. The easiest way to install them is by installing the Vulkan SDK. (5ac132df689fbf88da618181b0f1f73fca8bb1b4)
Expand Down
1 change: 1 addition & 0 deletions Docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For breaking API changes see [this document](https://github.com/jrouwe/JoltPhysi
* Added support for RISC-V RVV, the SIMD extension for RISC-V.
* Added JPH_BUILD_SHARED_LIBS cmake variable to determine whether to build static or shared libraries (it defaults to BUILD_SHARED_LIBS). This allows embedding Jolt as a static library within a shared library.
* Simulation stats: Added tracking of collision steps. This way we can know by how many steps we need to divide the numbers to get averages per step.
* Added a mass-normalized damping-only drive to `SixDOFConstraint` velocity motors. If `MotorSettings::mSpringSettings` has damping > 0 and no stiffness/frequency, the motor now produces `a = -damping * v_err` (soft, mass-independent velocity drive) instead of the hard velocity constraint. Motors with damping == 0 behave as before. New helpers `SpringPart::CalculateSpringPropertiesWithDamping`, `AngleConstraintPart::CalculateConstraintPropertiesWithDamping` and `AxisConstraintPart::CalculateConstraintPropertiesWithDamping` expose the underlying math.
* Various performance and memory optimizations.

### Bug Fixes
Expand Down
18 changes: 18 additions & 0 deletions Jolt/Physics/Constraints/ConstraintPart/AngleConstraintPart.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ class AngleConstraintPart
mSpringPart.CalculateSpringPropertiesWithStiffnessAndDamping(inDeltaTime, inv_effective_mass, inBias, inC, inSpringSettings.mStiffness, inSpringSettings.mDamping, mEffectiveMass);
}

/// Calculate properties used during the functions below for a mass-normalized damping-only drive: a = -inDamping * w_err
/// See SpringPart::CalculateSpringPropertiesWithDamping.
/// @param inDeltaTime Time step
/// @param inBody1 The first body that this constraint is attached to
/// @param inBody2 The second body that this constraint is attached to
/// @param inWorldSpaceAxis The axis of rotation along which the constraint acts (normalized)
/// @param inBias Bias term (b) for the constraint impulse: lambda = J v + b
/// @param inDamping Damping coefficient in 1/s
inline void CalculateConstraintPropertiesWithDamping(float inDeltaTime, const Body &inBody1, const Body &inBody2, Vec3Arg inWorldSpaceAxis, float inBias, float inDamping)
{
float inv_effective_mass = CalculateInverseEffectiveMass(inBody1, inBody2, inWorldSpaceAxis);

if (inv_effective_mass == 0.0f)
Deactivate();
else
mSpringPart.CalculateSpringPropertiesWithDamping(inDeltaTime, inv_effective_mass, inBias, inDamping, mEffectiveMass);
}

/// Deactivate this constraint
inline void Deactivate()
{
Expand Down
20 changes: 20 additions & 0 deletions Jolt/Physics/Constraints/ConstraintPart/AxisConstraintPart.h
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,26 @@ class AxisConstraintPart
mSpringPart.CalculateSpringPropertiesWithStiffnessAndDamping(inDeltaTime, inv_effective_mass, inBias, inC, inSpringSettings.mStiffness, inSpringSettings.mDamping, mEffectiveMass);
}

/// Calculate properties used during the functions below for a mass-normalized damping-only drive: a = -inDamping * v_err
/// See SpringPart::CalculateSpringPropertiesWithDamping.
/// @param inDeltaTime Time step
/// @param inBody1 The first body that this constraint is attached to
/// @param inR1PlusU See equations above (r1 + u)
/// @param inBody2 The second body that this constraint is attached to
/// @param inR2 See equations above (r2)
/// @param inWorldSpaceAxis Axis along which the constraint acts (normalized, pointing from body 1 to 2)
/// @param inBias Bias term (b) for the constraint impulse: lambda = J v + b
/// @param inDamping Damping coefficient in 1/s
inline void CalculateConstraintPropertiesWithDamping(float inDeltaTime, const Body &inBody1, Vec3Arg inR1PlusU, const Body &inBody2, Vec3Arg inR2, Vec3Arg inWorldSpaceAxis, float inBias, float inDamping)
{
float inv_effective_mass = CalculateInverseEffectiveMass(inBody1, inR1PlusU, inBody2, inR2, inWorldSpaceAxis);

if (inv_effective_mass == 0.0f)
Deactivate();
else
mSpringPart.CalculateSpringPropertiesWithDamping(inDeltaTime, inv_effective_mass, inBias, inDamping, mEffectiveMass);
}

/// Deactivate this constraint
inline void Deactivate()
{
Expand Down
23 changes: 23 additions & 0 deletions Jolt/Physics/Constraints/ConstraintPart/SpringPart.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,29 @@ class SpringPart
}
}

/// Calculate spring properties for a mass-normalized damping-only drive: a = -inDamping * v_err
/// Produces constraint force F = -m_eff * inDamping * v_err where m_eff = 1 / inInvEffectiveMass.
///
/// @param inDeltaTime Time step
/// @param inInvEffectiveMass Inverse effective mass K
/// @param inBias Bias term (b) for the constraint impulse: lambda = J v + b
/// @param inDamping Damping coefficient in 1/s. If <= 0, falls back to a bias-only setup.
/// @param outEffectiveMass On return, this contains the new effective mass K^-1
inline void CalculateSpringPropertiesWithDamping(float inDeltaTime, float inInvEffectiveMass, float inBias, float inDamping, float &outEffectiveMass)
{
if (inDamping > 0.0f)
{
// Convert acceleration-mode damping to force-space damping: c = m_eff * damping
float c = inDamping / inInvEffectiveMass;
CalculateSpringPropertiesHelper(inDeltaTime, inInvEffectiveMass, inBias, 0.0f, 0.0f, c, outEffectiveMass);
}
else
{
outEffectiveMass = 1.0f / inInvEffectiveMass;
CalculateSpringPropertiesWithBias(inBias);
}
}

/// Returns if this spring is active
inline bool IsActive() const
{
Expand Down
24 changes: 20 additions & 4 deletions Jolt/Physics/Constraints/SixDOFConstraint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -424,8 +424,17 @@ void SixDOFConstraint::SetupVelocityConstraint(float inDeltaTime)
break;

case EMotorState::Velocity:
mMotorTranslationConstraintPart[i].CalculateConstraintProperties(*mBody1, r1_plus_u, *mBody2, r2, translation_axis, -mTargetVelocity[i]);
break;
{
// Acceleration-mode velocity motor: mSpringSettings has damping > 0 but no stiffness.
// Produces a soft, mass-normalized velocity drive (a = -damping * v_err).
// Falls back to a hard velocity constraint (bounded by the motor force limit) otherwise.
const SpringSettings &spring_settings = mMotorSettings[i].mSpringSettings;
if (!spring_settings.HasStiffness() && spring_settings.mDamping > 0.0f)
mMotorTranslationConstraintPart[i].CalculateConstraintPropertiesWithDamping(inDeltaTime, *mBody1, r1_plus_u, *mBody2, r2, translation_axis, -mTargetVelocity[i], spring_settings.mDamping);
else
mMotorTranslationConstraintPart[i].CalculateConstraintProperties(*mBody1, r1_plus_u, *mBody2, r2, translation_axis, -mTargetVelocity[i]);
break;
}

case EMotorState::Position:
{
Expand Down Expand Up @@ -555,8 +564,15 @@ void SixDOFConstraint::SetupVelocityConstraint(float inDeltaTime)
break;

case EMotorState::Velocity:
mMotorRotationConstraintPart[i].CalculateConstraintProperties(*mBody1, *mBody2, rotation_axis, -mTargetAngularVelocity[i]);
break;
{
// See matching comment on the translation motor case above.
const SpringSettings &spring_settings = mMotorSettings[axis].mSpringSettings;
if (!spring_settings.HasStiffness() && spring_settings.mDamping > 0.0f)
mMotorRotationConstraintPart[i].CalculateConstraintPropertiesWithDamping(inDeltaTime, *mBody1, *mBody2, rotation_axis, -mTargetAngularVelocity[i], spring_settings.mDamping);
else
mMotorRotationConstraintPart[i].CalculateConstraintProperties(*mBody1, *mBody2, rotation_axis, -mTargetAngularVelocity[i]);
break;
}

case EMotorState::Position:
{
Expand Down
60 changes: 60 additions & 0 deletions UnitTests/Physics/SixDOFConstraintTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,66 @@ TEST_SUITE("SixDOFConstraintTests")
}
}

// Test that a velocity motor with damping-only SpringSettings acts as a soft, mass-normalized
// acceleration-mode drive: a = -damping * v_err. Velocity follows v_new = (v_old + dt * c * v_target) / (1 + dt * c)
// independently of body mass/inertia. Exercises both translational and rotational motor paths and both spring modes
// (HasStiffness() is mode-agnostic, so either mode with frequency/stiffness == 0 should hit the acceleration-mode path).
TEST_CASE("TestSixDOFMotorVelocityAcceleration")
{
const float cDamping = 4.0f;
const float cTargetVelocity = 5.0f;

// Cover all translation and rotation motor axes
for (int axis = 0; axis < 6; ++axis)
{
const bool is_rotation = axis >= 3;
// Run against two sphere sizes; identical motion curves prove mass/inertia independence
for (float radius : { 0.5f, 1.0f })
{
// Test both spring modes
for (int mode = 0; mode < 2; ++mode)
{
PhysicsTestContext context;
context.ZeroGravity();
Body &body = context.CreateSphere(RVec3::sZero(), radius, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
body.GetMotionProperties()->SetLinearDamping(0.0f);
body.GetMotionProperties()->SetAngularDamping(0.0f);

SixDOFConstraintSettings settings;
settings.mPosition1 = settings.mPosition2 = RVec3::sZero();
// Either mode works — HasStiffness() checks the shared union slot, so both resolve to "no stiffness/frequency, damping > 0"
settings.mMotorSettings[axis].mSpringSettings = (mode == 0)
? SpringSettings(ESpringMode::StiffnessAndDamping, 0.0f, cDamping)
: SpringSettings(ESpringMode::FrequencyAndDamping, 0.0f, cDamping);
SixDOFConstraint &constraint = context.CreateConstraint<SixDOFConstraint>(Body::sFixedToWorld, body, settings);
SixDOFConstraintSettings::EAxis eaxis = (SixDOFConstraintSettings::EAxis)axis;
constraint.SetMotorState(eaxis, EMotorState::Velocity);
Vec3 target = Vec3::sZero();
target.SetComponent(axis % 3, cTargetVelocity);
if (is_rotation)
constraint.SetTargetAngularVelocityCS(target);
else
constraint.SetTargetVelocityCS(target);

// Predicted velocity. Implicit Euler soft-constraint form from 'Soft Constraints: Reinventing
// The Spring' - Erin Catto - GDC 2011 (page 32), k = 0 limit, mass-normalized damping c:
// v_new = (v_old + dt * c * v_target) / (1 + dt * c)
float v = 0.0f;
float dt = context.GetDeltaTime();
for (int i = 0; i < 120; ++i)
{
v = (v + dt * cDamping * cTargetVelocity) / (1.0f + dt * cDamping);
context.SimulateSingleStep();
Vec3 expected = Vec3::sZero();
expected.SetComponent(axis % 3, v);
Vec3 actual = is_rotation ? body.GetAngularVelocity() : body.GetLinearVelocity();
CHECK_APPROX_EQUAL(expected, actual, 1.0e-5f);
}
}
}
}
}

// Test combination of locked rotation axis with a 6DOF constraint
TEST_CASE("TestSixDOFLockedRotation")
{
Expand Down