After fixing the torque bugs in "The actual buoyancy math", 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
The inertia tensor is three values for how hard the body is to rotate around each principal axis. Equal for a cube, not for most shapes, a long rod is easy to spin around its long axis and hard around the short ones. Angular momentum and angular velocity are linked through it,
and is the conserved quantity, not .
Unity's angularDamping scales directly, ω *= 0.98. The same 2% off removes a lot of on a high-inertia axis and almost none on a low-inertia one, so the damping is uneven and you get visible axis-dependent drift.
Hirae's fix, damp instead. Scaling by a constant removes the same fraction of spin energy uniformly across all axes, then recompute .
integrating L in C#
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 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 , 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 in, divide component-wise, then rotate back out:
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 in water. A linearDrag of around 3 settles the cube quickly without feeling overdamped.
the cube settles
With angular damping on 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.