Table of Contents

Water Sampler

You can sample water to obtain exact wave height at given position in two ways:

In fact, first option is just exposed simplified version of WaterSampler usage and use it only occasionally for single, synchronous checks.
Using WaterSampler manually is preferable solution, as it not only offers way more configurable options, but also asynchronous operation (via Job System) to provide best possible performance.

WaterSampler

The most usefull class.
Create it's object to sample water.
You can reuse this object (keep calling Schedule and Complete) but always remember to Dispose() when no longer needed.

public class WaterSampler : IDisposable
{
    public static IReadOnlyList<WaterSampler> ActiveSamplers { get; } // access to all currently active samplers
 
 
    public enum SurfaceDetectionMode
    {
        Default, // use default surface, if its null then fallbacks to AutoCull
        AutoCull, // automatically scans nearby surfaces
        Custom // tests only aainst provided surfaces in SurfaceDetectionCustom list
    }
 
    public enum CutoutDetectionMode
    {
        AutoCull, // automatically scans nearby colliders to cutout
        DontCutout, // do not care about cutouts at all - always sample
        Custom // tests only against provided cutouts in CutoutDetectionCustom list
    }
 
 
    public int Precision = 0; // amount of iterations; 0 refers to Constants.DefaultSamplerPrecision and 3-4 is most cases is more than enough
    public SurfaceDetectionMode SurfaceDetection = SurfaceDetectionMode.Default; // how nearby surfaces are detected
    public CutoutDetectionMode CutoutDetection = CutoutDetectionMode.AutoCull; // how to to process cutout volumes
    public List<WaterSurface> SurfaceDetectionCustom = null; // you NEED to assign it in case of Custom mode
    public List<WaterCutoutVolume> CutoutDetectionCustom = null; // you NEED to assign it in case of Custom mode
    public Bounds CullingBox = default; // if left default, it will automatically create bounding box for all points before scheduling
 
    public int MaxSize { get; }; // maximum amount of points this sampler can process; assigned via constructor
    public bool IsRunning { get; }; // if its running, you shouldn't modify anything here
    public HitResult[] Results { get; } // access to array of results of last scan - it's size is always MaxSize even if you ran it for less points
    public int ResultCount { get; } // actual amount of available results in Results array
 
 
    public WaterSampler(int maxSize = 1); // construction - you have to specify  max size
    public void Dispose(); // ALWAYS remember to dispose no longer needed sampler
    public void Resize(int newMaxSize); // resize to new MaxSize
 
    // fill points to test against water
    public void SetPoint(int index, Vector3 point); // set single point and specified index
    public void SetPoints(IReadOnlyList<Vector3> points); // set points since index 0 
    public void SetPoints(ArraySegment<Vector3> points); // set points since index 0 
 
    // schedule
    public void Schedule(); // calls Schedule with MaxSize
    public void Schedule(int pointsCount); // amount of points to process starting from index 0; can't be larger than MaxSize
    public void Complete(); // complete processing - recommended to call next frame
 
    // caching auto culls
    // be default AutoCull modes are very expensive - with this functions you can temporarily cache their result so it will act like Custom options
    public void CacheAutoCull(); // calls CacheAutoCull with MaxSize
    public void CacheAutoCull(int points);
    public void ClearAutoCullCache();
 
    // clears sampler until next scheduling, so it acts like not scheduled yet at all with no results
    public void ClearState();
}

WaterSampler.HitResult

Struct that contains sampling result of a single point against water.

public struct HitResult
{
    public Vector3 sampledPoint;
 
    public WaterSurface surface; // null if no surface detected
    public Vector3 hitPoint; // actual position on water surface, possibly very closely to sampledPoint
    public Vector3 hitNormal; // normal vector of water here
 
    public bool HasHit => surface != null;
    public bool IsUnderwater => sampledPoint.y < hitPoint.y;
    public float Depth => hitPoint.y - sampledPoint.y; // positive when under water
    public float Height => sampledPoint.y - hitPoint.y; // positive when above water
    public float WaterLevel => hitPoint.y;
}

2023/06/11 23:46 · Bartek Dragon

Usage

Basic

With default SurfaceDetection set to Default, sampler will tests only against water surface that has been set as default.
If there is no default surface active, then it will fallback to AutoCull and will automatically detect nearby surfaces and tests against them all.
AutoCull is medium-heavy operation, so if you need to check only against specific surface that is not default, set it to Custom and manually provide list of surfaces you want to check.

With default CutoutDetection set to AutoCull, sampler will scan for all nearby cutout volumes and check if given points are inside them or outside. Then, accordingly to the results, will properly respect cutout usage by each surface.
AutoCull is heavy operation. If you do not need to test it (e.g. you know that your points will never be inside volumes or you simply don't need it) - turn it off by setting it to DontCutout.
You can also set it to Custom and manually provide list of cutouts you want to test against, what will also significantly improve performance comparing to AutoCull.

Default Precision is set to Constants.DefaultSamplerPrecision = 4 and in most cases it's more than enough.

var sampler = new WaterSampler(1); // size of 1
sampler.SetPoint(0, Vector3.zero); // set first (the only one) point
sampler.Schedule(1); // schedule with this single point
 
// now, for best asyncrhonous performance
// you should wait for next frame and Complete() it then
 
sampler.Complete(); // always call Complete even if you've waited for N frames already
var result = sampler.Results[0];
 
sampler.Dispose(); // ALWAYS remember to to Dispose no longer needed sampler

Custom Detection Mode

This example shows how to use Custom modes.

var sampler = new WaterSampler();
 
sampler.SurfaceDetection = WaterSampler.SurfaceDetectionMode.Custom;
sampler.SurfaceDetectionCustom = new() { yourSurface1 };
 
sampler.CutoutDetection = WaterSampler.CutoutDetectionMode.Custom;
sampler.CutoutDetectionCustom = new() { cutout1, cutout2 };
 
// <schedule>
// <wait for frame>
// <complete>
 
sampler.Dispose();

Auto Cull Caching

As mentioned above, AutoCull modes are heavy. However, you can significantly improve performance by using built-in caching feature.
After you set up your points, call CacheAutoCull(), so sampler will temporarily act like with Custom modes and won't cull nearby surfaces and cutouts.
If you constantly use sampler every frame with auto culling, you can re-cache auto cull like once (or few times) per second to have quite up-to-date results while maintaning good performance.

var sampler = new WaterSampler();
 
// <first, set your points here>
 
sampler.CacheAutoCull();
 
// now, all Schedule operation will be very perfomrant because
// sampler will no longer AutoCull surfaces and cutouts and used cached once instead
// so it acts like temporal Custom mode
 
sampler.ClearAutoCullCache(); // now it will come back to auto cull every next Schedule
 
// <...>
 
sampler.Dispose();

Bounding Box

If any of your modes is set to AutoCull, sampler will need bounding box that covers all of your points for culling.
By default sampler calculates it automatically which is pretty cheap operation for just a few points.
However, this may become expensive for like hundreds of points. In this scenario, you will most likely want to assign CullingBox property manually.
This property is a bounding box that must contains all of given points (it doesn't need to be exact, approximation is enough).

Also, try not to create sampler that covers your entire large world. Sampler is more effective for large amount of points, but these points should belong to relatively small, logically separated area (e.g. area of ship).