移除水

This commit is contained in:
2025-06-21 21:58:06 +08:00
parent d61516a576
commit e9f76d0f11
1566 changed files with 9218 additions and 300913 deletions

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 53c547efc6e8b4d068d60adf2aae4695
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,143 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Debug draw crosses in an area around the GameObject on the water surface.
/// </summary>
[@ExecuteDuringEditMode]
[AddComponentMenu(Constants.k_MenuPrefixDebug + "Collision Area Visualizer")]
sealed class CollisionAreaVisualizer : ManagedBehaviour<WaterRenderer>
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Tooltip(ICollisionProvider.k_LayerTooltip)]
[SerializeField]
internal CollisionLayer _Layer;
[SerializeField]
float _ObjectWidth = 0f;
[SerializeField]
float _StepSize = 5f;
[SerializeField]
int _Steps = 10;
[SerializeField]
bool _UseDisplacements;
[SerializeField]
bool _UseNormals;
float[] _ResultHeights;
Vector3[] _ResultDisplacements;
Vector3[] _ResultNormals;
Vector3[] _SamplePositions;
private protected override System.Action<WaterRenderer> OnUpdateMethod => OnUpdate;
void OnUpdate(WaterRenderer water)
{
if (water.AnimatedWavesLod.Provider == null)
{
return;
}
if (_ResultHeights == null || _ResultHeights.Length != _Steps * _Steps)
{
_ResultHeights = new float[_Steps * _Steps];
}
if (_ResultDisplacements == null || _ResultDisplacements.Length != _Steps * _Steps)
{
_ResultDisplacements = new Vector3[_Steps * _Steps];
}
if (_ResultNormals == null || _ResultNormals.Length != _Steps * _Steps)
{
_ResultNormals = new Vector3[_Steps * _Steps];
for (var i = 0; i < _ResultNormals.Length; i++)
{
_ResultNormals[i] = Vector3.up;
}
}
if (_SamplePositions == null || _SamplePositions.Length != _Steps * _Steps)
{
_SamplePositions = new Vector3[_Steps * _Steps];
}
var collProvider = water.AnimatedWavesLod.Provider;
for (var i = 0; i < _Steps; i++)
{
for (var j = 0; j < _Steps; j++)
{
_SamplePositions[j * _Steps + i] = new(((i + 0.5f) - _Steps / 2f) * _StepSize, 0f, ((j + 0.5f) - _Steps / 2f) * _StepSize);
_SamplePositions[j * _Steps + i].x += transform.position.x;
_SamplePositions[j * _Steps + i].z += transform.position.z;
}
}
if (_UseDisplacements)
{
if (collProvider.RetrieveSucceeded(collProvider.Query(GetHashCode(), _ObjectWidth, _SamplePositions, _ResultDisplacements, _UseNormals ? _ResultNormals : null, null, _Layer)))
{
for (var i = 0; i < _Steps; i++)
{
for (var j = 0; j < _Steps; j++)
{
var result = _SamplePositions[j * _Steps + i];
result.y = water.SeaLevel;
result += _ResultDisplacements[j * _Steps + i];
var norm = _UseNormals ? _ResultNormals[j * _Steps + i] : Vector3.up;
DebugDrawCross(result, norm, Mathf.Min(_StepSize / 4f, 1f), Color.green);
}
}
}
}
else
{
if (collProvider.RetrieveSucceeded(collProvider.Query(GetHashCode(), _ObjectWidth, _SamplePositions, _ResultHeights, _UseNormals ? _ResultNormals : null, null, _Layer)))
{
for (var i = 0; i < _Steps; i++)
{
for (var j = 0; j < _Steps; j++)
{
var result = _SamplePositions[j * _Steps + i];
result.y = _ResultHeights[j * _Steps + i];
var norm = _UseNormals ? _ResultNormals[j * _Steps + i] : Vector3.up;
DebugDrawCross(result, norm, Mathf.Min(_StepSize / 4f, 1f), Color.green);
}
}
}
}
}
public static void DebugDrawCross(Vector3 pos, float r, Color col, float duration = 0f)
{
Debug.DrawLine(pos - Vector3.up * r, pos + Vector3.up * r, col, duration);
Debug.DrawLine(pos - Vector3.right * r, pos + Vector3.right * r, col, duration);
Debug.DrawLine(pos - Vector3.forward * r, pos + Vector3.forward * r, col, duration);
}
public static void DebugDrawCross(Vector3 pos, Vector3 up, float r, Color col, float duration = 0f)
{
up.Normalize();
var right = Vector3.Normalize(Vector3.Cross(up, Vector3.forward));
var forward = Vector3.Cross(up, right);
Debug.DrawLine(pos - up * r, pos + up * r, col, duration);
Debug.DrawLine(pos - right * r, pos + right * r, col, duration);
Debug.DrawLine(pos - forward * r, pos + forward * r, col, duration);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 02e47243a77e64c84b8a2d351386a074
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,82 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// NOTE: DWP2 depends on this file. Any API changes need to be communicated to the DWP2 authors in advance.
using UnityEngine;
// Linter does not support mixing inheritdoc plus defining own parameters.
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
namespace WaveHarmonic.Crest
{
/// <summary>
/// A layer/event where queries are executed.
/// </summary>
public enum CollisionLayer
{
/// <summary>
/// Include all displacement.
/// </summary>
[Tooltip("Include all displacement.")]
Everything,
/// <summary>
/// Only include Animated Waves.
/// </summary>
[Tooltip("Only include Animated Waves.")]
AfterAnimatedWaves,
/// <summary>
/// Include Animated Waves and Dynamic Waves.
/// </summary>
[Tooltip("Include Animated Waves and Dynamic Waves.")]
AfterDynamicWaves,
}
/// <summary>
/// Interface for an object that returns water surface displacement and height.
/// </summary>
public interface ICollisionProvider : IQueryProvider
{
internal const string k_LayerTooltip = "Which water collision layer to target.";
internal static NoneProvider None { get; } = new();
/// <summary>
/// Gives a flat, still water.
/// </summary>
internal sealed class NoneProvider : ICollisionProvider
{
public int Query(int _0, float _1, Vector3[] _2, Vector3[] result0, Vector3[] result1, Vector3[] result2, CollisionLayer _3 = CollisionLayer.Everything)
{
if (result0 != null) System.Array.Fill(result0, Vector3.zero);
if (result1 != null) System.Array.Fill(result1, Vector3.up);
if (result2 != null) System.Array.Fill(result2, Vector3.zero);
return 0;
}
public int Query(int _0, float _1, Vector3[] _2, float[] result0, Vector3[] result1, Vector3[] result2, CollisionLayer _3 = CollisionLayer.Everything)
{
if (result0 != null) System.Array.Fill(result0, WaterRenderer.Instance.SeaLevel);
if (result1 != null) System.Array.Fill(result1, Vector3.up);
if (result2 != null) System.Array.Fill(result2, Vector3.zero);
return 0;
}
}
/// <summary>
/// Query water physical data at a set of points. Pass in null to any out parameters that are not required.
/// </summary>
/// <param name="heights">Resulting heights (displacement Y + sea level) at the query positions. Pass null if this information is not required.</param>
/// <param name="normals">Resulting normals at the query positions. Pass null if this information is not required.</param>
/// <param name="velocities">Resulting velocities at the query positions. Pass null if this information is not required.</param>
/// <inheritdoc cref="IQueryProvider.Query(int, float, Vector3[], int)" />
int Query(int hash, float minimumLength, Vector3[] points, float[] heights, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything);
/// <param name="displacements">Resulting displacement vectors at the query positions. Add sea level to Y to get world space height.</param>
/// <inheritdoc cref="IQueryProvider.Query(int, float, Vector3[], int)" />
/// <inheritdoc cref="Query(int, float, Vector3[], float[], Vector3[], Vector3[], CollisionLayer)" />
int Query(int hash, float minimumLength, Vector3[] points, Vector3[] displacements, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything);
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 55075bf334a1445828a0ad179248fe9c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,148 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Samples water surface shape - displacement, height, normal, velocity.
/// </summary>
sealed class CollisionQuery : QueryBase, ICollisionProvider
{
public CollisionQuery(WaterRenderer water) : base(water) { }
protected override int Kernel => 0;
public int Query(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] resultDisplacements, Vector3[] resultNormals, Vector3[] resultVelocities, CollisionLayer layer = CollisionLayer.Everything)
{
var result = (int)QueryStatus.OK;
if (!UpdateQueryPoints(ownerHash, minSpatialLength, queryPoints, resultNormals != null ? queryPoints : null))
{
result |= (int)QueryStatus.PostFailed;
}
if (!RetrieveResults(ownerHash, resultDisplacements, null, resultNormals))
{
result |= (int)QueryStatus.RetrieveFailed;
}
if (resultVelocities != null)
{
result |= CalculateVelocities(ownerHash, resultVelocities);
}
return result;
}
public int Query(int ownerHash, float minimumSpatialLength, Vector3[] queryPoints, float[] resultHeights, Vector3[] resultNormals, Vector3[] resultVelocities, CollisionLayer layer = CollisionLayer.Everything)
{
var result = (int)QueryStatus.OK;
if (!UpdateQueryPoints(ownerHash, minimumSpatialLength, queryPoints, resultNormals != null ? queryPoints : null))
{
result |= (int)QueryStatus.PostFailed;
}
if (!RetrieveResults(ownerHash, null, resultHeights, resultNormals))
{
result |= (int)QueryStatus.RetrieveFailed;
}
if (resultVelocities != null)
{
result |= CalculateVelocities(ownerHash, resultVelocities);
}
return result;
}
}
sealed class CollisionQueryWithPasses : ICollisionProvider, IQueryable
{
readonly CollisionQuery _AnimatedWaves;
readonly CollisionQuery _DynamicWaves;
readonly CollisionQuery _Displacement;
readonly WaterRenderer _Water;
public int ResultGuidCount => _AnimatedWaves.ResultGuidCount + _DynamicWaves.ResultGuidCount + _Displacement.ResultGuidCount;
public int RequestCount => _AnimatedWaves.RequestCount + _DynamicWaves.RequestCount + _Displacement.RequestCount;
public int QueryCount => _AnimatedWaves.QueryCount + _DynamicWaves.QueryCount + _Displacement.QueryCount;
public CollisionQueryWithPasses(WaterRenderer water)
{
_Water = water;
_AnimatedWaves = new(water);
_DynamicWaves = new(water);
_Displacement = new(water);
}
// Gets the provider for the given layer, falling back to previous layer when needed.
CollisionQuery GetProvider(CollisionLayer layer)
{
var layers = _Water.AnimatedWavesLod._CollisionLayers;
if (layers == CollisionLayers.Nothing)
{
return _Displacement;
}
if (layer == CollisionLayer.Everything)
{
if (layers.HasFlag(CollisionLayers.Displacement))
{
return _Displacement;
}
}
if (layer >= CollisionLayer.AfterDynamicWaves)
{
if (layers.HasFlag(CollisionLayers.DynamicWaves) && _Water.DynamicWavesLod.Enabled)
{
return _DynamicWaves;
}
}
return _AnimatedWaves;
}
public int Query(int hash, float minimumLength, Vector3[] points, float[] heights, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything)
{
return GetProvider(layer).Query(hash, minimumLength, points, heights, normals, velocities);
}
public int Query(int hash, float minimumLength, Vector3[] points, Vector3[] displacements, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything)
{
return GetProvider(layer).Query(hash, minimumLength, points, displacements, normals, velocities);
}
public void UpdateQueries(WaterRenderer water, CollisionLayer layer)
{
switch (layer)
{
case CollisionLayer.Everything: _Displacement.UpdateQueries(water); break;
case CollisionLayer.AfterAnimatedWaves: _AnimatedWaves.UpdateQueries(water); break;
case CollisionLayer.AfterDynamicWaves: _DynamicWaves.UpdateQueries(water); break;
}
}
public void UpdateQueries(WaterRenderer water)
{
_Displacement.UpdateQueries(water);
}
public void CleanUp()
{
_AnimatedWaves.CleanUp();
_DynamicWaves.CleanUp();
_Displacement.CleanUp();
}
}
static partial class Extensions
{
public static void UpdateQueries(this ICollisionProvider self, WaterRenderer water, CollisionLayer layer) => (self as CollisionQueryWithPasses)?.UpdateQueries(water, layer);
public static void UpdateQueries(this ICollisionProvider self, WaterRenderer water) => (self as IQueryable)?.UpdateQueries(water);
public static void CleanUp(this ICollisionProvider self) => (self as IQueryable)?.CleanUp();
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 3cddd187674954ce1a0e2e19017a744a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,98 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Helper to trace a ray against the water surface.
/// </summary>
/// <remarks>
/// Works by sampling at a set of points along the ray and interpolating the
/// intersection location.
/// </remarks>
public sealed class RayCastHelper : Internal.SampleHelper
{
readonly float _RayStepSize;
readonly float _MinimumLength;
/// <summary>
/// The length of the ray and the step size must be given here. The smaller the step size, the greater the accuracy.
/// </summary>
/// <param name="rayLength">Length of the ray.</param>
/// <param name="rayStepSize">Size of the step. With length the number of steps is computed.</param>
public RayCastHelper(float rayLength, float rayStepSize = 2f) : base(ComputeQueryCount(rayLength, ref rayStepSize))
{
_RayStepSize = rayStepSize;
// Waves go max double along min length. Thats too much - only allow half of a wave per step.
_MinimumLength = _RayStepSize * 4f;
}
static int ComputeQueryCount(float rayLength, ref float rayStepSize)
{
Debug.Assert(rayLength > 0f);
Debug.Assert(rayStepSize > 0f);
var stepCount = Mathf.CeilToInt(rayLength / rayStepSize) + 1;
var maxStepCount = 128;
if (stepCount > maxStepCount)
{
stepCount = maxStepCount;
rayStepSize = rayLength / (stepCount - 1f);
Debug.LogWarning($"Crest: RayTraceHelper: ray steps exceed maximum ({maxStepCount}), step size increased to {rayStepSize} to reduce step count.");
}
return stepCount;
}
/// <summary>
/// Call this once each frame to do the query.
/// </summary>
/// <param name="origin">World space position of ray origin</param>
/// <param name="direction">World space ray direction</param>
/// <param name="distance">The distance along the ray to the first intersection with the water surface.</param>
/// <param name="layer">The layer this ray targets.</param>
/// <returns>True if the results have come back from the GPU, and if the ray intersects the water surface.</returns>
public bool RayCast(Vector3 origin, Vector3 direction, out float distance, CollisionLayer layer = CollisionLayer.Everything)
{
distance = -1f;
Validate(allowMultipleCallsPerFrame: false);
var water = WaterRenderer.Instance;
var provider = water == null ? null : water.AnimatedWavesLod.Provider;
if (provider == null) return false;
for (var i = 0; i < _QueryPosition.Length; i++)
{
_QueryPosition[i] = origin + i * _RayStepSize * direction;
}
var status = provider.Query(GetHashCode(), _MinimumLength, _QueryPosition, _QueryResult, null, null, layer);
if (!provider.RetrieveSucceeded(status))
{
return false;
}
// Now that data is available, compare the height of the water to the height of each point of the ray. If
// the ray crosses the surface, the distance to the intersection is interpolated from the heights.
for (var i = 1; i < _QueryPosition.Length; i++)
{
var height0 = _QueryResult[i - 1].y + water.SeaLevel - _QueryPosition[i - 1].y;
var height1 = _QueryResult[i].y + water.SeaLevel - _QueryPosition[i].y;
if (Mathf.Sign(height0) != Mathf.Sign(height1))
{
var prop = Mathf.Abs(height0) / (Mathf.Abs(height0) + Mathf.Abs(height1));
distance = (i - 1 + prop) * _RayStepSize;
break;
}
}
return distance >= 0f;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: f1f458d6962ed46be8f00bdff45b2725
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,44 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Debug draw a line trace from this gameobjects position, in this gameobjects forward direction.
/// </summary>
[@ExecuteDuringEditMode]
[AddComponentMenu(Constants.k_MenuPrefixDebug + "Ray Cast Visualizer")]
sealed class RayTraceVisualizer : ManagedBehaviour<WaterRenderer>
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
readonly RayCastHelper _RayCast = new(50f, 2f);
private protected override System.Action<WaterRenderer> OnUpdateMethod => OnUpdate;
void OnUpdate(WaterRenderer water)
{
if (water.AnimatedWavesLod.Provider == null)
{
return;
}
// Even if only a single ray trace is desired, this still must be called every frame until it returns true
if (_RayCast.RayCast(transform.position, transform.forward, out var dist))
{
var endPos = transform.position + transform.forward * dist;
Debug.DrawLine(transform.position, endPos, Color.green);
CollisionAreaVisualizer.DebugDrawCross(endPos, 2f, Color.green, 0f);
}
else
{
Debug.DrawLine(transform.position, transform.position + transform.forward * 50f, Color.red);
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 159faa53a32b34aff9d79e974c14d100
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: a9f84066852dc4c839cf39679aa347db
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,34 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
// Linter does not support mixing inheritdoc plus defining own parameters.
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
namespace WaveHarmonic.Crest
{
/// <summary>
/// Interface for an object that returns water depth and distance to water edge.
/// </summary>
public interface IDepthProvider : IQueryProvider
{
internal static NoneProvider None { get; } = new();
internal sealed class NoneProvider : IDepthProvider
{
public int Query(int _0, float _1, Vector3[] _2, Vector3[] result)
{
if (result != null) System.Array.Clear(result, 0, result.Length);
return 0;
}
}
/// <summary>
/// Query water depth data at a set of points.
/// </summary>
/// <param name="results">Water depth and distance to shoreline (XY respectively). Both are signed.</param>
/// <inheritdoc cref="IQueryProvider.Query(int, float, Vector3[], int)" />
int Query(int hash, float minimumLength, Vector3[] points, Vector3[] results);
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 8c8c76a0ab417499bb25ecea61560108
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,30 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
sealed class DepthQuery : QueryBase, IDepthProvider
{
public DepthQuery(WaterRenderer water) : base(water) { }
protected override int Kernel => 2;
public override int Query(int hash, float minimumSpatialLength, Vector3[] queries, Vector3[] results)
{
var id = base.Query(hash, minimumSpatialLength, queries, results);
// Infinity will become NaN. Convert back to infinity.
// Negative infinity should not happen.
for (var i = 0; i < results.Length; i++)
{
var value = results[i];
if (float.IsNaN(value.x)) value.x = float.PositiveInfinity;
if (float.IsNaN(value.y)) value.y = float.PositiveInfinity;
results[i] = value;
}
return id;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 92350636082384718babd120eff1cbf0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: cacd27cd27186443ba5376920b9fd300
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,37 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
// Linter does not support mixing inheritdoc + defining own parameters.
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
namespace WaveHarmonic.Crest
{
/// <summary>
/// Interface for an object that returns water surface displacement and height.
/// </summary>
public interface IFlowProvider : IQueryProvider
{
internal static NoneProvider None { get; } = new();
/// <summary>
/// Gives a stationary water (no horizontal flow).
/// </summary>
internal sealed class NoneProvider : IFlowProvider
{
public int Query(int _0, float _1, Vector3[] _2, Vector3[] result)
{
if (result != null) System.Array.Clear(result, 0, result.Length);
return 0;
}
}
/// <summary>
/// Query water flow data (horizontal motion) at a set of points.
/// </summary>
/// <param name="results">Water surface flow velocities at the query positions.</param>
/// <inheritdoc cref="IQueryProvider.Query(int, float, Vector3[], int)" />
int Query(int hash, float minimumLength, Vector3[] points, Vector3[] results);
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c710a5a446bea43bdbd39db4be198f98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,14 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
namespace WaveHarmonic.Crest
{
/// <summary>
/// Samples horizontal motion of water volume
/// </summary>
sealed class FlowQuery : QueryBase, IFlowProvider
{
public FlowQuery(WaterRenderer water) : base(water) { }
protected override int Kernel => 1;
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a646106f5d35b4abea362416da973f83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,636 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// Potential improvements
// - Half return values
// - Half minGridSize
using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Base interface for providers.
/// </summary>
public interface IQueryProvider
{
// NOTE: Here for documentation reuse.
/// <param name="hash">Unique ID for calling code. Typically acquired by calling GetHashCode.</param>
/// <param name="minimumLength">The minimum spatial length of the object, such as the width of a boat. Useful for filtering out detail when not needed. Set to zero to get full available detail.</param>
/// <param name="points">The world space points that will be queried.</param>
/// <param name="layer">The layer this query targets.</param>
/// <returns>The status of the query.</returns>
internal static int Query(int hash, float minimumLength, Vector3[] points, int layer) => 0;
/// <summary>
/// Check if the query results could be retrieved successfully using the return code
/// from Query method.
/// </summary>
/// <param name="status">The query status returned from Query.</param>
/// <returns>Whether the retrieve was successful.</returns>
bool RetrieveSucceeded(int status)
{
return (status & (int)QueryBase.QueryStatus.RetrieveFailed) == 0;
}
}
interface IQueryable
{
int ResultGuidCount { get; }
int RequestCount { get; }
int QueryCount { get; }
void UpdateQueries(WaterRenderer water);
void CleanUp();
}
/// <summary>
/// Provides heights and other physical data about the water surface. Works by uploading query positions to GPU and computing
/// the data and then transferring back the results asynchronously. An exception to this is water surface velocities - these can
/// not be computed on the GPU and are instead computed on the CPU by retaining last frames' query results and computing finite diffs.
/// </summary>
abstract class QueryBase : IQueryable
{
protected abstract int Kernel { get; }
// 4 was enough for a long time, but Linux setups seems to demand 7
const int k_MaximumRequests = 7;
const int k_MaximumGuids = 1024;
// We need only two additional queries to compute normals.
const int k_NormalAdditionalQueryCount = 2;
readonly WaterRenderer _Water;
readonly PropertyWrapperCompute _Wrapper;
readonly System.Action<AsyncGPUReadbackRequest> _DataArrivedAction;
// Must match value in compute shader
const int k_ComputeGroupSize = 64;
static class ShaderIDs
{
public static readonly int s_QueryPositions_MinimumGridSizes = Shader.PropertyToID("_Crest_QueryPositions_MinimumGridSizes");
}
const float k_FiniteDifferenceDx = 0.1f;
readonly ComputeBuffer _ComputeBufferQueries;
readonly ComputeBuffer _ComputeBufferResults;
internal const int k_DefaultMaximumQueryCount = 4096;
readonly int _MaximumQueryCount = k_DefaultMaximumQueryCount;
readonly Vector3[] _QueryPositionXZ_MinimumGridSize = new Vector3[k_DefaultMaximumQueryCount];
/// <summary>
/// Holds information about all query points. Maps from unique hash code to position in point array.
/// </summary>
sealed class SegmentRegistrar
{
// Map from guids to (segment start index, segment end index, frame number when query was made)
public Dictionary<int, Vector3Int> _Segments = new();
public int _QueryCount = 0;
}
/// <summary>
/// Since query results return asynchronously and may not return at all (in theory), we keep a ringbuffer
/// of the registrars of the last frames so that when data does come back it can be interpreted correctly.
/// </summary>
sealed class SegmentRegistrarRingBuffer
{
// Requests in flight plus 2 held values, plus one current
static readonly int s_PoolSize = k_MaximumRequests + 2 + 1;
readonly SegmentRegistrar[] _Segments = new SegmentRegistrar[s_PoolSize];
public int _SegmentRelease = 0;
public int _SegmentAcquire = 0;
public SegmentRegistrar Current => _Segments[_SegmentAcquire];
public SegmentRegistrarRingBuffer()
{
for (var i = 0; i < _Segments.Length; i++)
{
_Segments[i] = new();
}
}
public void AcquireNew()
{
var lastIndex = _SegmentAcquire;
{
var newSegmentAcquire = (_SegmentAcquire + 1) % _Segments.Length;
if (newSegmentAcquire == _SegmentRelease)
{
// The last index has incremented and landed on the first index. This shouldn't happen normally, but
// can happen if the Scene and Game view are not visible, in which case async readbacks dont get processed
// and the pipeline blocks up.
#if !UNITY_EDITOR
Debug.LogError("Crest: Query ring buffer exhausted. Please report this to developers.");
#endif
return;
}
_SegmentAcquire = newSegmentAcquire;
}
// Copy the registrations across from the previous frame. This makes queries persistent. This is needed because
// queries are often made from FixedUpdate(), and at high framerates this may not be called, which would mean
// the query would get lost and this leads to stuttering and other artifacts.
{
_Segments[_SegmentAcquire]._QueryCount = 0;
_Segments[_SegmentAcquire]._Segments.Clear();
foreach (var segment in _Segments[lastIndex]._Segments)
{
var age = Time.frameCount - segment.Value.z;
// Don't keep queries around if they have not be active in the last 10 frames
if (age < 10)
{
// Compute a new segment range - we may have removed some segments that were too old, so this ensures
// we have a nice compact array of queries each frame rather than accumulating persistent air bubbles
var newSegment = segment.Value;
newSegment.x = _Segments[_SegmentAcquire]._QueryCount;
newSegment.y = newSegment.x + (segment.Value.y - segment.Value.x);
_Segments[_SegmentAcquire]._QueryCount = newSegment.y + 1;
_Segments[_SegmentAcquire]._Segments.Add(segment.Key, newSegment);
}
}
}
}
public void ReleaseLast()
{
_SegmentRelease = (_SegmentRelease + 1) % _Segments.Length;
}
public void RemoveRegistrations(int key)
{
// Remove the guid for all of the next spare segment registrars. However, don't touch the ones that are being
// used for active requests.
var i = _SegmentAcquire;
while (true)
{
if (_Segments[i]._Segments.ContainsKey(key))
{
_Segments[i]._Segments.Remove(key);
}
i = (i + 1) % _Segments.Length;
if (i == _SegmentRelease)
{
break;
}
}
}
public void ClearAvailable()
{
// Extreme approach - flush all segments for next spare registrars (but don't touch ones being used for active requests)
var i = _SegmentAcquire;
while (true)
{
_Segments[i]._Segments.Clear();
_Segments[i]._QueryCount = 0;
i = (i + 1) % _Segments.Length;
if (i == _SegmentRelease)
{
break;
}
}
}
public void ClearAll()
{
for (var i = 0; i < _Segments.Length; i++)
{
_Segments[i]._QueryCount = 0;
_Segments[i]._Segments.Clear();
}
}
}
readonly SegmentRegistrarRingBuffer _SegmentRegistrarRingBuffer = new();
NativeArray<Vector3> _QueryResults;
float _QueryResultsTime = -1f;
Dictionary<int, Vector3Int> _ResultSegments;
NativeArray<Vector3> _QueryResultsLast;
float _QueryResultsTimeLast = -1f;
Dictionary<int, Vector3Int> _ResultSegmentsLast;
struct ReadbackRequest
{
public AsyncGPUReadbackRequest _Request;
public float _DataTimestamp;
public Dictionary<int, Vector3Int> _Segments;
}
readonly List<ReadbackRequest> _Requests = new();
public enum QueryStatus
{
OK = 0,
RetrieveFailed = 1,
PostFailed = 2,
NotEnoughDataForVels = 4,
VelocityDataInvalidated = 8,
InvalidDtForVelocity = 16,
}
public QueryBase(WaterRenderer water)
{
_Water = water;
_DataArrivedAction = new(DataArrived);
if (_MaximumQueryCount != water._AnimatedWavesLod.MaximumQueryCount)
{
_MaximumQueryCount = water._AnimatedWavesLod.MaximumQueryCount;
_QueryPositionXZ_MinimumGridSize = new Vector3[_MaximumQueryCount];
}
_ComputeBufferQueries = new(_MaximumQueryCount, 12, ComputeBufferType.Default);
_ComputeBufferResults = new(_MaximumQueryCount, 12, ComputeBufferType.Default);
_QueryResults = new(_MaximumQueryCount, Allocator.Persistent, NativeArrayOptions.ClearMemory);
_QueryResultsLast = new(_MaximumQueryCount, Allocator.Persistent, NativeArrayOptions.ClearMemory);
var shader = WaterResources.Instance.Compute._Query;
if (shader == null)
{
Debug.LogError($"Crest: Could not load Query compute shader");
return;
}
_Wrapper = new(water.SimulationBuffer, shader, Kernel);
}
/// <summary>
/// Takes a unique request ID and some world space XZ positions, and computes the displacement vector that lands at this position,
/// to a good approximation. The world space height of the water at that position is then SeaLevel + displacement.y.
/// </summary>
protected bool UpdateQueryPoints(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] queryNormals)
{
if (queryPoints.Length + _SegmentRegistrarRingBuffer.Current._QueryCount > _MaximumQueryCount)
{
Debug.LogError($"Crest: Max query count ({_MaximumQueryCount}) exceeded, increase the max query count in the Animated Waves Settings to support a higher number of queries.");
return false;
}
var segmentRetrieved = false;
// We'll send in 2 points to get normals
var countPts = queryPoints != null ? queryPoints.Length : 0;
var countNorms = queryNormals != null ? queryNormals.Length : 0;
var countTotal = countPts + countNorms * k_NormalAdditionalQueryCount;
if (_SegmentRegistrarRingBuffer.Current._Segments.TryGetValue(ownerHash, out var segment))
{
var segmentSize = segment.y - segment.x + 1;
if (segmentSize == countTotal)
{
// Update frame count
segment.z = Time.frameCount;
_SegmentRegistrarRingBuffer.Current._Segments[ownerHash] = segment;
segmentRetrieved = true;
}
else
{
_SegmentRegistrarRingBuffer.Current._Segments.Remove(ownerHash);
}
}
if (countTotal == 0)
{
// No query data
return false;
}
if (!segmentRetrieved)
{
if (_SegmentRegistrarRingBuffer.Current._Segments.Count >= k_MaximumGuids)
{
Debug.LogError("Crest: Too many guids registered with CollProviderCompute. Increase s_maxGuids.");
return false;
}
segment.x = _SegmentRegistrarRingBuffer.Current._QueryCount;
segment.y = segment.x + countTotal - 1;
segment.z = Time.frameCount;
_SegmentRegistrarRingBuffer.Current._Segments.Add(ownerHash, segment);
_SegmentRegistrarRingBuffer.Current._QueryCount += countTotal;
//Debug.Log("Crest: Added points for " + guid);
}
// The smallest wavelengths should repeat no more than twice across the smaller spatial length. Unless we're
// in the last LOD - then this is the best we can do.
var minWavelength = minSpatialLength / 2f;
var samplesPerWave = 2f;
var minGridSize = minWavelength / samplesPerWave;
if (countPts + segment.x > _QueryPositionXZ_MinimumGridSize.Length)
{
Debug.LogError("Crest: Too many wave height queries. Increase Max Query Count in the Animated Waves Settings.");
return false;
}
for (var pointi = 0; pointi < countPts; pointi++)
{
_QueryPositionXZ_MinimumGridSize[pointi + segment.x].x = queryPoints[pointi].x;
_QueryPositionXZ_MinimumGridSize[pointi + segment.x].y = queryPoints[pointi].z;
_QueryPositionXZ_MinimumGridSize[pointi + segment.x].z = minGridSize;
}
// To compute each normal, post 2 query points (reuse point above)
for (var normi = 0; normi < countNorms; normi++)
{
var arrIdx = segment.x + countPts + k_NormalAdditionalQueryCount * normi;
_QueryPositionXZ_MinimumGridSize[arrIdx].x = queryNormals[normi].x + k_FiniteDifferenceDx;
_QueryPositionXZ_MinimumGridSize[arrIdx].y = queryNormals[normi].z;
_QueryPositionXZ_MinimumGridSize[arrIdx].z = minGridSize;
arrIdx += 1;
_QueryPositionXZ_MinimumGridSize[arrIdx].x = queryNormals[normi].x;
_QueryPositionXZ_MinimumGridSize[arrIdx].y = queryNormals[normi].z + k_FiniteDifferenceDx;
_QueryPositionXZ_MinimumGridSize[arrIdx].z = minGridSize;
}
return true;
}
/// <summary>
/// Signal that we're no longer servicing queries. Note this leaves an air bubble in the query buffer.
/// </summary>
void RemoveQueryPoints(int guid)
{
_SegmentRegistrarRingBuffer.RemoveRegistrations(guid);
}
/// <summary>
/// Remove air bubbles from the query array. Currently this lazily just nukes all the registered
/// query IDs so they'll be recreated next time (generating garbage).
/// </summary>
void CompactQueryStorage()
{
_SegmentRegistrarRingBuffer.ClearAvailable();
}
/// <summary>
/// Copy out displacements, heights, normals. Pass null if info is not required.
/// </summary>
protected bool RetrieveResults(int guid, Vector3[] displacements, float[] heights, Vector3[] normals)
{
if (_ResultSegments == null)
{
return false;
}
// Check if there are results that came back for this guid
if (!_ResultSegments.TryGetValue(guid, out var segment))
{
// Guid not found - no result
return false;
}
var countPoints = 0;
if (displacements != null) countPoints = displacements.Length;
if (heights != null) countPoints = heights.Length;
if (displacements != null && heights != null) Debug.Assert(displacements.Length == heights.Length);
var countNorms = normals != null ? normals.Length : 0;
if (countPoints > 0)
{
// Retrieve Results
if (displacements != null) _QueryResults.Slice(segment.x, countPoints).CopyTo(displacements);
// Retrieve Result heights
if (heights != null)
{
var seaLevel = _Water.SeaLevel;
for (var i = 0; i < countPoints; i++)
{
heights[i] = seaLevel + _QueryResults[i + segment.x].y;
}
}
}
if (countNorms > 0)
{
var firstNorm = segment.x + countPoints;
var dx = -Vector3.right * k_FiniteDifferenceDx;
var dz = -Vector3.forward * k_FiniteDifferenceDx;
for (var i = 0; i < countNorms; i++)
{
var p = _QueryResults[i + segment.x];
var px = dx + _QueryResults[firstNorm + k_NormalAdditionalQueryCount * i];
var pz = dz + _QueryResults[firstNorm + k_NormalAdditionalQueryCount * i + 1];
normals[i] = Vector3.Cross(p - px, p - pz).normalized;
normals[i].y *= -1f;
}
}
return true;
}
/// <summary>
/// Compute time derivative of the displacements by calculating difference from last query. More complicated than it would seem - results
/// may not be available in one or both of the results, or the query locations in the array may change.
/// </summary>
protected int CalculateVelocities(int ownerHash, Vector3[] results)
{
// Need at least 2 returned results to do finite difference
if (_QueryResultsTime < 0f || _QueryResultsTimeLast < 0f)
{
return 1;
}
if (!_ResultSegments.TryGetValue(ownerHash, out var segment))
{
return (int)QueryStatus.RetrieveFailed;
}
if (!_ResultSegmentsLast.TryGetValue(ownerHash, out var segmentLast))
{
return (int)QueryStatus.NotEnoughDataForVels;
}
if ((segment.y - segment.x) != (segmentLast.y - segmentLast.x))
{
// Number of queries changed - can't handle that
return (int)QueryStatus.VelocityDataInvalidated;
}
var dt = _QueryResultsTime - _QueryResultsTimeLast;
if (dt < 0.0001f)
{
return (int)QueryStatus.InvalidDtForVelocity;
}
var count = results.Length;
for (var i = 0; i < count; i++)
{
results[i] = (_QueryResults[i + segment.x] - _QueryResultsLast[i + segmentLast.x]) / dt;
}
return 0;
}
/// <summary>
/// Per-frame update callback.
/// </summary>
/// <param name="water">The current <see cref="WaterRenderer"/>.</param>
public void UpdateQueries(WaterRenderer water)
{
#if UNITY_EDITOR
// Seems to be a terrible memory leak coming from creating async GPU readbacks.
// This was marked as resolved by Unity and confirmed fixed by forum posts.
// May be worth keeping. See issue #630 for more details.
if (!water._HeightQueries && !Application.isPlaying) return;
#endif
if (_SegmentRegistrarRingBuffer.Current._QueryCount > 0)
{
ExecuteQueries();
// Remove oldest requests if we have hit the limit
while (_Requests.Count >= k_MaximumRequests)
{
_Requests.RemoveAt(0);
}
ReadbackRequest request;
request._DataTimestamp = Time.time - Time.deltaTime;
request._Request = AsyncGPUReadback.Request(_ComputeBufferResults, _DataArrivedAction);
request._Segments = _SegmentRegistrarRingBuffer.Current._Segments;
_Requests.Add(request);
_SegmentRegistrarRingBuffer.AcquireNew();
}
}
void ExecuteQueries()
{
_ComputeBufferQueries.SetData(_QueryPositionXZ_MinimumGridSize, 0, 0, _SegmentRegistrarRingBuffer.Current._QueryCount);
_Wrapper.SetBuffer(ShaderIDs.s_QueryPositions_MinimumGridSizes, _ComputeBufferQueries);
_Wrapper.SetBuffer(Crest.ShaderIDs.s_Target, _ComputeBufferResults);
var numGroups = (_SegmentRegistrarRingBuffer.Current._QueryCount + k_ComputeGroupSize - 1) / k_ComputeGroupSize;
_Wrapper.Dispatch(numGroups, 1, 1);
}
/// <summary>
/// Called when a compute buffer has been read back from the GPU to the CPU.
/// </summary>
void DataArrived(AsyncGPUReadbackRequest req)
{
// Can get callbacks after disable, so detect this.
if (!_QueryResults.IsCreated)
{
_Requests.Clear();
return;
}
// Remove any error requests
for (var i = _Requests.Count - 1; i >= 0; --i)
{
if (_Requests[i]._Request.hasError)
{
_Requests.RemoveAt(i);
_SegmentRegistrarRingBuffer.ReleaseLast();
}
}
// Find the last request that was completed
var lastDoneIndex = _Requests.Count - 1;
while (lastDoneIndex >= 0 && !_Requests[lastDoneIndex]._Request.done)
{
--lastDoneIndex;
}
// If there is a completed request, process it
if (lastDoneIndex >= 0)
{
// Update "last" results
(_QueryResults, _QueryResultsLast) = (_QueryResultsLast, _QueryResults);
_QueryResultsTimeLast = _QueryResultsTime;
_ResultSegmentsLast = _ResultSegments;
var data = _Requests[lastDoneIndex]._Request.GetData<Vector3>();
data.CopyTo(_QueryResults);
_QueryResultsTime = _Requests[lastDoneIndex]._DataTimestamp;
_ResultSegments = _Requests[lastDoneIndex]._Segments;
}
// Remove all the requests up to the last completed one
for (var i = lastDoneIndex; i >= 0; --i)
{
_Requests.RemoveAt(i);
_SegmentRegistrarRingBuffer.ReleaseLast();
}
}
/// <summary>
/// On destroy, to clean up resources.
/// </summary>
public void CleanUp()
{
_ComputeBufferQueries.Dispose();
_ComputeBufferResults.Dispose();
if (_QueryResults.IsCreated) _QueryResults.Dispose();
if (_QueryResultsLast.IsCreated) _QueryResultsLast.Dispose();
_SegmentRegistrarRingBuffer.ClearAll();
}
public virtual int Query(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] results)
{
var result = (int)QueryStatus.OK;
if (!UpdateQueryPoints(ownerHash, minSpatialLength, queryPoints, null))
{
result |= (int)QueryStatus.PostFailed;
}
if (!RetrieveResults(ownerHash, results, null, null))
{
result |= (int)QueryStatus.RetrieveFailed;
}
return result;
}
public int ResultGuidCount => _ResultSegments != null ? _ResultSegments.Count : 0;
public int RequestCount => _Requests != null ? _Requests.Count : 0;
public int QueryCount => _SegmentRegistrarRingBuffer != null ? _SegmentRegistrarRingBuffer.Current._QueryCount : 0;
}
static partial class Extensions
{
public static void UpdateQueries(this IQueryProvider self, WaterRenderer water) => (self as IQueryable)?.UpdateQueries(water);
public static void CleanUp(this IQueryProvider self) => (self as IQueryable)?.CleanUp();
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: b047926acd1d644acb5797a1d2b7b7b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,267 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using UnityEngine.Events;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
/// <summary>
/// What transform to use for queries.
/// </summary>
public enum QuerySource
{
/// <summary>
/// This game object's transform.
/// </summary>
Transform,
/// <summary>
/// The viewer's transform.
/// </summary>
/// <remarks>
/// The viewer is the main camera the system uses.
/// </remarks>
Viewer
}
/// <summary>
/// Emits events (UnityEvents) based on the sampled water data.
/// </summary>
[AddComponentMenu(Constants.k_MenuPrefixScripts + "Query Events")]
[@HelpURL("Manual/Events.html#query-events")]
public sealed partial class QueryEvents : ManagedBehaviour<WaterRenderer>
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Tooltip("What transform should the queries be based on.\n\n\"Viewer\" will reuse queries already performed by the Water Renderer")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
QuerySource _Source;
[Tooltip(ICollisionProvider.k_LayerTooltip)]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal CollisionLayer _Layer;
[Header("Distance From Water Surface")]
[Tooltip("The minimum wavelength for queries.\n\nThe higher the value, the more smaller waves will be ignored when sampling the water surface.")]
[@Predicated(nameof(_Source), inverted: true, nameof(QuerySource.Transform))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _MinimumWavelength = 1f;
[@Label("Signed")]
[Tooltip("Whether to keep the sign of the value (ie positive/negative).\n\nA positive value means the query point is above the surface, while a negative means it below the surface.")]
[@Predicated(nameof(_DistanceFromSurfaceUseCurve), inverted: true)]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DistanceFromSurfaceSigned;
[@Label("Maximum Distance")]
[Tooltip("The maximum distance.\n\nAlways use a real distance in real units (ie not normalized).")]
[@GenerateAPI]
[UnityEngine.Serialization.FormerlySerializedAs("_MaximumDistance")]
[@DecoratedField, SerializeField]
float _DistanceFromSurfaceMaximum = 100f;
[@Label("Use Curve")]
[Tooltip("Whether to apply a curve to the distance.\n\nNormalizes and inverts the distance to be between zero and one, then applies a curve.")]
[@GenerateAPI]
[UnityEngine.Serialization.FormerlySerializedAs("_NormaliseDistance")]
[@DecoratedField, SerializeField]
bool _DistanceFromSurfaceUseCurve = true;
[@Label("Curve")]
[Tooltip("Apply a curve to the distance.\n\nValues towards \"one\" means closer to the water surface.")]
[@Predicated(nameof(_DistanceFromSurfaceUseCurve))]
[@GenerateAPI]
[UnityEngine.Serialization.FormerlySerializedAs("_DistanceCurve")]
[@DecoratedField, SerializeField]
AnimationCurve _DistanceFromSurfaceCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
[Header("Distance From Water Edge")]
[@Label("Signed")]
[Tooltip("Whether to keep the sign of the value (ie positive/negative).\n\nA positive value means the query point is over water, while a negative means it is over land.")]
[@Predicated(nameof(_DistanceFromEdgeUseCurve), inverted: true)]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DistanceFromEdgeSigned;
[@Label("Maximum Distance")]
[Tooltip("The maximum distance.\n\nAlways use a real distance in real units (ie not normalized).")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _DistanceFromEdgeMaximum = 100f;
[@Label("Use Curve")]
[Tooltip("Apply a curve to the distance.\n\nNormalizes and inverts the distance to be between zero and one, then applies a curve.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DistanceFromEdgeUseCurve = true;
[@Label("Curve")]
[Tooltip("Apply a curve to the distance.\n\nValues towards \"one\" means closer to the water's edge.")]
[@Predicated(nameof(_DistanceFromEdgeUseCurve))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
AnimationCurve _DistanceFromEdgeCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
[Header("Events")]
[Tooltip("Triggers when game object goes below water surface.\n\nTriggers once per state change.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
UnityEvent _OnBelowWater = new();
[Tooltip("Triggers when game object goes above water surface.\n\nTriggers once per state change.")]
[@GenerateAPI]
[SerializeField]
UnityEvent _OnAboveWater = new();
[Tooltip("Sends the distance from the water surface.")]
[@GenerateAPI]
[UnityEngine.Serialization.FormerlySerializedAs("_DistanceFromWater")]
[SerializeField]
internal UnityEvent<float> _DistanceFromSurface = new();
[Tooltip("Sends the distance from the water's edge.")]
[@GenerateAPI]
[SerializeField]
internal UnityEvent<float> _DistanceFromEdge = new();
bool HasOnBelowWater => OnBelowWater != null || !_OnBelowWater.IsEmpty();
bool HasOnAboveWater => OnAboveWater != null || !_OnAboveWater.IsEmpty();
bool HasDistanceFromSurface => DistanceFromSurface != null || !_DistanceFromSurface.IsEmpty();
bool HasDistanceFromEdge => DistanceFromEdge != null || !_DistanceFromEdge.IsEmpty();
// Store state
bool _IsAboveSurface = false;
bool _IsFirstUpdate = true;
readonly SampleCollisionHelper _SampleHeightHelper = new();
readonly SampleDepthHelper _SampleDepthHelper = new();
private protected override System.Action<WaterRenderer> OnUpdateMethod => OnUpdate;
void OnUpdate(WaterRenderer water)
{
// Transform requires queries which need to be in Update.
if (_Source != QuerySource.Transform) return;
SendDistanceFromSurface(water);
SendDistanceFromEdge(water);
}
private protected override System.Action<WaterRenderer> OnLateUpdateMethod => OnLateUpdate;
void OnLateUpdate(WaterRenderer water)
{
// Viewer is set between Update and LateUpdate.
if (_Source != QuerySource.Viewer) return;
SendDistanceFromSurface(water);
SendDistanceFromEdge(water);
}
void SendDistanceFromSurface(WaterRenderer water)
{
if (!HasDistanceFromSurface && !HasOnAboveWater && !HasOnBelowWater)
{
return;
}
var distance = water.ViewerHeightAboveWater;
if (_Source == QuerySource.Transform)
{
if (!_SampleHeightHelper.SampleHeight(transform.position, out var height, minimumLength: 2f * _MinimumWavelength, _Layer)) return;
distance = transform.position.y - height;
}
var isAboveSurface = distance > 0;
// Has the below/above water surface state changed?
if (_IsAboveSurface != isAboveSurface || _IsFirstUpdate)
{
_IsAboveSurface = isAboveSurface;
_IsFirstUpdate = false;
if (_IsAboveSurface)
{
_OnAboveWater?.Invoke();
OnAboveWater?.Invoke();
}
else
{
_OnBelowWater?.Invoke();
OnBelowWater?.Invoke();
}
}
// Save some processing when not being used.
if (HasDistanceFromSurface)
{
distance = Mathf.Clamp(distance, -_DistanceFromSurfaceMaximum, _DistanceFromSurfaceMaximum);
// Throw away sign when using a curve. Cannot think of a use case for negative
// normalized numbers.
if (!_DistanceFromSurfaceSigned || _DistanceFromSurfaceUseCurve)
{
distance = Mathf.Abs(distance);
}
if (_DistanceFromSurfaceUseCurve)
{
// Normalize for the curve. Invert so towards one is closer to target.
distance = _DistanceFromSurfaceCurve.Evaluate(1f - distance / _DistanceFromSurfaceMaximum);
}
_DistanceFromSurface?.Invoke(distance);
DistanceFromSurface?.Invoke(distance);
}
}
void SendDistanceFromEdge(WaterRenderer water)
{
// No events to process.
if (!HasDistanceFromEdge)
{
return;
}
var distance = water.ViewerDistanceToShoreline;
if (_Source == QuerySource.Transform)
{
if (!_SampleDepthHelper.SampleDistanceToWaterEdge(transform.position, out distance))
{
return;
}
}
distance = Mathf.Clamp(distance, -_DistanceFromEdgeMaximum, _DistanceFromEdgeMaximum);
// Throw away sign when using a curve. Cannot think of a use case for negative
// normalized numbers.
if (!_DistanceFromEdgeSigned || _DistanceFromEdgeUseCurve)
{
distance = Mathf.Abs(distance);
}
if (_DistanceFromEdgeUseCurve)
{
// Normalize for the curve. Invert so towards one is closer to target.
distance = _DistanceFromEdgeCurve.Evaluate(1f - distance / _DistanceFromEdgeMaximum);
}
_DistanceFromEdge?.Invoke(distance);
DistanceFromEdge?.Invoke(distance);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 555f01a6f203a4b6db9ec558f7451e4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,320 +0,0 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
// Linter does not support mixing inheritdoc plus defining own parameters.
#pragma warning disable CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
namespace WaveHarmonic.Crest.Internal
{
/// <summary>
/// Base class for sample helpers which sample a single point.
/// </summary>
/// <remarks>
/// It is not particularly efficient to sample a single point, but is a fairly
/// common case.
/// </remarks>
public abstract class SampleHelper
{
private protected readonly Vector3[] _QueryPosition;
private protected readonly Vector3[] _QueryResult;
int _LastFrame = -1;
private protected SampleHelper(int queryCount = 1)
{
_QueryPosition = new Vector3[queryCount];
_QueryResult = new Vector3[queryCount];
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private protected void Validate(bool allowMultipleCallsPerFrame)
{
if (Application.isPlaying && !Time.inFixedTimeStep && !allowMultipleCallsPerFrame && _LastFrame == Time.frameCount)
{
var type = GetType().Name;
Debug.LogWarning($"Crest: {type} sample called multiple times in one frame which is not expected. Each {type} object services a single sample per frame. To perform multiple queries, create multiple {type} objects or use the query provider API directly.");
}
_LastFrame = Time.frameCount;
}
// The first method is there just to get inheritdoc to work as it does not like
// inheriting params plus adding additional params.
/// <remarks>
/// Only call once per frame.
/// </remarks>
/// <param name="position">World space position to sample.</param>
/// <param name="minimumLength">
/// The smallest length scale you are interested in. If you are sampling data for boat physics,
/// pass in the boats width. Larger objects will ignore smaller details in the data.
/// </param>
/// <param name="layer">The collision layer to target.</param>
/// <returns>Whether the query was successful.</returns>
bool Sample(Vector3 position, float minimumLength, CollisionLayer layer) => false;
}
}
namespace WaveHarmonic.Crest
{
/// <summary>
/// Helper to obtain the water surface collision at a single point per frame.
/// </summary>
/// <inheritdoc/>
public sealed class SampleCollisionHelper : Internal.SampleHelper
{
readonly Vector3[] _QueryResultNormal = new Vector3[1];
readonly Vector3[] _QueryResultVelocity = new Vector3[1];
enum QueryType
{
Displacement,
Height,
}
[System.Flags]
enum QueryOptions
{
None,
Velocity,
Normal,
All = Velocity | Normal,
}
bool Sample(int id, Vector3 position, out Vector3 displacement, out float height, out Vector3 velocity, out Vector3 normal, QueryType type, QueryOptions options, CollisionLayer layer = CollisionLayer.Everything, float minimumLength = 0f, bool allowMultipleCallsPerFrame = false)
{
var water = WaterRenderer.Instance;
var provider = water == null ? null : water.AnimatedWavesLod.Provider;
height = 0f;
displacement = Vector3.zero;
velocity = Vector3.zero;
normal = Vector3.up;
if (provider == null)
{
return false;
}
var isHeight = type == QueryType.Height;
var isDisplacement = type == QueryType.Displacement;
var isVelocity = (options & QueryOptions.Velocity) == QueryOptions.Velocity;
var isNormal = (options & QueryOptions.Normal) == QueryOptions.Normal;
Validate(allowMultipleCallsPerFrame);
_QueryPosition[0] = position;
var status = provider.Query
(
id,
minimumLength,
_QueryPosition,
_QueryResult,
isNormal ? _QueryResultNormal : null,
isVelocity ? _QueryResultVelocity : null,
layer
);
if (!provider.RetrieveSucceeded(status))
{
height = water.SeaLevel;
return false;
}
if (isHeight) height = _QueryResult[0].y + water.SeaLevel;
if (isDisplacement) displacement = _QueryResult[0];
if (isVelocity) velocity = _QueryResultVelocity[0];
if (isNormal) normal = _QueryResultNormal[0];
return true;
}
internal bool SampleHeight(int id, Vector3 position, out float height, CollisionLayer layer = CollisionLayer.Everything, float minimumLength = 0f, bool allowMultipleCallsPerFrame = false)
{
return Sample(id, position, out _, out height, out _, out _, QueryType.Height, QueryOptions.None, layer, minimumLength, allowMultipleCallsPerFrame);
}
internal bool SampleHeight(int id, Vector3 position, out float height, out Vector3 velocity, CollisionLayer layer = CollisionLayer.Everything, float minimumLength = 0f, bool allowMultipleCallsPerFrame = false)
{
return Sample(id, position, out _, out height, out velocity, out _, QueryType.Height, QueryOptions.Velocity, layer, minimumLength, allowMultipleCallsPerFrame);
}
internal bool SampleHeight(int id, Vector3 position, out float height, out Vector3 velocity, out Vector3 normal, CollisionLayer layer = CollisionLayer.Everything, float minimumLength = 0f, bool allowMultipleCallsPerFrame = false)
{
return Sample(id, position, out _, out height, out velocity, out normal, QueryType.Height, QueryOptions.All, layer, minimumLength, allowMultipleCallsPerFrame);
}
internal bool SampleDisplacement(int id, Vector3 position, out Vector3 displacement, out Vector3 velocity, out Vector3 normal, CollisionLayer layer = CollisionLayer.Everything, float minimumLength = 0f, bool allowMultipleCallsPerFrame = false)
{
return Sample(id, position, out displacement, out _, out velocity, out normal, QueryType.Displacement, QueryOptions.All, layer, minimumLength, allowMultipleCallsPerFrame);
}
internal bool SampleDisplacement(int id, Vector3 position, out Vector3 displacement, out Vector3 velocity, CollisionLayer layer = CollisionLayer.Everything, float minimumLength = 0f, bool allowMultipleCallsPerFrame = false)
{
return Sample(id, position, out displacement, out _, out velocity, out _, QueryType.Displacement, QueryOptions.Velocity, layer, minimumLength, allowMultipleCallsPerFrame);
}
internal bool SampleDisplacement(int id, Vector3 position, out Vector3 displacement, CollisionLayer layer = CollisionLayer.Everything, float minimumLength = 0f, bool allowMultipleCallsPerFrame = false)
{
return Sample(id, position, out displacement, out _, out _, out _, QueryType.Displacement, QueryOptions.None, layer, minimumLength, allowMultipleCallsPerFrame);
}
// The first method is there just to get inheritdoc to work as it does not like
// inheriting params plus adding additional params.
/// <summary>
/// Sample displacement data.
/// </summary>
/// <param name="displacement">The water surface displacement to point.</param>
/// <param name="height">The water surface height.</param>
/// <param name="velocity">The velocity of the water surface excluding flow velocity.</param>
/// <param name="normal">The water surface normal.</param>
/// <inheritdoc cref="Internal.SampleHelper.Sample" />
bool Sample(Vector3 position, float height, Vector3 displacement, Vector3 normal, Vector3 velocity, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => false;
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
[System.Obsolete("Please use SampleDisplacement instead. Be wary that the new API has switch the normal parameter with velocity.")]
public bool Sample(Vector3 position, out Vector3 displacement, out Vector3 normal, out Vector3 velocity, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleDisplacement(GetHashCode(), position, out displacement, out velocity, out normal, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
[System.Obsolete("Please use SampleHeight instead. Be wary that the new API has switch the normal parameter with velocity.")]
public bool Sample(Vector3 position, out float height, out Vector3 normal, out Vector3 velocity, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleHeight(GetHashCode(), position, out height, out velocity, out normal, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
[System.Obsolete("Please use SampleHeight instead. Be wary that the new API has switch the normal parameter with velocity.")]
public bool Sample(Vector3 position, out float height, out Vector3 normal, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleHeight(GetHashCode(), position, out height, out _, out normal, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
[System.Obsolete("Please use SampleHeight instead. Be wary that the new API has switch the normal parameter with velocity.")]
public bool Sample(Vector3 position, out float height, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleHeight(GetHashCode(), position, out height, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
public bool SampleDisplacement(Vector3 position, out Vector3 displacement, out Vector3 velocity, out Vector3 normal, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleDisplacement(GetHashCode(), position, out displacement, out velocity, out normal, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
public bool SampleDisplacement(Vector3 position, out Vector3 displacement, out Vector3 velocity, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleDisplacement(GetHashCode(), position, out displacement, out velocity, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
public bool SampleDisplacement(Vector3 position, out Vector3 displacement, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleDisplacement(GetHashCode(), position, out displacement, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
public bool SampleHeight(Vector3 position, out float height, out Vector3 velocity, out Vector3 normal, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleHeight(GetHashCode(), position, out height, out velocity, out normal, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
public bool SampleHeight(Vector3 position, out float height, out Vector3 velocity, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleHeight(GetHashCode(), position, out height, out velocity, layer, minimumLength);
/// <inheritdoc cref="Sample(Vector3, float, Vector3, Vector3, Vector3, float, CollisionLayer)" />
public bool SampleHeight(Vector3 position, out float height, float minimumLength = 0f, CollisionLayer layer = CollisionLayer.Everything) => SampleHeight(GetHashCode(), position, out height, layer, minimumLength);
}
/// <summary>
/// Helper to obtain the flow data (horizontal water motion) at a single point.
/// </summary>
/// <inheritdoc/>
public sealed class SampleFlowHelper : Internal.SampleHelper
{
/// <summary>
/// Sample flow data.
/// </summary>
/// <param name="flow">Filled with resulting flow.</param>
/// <returns>Whether the query was successful.</returns>
/// <inheritdoc cref="Internal.SampleHelper.Sample" />
public bool Sample(Vector3 position, out Vector2 flow, float minimumLength = 0f)
{
var water = WaterRenderer.Instance;
var flowProvider = WaterRenderer.Instance == null ? null : water.FlowLod.Provider;
if (flowProvider == null)
{
flow = Vector2.zero;
return false;
}
Validate(false);
_QueryPosition[0] = position;
var status = flowProvider.Query(GetHashCode(), minimumLength, _QueryPosition, _QueryResult);
if (!flowProvider.RetrieveSucceeded(status))
{
flow = Vector2.zero;
return false;
}
// We don't support float2 queries unfortunately, so unpack from float3
flow.x = _QueryResult[0].x;
flow.y = _QueryResult[0].z;
return true;
}
}
/// <summary>
/// Helper to obtain the depth data at a single point.
/// </summary>
public sealed class SampleDepthHelper : Internal.SampleHelper
{
bool Sample(Vector3 position, out Vector2 result)
{
var water = WaterRenderer.Instance;
var depthProvider = WaterRenderer.Instance == null ? null : water.DepthLod.Provider;
if (depthProvider == null)
{
result = Vector2.zero;
return false;
}
Validate(false);
_QueryPosition[0] = position;
var status = depthProvider.Query(GetHashCode(), minimumLength: 0, _QueryPosition, _QueryResult);
if (!depthProvider.RetrieveSucceeded(status))
{
result = Vector2.zero;
return false;
}
result = _QueryResult[0];
return true;
}
/// <summary>
/// Sample both the water depth and water edge distance.
/// </summary>
/// <param name="depth">Filled by the water depth at the query position.</param>
/// <param name="distance">Filled by the distance to water edge at the query position.</param>
/// <inheritdoc cref="Internal.SampleHelper.Sample" />
bool Sample(Vector3 position, out float depth, out float distance)
{
var success = Sample(position, out var result);
depth = result.x;
distance = result.y;
return success;
}
/// <summary>Sample water depth.</summary>
/// <inheritdoc cref="Sample(Vector3, out float, out float)"/>
bool SampleWaterDepth(Vector3 position, out float depth)
{
var success = Sample(position, out var result);
depth = result.x;
return success;
}
/// <summary>Sample water edge distance.</summary>
/// <inheritdoc cref="Sample(Vector3, out float, out float)"/>
public bool SampleDistanceToWaterEdge(Vector3 position, out float distance)
{
var success = Sample(position, out var result);
distance = result.y;
return success;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: c4b71f2a526b64377bc96c6e283e4535
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: