// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Utility;
namespace WaveHarmonic.Crest
{
///
/// Gerstner wave shape.
///
[AddComponentMenu(Constants.k_MenuPrefixInputs + "Shape Gerstner")]
public sealed partial class ShapeGerstner : ShapeWaves
{
// Waves
[@Space(10)]
[Tooltip("Use a swell spectrum as the default.\n\nUses a swell spectrum as default (when none is assigned), and disabled reverse waves.")]
[@GenerateAPI]
[@DecoratedField(order = -3), SerializeField]
bool _Swell = true;
[Tooltip("The weight of the opposing, second pair of Gerstner waves.\n\nEach Gerstner wave is actually a pair of waves travelling in opposite directions (similar to FFT). This weight is applied to the wave travelling in against-wind direction. Set to zero to obtain simple single waves which are useful for shorelines waves.")]
[Predicated(nameof(_Swell), inverted: true)]
[@Range(0f, 1f, order = -4)]
[@GenerateAPI(Getter.Custom)]
[SerializeField]
float _ReverseWaveWeight = 0.5f;
// Generation Settings
[Tooltip("How many wave components to generate in each octave.")]
[@Delayed]
[@GenerateAPI]
[SerializeField]
int _ComponentsPerOctave = 8;
[Tooltip("Change to get a different set of waves.")]
[@GenerateAPI]
[SerializeField]
int _RandomSeed = 0;
[Tooltip("Prevent data arrays from being written to so one can provide their own.")]
[@GenerateAPI]
[SerializeField]
bool _ManualGeneration;
private protected override int MinimumResolution => 8;
private protected override int MaximumResolution => 64;
float _WindSpeedWhenGenerated = -1f;
const int k_MaximumWaveComponents = 1024;
// Data for all components
///
/// Wavelengths. Requires Manual Generation to be enabled.
///
[System.NonSerialized]
public float[] _Wavelengths;
///
/// Amplitudes. Requires Manual Generation to be enabled.
///
[System.NonSerialized]
public float[] _Amplitudes;
///
/// Powers. Requires Manual Generation to be enabled.
///
[System.NonSerialized]
public float[] _Powers;
///
/// Angles. Requires Manual Generation to be enabled.
///
[System.NonSerialized]
public float[] _AngleDegrees;
///
/// Phases. Requires Manual Generation to be enabled.
///
[System.NonSerialized]
public float[] _Phases;
// Reverse.
float[] _Amplitudes2;
float[] _Phases2;
struct GerstnerCascadeParams
{
public int _StartIndex;
}
ComputeBuffer _BufferCascadeParameters;
readonly GerstnerCascadeParams[] _CascadeParameters = new GerstnerCascadeParams[k_CascadeCount + 1];
// Caution - order here impact performance. Rearranging these to match order
// they're read in the compute shader made it 50% slower..
struct GerstnerWaveComponent4
{
public Vector4 _TwoPiOverWavelength;
public Vector4 _Amplitude;
public Vector4 _WaveDirectionX;
public Vector4 _WaveDirectionZ;
public Vector4 _Omega;
public Vector4 _Phase;
public Vector4 _ChopAmplitude;
// Waves are generated in pairs, these values are for the second in the pair
public Vector4 _Amplitude2;
public Vector4 _ChopAmplitude2;
public Vector4 _Phase2;
}
ComputeBuffer _BufferWaveData;
readonly GerstnerWaveComponent4[] _WaveData = new GerstnerWaveComponent4[k_MaximumWaveComponents / 4];
ComputeShader _ShaderGerstner;
int _KernelGerstner = -1;
private protected override WaveSpectrum DefaultSpectrum => _Swell ? SwellSpectrum : WindSpectrum;
static WaveSpectrum s_SwellSpectrum;
static WaveSpectrum SwellSpectrum
{
get
{
if (s_SwellSpectrum == null)
{
s_SwellSpectrum = ScriptableObject.CreateInstance();
s_SwellSpectrum.name = "Swell Waves (auto)";
s_SwellSpectrum.hideFlags = HideFlags.DontSave | HideFlags.NotEditable;
s_SwellSpectrum._PowerDisabled[0] = true;
s_SwellSpectrum._PowerDisabled[1] = true;
s_SwellSpectrum._PowerDisabled[2] = true;
s_SwellSpectrum._PowerDisabled[3] = true;
s_SwellSpectrum._PowerDisabled[4] = true;
s_SwellSpectrum._PowerDisabled[5] = true;
s_SwellSpectrum._PowerDisabled[6] = true;
s_SwellSpectrum._PowerDisabled[7] = true;
s_SwellSpectrum._WaveDirectionVariance = 15f;
s_SwellSpectrum._Chop = 1.3f;
}
return s_SwellSpectrum;
}
}
static new class ShaderIDs
{
public static readonly int s_FirstCascadeIndex = Shader.PropertyToID("_Crest_FirstCascadeIndex");
public static readonly int s_TextureRes = Shader.PropertyToID("_Crest_TextureRes");
public static readonly int s_CascadeParams = Shader.PropertyToID("_Crest_GerstnerCascadeParams");
public static readonly int s_GerstnerWaveData = Shader.PropertyToID("_Crest_GerstnerWaveData");
}
readonly float _TwoPi = 2f * Mathf.PI;
readonly float _ReciprocalTwoPi = 1f / (2f * Mathf.PI);
internal static readonly SortedList s_Instances = new(Helpers.SiblingIndexComparison);
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void InitStatics()
{
s_Instances.Clear();
}
float GetReverseWaveWeight()
{
return _Swell ? 0f : _ReverseWaveWeight;
}
void InitData()
{
if (_WaveBuffers == null)
{
_WaveBuffers = new(_Resolution, _Resolution, 0, GraphicsFormat.R16G16B16A16_SFloat);
}
else
{
_WaveBuffers.Release();
}
{
_WaveBuffers.width = _WaveBuffers.height = _Resolution;
_WaveBuffers.wrapMode = TextureWrapMode.Clamp;
_WaveBuffers.antiAliasing = 1;
_WaveBuffers.filterMode = FilterMode.Bilinear;
_WaveBuffers.anisoLevel = 0;
_WaveBuffers.useMipMap = false;
_WaveBuffers.name = "_Crest_GerstnerCascades";
_WaveBuffers.dimension = TextureDimension.Tex2DArray;
_WaveBuffers.volumeDepth = k_CascadeCount;
_WaveBuffers.enableRandomWrite = true;
_WaveBuffers.Create();
}
_BufferCascadeParameters?.Release();
_BufferWaveData?.Release();
_BufferCascadeParameters = new(k_CascadeCount + 1, UnsafeUtility.SizeOf());
_BufferWaveData = new(k_MaximumWaveComponents / 4, UnsafeUtility.SizeOf());
_ShaderGerstner = WaterResources.Instance.Compute._Gerstner;
_KernelGerstner = _ShaderGerstner.FindKernel("Gerstner");
}
private protected override void OnUpdate(WaterRenderer water)
{
var isFirstUpdate = _FirstUpdate;
base.OnUpdate(water);
if (_WaveBuffers == null || _Resolution != _WaveBuffers.width || _BufferCascadeParameters == null || _BufferWaveData == null)
{
InitData();
}
var windSpeed = WindSpeedMPS;
if (isFirstUpdate || UpdateDataEachFrame || windSpeed != _WindSpeedWhenGenerated)
{
UpdateWaveData(water, windSpeed);
_WindSpeedWhenGenerated = windSpeed;
}
ReportMaxDisplacement(water);
}
internal override void Draw(Lod lod, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1, int slice = -1)
{
if (_LastGenerateFrameCount != Time.frameCount)
{
if (_FirstCascade >= 0 && _LastCascade >= 0)
{
UpdateGenerateWaves(buffer);
// Above changes the render target. Change it back if necessary.
if (!IsCompute) CoreUtils.SetRenderTarget(buffer, target, depthSlice: slice);
}
_LastGenerateFrameCount = Time.frameCount;
}
base.Draw(lod, buffer, target, pass, weight, slice);
}
private protected override void SetRenderParameters(WaterRenderer water, T wrapper)
{
base.SetRenderParameters(water, wrapper);
wrapper.SetVector(ShapeWaves.ShaderIDs.s_AxisX, PrimaryWaveDirection);
}
void SliceUpWaves(WaterRenderer water, float windSpeed)
{
// Do not filter cascades if blending as the blend operation might be skipped.
// Same for renderer as we do not know the blend operation.
var isFilterable = Blend != LodInputBlend.Alpha && _Mode != LodInputMode.Renderer;
_FirstCascade = isFilterable ? -1 : 0;
_LastCascade = -2;
var cascadeIdx = 0;
var componentIdx = 0;
var outputIdx = 0;
_CascadeParameters[0]._StartIndex = 0;
if (_ManualGeneration)
{
for (var i = 0; i < _WaveData.Length; i++)
{
_WaveData[i]._Phase2 = Vector4.zero;
_WaveData[i]._Amplitude2 = Vector4.zero;
_WaveData[i]._ChopAmplitude2 = Vector4.zero;
}
}
// Seek forward to first wavelength that is big enough to render into current cascades
var minWl = MinWavelength(cascadeIdx);
while (componentIdx < _Wavelengths.Length && _Wavelengths[componentIdx] < minWl)
{
componentIdx++;
}
//Debug.Log($"Crest: {cascadeIdx}: start {_cascadeParams[cascadeIdx]._startIndex} minWL {minWl}");
for (; componentIdx < _Wavelengths.Length; componentIdx++)
{
// Skip small amplitude waves
while (componentIdx < _Wavelengths.Length && _Amplitudes[componentIdx] < 0.001f)
{
componentIdx++;
}
if (componentIdx >= _Wavelengths.Length) break;
// Check if we need to move to the next cascade
while (cascadeIdx < k_CascadeCount && _Wavelengths[componentIdx] >= 2f * minWl)
{
// Wrap up this cascade and begin next
// Fill remaining elements of current vector4 with 0s
var vi = outputIdx / 4;
var ei = outputIdx - vi * 4;
while (ei != 0)
{
_WaveData[vi]._TwoPiOverWavelength[ei] = 1f;
_WaveData[vi]._Amplitude[ei] = 0f;
_WaveData[vi]._WaveDirectionX[ei] = 0f;
_WaveData[vi]._WaveDirectionZ[ei] = 0f;
_WaveData[vi]._Omega[ei] = 0f;
_WaveData[vi]._Phase[ei] = 0f;
_WaveData[vi]._ChopAmplitude[ei] = 0f;
if (!_ManualGeneration)
{
_WaveData[vi]._Phase2[ei] = 0f;
_WaveData[vi]._Amplitude2[ei] = 0f;
_WaveData[vi]._ChopAmplitude2[ei] = 0f;
}
ei = (ei + 1) % 4;
outputIdx++;
}
if (outputIdx > 0 && _FirstCascade < 0) _FirstCascade = cascadeIdx;
cascadeIdx++;
_CascadeParameters[cascadeIdx]._StartIndex = outputIdx / 4;
minWl *= 2f;
//Debug.Log($"Crest: {cascadeIdx}: start {_cascadeParams[cascadeIdx]._startIndex} minWL {minWl}");
}
if (cascadeIdx == k_CascadeCount) break;
{
// Pack into vector elements
var vi = outputIdx / 4;
var ei = outputIdx - vi * 4;
_WaveData[vi]._Amplitude[ei] = _Amplitudes[componentIdx];
var chopScale = _ActiveSpectrum._ChopScales[componentIdx / _ComponentsPerOctave];
_WaveData[vi]._ChopAmplitude[ei] = -chopScale * _ActiveSpectrum._Chop * _Amplitudes[componentIdx];
if (!_ManualGeneration)
{
_WaveData[vi]._Amplitude2[ei] = _Amplitudes2[componentIdx];
_WaveData[vi]._ChopAmplitude2[ei] = -chopScale * _ActiveSpectrum._Chop * _Amplitudes2[componentIdx];
}
var angle = Mathf.Deg2Rad * _AngleDegrees[componentIdx];
var dx = Mathf.Cos(angle);
var dz = Mathf.Sin(angle);
var gravityScale = _ActiveSpectrum._GravityScales[componentIdx / _ComponentsPerOctave];
var gravity = water.Gravity * _ActiveSpectrum._GravityScale;
var c = Mathf.Sqrt(_Wavelengths[componentIdx] * gravity * gravityScale * _ReciprocalTwoPi);
var k = _TwoPi / _Wavelengths[componentIdx];
// Constrain wave vector (wavelength and wave direction) to ensure wave tiles across domain
{
var kx = k * dx;
var kz = k * dz;
var diameter = 0.5f * (1 << cascadeIdx);
// Number of times wave repeats across domain in x and z
var n = kx / (_TwoPi / diameter);
var m = kz / (_TwoPi / diameter);
// Ensure the wave repeats an integral number of times across domain
kx = _TwoPi * Mathf.Round(n) / diameter;
kz = _TwoPi * Mathf.Round(m) / diameter;
// Compute new wave vector and direction
k = Mathf.Sqrt(kx * kx + kz * kz);
dx = kx / k;
dz = kz / k;
}
_WaveData[vi]._TwoPiOverWavelength[ei] = k;
_WaveData[vi]._WaveDirectionX[ei] = dx;
_WaveData[vi]._WaveDirectionZ[ei] = dz;
// Repeat every 2pi to keep angle bounded - helps precision on 16bit platforms
_WaveData[vi]._Omega[ei] = k * c;
_WaveData[vi]._Phase[ei] = Mathf.Repeat(_Phases[componentIdx], Mathf.PI * 2f);
if (!_ManualGeneration)
{
_WaveData[vi]._Phase2[ei] = Mathf.Repeat(_Phases2[componentIdx], Mathf.PI * 2f);
}
outputIdx++;
}
}
_LastCascade = isFilterable ? cascadeIdx : k_CascadeCount - 1;
{
// Fill remaining elements of current vector4 with 0s
var vi = outputIdx / 4;
var ei = outputIdx - vi * 4;
while (ei != 0)
{
_WaveData[vi]._TwoPiOverWavelength[ei] = 1f;
_WaveData[vi]._Amplitude[ei] = 0f;
_WaveData[vi]._WaveDirectionX[ei] = 0f;
_WaveData[vi]._WaveDirectionZ[ei] = 0f;
_WaveData[vi]._Omega[ei] = 0f;
_WaveData[vi]._Phase[ei] = 0f;
_WaveData[vi]._ChopAmplitude[ei] = 0f;
if (!_ManualGeneration)
{
_WaveData[vi]._Phase2[ei] = 0f;
_WaveData[vi]._Amplitude2[ei] = 0f;
_WaveData[vi]._ChopAmplitude2[ei] = 0f;
}
ei = (ei + 1) % 4;
outputIdx++;
}
}
while (cascadeIdx < k_CascadeCount)
{
cascadeIdx++;
minWl *= 2f;
_CascadeParameters[cascadeIdx]._StartIndex = outputIdx / 4;
//Debug.Log($"Crest: {cascadeIdx}: start {_cascadeParams[cascadeIdx]._startIndex} minWL {minWl}");
}
_BufferCascadeParameters.SetData(_CascadeParameters);
_BufferWaveData.SetData(_WaveData);
}
void UpdateGenerateWaves(CommandBuffer buf)
{
// Clear existing waves or they could get copied.
CoreUtils.SetRenderTarget(buf, _WaveBuffers, ClearFlag.Color);
buf.SetComputeFloatParam(_ShaderGerstner, ShaderIDs.s_TextureRes, _WaveBuffers.width);
buf.SetComputeIntParam(_ShaderGerstner, ShaderIDs.s_FirstCascadeIndex, _FirstCascade);
buf.SetComputeBufferParam(_ShaderGerstner, _KernelGerstner, ShaderIDs.s_CascadeParams, _BufferCascadeParameters);
buf.SetComputeBufferParam(_ShaderGerstner, _KernelGerstner, ShaderIDs.s_GerstnerWaveData, _BufferWaveData);
buf.SetComputeTextureParam(_ShaderGerstner, _KernelGerstner, ShapeWaves.ShaderIDs.s_WaveBuffer, _WaveBuffers);
buf.DispatchCompute(_ShaderGerstner, _KernelGerstner, _WaveBuffers.width / Lod.k_ThreadGroupSizeX, _WaveBuffers.height / Lod.k_ThreadGroupSizeY, _LastCascade - _FirstCascade + 1);
}
///
/// Resamples wave spectrum
///
/// The water renderer.
/// Wind speed in m/s
void UpdateWaveData(WaterRenderer water, float windSpeed)
{
if (_ManualGeneration)
{
if (_Wavelengths != null)
{
SliceUpWaves(water, windSpeed);
}
return;
}
// Set random seed to get repeatable results
var randomStateBkp = Random.state;
Random.InitState(_RandomSeed);
_ActiveSpectrum.GenerateWaveData(_ComponentsPerOctave, ref _Wavelengths, ref _AngleDegrees);
UpdateAmplitudes(water);
// Won't run every time so put last in the random sequence
if (_Phases == null || _Phases.Length != _Wavelengths.Length || _Phases2 == null || _Phases2.Length != _Wavelengths.Length)
{
InitPhases();
}
Random.state = randomStateBkp;
SliceUpWaves(water, windSpeed);
}
void UpdateAmplitudes(WaterRenderer water)
{
if (_Amplitudes == null || _Amplitudes.Length != _Wavelengths.Length)
{
_Amplitudes = new float[_Wavelengths.Length];
}
if (_Amplitudes2 == null || _Amplitudes2.Length != _Wavelengths.Length)
{
_Amplitudes2 = new float[_Wavelengths.Length];
}
if (_Powers == null || _Powers.Length != _Wavelengths.Length)
{
_Powers = new float[_Wavelengths.Length];
}
var windSpeed = WindSpeedMPS;
for (var i = 0; i < _Wavelengths.Length; i++)
{
var amp = _ActiveSpectrum.GetAmplitude(_Wavelengths[i], _ComponentsPerOctave, windSpeed, water.Gravity, out _Powers[i]);
_Amplitudes[i] = Random.value * amp;
_Amplitudes2[i] = Random.value * amp * ReverseWaveWeight;
}
}
void InitPhases()
{
// Set random seed to get repeatable results
var randomStateBkp = Random.state;
Random.InitState(_RandomSeed);
var totalComps = _ComponentsPerOctave * WaveSpectrum.k_NumberOfOctaves;
_Phases = new float[totalComps];
_Phases2 = new float[totalComps];
for (var octave = 0; octave < WaveSpectrum.k_NumberOfOctaves; octave++)
{
for (var i = 0; i < _ComponentsPerOctave; i++)
{
var index = octave * _ComponentsPerOctave + i;
var rnd = (i + Random.value) / _ComponentsPerOctave;
_Phases[index] = 2f * Mathf.PI * rnd;
var rnd2 = (i + Random.value) / _ComponentsPerOctave;
_Phases2[index] = 2f * Mathf.PI * rnd2;
}
}
Random.state = randomStateBkp;
}
private protected override void ReportMaxDisplacement(WaterRenderer water)
{
if (!Enabled) return;
if (_ActiveSpectrum._ChopScales.Length != WaveSpectrum.k_NumberOfOctaves)
{
Debug.LogError($"Crest: {nameof(WaveSpectrum)} {_ActiveSpectrum.name} is out of date, please open this asset and resave in editor.", _ActiveSpectrum);
}
if (_Wavelengths == null)
{
return;
}
var ampSum = 0f;
for (var i = 0; i < _Wavelengths.Length; i++)
{
ampSum += _Amplitudes[i] * _ActiveSpectrum._ChopScales[i / _ComponentsPerOctave];
}
// Apply weight or will cause popping due to scale change.
ampSum *= Weight;
MaximumReportedHorizontalDisplacement = ampSum * _ActiveSpectrum._Chop;
MaximumReportedVerticalDisplacement = ampSum;
MaximumReportedWavesDisplacement = ampSum;
if (Mode == LodInputMode.Global)
{
water.ReportMaximumDisplacement(ampSum * _ActiveSpectrum._Chop, ampSum, ampSum);
}
}
private protected override void Initialize()
{
base.Initialize();
s_Instances.Add(transform.GetSiblingIndex(), this);
}
private protected override void OnDisable()
{
base.OnDisable();
s_Instances.Remove(this);
if (_BufferCascadeParameters != null && _BufferCascadeParameters.IsValid())
{
_BufferCascadeParameters.Dispose();
_BufferCascadeParameters = null;
}
if (_BufferWaveData != null && _BufferWaveData.IsValid())
{
_BufferWaveData.Dispose();
_BufferWaveData = null;
}
if (_WaveBuffers != null)
{
Helpers.Destroy(_WaveBuffers);
_WaveBuffers = null;
}
}
#if UNITY_EDITOR
void OnGUI()
{
if (_DrawSlicesInEditor && _WaveBuffers != null && _WaveBuffers.IsCreated())
{
DebugGUI.DrawTextureArray(_WaveBuffers, 8, 0.5f);
}
}
#endif
}
partial class ShapeGerstner
{
static int s_InstanceCount;
private protected override void Awake()
{
base.Awake();
s_InstanceCount++;
}
private protected override void OnDestroy()
{
base.OnDestroy();
if (s_SwellSpectrum != null)
{
Helpers.Destroy(s_SwellSpectrum);
}
}
}
partial class ShapeGerstner : ISerializationCallbackReceiver
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 2;
#pragma warning restore 414
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
_Version = MigrateV1(_Version);
if (_Version < 2)
{
_Swell = false;
}
_Version = MigrateV2(_Version);
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
// Empty.
}
}
}