Skip to content

Commit 3f193be

Browse files
committed
well those are some quite latent bugs oops
1 parent b754cee commit 3f193be

2 files changed

Lines changed: 97 additions & 72 deletions

File tree

BepuPhysics/Constraints/AreaConstraint.cs

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ private static void ApplyImpulse(in Vector<float> inverseMassA, in Vector<float>
8989
}
9090

9191
[MethodImpl(MethodImplOptions.AggressiveInlining)]
92-
static void ComputeJacobian(in Vector3Wide positionA, in Vector3Wide positionB, in Vector3Wide positionC, out Vector<float> normalLength, out Vector3Wide negatedJacobianA, out Vector3Wide jacobianB, out Vector3Wide jacobianC)
92+
static void ComputeJacobian(in Vector3Wide positionA, in Vector3Wide positionB, in Vector3Wide positionC,
93+
out Vector<float> normalLength,
94+
out Vector3Wide negatedJacobianA, out Vector3Wide jacobianB, out Vector3Wide jacobianC,
95+
out Vector<float> contributionA, out Vector<float> contributionB, out Vector<float> contributionC,
96+
out Vector<float> inverseJacobianLength)
9397
{
9498
//Area of a triangle with vertices a, b, and c is:
9599
//||ab x ac|| * 0.5
@@ -118,52 +122,69 @@ static void ComputeJacobian(in Vector3Wide positionA, in Vector3Wide positionB,
118122
Vector3Wide.CrossWithoutOverlap(normal, ab, out jacobianC);
119123
//Similar to the volume constraint, we could create a similar expression for jacobianA, but it's cheap to just do a couple of adds.
120124
Vector3Wide.Add(jacobianB, jacobianC, out negatedJacobianA);
125+
//Normalize the jacobian to unit length. The jacobians are cross products of edges with the unit normal,
126+
//giving magnitudes proportional to edge lengths (~L). Without normalization, the inverse effective mass
127+
//scales with L², causing the accumulated impulse scale to vary with triangle size and making warm starting unstable.
128+
//Normalizing gives a unit-length effective jacobian J_eff = inverseJacobianLength * J_raw.
129+
//The inverse effective mass becomes a weighted average of inverse masses (always bounded),
130+
//and the physical impulse is identical because the scaling factors cancel in the solve.
131+
Vector3Wide.Dot(negatedJacobianA, negatedJacobianA, out contributionA);
132+
Vector3Wide.Dot(jacobianB, jacobianB, out contributionB);
133+
Vector3Wide.Dot(jacobianC, jacobianC, out contributionC);
134+
var jacobianLengthSquared = contributionA + contributionB + contributionC;
135+
//Guard against the degenerate case where edges are parallel/antiparallel (triangle collapses to a line).
136+
jacobianLengthSquared = Vector.Max(new Vector<float>(1e-14f), jacobianLengthSquared);
137+
inverseJacobianLength = MathHelper.FastReciprocalSquareRoot(jacobianLengthSquared);
121138
}
122139

123140
public static void WarmStart(
124141
in Vector3Wide positionA, in QuaternionWide orientationA, in BodyInertiaWide inertiaA,
125-
in Vector3Wide positionB, in QuaternionWide orientationB, in BodyInertiaWide inertiaB,
126-
in Vector3Wide positionC, in QuaternionWide orientationC, in BodyInertiaWide inertiaC,
142+
in Vector3Wide positionB, in QuaternionWide orientationB, in BodyInertiaWide inertiaB,
143+
in Vector3Wide positionC, in QuaternionWide orientationC, in BodyInertiaWide inertiaC,
127144
ref AreaConstraintPrestepData prestep, ref Vector<float> accumulatedImpulses, ref BodyVelocityWide wsvA, ref BodyVelocityWide wsvB, ref BodyVelocityWide wsvC)
128145
{
129-
ComputeJacobian(positionA, positionB, positionC, out _, out var negatedJacobianA, out var jacobianB, out var jacobianC);
130-
ApplyImpulse(inertiaA.InverseMass, inertiaB.InverseMass, inertiaC.InverseMass, negatedJacobianA, jacobianB, jacobianC, accumulatedImpulses, ref wsvA, ref wsvB, ref wsvC);
146+
ComputeJacobian(positionA, positionB, positionC, out _, out var negatedJacobianA, out var jacobianB, out var jacobianC, out _, out _, out _, out var inverseJacobianLength);
147+
//The accumulated impulse is in unit-jacobian space. Replay through J_eff = inverseJacobianLength * J_raw.
148+
//Since |J_eff| = 1, the warm start magnitude is bounded by |accumulated| * max(invMass), same as a distance constraint.
149+
ApplyImpulse(inertiaA.InverseMass, inertiaB.InverseMass, inertiaC.InverseMass, negatedJacobianA, jacobianB, jacobianC, inverseJacobianLength * accumulatedImpulses, ref wsvA, ref wsvB, ref wsvC);
131150
}
132151

133152
public static void Solve(in Vector3Wide positionA, in QuaternionWide orientationA, in BodyInertiaWide inertiaA, in Vector3Wide positionB, in QuaternionWide orientationB, in BodyInertiaWide inertiaB, in Vector3Wide positionC, in QuaternionWide orientationC, in BodyInertiaWide inertiaC, float dt, float inverseDt, ref AreaConstraintPrestepData prestep, ref Vector<float> accumulatedImpulses, ref BodyVelocityWide wsvA, ref BodyVelocityWide wsvB, ref BodyVelocityWide wsvC)
134153
{
135-
ComputeJacobian(positionA, positionB, positionC, out var normalLength, out var negatedJacobianA, out var jacobianB, out var jacobianC);
136-
137-
Vector3Wide.Dot(negatedJacobianA, negatedJacobianA, out var contributionA);
138-
Vector3Wide.Dot(jacobianB, jacobianB, out var contributionB);
139-
Vector3Wide.Dot(jacobianC, jacobianC, out var contributionC);
140-
141-
//Protect against singularity by padding the jacobian contributions. This is very much a hack, but it's a pretty simple hack.
142-
//Less sensitive to tuning than attempting to guard the inverseEffectiveMass itself, since that is sensitive to both scale AND mass.
143-
144-
//Choose an epsilon based on the target area. Note that area ~= width^2 and our jacobian contributions are things like (ac x N) * (ac x N).
145-
//Given that N is perpendicular to AC, ||(ac x N)|| == ||ac||, so the contribution is just ||ac||^2. Given the square, it's proportional to area and the area is a decent epsilon source.
146-
var epsilon = 5e-4f * prestep.TargetScaledArea;
147-
contributionA = Vector.Max(epsilon, contributionA);
148-
contributionB = Vector.Max(epsilon, contributionB);
149-
contributionC = Vector.Max(epsilon, contributionC);
150-
var inverseEffectiveMass = contributionA * inertiaA.InverseMass + contributionB * inertiaB.InverseMass + contributionC * inertiaC.InverseMass;
154+
//The area jacobians (ac x normal, normal x ab) have magnitude ~L (edge lengths).
155+
//Without normalization, the inverse effective mass scales with L², making the accumulated impulse
156+
//scale vary with triangle size and causing warm start instability.
157+
//We normalize to a unit-length effective jacobian: J_eff = J_raw * inverseJacobianLength, where inverseJacobianLength = 1/|J_raw|.
158+
//The inverse effective mass becomes a weighted average of inverse masses (always bounded),
159+
//keeping the accumulated impulse well-scaled across substeps.
160+
//
161+
//The position error is scaled to match: error = (targetArea - area) * inverseJacobianLength.
162+
//The physical impulse (inverseJacobianLength * csi applied through J_raw) is identical to the raw formulation
163+
//because the inverseJacobianLength factors cancel.
164+
ComputeJacobian(positionA, positionB, positionC, out var normalLength, out var negatedJacobianA, out var jacobianB, out var jacobianC, out var contributionA, out var contributionB, out var contributionC, out var inverseJacobianLength);
165+
var inverseJacobianLengthSquared = inverseJacobianLength * inverseJacobianLength;
166+
167+
//With the unit-length jacobian, the inverse effective mass is a weighted average of inverse masses, always bounded.
168+
//Guard against degenerate configurations (e.g. triangle collapsed to a line) where all jacobian contributions are zero,
169+
//which would cause a division by zero when computing the effective mass.
170+
var inverseEffectiveMass = Vector.Max(new Vector<float>(1e-14f),
171+
inverseJacobianLengthSquared * (contributionA * inertiaA.InverseMass + contributionB * inertiaB.InverseMass + contributionC * inertiaC.InverseMass));
151172

152173
SpringSettingsWide.ComputeSpringiness(prestep.SpringSettings, dt, out var positionErrorToVelocity, out var effectiveMassCFMScale, out var softnessImpulseScale);
153174

154175
var effectiveMass = effectiveMassCFMScale / inverseEffectiveMass;
155176
//Compute the position error and bias velocities. Note the order of subtraction when calculating error- we want the bias velocity to counteract the separation.
156-
var biasVelocity = (prestep.TargetScaledArea - normalLength) * positionErrorToVelocity;
177+
var biasVelocity = (prestep.TargetScaledArea - normalLength) * inverseJacobianLength * positionErrorToVelocity;
157178

158179
//csi = projection.BiasImpulse - accumulatedImpulse * projection.SoftnessImpulseScale - (csiaLinear + csiaAngular + csibLinear + csibAngular);
159180
Vector3Wide.Dot(negatedJacobianA, wsvA.Linear, out var negatedVelocityContributionA);
160181
Vector3Wide.Dot(jacobianB, wsvB.Linear, out var velocityContributionB);
161182
Vector3Wide.Dot(jacobianC, wsvC.Linear, out var velocityContributionC);
162-
var csv = velocityContributionB + velocityContributionC - negatedVelocityContributionA;
183+
var csv = inverseJacobianLength * (velocityContributionB + velocityContributionC - negatedVelocityContributionA);
163184
var csi = (biasVelocity - csv) * effectiveMass - accumulatedImpulses * softnessImpulseScale;
164185
accumulatedImpulses += csi;
165186

166-
ApplyImpulse(inertiaA.InverseMass, inertiaB.InverseMass, inertiaC.InverseMass, negatedJacobianA, jacobianB, jacobianC, csi, ref wsvA, ref wsvB, ref wsvC);
187+
ApplyImpulse(inertiaA.InverseMass, inertiaB.InverseMass, inertiaC.InverseMass, negatedJacobianA, jacobianB, jacobianC, inverseJacobianLength * csi, ref wsvA, ref wsvB, ref wsvC);
167188
}
168189

169190
public static bool RequiresIncrementalSubstepUpdates => false;

0 commit comments

Comments
 (0)