crispigt.

Dampning

2026-05-09
C++C#BuoyancyPhysicsUnityRigidbody

After fixing the torque bugs from Phase 3, the cube finally reacted to damping. But even then, Unity's built-in angularDamping produced subtle wobble artifacts that didn't look right. Hirae et al. explain why in their paper, and the fix is a bit more involved than just multiplying by a factor.

Why damping angular velocity is wrong

A rigid body has an inertia tensor, three values (Ix,Iy,Iz)(I_x, I_y, I_z) describing how hard it is to rotate around each of its principal axes. For a cube they're equal, but for most shapes they're not. A long rod is easy to spin around its long axis, hard around the short ones.

The physical relationship between angular momentum LL and angular velocity ω\omega is:

L=IωL = I \cdot \omega

LL is the conserved quantity, the "amount of spin." ω\omega is how fast it's actually rotating. They're linked through the inertia tensor.

Unity's angularDamping scales ω\omega directly: ω *= 0.98. The problem is that the same 2% reduction in ω\omega removes very different amounts of LL depending on the inertia of that axis. On a high-inertia axis you're removing a lot of momentum; on a low-inertia axis, almost none. The damping is uneven, which creates visible axis-dependent drift.

Hirae's fix, damp LL instead. Scaling LL by a constant α\alpha removes the same fraction of spin energy uniformly across all axes, then recompute ω=I1L\omega = I^{-1} L.

The implementation

This lives on the C# side since it needs Unity's inertiaTensor and inertiaTensorRotation. The DLL returns raw buoyancy torque, C# owns the rotational integration.

In Start(), initialize L=IωL = I \cdot \omega from whatever spin the rigidbody starts with, and disable Unity's built-in damping so it doesn't fight the manual integration:

rb.angularDamping = 0f;
Quaternion q = rb.inertiaTensorRotation;
Vector3 w = rb.angularVelocity;
L = q * Vector3.Scale(rb.inertiaTensor, Quaternion.Inverse(q) * w);

For a cube starting at rest this is just (0,0,0)(0,0,0), but the formula is correct for any initial spin.

In FixedUpdate():

float dt = Time.fixedDeltaTime;
L += lastTorque * dt;
L *= angularAlpha;

Quaternion q = rb.inertiaTensorRotation;
Vector3 L_local = Quaternion.Inverse(q) * L;
Vector3 omega_local;
omega_local.x = L_local.x / rb.inertiaTensor.x;
omega_local.y = L_local.y / rb.inertiaTensor.y;
omega_local.z = L_local.z / rb.inertiaTensor.z;
rb.angularVelocity = q * omega_local;

The conversion dance is necessary because the inertia tensor is diagonal only in its own local frame. In world space the division doesn't make sense, so you rotate LL in, divide component-wise, then rotate ω\omega back out:

Lworldq1Llocal÷IωlocalqωworldL_\text{world} \xrightarrow{q^{-1}} L_\text{local} \xrightarrow{\div I} \omega_\text{local} \xrightarrow{q} \omega_\text{world}

Setting rb.angularVelocity directly replaces rb.AddTorque, Unity still integrates the quaternion orientation from angular velocity each physics step, we just supply that velocity ourselves.

Linear drag only in water

Unity's linearDamping applies everywhere, including in air. Added a drag force conditioned on whether the buoyancy force is nonzero, a cheap proxy for "is anything submerged":

rb.AddForce(lastForce, ForceMode.Force);
if (lastForce.sqrMagnitude > 0.0001f)
    rb.AddForce(-linearDrag * rb.linearVelocity, ForceMode.Force);

This gives normal unity drag in air and viscous drag F=cvF = -c\,v in water. A linearDrag of around 3 settles the cube quickly without feeling overdamped.

Result

With angular damping on LL and water-only linear drag, the cube now falls, hits the surface, oscillates a few times and settles at its stable tilt. The angularAlpha slider in the inspector lets me tune the feel, 0.98 is about right for water, 0.95 feels more like heavy oil.

Next up we have Gerstner waves, so we can test on non planar water.