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.
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.
| 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) |
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).
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.
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
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.
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.
The Vertex Fragment buoyancy guide uses two pragmatic damping forces per voxel:
depthScale = Clamp01(waterHeight - samplePosition.y) -- Reduces force near the surface, preventing jitter at the waterline.
localDampingForce = velocity * dampingCoefficient * mass -- Velocity-proportional damping that opposes motion.
The combined force per voxel:
buoyantForce = archimedesForce * depthScale + localDampingForce
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.
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.
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
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).
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
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.
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.
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.
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
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
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.
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
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.
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 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.
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.
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.
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.
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.
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));
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.
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.
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)
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
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.
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
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 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);
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
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
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
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)
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)
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)
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
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