diff --git a/Docs/APIChanges.md b/Docs/APIChanges.md index bd779befb..ab2bc52a3 100644 --- a/Docs/APIChanges.md +++ b/Docs/APIChanges.md @@ -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) diff --git a/Docs/ReleaseNotes.md b/Docs/ReleaseNotes.md index 77f7661a5..fa4dd2e19 100644 --- a/Docs/ReleaseNotes.md +++ b/Docs/ReleaseNotes.md @@ -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 diff --git a/Jolt/Physics/Constraints/ConstraintPart/AngleConstraintPart.h b/Jolt/Physics/Constraints/ConstraintPart/AngleConstraintPart.h index f7493102e..0e834a37a 100644 --- a/Jolt/Physics/Constraints/ConstraintPart/AngleConstraintPart.h +++ b/Jolt/Physics/Constraints/ConstraintPart/AngleConstraintPart.h @@ -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() { diff --git a/Jolt/Physics/Constraints/ConstraintPart/AxisConstraintPart.h b/Jolt/Physics/Constraints/ConstraintPart/AxisConstraintPart.h index 66373fb47..7f1189de6 100644 --- a/Jolt/Physics/Constraints/ConstraintPart/AxisConstraintPart.h +++ b/Jolt/Physics/Constraints/ConstraintPart/AxisConstraintPart.h @@ -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() { diff --git a/Jolt/Physics/Constraints/ConstraintPart/SpringPart.h b/Jolt/Physics/Constraints/ConstraintPart/SpringPart.h index 0a8a4a973..b698c0c3e 100644 --- a/Jolt/Physics/Constraints/ConstraintPart/SpringPart.h +++ b/Jolt/Physics/Constraints/ConstraintPart/SpringPart.h @@ -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 { diff --git a/Jolt/Physics/Constraints/SixDOFConstraint.cpp b/Jolt/Physics/Constraints/SixDOFConstraint.cpp index 6ec73d689..716159d0c 100644 --- a/Jolt/Physics/Constraints/SixDOFConstraint.cpp +++ b/Jolt/Physics/Constraints/SixDOFConstraint.cpp @@ -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: { @@ -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: { diff --git a/UnitTests/Physics/SixDOFConstraintTests.cpp b/UnitTests/Physics/SixDOFConstraintTests.cpp index 18f8aecda..045997d95 100644 --- a/UnitTests/Physics/SixDOFConstraintTests.cpp +++ b/UnitTests/Physics/SixDOFConstraintTests.cpp @@ -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(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") {