← All notes

Game Buoyancy Physics: Comprehensive Technical Notes

These notes cover the physics, math, and implementation patterns needed to build a boat-on-Gerstner-waves system for a game. Each section is self-contained but builds on the others.


1. Buoyancy Spring Models in Games

The Two Main Approaches

Sample-Point / Voxel Buoyancy ("Spring" Approach)

The most common game-friendly method. You place discrete sample points (also called "pontoons," "floaters," or "voxels") on the rigid body and compute buoyancy independently at each one.

Algorithm: 1. Subdivide the object into N sample points (or voxels). 2. Each frame, for each sample point, check if it is below the water surface. 3. If submerged, compute an upward force proportional to the submersion depth. 4. Apply that force at the sample point's world position via AddForceAtPosition().

The per-voxel force is typically:

archimedesForce = waterDensity * voxelVolume * -Physics.gravity

Where voxelVolume = totalVolume / numVoxels and totalVolume = mass / objectDensity.

This is essentially a spring system: the deeper the point goes below the waterline, the greater the restoring force. The "spring stiffness" is implicitly waterDensity * g * crossSectionalArea.

Volume Displacement (Mesh-Based Archimedes)

For higher fidelity (e.g. realistic ship simulators), compute the actual submerged volume by intersecting the hull mesh with the water surface mesh. The buoyancy force equals waterDensity * submergedVolume * g, applied at the centroid of the submerged volume.

The "surfacic" variant (used in Just Cause 3, described by Jacques Kerner) skips volume computation entirely: it calculates hydrostatic pressure forces on each submerged triangle and sums their linear and angular contributions. This is more robust because it doesn't require closing the mesh into a watertight volume.

Comparison

Aspect Sample Points Mesh-Based
Implementation Simple; 10-50 lines of code Complex; triangle-water intersection
Performance O(N) where N = sample count O(T) where T = triangle count
Accuracy Coarse; gaps between points Smooth; continuous waterline
Torque quality Depends on point placement Naturally correct from pressure distribution
Best for Floating crates, barrels, arcade boats Realistic ship sims, large vessels
Tuning Easy (move points, tweak density) Harder (mesh quality matters)

Why Naive Springs Cause Problems

A spring-based buoyancy system is mathematically a mass-spring-damper. Without damping, it oscillates indefinitely. With discrete timesteps (FixedUpdate at 50Hz), deep submersion creates enormous forces that overshoot, causing the object to rocket upward. This is the classic "shooting out" problem (see section 4).

Key Sources


2. Damping in Buoyancy Systems

Why Damping is Essential

Without damping, a floating object is a simple harmonic oscillator: it bobs up and down forever. Real water dissipates energy through viscous drag, wave radiation, and turbulence. In games, we approximate this with explicit damping forces.

Linear Drag

F_drag = -b * v

Where b is the damping coefficient and v is velocity. This is the textbook damped harmonic oscillator model. It allows clean analytical solutions and a well-defined critical damping point.

The damped oscillator equation: m*a + b*v + k*x = 0

Three regimes based on damping ratio zeta = b / (2 * sqrt(k * m)): - zeta < 1 (underdamped): oscillates with exponentially decaying amplitude - zeta = 1 (critically damped): returns to equilibrium as fast as possible without overshooting - zeta > 1 (overdamped): returns slowly without oscillating

Critical Damping Formula

b_critical = 2 * sqrt(k * m)

Or equivalently: b_critical = 2 * m * omega_n where omega_n = sqrt(k/m) is the natural frequency.

For buoyancy, k (the effective spring stiffness) is approximately waterDensity * g * waterplaneArea, where waterplaneArea is the cross-sectional area of the object at the waterline.

Quadratic Drag

F_drag = -0.5 * rho * Cd * A * |v| * v

This is the physically correct fluid drag model (used in the Kerner boat model). It scales with velocity squared, which means: - At low speeds: very little damping (objects can oscillate) - At high speeds: very strong damping (quickly kills violent motions)

Important: a quadratically-damped oscillator is never critically damped or overdamped. It always oscillates, just with rapidly decreasing amplitude. The oscillation frequency remains approximately equal to the natural frequency.

Practical Game Implementation

The Vertex Fragment buoyancy guide uses two pragmatic damping forces per voxel:

  1. depthScale = Clamp01(waterHeight - samplePosition.y) -- Reduces force near the surface, preventing jitter at the waterline.

  2. localDampingForce = velocity * dampingCoefficient * mass -- Velocity-proportional damping that opposes motion.

The combined force per voxel:

buoyantForce = archimedesForce * depthScale + localDampingForce

Making Buoyancy Feel "Heavy"

Key Sources


3. Mass-Independent Buoyancy

Real Physics: Archimedes' Principle

F_buoyancy = rho_fluid * V_displaced * g

This force depends on: - The density of the fluid (not the object) - The volume of fluid displaced (not the object's mass) - Gravitational acceleration

The object's mass determines the gravitational force: F_gravity = m * g. At equilibrium, these balance:

rho_fluid * V_submerged * g = m * g

The g cancels:

rho_fluid * V_submerged = m

So: V_submerged / V_total = rho_object / rho_fluid

An object with half the density of water floats with half its volume submerged, regardless of its total mass. A 1kg block of wood and a 1000kg log of the same wood density both float at the same relative depth.

The Game Dev Problem

In many naive implementations, buoyancy force is computed as a fixed upward force or a spring constant. When you change the object's mass (e.g., loading cargo onto a boat), the boat either sinks like a rock or floats impossibly high -- you have to re-tune.

The Solution: Density-Based Formulation

Instead of tuning a "buoyancy strength" parameter, derive everything from density:

object_density = mass / volume  # or set density directly and derive mass
total_volume = mass / object_density
volume_per_sample = total_volume / num_samples

# Per sample point, when submerged:
buoyancy_force = water_density * volume_per_sample * gravity * submersion_factor

This way: - Doubling the mass doubles the gravity force AND doubles the total volume, which doubles the buoyancy force - The equilibrium depth remains the same - No re-tuning needed

Should Forces Be Proportional to Mass?

Buoyancy force: NO. It should be proportional to displaced volume (and therefore implicitly to the object's own volume, not its mass).

Damping force: YES (usually). Making damping proportional to mass keeps the damping ratio constant: damping = coefficient * mass * velocity. This way a heavier object damps at the same rate and doesn't become underdamped.

Gravity: inherently proportional to mass (F = mg).

Practical Pattern

density = 500  # kg/m^3 (lighter than water = floats)
volume = mass / density
archimedes_per_point = (water_density * (volume / N) * g) * submersion_ratio

If you change mass from 100kg to 200kg: - Volume goes from 0.2m^3 to 0.4m^3 - Buoyancy per point doubles - Weight doubles - Equilibrium depth stays the same

Key Sources


4. The "Shooting Out" Problem

What Happens

When an object is pushed deep underwater and released: 1. Every sample point is fully submerged, generating maximum buoyancy force. 2. Total upward force far exceeds gravity. 3. The object accelerates upward very fast. 4. By the time it reaches the surface, it has enormous velocity. 5. It rockets out of the water, sometimes flying high into the air.

This is the discrete-timestep equivalent of a stiff spring diverging: the Euler integration accumulates too much energy because the force is huge and the timestep is too large.

Root Cause

The buoyancy force as a spring has stiffness k = rho_water * g * A (where A is the waterplane area). For a large, light object, this gives a very high natural frequency omega = sqrt(k/m). When omega * dt > 1 (where dt is the physics timestep), the system becomes unstable.

Solutions (from least to most sophisticated)

1. Clamp Submersion Depth

submersion = clamp(water_height - sample_y, 0, max_depth)
force = water_density * voxel_volume * g * (submersion / max_depth)

The max_depth caps how much force can build up. Once fully submerged, force is constant regardless of how much deeper you push it. This is physically correct -- Archimedes' force doesn't increase for a fully submerged object.

2. Clamp the Force Magnitude

max_force = water_density * total_volume * g  # force when fully submerged
force = min(computed_force, max_force / num_samples)

3. Velocity-Based Drag

if velocity.y > 0 and position.y < water_surface:
    drag = -0.5 * rho * Cd * area * velocity.y^2 * sign(velocity.y)

Quadratic drag kills high velocities very effectively. An object at 10 m/s experiences 100x more drag than at 1 m/s.

4. Added Mass Effect

In reality, a submerged object has to push water out of the way, which effectively increases its inertia. For submarines, added mass can be 1-2x the actual mass. Implementing this prevents unrealistic acceleration:

effective_mass = mass + added_mass_coefficient * water_density * volume
acceleration = net_force / effective_mass

This is rarely implemented in games but dramatically improves realism.

5. Higher Physics Tick Rate

Reducing Time.fixedDeltaTime from 0.02 (50Hz) to 0.005 (200Hz) gives the integrator more chances to correct, preventing huge single-frame accelerations. This is expensive but is the "correct" solution from a numerical standpoint.

6. Non-Linear Force Curve

Instead of linear force = k * depth, use a curve that saturates:

force = max_force * (1 - exp(-alpha * depth))

This gives linear-ish behavior near the surface but asymptotically approaches max_force at depth.

For an arcade boat game, use: depth clamping (solution 1) + velocity-based drag (solution 3) + moderate damping. This is what most shipped games do.

Key Sources


5. Torque from Buoyancy Sample Points

The Core Mechanism

When you call AddForceAtPosition(force, position) in a physics engine, it automatically decomposes the force into: 1. Linear force = the full force vector (accelerates the center of mass) 2. Torque = cross(position - centerOfMass, force) (rotates the body)

If a buoyancy sample point is to the left of the center of mass and is submerged deeper than the point on the right, the left point generates more upward force, creating a torque that rolls the body to the right. This naturally produces: - Pitching (bow up/down) from fore/aft buoyancy differences - Rolling (side to side) from port/starboard buoyancy differences - Self-righting behavior when the center of gravity is below the center of buoyancy

Sample Point Placement Strategy

For a boat, a typical layout:

        [Bow]
       /     \
  [Port-Mid]  [Starboard-Mid]
       \     /
  [Port-Stern] [Starboard-Stern]

5-7 points is usually sufficient. More points = smoother response but more computation.

Rules of thumb: - Place points at the waterline, not at the bottom of the hull - Spread them as wide as possible for roll stability - Spread them as far fore/aft as possible for pitch stability - Use the same number on each side for symmetry

Center of Buoyancy vs Center of Mass

A floating object is stable when the metacentric height (distance from center of mass to metacenter) is positive. In practice: - Center of mass below center of buoyancy = always stable (pendulum-like) - Center of mass above center of buoyancy = can still be stable if the waterplane area is large enough (most boats)

In game terms: lower the Rigidbody.centerOfMass to improve stability. A common trick is to set it 0.5-1.0 units below the geometric center.

Controlling Angular Response

Too much angular response (twitchy rolling): - Increase angular drag on the Rigidbody - Spread sample points wider apart (increases the waterplane moment of inertia) - Increase the inertia tensor values (see section 9)

Too little angular response (boat doesn't react to waves): - Decrease angular drag - Move sample points closer together - Ensure sample points are actually at different heights on the wave surface

The Residual Torque Problem

When using few sample points or triangles, applying force at the triangle center (instead of the true center of pressure, which is lower on the triangle due to hydrostatic pressure gradient) introduces a "residual torque" error. For high-fidelity simulations, the true center of pressure must be calculated. For arcade games, this is negligible.

Key Sources


6. Wave Forces on Floating Objects

Gerstner Wave Recap

A Gerstner wave displaces a surface point from its rest position (x0, 0, z0) to:

x = x0 - sum_i( (D_i.x * A_i / k_i) * sin(k_i * dot(D_i, x0z0) - omega_i * t) )
z = z0 - sum_i( (D_i.z * A_i / k_i) * sin(k_i * dot(D_i, x0z0) - omega_i * t) )
y = sum_i( (A_i / k_i) * cos(k_i * dot(D_i, x0z0) - omega_i * t) )

Where: - D_i = normalized direction of wave i - A_i = amplitude - k_i = wave number = 2*pi / wavelength - omega_i = angular frequency = sqrt(g * k_i) - Q_i = steepness parameter (0 = sine wave, 1 = sharp crests)

The key insight: Gerstner waves move vertices both vertically AND horizontally. Vertices crowd together at crests and spread apart in troughs, creating the characteristic sharp crests and flat troughs.

The Surface Normal

The normal vector at any point on the Gerstner wave surface:

tangent = (1 - sum(D_x^2 * Q * sin(f)),  sum(D_x * Q * cos(f)),  -sum(D_x*D_z * Q * sin(f)))
binormal = (-sum(D_x*D_z * Q * sin(f)),  sum(D_z * Q * cos(f)),  1 - sum(D_z^2 * Q * sin(f)))
normal = normalize(cross(binormal, tangent))

Where f = k * dot(D, pos) - omega * t for each wave component.

How Wave Normals Create Push Forces

When the water surface is tilted (wave slope), the buoyancy force acts along the surface normal, not straight up. This creates a horizontal component:

buoyancy_direction = surface_normal  // not just Vector3.up!
force = buoyancy_magnitude * buoyancy_direction

On a wave slope tilted 20 degrees from horizontal: - Vertical component: F * cos(20) = ~94% of F (still mostly holds the object up) - Horizontal component: F * sin(20) = ~34% of F (pushes the object sideways/forward)

This naturally causes objects to drift toward wave troughs and get pushed along the wave direction when riding a slope.

Stokes Drift vs Gerstner: A Subtlety

Pure Gerstner waves produce closed circular particle orbits with no net drift. Real ocean waves exhibit Stokes drift (a slow net forward movement). If you want floating objects to gradually drift in the wave direction, you need to add an explicit drift velocity:

stokes_drift = (omega * k * A^2) * wave_direction  // approximate Stokes drift

Or simply apply a constant lateral force in the dominant wave direction as a game-design hack.

Sampling Wave Height for Buoyancy

The main challenge: to compute buoyancy, you need the water height at the object's position. But Gerstner waves displace vertices horizontally, so the displaced position (x, z) doesn't correspond to the original grid position (x0, z0) in a simple way.

Approaches: 1. Ignore horizontal displacement -- Sample height using the object's xz position as if it were the rest-space position. Introduces some error but works for small amplitudes. 2. Iterative correction -- Start with the object's xz, compute the Gerstner displacement, use the result to back-solve for the rest-space coordinate. 2-3 iterations usually suffice. 3. GPU readback -- Sample the displaced mesh on GPU, read back height values. Used by Crest ocean system.

Key Sources


7. Boat Motor/Thrust Models for Arcade Games

Where to Apply Thrust

The thrust force application point determines how the boat turns:

At the stern (water jet / propeller position):

Vector3 thrustForce = -waterJetTransform.forward * power;
rb.AddForceAtPosition(thrustForce, waterJetTransform.position);

This naturally creates a turning torque when the jet/propeller direction is angled. The Habrador tutorial implements this with a rotatable water jet nozzle.

At the center of mass:

rb.AddForce(transform.forward * power);

Pure forward acceleration with no turning torque. Steering must be handled separately.

For arcade games, applying at the stern with a rotatable thrust direction gives the most natural feel. The boat pivots around its center of mass, with the stern swinging outward during turns -- exactly how real boats feel.

Steering Models

Model A: Rudder Torque (Realistic) A rudder deflects water flow, creating a side force at the stern. The force is proportional to speed squared and rudder angle:

rudder_force = 0.5 * rho * Cd * A_rudder * v^2 * sin(rudder_angle)
torque = rudder_force * distance_from_CoM

At low speeds, this gives almost no steering -- realistic but frustrating for players.

Model B: Direct Torque (Arcade)

rb.AddTorque(Vector3.up * steeringInput * turnRate);

Instant, speed-independent turning. Responsive but unrealistic.

Model C: Hybrid (Recommended for Arcade)

// Base torque always available (minimum steering)
baseTorque = steeringInput * minTurnRate;
// Speed-dependent torque (increases with speed)
speedTorque = steeringInput * turnRate * (speed / maxSpeed);
// Final torque
rb.AddTorque(Vector3.up * (baseTorque + speedTorque));

Speed Curves

Real boats have a non-linear speed curve: - Displacement mode (low speed): drag is roughly proportional to speed - Transition (hull speed): wave-making resistance creates a "hump" - Planing mode (high speed): boat rises on its own bow wave, drag decreases then stabilizes

For arcade games, use a simple model:

thrust = enginePower * throttleInput
drag = linearDrag * speed + quadraticDrag * speed^2
acceleration = (thrust - drag) / mass

The terminal velocity is where thrust = drag. You can tune linearDrag and quadraticDrag to get the speed curve you want.

Lateral (Keel) Drag

Boats resist sideways motion much more than forward motion. This is critical for making boats feel like boats:

local_velocity = inverse_transform(world_velocity)
# High drag for sideways, low drag for forward
lateral_drag = local_velocity.x * lateral_drag_coefficient
forward_drag = local_velocity.z * forward_drag_coefficient
# lateral_drag_coefficient >> forward_drag_coefficient

Aaron Vanderpoel's "Simple 2D Boat Physics" uses the angle between velocity and heading to modulate drag: 0 degrees (forward) = minimum drag; 90 degrees (sideways) = maximum drag.

An alternative: "reflect" the velocity off the keel line to convert lateral motion into forward motion, simulating how a keel redirects drift.

Making It Feel Right

Key Sources


8. Kinematic vs Dynamic Rigidbody Approaches

Dynamic Rigidbody (Force-Driven)

The boat is a physics object driven by forces (buoyancy, thrust, drag). The physics engine resolves all motion.

// In FixedUpdate:
foreach (var point in samplePoints) {
    float depth = waterHeight - point.position.y;
    if (depth > 0) {
        Vector3 force = Vector3.up * waterDensity * voxelVolume * gravity * Mathf.Clamp01(depth);
        rb.AddForceAtPosition(force, point.position);
    }
}

Pros: - Physically correct interactions (collisions, stacking, explosions affect the boat) - Natural wave response from buoyancy forces - Other objects can push/bump the boat

Cons: - Harder to tune (many interacting parameters) - Can be unstable (shooting-out problem) - Boat behavior depends on physics timestep - Hard to guarantee the boat stays on the water (can sink or fly)

Kinematic Rigidbody (Position-Driven)

The boat's position and rotation are directly set from wave data each frame. No physics forces involved.

// In Update or FixedUpdate:
float waterY = SampleWaveHeight(transform.position);
Vector3 waterNormal = SampleWaveNormal(transform.position);

transform.position = new Vector3(transform.position.x, waterY + offset, transform.position.z);
transform.up = Vector3.Lerp(transform.up, waterNormal, smoothing * Time.deltaTime);

Pros: - Perfectly stable (never sinks, never shoots out) - Predictable, easy to tune - Always matches the wave surface exactly - No physics timestep issues

Cons: - No physics interaction (other objects can't push it) - Movement feels "on rails" - No natural bobbing or inertia - Collisions require custom handling

Hybrid Approaches

Approach A: Kinematic Position + Dynamic Rotation Set the vertical position kinematically from the wave height, but let physics handle rotation via buoyancy torques.

Approach B: Dynamic with Constraints Use dynamic physics but add strong constraints: - Clamp vertical position to stay within X meters of the wave surface - Clamp angular velocity to prevent wild spinning - If boat goes too deep, teleport it back up with zero vertical velocity

Approach C: Blend Factor

float dynamicWeight = 0.7f; // 70% physics, 30% wave tracking
float kinematicY = SampleWaveHeight(pos);
float physicsY = rb.position.y;
float blendedY = Mathf.Lerp(kinematicY, physicsY, dynamicWeight);

Approach D: State Switching Use dynamic rigidbody normally, but switch to kinematic during scripted events (cutscenes, docking, entering triggers).

Warning: switching between kinematic and dynamic can cause a 1-frame hitch. Set velocity/angular velocity appropriately when transitioning.

Recommendation for Arcade Boat Game

Use dynamic physics for the primary boat with: - Robust damping and clamping to prevent instability - A "safety net" that gently pulls the boat toward the wave surface if it drifts too far - Kinematic mode only for non-player ambient boats in the distance

Key Sources


9. Angular Damping and Inertia Tensor

Inertia Tensor: The Rotational Analog of Mass

The inertia tensor is a 3x3 matrix (stored as a Vector3 of diagonal elements + a rotation quaternion in Unity) that describes how hard it is to spin an object around each axis.

torque = I * angular_acceleration

For a box of mass m with dimensions (w, h, d):

I_x = (m/12) * (h^2 + d^2)   // roll axis
I_y = (m/12) * (w^2 + d^2)   // yaw axis
I_z = (m/12) * (w^2 + h^2)   // pitch axis

For a boat, you typically want: - I_x (roll): moderate -- boat should roll with waves but not too easily - I_y (yaw): large -- boat should resist spinning (hard to turn) - I_z (pitch): moderate-to-large -- boat shouldn't pitch wildly

Unity's Angular Drag

Unity applies angular damping each physics step:

angularVelocity *= 1.0 / (1.0 + fixedDeltaTime * angularDrag)

This is exponential decay. Higher angularDrag = faster slowdown. A value of 0.5-2.0 is typical for boats.

For more control, you can apply custom angular damping per-axis:

Vector3 localAngVel = transform.InverseTransformDirection(rb.angularVelocity);
localAngVel.x *= (1 - rollDamping * Time.fixedDeltaTime);   // roll
localAngVel.y *= (1 - yawDamping * Time.fixedDeltaTime);    // yaw
localAngVel.z *= (1 - pitchDamping * Time.fixedDeltaTime);  // pitch
rb.angularVelocity = transform.TransformDirection(localAngVel);

Setting the Inertia Tensor Manually

Unity auto-computes the inertia tensor from attached colliders. For boats, the auto-computed values are often wrong because the collider shape doesn't match the actual mass distribution.

rb.inertiaTensor = new Vector3(rollInertia, yawInertia, pitchInertia);
rb.inertiaTensorRotation = Quaternion.identity;

Guidelines: - To make roll slower: increase I_x - To make yaw (turning) slower: increase I_y - To make pitch slower: increase I_z - Multiply the auto-computed values by 2-10x for a heavier feel

PD Controller for Angular Control

For precise rotational control (e.g., aligning a boat to the wave normal), use David Wu's stable backwards PD controller:

float g = 1f / (1f + kd * dt + kp * dt * dt);
float ksg = kp * g;
float kdg = (kd + kp * dt) * g;

// For rotation:
Quaternion errorQuat = targetRotation * Quaternion.Inverse(currentRotation);
errorQuat.ToAngleAxis(out float angle, out Vector3 axis);
if (angle > 180f) angle -= 360f;

Vector3 angularError = axis * angle * Mathf.Deg2Rad;
Vector3 angularVelocityError = targetAngularVelocity - currentAngularVelocity;

Vector3 torque = angularError * ksg + angularVelocityError * kdg;
// Transform through inertia tensor:
torque = rb.inertiaTensorRotation * Vector3.Scale(rb.inertiaTensor,
    Quaternion.Inverse(rb.inertiaTensorRotation) * torque);

rb.AddTorque(torque);

For critical damping with this formulation:

kp = (6 * frequency)^2 * 0.25
kd = 4.5 * frequency * damping   // damping = 1.0 for critical

Key Sources


10. Reference Implementations

Boat Attack (Unity Technologies)

What: Official Unity demo project for Universal Render Pipeline, featuring a boat racing vertical slice.

Water system: Custom water package with Gerstner-like waves rendered in URP. The water system is a separate Unity package.

Buoyancy: Uses sample-point buoyancy with the URP water surface. The boat physics are force-driven (dynamic rigidbody).

Status: Last updated for Unity 2020 LTS. The water system is not officially supported.

Links: - GitHub: Unity-Technologies/BoatAttack - GitHub: Unity-Technologies/boat-attack-water


Sea of Thieves (Rare)

What: Open-world pirate game with extensive ocean sailing.

Water: FFT-based ocean simulation (believed to use the Phillips spectrum). Presented at SIGGRAPH 2018 ("The Technical Art of Sea of Thieves"). The ocean runs primarily on GPU with stylized visual treatment.

Boat physics: Boats are rigid bodies with buoyancy forces from the wave surface. The water does NOT react to the boat (one-way coupling). Boat physics are computed on CPU, with the water surface often simplified to a plane for buoyancy calculations.

Notable: The game also features real-time GPU-based surface fluid simulation for incidental water on the ship deck, and a physically-based rope-and-pulley rendering system.

Links: - The Technical Art of Sea of Thieves (ResearchGate) -- SIGGRAPH 2018 talk - Sea of Thieves Water Recreation in UE (80.lv)


Assassin's Creed IV: Black Flag (Ubisoft)

What: Open-world pirate game with naval combat on the AnvilNext engine.

Water: Multi-layer ocean system with 3 frequency bands (low = swell, mid = waves, high = detail via tessellation on next-gen). Beaufort-scale controllable. Level designers can tune ocean settings per-mission.

Boat physics: Ships use "buoyancy spheres" -- large spheres positioned along the hull (4+ per side). The number and size vary by ship class. Having separate spheres on each side enables roll/rocking from sideways waves. The system is described as "a compromise between real physics and needs for real time."

Wave-ship interaction: Ships push waves down and waves push back on ships. A "DepthMask" technique renders an invisible depth plane to prevent water from appearing inside the hull.

Particles: Individually-simulated, physically-tracked, individually-lit spray and rain particles.

Links: - AC4 Ocean Technology (GameDev.net Discussion) - Black Flag Waterplane (Simon Schreibt) - 5 Things About AC4 Tech (FXGuide) - AC4 Graphics Analysis (GamersNexus)


Just Cause 3 (Avalanche Studios) -- Jacques Kerner's Model

What: The most detailed publicly-documented game boat physics model, published as a two-part article series on Game Developer (formerly Gamasutra).

Approach: Triangle-based surfacic method. The hull mesh is intersected with the water height field each frame. Each submerged triangle receives hydrostatic pressure forces, viscous resistance (ITTC 1957 correlation), pressure drag, and slamming forces.

Performance target: < 1ms per boat. Supports boats from 2m jet skis to 50m corvettes.

Key insight: Computing the correct force application point on each triangle (below the triangle center due to hydrostatic pressure gradient) is critical for correct torque.

Links: - Water Interaction Model Part 1 (Game Developer) - Water Interaction Model Part 2 (Game Developer)


Crest Ocean System (Wave Harmonic)

What: A class-leading open-source water system for Unity (URP and HDRP).

Buoyancy options: 1. SimpleFloatingObject -- matches position/rotation to wave surface (kinematic-style) 2. BoatProbes -- multiple force points with proper buoyancy forces (dynamic) 3. BoatAlignNormal -- rudimentary boat with engine/rudder

Wave sampling: GPU compute shader queries wave data at arbitrary positions. Supports SampleHeightHelper and SampleFlowHelper for C# usage. A MinSpatialLength parameter filters out waves smaller than the boat.

CPU fallback: Baked collision data for headless servers without GPU.

Links: - GitHub: wave-harmonic/crest - Crest Docs: Collision & Buoyancy - Crest Docs: Watercraft


Habrador's Unity Boat Tutorial

What: A complete, step-by-step tutorial for implementing realistic boat physics in Unity using real physics equations.

Covers: Mesh-based buoyancy (triangle intersection), water jet propulsion, rudder/steering, wave generation, and all the supporting math. Inspired by Jacques Kerner's Game Developer article.

Links: - Habrador Boat Tutorial - GitHub: Habrador/Unity-Boat-physics-Tutorial


Other Notable Resources


  1. Start simple: Single sample-point buoyancy with spring + damping (sections 1, 2)
  2. Fix the basics: Add depth clamping and drag to prevent shooting-out (section 4)
  3. Make it mass-independent: Use density-based formulation (section 3)
  4. Add multiple points: Get torque from off-center forces (section 5)
  5. Add waves: Implement Gerstner waves and sample height (section 6)
  6. Add thrust: Implement motor and steering (section 7)
  7. Tune feel: Adjust angular damping, inertia tensor, lateral drag (sections 9, 7)
  8. Polish: Decide kinematic vs dynamic tradeoffs (section 8)
  9. Study references: Read Jacques Kerner's articles, explore Crest/BoatAttack code (section 10)