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.
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.
-
GetWaterHeightreturnsfloat.MinValuewhen 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(orh < -1e30fas 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() HeightSamplingIterations- the body's choppy-displacement inversion step count (read-only). The physics-field bake uses the same count so GPU and CPU heights agree.ShouldRun- true in play mode, or in edit mode with Run-In-Editor on.AllBodies- all enabledTidalBodyinstances.IsDebug()- the body's Debug toggle state.
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
- 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.GetCameraTidalBodyor 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.