← All notes

Water Physics System

A probe-based buoyancy and wave simulation built on top of Stylized Water 3's Gerstner wave solver. The system is split into force calculation (WaterForces), force application (ApplyWaterForces variants), and boat-specific dynamics (BoatDynamics + BoatDriver).


Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                        Per-FixedUpdate Flow                         │
│                                                                     │
│  BoatDriver (player/AI input)                                       │
│       │ calls BoatDynamics.SetInput(throttle, turning)              │
│       ▼                                                             │
│  BoatDynamics.FixedUpdate()                                         │
│       │ reads WaterForces.LastVelocity/SubmergedRatio               │
│       │   (from previous frame's GetForces call)                    │
│       │ applies thrust, rudder, planing, banking via                │
│       │   WaterForces.AddForce() / AddTorque()                      │
│       ▼                                                             │
│  ApplyWaterForcesBase coroutine (yields WaitForFixedUpdate)         │
│       │ calls WaterForces.GetForces(PhysicsState)                   │
│       │   → CalculateForces() [cached per frame]                    │
│       │   → runs ForceFilters                                       │
│       │ applies result to Rigidbody / Transform                     │
│       ▼                                                             │
│  Physics step complete                                              │
└─────────────────────────────────────────────────────────────────────┘

Core Design Principles

  1. Separation of concernsWaterForces only calculates forces; an ApplyWaterForces* sibling applies them.
  2. Frame cachingGetForces() computes once per fixed frame; multiple consumers read the same result.
  3. Late apply — The applier runs as a coroutine yielding WaitForFixedUpdate, ensuring all FixedUpdate producers (BoatDynamics, external systems) have submitted their forces before integration.
  4. Probe-based — Buoyancy, drag, and wave push are all derived from a configurable set of sample points (probes) attached to the object.

Component Reference

WaterForces

File: Assets/05 Scripts/WaterPhysics/WaterForces.cs

The central force calculator. Does not move the object — it only produces a ForceResult (linear force, torque, submerged ratio).

Key Concepts

Concept Description
Probes Local-space sample points that detect submersion. Each probe has a localPos and a normalized weight.
Center of Mass A configurable centerOfMass offset used as the torque reference point.
Hull Depth The depth at which a probe is considered fully submerged. Buoyancy ramps linearly from 0 (at waterline) to 100% (at hullDepth).
Draft Vertical offset that raises or lowers the resting point relative to the water surface.
PhysicsState A struct of {position, rotation, velocity, angularVelocity} passed in by the applier each frame.

Force Calculation Pipeline (CalculateForces)

The method runs once per fixed frame and produces a ForceResult:

1. Gravity          →  F = mass × 9.81 × gravityMultiplier
2. Per-Probe Loop:
   a. Sample water height at probe world position
   b. Compute displacement = probe.y − waterHeight − draft
   c. If submerged (displacement < 0):
      - Buoyancy force (proportional to depth)
      - Vertical drag (opposing probe's vertical velocity)
      - Exit drag (extra drag when surfacing, scales with surface proximity)
      - Slamming force (stopping-force on rapid entry)
      - Torque from buoyancy around CoM
      - Wave push (PerProbe mode only)
3. CenterOfMass wave push (if using CenterOfMass mode)
4. Wave push post-processing pipeline:
   a. Smoothing (rolling average over N frames)
   b. Clamping (freeze direction/magnitude when force drops below threshold)
   c. Strength multiplier + angle-based multiplier
   d. Damping (exponential decay toward target)
5. Horizontal drag (anisotropic: forward vs lateral)
6. Angular drag (anisotropic: yaw vs pitch/roll)
7. Drain pending external forces (AddForce/AddTorque API)
8. Run ForceFilters

Buoyancy

Each submerged probe contributes an upward force proportional to how deep it is below the waterline:

submersionRatio = clamp(displacement / hullDepth, 0, 1)
probeForce = submersionRatio × buoyancyForce × weight

The force is applied at the probe's world position, which naturally creates a torque that rights the object. A probe deeper on one side pushes harder, rotating the hull level.

Vertical Drag

Opposes the probe's vertical velocity (including rotational contribution):

pointVel = velocity + angularVelocity × offsetFromCoM
probeForce -= pointVel.y × verticalDrag × weight
When `exitDrag > 0` and the probe is moving **upward**, additional drag is added that scales with surface proximity (1 at waterline → 0 when fully submerged). This simulates the "suction" effect of water resisting a hull pulling out.

Slamming

A stopping-force approach inspired by Avalanche's JC3 water physics:

if (downwardSpeed > threshold):
    intensity = pow(downwardSpeed / rampSpeed, slammingPower)
    stoppingForce = mass × downwardSpeed / dt
    probeForce += stoppingForce × intensity × weight

This is inherently stable — the force can never exceed what would halt the probe's downward motion in one frame, so it cannot overshoot.

Wave Push

Drives the object horizontally based on wave slope and vertical water velocity. Two quality modes:

Mode Extra Samples Description
Off 0 No wave push.
CenterOfMass 2 total Samples wave slope once at the center of mass. No torque.
PerProbe 2 per submerged probe Samples wave slope at every submerged probe. Generates torque.

Slope computation: For each sample point, the water surface normal is computed by sampling the height at the point and at two offset positions (+X and +Z by normalSampleOffset). The horizontal component of the normal gives the slope direction.

Velocity modes: - Absolute — Push scales with water's vertical velocity alone. Consistent strength. - Relative — Push scales with the difference between water velocity and the object's velocity. Weaker when buoyancy tracks the surface well.

Backside behaviour controls what happens when the water height is dropping (backside of a wave): - PositiveForce — Default: slope direction naturally produces a backward push. - NegativeForce — Multiplied by waveBacksideMultiplier (typically negative, creating a pull-back). - Diminish — Force zeroed.

Wave push post-processing pipeline:

  1. Smoothing — Rolling average over smoothingFrames frames. Reduces jitter.
  2. Clamping — When the smoothed force drops below wavePushMinMagnitude, holds the last valid magnitude and/or direction for up to wavePushClampFalloffFrames frames. Prevents wave push from flickering on/off near zero.
  3. Strength & angle multiplier — Multiplied by wavePushStrength, then optionally by an angle-based curve from WavePushAngleConfig (see below).
  4. Damping — Exponential decay (Lerp with 1 - e^(-speed × dt)) toward the target force. Smooths transitions without adding latency.

Note: This pipeline operates internally on wave push forces during CalculateForces(). After all forces (including wave push) are summed, the separate ForceFilter system runs on the combined ForceResult — see the Force Filters section below.

Horizontal Drag (Anisotropic)

Applied at the center of mass when any probe is submerged. Decomposes horizontal velocity into forward and lateral components relative to the object's heading:

fwdVel = Dot(horizontalVel, heading) × heading
latVel = horizontalVel − fwdVel

Each component is damped independently by forwardDrag and lateralDrag. Supports linear (F = -drag × v) and quadratic (F = -drag × |v| × v) modes.

If scaleDragWithSubmersion is true, drag scales with the submerged ratio (partial submersion = partial drag). Otherwise it's binary (any probe submerged = full drag).

Angular Drag (Anisotropic)

Resists rotation when submerged, split into yaw (Y axis) and tilt (X + Z axes):

localAngVel = Inverse(rot) × angularVelocity
localDragTorque = (-localAngVel.x × tiltDrag, -localAngVel.y × yawDrag, -localAngVel.z × tiltDrag)

Converted back to world space and added to the torque accumulator.

External Force API

Other systems can contribute forces each frame via:

These are drained (reset to zero) each frame after being incorporated into the result.

Force Filters

Post-processors that run after all forces are calculated. Stored in a [SerializeReference] array, enabling polymorphic serialization. Built-in filters:

Filter Purpose
CapsizingPreventionFilter Spring-damper that counteracts pitch/roll beyond a threshold angle. Uses SmoothStep to ramp engagement between min and max trigger angles.
ClampForces Hard caps on linear force and torque magnitudes.
ScaleForces Simple multiplier on force and torque (e.g., for difficulty scaling).
VerticalRangeFilter Safety net preventing the object from flying too high or sinking too deep. Applies a spring force plus velocity damping when outside the allowed range.

Custom filters subclass ForceFilter, mark [System.Serializable], and implement FilterForces(ref ForceResult, WaterForces).


ApplyWaterForcesBase

File: Assets/05 Scripts/WaterPhysics/ApplyWaterForcesBase.cs

Abstract base class for force appliers. Manages a coroutine that yields WaitForFixedUpdate then calls OnApplyForcesStep(dt). This ensures all FixedUpdate producers have finished before forces are integrated.

Shared features: - applyPosition / applyRotationAxisMask flags to selectively enable/disable individual axes. - dampingEpsilon — Tiny velocity decay to absorb floating-point drift (separate from water drag). - Teleport(pos, rot, resetMomentum) — Repositions the object instantly. - AddImpulse(Vector3) — Applies an instantaneous velocity change.

Variants

Applier Target Integration Notes
ApplyWaterForcesToDynamicRigidbody Non-kinematic Rigidbody AddForce / AddTorque Disables Rigidbody gravity (WaterForces handles it). Sets CoM and inertia.
ApplyWaterForcesToKinematicRigidbody Kinematic Rigidbody Manual via MovePosition / MoveRotation Includes overlap-depenetration collision. Can push dynamic bodies.
ApplyWaterForcesToTransform Transform (no Rigidbody) Manual Includes smooth visual interpolation in Update/LateUpdate.

ApplyWaterForcesManualBase

File: Assets/05 Scripts/WaterPhysics/ApplyWaterForcesManualBase.cs

Base for kinematic Rigidbody and Transform appliers. Performs explicit Euler integration:

  1. Linear: velocity += (force / mass) × dt, then position += velocity × dt
  2. Angular: Converts torque to local space, divides by per-axis inertia (inertia × mass), integrates angular velocity, and applies via axis-angle rotation to avoid gimbal lock.

Supports UnityStyle (1 - damping × dt) and Exponential (e^(-damping × dt)) damping modes.

ApplyWaterForcesToKinematicRigidbody — Collision

Uses an overlap-depenetration approach rather than sweep casting:

  1. After integrating forces, tests the final pose (position + rotation) against scene geometry using Physics.OverlapBoxNonAlloc.
  2. For each overlap, Physics.ComputePenetration computes the exact minimum separation vector.
  3. The object is pushed out along that vector (plus skinWidth).
  4. Velocity is deflected: the component going into the surface is removed and optionally bounced by bounciness.
  5. Runs up to maxCollisionIterations passes to handle multiple simultaneous contacts.
  6. Dynamic bodies encountered during resolution receive a proportional push impulse.

This catches rotation-induced collisions (e.g., bow swinging into a rock) that pure sweep-based approaches miss.

Additionally, when receiveCollisions is enabled, OnCollisionEnter/Stay callbacks from dynamic bodies hitting the kinematic boat are forwarded as forces into WaterForces.


WaterSampler

File: Assets/05 Scripts/WaterPhysics/System/WaterSampler.cs

Singleton that wraps Stylized Water 3's Gerstner wave computation. Key features:


BoatDynamics

File: Assets/05 Scripts/CharacterControl/Boat/Shared/BoatDynamics.cs

Concrete boat dynamics engine: thrust, steering, planing, banking, and anchor. Receives input from BoatDriver subclasses via SetInput(throttle, turning).

Input Processing

  1. Throttle smoothingMoveTowards with separate ramp-up and ramp-down rates.
  2. Turning inversion — When reversing (throttle < 0), turning input is inverted so the rudder feels natural.
  3. BoostSetBoostInput(thrust, torqueReduction) adds extra thrust and optionally reduces rudder torque (for boost mechanics).

Force Contributions (per FixedUpdate)

Force Description
Thrust direction × (curvedThrottle × thrustForce + accelBoost + boostThrust). Gated by submersion. Reversing scales by reverseThrust.
Rudder torque turning × rudderTorque × rudderCurve(normalizedSpeed) × throttleBoost × mass. The rudder speed curve reduces effectiveness at low speed.
Acceleration boost Extra thrust at low speed (evaluated from accelerationBoostCurve based on currentSpeed / maxSpeed).
Planing Pitch torque at speed — the bow rises as the boat planes. Evaluated from planingCurve.
Banking Cosmetic roll into turns. Smoothed with exponential decay.
Anchor spring When anchored, a spring-damper pulls the boat toward the anchor point. Slack radius allows free movement within range.

Stats (BoatStats ScriptableObject)

All tuning values come from a BoatStats SO. ApplyStats() copies values into runtime fields and overwrites WaterForces settings (drag, wave push, etc.) to keep everything in sync. This means the BoatStats SO is the single source of truth for a boat's physics parameters.

Max Speed Calculation

Linear drag:    maxSpeed = thrust / (forwardDrag × forceScale)
Quadratic drag: maxSpeed = sqrt(thrust / (forwardDrag × forceScale))

Where thrust = throttleCurve(1.0) × thrustForce and forceScale = mass if scaleForcesWithMass.

Direction Tracking

BoatDynamics maintains a m_direction vector (horizontal-only) that lerps toward transform.forward over time. This provides a smooth heading reference for thrust application, independent of wave-induced pitching.


BoatDriver

File: Assets/05 Scripts/CharacterControl/Boat/Shared/BoatDriver.cs

Abstract base class for boat controllers. Subclasses compute throttle and turning values and call Dynamics.SetInput(throttle, turning). Two concrete implementations:


PlayerBoatDriver

File: Assets/05 Scripts/CharacterControl/Boat/Player Boat/PlayerBoatDriver.cs

Reads Unity InputSystem actions (keyboard or controller thumbstick). For controller input, throttle and turning are derived from the thumbstick angle using configurable dead-zone angles (fullThrottleInputAngle, fullTurningInputAngle) so the player doesn't need to be perfectly precise on the stick. Also handles game-system integration (POI registration, dialogue anchoring — auto-drops anchor during dialogue).


NPCBoatDriver

File: Assets/05 Scripts/CharacterControl/Boat/NPC Boat/NPCBoatDriver.cs

AI-driven boat driver with a four-state state machine and PD-controlled steering/throttle.

State Machine

State Description
Idle No input fed to dynamics. Still scans for chase targets.
FollowPath Follows waypoints from a Route component. Supports ping-pong direction.
Chase Pursues a moving chaseTarget (typically the player). Uses NavMesh pathing when available.
Anchor Drops anchor via BoatDynamics.DropAnchor(). Zero input fed.

State transitions:

- **FollowPath → Chase**: When `chaseTarget` enters `detectionRadius`.
- **Chase → FollowPath**: When target leaves `escapeRadius`, or boat exceeds `maxChaseRadius` from the route centroid. Both trigger a cooldown timer to prevent instant re-detection.
- **Any → Anchor**: Via `SetState(NPCBoatState.Anchor)`.
- **Idle → Chase**: Same detection check as FollowPath.

NPC boats follow a Route (a set of waypoints). When the Route has a baked NavMesh:

When no NavMesh is available, the boat steers directly toward waypoints.

Anticipation blends the steering target toward the next waypoint as the boat approaches the current one, creating smooth arcs instead of hard turns. (Currently disabled due to NavMesh interaction issues.)

AI Input Computation (PD Controller)

Runs in Update(), produces throttle and turning values fed into BoatDynamics.SetInput():

Steering (PD control):

proportional = steeringCurve(angle / fullAngle) × sign(angle)
derivative = yawRateDeg / yawDampingFullRate × yawDampingGain
turning = clamp(proportional − derivative, −1, 1)

The derivative term reads WaterForces.LastAngularVelocity.y (current yaw rate) and opposes the rudder to prevent overshoot. Higher yawDampingGain = smoother turns with less oscillation.

Throttle (distance × alignment):

distanceFactor = distanceCurve(distance / effectiveArrival)
alignmentFactor = alignmentCurve(dot(heading, targetDir) remapped to 0–1)
throttle = max(distanceFactor × alignmentFactor, minTurningThrottle × offHeadingFactor)

Fallback Movement (LOD Cheap Mode)

When BoatDynamics.PhysicsActive is false (BoatLOD has downgraded the boat), the NPCBoatDriver drives the Transform directly:

This ensures NPC boats keep moving visually even without full physics simulation.


WavePushAngleConfig

File: Assets/05 Scripts/WaterPhysics/WavePushAngleConfig.cs

A serializable struct that defines angle-based wave push multipliers using dot-product bands. Each AngleBand specifies a [dotMin, dotMax] range and a multiplier:

When enabled, the dot product between the boat's forward heading and the wave push direction is evaluated against all bands. The matching band's multiplier scales the wave push force and torque. If no band matches, fallbackMultiplier is used.


WaterProbePresets

File: Assets/05 Scripts/WaterPhysics/WaterProbePresets.cs

Static utility that generates common probe layouts:

Preset Points Layout
Cross 4 Front, back, left, right
Square 4 Four corners
Triangle 3 Forward point + two rear points (equilateral)
Square8 8 Four corners + four edge midpoints

All weights start at 1.0 and are normalized by WaterForces.NormalizeProbeWeights() so they sum to 1.0.


BoatLOD

File: Assets/05 Scripts/WaterPhysics/BoatLOD.cs

Three-tier LOD system for NPC boats:

Tier Method Cost
FullPhysics WaterForces + BoatDynamics + applier Full force calculation, collision
TriangleSample SimpleWaterAlign (3-point) + NPC movement CPU wave samples, no physics
SinglePoint SimpleWaterAlign (1-point) + procedural bob Minimal

Distance thresholds use hysteresis to prevent flickering at boundaries. When transitioning back to FullPhysics, the applier is teleported to the current visual pose and given an impulse matching the velocity at the time of downgrade.


SimpleWaterAlign

File: Assets/05 Scripts/WaterPhysics/SimpleWaterAlign.cs

Lightweight water alignment for LOD cheap tiers:

Both modes run in LateUpdate and apply smooth interpolation on height and rotation.


WaveProfile (Stylized Water 3)

File: Assets/08 Plugins/Stylized Water 3/Runtime/WaveProfile.cs

A ScriptableObject that defines a set of Gerstner wave layers. Each Wave layer has: - waveLength — distance between crests - amplitude — height from base to peak - steepness — horizontal displacement (too high causes crest looping) - direction — travel angle in degrees - mode — Directional (angle-based) or Radial (point-origin) - enabled — whether the layer contributes

Global multipliers (waveLengthMultiplier, amplitudeMultiplier, steepnessMultiplier) scale all layers uniformly. Per-layer curves (waveLengthCurve, amplitudeCurve, steepnessCurve) apply additional scaling based on layer index. steepnessClamping normalizes steepness across layers to prevent crest looping.

When UpdateShaderParameters() is called, the profile writes all layer data into a lookup texture (LUT) — an 8×2 RGBAHalf texture read by the water shader on the GPU. This is the bridge between CPU-side wave data and GPU rendering. WaterSampler also reads the same profile data for CPU-side height queries.

ReferenceWaveProfile

File: Assets/04 Data/WaterPhysics/ReferenceWaveProfile.asset

The project's base wave template. WaveManager holds two profiles: - ReferenceProfile — this asset, the "source of truth" for wave shapes. - ActiveProfile — the runtime profile that WaterSampler and the water material read. Modified in place by WaveManager and WaveTweener.

The ReferenceProfile is never modified at runtime. On startup, WaveManager.Awake() clones its global properties onto the ActiveProfile. All procedural wave generation (SetToRandomWaveProfile) works by copying the ReferenceProfile's layers, rotating their directions, and jittering their amplitudes.

The ReferenceProfile has 8 layers with a specific naming convention:

Index Name Purpose
0 Major Direction Primary swell — longest wavelength (60m), highest amplitude (2.0), base direction (0°). Defines the dominant wave direction.
1 Interference Wave Secondary swell — shorter wavelength (30m), moderate amplitude (1.0), slight angle offset (20°). Creates cross-swell interference patterns.
2 Ripples Wave #1 Surface detail — amplitude 0.25, perpendicular direction (90°). Adds visual complexity.
3 Ripples Wave #2 Surface detail — amplitude 0.3, diagonal direction (150°). Fills in texture from another angle.
4–7 Lerp #1–4 Reserve slots — amplitude at MIN_AMPLITUDE (0.012), effectively silent. Used by WaveTweener as the "incoming" set during crossfade transitions.

The 4+4 split is critical: layers 0–3 are the "current" waves the player sees and the physics system samples. Layers 4–7 are the "incoming" waves that WaveTweener fades in during a transition. Once the crossfade completes, the incoming data is copied into layers 0–3 and reserves are reset to MIN_AMPLITUDE. This is why ApplyLayersInstantly always writes to layers 0–3 and clears 4–7.


WaveManager

File: Assets/05 Scripts/WaterPhysics/WaveManager.cs

Singleton that controls two independent aspects of the wave system:

Global Amplitude

Controls how tall waves are overall. This is a single float (amplitudeMultiplier) on the active WaveProfile that scales all wave layers uniformly. It is not the wave shape — just the overall height multiplier.

Amplitude is calculated each frame using Inverse Distance Weighting (IDW) between the player boat's position and registered WaveAmplitudeInfluenceArea volumes (safe harbors, calm zones, etc.):

For each influence area:
    weight = 1 / (normalizedDistance² + ε)

Add a virtual "ocean" contributor with weight = OceanStrength and amplitude = OceanMaxAmplitude.

targetAmplitude = Σ(weight × area.amplitude) / Σ(weight)

This creates smooth transitions: near an influence area, its calm amplitude dominates; far from all areas, the open ocean amplitude takes over. The OceanInfluenceRadius normalizes the distance calculation, and OceanStrength controls how quickly the ocean wins.

The current amplitude exponentially decays toward the target at AmplitudeBlendSpeed:

current = target + (current - target) × e^(-blendSpeed × dt)

Amplitude locking: LockGlobalAmplitude(value) freezes the amplitude at a specific value (for cutscenes, scripted moments). UnlockGlobalAmplitude() resumes IDW-based calculation. Lock state overrides everything.

Wave Profile Transitions

Controls the shape of waves (direction, wavelength, steepness per layer) via the active WaveProfile's layer array. Transitions are handled by WaveTweener (see below).

Key API:

Method Description
SetToRandomWaveProfile() Procedurally generate a profile by rotating the reference profile by a random angle and jittering amplitude by ±AmplitudeChangePercentage. Primary method for gameplay use.
SetToRandomWaveProfileWithDirection(dir) Same but with a specific wave direction. Primary method for gameplay use.
SetWaveProfileToReference() Snap back to the reference profile's layers.
SetWaveProfileManually(profile) Apply a hand-authored profile's layers (max 4). Does not change amplitude. Debug / cutscene use only — not currently used at runtime. For when a very specific sea state is needed for a scripted moment.

All methods support instantSet (snap immediately) or customDuration (smooth transition via WaveTweener).

WaveAmplitudeInfluenceArea

File: Assets/05 Scripts/WaterPhysics/WaveAmplitudeInfluenceArea.cs

A trigger collider that registers itself with WaveManager. Defines a volume where waves should be calmer. Distance to the player is computed via Collider.ClosestPoint, handling any collider shape automatically. Each area has its own TargetAmplitude.


WaveTweener

File: Assets/05 Scripts/WaterPhysics/WaveTweener.cs

Singleton that smoothly crossfades between wave layer sets using Stylized Water 3's 8-layer system. The active WaveProfile has 8 layer slots — WaveTweener uses layers 0–3 as the "current" set and layers 4–7 as the "incoming" set.

Crossfade Mechanism

When CrossfadeToLayers(targetLayers, duration) is called:

  1. Copy the incoming wave data into layers 4–7 (reserve slots), with amplitude set to MIN_AMPLITUDE.
  2. Over duration seconds, simultaneously: - Fade out layers 0–3: amplitude lerps toward MIN_AMPLITUDE using FadeOutCurve. - Fade in layers 4–7: amplitude lerps toward target amplitude using FadeInCurve.
  3. On completion, copy reserve layer data into base layers 0–3 and clear reserves.

The fade-in and fade-out phases have configurable start/end points within the transition timeline (FadeOutStart/End, FadeInStart/End), so the incoming waves can start appearing before the old ones have fully faded, creating an overlap period.

Interruption

If a new crossfade is requested while one is already running, the tweener: 1. Stops the current coroutine. 2. Runs a 1-second interrupt phase that fades out the reserve layers (4–7) to clean up any partial crossfade. 3. Starts a fresh crossfade to the new target.

A second interruption during the interrupt phase queues the target but does not restart — the interrupt must complete first.

WaveProfileUtility

File: Assets/05 Scripts/WaterPhysics/WaveProfileUtility.cs

Static helpers for copying WaveProfile.Wave layer data without allocations (CopyWaveData, ResetToEmptyLayer, CloneWaveProfile). Used by WaveManager and WaveTweener to manipulate the active profile in place.


RotationUtils

File: Assets/05 Scripts/WaterPhysics/RotationUtils.cs

Provides DecomposeSwingTwist — splits a quaternion into swing (tilts the axis) and twist (rotates around the axis) components. Used throughout the system to preserve yaw while applying wave-induced pitch/roll.


Data Flow: Complete Frame Sequence

FixedUpdate Phase

FixedUpdate #N begins
│
├─ BoatDriver.Update() [can run in Update, sets input]
│   └─ Dynamics.SetInput(throttle, turning)
│
├─ BoatDynamics.FixedUpdate()
│   ├─ Reads WaterForces.LastVelocity (from frame N-1)
│   ├─ Reads WaterForces.SubmergedRatio (from frame N-1)
│   ├─ Throttle smoothing
│   ├─ Thrust → WaterForces.AddForce()
│   ├─ Rudder → WaterForces.AddTorque()
│   ├─ Planing → WaterForces.AddTorque()
│   ├─ Banking → WaterForces.AddTorque()
│   └─ Anchor spring → WaterForces.AddForce()
│
├─ [Other systems may also call AddForce/AddTorque here]
│
├─ ApplyWaterForces coroutine fires (yields WaitForFixedUpdate)
│   ├─ WaterForces.GetForces(state)
│   │   ├─ WaterSampler.ComputeHeight() for each probe
│   │   │   (reads wave shape from ActiveProfile, which WaveTweener
│   │   │    may be crossfading — see Update phase below)
│   │   ├─ Buoyancy, drag, slamming per submerged probe
│   │   ├─ Wave push computed and post-processed
│   │   ├─ Horizontal + angular drag
│   │   ├─ Pending external forces drained
│   │   ├─ ForceFilters run on combined result
│   │   └─ Result cached, LastVelocity/SubmergedRatio updated
│   └─ Applies force to Rigidbody / integrates manually
│
└─ FixedUpdate #N complete

Update Phase

Update
│
├─ WaveManager.Update()
│   ├─ Enforce GlobalSteepness on ActiveProfile
│   └─ UpdateAmplitude()
│       ├─ If amplitude locked → target = locked value
│       ├─ Else → CalculateIDWAmplitude()
│       │   ├─ For each WaveAmplitudeInfluenceArea:
│       │   │   weight = 1 / (dist²/radius² + ε)
│       │   ├─ Virtual ocean contributor (OceanStrength × OceanMaxAmplitude)
│       │   └─ target = weightedSum / totalWeight
│       └─ current = exponential decay toward target
│           → ActiveProfile.amplitudeMultiplier = current
│           → ActiveProfile.UpdateShaderParameters()
│
├─ WaveTweener coroutine (if crossfading)
│   └─ Lerp amplitude: layers 0–3 fade out, layers 4–7 fade in
│       → ActiveProfile.UpdateShaderParameters()
│       (WaterSampler reads this same profile next FixedUpdate)
│
├─ NPCBoatDriver.Update() [if NPC boat]
│   ├─ State machine transitions
│   ├─ NavMesh path refresh
│   ├─ ComputeAIInputs (PD steering/throttle)
│   ├─ Dynamics.SetInput(throttle, turning)
│   └─ FallbackMove if PhysicsActive == false
│
├─ PlayerBoatDriver.Update() [if player boat]
│   ├─ Read InputSystem actions
│   └─ Dynamics.SetInput(throttle, turning)
│
├─ ApplyWaterForcesToTransform.PerformInterpolation()
│   └─ Smooth visual position/rotation between fixed steps
│
└─ SimpleWaterAlign.LateUpdate() [LOD cheap tiers only]

Probe System In Detail

How Probes Work

Probes are local-space points sampled against the water surface each frame. A probe is submerged when its world Y position is below the water height (minus draft).

worldPos = objectPosition + objectRotation × (probe.localPos × objectScale)
waterHeight = WaterSampler.ComputeHeight(worldPos.x, worldPos.z)
displacement = worldPos.y - waterHeight - draft
submerged = displacement < 0

The displacement is clamped to [-hullDepth, 0] so buoyancy maxes out at full submersion.

Weight Normalization

All probe weights are normalized to sum to 1.0 (NormalizeProbeWeights()). This means: - Adding more probes doesn't increase total force. - A probe with weight 0.5 contributes half as much buoyancy/drag as one with weight 1.0 (before normalization). - The submerged ratio is calculated as sum(submerged probe weights) / totalWeight.

Choosing a Probe Layout

More probes = more ComputeHeight calls per frame. The CenterOfMass wave push mode adds only 2 extra samples total; PerProbe adds 2 per submerged probe.


Debug Tools

DraggableDebugWindow

File: Assets/05 Scripts/Utility/DraggableDebugWindow.cs

A reusable runtime IMGUI window. Draggable, toggle-able via hotkey, with position and visibility persisted to EditorPrefs between play sessions (editor only). Provides styled helpers for common layouts — Header(), Row(), BannerHeader(), DrawBar() — with a dark theme that matches Unity's default look.

Used by the water physics test scenes and other debug overlays throughout the project.

TestSceneDebugWindow

File: Assets/05 Scripts/Utility/TestSceneDebugWindow.cs

A MonoBehaviour that wraps DraggableDebugWindow for zero-code test scene setup. Drop it on any GameObject, configure the Inspector fields:

Toggle with F1 (configurable). Used in the water physics test scenes to let designers quickly toggle wave push modes, reset positions, switch profiles, etc. without writing code.