// 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. } } }