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 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 and angular velocity is:
is the conserved quantity, the "amount of spin." is how fast it's actually rotating. They're linked through the inertia tensor.
Unity's angularDamping scales directly: ω *= 0.98. The problem is that the same 2% reduction in removes very different amounts of 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 instead. Scaling by a constant removes the same fraction of spin energy uniformly across all axes, then recompute .
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 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.
Result
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.