Tidal 2 URP
Scripting Reference

Water Queries & API

Runtime scripting reference - namespace Tidal.Core.

Tidal 2 exposes fast, allocation-free CPU water sampling backed by the GPU physics field (FFT + zones + wake). The members below live in the Tidal.Core namespace. Water heights returned are world-space Y values; the water level of a body is its transform Y.

Note: Queries are per-body. With multiple water bodies, pick the correct TidalBody first (see TidalManager.GetCameraTidalBody below) and query that body.

TidalBody water queries

On TidalBody. The fast GetWaterHeight path reads back the GPU physics field, so it includes FFT waves, wake, zones, and volume effects.

Height

float GetWaterHeight(Vector3 position) float GetWaterHeight(Vector3 position, List<TidalVolume> ignoreVolumes)

GetWaterHeight is the fast CPU query backed by the physics-field readback (FFT + wake + zones + damping in one tap, with coarse outer LODs covering the whole play area). It is the single CPU height source. Pass an ignoreVolumes list to exclude an object's own displacement volume from its height query.

Always test the float.MinValue sentinel
  • GetWaterHeight returns float.MinValue when there is no valid sample: outside a bounded body's rect, beyond clipmap coverage, or before the first readback lands (the first frames of play).
  • Always test h > float.MinValue (or h < -1e30f as the invalid check) before using the value.
  • Writing an unchecked result into a transform teleports the object to y ≈ -3.4e38 and triggers Unity "Invalid worldAABB" errors.

Normal

Vector3 GetWaterNormal(Vector3 position, List<TidalVolume> ignoreVolumes = null, float sampleDist = 1.0f)

Surface normal including FFT, wake, and zones. Uses the physics field's analytic normal when available, otherwise central differences at sampleDist spacing. Unlike GetWaterHeight, an invalid sample returns Vector3.up - never garbage - so aligning a transform to it is always safe.

Flow

Vector2 GetWaterFlow(Vector3 position)

Local current in the XZ plane, composited from Flow Emitters and baked Flow Zones. Used by the demo boat to ride currents.

State & registry

int HeightSamplingIterations bool ShouldRun static List<TidalBody> AllBodies bool IsDebug()

TidalPhysicsFieldSubSystem

The physics field bakes water height + normal around each point of interest on the GPU and reads it back asynchronously. Its Try* methods are coverage-aware: they return false when there is no data instead of the sentinel value, so you never have to test for float.MinValue.

bool TryGetWaterHeight(Vector3 worldPos, out float height) bool TryGetWaterSurface(Vector3 pos, out float height, out Vector3 normal)

TryGetWaterSurface returns height and the analytic normal in a single tap - the recommended path for buoyancy and object placement.

TidalManager

static TidalManager Instance static void CreateIfNotInitializedYet() TidalBody GetCameraTidalBody(Camera cam)

GetCameraTidalBody returns the body a camera is associated with, with a nearest-body fallback when no body has water at the camera. Use it to resolve which body to query when several exist. Call CreateIfNotInitializedYet() if you need the manager before it lazily initializes.

Conventions & gotchas

Read before scripting against the water
  • Water level = the body transform's Y; returned heights are world-space Y.
  • Queries are per-body - with multiple bodies, pick the body (via TidalManager.GetCameraTidalBody or your own logic) before querying.
  • The physics field refreshes at its Update Hz with async readback latency (~2-4 frames); for camera-critical logic prefer the same query the systems use rather than caching a stale value.
  • Never cache subsystem references across scene loads - always get them from the body.

Example: sentinel-checked buoyancy sample

// Per float point: apply uplift only from a valid water sample.
float h = body.GetWaterHeight(point.position);
if (h > float.MinValue)
{
    float submersion = h - point.position.y;
    if (submersion > 0f)
    {
        // point is under the surface - push it up
        Vector3 up = body.GetWaterNormal(point.position); // Vector3.up if invalid
        rb.AddForceAtPosition(up * submersion * buoyancy, point.position);
    }
}
// else: no coverage this frame - contribute no force (never write h to a transform)

Prefer TidalPhysicsFieldSubSystem.TryGetWaterSurface for new code - the bool return removes the sentinel check entirely. See the Buoyancy page for the ready-made TidalBuoyancy component and Upgrade from v1.0 for migrating old per-frame sampling paths.