You can sample water to obtain exact wave height at given position in two ways:
DragonWaterManager.Instance.SampleWater()
function, or
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.
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(); }
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; }
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
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();
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();
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).