升级6.4.升级水,升级天气

This commit is contained in:
2026-04-05 00:26:54 +08:00
parent 63bc9b5536
commit 5f7cbfb713
635 changed files with 34718 additions and 22567 deletions

View File

@@ -9,6 +9,7 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Ripples")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Waterfall")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.CPUQueries")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.CPUQueries.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Paint")]

View File

@@ -24,12 +24,25 @@ namespace WaveHarmonic.Crest
#if UNITY_EDITOR
public const int k_FieldGroupOrder = Editor.Inspector.k_FieldGroupOrder;
public const int k_FieldHeadingOrder = Editor.Inspector.k_FieldHeadingOrder;
#else
public const int k_FieldGroupOrder = 0;
public const int k_FieldHeadingOrder = 0;
#endif
// Unity only supports textures up to a size of 16384, even if maxTextureSize returns a larger size.
// https://docs.unity3d.com/ScriptReference/SystemInfo-maxTextureSize.html
public const int k_MaximumTextureResolution = 16384;
public static class Symbols
{
public const string k_Refraction =
#if d_Crest_SimpleTransparency
"CREST_NOOP";
#else
"UNITY_2022_3_OR_NEWER";
#endif
}
}
}

View File

@@ -18,7 +18,8 @@ namespace WaveHarmonic.Crest
internal override string ID => "Albedo";
internal override Color GizmoColor => s_GizmoColor;
private protected override Color ClearColor => Color.clear;
private protected override bool NeedToReadWriteTextureData => false;
private protected override bool NeedToReadWriteTextureData => Blur;
internal override bool SkipEndOfFrame => true;
private protected override GraphicsFormat RequestedTextureFormat => _TextureFormatMode switch
{

View File

@@ -1,13 +1,15 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
abstract class BakedWaveData : ScriptableObject
abstract class BakedWaveData : CustomScriptableObject
{
public abstract ICollisionProvider CreateCollisionProvider();
public abstract float WindSpeed { get; }
@@ -20,6 +22,7 @@ namespace WaveHarmonic.Crest
/// <summary>
/// The source of collisions (ie water shape).
/// </summary>
[System.Obsolete("Please use QuerySource and LodQuerySource.")]
[@GenerateDoc]
public enum CollisionSource
{
@@ -67,13 +70,9 @@ namespace WaveHarmonic.Crest
{
// NOTE: numbers must be in order for defaults to work (everything first).
/// <inheritdoc cref="Generated.CollisionLayers.Everything"/>
[Tooltip("All layers.")]
Everything = -1,
/// <inheritdoc cref="Generated.CollisionLayers.Nothing"/>
[Tooltip("No extra layers (ie single layer).")]
Nothing,
Nothing = 0,
/// <inheritdoc cref="Generated.CollisionLayers.DynamicWaves"/>
[Tooltip("Separate layer for dynamic waves.\n\nDynamic waves are normally combined together for efficiency. By enabling this layer, dynamic waves are combined and added in a separate pass.")]
@@ -82,6 +81,29 @@ namespace WaveHarmonic.Crest
/// <inheritdoc cref="Generated.CollisionLayers.Displacement"/>
[Tooltip("Extra displacement layer for visual displacement.")]
Displacement = 1 << 2,
/// <inheritdoc cref="Generated.CollisionLayers.Everything"/>
[Tooltip("All layers.")]
Everything = ~0,
}
/// <summary>
/// The wave sampling method to determine quality and performance.
/// </summary>
[@GenerateDoc]
public enum WaveSampling
{
/// <inheritdoc cref="Generated.WaveSampling.Automatic"/>
[Tooltip("Automatically chooses the other options as needed (512+ resolution needs precision).")]
Automatic,
/// <inheritdoc cref="Generated.WaveSampling.Performance"/>
[Tooltip("Reduces samples by copying waves from higher LODs to lower LODs.\n\nBest for resolutions lower than 512.")]
Performance,
/// <inheritdoc cref="Generated.WaveSampling.Precision"/>
[Tooltip("Samples directly from the wave buffers to preserve wave quality.\n\nNeeded for higher resolutions (512+). Higher LOD counts can also benefit with this enabled.")]
Precision,
}
/// <summary>
@@ -108,14 +130,33 @@ namespace WaveHarmonic.Crest
[@FilterEnum(nameof(_TextureFormatMode), Filtered.Mode.Exclude, (int)LodTextureFormatMode.Automatic)]
public sealed partial class AnimatedWavesLod : Lod<ICollisionProvider>
{
[Tooltip("Collision layers to enable.\n\nSome layers will have overhead with CPU, GPU and memory.")]
[@Enable(nameof(_QuerySource), nameof(LodQuerySource.GPU))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal CollisionLayers _CollisionLayers = CollisionLayers.Everything;
[@Enable(nameof(_QuerySource), nameof(LodQuerySource.CPU))]
[@DecoratedField, SerializeField]
internal BakedWaveData _BakedWaveData;
[@Space(10)]
[Tooltip("Shifts wavelengths to maintain quality for higher resolutions.\n\nSet this to 2 to improve wave quality. In some cases like flowing rivers, this can make a substantial difference to visual stability. We recommend doubling the Resolution on the WaterRenderer component to preserve detail after making this change.")]
[@Range(1f, 4f)]
[Tooltip("The wave sampling method to determine quality and performance.")]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
internal WaveSampling _WaveSampling;
[Tooltip("Shifts wavelengths to maintain quality for higher resolutions.\n\nSet this to 2 to improve wave quality. In some cases like flowing rivers, this can make a substantial difference to visual stability. We recommend doubling the Resolution on the WaterRenderer component to preserve detail after making this change.")]
[@Enable(nameof(_WaveSampling), nameof(WaveSampling.Performance))]
[@Range(1f, 4f)]
[@GenerateAPI(Getter.Custom)]
[SerializeField]
float _WaveResolutionMultiplier = 1f;
[@Space(10)]
[Tooltip("How much waves are dampened in shallow water.")]
[@Range(0f, 1f)]
[@GenerateAPI]
@@ -129,30 +170,6 @@ namespace WaveHarmonic.Crest
float _ShallowsMaximumDepth = 1000f;
[@Heading("Collisions")]
[Tooltip("Where to obtain water shape on CPU for physics / gameplay.")]
[@GenerateAPI(Setter.Internal)]
[@DecoratedField, SerializeField]
internal CollisionSource _CollisionSource = CollisionSource.GPU;
[Tooltip("Collision layers to enable.\n\nSome layers will have overhead with CPU, GPU and memory.")]
[@Predicated(nameof(_CollisionSource), inverted: true, nameof(CollisionSource.GPU))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal CollisionLayers _CollisionLayers = CollisionLayers.Everything;
[Tooltip("Maximum number of wave queries that can be performed when using GPU queries.")]
[@Predicated(nameof(_CollisionSource), true, nameof(CollisionSource.GPU))]
[@GenerateAPI(Setter.None)]
[@DecoratedField, SerializeField]
int _MaximumQueryCount = QueryBase.k_DefaultMaximumQueryCount;
[@Predicated(nameof(_CollisionSource), true, nameof(CollisionSource.CPU))]
[@DecoratedField, SerializeField]
internal BakedWaveData _BakedWaveData;
const string k_DrawCombine = "Combine";
@@ -160,7 +177,6 @@ namespace WaveHarmonic.Crest
{
public static readonly int s_WaveBuffer = Shader.PropertyToID("_Crest_WaveBuffer");
public static readonly int s_DynamicWavesTarget = Shader.PropertyToID("_Crest_DynamicWavesTarget");
public static readonly int s_AnimatedWavesTarget = Shader.PropertyToID("_Crest_AnimatedWavesTarget");
public static readonly int s_AttenuationInShallows = Shader.PropertyToID("_Crest_AttenuationInShallows");
}
@@ -171,6 +187,9 @@ namespace WaveHarmonic.Crest
/// </summary>
internal static bool s_Combine = true;
WaterResources.ShapeCombineCompute _CombineShader;
RenderTexture _PersistentDataTexture;
internal override string ID => "AnimatedWaves";
internal override string Name => "Animated Waves";
internal override Color GizmoColor => s_GizmoColor;
@@ -187,17 +206,13 @@ namespace WaveHarmonic.Crest
_ => throw new System.NotImplementedException(),
};
ComputeShader _CombineShader;
int _KernalShapeCombine = -1;
int _KernalShapeCombine_DISABLE_COMBINE = -1;
int _KernalShapeCombine_FLOW_ON = -1;
int _KernalShapeCombine_FLOW_ON_DISABLE_COMBINE = -1;
int _KernalShapeCombine_DYNAMIC_WAVE_SIM_ON = -1;
int _KernalShapeCombine_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE = -1;
int _KernalShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON = -1;
int _KernalShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE = -1;
internal bool PreserveWaveQuality => WaveSampling switch
{
WaveSampling.Automatic => Resolution >= 512,
WaveSampling.Performance => false,
WaveSampling.Precision => true,
_ => throw new System.NotImplementedException(),
};
internal AnimatedWavesLod()
{
@@ -208,35 +223,67 @@ namespace WaveHarmonic.Crest
internal override void Initialize()
{
_CombineShader = WaterResources.Instance.Compute._ShapeCombine;
if (_CombineShader == null)
_CombineShader = WaterResources.Instance._ComputeLibrary._ShapeCombineCompute;
if (_CombineShader._Shader == null)
{
_Valid = false;
return;
}
base.Initialize();
if (Persistent && !_Water.IsMultipleViewpointMode)
{
_PersistentDataTexture = CreateLodDataTextures();
}
}
private protected override void Allocate()
internal override void Destroy()
{
base.Allocate();
base.Destroy();
_KernalShapeCombine = _CombineShader.FindKernel("ShapeCombine");
_KernalShapeCombine_DISABLE_COMBINE = _CombineShader.FindKernel("ShapeCombine_DISABLE_COMBINE");
_KernalShapeCombine_FLOW_ON = _CombineShader.FindKernel("ShapeCombine_FLOW_ON");
_KernalShapeCombine_FLOW_ON_DISABLE_COMBINE = _CombineShader.FindKernel("ShapeCombine_FLOW_ON_DISABLE_COMBINE");
_KernalShapeCombine_DYNAMIC_WAVE_SIM_ON = _CombineShader.FindKernel("ShapeCombine_DYNAMIC_WAVE_SIM_ON");
_KernalShapeCombine_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE = _CombineShader.FindKernel("ShapeCombine_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE");
_KernalShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON = _CombineShader.FindKernel("ShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON");
_KernalShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE = _CombineShader.FindKernel("ShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE");
if (_PersistentDataTexture != null) _PersistentDataTexture.Release();
Helpers.Destroy(_PersistentDataTexture);
foreach (var data in _AdditionalCameraData.Values)
{
if (data != null) data.Release();
Helpers.Destroy(data);
}
_AdditionalCameraData.Clear();
}
internal override void SetGlobals(bool enable)
{
base.SetGlobals(enable);
if (_Water.IsRunningWithoutGraphics)
{
return;
}
if (Persistent)
{
Shader.SetGlobalTexture(_TextureSourceShaderID, enable && Enabled ? _PersistentDataTexture : NullTexture);
}
}
internal override void BuildCommandBuffer(WaterRenderer water, CommandBuffer buffer)
{
buffer.BeginSample(ID);
FlipBuffers();
FlipBuffers(buffer);
// Flip textures
if (Persistent)
{
(_PersistentDataTexture, DataTexture) = (DataTexture, _PersistentDataTexture);
// Update current and previous. Latter for MVs and/or VFX.
buffer.SetGlobalTexture(_TextureSourceShaderID, _PersistentDataTexture);
buffer.SetGlobalTexture(_TextureShaderID, DataTexture);
}
Shader.SetGlobalFloat(ShaderIDs.s_AttenuationInShallows, _AttenuationInShallows);
@@ -244,6 +291,18 @@ namespace WaveHarmonic.Crest
buffer.GetTemporaryRT(ShaderIDs.s_WaveBuffer, DataTexture.descriptor);
CoreUtils.SetRenderTarget(buffer, ShaderIDs.s_WaveBuffer, ClearFlag.Color, ClearColor);
// Custom clear because clear not working.
if (Helpers.RequiresCustomClear && WaterResources.Instance.Compute._Clear != null)
{
var compute = WaterResources.Instance._ComputeLibrary._ClearCompute;
var wrapper = new PropertyWrapperCompute(buffer, compute._Shader, compute._KernelClearTarget);
compute.SetVariantForFormat(wrapper, DataTexture.graphicsFormat);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, ShaderIDs.s_WaveBuffer);
wrapper.SetVector(Crest.ShaderIDs.s_ClearMask, Color.white);
wrapper.SetVector(Crest.ShaderIDs.s_ClearColor, ClearColor);
wrapper.Dispatch(Resolution / k_ThreadGroupSizeX, Resolution / k_ThreadGroupSizeY, Slices);
}
// LOD dependent data.
// Write to per-octave _WaveBuffers. Not the same as _AnimatedWaves.
// Draw any data with lod preference.
@@ -254,54 +313,53 @@ namespace WaveHarmonic.Crest
// Combine the LODs - copy results from biggest LOD down to LOD 0
{
var combineShaderKernel = _KernalShapeCombine;
var combineShaderKernel_lastLOD = _KernalShapeCombine_DISABLE_COMBINE;
{
var isFlowOn = _Water._FlowLod.Enabled;
var isDynamicWavesOn = _Water._DynamicWavesLod.Enabled && !_CollisionLayers.HasFlag(CollisionLayers.DynamicWaves);
var wrapper = new PropertyWrapperCompute
(
buffer,
_CombineShader._Shader,
PreserveWaveQuality
? _CombineShader._CopyAnimatedWavesKernel
: _CombineShader._CombineAnimatedWavesKernel
);
// Set the shader kernels that we will use.
if (isFlowOn && isDynamicWavesOn)
{
combineShaderKernel = _KernalShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON;
combineShaderKernel_lastLOD = _KernalShapeCombine_FLOW_ON_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE;
}
else if (isFlowOn)
{
combineShaderKernel = _KernalShapeCombine_FLOW_ON;
combineShaderKernel_lastLOD = _KernalShapeCombine_FLOW_ON_DISABLE_COMBINE;
}
else if (isDynamicWavesOn)
{
combineShaderKernel = _KernalShapeCombine_DYNAMIC_WAVE_SIM_ON;
combineShaderKernel_lastLOD = _KernalShapeCombine_DYNAMIC_WAVE_SIM_ON_DISABLE_COMBINE;
}
if (_Water._DynamicWavesLod.Enabled)
{
_Water._DynamicWavesLod.Bind(wrapper);
}
buffer.BeginSample(k_DrawCombine);
// Start with last LOD which does not combine.
wrapper.SetKeyword(_CombineShader._CombineKeyword, false);
wrapper.SetKeyword(_CombineShader._DynamicWavesKeyword, _Water._DynamicWavesLod.Enabled && !PreserveWaveQuality && !_CollisionLayers.HasFlag(CollisionLayers.DynamicWaves));
// Combine waves.
for (var slice = lastSlice; slice >= 0; slice--)
// The per-octave wave buffers we read from.
wrapper.SetTexture(ShaderIDs.s_WaveBuffer, ShaderIDs.s_WaveBuffer);
// Set the animated waves texture where we read/write to combine the results.
wrapper.SetTexture(Crest.ShaderIDs.s_Target, DataTexture);
if (PreserveWaveQuality)
{
var kernel = slice < lastSlice && s_Combine
? combineShaderKernel : combineShaderKernel_lastLOD;
var wrapper = new PropertyWrapperCompute(buffer, _CombineShader, kernel);
// The per-octave wave buffers we read from.
wrapper.SetTexture(ShaderIDs.s_WaveBuffer, ShaderIDs.s_WaveBuffer);
if (_Water._DynamicWavesLod.Enabled) _Water._DynamicWavesLod.Bind(wrapper);
// Set the animated waves texture where we read/write to combine the results. Use
// compute suffix to avoid collision as a file already uses the normal name.
wrapper.SetTexture(Crest.ShaderIDs.s_Target, DataTexture);
wrapper.SetInteger(Lod.ShaderIDs.s_LodIndex, slice);
wrapper.Dispatch(threadSize, threadSize, 1);
wrapper.Dispatch(threadSize, threadSize, Slices);
}
else
{
buffer.BeginSample(k_DrawCombine);
buffer.EndSample(k_DrawCombine);
// Combine waves.
for (var slice = lastSlice; slice >= 0; slice--)
{
wrapper.SetInteger(Lod.ShaderIDs.s_LodIndex, slice);
wrapper.Dispatch(threadSize, threadSize, 1);
if (slice == lastSlice)
{
// From here on, use combine.
wrapper.SetKeyword(_CombineShader._CombineKeyword, s_Combine);
}
}
buffer.EndSample(k_DrawCombine);
}
}
buffer.ReleaseTemporaryRT(ShaderIDs.s_WaveBuffer);
@@ -312,20 +370,15 @@ namespace WaveHarmonic.Crest
// Alpha channel is cleared in combine step, but if any inputs draw in post-combine
// step then alpha may have data.
var clear = WaterResources.Instance.Compute._Clear;
if (drawn && clear != null)
if (drawn && WaterResources.Instance.Compute._Clear != null)
{
buffer.SetComputeTextureParam(clear, 0, Crest.ShaderIDs.s_Target, DataTexture);
buffer.SetComputeVectorParam(clear, Crest.ShaderIDs.s_ClearMask, Color.black);
buffer.SetComputeVectorParam(clear, Crest.ShaderIDs.s_ClearColor, Color.clear);
buffer.DispatchCompute
(
clear,
0,
Resolution / k_ThreadGroupSizeX,
Resolution / k_ThreadGroupSizeY,
Slices
);
var compute = WaterResources.Instance._ComputeLibrary._ClearCompute;
var wrapper = new PropertyWrapperCompute(buffer, compute._Shader, compute._KernelClearTarget);
compute.SetVariantForFormat(wrapper, DataTexture.graphicsFormat);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, DataTexture);
wrapper.SetVector(Crest.ShaderIDs.s_ClearMask, Color.black);
wrapper.SetVector(Crest.ShaderIDs.s_ClearColor, Color.clear);
wrapper.Dispatch(Resolution / k_ThreadGroupSizeX, Resolution / k_ThreadGroupSizeY, Slices);
}
// Pack height data into alpha channel.
@@ -355,15 +408,21 @@ namespace WaveHarmonic.Crest
}
// Transfer Dynamic Waves to Animated Waves.
if (_CollisionLayers.HasFlag(CollisionLayers.DynamicWaves) && _Water._DynamicWavesLod.Enabled)
if ((_CollisionLayers.HasFlag(CollisionLayers.DynamicWaves) || PreserveWaveQuality) && _Water._DynamicWavesLod.Enabled)
{
buffer.BeginSample(k_DrawCombine);
// Clearing not required as we overwrite enter texture.
buffer.GetTemporaryRT(ShaderIDs.s_DynamicWavesTarget, DataTexture.descriptor);
var wrapper = new PropertyWrapperCompute(buffer, _CombineShader, 9);
var wrapper = new PropertyWrapperCompute(buffer, _CombineShader._Shader, _CombineShader._CombineDynamicWavesKernel);
wrapper.SetTexture(ShaderIDs.s_DynamicWavesTarget, ShaderIDs.s_DynamicWavesTarget);
// Flow keyword is already set, and Dynamic Waves already bound. If binding Dynamic
// Waves becomes kernel specific (eg binding textures), then we need to rebind.
// Start with last LOD which does not combine.
wrapper.SetKeyword(_CombineShader._CombineKeyword, false);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, ShaderIDs.s_DynamicWavesTarget);
_Water._DynamicWavesLod.Bind(wrapper);
@@ -373,18 +432,21 @@ namespace WaveHarmonic.Crest
wrapper.SetInteger(Lod.ShaderIDs.s_LodIndex, slice);
wrapper.Dispatch(threadSize, threadSize, 1);
// Change to kernel with combine enabled.
if (slice == lastSlice)
{
wrapper = new(buffer, _CombineShader, 8);
// From here on, use combine.
wrapper.SetKeyword(_CombineShader._CombineKeyword, s_Combine);
}
}
// Copy Dynamic Waves displacement into Animated Waves.
if (WaterResources.Instance.Compute._Blit != null)
{
wrapper = new(buffer, _CombineShader, 10);
wrapper.SetTexture(ShaderIDs.s_AnimatedWavesTarget, DataTexture);
wrapper.SetTexture(ShaderIDs.s_DynamicWavesTarget, ShaderIDs.s_DynamicWavesTarget);
var compute = WaterResources.Instance._ComputeLibrary._BlitCompute;
wrapper = new(buffer, compute._Shader, 0);
compute.SetVariantForFormat(wrapper, DataTexture.graphicsFormat);
wrapper.SetTexture(Crest.ShaderIDs.s_Source, ShaderIDs.s_DynamicWavesTarget);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, DataTexture);
wrapper.Dispatch(threadSize, threadSize, Slices);
}
@@ -393,7 +455,10 @@ namespace WaveHarmonic.Crest
// Query collisions including Dynamic Waves.
// Does not require copying the water level as they are added with zero alpha.
Provider.UpdateQueries(_Water, CollisionLayer.AfterDynamicWaves);
if (_CollisionLayers.HasFlag(CollisionLayers.DynamicWaves))
{
Provider.UpdateQueries(_Water, CollisionLayer.AfterDynamicWaves);
}
}
if (_CollisionLayers.HasFlag(CollisionLayers.Displacement))
@@ -408,13 +473,6 @@ namespace WaveHarmonic.Crest
Queryable?.UpdateQueries(_Water);
}
if (BufferCount > 1)
{
// Update current and previous. Latter for MVs and/or VFX.
Shader.SetGlobalTexture(_TextureSourceShaderID, _Targets.Previous(1));
Shader.SetGlobalTexture(_TextureShaderID, DataTexture);
}
buffer.EndSample(ID);
}
@@ -426,50 +484,59 @@ namespace WaveHarmonic.Crest
/// <summary>
/// Provides water shape to CPU.
/// </summary>
private protected override ICollisionProvider CreateProvider(bool enable)
private protected override ICollisionProvider CreateProvider(bool onEnable)
{
ICollisionProvider result = null;
ICollisionProvider provider = ICollisionProvider.None;
Queryable?.CleanUp();
if (!enable)
// Respect user choice and early exit. If not in OnEnable, then none provider.
// Do not check Enabled, as it includes _Valid which checks for GPU.
if (!_Enabled || !onEnable)
{
return ICollisionProvider.None;
return provider;
}
switch (_CollisionSource)
var source = QuerySource;
if (_Water.Surface.IsQuadMesh)
{
case CollisionSource.None:
result = ICollisionProvider.None;
break;
case CollisionSource.GPU:
if (_Valid && !_Water.IsRunningWithoutGraphics)
source = LodQuerySource.None;
}
switch (source)
{
case LodQuerySource.GPU:
{
if (_Valid)
{
result = new CollisionQueryWithPasses(_Water);
provider = ICollisionProvider.Create(_Water);
}
if (_Water.IsRunningWithoutGraphics)
{
Debug.LogError($"Crest: GPU queries not supported in headless/batch mode. To resolve, assign an Animated Wave Settings asset to the {nameof(WaterRenderer)} component and set the Collision Source to be a CPU option.");
Debug.LogError("Crest: GPU queries requires a GPU. Please consider CPU queries if running from a server without a GPU.");
}
break;
case CollisionSource.CPU:
}
case LodQuerySource.CPU:
{
if (_BakedWaveData != null)
{
result = _BakedWaveData.CreateCollisionProvider();
provider = _BakedWaveData.CreateCollisionProvider();
}
break;
}
}
if (result == null)
{
// This should not be hit, but can be if compute shaders aren't loaded correctly.
// They will print out appropriate errors. Don't just return null and have null reference
// exceptions spamming the logs.
return ICollisionProvider.None;
}
// This should not be hit, but can be if compute shaders aren't loaded correctly.
// They will print out appropriate errors. Don't just return null and have null
// reference exceptions spamming the logs.
provider ??= ICollisionProvider.None;
return result;
return provider;
}
//
@@ -484,15 +551,17 @@ namespace WaveHarmonic.Crest
public readonly float _ViewerAltitudeLevelAlpha;
public readonly int _Slice;
public readonly int _Slices;
public readonly bool _HighQualityCombine;
public WavelengthFilter(WaterRenderer water, int slice)
public WavelengthFilter(WaterRenderer water, int slice, int resolution)
{
_Slice = slice;
_Slices = water.LodLevels;
_Maximum = water.MaximumWavelength(slice);
_Maximum = water.MaximumWavelength(slice, resolution);
_Minimum = _Maximum * 0.5f;
_TransitionThreshold = water.MaximumWavelength(_Slices - 1) * 0.5f;
_TransitionThreshold = water.MaximumWavelength(_Slices - 1, resolution) * 0.5f;
_ViewerAltitudeLevelAlpha = water.ViewerAltitudeLevelAlpha;
_HighQualityCombine = water.AnimatedWavesLod.PreserveWaveQuality;
}
}
@@ -513,7 +582,7 @@ namespace WaveHarmonic.Crest
// If approaching end of lod chain, start smoothly transitioning any large wavelengths across last two lods
if (wavelength >= filter._TransitionThreshold)
{
if (filter._Slice == filter._Slices - 2)
if (filter._Slice == filter._Slices - 2 && !filter._HighQualityCombine)
{
return 1f - filter._ViewerAltitudeLevelAlpha;
}
@@ -529,12 +598,12 @@ namespace WaveHarmonic.Crest
return 1f;
}
return 0f;
return filter._HighQualityCombine ? 1f : 0f;
}
internal static float FilterByWavelength(WaterRenderer water, int slice, float wavelength)
internal static float FilterByWavelength(WaterRenderer water, int slice, float wavelength, int resolution)
{
return FilterByWavelength(new(water, slice), wavelength);
return FilterByWavelength(new(water, slice, resolution), wavelength);
}
@@ -550,8 +619,83 @@ namespace WaveHarmonic.Crest
{
s_Inputs.Clear();
}
}
// Multiple Viewpoints
partial class AnimatedWavesLod
{
readonly Dictionary<Camera, RenderTexture> _AdditionalCameraData = new();
internal override void LoadCameraData(Camera camera)
{
base.LoadCameraData(camera);
if (!Persistent)
{
return;
}
if (!_AdditionalCameraData.ContainsKey(camera))
{
_PersistentDataTexture = CreateLodDataTextures();
Clear(_PersistentDataTexture);
_AdditionalCameraData.Add(camera, _PersistentDataTexture);
}
else
{
_PersistentDataTexture = _AdditionalCameraData[camera];
}
}
internal override void StoreCameraData(Camera camera)
{
base.StoreCameraData(camera);
if (!Persistent)
{
return;
}
_AdditionalCameraData[camera] = _PersistentDataTexture;
}
internal override void RemoveCameraData(Camera camera)
{
base.RemoveCameraData(camera);
if (_AdditionalCameraData.ContainsKey(camera))
{
var rt = _AdditionalCameraData[camera];
if (rt != null) rt.Release();
Helpers.Destroy(rt);
_AdditionalCameraData.Remove(camera);
}
}
}
// API
partial class AnimatedWavesLod
{
float GetWaveResolutionMultiplier()
{
return PreserveWaveQuality ? 1f : _WaveResolutionMultiplier;
}
}
partial class AnimatedWavesLod
{
[@HideInInspector]
[@System.Obsolete("Please use QuerySource instead.")]
[Tooltip("Where to obtain water shape on CPU for physics / gameplay.")]
[@GenerateAPI(Setter.Internal)]
[@DecoratedField, SerializeField]
internal CollisionSource _CollisionSource = CollisionSource.GPU;
}
#if UNITY_EDITOR
// Editor
partial class AnimatedWavesLod
{
[@OnChange]
private protected override void OnChange(string propertyPath, object previousValue)
{
@@ -560,13 +704,10 @@ namespace WaveHarmonic.Crest
switch (propertyPath)
{
case nameof(_CollisionLayers):
case nameof(_CollisionSource):
if (_Water == null || !_Water.isActiveAndEnabled || !Enabled) return;
Queryable?.CleanUp();
InitializeProvider(true);
ResetQueryChange();
break;
}
}
#endif
}
#endif
}

View File

@@ -3,7 +3,6 @@
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Utility;
namespace WaveHarmonic.Crest
@@ -37,11 +36,6 @@ namespace WaveHarmonic.Crest
[@DecoratedField, SerializeField]
internal DefaultClippingState _DefaultClippingState = DefaultClippingState.NothingClipped;
static new class ShaderIDs
{
public static readonly int s_ClipByDefault = Shader.PropertyToID("g_Crest_ClipByDefault");
}
internal static readonly Color s_GizmoColor = new(0f, 1f, 1f, 0.5f);
internal override string ID => "Clip";
@@ -50,6 +44,7 @@ namespace WaveHarmonic.Crest
private protected override Color ClearColor => _DefaultClippingState == DefaultClippingState.EverythingClipped ? Color.white : Color.black;
private protected override bool NeedToReadWriteTextureData => true;
private protected override bool RequiresClearBorder => true;
internal override bool SkipEndOfFrame => true;
private protected override GraphicsFormat RequestedTextureFormat => _TextureFormatMode switch
{
@@ -65,24 +60,6 @@ namespace WaveHarmonic.Crest
_TextureFormat = GraphicsFormat.R8_UNorm;
}
internal override void SetGlobals(bool enable)
{
base.SetGlobals(enable);
Shader.SetGlobalFloat(ShaderIDs.s_ClipByDefault, enable && Enabled ? (float)_DefaultClippingState : (float)DefaultClippingState.NothingClipped);
}
internal override void Disable()
{
base.Disable();
Shader.SetGlobalFloat(ShaderIDs.s_ClipByDefault, (float)DefaultClippingState.NothingClipped);
}
internal override void BuildCommandBuffer(WaterRenderer water, CommandBuffer buffer)
{
base.BuildCommandBuffer(water, buffer);
Shader.SetGlobalFloat(ShaderIDs.s_ClipByDefault, (float)_DefaultClippingState);
}
internal static readonly SortedList<int, ILodInput> s_Inputs = new(Helpers.DuplicateComparison);
private protected override SortedList<int, ILodInput> Inputs => s_Inputs;
@@ -98,7 +75,7 @@ namespace WaveHarmonic.Crest
if (_Water == null || !_Water.isActiveAndEnabled || !Enabled) return;
// Change default clipping state.
_TargetsToClear = Mathf.Max(1, _TargetsToClear);
_TargetsToClear = true;
}
#if UNITY_EDITOR

View File

@@ -41,19 +41,19 @@ namespace WaveHarmonic.Crest
internal ShorelineVolumeColorSource _ShorelineColorSource;
[Tooltip("Color of the shoreline color.")]
[@Predicated(nameof(_ShorelineColorSource), inverted: false, nameof(ShorelineVolumeColorSource.None), hide: true)]
[@Hide(nameof(_ShorelineColorSource), nameof(ShorelineVolumeColorSource.None))]
[@GenerateAPI(Setter.Custom)]
[@DecoratedField, SerializeField]
private protected Color _ShorelineColor;
[Tooltip("The maximum distance of the shoreline color.\n\nIf using Depth, then it is maximum depth.")]
[@Predicated(nameof(_ShorelineColorSource), inverted: false, nameof(ShorelineVolumeColorSource.None), hide: true)]
[@Hide(nameof(_ShorelineColorSource), nameof(ShorelineVolumeColorSource.None))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _ShorelineColorMaximumDistance = 10f;
[Tooltip("Shoreline color falloff value.")]
[@Predicated(nameof(_ShorelineColorSource), inverted: false, nameof(ShorelineVolumeColorSource.None), hide: true)]
[@Hide(nameof(_ShorelineColorSource), nameof(ShorelineVolumeColorSource.None))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _ShorelineColorFalloff = 2f;
@@ -69,6 +69,7 @@ namespace WaveHarmonic.Crest
private protected abstract void SetShorelineColor(Color previous, Color current);
private protected Vector4 _ShorelineColorValue;
ShorelineColorInput _ShorelineColorInput;
internal override bool SkipEndOfFrame => true;
private protected override GraphicsFormat RequestedTextureFormat => _TextureFormatMode switch
{

View File

@@ -12,6 +12,7 @@ namespace WaveHarmonic.Crest
/// <summary>
/// Data that gives depth of the water (height of sea level above water floor).
/// </summary>
[FilterEnum(nameof(_QuerySource), Filtered.Mode.Exclude, (int)LodQuerySource.CPU)]
[FilterEnum(nameof(_TextureFormatMode), Filtered.Mode.Exclude, (int)LodTextureFormatMode.Automatic)]
public sealed partial class DepthLod : Lod<IDepthProvider>
{
@@ -33,11 +34,12 @@ namespace WaveHarmonic.Crest
// We want the clear color to be the mininimum terrain height (-1000m).
// Mathf.Infinity can cause problems for distance.
static readonly Color s_NullColor = new(-k_DepthBaseline, k_DepthBaseline, 0, 0);
static Color NullColor => Helpers.IsWebGPU ? new(float.MinValue, float.MaxValue, 0, 0) : s_NullColor;
internal override string ID => "Depth";
internal override string Name => "Water Depth";
internal override Color GizmoColor => s_GizmoColor;
private protected override Color ClearColor => s_NullColor;
private protected override Color ClearColor => NullColor;
private protected override bool NeedToReadWriteTextureData => true;
private protected override GraphicsFormat RequestedTextureFormat => _TextureFormatMode switch
@@ -56,7 +58,7 @@ namespace WaveHarmonic.Crest
{
if (_NullTexture == null)
{
var texture = TextureArrayHelpers.CreateTexture2D(s_NullColor, UnityEngine.TextureFormat.RFloat);
var texture = TextureArrayHelpers.CreateTexture2D(NullColor, UnityEngine.TextureFormat.RFloat);
texture.name = $"_Crest_{ID}LodTemporaryDefaultTexture";
_NullTexture = TextureArrayHelpers.CreateTexture2DArray(texture, k_MaximumSlices);
_NullTexture.name = $"_Crest_{ID}LodDefaultTexture";
@@ -71,13 +73,16 @@ namespace WaveHarmonic.Crest
{
_Enabled = true;
_TextureFormat = GraphicsFormat.R16G16_SFloat;
_MaximumQueryCount = 512;
}
private protected override IDepthProvider CreateProvider(bool enable)
private protected override IDepthProvider CreateProvider(bool onEnable)
{
Queryable?.CleanUp();
// Depth is GPU only, and can only be queried using the compute path.
return enable && Enabled ? new DepthQuery(_Water) : IDepthProvider.None;
return onEnable && Enabled && QuerySource == LodQuerySource.GPU
? IDepthProvider.Create(_Water)
: IDepthProvider.None;
}
internal static readonly SortedList<int, ILodInput> s_Inputs = new(Helpers.DuplicateComparison);

View File

@@ -97,12 +97,6 @@ namespace WaveHarmonic.Crest
simMaterial.SetFloat(AnimatedWavesLod.ShaderIDs.s_AttenuationInShallows, _AttenuationInShallows);
}
private protected override void GetSubstepData(float timeToSimulate, out int substeps, out float delta)
{
substeps = Mathf.FloorToInt(timeToSimulate * _SimulationFrequency);
delta = substeps > 0 ? (1f / _SimulationFrequency) : 0f;
}
internal static readonly SortedList<int, ILodInput> s_Inputs = new(Helpers.DuplicateComparison);
private protected override SortedList<int, ILodInput> Inputs => s_Inputs;

View File

@@ -3,6 +3,7 @@
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Utility;
namespace WaveHarmonic.Crest
@@ -10,11 +11,17 @@ namespace WaveHarmonic.Crest
/// <summary>
/// Simulates horizontal motion of water.
/// </summary>
[FilterEnum(nameof(_QuerySource), Filtered.Mode.Exclude, (int)LodQuerySource.CPU)]
[FilterEnum(nameof(_TextureFormatMode), Filtered.Mode.Exclude, (int)LodTextureFormatMode.Automatic)]
public sealed partial class FlowLod : Lod<IFlowProvider>
{
const string k_FlowKeyword = "CREST_FLOW_ON_INTERNAL";
static new class ShaderIDs
{
public static readonly int s_Flow = Shader.PropertyToID("g_Crest_Flow");
}
internal static readonly Color s_GizmoColor = new(0f, 0f, 1f, 0.5f);
internal override string ID => "Flow";
@@ -34,6 +41,7 @@ namespace WaveHarmonic.Crest
{
_Resolution = 128;
_TextureFormat = GraphicsFormat.R16G16_SFloat;
_MaximumQueryCount = 1024;
}
internal override void Enable()
@@ -50,11 +58,37 @@ namespace WaveHarmonic.Crest
Shader.DisableKeyword(k_FlowKeyword);
}
private protected override IFlowProvider CreateProvider(bool enable)
internal override void BuildCommandBuffer(WaterRenderer water, CommandBuffer buffer)
{
var time = water.CurrentTime;
var period = 1f;
var half = period * 0.5f;
var offset0 = Helpers.Fmod(time, period);
var weight0 = offset0 / half;
if (weight0 > 1f) weight0 = 2f - weight0;
var offset1 = Helpers.Fmod(time + half, period);
var weight1 = 1f - weight0;
Shader.SetGlobalVector(ShaderIDs.s_Flow, new(offset0, weight0, offset1, weight1));
base.BuildCommandBuffer(water, buffer);
}
private protected override IFlowProvider CreateProvider(bool onEnable)
{
Queryable?.CleanUp();
// Flow is GPU only, and can only be queried using the compute path.
return enable && Enabled ? new FlowQuery(_Water) : IFlowProvider.None;
return onEnable && Enabled && QuerySource == LodQuerySource.GPU
? IFlowProvider.Create(_Water)
: IFlowProvider.None;
}
internal override void SetGlobals(bool onEnable)
{
base.SetGlobals(onEnable);
// Zero offset. Use the first sample.
Shader.SetGlobalVector(ShaderIDs.s_Flow, new(0, 1, 0, 0));
}
internal static readonly SortedList<int, ILodInput> s_Inputs = new(Helpers.DuplicateComparison);

View File

@@ -69,13 +69,6 @@ namespace WaveHarmonic.Crest
properties.SetInteger(ShaderIDs.s_MinimumWavesSlice, Settings.FilterWaves);
}
private protected override void GetSubstepData(float timeToSimulate, out int substeps, out float delta)
{
substeps = Mathf.FloorToInt(timeToSimulate * _SimulationFrequency);
delta = substeps > 0 ? (1f / _SimulationFrequency) : 0f;
}
internal FoamLod()
{
_Enabled = true;

View File

@@ -59,6 +59,7 @@ namespace WaveHarmonic.Crest
{
base.Destroy();
Helpers.Destroy(_DefaultSettings);
_DefaultSettings = null;
}
}
@@ -93,6 +94,7 @@ namespace WaveHarmonic.Crest
{
base.Destroy();
Helpers.Destroy(_DefaultSettings);
_DefaultSettings = null;
}
}

View File

@@ -1,8 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -14,11 +12,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/WaterAppearance.html#volume-color-inputs")]
public sealed partial class AbsorptionLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
#if d_CrestPaint
internal override LodInputMode DefaultMode => LodInputMode.Paint;
#else

View File

@@ -1,8 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -14,11 +12,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/WaterAppearance.html#albedo-inputs")]
public sealed partial class AlbedoLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
internal override LodInputMode DefaultMode => LodInputMode.Renderer;
}
}

View File

@@ -23,14 +23,14 @@ namespace WaveHarmonic.Crest
DisplacementPass _DisplacementPass = DisplacementPass.LodIndependent;
[Tooltip("Whether to filter this input by wavelength.\n\nIf disabled, it will render to all LODs.")]
[@Predicated(nameof(_DisplacementPass), inverted: true, nameof(DisplacementPass.LodDependent))]
[@Enable(nameof(_DisplacementPass), nameof(DisplacementPass.LodDependent))]
[@GenerateAPI]
[DecoratedField, SerializeField]
bool _FilterByWavelength;
[Tooltip("Which octave to render into.\n\nFor example, set this to 2 to render into the 2m-4m octave. These refer to the same octaves as the wave spectrum editor.")]
[@Predicated(nameof(_DisplacementPass), inverted: true, nameof(DisplacementPass.LodDependent))]
[@Predicated(nameof(_FilterByWavelength))]
[@Enable(nameof(_DisplacementPass), nameof(DisplacementPass.LodDependent))]
[@Enable(nameof(_FilterByWavelength))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _OctaveWavelength = 512f;
@@ -49,7 +49,7 @@ namespace WaveHarmonic.Crest
float _MaximumDisplacementHorizontal = 0f;
[Tooltip("Use the bounding box of an attached renderer component to determine the maximum vertical displacement.")]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Renderer))]
[@Enable(nameof(_Mode), nameof(LodInputMode.Renderer))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _ReportRendererBounds = false;
@@ -63,23 +63,35 @@ namespace WaveHarmonic.Crest
_FollowHorizontalWaveMotion = true;
}
internal override float Filter(WaterRenderer water, int slice)
private protected override void Initialize()
{
return AnimatedWavesLod.FilterByWavelength(water, slice, _FilterByWavelength ? _OctaveWavelength : 0f);
base.Initialize();
_Reporter ??= new(this);
_DisplacementReporter = _Reporter;
}
private protected override void OnUpdate(WaterRenderer water)
private protected override void OnDisable()
{
base.OnUpdate(water);
base.OnDisable();
_DisplacementReporter = null;
}
internal override float Filter(WaterRenderer water, int slice)
{
return AnimatedWavesLod.FilterByWavelength(water, slice, _FilterByWavelength ? _OctaveWavelength : 0f, water.AnimatedWavesLod.Resolution);
}
bool ReportDisplacement(WaterRenderer water, ref Rect bounds, ref float horizontal, ref float vertical)
{
if (!Enabled)
{
return;
return false;
}
var maxDispVert = _MaximumDisplacementVertical;
// let water system know how far from the sea level this shape may displace the surface
// TODO: we need separate min/max vertical displacement to be optimal.
if (_ReportRendererBounds)
{
var range = Data.HeightRange;
@@ -89,19 +101,40 @@ namespace WaveHarmonic.Crest
maxDispVert = Mathf.Max(maxDispVert, Mathf.Abs(seaLevel - minY), Mathf.Abs(seaLevel - maxY));
}
if (_MaximumDisplacementHorizontal > 0f || maxDispVert > 0f)
var rect = Data.Rect;
if (bounds.Overlaps(rect, false))
{
water.ReportMaximumDisplacement(_MaximumDisplacementHorizontal, maxDispVert, 0f);
horizontal += _MaximumDisplacementHorizontal;
vertical += maxDispVert;
return true;
}
return false;
}
float ReportWaveDisplacement(WaterRenderer water, float displacement)
{
return displacement;
}
}
partial class AnimatedWavesLodInput : ISerializationCallbackReceiver
partial class AnimatedWavesLodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 1;
#pragma warning restore 414
Reporter _Reporter;
sealed class Reporter : IReportsDisplacement, IReportWaveDisplacement
{
readonly AnimatedWavesLodInput _Input;
public Reporter(AnimatedWavesLodInput input) => _Input = input;
public bool ReportDisplacement(WaterRenderer water, ref Rect bounds, ref float horizontal, ref float vertical) => _Input.ReportDisplacement(water, ref bounds, ref horizontal, ref vertical);
public float ReportWaveDisplacement(WaterRenderer water, float displacement) => _Input.ReportWaveDisplacement(water, displacement);
}
}
partial class AnimatedWavesLodInput
{
private protected override int Version => Mathf.Max(base.Version, 1);
[System.Obsolete("Please use DisplacementPass instead.")]
[Tooltip("When to render the input into the displacement data.\n\nIf enabled, it renders into all LODs of the simulation after the combine step rather than before with filtering. Furthermore, it will also affect dynamic waves.")]
@@ -110,30 +143,25 @@ namespace WaveHarmonic.Crest
[HideInInspector]
bool _RenderPostCombine = true;
[System.Obsolete]
void SetRenderPostCombine(bool previous, bool current, bool force = false)
{
if (previous == current && !force) return;
_DisplacementPass = current ? DisplacementPass.LodIndependent : DisplacementPass.LodDependent;
}
#pragma warning disable CS0612 // Type or member is obsolete
#pragma warning disable CS0618 // Type or member is obsolete
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
private protected override void OnMigrate()
{
base.OnMigrate();
if (_Version < 1)
{
SetRenderPostCombine(_RenderPostCombine, _RenderPostCombine, force: true);
_Version = 1;
}
}
#pragma warning restore CS0612 // Type or member is obsolete
#pragma warning restore CS0618 // Type or member is obsolete
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
}
}
}

View File

@@ -16,22 +16,17 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/Clipping.html#clip-inputs")]
public sealed partial class ClipLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[@Heading("Primitive")]
[Tooltip("The primitive to render (signed distance) into the simulation.")]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Primitive), hide: true)]
[@Show(nameof(_Mode), nameof(LodInputMode.Primitive))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal LodInputPrimitive _Primitive = LodInputPrimitive.Cube;
// Only Mode.Primitive SDF supports inverted.
[Tooltip("Removes clip surface data instead of adding it.")]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Primitive), hide: true)]
[@Show(nameof(_Mode), nameof(LodInputMode.Primitive))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _Inverted;
@@ -39,8 +34,7 @@ namespace WaveHarmonic.Crest
[@Heading("Culling")]
[Tooltip("Prevents inputs from cancelling each other out when aligned vertically.\n\nIt is imperfect so custom logic might be needed for your use case.")]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Paint), hide: true)]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Renderer))]
[@Show(nameof(_Mode), nameof(LodInputMode.Renderer))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _WaterHeightDistanceCulling = false;

View File

@@ -21,11 +21,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/ShallowsAndShorelines.html#sea-floor-depth")]
public sealed partial class DepthLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[@Space(10)]
[@Label("Relative Height")]

View File

@@ -1,8 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -15,11 +13,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/Waves.html#dynamic-waves-inputs")]
public sealed partial class DynamicWavesLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
internal override LodInputMode DefaultMode => LodInputMode.Renderer;
}
}

View File

@@ -1,8 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -16,11 +14,6 @@ namespace WaveHarmonic.Crest
[FilterEnum(nameof(_Blend), Filtered.Mode.Include, (int)LodInputBlend.Additive, (int)LodInputBlend.Alpha)]
public sealed partial class FlowLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
// Countering will incur thrashing. Previously we allowed the option so the
// serialized value could be "false".
private protected override bool FollowHorizontalMotion => true;

View File

@@ -1,8 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -16,11 +14,6 @@ namespace WaveHarmonic.Crest
[@FilterEnum(nameof(_Blend), Filtered.Mode.Include, (int)LodInputBlend.Additive, (int)LodInputBlend.Maximum)]
public sealed partial class FoamLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
#if d_CrestPaint
internal override LodInputMode DefaultMode => LodInputMode.Paint;
#else

View File

@@ -19,11 +19,12 @@ namespace WaveHarmonic.Crest
[Tooltip("Whether to use the manual \"Height Range\" for water chunk culling.\n\nMandatory for non mesh inputs like \"Texture\".")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
[@InlineToggle]
[@SerializeField]
bool _OverrideHeight;
[Tooltip("The minimum and maximum height value to report for water chunk culling.")]
[@Predicated(nameof(_OverrideHeight))]
[@Enable(nameof(_OverrideHeight))]
[@Range(-100, 100, Range.Clamp.None)]
[@GenerateAPI]
[SerializeField]
@@ -38,8 +39,6 @@ namespace WaveHarmonic.Crest
private protected override bool FollowHorizontalMotion => true;
internal override LodInputMode DefaultMode => LodInputMode.Geometry;
internal Rect _Rect;
internal override void InferBlend()
{
base.InferBlend();
@@ -56,35 +55,52 @@ namespace WaveHarmonic.Crest
{
base.Initialize();
_Reporter ??= new(this);
WaterChunkRenderer.HeightReporters.Add(_Reporter);
_HeightReporter = _Reporter;
}
private protected override void OnDisable()
{
base.OnDisable();
WaterChunkRenderer.HeightReporters.Remove(_Reporter);
_HeightReporter = null;
}
bool ReportHeight(ref Rect bounds, ref float minimum, ref float maximum)
bool ReportHeight(WaterRenderer water, ref Rect bounds, ref float minimum, ref float maximum)
{
if (!Enabled)
{
return false;
}
_Rect = Data.Rect;
// These modes do not provide a height yet.
if (!Data.HasHeightRange && !_OverrideHeight)
{
return false;
}
if (bounds.Overlaps(_Rect, false))
var rect = Data.Rect;
if (bounds.Overlaps(rect, false))
{
var range = _OverrideHeight ? _HeightRange : Data.HeightRange;
minimum = range.x;
maximum = range.y;
range *= Weight;
// Make relative to sea level.
range.x -= water.SeaLevel;
range.y -= water.SeaLevel;
var current = new Vector2(minimum, maximum);
range = _Blend switch
{
LodInputBlend.Additive => range + current,
LodInputBlend.Minimum => Vector2.Min(range, current),
LodInputBlend.Maximum => Vector2.Max(range, current),
_ => range,
};
minimum = Mathf.Min(minimum, range.x);
maximum = Mathf.Max(maximum, range.y);
return true;
}
@@ -100,33 +116,25 @@ namespace WaveHarmonic.Crest
{
readonly LevelLodInput _Input;
public Reporter(LevelLodInput input) => _Input = input;
public bool ReportHeight(ref Rect bounds, ref float minimum, ref float maximum) => _Input.ReportHeight(ref bounds, ref minimum, ref maximum);
public bool ReportHeight(WaterRenderer water, ref Rect bounds, ref float minimum, ref float maximum) =>
_Input.ReportHeight(water, ref bounds, ref minimum, ref maximum);
}
}
partial class LevelLodInput : ISerializationCallbackReceiver
partial class LevelLodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 1;
#pragma warning restore 414
private protected override int Version => Mathf.Max(base.Version, 1);
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
private protected override void OnMigrate()
{
base.OnMigrate();
// Version 1
// - Implemented blend mode but default value was serialized as Additive.
if (_Version < 1)
{
if (_Mode is LodInputMode.Spline or LodInputMode.Renderer) _Blend = LodInputBlend.Off;
_Version = 1;
}
}
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
// Empty.
}
}
}

View File

@@ -36,6 +36,10 @@ namespace WaveHarmonic.Crest
MonoBehaviour Component { get; }
IReportsHeight HeightReporter => null;
IReportsDisplacement DisplacementReporter => null;
IReportWaveDisplacement WaveDisplacementReporter => null;
// Allow sorting within a queue. Callers can pass in things like sibling index to
// get deterministic sorting.
int Order => Queue * k_QueueMaximumSubIndex + Mathf.Min(Component.transform.GetSiblingIndex(), k_QueueMaximumSubIndex - 1);
@@ -70,9 +74,9 @@ namespace WaveHarmonic.Crest
// For others it is a case of only supporting unsupported mode(s).
[Tooltip("Scales the input.")]
[@Predicated(typeof(AlbedoLodInput), inverted: true, hide: true)]
[@Predicated(typeof(ClipLodInput), inverted: true, hide: true)]
[@Predicated(typeof(DepthLodInput), inverted: true, hide: true)]
[@Hide(typeof(AlbedoLodInput))]
[@Hide(typeof(ClipLodInput))]
[@Hide(typeof(DepthLodInput))]
[@Range(0f, 1f)]
[@GenerateAPI]
[SerializeField]
@@ -84,17 +88,17 @@ namespace WaveHarmonic.Crest
int _Queue;
[Tooltip("How this input blends into existing data.\n\nSimilar to blend operations in shaders. For inputs which have materials, use the blend functionality on the shader/material.")]
[@Predicated(typeof(AbsorptionLodInput), inverted: true, hide: true)]
[@Predicated(typeof(AlbedoLodInput), inverted: true, hide: true)]
[@Predicated(typeof(AnimatedWavesLodInput), inverted: true, hide: true)]
[@Predicated(typeof(ClipLodInput), inverted: true, hide: true)]
[@Predicated(typeof(DepthLodInput), inverted: true, hide: true)]
[@Predicated(typeof(DynamicWavesLodInput), inverted: true, hide: true)]
[@Predicated(typeof(ScatteringLodInput), inverted: true, hide: true)]
[@Predicated(typeof(ShadowLodInput), inverted: true, hide: true)]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Primitive))]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Renderer))]
[@Hide(typeof(AbsorptionLodInput))]
[@Hide(typeof(AlbedoLodInput))]
[@Hide(typeof(AnimatedWavesLodInput))]
[@Hide(typeof(ClipLodInput))]
[@Hide(typeof(DepthLodInput))]
[@Hide(typeof(DynamicWavesLodInput))]
[@Hide(typeof(ScatteringLodInput))]
[@Hide(typeof(ShadowLodInput))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Global))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Primitive))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Renderer))]
[@Filtered]
[@GenerateAPI]
[SerializeField]
@@ -102,35 +106,35 @@ namespace WaveHarmonic.Crest
[@Label("Feather")]
[Tooltip("The width of the feathering to soften the edges to blend inputs.\n\nInputs that do not support feathering will have this field disabled or hidden in UI.")]
[@Predicated(typeof(AlbedoLodInput), inverted: true, hide: true)]
[@Predicated(typeof(AnimatedWavesLodInput), inverted: true, hide: true)]
[@Predicated(typeof(ClipLodInput), inverted: true, hide: true)]
[@Predicated(typeof(DepthLodInput), inverted: true, hide: true)]
[@Predicated(typeof(DynamicWavesLodInput), inverted: true, hide: true)]
[@Predicated(typeof(LevelLodInput), inverted: true, hide: true)]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Renderer))]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Primitive))]
[@Hide(typeof(AlbedoLodInput))]
[@Hide(typeof(AnimatedWavesLodInput))]
[@Hide(typeof(ClipLodInput))]
[@Hide(typeof(DepthLodInput))]
[@Hide(typeof(DynamicWavesLodInput))]
[@Hide(typeof(LevelLodInput))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Renderer))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Global))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Primitive))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _FeatherWidth = 0.1f;
[Tooltip("How this input responds to horizontal displacement.\n\nIf false, data will not move horizontally with the waves. Has a small performance overhead when disabled. Only suitable for inputs of small size.")]
[@Predicated(typeof(ClipLodInput), inverted: true, hide: true)]
[@Predicated(typeof(FlowLodInput), inverted: true, hide: true)]
[@Predicated(typeof(LevelLodInput), inverted: true, hide: true)]
[@Predicated(typeof(ShapeWaves), inverted: true, hide: true)]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Spline))]
[@Hide(typeof(ClipLodInput))]
[@Hide(typeof(FlowLodInput))]
[@Hide(typeof(LevelLodInput))]
[@Hide(typeof(ShapeWaves))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Global))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Spline))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
private protected bool _FollowHorizontalWaveMotion = false;
[@Heading("Mode")]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Unset), hide: true)]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Primitive), hide: true)]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global), hide: true)]
[@Hide(nameof(_Mode), nameof(LodInputMode.Unset))]
[@Hide(nameof(_Mode), nameof(LodInputMode.Primitive))]
[@Hide(nameof(_Mode), nameof(LodInputMode.Global))]
[@Stripped]
[SerializeReference]
internal LodInputData _Data;
@@ -140,7 +144,7 @@ namespace WaveHarmonic.Crest
[@Group("Debug", order = k_DebugGroupOrder)]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Global))]
[@Disable(nameof(_Mode), nameof(LodInputMode.Global))]
[@DecoratedField, SerializeField]
internal bool _DrawBounds;
@@ -376,6 +380,16 @@ namespace WaveHarmonic.Crest
//
#if UNITY_EDITOR
void SetMode(LodInputMode previous, LodInputMode current)
{
if (previous == current) return;
if (!isActiveAndEnabled) { ChangeMode(Mode); return; }
OnDisable();
ChangeMode(Mode);
UnityEditor.EditorTools.ToolManager.RefreshAvailableTools();
OnEnable();
}
[@OnChange(skipIfInactive: false)]
void OnChange(string propertyPath, object previousValue)
{
@@ -385,15 +399,11 @@ namespace WaveHarmonic.Crest
SetQueue((int)previousValue, _Queue);
break;
case nameof(_Mode):
if (!isActiveAndEnabled) { ChangeMode(Mode); break; }
OnDisable();
ChangeMode(Mode);
UnityEditor.EditorTools.ToolManager.RefreshAvailableTools();
OnEnable();
SetMode((LodInputMode)previousValue, Mode);
break;
case nameof(_Blend):
// TODO: Make compatible with disabled.
if (isActiveAndEnabled) Data.OnChange($"../{propertyPath}", previousValue);
if (isActiveAndEnabled) Data?.OnChange($"../{propertyPath}", previousValue);
break;
}
}
@@ -467,6 +477,9 @@ namespace WaveHarmonic.Crest
partial class LodInput
{
Input _Input;
private protected IReportsHeight _HeightReporter;
internal IReportsDisplacement _DisplacementReporter;
private protected IReportWaveDisplacement _WaveDisplacementReporter;
sealed class Input : ILodInput
{
@@ -478,6 +491,9 @@ namespace WaveHarmonic.Crest
public int Pass => _Input.Pass;
public Rect Rect => _Input.Rect;
public MonoBehaviour Component => _Input;
public IReportsHeight HeightReporter => _Input._HeightReporter;
public IReportsDisplacement DisplacementReporter => _Input._DisplacementReporter;
public IReportWaveDisplacement WaveDisplacementReporter => _Input._WaveDisplacementReporter;
public float Filter(WaterRenderer water, int slice) => _Input.Filter(water, slice);
public void Draw(Lod lod, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1, int slice = -1) => _Input.Draw(lod, buffer, target, pass, weight, slice);
}

View File

@@ -4,6 +4,7 @@
using System;
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
@@ -24,7 +25,7 @@ namespace WaveHarmonic.Crest
/// Data storage for an input, pertinent to the associated input mode.
/// </summary>
[Serializable]
public abstract class LodInputData
public abstract partial class LodInputData : Versioned
{
[SerializeField, HideInInspector]
internal LodInput _Input;

View File

@@ -2,10 +2,15 @@
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
using WaveHarmonic.Crest.Utility;
#if !UNITY_6000_0_OR_NEWER
using GraphicsFormatUsage = UnityEngine.Experimental.Rendering.FormatUsage;
#endif
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -33,7 +38,9 @@ namespace WaveHarmonic.Crest
[Tooltip("Populates the DepthProbe in Start.")]
OnStart = 0,
// EveryFrame = 1,
/// <inheritdoc cref="Generated.DepthProbeRefreshMode.EveryFrame"/>
[Tooltip("Populates the DepthProbe every frame.")]
EveryFrame = 1,
/// <inheritdoc cref="Generated.DepthProbeRefreshMode.ViaScripting"/>
[Tooltip("Requires manual updating via DepthProbe.Populate.")]
@@ -77,8 +84,13 @@ namespace WaveHarmonic.Crest
[SerializeField]
internal DepthProbeMode _Type = DepthProbeMode.RealTime;
[Tooltip("Controls how the probe is refreshed in the Player.\n\nCall Populate() if scripting.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime), hide: true)]
[Tooltip("Where the Depth Probe is placed.\n\nThe default performs the best.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
Placement _Placement;
[Tooltip("Controls how the probe is refreshed in the Player.\n\nCall Populate() if scripting.\n\nWhen Placement is not set to Fixed, EveryFrame is still applicable, but the others are not (update only happens on position change).")]
[@Show(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal DepthProbeRefreshMode _RefreshMode = DepthProbeRefreshMode.OnStart;
@@ -87,26 +99,26 @@ namespace WaveHarmonic.Crest
[@Heading("Capture")]
[Tooltip("The layers to render into the probe.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@GenerateAPI(Setter.Dirty)]
[@DecoratedField, SerializeField]
internal LayerMask _Layers = 1; // Default
[Tooltip("The resolution of the probe.\n\nLower will be more efficient.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@GenerateAPI(Setter.Dirty)]
[@DecoratedField, SerializeField]
internal int _Resolution = 512;
[Tooltip("The far and near plane of the depth probe camera respectively, relative to the transform.\n\nDepth is captured top-down and orthographically. The gizmo will visualize this range as the bottom box.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@Range(-100f, 100f, Range.Clamp.None)]
[@GenerateAPI(Setter.Dirty)]
[SerializeField]
internal Vector2 _CaptureRange = new(-1000f, 1000f);
[Tooltip("Fills holes left by the maximum of the capture range.\n\nSetting the maximum capture range lower than the highest point of geometry can be useful for eliminating depth artifacts from overhangs, but the side effect is there will be a hole in the depth data where geometry is clipped by the near plane. This will only capture where the holes are to fill them in. This height is relative to the maximum capture range. Set to zero to skip.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@Range(0f, 100f, Range.Clamp.Minimum)]
[UnityEngine.Serialization.FormerlySerializedAs("_MaximumHeight")]
[@GenerateAPI(Setter.Dirty)]
@@ -115,14 +127,14 @@ namespace WaveHarmonic.Crest
[@Label("Enable Back-Face Inclusion")]
[Tooltip("Increase coverage by testing mesh back faces within the Fill Holes area.\n\nUses the back-faces to include meshes where the front-face is within the Fill Holes area and the back-face is within the capture area. An example would be an upright cylinder not over a hole but was not captured due to the top being clipped by the near plane.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Predicated(nameof(_FillHolesCaptureHeight), inverted: false, 0f)]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@Disable(nameof(_FillHolesCaptureHeight), 0f)]
[@GenerateAPI(Setter.Dirty)]
[@DecoratedField, SerializeField]
bool _EnableBackFaceInclusion = true;
[Tooltip("Overrides global quality settings.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[GenerateAPI(Setter.None)]
[@DecoratedField, SerializeField]
QualitySettingsOverride _QualitySettingsOverride = new()
@@ -139,7 +151,7 @@ namespace WaveHarmonic.Crest
[Tooltip("Baked probe.\n\nCan only bake in edit mode.")]
[@Disabled]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.Baked), hide: true)]
[@Show(nameof(_Type), nameof(DepthProbeMode.Baked))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
#pragma warning disable 649
@@ -151,7 +163,7 @@ namespace WaveHarmonic.Crest
[@Label("Generate")]
[Tooltip("Generate a signed distance field for the shoreline.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@GenerateAPI(Setter.Dirty)]
[@DecoratedField, SerializeField]
internal bool _GenerateSignedDistanceField = true;
@@ -159,8 +171,8 @@ namespace WaveHarmonic.Crest
// Additional rounds of JFA, over the standard log2(resolution), can help reduce
// innacuracies from JFA, see paper for details.
[Tooltip("How many additional Jump Flood rounds to use.\n\nThe standard number of rounds is log2(resolution). Additional rounds can reduce innaccuracies.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@Predicated(nameof(_GenerateSignedDistanceField))]
[@Enable(nameof(_Type), nameof(DepthProbeMode.RealTime))]
[@Enable(nameof(_GenerateSignedDistanceField))]
[@GenerateAPI(Setter.Dirty)]
[@DecoratedField, SerializeField]
int _AdditionalJumpFloodRounds = 7;
@@ -174,11 +186,6 @@ namespace WaveHarmonic.Crest
[System.Serializable]
internal sealed class DebugFields
{
[Tooltip("Will render into the probe every frame. Intended for debugging, will generate garbage.")]
[@Predicated(nameof(_Type), inverted: true, nameof(DepthProbeMode.RealTime))]
[@DecoratedField, SerializeField]
public bool _ForceAlwaysUpdateDebug;
[Tooltip("Shows hidden objects like the camera which renders into the probe.")]
[@DecoratedField, SerializeField]
public bool _ShowHiddenObjects;
@@ -342,6 +349,11 @@ namespace WaveHarmonic.Crest
if (transform.hasChanged)
{
_RecalculateBounds = true;
if (_Placement == Placement.Transform)
{
UpdatePosition(water, transform);
}
}
}
@@ -351,6 +363,14 @@ namespace WaveHarmonic.Crest
transform.hasChanged = false;
}
void OnBeforeBuildCommandBuffer(WaterRenderer water, Camera camera)
{
if (_Placement == Placement.Viewpoint)
{
UpdatePosition(water, camera.transform);
}
}
internal bool Outdated => _CurrentStateHash != _RenderedStateHash;
bool IsTextureOutdated(RenderTexture texture, bool target)
@@ -358,16 +378,19 @@ namespace WaveHarmonic.Crest
return texture != null &&
texture.width != _Resolution ||
texture.height != _Resolution ||
texture.format != (target ? RenderTextureFormat.Depth : FinalFormat);
texture.graphicsFormat != (target ? SystemInfo.GetGraphicsFormat(DefaultFormat.DepthStencil) : FinalFormat);
}
RenderTextureFormat FinalFormat => _GenerateSignedDistanceField ? RenderTextureFormat.RGFloat : RenderTextureFormat.RFloat;
GraphicsFormat FinalFormat => _GenerateSignedDistanceField
? Helpers.GetCompatibleTextureFormat(GraphicsFormat.R32G32_SFloat, Helpers.s_DataGraphicsFormatUsage, "Depth Probe", randomWrite: true)
: GraphicsFormat.R32_SFloat;
void MakeRT(RenderTexture texture, bool target)
{
var format = target ? RenderTextureFormat.Depth : FinalFormat;
var format = target ? SystemInfo.GetGraphicsFormat(DefaultFormat.DepthStencil) : FinalFormat;
var descriptor = texture.descriptor;
descriptor.colorFormat = format;
descriptor.graphicsFormat = target ? GraphicsFormat.None : format;
descriptor.depthStencilFormat = target ? format : GraphicsFormat.None;
descriptor.width = descriptor.height = _Resolution;
descriptor.depthBufferBits = target ? 24 : 0;
descriptor.useMipMap = false;
@@ -375,7 +398,11 @@ namespace WaveHarmonic.Crest
descriptor.enableRandomWrite = !target;
texture.descriptor = descriptor;
texture.Create();
Debug.Assert(SystemInfo.SupportsRenderTextureFormat(format), "Crest: The graphics device does not support the render texture format " + format.ToString());
if (!target)
{
Debug.Assert(SystemInfo.IsFormatSupported(format, GraphicsFormatUsage.Sample), "Crest: The graphics device does not support the render texture format " + format.ToString());
}
}
bool InitObjects()
@@ -704,7 +731,7 @@ namespace WaveHarmonic.Crest
var descriptor = new RenderTextureDescriptor(_Resolution, _Resolution)
{
autoGenerateMips = false,
colorFormat = RenderTextureFormat.RGHalf,
graphicsFormat = Helpers.GetCompatibleTextureFormat(GraphicsFormat.R16G16_SFloat, Helpers.s_DataGraphicsFormatUsage, "Depth Probe SDF", randomWrite: true),
useMipMap = false,
enableRandomWrite = true,
depthBufferBits = 0,
@@ -838,6 +865,9 @@ namespace WaveHarmonic.Crest
ILodInput.Attach(_Input, DepthLod.s_Inputs);
HashState(ref _CurrentStateHash);
WaterRenderer.s_OnBeforeBuildCommandBuffer -= OnBeforeBuildCommandBuffer;
WaterRenderer.s_OnBeforeBuildCommandBuffer += OnBeforeBuildCommandBuffer;
#if CREST_DEBUG
if (_Debug._ShowSimulationDataInScene)
{
@@ -852,6 +882,8 @@ namespace WaveHarmonic.Crest
base.OnDisable();
ILodInput.Detach(_Input, DepthLod.s_Inputs);
WaterRenderer.s_OnBeforeBuildCommandBuffer -= OnBeforeBuildCommandBuffer;
#if CREST_DEBUG
if (_Debug._ShowSimulationDataInScene)
{
@@ -926,6 +958,29 @@ namespace WaveHarmonic.Crest
}
}
sealed partial class DepthProbe
{
Vector3 _PreviousPosition;
void UpdatePosition(WaterRenderer water, Transform target)
{
var position = target.position.XNZ(water.transform.position.y);
var texelSize = Scale / _Resolution;
position.x = Mathf.Round(position.x / texelSize.x) * texelSize.x;
position.z = Mathf.Round(position.z / texelSize.y) * texelSize.y;
if ((_Placement == Placement.Viewpoint && !water.IsSingleViewpointMode) || _RefreshMode == DepthProbeRefreshMode.EveryFrame || _PreviousPosition != position)
{
Managed = true;
OverridePosition = true;
Position = position;
Scale = new(transform.lossyScale.x, transform.lossyScale.z);
Populate();
_PreviousPosition = position;
}
}
}
sealed partial class DepthProbe
{
// Hash is used to notify whether the probe is outdated in the UI.
@@ -974,7 +1029,7 @@ namespace WaveHarmonic.Crest
void Update()
{
if (_Debug._ForceAlwaysUpdateDebug && _Type != DepthProbeMode.Baked)
if (_RefreshMode == DepthProbeRefreshMode.EveryFrame && _Placement == Placement.Fixed && _Type != DepthProbeMode.Baked)
{
Populate();
}
@@ -987,30 +1042,21 @@ namespace WaveHarmonic.Crest
#endif
}
partial class DepthProbe : ISerializationCallbackReceiver
partial class DepthProbe
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 1;
#pragma warning restore 414
private protected override int Version => Mathf.Max(base.Version, 1);
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
private protected override void OnMigrate()
{
base.OnMigrate();
// Version 1 (2024.06.04)
// - Added new capture options replacing Maximum Height's behaviour.
if (_Version < 1)
{
_CaptureRange.y = _FillHolesCaptureHeight;
_FillHolesCaptureHeight = 0f;
_Version = 1;
}
}
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
}
}
}

View File

@@ -26,11 +26,12 @@ namespace WaveHarmonic.Crest
[Tooltip("Whether to set the shader pass manually.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
[@InlineToggle]
[@SerializeField]
internal bool _OverrideShaderPass;
[Tooltip("The shader pass to execute.\n\nSet to -1 to execute all passes.")]
[@Predicated(nameof(_OverrideShaderPass))]
[@Enable(nameof(_OverrideShaderPass))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal int _ShaderPassIndex;

View File

@@ -1,8 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -14,11 +12,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/WaterAppearance.html#volume-color-inputs")]
public sealed partial class ScatteringLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
#if d_CrestPaint
internal override LodInputMode DefaultMode => LodInputMode.Paint;
#else

View File

@@ -1,8 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest
{
/// <summary>
@@ -14,11 +12,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/WaterAppearance.html#shadows-lod")]
public sealed partial class ShadowLodInput : LodInput
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
internal override LodInputMode DefaultMode => LodInputMode.Renderer;
}
}

View File

@@ -14,21 +14,32 @@ namespace WaveHarmonic.Crest
{
// Waves
[Tooltip("Whether to use the wind turbulence on this component rather than the global wind turbulence.\n\nGlobal wind turbulence comes from the Water Renderer component.")]
[Tooltip("Whether to apply the options shown when \"Show Advanced Controls\" is active.")]
[@Order(nameof(_EvaluateSpectrumAtRunTimeEveryFrame), Order.Placement.Below)]
[@DecoratedField]
[@GenerateAPI]
[@InlineToggle(order = -3), SerializeField]
[@SerializeField]
bool _ApplyAdvancedSpectrumControls;
[Tooltip("Whether to use the wind turbulence on this component rather than the global wind turbulence.\n\nGlobal wind turbulence comes from the Water Renderer component.")]
[@Order("Waves")]
[@InlineToggle]
[@GenerateAPI]
[@SerializeField]
bool _OverrideGlobalWindTurbulence;
[Tooltip("How turbulent/chaotic the waves are.")]
[@Predicated(nameof(_OverrideGlobalWindTurbulence), hide: true)]
[@Order("Waves")]
[@Show(nameof(_OverrideGlobalWindTurbulence))]
[@ShowComputedProperty(nameof(WindTurbulence))]
[@Range(0, 1, order = -4)]
[@Range(0, 1)]
[@GenerateAPI(Getter.Custom)]
[SerializeField]
float _WindTurbulence = 0.145f;
[Tooltip("How aligned the waves are with wind.")]
[@Range(0, 1, order = -5)]
[@Range(0, 1)]
[@Order("Waves")]
[@GenerateAPI]
[SerializeField]
float _WindAlignment;
@@ -37,21 +48,30 @@ namespace WaveHarmonic.Crest
// Generation
[Tooltip("FFT waves will loop with a period of this many seconds.")]
[@Order("Generation Settings")]
[@Range(4f, 128f, Range.Clamp.Minimum)]
[@GenerateAPI]
[SerializeField]
float _TimeLoopLength = Mathf.Infinity;
[Header("Culling")]
[@Heading("Culling")]
[Tooltip("Whether to override automatic culling based on heuristics.")]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
bool _OverrideCulling;
[Tooltip("Maximum amount the surface will be displaced vertically from sea level.\n\nIncrease this if gaps appear at bottom of screen.")]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
float _MaximumVerticalDisplacement = 10f;
[Tooltip("Maximum amount a point on the surface will be displaced horizontally by waves from its rest position.\n\nIncrease this if gaps appear at sides of screen.")]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
float _MaximumHorizontalDisplacement = 15f;
@@ -63,7 +83,7 @@ namespace WaveHarmonic.Crest
#endif
[Tooltip("Enable running this FFT with baked data.\n\nThis makes the FFT periodic (repeating in time).")]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Global), hide: true)]
[@Show(nameof(_Mode), nameof(LodInputMode.Global))]
[@DecoratedField, SerializeField]
internal bool _EnableBakedCollision = false;
@@ -72,8 +92,8 @@ namespace WaveHarmonic.Crest
#endif
[Tooltip("Frames per second of baked data.\n\nLarger values may help the collision track the surface closely at the cost of more frames and increase baked data size.")]
[@Predicated(nameof(_EnableBakedCollision))]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Global), hide: true)]
[@Enable(nameof(_EnableBakedCollision))]
[@Show(nameof(_Mode), nameof(LodInputMode.Global))]
[@DecoratedField, SerializeField]
internal int _TimeResolution = 4;
@@ -82,8 +102,8 @@ namespace WaveHarmonic.Crest
#endif
[Tooltip("Smallest wavelength required in collision.\n\nTo preview the effect of this, disable power sliders in spectrum for smaller values than this number. Smaller values require more resolution and increase baked data size.")]
[@Predicated(nameof(_EnableBakedCollision))]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Global), hide: true)]
[@Enable(nameof(_EnableBakedCollision))]
[@Show(nameof(_Mode), nameof(LodInputMode.Global))]
[@DecoratedField, SerializeField]
internal float _SmallestWavelengthRequired = 2f;
@@ -92,8 +112,8 @@ namespace WaveHarmonic.Crest
#endif
[Tooltip("FFT waves will loop with a period of this many seconds.\n\nSmaller values decrease data size but can make waves visibly repetitive.")]
[@Predicated(nameof(_EnableBakedCollision))]
[@Predicated(nameof(_Mode), inverted: true, nameof(LodInputMode.Global), hide: true)]
[@Enable(nameof(_EnableBakedCollision))]
[@Show(nameof(_Mode), nameof(LodInputMode.Global))]
[@Range(4f, 128f)]
[SerializeField]
internal float _BakedTimeLoopLength = 32f;
@@ -104,8 +124,9 @@ namespace WaveHarmonic.Crest
#endif
_TimeLoopLength;
// WebGPU will crash above at 128.
private protected override int MinimumResolution => 16;
private protected override int MaximumResolution => int.MaxValue;
private protected override int MaximumResolution => Helpers.IsWebGPU ? 64 : int.MaxValue;
FFTCompute _FFTCompute;
@@ -119,7 +140,8 @@ namespace WaveHarmonic.Crest
WindDirRadForFFT,
WindTurbulence,
_WindAlignment,
gravity
gravity,
_ApplyAdvancedSpectrumControls
);
private protected override void OnUpdate(WaterRenderer water)
@@ -189,13 +211,28 @@ namespace WaveHarmonic.Crest
{
if (!Enabled) return;
// Apply weight or will cause popping due to scale change.
MaximumReportedHorizontalDisplacement = _MaximumHorizontalDisplacement * Weight;
MaximumReportedVerticalDisplacement = MaximumReportedWavesDisplacement = _MaximumVerticalDisplacement * Weight;
if (Mode == LodInputMode.Global)
if (_OverrideCulling)
{
water.ReportMaximumDisplacement(MaximumReportedHorizontalDisplacement, MaximumReportedVerticalDisplacement, MaximumReportedVerticalDisplacement);
// Apply weight or will cause popping due to scale change.
MaximumReportedHorizontalDisplacement = _MaximumHorizontalDisplacement * Weight;
MaximumReportedVerticalDisplacement = MaximumReportedWavesDisplacement = _MaximumVerticalDisplacement * Weight;
}
else
{
var powerLinear = 0f;
for (var i = 0; i < WaveSpectrum.k_NumberOfOctaves; i++)
{
powerLinear += _ActiveSpectrum._PowerLinearScales[i];
}
// Empirical multiplier (3-5), went with 5 to be safe.
// We may be missing some more multipliers from the compute shader.
// Look there if this proves insufficient.
var wind = Mathf.Clamp01(WindSpeedKPH / 150f);
var rms = Mathf.Sqrt(powerLinear) * 5f;
MaximumReportedHorizontalDisplacement = rms * _ActiveSpectrum._Chop * Weight * wind;
MaximumReportedVerticalDisplacement = MaximumReportedWavesDisplacement = rms * Weight * wind;
}
}
@@ -250,28 +287,18 @@ namespace WaveHarmonic.Crest
}
}
partial class ShapeFFT : ISerializationCallbackReceiver
partial class ShapeFFT
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 2;
#pragma warning restore 414
private protected override int Version => Mathf.Max(base.Version, 2);
void ISerializationCallbackReceiver.OnAfterDeserialize()
private protected override void OnMigrate()
{
_Version = MigrateV1(_Version);
base.OnMigrate();
if (_Version < 2)
{
_OverrideGlobalWindTurbulence = true;
}
_Version = MigrateV2(_Version);
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
// Empty.
}
}

View File

@@ -19,14 +19,18 @@ namespace WaveHarmonic.Crest
[@Space(10)]
// If disabled, there will be a difference due to Random.value.
[Tooltip("Use a swell spectrum as the default.\n\nUses a swell spectrum as default (when none is assigned), and disabled reverse waves.")]
[@Order("Waves")]
[@DecoratedField]
[@GenerateAPI]
[@DecoratedField(order = -3), SerializeField]
[@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)]
[@Order("Waves")]
[@Disable(nameof(_Swell))]
[@Range(0f, 1f)]
[@GenerateAPI(Getter.Custom)]
[SerializeField]
float _ReverseWaveWeight = 0.5f;
@@ -35,23 +39,28 @@ namespace WaveHarmonic.Crest
// Generation Settings
[Tooltip("How many wave components to generate in each octave.")]
[@Order("Generation Settings")]
[@Delayed]
[@GenerateAPI]
[SerializeField]
int _ComponentsPerOctave = 8;
[Tooltip("Change to get a different set of waves.")]
[@Order("Generation Settings")]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
int _RandomSeed = 0;
[Tooltip("Prevent data arrays from being written to so one can provide their own.")]
[@Order("Generation Settings")]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
bool _ManualGeneration;
private protected override int MinimumResolution => 8;
private protected override int MaximumResolution => 64;
private protected override int MaximumResolution => int.MaxValue;
float _WindSpeedWhenGenerated = -1f;
@@ -93,12 +102,7 @@ namespace WaveHarmonic.Crest
float[] _Amplitudes2;
float[] _Phases2;
struct GerstnerCascadeParams
{
public int _StartIndex;
}
ComputeBuffer _BufferCascadeParameters;
readonly GerstnerCascadeParams[] _CascadeParameters = new GerstnerCascadeParams[k_CascadeCount + 1];
readonly int[] _StartIndices = new int[k_CascadeCount];
// Caution - order here impact performance. Rearranging these to match order
// they're read in the compute shader made it 50% slower..
@@ -111,6 +115,7 @@ namespace WaveHarmonic.Crest
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;
@@ -119,8 +124,7 @@ namespace WaveHarmonic.Crest
ComputeBuffer _BufferWaveData;
readonly GerstnerWaveComponent4[] _WaveData = new GerstnerWaveComponent4[k_MaximumWaveComponents / 4];
ComputeShader _ShaderGerstner;
int _KernelGerstner = -1;
WaterResources.GerstnerCompute _Shader;
private protected override WaveSpectrum DefaultSpectrum => _Swell ? SwellSpectrum : WindSpectrum;
static WaveSpectrum s_SwellSpectrum;
@@ -154,7 +158,7 @@ namespace WaveHarmonic.Crest
{
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_StartIndices = Shader.PropertyToID("_Crest_StartIndices");
public static readonly int s_GerstnerWaveData = Shader.PropertyToID("_Crest_GerstnerWaveData");
}
@@ -179,7 +183,7 @@ namespace WaveHarmonic.Crest
{
if (_WaveBuffers == null)
{
_WaveBuffers = new(_Resolution, _Resolution, 0, GraphicsFormat.R16G16B16A16_SFloat);
_WaveBuffers = new(Resolution, Resolution, 0, Helpers.GetCompatibleTextureFormat(GraphicsFormat.R16G16B16A16_SFloat, randomWrite: true));
}
else
{
@@ -187,7 +191,7 @@ namespace WaveHarmonic.Crest
}
{
_WaveBuffers.width = _WaveBuffers.height = _Resolution;
_WaveBuffers.width = _WaveBuffers.height = Resolution;
_WaveBuffers.wrapMode = TextureWrapMode.Clamp;
_WaveBuffers.antiAliasing = 1;
_WaveBuffers.filterMode = FilterMode.Bilinear;
@@ -200,14 +204,11 @@ namespace WaveHarmonic.Crest
_WaveBuffers.Create();
}
_BufferCascadeParameters?.Release();
_BufferWaveData?.Release();
_BufferCascadeParameters = new(k_CascadeCount + 1, UnsafeUtility.SizeOf<GerstnerCascadeParams>());
_BufferWaveData = new(k_MaximumWaveComponents / 4, UnsafeUtility.SizeOf<GerstnerWaveComponent4>());
_ShaderGerstner = WaterResources.Instance.Compute._Gerstner;
_KernelGerstner = _ShaderGerstner.FindKernel("Gerstner");
_Shader = WaterResources.Instance._ComputeLibrary._GerstnerCompute;
}
private protected override void OnUpdate(WaterRenderer water)
@@ -216,7 +217,7 @@ namespace WaveHarmonic.Crest
base.OnUpdate(water);
if (_WaveBuffers == null || _Resolution != _WaveBuffers.width || _BufferCascadeParameters == null || _BufferWaveData == null)
if (_WaveBuffers == null || Resolution != _WaveBuffers.width || _BufferWaveData == null)
{
InitData();
}
@@ -266,7 +267,7 @@ namespace WaveHarmonic.Crest
var cascadeIdx = 0;
var componentIdx = 0;
var outputIdx = 0;
_CascadeParameters[0]._StartIndex = 0;
_StartIndices[0] = 0;
if (_ManualGeneration)
{
@@ -313,12 +314,14 @@ namespace WaveHarmonic.Crest
_WaveData[vi]._Omega[ei] = 0f;
_WaveData[vi]._Phase[ei] = 0f;
_WaveData[vi]._ChopAmplitude[ei] = 0f;
if (!_ManualGeneration)
if (!_ManualGeneration && !_Swell)
{
_WaveData[vi]._Phase2[ei] = 0f;
_WaveData[vi]._Amplitude2[ei] = 0f;
_WaveData[vi]._ChopAmplitude2[ei] = 0f;
}
ei = (ei + 1) % 4;
outputIdx++;
}
@@ -326,7 +329,7 @@ namespace WaveHarmonic.Crest
if (outputIdx > 0 && _FirstCascade < 0) _FirstCascade = cascadeIdx;
cascadeIdx++;
_CascadeParameters[cascadeIdx]._StartIndex = outputIdx / 4;
_StartIndices[cascadeIdx] = outputIdx / 4;
minWl *= 2f;
//Debug.Log($"Crest: {cascadeIdx}: start {_cascadeParams[cascadeIdx]._startIndex} minWL {minWl}");
@@ -343,7 +346,7 @@ namespace WaveHarmonic.Crest
var chopScale = _ActiveSpectrum._ChopScales[componentIdx / _ComponentsPerOctave];
_WaveData[vi]._ChopAmplitude[ei] = -chopScale * _ActiveSpectrum._Chop * _Amplitudes[componentIdx];
if (!_ManualGeneration)
if (!_ManualGeneration && !_Swell)
{
_WaveData[vi]._Amplitude2[ei] = _Amplitudes2[componentIdx];
_WaveData[vi]._ChopAmplitude2[ei] = -chopScale * _ActiveSpectrum._Chop * _Amplitudes2[componentIdx];
@@ -385,7 +388,7 @@ namespace WaveHarmonic.Crest
_WaveData[vi]._Omega[ei] = k * c;
_WaveData[vi]._Phase[ei] = Mathf.Repeat(_Phases[componentIdx], Mathf.PI * 2f);
if (!_ManualGeneration)
if (!_ManualGeneration && !_Swell)
{
_WaveData[vi]._Phase2[ei] = Mathf.Repeat(_Phases2[componentIdx], Mathf.PI * 2f);
}
@@ -410,40 +413,46 @@ namespace WaveHarmonic.Crest
_WaveData[vi]._Omega[ei] = 0f;
_WaveData[vi]._Phase[ei] = 0f;
_WaveData[vi]._ChopAmplitude[ei] = 0f;
if (!_ManualGeneration)
if (!_ManualGeneration && !_Swell)
{
_WaveData[vi]._Phase2[ei] = 0f;
_WaveData[vi]._Amplitude2[ei] = 0f;
_WaveData[vi]._ChopAmplitude2[ei] = 0f;
}
ei = (ei + 1) % 4;
outputIdx++;
}
}
while (cascadeIdx < k_CascadeCount)
// Fill the remaining.
while (cascadeIdx < k_CascadeCount - 1)
{
cascadeIdx++;
minWl *= 2f;
_CascadeParameters[cascadeIdx]._StartIndex = outputIdx / 4;
_StartIndices[cascadeIdx] = outputIdx / 4;
//Debug.Log($"Crest: {cascadeIdx}: start {_cascadeParams[cascadeIdx]._startIndex} minWL {minWl}");
}
_BufferCascadeParameters.SetData(_CascadeParameters);
_BufferWaveData.SetData(_WaveData);
}
void UpdateGenerateWaves(CommandBuffer buf)
void UpdateGenerateWaves(CommandBuffer buffer)
{
// 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);
CoreUtils.SetRenderTarget(buffer, _WaveBuffers, ClearFlag.Color);
buf.DispatchCompute(_ShaderGerstner, _KernelGerstner, _WaveBuffers.width / Lod.k_ThreadGroupSizeX, _WaveBuffers.height / Lod.k_ThreadGroupSizeY, _LastCascade - _FirstCascade + 1);
var wrapper = new PropertyWrapperCompute(buffer, _Shader._Shader, _Shader._ExecuteKernel);
wrapper.SetFloat(ShaderIDs.s_TextureRes, _WaveBuffers.width);
wrapper.SetInteger(ShaderIDs.s_FirstCascadeIndex, _FirstCascade);
wrapper.SetIntegers(ShaderIDs.s_StartIndices, _StartIndices);
wrapper.SetBuffer(ShaderIDs.s_GerstnerWaveData, _BufferWaveData);
wrapper.SetTexture(ShapeWaves.ShaderIDs.s_WaveBuffer, _WaveBuffers);
wrapper.SetKeyword(_Shader._WavePairsKeyword, !_Swell && _ReverseWaveWeight > 0f);
wrapper.Dispatch(_WaveBuffers.width / Lod.k_ThreadGroupSizeX, _WaveBuffers.height / Lod.k_ThreadGroupSizeY, _LastCascade - _FirstCascade + 1);
}
/// <summary>
@@ -503,16 +512,16 @@ namespace WaveHarmonic.Crest
{
var amp = _ActiveSpectrum.GetAmplitude(_Wavelengths[i], _ComponentsPerOctave, windSpeed, water.Gravity, out _Powers[i]);
_Amplitudes[i] = Random.value * amp;
_Amplitudes2[i] = Random.value * amp * ReverseWaveWeight;
if (!_Swell)
{
_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];
@@ -524,12 +533,13 @@ namespace WaveHarmonic.Crest
var rnd = (i + Random.value) / _ComponentsPerOctave;
_Phases[index] = 2f * Mathf.PI * rnd;
var rnd2 = (i + Random.value) / _ComponentsPerOctave;
_Phases2[index] = 2f * Mathf.PI * rnd2;
if (!_Swell)
{
var rnd2 = (i + Random.value) / _ComponentsPerOctave;
_Phases2[index] = 2f * Mathf.PI * rnd2;
}
}
}
Random.state = randomStateBkp;
}
private protected override void ReportMaxDisplacement(WaterRenderer water)
@@ -546,23 +556,23 @@ namespace WaveHarmonic.Crest
return;
}
var ampSum = 0f;
MaximumReportedVerticalDisplacement = 0;
MaximumReportedHorizontalDisplacement = 0;
for (var i = 0; i < _Wavelengths.Length; i++)
{
ampSum += _Amplitudes[i] * _ActiveSpectrum._ChopScales[i / _ComponentsPerOctave];
var amplitude = _Amplitudes[i];
MaximumReportedVerticalDisplacement += amplitude;
MaximumReportedHorizontalDisplacement += amplitude * _ActiveSpectrum._ChopScales[i / _ComponentsPerOctave];
}
MaximumReportedHorizontalDisplacement *= _ActiveSpectrum._Chop;
// Apply weight or will cause popping due to scale change.
ampSum *= Weight;
MaximumReportedVerticalDisplacement *= Weight;
MaximumReportedHorizontalDisplacement *= Weight;
MaximumReportedHorizontalDisplacement = ampSum * _ActiveSpectrum._Chop;
MaximumReportedVerticalDisplacement = ampSum;
MaximumReportedWavesDisplacement = ampSum;
if (Mode == LodInputMode.Global)
{
water.ReportMaximumDisplacement(ampSum * _ActiveSpectrum._Chop, ampSum, ampSum);
}
MaximumReportedWavesDisplacement = MaximumReportedVerticalDisplacement;
}
private protected override void Initialize()
@@ -577,11 +587,6 @@ namespace WaveHarmonic.Crest
s_Instances.Remove(this);
if (_BufferCascadeParameters != null && _BufferCascadeParameters.IsValid())
{
_BufferCascadeParameters.Dispose();
_BufferCascadeParameters = null;
}
if (_BufferWaveData != null && _BufferWaveData.IsValid())
{
_BufferWaveData.Dispose();
@@ -623,32 +628,23 @@ namespace WaveHarmonic.Crest
if (s_SwellSpectrum != null)
{
Helpers.Destroy(s_SwellSpectrum);
s_SwellSpectrum = null;
}
}
}
partial class ShapeGerstner : ISerializationCallbackReceiver
partial class ShapeGerstner
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 2;
#pragma warning restore 414
private protected override int Version => Mathf.Max(base.Version, 2);
void ISerializationCallbackReceiver.OnAfterDeserialize()
private protected override void OnMigrate()
{
_Version = MigrateV1(_Version);
base.OnMigrate();
if (_Version < 2)
{
_Swell = false;
}
_Version = MigrateV2(_Version);
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
// Empty.
}
}
}

View File

@@ -4,6 +4,7 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Serialization;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
@@ -26,8 +27,11 @@ namespace WaveHarmonic.Crest
[Tooltip("Whether to evaluate the spectrum every frame.\n\nWhen false, the wave spectrum is evaluated once on startup in editor play mode and standalone builds, rather than every frame. This is less flexible, but it reduces the performance cost significantly.")]
[@GenerateAPI]
[FormerlySerializedAs("_SpectrumFixedAtRuntime")]
[@DecoratedField]
[SerializeField]
bool _EvaluateSpectrumAtRunTimeEveryFrame;
private protected bool _EvaluateSpectrumAtRunTimeEveryFrame;
[@Space(10)]
[Tooltip("How much these waves respect the shallow water attenuation.\n\nAttenuation is defined on the Animated Waves. Set to zero to ignore attenuation.")]
[@Range(0f, 1f)]
@@ -35,6 +39,15 @@ namespace WaveHarmonic.Crest
[SerializeField]
float _RespectShallowWaterAttenuation = 1f;
[Tooltip("Whether global waves is applied above or below sea level.\n\nWaves are faded out to avoid hard transitionds. They are fully faded by 1m from sea level.")]
[@Enable(nameof(_Mode), nameof(LodInputMode.Global))]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
bool _SeaLevelOnly = true;
[@Space(10)]
[Tooltip("Whether to use the wind direction on this component rather than the global wind direction.\n\nGlobal wind direction comes from the Water Renderer component.")]
[@GenerateAPI]
[@InlineToggle, SerializeField]
@@ -42,8 +55,8 @@ namespace WaveHarmonic.Crest
[@Label("Wind Direction")]
[Tooltip("Primary wave direction heading (degrees).\n\nThis is the angle from x axis in degrees that the waves are oriented towards. If a spline is being used to place the waves, this angle is relative to the spline.")]
[@Predicated(nameof(_Mode), inverted: false, nameof(LodInputMode.Paint))]
[@Predicated(nameof(_OverrideGlobalWindDirection), hide: true)]
[@Disable(nameof(_Mode), nameof(LodInputMode.Paint))]
[@Show(nameof(_OverrideGlobalWindDirection))]
[@ShowComputedProperty(nameof(WaveDirectionHeadingAngle))]
[@Range(-180, 180)]
[@GenerateAPI(Getter.Custom)]
@@ -57,22 +70,32 @@ namespace WaveHarmonic.Crest
[Tooltip("Wind speed in km/h. Controls wave conditions.")]
[@ShowComputedProperty(nameof(WindSpeedKPH))]
[@Predicated(nameof(_OverrideGlobalWindSpeed), hide: true)]
[@Show(nameof(_OverrideGlobalWindSpeed))]
[@Range(0, 150f, scale: 2f)]
[@GenerateAPI]
[SerializeField]
float _WindSpeed = 20f;
[Header("Generation Settings")]
[@Heading("Generation Settings")]
[Tooltip("Resolution to use for wave generation buffers.\n\nLow resolutions are more efficient but can result in noticeable patterns in the shape.")]
[@Stepped(16, 512, step: 2, power: true)]
[@GenerateAPI]
[@GenerateAPI(Getter.Custom)]
[SerializeField]
private protected int _Resolution = 128;
[@Heading("Level of Detail")]
[Tooltip("Whether the maximum possible vertical displacement is used for the Drop Detail Height Based On Waves calculation.\n\nThis setting is ignored for global waves, as they always contribute. For local waves, only enable for large areas that are treated like global waves (eg a storm).")]
[@Disable(nameof(_Mode), nameof(LodInputMode.Global))]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
bool _IncludeInDropDetailHeightBasedOnWaves;
// Debug
[Tooltip("In Editor, shows the wave generation buffers on screen.")]
@@ -92,6 +115,8 @@ namespace WaveHarmonic.Crest
public static readonly int s_RespectShallowWaterAttenuation = Shader.PropertyToID("_Crest_RespectShallowWaterAttenuation");
public static readonly int s_MaximumAttenuationDepth = Shader.PropertyToID("_Crest_MaximumAttenuationDepth");
public static readonly int s_AxisX = Shader.PropertyToID("_Crest_AxisX");
public static readonly int s_SeaLevelOnly = Shader.PropertyToID("_Crest_SeaLevelOnly");
public static readonly int s_WaveBufferAttenuation = Shader.PropertyToID("_Crest_WaveBufferAttenuation");
}
private protected virtual WaveSpectrum DefaultSpectrum => WindSpectrum;
@@ -136,7 +161,7 @@ namespace WaveHarmonic.Crest
/// <summary>
/// The wind speed in meters per second (MPS).
/// </summary>
/// /// <remarks>
/// <remarks>
/// Wind speed can come from this component or the <see cref="WaterRenderer"/>.
/// </remarks>
public float WindSpeedMPS => WindSpeedKPH / 3.6f;
@@ -150,13 +175,32 @@ namespace WaveHarmonic.Crest
{
base.Attach();
_Reporter ??= new(this);
WaterChunkRenderer.DisplacementReporters.Add(_Reporter);
_DisplacementReporter = _Reporter;
_WaveDisplacementReporter = _Reporter;
}
private protected override void Detach()
{
base.Detach();
WaterChunkRenderer.DisplacementReporters.Remove(_Reporter);
_DisplacementReporter = null;
_WaveDisplacementReporter = null;
}
internal enum WindSpeedSource
{
None, // Wind Speed is at maximum.
ShapeWaves,
WaterRenderer,
}
internal WindSpeedSource GetWindSpeedSource()
{
if (WindSpeedKPH >= WaterRenderer.k_MaximumWindSpeedKPH)
{
return WindSpeedSource.None;
}
return OverrideGlobalWindSpeed ? WindSpeedSource.ShapeWaves : WindSpeedSource.WaterRenderer;
}
internal override void Draw(Lod simulation, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1f, int slice = -1)
@@ -173,30 +217,32 @@ namespace WaveHarmonic.Crest
return;
}
var lodCount = simulation.Slices;
var shape = (AnimatedWavesLod)simulation;
var wrapper = new PropertyWrapperCompute(buffer, s_TransferWavesComputeShader, 0);
if (_FirstCascade < 0 || _LastCascade < 0)
{
return;
}
var lodCount = simulation.Slices;
var lodResolution = simulation.Resolution;
var shape = (AnimatedWavesLod)simulation;
var wrapper = new PropertyWrapperCompute(buffer, s_TransferWavesComputeShader, 0);
// Write to per-octave _WaveBuffers (ie pre-combined). Not the same as _AnimatedWaves.
wrapper.SetTexture(Crest.ShaderIDs.s_Target, target);
// Input weight. Weight for each octave calculated in compute.
wrapper.SetFloat(LodInput.ShaderIDs.s_Weight, Weight);
wrapper.SetInteger(Crest.ShaderIDs.s_Resolution, Resolution);
var water = shape._Water;
for (var lodIdx = lodCount - 1; lodIdx >= lodCount - slice; lodIdx--)
{
_WaveBufferParameters[lodIdx] = new(-1, -2, 0, 0);
_WaveBufferParameters[lodIdx] = new(-1, -2, 0, 1);
var found = false;
var filter = new AnimatedWavesLod.WavelengthFilter(water, lodIdx);
var filter = new AnimatedWavesLod.WavelengthFilter(water, lodIdx, lodResolution);
for (var i = _FirstCascade; i <= _LastCascade; i++)
{
@@ -221,13 +267,17 @@ namespace WaveHarmonic.Crest
}
// Set transitional weights.
_WaveBufferParameters[lodCount - 2].w = 1f - water.ViewerAltitudeLevelAlpha;
if (!shape.PreserveWaveQuality)
{
_WaveBufferParameters[lodCount - 2].w = 1f - water.ViewerAltitudeLevelAlpha;
}
_WaveBufferParameters[lodCount - 1].w = water.ViewerAltitudeLevelAlpha;
SetRenderParameters(water, wrapper);
wrapper.SetFloat(ShaderIDs.s_WaveResolutionMultiplier, shape.WaveResolutionMultiplier);
wrapper.SetFloat(ShaderIDs.s_TransitionalWavelengthThreshold, water.MaximumWavelength(water.LodLevels - 1) * 0.5f);
wrapper.SetFloat(ShaderIDs.s_TransitionalWavelengthThreshold, water.MaximumWavelength(water.LodLevels - 1, simulation.Resolution) * 0.5f);
wrapper.SetVectorArray(ShaderIDs.s_WaveBufferParameters, _WaveBufferParameters);
var isTexture = Mode is LodInputMode.Paint or LodInputMode.Texture;
@@ -236,6 +286,8 @@ namespace WaveHarmonic.Crest
wrapper.SetKeyword(s_KeywordTexture, isTexture && !isAlphaBlend);
wrapper.SetKeyword(s_KeywordTextureBlend, isTexture && isAlphaBlend);
wrapper.SetFloatArray(ShaderIDs.s_WaveBufferAttenuation, _ActiveSpectrum._Attenuation);
if (isTexture)
{
wrapper.SetInteger(Crest.ShaderIDs.s_Blend, (int)_Blend);
@@ -243,6 +295,8 @@ namespace WaveHarmonic.Crest
if (Mode == LodInputMode.Global)
{
wrapper.SetBoolean(ShaderIDs.s_SeaLevelOnly, _SeaLevelOnly);
var threads = shape.Resolution / Lod.k_ThreadGroupSize;
wrapper.Dispatch(threads, threads, slice);
}
@@ -255,6 +309,7 @@ namespace WaveHarmonic.Crest
void GraphicsDraw(Lod simulation, CommandBuffer buffer, RenderTargetIdentifier target, int pass, float weight, int slice)
{
var lod = simulation as AnimatedWavesLod;
var lodResolution = simulation.Resolution;
var wrapper = new PropertyWrapperBuffer(buffer);
SetRenderParameters(simulation._Water, wrapper);
@@ -266,7 +321,7 @@ namespace WaveHarmonic.Crest
_Wavelength = MinWavelength(i) / lod.WaveResolutionMultiplier;
// Do the weight from scratch because this is the real filter.
weight = AnimatedWavesLod.FilterByWavelength(simulation._Water, slice, _Wavelength) * Weight;
weight = AnimatedWavesLod.FilterByWavelength(simulation._Water, slice, _Wavelength, lodResolution) * Weight;
if (weight <= 0f) continue;
var average = _Wavelength * 1.5f * lod.WaveResolutionMultiplier;
@@ -411,40 +466,101 @@ namespace WaveHarmonic.Crest
s_KeywordTextureBlend = WaterResources.Instance.Keywords.AnimatedWavesTransferWavesTextureBlend;
}
bool ReportDisplacement(ref Rect bounds, ref float horizontal, ref float vertical)
bool ReportDisplacement(WaterRenderer water, ref Rect bounds, ref float horizontal, ref float vertical)
{
if (Mode == LodInputMode.Global || !Enabled)
if (!Enabled)
{
return false;
}
if (Mode == LodInputMode.Global)
{
// Global is always additive.
horizontal += MaximumReportedHorizontalDisplacement;
vertical += MaximumReportedVerticalDisplacement;
return true;
}
_Rect = Data.Rect;
if (bounds.Overlaps(_Rect, false))
{
horizontal = MaximumReportedHorizontalDisplacement;
vertical = MaximumReportedVerticalDisplacement;
var nh = horizontal;
var nv = vertical;
switch (Blend)
{
case LodInputBlend.Off:
nh = MaximumReportedHorizontalDisplacement;
nv = MaximumReportedVerticalDisplacement;
break;
case LodInputBlend.Additive:
nh += MaximumReportedHorizontalDisplacement;
nv += MaximumReportedVerticalDisplacement;
break;
case LodInputBlend.Alpha:
case LodInputBlend.AlphaClip:
nh = Mathf.Max(nh, MaximumReportedHorizontalDisplacement);
nv = Mathf.Max(nh, MaximumReportedVerticalDisplacement);
break;
}
horizontal = Mathf.Max(horizontal, nh);
vertical = Mathf.Max(vertical, nv);
return true;
}
return false;
}
float ReportWaveDisplacement(WaterRenderer water, float displacement)
{
if (Mode == LodInputMode.Global)
{
return displacement + MaximumReportedWavesDisplacement;
}
if (!_IncludeInDropDetailHeightBasedOnWaves)
{
return displacement;
}
// TODO: use bounds to transition slowly to avoid pops.
if (_Rect.Contains(water.Position.XZ()))
{
displacement = Blend switch
{
LodInputBlend.Off => MaximumReportedWavesDisplacement,
LodInputBlend.Additive => displacement + MaximumReportedWavesDisplacement,
LodInputBlend.Alpha or LodInputBlend.AlphaClip => Mathf.Max(displacement, MaximumReportedWavesDisplacement),
_ => MaximumReportedWavesDisplacement,
};
}
return displacement;
}
float GetWaveDirectionHeadingAngle()
{
return _OverrideGlobalWindDirection || WaterRenderer.Instance == null ? _WaveDirectionHeadingAngle : WaterRenderer.Instance.WindDirection;
}
internal int GetResolution()
{
return Mathf.Clamp(_Resolution, MinimumResolution, MaximumResolution);
}
}
partial class ShapeWaves
{
Reporter _Reporter;
sealed class Reporter : IReportsDisplacement
sealed class Reporter : IReportsDisplacement, IReportWaveDisplacement
{
readonly ShapeWaves _Input;
public Reporter(ShapeWaves input) => _Input = input;
public bool ReportDisplacement(ref Rect bounds, ref float horizontal, ref float vertical) => _Input.ReportDisplacement(ref bounds, ref horizontal, ref vertical);
public bool ReportDisplacement(WaterRenderer water, ref Rect bounds, ref float horizontal, ref float vertical) => _Input.ReportDisplacement(water, ref bounds, ref horizontal, ref vertical);
public float ReportWaveDisplacement(WaterRenderer water, float displacement) => _Input.ReportWaveDisplacement(water, displacement);
}
}
@@ -465,6 +581,7 @@ namespace WaveHarmonic.Crest
if (s_WindSpectrum != null)
{
Helpers.Destroy(s_WindSpectrum);
s_WindSpectrum = null;
}
}
}
@@ -472,16 +589,20 @@ namespace WaveHarmonic.Crest
partial class ShapeWaves
{
private protected override int Version => Mathf.Max(base.Version, 3);
[HideInInspector, SerializeField]
AlphaSource _AlphaSource;
enum AlphaSource { AlwaysOne, FromZero, FromZeroNormalized }
private protected int MigrateV1(int version)
private protected override void OnMigrate()
{
base.OnMigrate();
// Version 1
// - Merge Alpha Source into Blend.
// - Rename and invert Spectrum Fixed at Run-Time
if (version < 1)
if (_Version < 1)
{
if (_Blend == LodInputBlend.Alpha)
{
@@ -495,24 +616,19 @@ namespace WaveHarmonic.Crest
}
_EvaluateSpectrumAtRunTimeEveryFrame = !_EvaluateSpectrumAtRunTimeEveryFrame;
version = 1;
}
return version;
}
private protected int MigrateV2(int version)
{
// Version 2
// - Global wind direction
if (version < 2)
if (_Version < 2)
{
_OverrideGlobalWindDirection = true;
version = 2;
}
return version;
if (_Version < 3)
{
_SeaLevelOnly = false;
}
}
}

View File

@@ -17,11 +17,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/Waves.html#adding-interaction-forces")]
public sealed partial class SphereWaterInteraction : ManagedBehaviour<WaterRenderer>
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Tooltip("Radius of the sphere that is modelled from which the interaction forces are calculated.")]
[@Range(0.01f, 50f)]
[@GenerateAPI]

View File

@@ -34,7 +34,10 @@ namespace WaveHarmonic.Crest
internal override void RecalculateBounds()
{
throw new System.NotImplementedException();
var transform = _Input.transform;
var scale = transform.lossyScale.XZ();
scale = Helpers.RotateAndEncapsulateXZ(scale, transform.rotation.eulerAngles.y);
_Bounds = new(_Input.transform.position, scale.XNZ());
}
internal override void Draw(Lod lod, Component component, CommandBuffer buffer, RenderTargetIdentifier target, int slices)
@@ -45,7 +48,7 @@ namespace WaveHarmonic.Crest
wrapper.SetVector(ShaderIDs.s_TextureSize, transform.lossyScale.XZ());
wrapper.SetVector(ShaderIDs.s_TexturePosition, transform.position.XZ());
wrapper.SetVector(ShaderIDs.s_TextureRotation, rotation);
wrapper.SetVector(ShaderIDs.s_Resolution, new(_Texture.width, _Texture.height));
wrapper.SetVector(ShaderIDs.s_TextureResolution, new(_Texture.width, _Texture.height));
wrapper.SetVector(ShaderIDs.s_Multiplier, _Multiplier);
wrapper.SetFloat(ShaderIDs.s_FeatherWidth, _Input.FeatherWidth);
wrapper.SetTexture(ShaderIDs.s_Texture, _Texture);

View File

@@ -52,14 +52,14 @@ namespace WaveHarmonic.Crest
WatertightHullMode _Mode = WatertightHullMode.Displacement;
[Tooltip("Inverts the effect to remove clipping (ie add water).")]
[@Predicated(nameof(_Mode), inverted: true, nameof(WatertightHullMode.Clip), hide: true)]
[@Show(nameof(_Mode), nameof(WatertightHullMode.Clip))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _Inverted;
[@Label("Use Clip")]
[Tooltip("Whether to also to clip the surface when using displacement mode.\n\nDisplacement mode can have a leaky hull by allowing chop top push waves across the hull boundaries slightly. Clipping the surface will remove these interior leaks.")]
[@Predicated(nameof(_Mode), inverted: true, nameof(WatertightHullMode.Displacement), hide: true)]
[@Show(nameof(_Mode), nameof(WatertightHullMode.Displacement))]
[@GenerateAPI(Setter.Custom)]
[@DecoratedField, SerializeField]
bool _UseClipWithDisplacement = true;
@@ -248,28 +248,19 @@ namespace WaveHarmonic.Crest
}
}
partial class WatertightHull : ISerializationCallbackReceiver
partial class WatertightHull
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 1;
#pragma warning restore 414
private protected override int Version => Mathf.Max(base.Version, 1);
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
private protected override void OnMigrate()
{
base.OnMigrate();
if (_Version < 1)
{
// Keep clip for existing.
_Mode = WatertightHullMode.Clip;
_Version = 1;
}
}
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
}
}
}

View File

@@ -18,6 +18,7 @@ namespace WaveHarmonic.Crest
internal static readonly Color s_GizmoColor = new(75f / 255f, 0f, 130f / 255f, 0.5f);
internal override string ID => "Level";
internal override string Name => "Water Level";
internal override Color GizmoColor => s_GizmoColor;
private protected override Color ClearColor => Color.black;
private protected override bool NeedToReadWriteTextureData => true;
@@ -41,6 +42,7 @@ namespace WaveHarmonic.Crest
_OverrideResolution = false;
_TextureFormatMode = LodTextureFormatMode.Automatic;
_TextureFormat = GraphicsFormat.R16_SFloat;
_BlurIterations = 4;
}
internal static readonly SortedList<int, ILodInput> s_Inputs = new(Helpers.DuplicateComparison);

View File

@@ -1,19 +1,16 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
using WaveHarmonic.Crest.Utility;
#if !UNITY_2023_2_OR_NEWER
using GraphicsFormatUsage = UnityEngine.Experimental.Rendering.FormatUsage;
#endif
namespace WaveHarmonic.Crest
{
using Inputs = SortedList<int, ILodInput>;
using Inputs = Utility.SortedList<int, ILodInput>;
/// <summary>
/// Texture format preset.
@@ -42,7 +39,7 @@ namespace WaveHarmonic.Crest
/// Base class for data/behaviours created on each LOD.
/// </summary>
[System.Serializable]
public abstract partial class Lod
public abstract partial class Lod : Versioned
{
[Tooltip("Whether the simulation is enabled.")]
[@GenerateAPI(Getter.Custom, Setter.Custom)]
@@ -50,14 +47,13 @@ namespace WaveHarmonic.Crest
internal bool _Enabled;
[Tooltip("Whether to override the resolution.\n\nIf not enabled, then the simulation will use the resolution defined on the Water Renderer.")]
[@Predicated(typeof(AnimatedWavesLod), inverted: true, hide: true)]
[@Hide(typeof(AnimatedWavesLod))]
[@GenerateAPI(Setter.Dirty)]
[@InlineToggle(fix: true), SerializeField]
[@InlineToggle, SerializeField]
internal bool _OverrideResolution = true;
[Tooltip("The resolution of the simulation data.\n\nSet higher for sharper results at the cost of higher memory usage.")]
[@Predicated(typeof(AnimatedWavesLod), inverted: true, hide: true)]
[@Predicated(nameof(_OverrideResolution), hide: true)]
[@Show(nameof(_OverrideResolution))]
[@ShowComputedProperty(nameof(Resolution))]
[@Delayed]
[@GenerateAPI(Getter.Custom, Setter.Dirty)]
@@ -72,25 +68,45 @@ namespace WaveHarmonic.Crest
[Tooltip("The render texture format used for this simulation data.\n\nIt will be overriden if the format is incompatible with the platform.")]
[@ShowComputedProperty(nameof(RequestedTextureFormat))]
[@Predicated(nameof(_TextureFormatMode), inverted: true, nameof(LodTextureFormatMode.Manual), hide: true)]
[@Show(nameof(_TextureFormatMode), nameof(LodTextureFormatMode.Manual))]
[@GenerateAPI(Setter.Dirty)]
[@DecoratedField, SerializeField]
internal GraphicsFormat _TextureFormat;
[@Space(10)]
[Tooltip("Blurs the output.\n\nEnable if blurring is desired or intolerable artifacts are present.\nThe blur is optimized to only run on inner LODs and at lower scales.")]
[@Hide(typeof(AnimatedWavesLod))]
[@Hide(typeof(DynamicWavesLod))]
[@GenerateAPI(Setter.Dirty)]
[@DecoratedField]
[SerializeField]
private protected bool _Blur;
[Tooltip("Number of blur iterations.\n\nBlur iterations are optimized to only run maximum iterations on the inner LODs.")]
[@Hide(typeof(AnimatedWavesLod))]
[@Hide(typeof(DynamicWavesLod))]
[@Enable(nameof(_Blur))]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
private protected int _BlurIterations = 1;
// NOTE: This MUST match the value in Constants.hlsl, as it
// determines the size of the texture arrays in the shaders.
internal const int k_MaximumSlices = 15;
// NOTE: these MUST match the values in Constants.hlsl
// 64 recommended as a good common minimum: https://www.reddit.com/r/GraphicsProgramming/comments/aeyfkh/for_compute_shaders_is_there_an_ideal_numthreads/
internal const int k_ThreadGroupSize = 8;
internal const int k_ThreadGroupSizeX = k_ThreadGroupSize;
internal const int k_ThreadGroupSizeY = k_ThreadGroupSize;
internal const string k_BlurField = nameof(_Blur);
internal const string k_TextureFormatModeField = nameof(_TextureFormatMode);
internal static class ShaderIDs
{
public static readonly int s_LodIndex = Shader.PropertyToID("_Crest_LodIndex");
public static readonly int s_LodChange = Shader.PropertyToID("_Crest_LodChange");
public static readonly int s_TemporaryBlurLodTexture = Shader.PropertyToID("_Crest_TemporaryBlurLodTexture");
}
// Used for creating shader property names etc.
@@ -136,17 +152,10 @@ namespace WaveHarmonic.Crest
}
}
static readonly GraphicsFormatUsage s_GraphicsFormatUsage =
// Ensures a non compressed format is returned.
GraphicsFormatUsage.LoadStore |
// All these textures are sampled at some point.
GraphicsFormatUsage.Sample |
// Always use linear filtering.
GraphicsFormatUsage.Linear;
private protected bool Persistent => BufferCount > 1;
internal virtual bool SkipEndOfFrame => false;
private protected BufferedData<RenderTexture> _Targets;
internal RenderTexture DataTexture => _Targets.Current;
internal RenderTexture GetDataTexture(int frameDelta) => _Targets.Previous(frameDelta);
internal RenderTexture DataTexture { get; private protected set; }
private protected Matrix4x4[] _ViewMatrices = new Matrix4x4[k_MaximumSlices];
private protected Cascade[] _Cascades = new Cascade[k_MaximumSlices];
@@ -161,7 +170,7 @@ namespace WaveHarmonic.Crest
internal WaterRenderer _Water;
internal WaterRenderer Water => _Water;
private protected int _TargetsToClear;
private protected bool _TargetsToClear;
private protected readonly int _TextureShaderID;
private protected readonly int _TextureSourceShaderID;
@@ -184,8 +193,10 @@ namespace WaveHarmonic.Crest
_TextureName = $"_Crest_{ID}Lod";
}
private protected RenderTexture CreateLodDataTextures()
private protected RenderTexture CreateLodDataTextures(string postfix = null)
{
postfix ??= string.Empty;
RenderTexture result = new(Resolution, Resolution, 0, CompatibleTextureFormat)
{
wrapMode = TextureWrapMode.Clamp,
@@ -193,7 +204,7 @@ namespace WaveHarmonic.Crest
filterMode = FilterMode.Bilinear,
anisoLevel = 0,
useMipMap = false,
name = _TextureName,
name = _TextureName + postfix,
dimension = TextureDimension.Tex2DArray,
volumeDepth = Slices,
enableRandomWrite = NeedToReadWriteTextureData
@@ -202,7 +213,7 @@ namespace WaveHarmonic.Crest
return result;
}
private protected void FlipBuffers()
private protected void FlipBuffers(CommandBuffer commands)
{
if (_ReAllocateTexture)
{
@@ -214,11 +225,10 @@ namespace WaveHarmonic.Crest
if (!UnityEditor.EditorApplication.isPaused || Time.deltaTime > 0)
#endif
{
_Targets.Flip();
_SamplingParameters.Flip();
}
UpdateSamplingParameters();
UpdateSamplingParameters(commands);
}
private protected void Clear(RenderTexture target)
@@ -226,29 +236,36 @@ namespace WaveHarmonic.Crest
Helpers.ClearRenderTexture(target, ClearColor, depth: false);
}
/// <summary>
/// Clears persistent LOD data. Some simulations have persistent data which can linger for a little while after
/// being disabled. This will manually clear that data.
/// </summary>
internal virtual void ClearLodData()
{
// Empty.
}
private protected virtual bool AlwaysClear => false;
// Only works with input-only data (ie no simulation steps).
internal virtual void BuildCommandBuffer(WaterRenderer water, CommandBuffer buffer)
{
FlipBuffers();
FlipBuffers(buffer);
buffer.BeginSample(ID);
if (_TargetsToClear > 0 || AlwaysClear)
if (_TargetsToClear || AlwaysClear)
{
// IMPORTANT:
// We need both clears otherwise problems on PS5.
// With only native clear, flow would clear to non black (ie constant flow).
// With only custom clear, absorption/scattering inputs were not clearing.
CoreUtils.SetRenderTarget(buffer, DataTexture, ClearFlag.Color, ClearColor);
_TargetsToClear--;
// Custom clear because clear not working.
if (Helpers.RequiresCustomClear && WaterResources.Instance.Compute._Clear != null)
{
var compute = WaterResources.Instance._ComputeLibrary._ClearCompute;
var wrapper = new PropertyWrapperCompute(buffer, compute._Shader, compute._KernelClearTarget);
compute.SetVariantForFormat(wrapper, CompatibleTextureFormat);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, DataTexture);
wrapper.SetVector(Crest.ShaderIDs.s_ClearMask, Color.white);
wrapper.SetVector(Crest.ShaderIDs.s_ClearColor, ClearColor);
wrapper.Dispatch(Resolution / k_ThreadGroupSizeX, Resolution / k_ThreadGroupSizeY, Slices);
}
_TargetsToClear = false;
}
if (Inputs.Count > 0)
@@ -256,9 +273,11 @@ namespace WaveHarmonic.Crest
SubmitDraws(buffer, Inputs, DataTexture);
// Ensure all targets clear when there are no inputs.
_TargetsToClear = _Targets.Size;
_TargetsToClear = true;
}
TryBlur(buffer);
if (RequiresClearBorder)
{
ClearBorder(buffer);
@@ -354,14 +373,18 @@ namespace WaveHarmonic.Crest
{
var size = Resolution / 8;
var wrapper = new PropertyWrapperCompute(buffer, WaterResources.Instance.Compute._Clear, 1);
var compute = WaterResources.Instance._ComputeLibrary._ClearCompute;
var wrapper = new PropertyWrapperCompute(buffer, compute._Shader, compute._KernelClearTargetBoundaryX);
// Only need to be done once.
compute.SetVariantForFormat(wrapper, DataTexture.graphicsFormat);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, DataTexture);
wrapper.SetVector(Crest.ShaderIDs.s_ClearColor, ClearColor);
wrapper.SetInteger(Crest.ShaderIDs.s_Resolution, Resolution);
wrapper.SetInteger(Crest.ShaderIDs.s_TargetSlice, Slices - 1);
wrapper.Dispatch(size, 1, 1);
wrapper = new PropertyWrapperCompute(buffer, WaterResources.Instance.Compute._Clear, 2);
wrapper = new(buffer, compute._Shader, compute._KernelClearTargetBoundaryY);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, DataTexture);
wrapper.SetVector(Crest.ShaderIDs.s_ClearColor, ClearColor);
wrapper.SetInteger(Crest.ShaderIDs.s_Resolution, Resolution);
@@ -369,7 +392,7 @@ namespace WaveHarmonic.Crest
wrapper.Dispatch(1, size, 1);
}
void UpdateSamplingParameters(bool initialize = false)
void UpdateSamplingParameters(CommandBuffer commands, bool initialize = false)
{
var position = _Water.Position;
var resolution = _Enabled ? Resolution : TextureArrayHelpers.k_SmallTextureSize;
@@ -380,7 +403,7 @@ namespace WaveHarmonic.Crest
for (var slice = 0; slice < levels; slice++)
{
// Find snap period.
var texel = 2f * 2f * _Water._CascadeData.Current[slice].x / resolution;
var texel = 2f * 2f * _Water.CascadeData.Current[slice].x / resolution;
// Snap so that shape texels are stationary.
var snapped = position - new Vector3(Mathf.Repeat(position.x, texel), 0, Mathf.Repeat(position.z, texel));
@@ -392,11 +415,20 @@ namespace WaveHarmonic.Crest
_ViewMatrices[slice] = WaterRenderer.CalculateViewMatrixFromSnappedPositionRHS(snapped);
}
if (initialize)
if (!initialize)
{
Shader.SetGlobalVector(_SamplingParametersShaderID, new(levels, resolution, 1f / resolution, 0));
commands.SetGlobalVector(_SamplingParametersShaderID, new(levels, resolution, 1f / resolution, 0));
commands.SetGlobalVectorArray(_SamplingParametersCascadeShaderID, parameters);
if (BufferCount > 1)
{
commands.SetGlobalVectorArray(_SamplingParametersCascadeSourceShaderID, _SamplingParameters.Previous(1));
}
return;
}
Shader.SetGlobalVector(_SamplingParametersShaderID, new(levels, resolution, 1f / resolution, 0));
Shader.SetGlobalVectorArray(_SamplingParametersCascadeShaderID, parameters);
if (BufferCount > 1)
@@ -468,7 +500,7 @@ namespace WaveHarmonic.Crest
// 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 minimumWavelength = _Water.MaximumWavelength(index) / 2f;
var minimumWavelength = _Water.MaximumWavelength(index, Resolution) / 2f;
if (minimumWavelength < minimumSpatialLength / 2f && index < count - 1)
{
continue;
@@ -480,6 +512,48 @@ namespace WaveHarmonic.Crest
return -1;
}
// Blurs the output if enabled.
private protected void TryBlur(CommandBuffer commands)
{
if (!_Blur || _Water.Scale >= 32)
{
return;
}
var rt = DataTexture;
var compute = WaterResources.Instance._ComputeLibrary._BlurCompute;
var horizontal = new PropertyWrapperCompute(commands, compute._Shader, compute._KernelHorizontal);
var vertical = new PropertyWrapperCompute(commands, compute._Shader, compute._KernelVertical);
var temporary = ShaderIDs.s_TemporaryBlurLodTexture;
commands.GetTemporaryRT(temporary, rt.descriptor);
// Applies to both.
compute.SetVariantForFormat(horizontal, rt.graphicsFormat);
horizontal.SetInteger(Crest.ShaderIDs.s_Resolution, rt.width);
horizontal.SetTexture(Crest.ShaderIDs.s_Source, rt);
horizontal.SetTexture(Crest.ShaderIDs.s_Target, temporary);
vertical.SetTexture(Crest.ShaderIDs.s_Source, temporary);
vertical.SetTexture(Crest.ShaderIDs.s_Target, rt);
var x = rt.width / 8;
var y = rt.height / 8;
// Skip outer LODs.
var z = Mathf.Min(rt.volumeDepth, 4);
for (var i = 0; i < _BlurIterations; i++)
{
// Limit number of iterations for outer LODs.
horizontal.Dispatch(x, y, Mathf.Max(z - i, 1));
vertical.Dispatch(x, y, Mathf.Max(z - i, 1));
}
commands.ReleaseTemporaryRT(temporary);
}
/// <summary>
/// Bind data needed to load or compute from this simulation.
/// </summary>
@@ -500,7 +574,7 @@ namespace WaveHarmonic.Crest
// Validate textures.
{
// Find a compatible texture format.
CompatibleTextureFormat = Helpers.GetCompatibleTextureFormat(RequestedTextureFormat, s_GraphicsFormatUsage, Name, NeedToReadWriteTextureData);
CompatibleTextureFormat = Helpers.GetCompatibleTextureFormat(RequestedTextureFormat, Helpers.s_DataGraphicsFormatUsage, Name, NeedToReadWriteTextureData);
if (CompatibleTextureFormat == GraphicsFormat.None)
{
@@ -523,20 +597,15 @@ namespace WaveHarmonic.Crest
// Bind/unbind data texture for all shaders.
Shader.SetGlobalTexture(_TextureShaderID, enable && Enabled ? DataTexture : NullTexture);
if (BufferCount > 1)
{
Shader.SetGlobalTexture(_TextureSourceShaderID, enable && Enabled ? GetDataTexture(1) : NullTexture);
}
if (_SamplingParameters == null || _SamplingParameters.Size != BufferCount)
{
_SamplingParameters = new(BufferCount, () => new Vector4[k_MaximumSlices]);
}
// For safety. Disable to see if we are sampling outside of LOD chain.
_SamplingParameters.RunLambda(x => System.Array.Fill(x, Vector4.zero));
_SamplingParameters.RunLambda(x => System.Array.Fill(x, new(0, 0, 1, 0)));
UpdateSamplingParameters(initialize: true);
UpdateSamplingParameters(null, initialize: true);
}
internal virtual void Enable()
@@ -553,11 +622,10 @@ namespace WaveHarmonic.Crest
internal virtual void Destroy()
{
// Release resources and destroy object to avoid reference leak.
_Targets?.RunLambda(x =>
{
if (x != null) x.Release();
Helpers.Destroy(x);
});
if (DataTexture != null) DataTexture.Release();
Helpers.Destroy(DataTexture);
_AdditionalCameraData.Clear();
}
internal virtual void AfterExecute()
@@ -567,10 +635,11 @@ namespace WaveHarmonic.Crest
private protected virtual void Allocate()
{
_Targets = new(BufferCount, CreateLodDataTextures);
_Targets.RunLambda(Clear);
// Use per-camera data.
DataTexture = CreateLodDataTextures();
Clear(DataTexture);
// Bind globally once here on init, which will bind to all graphics shaders (not compute)
// Bind globally once here on init, which will bind to all shaders.
Shader.SetGlobalTexture(_TextureShaderID, DataTexture);
_ReAllocateTexture = false;
@@ -600,23 +669,21 @@ namespace WaveHarmonic.Crest
int GetResolution() => _OverrideResolution || Water == null ? _Resolution : Water.LodResolution;
private protected void ReAllocate()
private protected virtual void ReAllocate()
{
if (!Enabled) return;
CompatibleTextureFormat = Helpers.GetCompatibleTextureFormat(RequestedTextureFormat, s_GraphicsFormatUsage, Name, NeedToReadWriteTextureData);
var descriptor = _Targets.Current.descriptor;
CompatibleTextureFormat = Helpers.GetCompatibleTextureFormat(RequestedTextureFormat, Helpers.s_DataGraphicsFormatUsage, Name, NeedToReadWriteTextureData);
var descriptor = DataTexture.descriptor;
descriptor.height = descriptor.width = Resolution;
descriptor.graphicsFormat = CompatibleTextureFormat;
_Targets.RunLambda(texture =>
{
texture.Release();
texture.descriptor = descriptor;
texture.Create();
});
descriptor.enableRandomWrite = NeedToReadWriteTextureData;
DataTexture.Release();
DataTexture.descriptor = descriptor;
DataTexture.Create();
_ReAllocateTexture = false;
UpdateSamplingParameters(initialize: true);
UpdateSamplingParameters(null, initialize: true);
}
#if UNITY_EDITOR
@@ -628,6 +695,7 @@ namespace WaveHarmonic.Crest
case nameof(_Enabled):
SetEnabled((bool)previousValue, _Enabled);
break;
case nameof(_Blur):
case nameof(_Resolution):
case nameof(_OverrideResolution):
case nameof(_TextureFormat):
@@ -639,6 +707,45 @@ namespace WaveHarmonic.Crest
#endif
}
partial class Lod
{
readonly Dictionary<Camera, BufferedData<Vector4[]>> _AdditionalCameraData = new();
internal virtual void LoadCameraData(Camera camera)
{
Queryable?.Initialize(_Water);
// For non-persistent sims, we do not need to store per camera data.
if (!Persistent)
{
return;
}
if (!_AdditionalCameraData.ContainsKey(camera))
{
_SamplingParameters = new(BufferCount, () => new Vector4[k_MaximumSlices]);
_AdditionalCameraData.Add(camera, _SamplingParameters);
}
else
{
_SamplingParameters = _AdditionalCameraData[camera];
}
}
internal virtual void StoreCameraData(Camera camera)
{
}
internal virtual void RemoveCameraData(Camera camera)
{
if (_AdditionalCameraData.ContainsKey(camera))
{
_AdditionalCameraData.Remove(camera);
}
}
}
// API
partial class Lod
{
@@ -657,29 +764,78 @@ namespace WaveHarmonic.Crest
}
}
interface IQueryableLod<out T> where T : IQueryProvider
{
string Name { get; }
bool Enabled { get; }
WaterRenderer Water { get; }
int MaximumQueryCount { get; }
float Texel { get; }
LodQuerySource QuerySource { get; }
}
/// <summary>
/// The source of collisions (ie water shape).
/// </summary>
[@GenerateDoc]
public enum LodQuerySource
{
/// <inheritdoc cref="Generated.LodQuerySource.None"/>
[Tooltip("No query source.")]
None,
/// <inheritdoc cref="Generated.LodQuerySource.GPU"/>
[Tooltip("Uses AsyncGPUReadback to retrieve data from GPU to CPU.\n\nThis is the most optimal approach.")]
GPU,
/// <inheritdoc cref="Generated.LodQuerySource.CPU"/>
[Tooltip("Computes data entirely on the CPU.")]
CPU,
}
/// <summary>
/// Base type for simulations with a provider.
/// </summary>
/// <typeparam name="T">The query provider.</typeparam>
[System.Serializable]
public abstract class Lod<T> : Lod where T : IQueryProvider
public abstract partial class Lod<T> : Lod, IQueryableLod<T> where T : IQueryProvider
{
[@Space(10)]
[Tooltip("Where to obtain water data on CPU for physics / gameplay.")]
[@GenerateAPI(Setter.Internal)]
[@Filtered]
[SerializeField]
private protected LodQuerySource _QuerySource = LodQuerySource.GPU;
[Tooltip("Maximum number of queries that can be performed when using GPU queries.")]
[@Show(nameof(_QuerySource), nameof(LodQuerySource.GPU))]
[@GenerateAPI(Setter.None)]
[@DecoratedField]
[SerializeField]
private protected int _MaximumQueryCount = QueryBase.k_DefaultMaximumQueryCount;
/// <summary>
/// Provides data from the GPU to CPU.
/// </summary>
public T Provider { get; set; }
private protected abstract T CreateProvider(bool enable);
internal override void SetGlobals(bool enable)
WaterRenderer IQueryableLod<T>.Water => Water;
string IQueryableLod<T>.Name => Name;
float IQueryableLod<T>.Texel => _Cascades[0]._Texel;
private protected abstract T CreateProvider(bool onEnable);
internal override void SetGlobals(bool onEnable)
{
base.SetGlobals(enable);
base.SetGlobals(onEnable);
// We should always have a provider (null provider if disabled).
InitializeProvider(enable);
InitializeProvider(onEnable);
}
private protected void InitializeProvider(bool enable)
private protected void InitializeProvider(bool onEnable)
{
Provider = CreateProvider(enable);
Provider = CreateProvider(onEnable);
// None providers are not IQueryable.
Queryable = Provider as IQueryable;
}
@@ -689,4 +845,30 @@ namespace WaveHarmonic.Crest
Queryable?.SendReadBack(_Water);
}
}
#if UNITY_EDITOR
partial class Lod<T>
{
private protected void ResetQueryChange()
{
if (_Water == null || !_Water.isActiveAndEnabled || !_Enabled) return;
Queryable?.CleanUp();
InitializeProvider(true);
}
[@OnChange]
private protected override void OnChange(string path, object previous)
{
base.OnChange(path, previous);
switch (path)
{
case nameof(_QuerySource):
case nameof(_MaximumQueryCount):
ResetQueryChange();
break;
}
}
}
#endif
}

View File

@@ -12,8 +12,10 @@ namespace WaveHarmonic.Crest
[System.Serializable]
public abstract partial class PersistentLod : Lod
{
[@Space(10)]
[Tooltip("Frequency to run the simulation, in updates per second.\n\nLower frequencies are more efficient but may lead to visible jitter or slowness.")]
[@Range(15, 200, order = -1000)]
[@Range(15, 200)]
[@GenerateAPI]
[SerializeField]
private protected int _SimulationFrequency = 60;
@@ -21,27 +23,27 @@ namespace WaveHarmonic.Crest
static new class ShaderIDs
{
public static readonly int s_SimDeltaTime = Shader.PropertyToID("_Crest_SimDeltaTime");
public static readonly int s_SimDeltaTimePrev = Shader.PropertyToID("_Crest_SimDeltaTimePrev");
public static readonly int s_TemporaryPersistentTarget = Shader.PropertyToID("_Crest_TemporaryPersistentTarget");
}
private protected override bool NeedToReadWriteTextureData => true;
internal override int BufferCount => 2;
float _PreviousSubstepDeltaTime = 1f / 60f;
// Is this the first step since being enabled?
private protected bool _NeedsPrewarmingThisStep = true;
// This is how far the simulation time is behind Unity's time.
private protected float _TimeToSimulate = 0f;
// Pristine historic data. Needed if using blur or multiple viewpoints. For the
// latter, we cannot optimize the upstream data texture away due to camera filtering.
private protected RenderTexture _PersistentDataTexture;
internal int LastUpdateSubstepCount { get; private set; }
private protected virtual int Kernel => 0;
private protected virtual bool SkipFlipBuffers => false;
private protected abstract ComputeShader SimulationShader { get; }
private protected abstract void GetSubstepData(float timeToSimulate, out int substeps, out float delta);
internal override void Initialize()
{
@@ -56,28 +58,51 @@ namespace WaveHarmonic.Crest
_NeedsPrewarmingThisStep = true;
}
internal override void ClearLodData()
private protected override void Allocate()
{
base.ClearLodData();
_Targets.RunLambda(x => Clear(x));
base.Allocate();
// Use per-camera data.
if (!_Water.IsSingleViewpointMode)
{
return;
}
if (Blur)
{
_PersistentDataTexture = CreateLodDataTextures("_Source");
}
}
internal override void Destroy()
{
base.Destroy();
if (_PersistentDataTexture != null) _PersistentDataTexture.Release();
Helpers.Destroy(_PersistentDataTexture);
foreach (var data in _AdditionalCameraData.Values)
{
var x = data._PersistentData;
if (x != null) x.Release();
Helpers.Destroy(x);
}
_AdditionalCameraData.Clear();
}
internal override void BuildCommandBuffer(WaterRenderer water, CommandBuffer buffer)
{
buffer.BeginSample(ID);
if (!SkipFlipBuffers)
{
FlipBuffers();
}
var slices = water.LodLevels;
FlipBuffers(buffer);
// How far are we behind.
_TimeToSimulate += water.DeltaTime;
// Do a set of substeps to catch up.
GetSubstepData(_TimeToSimulate, out var substeps, out var delta);
var substeps = Mathf.FloorToInt(_TimeToSimulate * _SimulationFrequency);
var delta = substeps > 0 ? (1f / _SimulationFrequency) : 0f;
LastUpdateSubstepCount = substeps;
@@ -91,7 +116,10 @@ namespace WaveHarmonic.Crest
delta = 0f;
}
if (substeps > 1)
// Use temporary if only storing one texture upstream which has the source.
var useTemporary = _Water.IsSingleViewpointMode && !Blur;
if (useTemporary)
{
// No need to clear, as the update dispatch overwrites every pixel, but finding
// artifacts if not and there is a renderer input. Happens for foam and dynamic
@@ -100,9 +128,9 @@ namespace WaveHarmonic.Crest
CoreUtils.SetRenderTarget(buffer, ShaderIDs.s_TemporaryPersistentTarget, ClearFlag.Color, ClearColor);
}
var target = new RenderTargetIdentifier(DataTexture);
var source = new RenderTargetIdentifier(ShaderIDs.s_TemporaryPersistentTarget);
var current = target;
var final = new RenderTargetIdentifier(DataTexture);
var target = useTemporary ? new RenderTargetIdentifier(ShaderIDs.s_TemporaryPersistentTarget) : final;
var source = useTemporary ? final : new RenderTargetIdentifier(_PersistentDataTexture);
var wrapper = new PropertyWrapperCompute(buffer, SimulationShader, Kernel);
@@ -130,10 +158,9 @@ namespace WaveHarmonic.Crest
// Both simulation update and input draws need delta time.
buffer.SetGlobalFloat(ShaderIDs.s_SimDeltaTime, delta);
buffer.SetGlobalFloat(ShaderIDs.s_SimDeltaTimePrev, _PreviousSubstepDeltaTime);
wrapper.SetTexture(Crest.ShaderIDs.s_Source, source);
wrapper.SetTexture(Crest.ShaderIDs.s_Target, target);
wrapper.SetTexture(_TextureSourceShaderID, isFirstStep ? _Targets.Previous(1) : source);
// Compute which LOD data we are sampling source data from. if a scale change has
// happened this can be any LOD up or down the chain. This is only valid on the
@@ -141,7 +168,7 @@ namespace WaveHarmonic.Crest
// places.
wrapper.SetFloat(Lod.ShaderIDs.s_LodChange, isFirstStep ? _Water.ScaleDifferencePower2 : 0);
wrapper.SetVectorArray(WaterRenderer.ShaderIDs.s_CascadeDataSource, _Water._CascadeData.Previous(frame));
wrapper.SetVectorArray(WaterRenderer.ShaderIDs.s_CascadeDataSource, _Water.CascadeData.Previous(frame));
wrapper.SetVectorArray(_SamplingParametersCascadeSourceShaderID, _SamplingParameters.Previous(frame));
SetAdditionalSimulationParameters(wrapper);
@@ -157,22 +184,25 @@ namespace WaveHarmonic.Crest
// The very first step since being enabled.
_NeedsPrewarmingThisStep = false;
_PreviousSubstepDeltaTime = delta;
}
// Swap textures if needed.
if (target != current)
if (target != final)
{
buffer.CopyTexture(target, DataTexture);
buffer.CopyTexture(target, final);
}
// Preserve non-blurred historic data.
else if (!useTemporary)
{
buffer.CopyTexture(target, source);
}
if (substeps > 1)
if (useTemporary)
{
buffer.ReleaseTemporaryRT(ShaderIDs.s_TemporaryPersistentTarget);
}
// Set the target texture as to make sure we catch the 'pong' each frame.
Shader.SetGlobalTexture(_TextureShaderID, DataTexture);
TryBlur(buffer);
buffer.EndSample(ID);
}
@@ -184,5 +214,107 @@ namespace WaveHarmonic.Crest
{
}
private protected override void ReAllocate()
{
base.ReAllocate();
if (!Enabled)
{
return;
}
var descriptor = DataTexture.descriptor;
if (_Water.IsMultipleViewpointMode)
{
foreach (var (key, data) in _AdditionalCameraData)
{
var texture = data._PersistentData;
texture.Release();
texture.descriptor = descriptor;
texture.Create();
}
return;
}
if (_PersistentDataTexture != null)
{
_PersistentDataTexture.Release();
if (Blur)
{
_PersistentDataTexture.descriptor = descriptor;
_PersistentDataTexture.Create();
}
else
{
Helpers.Destroy(_PersistentDataTexture);
}
}
else if (Blur)
{
_PersistentDataTexture = CreateLodDataTextures("_Source");
}
}
}
partial class PersistentLod
{
sealed class AdditionalCameraData
{
public RenderTexture _PersistentData;
public float _TimeToSimulate;
}
readonly System.Collections.Generic.Dictionary<Camera, AdditionalCameraData> _AdditionalCameraData = new();
internal override void LoadCameraData(Camera camera)
{
base.LoadCameraData(camera);
AdditionalCameraData data;
if (!_AdditionalCameraData.ContainsKey(camera))
{
data = new()
{
_PersistentData = CreateLodDataTextures("_Source"),
_TimeToSimulate = _TimeToSimulate,
};
_AdditionalCameraData.Add(camera, data);
}
else
{
data = _AdditionalCameraData[camera];
}
_PersistentDataTexture = data._PersistentData;
_TimeToSimulate = data._TimeToSimulate;
}
internal override void StoreCameraData(Camera camera)
{
base.StoreCameraData(camera);
if (_AdditionalCameraData.ContainsKey(camera))
{
_AdditionalCameraData[camera]._TimeToSimulate = _TimeToSimulate;
}
}
internal override void RemoveCameraData(Camera camera)
{
base.RemoveCameraData(camera);
if (_AdditionalCameraData.ContainsKey(camera))
{
var rt = _AdditionalCameraData[camera]._PersistentData;
if (rt != null) rt.Release();
Helpers.Destroy(rt);
_AdditionalCameraData.Remove(camera);
}
}
}
}

View File

@@ -14,11 +14,6 @@ namespace WaveHarmonic.Crest
[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;

View File

@@ -38,12 +38,17 @@ namespace WaveHarmonic.Crest
internal static NoneProvider None { get; } = new();
internal static ICollisionProvider Create(WaterRenderer water)
{
return water.IsMultipleViewpointMode ? new CollisionQueryPerCamera(water) : new CollisionQueryWithPasses(water);
}
/// <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)
public int Query(int _0, float _1, Vector3[] _2, Vector3[] result0, Vector3[] result1, Vector3[] result2, CollisionLayer _3 = CollisionLayer.Everything, Vector3? _4 = null)
{
if (result0 != null) System.Array.Fill(result0, Vector3.zero);
if (result1 != null) System.Array.Fill(result1, Vector3.up);
@@ -51,7 +56,7 @@ namespace WaveHarmonic.Crest
return 0;
}
public int Query(int _0, float _1, Vector3[] _2, float[] result0, Vector3[] result1, Vector3[] result2, CollisionLayer _3 = CollisionLayer.Everything)
public int Query(int _0, float _1, Vector3[] _2, float[] result0, Vector3[] result1, Vector3[] result2, CollisionLayer _3 = CollisionLayer.Everything, Vector3? _4 = null)
{
if (result0 != null) System.Array.Fill(result0, WaterRenderer.Instance.SeaLevel);
if (result1 != null) System.Array.Fill(result1, Vector3.up);
@@ -66,12 +71,11 @@ namespace WaveHarmonic.Crest
/// <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);
/// <inheritdoc cref="IQueryProvider.Query(int, float, Vector3[], int, Vector3?)" />
int Query(int hash, float minimumLength, Vector3[] points, float[] heights, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null);
/// <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);
/// <inheritdoc cref="Query(int, float, Vector3[], float[], Vector3[], Vector3[], CollisionLayer, Vector3?)" />
int Query(int hash, float minimumLength, Vector3[] points, Vector3[] displacements, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null);
}
}

View File

@@ -2,6 +2,7 @@
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
@@ -10,10 +11,10 @@ namespace WaveHarmonic.Crest
/// </summary>
sealed class CollisionQuery : QueryBase, ICollisionProvider
{
public CollisionQuery(WaterRenderer water) : base(water) { }
public CollisionQuery(WaterRenderer water) : base(water.AnimatedWavesLod) { }
protected override int Kernel => 0;
public int Query(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] resultDisplacements, Vector3[] resultNormals, Vector3[] resultVelocities, CollisionLayer layer = CollisionLayer.Everything)
public int Query(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] resultDisplacements, Vector3[] resultNormals, Vector3[] resultVelocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null)
{
var result = (int)QueryStatus.OK;
@@ -35,7 +36,7 @@ namespace WaveHarmonic.Crest
return result;
}
public int Query(int ownerHash, float minimumSpatialLength, Vector3[] queryPoints, float[] resultHeights, Vector3[] resultNormals, Vector3[] resultVelocities, CollisionLayer layer = CollisionLayer.Everything)
public int Query(int ownerHash, float minimumSpatialLength, Vector3[] queryPoints, float[] resultHeights, Vector3[] resultNormals, Vector3[] resultVelocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null)
{
var result = (int)QueryStatus.OK;
@@ -58,6 +59,102 @@ namespace WaveHarmonic.Crest
}
}
sealed class CollisionQueryPerCamera : QueryPerCamera<CollisionQueryWithPasses>, ICollisionProvider
{
public CollisionQueryPerCamera() : base(WaterRenderer.Instance) { }
public CollisionQueryPerCamera(WaterRenderer water) : base(water) { }
public int Query(int hash, float minimumLength, Vector3[] points, float[] heights, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null)
{
if (_Water.IsSeparateViewpointCameraLoop)
{
return _Providers[_Water.CurrentCamera].Query(hash, minimumLength, points, heights, normals, velocities, layer, center);
}
var lastStatus = -1;
var lastDistance = Mathf.Infinity;
var newCenter = FindCenter(points, center);
foreach (var provider in _Providers)
{
var camera = provider.Key;
if (!_Water.ShouldExecuteQueries(camera))
{
continue;
}
var distance = (newCenter - camera.transform.position.XZ()).sqrMagnitude;
if (lastStatus == (int)QueryBase.QueryStatus.OK && lastDistance < distance)
{
continue;
}
var status = provider.Value.Query(hash, minimumLength, points, heights, normals, velocities, layer, center);
if (lastStatus < 0 || status == (int)QueryBase.QueryStatus.OK)
{
lastStatus = status;
lastDistance = distance;
}
}
return lastStatus;
}
public int Query(int hash, float minimumLength, Vector3[] points, Vector3[] displacements, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null)
{
if (_Water.IsSeparateViewpointCameraLoop)
{
return _Providers[_Water.CurrentCamera].Query(hash, minimumLength, points, displacements, normals, velocities, layer, center);
}
var lastStatus = -1;
var lastDistance = Mathf.Infinity;
var newCenter = FindCenter(points, center);
foreach (var provider in _Providers)
{
var camera = provider.Key;
if (!_Water.ShouldExecuteQueries(camera))
{
continue;
}
var distance = (newCenter - camera.transform.position.XZ()).sqrMagnitude;
if (lastStatus == (int)QueryBase.QueryStatus.OK && lastDistance < distance)
{
continue;
}
var status = provider.Value.Query(hash, minimumLength, points, displacements, normals, velocities, layer, center);
if (lastStatus < 0 || status == (int)QueryBase.QueryStatus.OK)
{
lastStatus = status;
lastDistance = distance;
}
}
return lastStatus;
}
public void SendReadBack(WaterRenderer water, CollisionLayers layers)
{
_Providers[water.CurrentCamera].SendReadBack(water, layers);
}
public void UpdateQueries(WaterRenderer water, CollisionLayer layer)
{
_Providers[water.CurrentCamera].UpdateQueries(water, layer);
}
}
sealed class CollisionQueryWithPasses : ICollisionProvider, IQueryable
{
readonly CollisionQuery _AnimatedWaves;
@@ -69,6 +166,14 @@ namespace WaveHarmonic.Crest
public int RequestCount => _AnimatedWaves.RequestCount + _DynamicWaves.RequestCount + _Displacement.RequestCount;
public int QueryCount => _AnimatedWaves.QueryCount + _DynamicWaves.QueryCount + _Displacement.QueryCount;
public CollisionQueryWithPasses()
{
_Water = WaterRenderer.Instance;
_AnimatedWaves = new(_Water);
_DynamicWaves = new(_Water);
_Displacement = new(_Water);
}
public CollisionQueryWithPasses(WaterRenderer water)
{
_Water = water;
@@ -107,12 +212,12 @@ namespace WaveHarmonic.Crest
return _AnimatedWaves;
}
public int Query(int hash, float minimumLength, Vector3[] points, float[] heights, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything)
public int Query(int hash, float minimumLength, Vector3[] points, float[] heights, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null)
{
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)
public int Query(int hash, float minimumLength, Vector3[] points, Vector3[] displacements, Vector3[] normals, Vector3[] velocities, CollisionLayer layer = CollisionLayer.Everything, Vector3? center = null)
{
return GetProvider(layer).Query(hash, minimumLength, points, displacements, normals, velocities);
}
@@ -151,13 +256,55 @@ namespace WaveHarmonic.Crest
_DynamicWaves.CleanUp();
_Displacement.CleanUp();
}
public void Initialize(WaterRenderer water)
{
}
}
// These are required because of collision layer.
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, CollisionLayer layer)
{
if (self is CollisionQueryPerCamera a)
{
a.UpdateQueries(water, layer);
}
else if (self is CollisionQueryWithPasses b)
{
b.UpdateQueries(water, layer);
}
else if (self is ICollisionProvider.NoneProvider c)
{
}
else
{
Debug.LogError("Crest: no valid query provider. Report this to developers!");
}
}
public static void UpdateQueries(this ICollisionProvider self, WaterRenderer water) => (self as IQueryable)?.UpdateQueries(water);
public static void SendReadBack(this ICollisionProvider self, WaterRenderer water, CollisionLayers layer) => (self as CollisionQueryWithPasses)?.SendReadBack(water, layer);
public static void SendReadBack(this ICollisionProvider self, WaterRenderer water, CollisionLayers layer)
{
if (self is CollisionQueryPerCamera a)
{
a.SendReadBack(water, layer);
}
else if (self is CollisionQueryWithPasses b)
{
b.SendReadBack(water, layer);
}
else if (self is ICollisionProvider.NoneProvider c)
{
}
else
{
Debug.LogError("Crest: no valid query provider. Report this to developers!");
}
}
public static void CleanUp(this ICollisionProvider self) => (self as IQueryable)?.CleanUp();
}
}

View File

@@ -59,7 +59,9 @@ namespace WaveHarmonic.Crest
{
distance = -1f;
Validate(allowMultipleCallsPerFrame: false);
var id = GetHashCode();
Validate(allowMultipleCallsPerFrame: false, id);
var water = WaterRenderer.Instance;
var provider = water == null ? null : water.AnimatedWavesLod.Provider;
@@ -70,7 +72,7 @@ namespace WaveHarmonic.Crest
_QueryPosition[i] = origin + i * _RayStepSize * direction;
}
var status = provider.Query(GetHashCode(), _MinimumLength, _QueryPosition, _QueryResult, null, null, layer);
var status = provider.Query(id, _MinimumLength, _QueryPosition, _QueryResult, null, null, layer);
if (!provider.RetrieveSucceeded(status))
{

View File

@@ -14,11 +14,6 @@ namespace WaveHarmonic.Crest
[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;

View File

@@ -15,9 +15,14 @@ namespace WaveHarmonic.Crest
{
internal static NoneProvider None { get; } = new();
internal static IDepthProvider Create(WaterRenderer water)
{
return water.IsMultipleViewpointMode ? new DepthQueryPerCamera(water) : new DepthQuery(water);
}
internal sealed class NoneProvider : IDepthProvider
{
public int Query(int _0, float _1, Vector3[] _2, Vector3[] result)
public int Query(int _0, float _1, Vector3[] _2, Vector3[] result, Vector3? _3 = null)
{
if (result != null) System.Array.Clear(result, 0, result.Length);
return 0;
@@ -28,7 +33,7 @@ namespace WaveHarmonic.Crest
/// 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);
/// <inheritdoc cref="IQueryProvider.Query(int, float, Vector3[], int, Vector3?)" />
int Query(int hash, float minimumLength, Vector3[] points, Vector3[] results, Vector3? center = null);
}
}

View File

@@ -5,14 +5,15 @@ using UnityEngine;
namespace WaveHarmonic.Crest
{
sealed class DepthQuery : QueryBase, IDepthProvider
sealed class DepthQuery : QueryBaseSimple, IDepthProvider
{
public DepthQuery(WaterRenderer water) : base(water) { }
public DepthQuery() : base(WaterRenderer.Instance.DepthLod) { }
public DepthQuery(WaterRenderer water) : base(water.DepthLod) { }
protected override int Kernel => 2;
public override int Query(int hash, float minimumSpatialLength, Vector3[] queries, Vector3[] results)
public override int Query(int hash, float minimumSpatialLength, Vector3[] queries, Vector3[] results, Vector3? center = null)
{
var id = base.Query(hash, minimumSpatialLength, queries, results);
var id = base.Query(hash, minimumSpatialLength, queries, results, center);
// Infinity will become NaN. Convert back to infinity.
// Negative infinity should not happen.
@@ -27,4 +28,9 @@ namespace WaveHarmonic.Crest
return id;
}
}
sealed class DepthQueryPerCamera : QueryPerCameraSimple<DepthQuery>, IDepthProvider
{
public DepthQueryPerCamera(WaterRenderer water) : base(water) { }
}
}

View File

@@ -15,12 +15,17 @@ namespace WaveHarmonic.Crest
{
internal static NoneProvider None { get; } = new();
internal static IFlowProvider Create(WaterRenderer water)
{
return water.IsMultipleViewpointMode ? new FlowQueryPerCamera(water) : new FlowQuery(water);
}
/// <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)
public int Query(int _0, float _1, Vector3[] _2, Vector3[] result, Vector3? _3 = null)
{
if (result != null) System.Array.Clear(result, 0, result.Length);
return 0;
@@ -31,7 +36,7 @@ namespace WaveHarmonic.Crest
/// 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);
/// <inheritdoc cref="IQueryProvider.Query(int, float, Vector3[], int, Vector3?)" />
int Query(int hash, float minimumLength, Vector3[] points, Vector3[] results, Vector3? center = null);
}
}

View File

@@ -6,9 +6,15 @@ namespace WaveHarmonic.Crest
/// <summary>
/// Samples horizontal motion of water volume
/// </summary>
sealed class FlowQuery : QueryBase, IFlowProvider
sealed class FlowQuery : QueryBaseSimple, IFlowProvider
{
public FlowQuery(WaterRenderer water) : base(water) { }
public FlowQuery() : base(WaterRenderer.Instance.FlowLod) { }
public FlowQuery(WaterRenderer water) : base(water.FlowLod) { }
protected override int Kernel => 1;
}
sealed class FlowQueryPerCamera : QueryPerCameraSimple<FlowQuery>, IFlowProvider
{
public FlowQueryPerCamera(WaterRenderer water) : base(water) { }
}
}

View File

@@ -9,6 +9,7 @@ using System.Collections.Generic;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
@@ -22,8 +23,9 @@ namespace WaveHarmonic.Crest
/// <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>
/// <param name="center">The center of all the query positions. Used to choose the closest query provider.</param>
/// <returns>The status of the query.</returns>
internal static int Query(int hash, float minimumLength, Vector3[] points, int layer) => 0;
internal static int Query(int hash, float minimumLength, Vector3[] points, int layer, Vector3? center) => throw new System.NotImplementedException("Crest: this method is for documentation reuse only. Do not invoke.");
/// <summary>
/// Check if the query results could be retrieved successfully using the return code
@@ -45,6 +47,12 @@ namespace WaveHarmonic.Crest
void UpdateQueries(WaterRenderer water);
void SendReadBack(WaterRenderer water);
void CleanUp();
void Initialize(WaterRenderer water);
}
interface IQueryableSimple : IQueryable
{
int Query(int hash, float minimumLength, Vector3[] queries, Vector3[] results, Vector3? center);
}
/// <summary>
@@ -64,6 +72,7 @@ namespace WaveHarmonic.Crest
const int k_NormalAdditionalQueryCount = 2;
readonly WaterRenderer _Water;
readonly IQueryableLod<IQueryProvider> _Lod;
readonly PropertyWrapperCompute _Wrapper;
@@ -85,8 +94,8 @@ namespace WaveHarmonic.Crest
internal const int k_DefaultMaximumQueryCount = 4096;
readonly int _MaximumQueryCount = k_DefaultMaximumQueryCount;
readonly Vector3[] _QueryPositionXZ_MinimumGridSize = new Vector3[k_DefaultMaximumQueryCount];
readonly int _MaximumQueryCount;
readonly Vector3[] _QueryPositionXZ_MinimumGridSize;
/// <summary>
/// Holds information about all query points. Maps from unique hash code to position in point array.
@@ -147,8 +156,9 @@ namespace WaveHarmonic.Crest
// 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();
var registrar = _Segments[_SegmentAcquire];
registrar._QueryCount = 0;
registrar._Segments.Clear();
foreach (var segment in _Segments[lastIndex]._Segments)
{
@@ -160,11 +170,10 @@ namespace WaveHarmonic.Crest
// 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.x = registrar._QueryCount;
newSegment.y = newSegment.x + (segment.Value.y - segment.Value.x);
_Segments[_SegmentAcquire]._QueryCount = newSegment.y + 1;
_Segments[_SegmentAcquire]._Segments.Add(segment.Key, newSegment);
registrar._QueryCount = newSegment.y + 1;
registrar._Segments.Add(segment.Key, newSegment);
}
}
}
@@ -253,17 +262,15 @@ namespace WaveHarmonic.Crest
InvalidDtForVelocity = 16,
}
public QueryBase(WaterRenderer water)
public QueryBase(IQueryableLod<IQueryProvider> lod)
{
_Water = water;
_Water = lod.Water;
_Lod = lod;
_DataArrivedAction = new(DataArrived);
if (_MaximumQueryCount != water._AnimatedWavesLod.MaximumQueryCount)
{
_MaximumQueryCount = water._AnimatedWavesLod.MaximumQueryCount;
_QueryPositionXZ_MinimumGridSize = new Vector3[_MaximumQueryCount];
}
_MaximumQueryCount = lod.MaximumQueryCount;
_QueryPositionXZ_MinimumGridSize = new Vector3[_MaximumQueryCount];
_ComputeBufferQueries = new(_MaximumQueryCount, 12, ComputeBufferType.Default);
_ComputeBufferResults = new(_MaximumQueryCount, 12, ComputeBufferType.Default);
@@ -277,7 +284,13 @@ namespace WaveHarmonic.Crest
Debug.LogError($"Crest: Could not load Query compute shader");
return;
}
_Wrapper = new(water.SimulationBuffer, shader, Kernel);
_Wrapper = new(_Water.SimulationBuffer, shader, Kernel);
}
void LogMaximumQueryCountExceededError()
{
Debug.LogError($"Crest: Maximum query count ({_MaximumQueryCount}) exceeded, increase the <i>{nameof(WaterRenderer)} > Simulations > {_Lod.Name} > {nameof(_Lod.MaximumQueryCount)}</i> to support a higher number of queries.", _Water);
}
/// <summary>
@@ -286,9 +299,11 @@ namespace WaveHarmonic.Crest
/// </summary>
protected bool UpdateQueryPoints(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] queryNormals)
{
if (queryPoints.Length + _SegmentRegistrarRingBuffer.Current._QueryCount > _MaximumQueryCount)
var registrar = _SegmentRegistrarRingBuffer.Current;
if (queryPoints.Length + registrar._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.");
LogMaximumQueryCountExceededError();
return false;
}
@@ -299,20 +314,20 @@ namespace WaveHarmonic.Crest
var countNorms = queryNormals != null ? queryNormals.Length : 0;
var countTotal = countPts + countNorms * k_NormalAdditionalQueryCount;
if (_SegmentRegistrarRingBuffer.Current._Segments.TryGetValue(ownerHash, out var segment))
if (registrar._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;
registrar._Segments[ownerHash] = segment;
segmentRetrieved = true;
}
else
{
_SegmentRegistrarRingBuffer.Current._Segments.Remove(ownerHash);
registrar._Segments.Remove(ownerHash);
}
}
@@ -324,18 +339,18 @@ namespace WaveHarmonic.Crest
if (!segmentRetrieved)
{
if (_SegmentRegistrarRingBuffer.Current._Segments.Count >= k_MaximumGuids)
if (registrar._Segments.Count >= k_MaximumGuids)
{
Debug.LogError("Crest: Too many guids registered with CollProviderCompute. Increase s_maxGuids.");
return false;
}
segment.x = _SegmentRegistrarRingBuffer.Current._QueryCount;
segment.x = registrar._QueryCount;
segment.y = segment.x + countTotal - 1;
segment.z = Time.frameCount;
_SegmentRegistrarRingBuffer.Current._Segments.Add(ownerHash, segment);
registrar._Segments.Add(ownerHash, segment);
_SegmentRegistrarRingBuffer.Current._QueryCount += countTotal;
registrar._QueryCount += countTotal;
//Debug.Log("Crest: Added points for " + guid);
}
@@ -346,9 +361,14 @@ namespace WaveHarmonic.Crest
var samplesPerWave = 2f;
var minGridSize = minWavelength / samplesPerWave;
// Displacements should not utilize the last slice which is used for transitioning
// waves between sampling resolutions. While it might be ok to use the last slice
// for other targets, we avoid using it to be consistent with displacements.
var minimumSlice = Mathf.Clamp(Mathf.FloorToInt(Mathf.Log(Mathf.Max(minGridSize / _Lod.Texel, 1f), 2f)), 0, _Water.LodLevels - 2);
if (countPts + segment.x > _QueryPositionXZ_MinimumGridSize.Length)
{
Debug.LogError("Crest: Too many wave height queries. Increase Max Query Count in the Animated Waves Settings.");
LogMaximumQueryCountExceededError();
return false;
}
@@ -356,7 +376,7 @@ namespace WaveHarmonic.Crest
{
_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;
_QueryPositionXZ_MinimumGridSize[pointi + segment.x].z = minimumSlice;
}
// To compute each normal, post 2 query points (reuse point above)
@@ -366,13 +386,13 @@ namespace WaveHarmonic.Crest
_QueryPositionXZ_MinimumGridSize[arrIdx].x = queryNormals[normi].x + k_FiniteDifferenceDx;
_QueryPositionXZ_MinimumGridSize[arrIdx].y = queryNormals[normi].z;
_QueryPositionXZ_MinimumGridSize[arrIdx].z = minGridSize;
_QueryPositionXZ_MinimumGridSize[arrIdx].z = minimumSlice;
arrIdx += 1;
_QueryPositionXZ_MinimumGridSize[arrIdx].x = queryNormals[normi].x;
_QueryPositionXZ_MinimumGridSize[arrIdx].y = queryNormals[normi].z + k_FiniteDifferenceDx;
_QueryPositionXZ_MinimumGridSize[arrIdx].z = minGridSize;
_QueryPositionXZ_MinimumGridSize[arrIdx].z = minimumSlice;
}
return true;
@@ -590,10 +610,11 @@ namespace WaveHarmonic.Crest
_QueryResultsTimeLast = _QueryResultsTime;
_ResultSegmentsLast = _ResultSegments;
var data = _Requests[lastDoneIndex]._Request.GetData<Vector3>();
var last = _Requests[lastDoneIndex];
var data = last._Request.GetData<Vector3>();
data.CopyTo(_QueryResults);
_QueryResultsTime = _Requests[lastDoneIndex]._DataTimestamp;
_ResultSegments = _Requests[lastDoneIndex]._Segments;
_QueryResultsTime = last._DataTimestamp;
_ResultSegments = last._Segments;
}
// Remove all the requests up to the last completed one
@@ -618,7 +639,23 @@ namespace WaveHarmonic.Crest
_SegmentRegistrarRingBuffer.ClearAll();
}
public virtual int Query(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] results)
public virtual void Initialize(WaterRenderer water)
{
}
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;
}
abstract class QueryBaseSimple : QueryBase, IQueryableSimple
{
protected QueryBaseSimple(IQueryableLod<IQueryProvider> lod) : base(lod) { }
public virtual int Query(int ownerHash, float minSpatialLength, Vector3[] queryPoints, Vector3[] results, Vector3? center)
{
var result = (int)QueryStatus.OK;
@@ -634,12 +671,159 @@ namespace WaveHarmonic.Crest
return result;
}
}
public int ResultGuidCount => _ResultSegments != null ? _ResultSegments.Count : 0;
abstract class QueryPerCamera<T> : IQueryable where T : IQueryable, new()
{
internal readonly WaterRenderer _Water;
internal readonly Dictionary<Camera, T> _Providers = new();
public int RequestCount => _Requests != null ? _Requests.Count : 0;
public QueryPerCamera(WaterRenderer water)
{
_Water = water;
Initialize(water);
}
public int QueryCount => _SegmentRegistrarRingBuffer != null ? _SegmentRegistrarRingBuffer.Current._QueryCount : 0;
public int ResultGuidCount
{
get
{
var total = 0;
foreach (var (camera, provider) in _Providers)
{
if (_Water.ShouldExecuteQueries(camera)) total += provider.ResultGuidCount;
}
return total;
}
}
public int RequestCount
{
get
{
var total = 0;
foreach (var (camera, provider) in _Providers)
{
if (_Water.ShouldExecuteQueries(camera)) total += provider.RequestCount;
}
return total;
}
}
public int QueryCount
{
get
{
var total = 0;
foreach (var (camera, provider) in _Providers)
{
if (_Water.ShouldExecuteQueries(camera)) total += provider.QueryCount;
}
return total;
}
}
public void CleanUp()
{
foreach (var provider in _Providers.Values)
{
provider?.CleanUp();
}
}
public void Initialize(WaterRenderer water)
{
var camera = water.CurrentCamera;
if (camera == null)
{
camera = water.Viewer;
}
if (camera == null)
{
return;
}
if (!_Providers.ContainsKey(camera))
{
// Cannot use parameters. We could use System.Activator.CreateInstance to get
// around that, but instead we just use WaterRenderer.Instance.
_Providers.Add(camera, new());
}
}
public void SendReadBack(WaterRenderer water)
{
_Providers[water.CurrentCamera].SendReadBack(water);
}
public void UpdateQueries(WaterRenderer water)
{
_Providers[water.CurrentCamera].UpdateQueries(water);
}
public Vector2 FindCenter(Vector3[] queries, Vector3? center)
{
if (center != null)
{
return ((Vector3)center).XZ();
}
// Calculate center if none provided.
var sum = Vector2.zero;
foreach (var point in queries)
{
sum += point.XZ();
}
return new(sum.x / queries.Length, sum.y / queries.Length);
}
}
abstract class QueryPerCameraSimple<T> : QueryPerCamera<T>, IQueryableSimple where T : IQueryableSimple, new()
{
protected QueryPerCameraSimple(WaterRenderer water) : base(water) { }
public int Query(int id, float length, Vector3[] queries, Vector3[] results, Vector3? center = null)
{
if (_Water.IsSeparateViewpointCameraLoop)
{
return _Providers[_Water.CurrentCamera].Query(id, length, queries, results, center);
}
var lastStatus = -1;
var lastDistance = Mathf.Infinity;
var newCenter = FindCenter(queries, center);
foreach (var provider in _Providers)
{
var camera = provider.Key;
if (!_Water.ShouldExecuteQueries(camera))
{
continue;
}
var distance = (newCenter - camera.transform.position.XZ()).sqrMagnitude;
if (lastStatus == (int)QueryBase.QueryStatus.OK && lastDistance < distance)
{
continue;
}
var status = provider.Value.Query(id, length, queries, results, center);
if (lastStatus < 0 || status == (int)QueryBase.QueryStatus.OK)
{
lastStatus = status;
lastDistance = distance;
}
}
return lastStatus;
}
}
static partial class Extensions

View File

@@ -29,17 +29,18 @@ namespace WaveHarmonic.Crest
[@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("The viewer as the source of the queries.\n\nOnly needs to be set if using multiple viewpoints on the Water Renderer.")]
[@Show(nameof(_Source), nameof(QuerySource.Viewer))]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
Camera _Viewer;
[Tooltip(ICollisionProvider.k_LayerTooltip)]
[@GenerateAPI]
[@DecoratedField, SerializeField]
@@ -49,14 +50,14 @@ namespace WaveHarmonic.Crest
[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))]
[@Enable(nameof(_Source), 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)]
[@Disable(nameof(_DistanceFromSurfaceUseCurve))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DistanceFromSurfaceSigned;
@@ -77,7 +78,7 @@ namespace WaveHarmonic.Crest
[@Label("Curve")]
[Tooltip("Apply a curve to the distance.\n\nValues towards \"one\" means closer to the water surface.")]
[@Predicated(nameof(_DistanceFromSurfaceUseCurve))]
[@Enable(nameof(_DistanceFromSurfaceUseCurve))]
[@GenerateAPI]
[UnityEngine.Serialization.FormerlySerializedAs("_DistanceCurve")]
[@DecoratedField, SerializeField]
@@ -88,7 +89,7 @@ namespace WaveHarmonic.Crest
[@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)]
[@Disable(nameof(_DistanceFromEdgeUseCurve))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DistanceFromEdgeSigned;
@@ -107,7 +108,7 @@ namespace WaveHarmonic.Crest
[@Label("Curve")]
[Tooltip("Apply a curve to the distance.\n\nValues towards \"one\" means closer to the water's edge.")]
[@Predicated(nameof(_DistanceFromEdgeUseCurve))]
[@Enable(nameof(_DistanceFromEdgeUseCurve))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
AnimationCurve _DistanceFromEdgeCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
@@ -174,6 +175,11 @@ namespace WaveHarmonic.Crest
var distance = water.ViewerHeightAboveWater;
if (water.MultipleViewpoints && (_Viewer == null || !water.GetViewerHeightAboveWater(_Viewer, out distance)))
{
return;
}
if (_Source == QuerySource.Transform)
{
if (!_SampleHeightHelper.SampleHeight(transform.position, out var height, minimumLength: 2f * _MinimumWavelength, _Layer)) return;
@@ -233,6 +239,11 @@ namespace WaveHarmonic.Crest
var distance = water.ViewerDistanceToShoreline;
if (water.MultipleViewpoints && (_Viewer == null || !water.GetViewerDistanceToShoreline(_Viewer, out distance)))
{
return;
}
if (_Source == QuerySource.Transform)
{
if (!_SampleDepthHelper.SampleDistanceToWaterEdge(transform.position, out distance))

View File

@@ -1,6 +1,7 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Collections.Generic;
using UnityEngine;
// Linter does not support mixing inheritdoc plus defining own parameters.
@@ -20,7 +21,7 @@ namespace WaveHarmonic.Crest.Internal
private protected readonly Vector3[] _QueryPosition;
private protected readonly Vector3[] _QueryResult;
int _LastFrame = -1;
readonly Dictionary<int, int> _LastFrame = new();
private protected SampleHelper(int queryCount = 1)
{
@@ -29,15 +30,29 @@ namespace WaveHarmonic.Crest.Internal
}
[System.Diagnostics.Conditional("UNITY_EDITOR")]
private protected void Validate(bool allowMultipleCallsPerFrame)
private protected void Validate(bool allowMultipleCallsPerFrame, int id)
{
if (Application.isPlaying && !Time.inFixedTimeStep && !allowMultipleCallsPerFrame && _LastFrame == Time.frameCount)
if (!_LastFrame.ContainsKey(id))
{
_LastFrame.Add(id, -1);
}
#if UNITY_EDITOR
// Prevent false positives spamming the console.
if (!UnityEditor.EditorApplication.isFocused || (Application.isPlaying && UnityEditor.EditorApplication.isPaused))
{
_LastFrame[id] = Time.frameCount;
return;
}
#endif
if (!Time.inFixedTimeStep && !allowMultipleCallsPerFrame && _LastFrame[id] == 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;
_LastFrame[id] = Time.frameCount;
}
// The first method is there just to get inheritdoc to work as it does not like
@@ -103,7 +118,7 @@ namespace WaveHarmonic.Crest
var isVelocity = (options & QueryOptions.Velocity) == QueryOptions.Velocity;
var isNormal = (options & QueryOptions.Normal) == QueryOptions.Normal;
Validate(allowMultipleCallsPerFrame);
Validate(allowMultipleCallsPerFrame, id);
_QueryPosition[0] = position;
@@ -115,7 +130,8 @@ namespace WaveHarmonic.Crest
_QueryResult,
isNormal ? _QueryResultNormal : null,
isVelocity ? _QueryResultVelocity : null,
layer
layer,
position
);
if (!provider.RetrieveSucceeded(status))
@@ -234,11 +250,13 @@ namespace WaveHarmonic.Crest
return false;
}
Validate(false);
var id = GetHashCode();
Validate(false, id);
_QueryPosition[0] = position;
var status = flowProvider.Query(GetHashCode(), minimumLength, _QueryPosition, _QueryResult);
var status = flowProvider.Query(id, minimumLength, _QueryPosition, _QueryResult, position);
if (!flowProvider.RetrieveSucceeded(status))
{
@@ -259,25 +277,25 @@ namespace WaveHarmonic.Crest
/// </summary>
public sealed class SampleDepthHelper : Internal.SampleHelper
{
bool Sample(Vector3 position, out Vector2 result)
internal bool Sample(int id, Vector3 position, out Vector2 result, bool allowMultipleCallsPerFrame = false)
{
var water = WaterRenderer.Instance;
var depthProvider = water == null ? null : water.DepthLod.Provider;
if (depthProvider == null)
{
result = Vector2.zero;
result = new(Mathf.Infinity, Mathf.Infinity);
return false;
}
Validate(false);
Validate(allowMultipleCallsPerFrame, id);
_QueryPosition[0] = position;
var status = depthProvider.Query(GetHashCode(), minimumLength: 0, _QueryPosition, _QueryResult);
var status = depthProvider.Query(id, minimumLength: 0, _QueryPosition, _QueryResult, position);
if (!depthProvider.RetrieveSucceeded(status))
{
result = Vector2.zero;
result = new(Mathf.Infinity, Mathf.Infinity);
return false;
}
@@ -285,13 +303,18 @@ namespace WaveHarmonic.Crest
return true;
}
bool Sample(Vector3 position, out Vector2 result)
{
return Sample(GetHashCode(), position, out result);
}
/// <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)
internal bool Sample(Vector3 position, out float depth, out float distance)
{
var success = Sample(position, out var result);
depth = result.x;
@@ -312,7 +335,12 @@ namespace WaveHarmonic.Crest
/// <inheritdoc cref="Sample(Vector3, out float, out float)"/>
public bool SampleDistanceToWaterEdge(Vector3 position, out float distance)
{
var success = Sample(position, out var result);
return SampleDistanceToWaterEdge(GetHashCode(), position, out distance);
}
internal bool SampleDistanceToWaterEdge(int id, Vector3 position, out float distance)
{
var success = Sample(id, position, out var result);
distance = result.y;
return success;
}

View File

@@ -19,22 +19,13 @@ namespace WaveHarmonic.Crest
{
var water = _Water;
if (!water._ShadowLod.Enabled)
if (!water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Shadows))
{
return;
}
#if UNITY_EDITOR
if (!WaterRenderer.IsWithinEditorUpdate)
{
return;
}
#endif
var camera = context.hdCamera.camera;
// Custom passes execute for every camera. We only support one camera for now.
if (!ReferenceEquals(camera, water.Viewer)) return;
// TODO: bail when not executing for main light or when no main light exists?
// if (renderingData.lightData.mainLightIndex == -1) return;

View File

@@ -45,6 +45,7 @@ namespace WaveHarmonic.Crest
}
}
#if URP_COMPATIBILITY_MODE
[System.Obsolete]
public override void Execute(ScriptableRenderContext context, ref RenderingData data)
{
@@ -54,6 +55,7 @@ namespace WaveHarmonic.Crest
context.ExecuteCommandBuffer(buffer);
CommandBufferPool.Release(buffer);
}
#endif
}
}

View File

@@ -34,25 +34,6 @@ namespace WaveHarmonic.Crest
return;
}
var water = s_Instance._Water;
if (!water._ShadowLod.Enabled)
{
return;
}
#if UNITY_EDITOR
if (!WaterRenderer.IsWithinEditorUpdate)
{
return;
}
#endif
// Only sample shadows for the main camera.
if (!ReferenceEquals(water.Viewer, camera))
{
return;
}
if (camera.TryGetComponent<UniversalAdditionalCameraData>(out var cameraData))
{

View File

@@ -12,11 +12,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/Waves.html#dynamic-waves-settings")]
public sealed partial class DynamicWavesLodSettings : LodSettings
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Header("Simulation")]
[Tooltip("How much energy is dissipated each frame.\n\nHelps simulation stability, but limits how far ripples will propagate. Set this as large as possible/acceptable. Default is 0.05.")]
@@ -52,5 +47,14 @@ namespace WaveHarmonic.Crest
[@GenerateAPI]
[SerializeField]
internal float _GravityMultiplier = 1f;
[@Heading("Culling")]
[Tooltip("Adds padding to water chunk bounds.\n\nDynamic Waves displaces the surface which can push vertices outside of the chunk bounds leading to culling issues. This value adds padding to the chunk bounds to mitigate this.")]
[@GenerateAPI]
[@DecoratedField]
[@SerializeField]
internal float _VerticalDisplacementCullingContributions = 5f;
}
}

View File

@@ -12,12 +12,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/WaterAppearance.html#foam-settings")]
public sealed partial class FoamLodSettings : LodSettings
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Header("General settings")]
[Tooltip("Foam will not exceed this value in the simulation which can be used to prevent foam from accumulating too much.")]

View File

@@ -1,14 +1,14 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Base class for simulation settings.
/// </summary>
public abstract partial class LodSettings : ScriptableObject
public abstract partial class LodSettings : CustomScriptableObject
{
}

View File

@@ -8,49 +8,55 @@ namespace WaveHarmonic.Crest
{
partial class ShadowLod
{
internal bool ShouldRender(Camera camera)
{
if (!_Enabled)
{
return false;
}
// Even though volume also uses shadows, it only makes sense with a surface.
if (!_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Surface))
{
return false;
}
// Only sample shadows for the main camera.
if (_Water.IsSingleViewpointMode && _Water.Viewer != camera)
{
return false;
}
return true;
}
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
// TODO: refactor this similar to MaskRenderer.
if (!RenderPipelineHelper.IsLegacy)
{
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
SampleShadowsURP.EnqueuePass(context, camera);
}
#endif
if (RenderPipelineHelper.IsUniversal)
{
SampleShadowsURP.EnqueuePass(context, camera);
return;
}
#endif
if (!RenderPipelineHelper.IsLegacy)
{
return;
}
CopyShadowMapBuffer?.Clear();
#if UNITY_EDITOR
// Do not execute when editor is not active to conserve power and prevent possible leaks.
if (!UnityEditorInternal.InternalEditorUtility.isApplicationActive)
{
CopyShadowMapBuffer?.Clear();
return;
}
if (!WaterRenderer.IsWithinEditorUpdate)
{
CopyShadowMapBuffer?.Clear();
return;
}
#endif
var water = _Water;
if (water == null)
{
return;
}
if (!WaterRenderer.ShouldRender(camera, water.Surface.Layer))
{
return;
}
if (camera == water.Viewer && CopyShadowMapBuffer != null)
if (CopyShadowMapBuffer != null)
{
if (_Light != null)
{
@@ -62,7 +68,7 @@ namespace WaveHarmonic.Crest
// Disable for XR SPI otherwise input will not have correct world position.
Rendering.BIRP.DisableXR(CopyShadowMapBuffer, camera);
BuildCommandBuffer(water, CopyShadowMapBuffer);
BuildCommandBuffer(_Water, CopyShadowMapBuffer);
// Restore XR SPI as we cannot rely on remaining pipeline to do it for us.
Rendering.BIRP.EnableXR(CopyShadowMapBuffer, camera);
@@ -71,6 +77,12 @@ namespace WaveHarmonic.Crest
internal void OnEndCameraRendering(Camera camera)
{
// Method is only called for either single viewpoint mode or separate viewpoints.
if (_Water.IsMultipleViewpointMode)
{
StoreCameraData(camera);
}
if (!RenderPipelineHelper.IsLegacy)
{
return;
@@ -83,36 +95,15 @@ namespace WaveHarmonic.Crest
CopyShadowMapBuffer?.Clear();
return;
}
if (!WaterRenderer.IsWithinEditorUpdate)
{
CopyShadowMapBuffer?.Clear();
return;
}
#endif
var water = _Water;
if (water == null)
// CBs added to a light are executed for every camera, but the LOD data is only
// supports a single camera. Removing the CB after the camera renders restricts the
// CB to one camera. Careful of recursive rendering for planar reflections, as it
// executes a camera within this camera's frame.
if (_Light != null && CopyShadowMapBuffer != null)
{
return;
}
if (!WaterRenderer.ShouldRender(camera, water.Surface.Layer))
{
return;
}
if (camera == water.Viewer)
{
// CBs added to a light are executed for every camera, but the LOD data is only
// supports a single camera. Removing the CB after the camera renders restricts the
// CB to one camera. Careful of recursive rendering for planar reflections, as it
// executes a camera within this camera's frame.
if (_Light != null && CopyShadowMapBuffer != null)
{
_Light.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, CopyShadowMapBuffer);
}
_Light.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, CopyShadowMapBuffer);
}
}
}

View File

@@ -29,13 +29,13 @@ namespace WaveHarmonic.Crest
bool _DynamicSoftShadows = true;
[Tooltip("Factor control for dynamic soft jitter.")]
[@Predicated(nameof(_DynamicSoftShadows), hide: true)]
[@Show(nameof(_DynamicSoftShadows))]
[@Range(0f, 1f, Range.Clamp.Minimum)]
[SerializeField]
float _SoftJitterExtinctionFactor = 0.75f;
[Tooltip("Jitter diameter for soft shadows, controls softness of this shadowing component.")]
[@Predicated(nameof(_DynamicSoftShadows), hide: true, inverted: true)]
[@Hide(nameof(_DynamicSoftShadows))]
[@Range(0f, k_MaximumJitter)]
[@GenerateAPI]
[SerializeField]
@@ -96,6 +96,7 @@ namespace WaveHarmonic.Crest
private protected override Color ClearColor => Color.black;
private protected override bool NeedToReadWriteTextureData => true;
internal override int BufferCount => 2;
internal override bool SkipEndOfFrame => true;
private protected override GraphicsFormat RequestedTextureFormat => _TextureFormatMode switch
{
@@ -150,17 +151,9 @@ namespace WaveHarmonic.Crest
#if d_UnityURP
var asset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
// TODO: Support single casacades as it is possible.
if (asset && asset.shadowCascadeCount < 2)
{
Debug.LogError("Crest shadowing requires shadow cascades to be enabled on the pipeline asset.", asset);
_Valid = false;
return;
}
if (asset.mainLightRenderingMode == LightRenderingMode.Disabled)
{
Debug.LogError("Crest: Main Light must be enabled to enable water shadowing.", _Water);
Debug.LogWarning("Crest: Main Light must be enabled to enable water shadowing.", _Water);
_Valid = false;
return;
}
@@ -171,7 +164,7 @@ namespace WaveHarmonic.Crest
if (isShadowsDisabled)
{
Debug.LogError("Crest: Shadows must be enabled in the quality settings to enable water shadowing.", _Water);
Debug.LogWarning("Crest: Shadows must be enabled in the quality settings to enable water shadowing.", _Water);
_Valid = false;
return;
}
@@ -236,8 +229,6 @@ namespace WaveHarmonic.Crest
{
base.Allocate();
_Targets.RunLambda(buffer => Clear(buffer));
{
_RenderMaterial = new PropertyWrapperMaterial[Slices];
var shader = WaterResources.Instance.Shaders._UpdateShadow;
@@ -263,12 +254,6 @@ namespace WaveHarmonic.Crest
}
}
internal override void ClearLodData()
{
base.ClearLodData();
_Targets.RunLambda(buffer => Clear(buffer));
}
/// <summary>
/// Validates the primary light.
/// </summary>
@@ -309,7 +294,7 @@ namespace WaveHarmonic.Crest
{
if (_Error != Error.IncorrectLightType)
{
Debug.LogError("Crest: Primary light must be of type Directional.", _Light);
Debug.LogWarning("Crest: Primary light must be of type Directional.", _Light);
_Error = Error.IncorrectLightType;
}
return false;
@@ -352,7 +337,8 @@ namespace WaveHarmonic.Crest
{
if (_Light != _Water.PrimaryLight)
{
_Targets.RunLambda(buffer => Clear(buffer));
Clear(DataTexture);
Clear(_PersistentDataTexture);
CleanUpShadowCommandBuffers();
_Light = null;
}
@@ -388,7 +374,8 @@ namespace WaveHarmonic.Crest
if (CopyShadowMapBuffer != null)
{
// If we have a command buffer, then there is likely shadow data so we need to clear it.
_Targets.RunLambda(buffer => Clear(buffer));
Clear(DataTexture);
Clear(_PersistentDataTexture);
CleanUpShadowCommandBuffers();
}
@@ -398,8 +385,6 @@ namespace WaveHarmonic.Crest
CopyShadowMapBuffer ??= new() { name = WaterRenderer.k_DrawLodData };
CopyShadowMapBuffer.Clear();
FlipBuffers();
// clear the shadow collection. it will be overwritten with shadow values IF the shadows render,
// which only happens if there are (nontransparent) shadow receivers around. this is only reliable
// in play mode, so don't do it in edit mode.
@@ -457,12 +442,6 @@ namespace WaveHarmonic.Crest
}
}
private protected override void GetSubstepData(float timeToSimulate, out int substeps, out float delta)
{
substeps = Mathf.FloorToInt(timeToSimulate * _SimulationFrequency);
delta = substeps > 0 ? (1f / _SimulationFrequency) : 0f;
}
private protected override void SetAdditionalSimulationParameters(PropertyWrapperCompute properties)
{
base.SetAdditionalSimulationParameters(properties);
@@ -521,6 +500,7 @@ namespace WaveHarmonic.Crest
{
_Enabled = true;
_TextureFormat = GraphicsFormat.R8G8_UNorm;
_Blur = true;
}
internal static SortedList<int, ILodInput> s_Inputs = new(Helpers.DuplicateComparison);

View File

@@ -17,11 +17,6 @@ namespace WaveHarmonic.Crest
[AddComponentMenu(Constants.k_MenuPrefixDebug + "Debug GUI")]
sealed class DebugGUI : ManagedBehaviour<WaterRenderer>
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[SerializeField]
bool _ShowWaterData = true;
@@ -136,6 +131,7 @@ namespace WaveHarmonic.Crest
{
// Safe as there should only be one instance at a time.
Helpers.Destroy(s_DebugArrayMaterial);
s_DebugArrayMaterial = null;
}
Vector3 _ViewerPositionLastFrame;

View File

@@ -81,71 +81,85 @@ namespace WaveHarmonic.Crest
// API
//
[Obsolete]
int GetLayer()
{
return Surface.Layer;
}
[Obsolete]
void SetLayer(int previous, int current)
{
Surface.Layer = current;
}
[Obsolete]
Material GetMaterial()
{
return Surface.Material;
}
[Obsolete]
void SetMaterial(Material previous, Material current)
{
Surface.Material = current;
}
[Obsolete]
Material GetVolumeMaterial()
{
return Surface.VolumeMaterial;
}
[Obsolete]
void SetVolumeMaterial(Material previous, Material current)
{
Surface.VolumeMaterial = current;
}
[Obsolete]
bool GetCastShadows()
{
return Surface.CastShadows;
}
[Obsolete]
void SetCastShadows(bool previous, bool current)
{
Surface.CastShadows = current;
}
[Obsolete]
bool GetWaterBodyCulling()
{
return Surface.WaterBodyCulling;
}
[Obsolete]
void SetWaterBodyCulling(bool previous, bool current)
{
Surface.WaterBodyCulling = current;
}
[Obsolete]
int GetTimeSliceBoundsUpdateFrameCount()
{
return Surface.TimeSliceBoundsUpdateFrameCount;
}
[Obsolete]
void SetTimeSliceBoundsUpdateFrameCount(int previous, int current)
{
Surface.TimeSliceBoundsUpdateFrameCount = current;
}
[Obsolete]
bool GetAllowRenderQueueSorting()
{
return Surface.AllowRenderQueueSorting;
}
[Obsolete]
void SetAllowRenderQueueSorting(bool previous, bool current)
{
Surface.AllowRenderQueueSorting = current;

View File

@@ -23,13 +23,9 @@ namespace WaveHarmonic.Crest
/// <summary>
/// Where to obtain water shape on CPU for physics / gameplay.
/// </summary>
[System.Obsolete("Please use QuerySource instead.")]
public CollisionSource CollisionSource { get => _CollisionSource; internal set => _CollisionSource = value; }
/// <summary>
/// Maximum number of wave queries that can be performed when using GPU queries.
/// </summary>
public int MaximumQueryCount => _MaximumQueryCount;
/// <summary>
/// Any water deeper than this will receive full wave strength.
/// </summary>
@@ -44,7 +40,12 @@ namespace WaveHarmonic.Crest
/// <remarks>
/// Set this to 2 to improve wave quality. In some cases like flowing rivers, this can make a substantial difference to visual stability. We recommend doubling the Resolution on the WaterRenderer component to preserve detail after making this change.
/// </remarks>
public float WaveResolutionMultiplier { get => _WaveResolutionMultiplier; set => _WaveResolutionMultiplier = value; }
public float WaveResolutionMultiplier { get => GetWaveResolutionMultiplier(); set => _WaveResolutionMultiplier = value; }
/// <summary>
/// The wave sampling method to determine quality and performance.
/// </summary>
public WaveSampling WaveSampling { get => _WaveSampling; set => _WaveSampling = value; }
}
}
@@ -321,6 +322,14 @@ namespace WaveHarmonic.Crest
/// </summary>
public UnityEngine.LayerMask Layers { get => _Layers; set => SetDirty(_Layers, _Layers = value); }
/// <summary>
/// Where the Depth Probe is placed.
/// </summary>
/// <remarks>
/// The default performs the best.
/// </remarks>
public Placement Placement { get => _Placement; set => _Placement = value; }
/// <summary>
/// Overrides global quality settings.
/// </summary>
@@ -424,6 +433,14 @@ namespace WaveHarmonic.Crest
/// Induce horizontal displacements to sharpen simulated waves.
/// </summary>
public float HorizontalDisplace { get => _HorizontalDisplace; set => _HorizontalDisplace = value; }
/// <summary>
/// Adds padding to water chunk bounds.
/// </summary>
/// <remarks>
/// Dynamic Waves displaces the surface which can push vertices outside of the chunk bounds leading to culling issues. This value adds padding to the chunk bounds to mitigate this.
/// </remarks>
public float VerticalDisplacementCullingContributions { get => _VerticalDisplacementCullingContributions; set => _VerticalDisplacementCullingContributions = value; }
}
}
@@ -661,6 +678,23 @@ namespace WaveHarmonic.Crest
{
partial class Lod
{
/// <summary>
/// Blurs the output.
/// </summary>
/// <remarks>
/// Enable if blurring is desired or intolerable artifacts are present.
/// The blur is optimized to only run on inner LODs and at lower scales.
/// </remarks>
public bool Blur { get => _Blur; set => SetDirty(_Blur, _Blur = value); }
/// <summary>
/// Number of blur iterations.
/// </summary>
/// <remarks>
/// Blur iterations are optimized to only run maximum iterations on the inner LODs.
/// </remarks>
public int BlurIterations { get => _BlurIterations; set => _BlurIterations = value; }
/// <summary>
/// Whether the simulation is enabled.
/// </summary>
@@ -697,6 +731,22 @@ namespace WaveHarmonic.Crest
}
}
namespace WaveHarmonic.Crest
{
partial class Lod<T>
{
/// <summary>
/// Maximum number of queries that can be performed when using GPU queries.
/// </summary>
public int MaximumQueryCount => _MaximumQueryCount;
/// <summary>
/// Where to obtain water data on CPU for physics / gameplay.
/// </summary>
public LodQuerySource QuerySource { get => _QuerySource; internal set => _QuerySource = value; }
}
}
namespace WaveHarmonic.Crest
{
partial class LodInput
@@ -752,6 +802,14 @@ namespace WaveHarmonic.Crest
{
partial class Meniscus
{
/// <summary>
/// Rules to exclude cameras from rendering the meniscus.
/// </summary>
/// <remarks>
/// These are exclusion rules, so for all cameras, select Nothing. These rules are applied on top of the Layer rules.
/// </remarks>
public WaterCameraExclusion CameraExclusions { get => _CameraExclusions; set => _CameraExclusions = value; }
/// <summary>
/// Whether the meniscus is enabled.
/// </summary>
@@ -942,6 +1000,14 @@ namespace WaveHarmonic.Crest
/// "Viewer" will reuse queries already performed by the Water Renderer
/// </remarks>
public QuerySource Source { get => _Source; set => _Source = value; }
/// <summary>
/// The viewer as the source of the queries.
/// </summary>
/// <remarks>
/// Only needs to be set if using multiple viewpoints on the Water Renderer.
/// </remarks>
public UnityEngine.Camera Viewer { get => _Viewer; set => _Viewer = value; }
}
}
@@ -1026,6 +1092,11 @@ namespace WaveHarmonic.Crest
{
partial class ShapeFFT
{
/// <summary>
/// Whether to apply the options shown when "Show Advanced Controls" is active.
/// </summary>
public bool ApplyAdvancedSpectrumControls { get => _ApplyAdvancedSpectrumControls; set => _ApplyAdvancedSpectrumControls = value; }
/// <summary>
/// Maximum amount a point on the surface will be displaced horizontally by waves from its rest position.
/// </summary>
@@ -1042,6 +1113,11 @@ namespace WaveHarmonic.Crest
/// </remarks>
public float MaximumVerticalDisplacement { get => _MaximumVerticalDisplacement; set => _MaximumVerticalDisplacement = value; }
/// <summary>
/// Whether to override automatic culling based on heuristics.
/// </summary>
public bool OverrideCulling { get => _OverrideCulling; set => _OverrideCulling = value; }
/// <summary>
/// Whether to use the wind turbulence on this component rather than the global wind turbulence.
/// </summary>
@@ -1116,6 +1192,14 @@ namespace WaveHarmonic.Crest
/// </remarks>
public bool EvaluateSpectrumAtRunTimeEveryFrame { get => _EvaluateSpectrumAtRunTimeEveryFrame; set => _EvaluateSpectrumAtRunTimeEveryFrame = value; }
/// <summary>
/// Whether the maximum possible vertical displacement is used for the Drop Detail Height Based On Waves calculation.
/// </summary>
/// <remarks>
/// This setting is ignored for global waves, as they always contribute. For local waves, only enable for large areas that are treated like global waves (eg a storm).
/// </remarks>
public bool IncludeInDropDetailHeightBasedOnWaves { get => _IncludeInDropDetailHeightBasedOnWaves; set => _IncludeInDropDetailHeightBasedOnWaves = value; }
/// <summary>
/// Whether to use the wind direction on this component rather than the global wind direction.
/// </summary>
@@ -1138,7 +1222,7 @@ namespace WaveHarmonic.Crest
/// <remarks>
/// Low resolutions are more efficient but can result in noticeable patterns in the shape.
/// </remarks>
public int Resolution { get => _Resolution; set => _Resolution = value; }
public int Resolution { get => GetResolution(); set => _Resolution = value; }
/// <summary>
/// How much these waves respect the shallow water attenuation.
@@ -1148,6 +1232,14 @@ namespace WaveHarmonic.Crest
/// </remarks>
public float RespectShallowWaterAttenuation { get => _RespectShallowWaterAttenuation; set => _RespectShallowWaterAttenuation = value; }
/// <summary>
/// Whether global waves is applied above or below sea level.
/// </summary>
/// <remarks>
/// Waves are faded out to avoid hard transitionds. They are fully faded by 1m from sea level.
/// </remarks>
public bool SeaLevelOnly { get => _SeaLevelOnly; set => _SeaLevelOnly = value; }
/// <summary>
/// The spectrum that defines the water surface shape.
/// </summary>
@@ -1273,6 +1365,14 @@ namespace WaveHarmonic.Crest
/// </remarks>
public bool AllowRenderQueueSorting { get => _AllowRenderQueueSorting; set => _AllowRenderQueueSorting = value; }
/// <summary>
/// Rules to exclude cameras from surface rendering.
/// </summary>
/// <remarks>
/// These are exclusion rules, so for all cameras, select Nothing. These rules are applied on top of the Layer rules.
/// </remarks>
public WaterCameraExclusion CameraExclusions { get => _CameraExclusions; set => _CameraExclusions = value; }
/// <summary>
/// Have the water surface cast shadows for albedo (both foam and custom).
/// </summary>
@@ -1296,6 +1396,14 @@ namespace WaveHarmonic.Crest
/// </summary>
public UnityEngine.Material Material { get => _Material; set => _Material = value; }
/// <summary>
/// Whether to support using the surface material with other renderers.
/// </summary>
/// <remarks>
/// Also requires enabling Custom Mesh on the material.
/// </remarks>
public bool SupportCustomRenderers { get => _SupportCustomRenderers; set => _SupportCustomRenderers = value; }
/// <summary>
/// How many frames to distribute the chunk bounds calculation.
/// </summary>
@@ -1351,8 +1459,17 @@ namespace WaveHarmonic.Crest
/// <remarks>
/// If disabled, then additionally ignore any camera that is not the view camera or our reflection camera. It will require managing culling masks of all cameras.
/// </remarks>
[System.Obsolete("Please use Camera Exclusion instead.")]
public bool AllCameras { get => _AllCameras; set => _AllCameras = value; }
/// <summary>
/// Rules to exclude cameras from rendering underwater.
/// </summary>
/// <remarks>
/// These are exclusion rules, so for all cameras, select Nothing. These rules are applied on top of the Layer rules.
/// </remarks>
public WaterCameraExclusion CameraExclusions { get => _CameraExclusions; set => _CameraExclusions = value; }
/// <summary>
/// Copying parameters each frame ensures underwater appearance stays consistent with the water surface.
/// </summary>
@@ -1369,6 +1486,11 @@ namespace WaveHarmonic.Crest
/// </remarks>
public float CullLimit { get => _CullLimit; set => _CullLimit = value; }
/// <summary>
/// Whether to enable culling of water chunks when below water.
/// </summary>
public bool EnableChunkCulling { get => _EnableChunkCulling; set => _EnableChunkCulling = value; }
/// <summary>
/// Whether the underwater effect is enabled.
/// </summary>
@@ -1506,6 +1628,7 @@ namespace WaveHarmonic.Crest
/// <summary>
/// Whether to allow MSAA.
/// </summary>
[System.Obsolete("MSAA for the planar reflection camera is no longer supported. This setting will be ignored.")]
public bool AllowMSAA { get => _AllowMSAA; set => _AllowMSAA = value; }
/// <summary>
@@ -1529,7 +1652,7 @@ namespace WaveHarmonic.Crest
/// <summary>
/// Disables shadows.
/// </summary>
public bool DisableShadows { get => _DisableShadows; set => _DisableShadows = value; }
public bool DisableShadows { get => _DisableShadows; set => SetDisableShadows(_DisableShadows, _DisableShadows = value); }
/// <summary>
/// Whether planar reflections are enabled.
@@ -1557,7 +1680,7 @@ namespace WaveHarmonic.Crest
/// <summary>
/// What side of the water surface to render planar reflections for.
/// </summary>
public WaterReflectionSide ReflectionSide { get => _Mode; set => _Mode = value; }
public WaterReflectionSide ReflectionSide { get => _Mode; set => SetReflectionSide(_Mode, _Mode = value); }
/// <summary>
/// Planar relfections using an oblique frustum for better performance.
@@ -1572,11 +1695,24 @@ namespace WaveHarmonic.Crest
/// </summary>
public float NonObliqueNearSurfaceThreshold { get => _NonObliqueNearSurfaceThreshold; set => _NonObliqueNearSurfaceThreshold = value; }
/// <summary>
/// Overscan amount to capture off-screen content.
/// </summary>
/// <remarks>
/// Renders the reflections at a larger viewport size to capture off-screen content when the surface reflects off-screen. This avoids a category of artifacts - especially when looking down. This can be expensive, as the value is a multiplier to the capture size.
/// </remarks>
public float Overscan { get => _Overscan; set => _Overscan = value; }
/// <summary>
/// Overrides global quality settings.
/// </summary>
public QualitySettingsOverride QualitySettingsOverride { get => _QualitySettingsOverride; set => _QualitySettingsOverride = value; }
/// <summary>
/// Renderer index for the reflection camera.
/// </summary>
public int RendererIndex { get => _RendererIndex; set => SetRendererIndex(_RendererIndex, _RendererIndex = value); }
/// <summary>
/// Whether to render to the viewer camera only.
/// </summary>
@@ -1649,6 +1785,14 @@ namespace WaveHarmonic.Crest
/// </remarks>
public UnityEngine.Camera Viewer { get => GetViewer(); set => _Camera = value; }
/// <summary>
/// Rules to exclude cameras from being a center-of-detail.
/// </summary>
/// <remarks>
/// These are exclusion rules, so for all cameras, select Nothing.
/// </remarks>
public WaterCameraExclusion CameraExclusions { get => _CameraExclusions; set => _CameraExclusions = value; }
/// <summary>
/// Have the water surface cast shadows for albedo (both foam and custom).
/// </summary>
@@ -1668,6 +1812,14 @@ namespace WaveHarmonic.Crest
/// </summary>
public ClipLod ClipLod => _ClipLod;
/// <summary>
/// The background rendering mode when a camera does not render.
/// </summary>
/// <remarks>
/// When switching between multiple cameras, simulations will not progress for cameras which do not render for that frame. This setting tells the system when it should continue to progress the simulations.
/// </remarks>
public WaterDataBackgroundMode DataBackgroundMode { get => _DataBackgroundMode; set => _DataBackgroundMode = value; }
/// <summary>
/// Water depth information used for shallow water, shoreline foam, wave attenuation, among others.
/// </summary>
@@ -1773,6 +1925,27 @@ namespace WaveHarmonic.Crest
/// </remarks>
public bool OverrideRenderHDR { get => _OverrideRenderHDR; set => _OverrideRenderHDR = value; }
#if d_UnityModuleWind
/// <summary>
/// Whether to override the given wind zone's wind direction.
/// </summary>
public bool OverrideWindZoneWindDirection { get => _OverrideWindZoneWindDirection; set => _OverrideWindZoneWindDirection = value; }
#endif
#if d_UnityModuleWind
/// <summary>
/// Whether to override the given wind zone's wind speed.
/// </summary>
public bool OverrideWindZoneWindSpeed { get => _OverrideWindZoneWindSpeed; set => _OverrideWindZoneWindSpeed = value; }
#endif
#if d_UnityModuleWind
/// <summary>
/// Whether to override the given wind zone's wind turbulence.
/// </summary>
public bool OverrideWindZoneWindTurbulence { get => _OverrideWindZoneWindTurbulence; set => _OverrideWindZoneWindTurbulence = value; }
#endif
/// <summary>
/// The portal renderer.
/// </summary>
@@ -1918,6 +2091,7 @@ namespace WaveHarmonic.Crest
/// </remarks>
public float WindTurbulence { get => GetWindTurbulence(); set => _WindTurbulence = value; }
#if d_UnityModuleWind
/// <summary>
/// Uses a provided WindZone as the source of global wind.
/// </summary>
@@ -1925,6 +2099,7 @@ namespace WaveHarmonic.Crest
/// It must be directional. Wind speed units are presumed to be in m/s.
/// </remarks>
public UnityEngine.WindZone WindZone { get => _WindZone; set => _WindZone = value; }
#endif
/// <summary>
/// Whether to enable motion vector support.

View File

@@ -29,11 +29,6 @@ namespace WaveHarmonic.Crest.Generated
{
enum CollisionLayers
{
/// <summary>
/// All layers.
/// </summary>
Everything,
/// <summary>
/// No extra layers (ie single layer).
/// </summary>
@@ -51,6 +46,11 @@ namespace WaveHarmonic.Crest.Generated
/// Extra displacement layer for visual displacement.
/// </summary>
Displacement,
/// <summary>
/// All layers.
/// </summary>
Everything,
}
}
@@ -123,6 +123,11 @@ namespace WaveHarmonic.Crest.Generated
/// </summary>
OnStart,
/// <summary>
/// Populates the DepthProbe every frame.
/// </summary>
EveryFrame,
/// <summary>
/// Requires manual updating via DepthProbe.Populate.
/// </summary>
@@ -288,6 +293,31 @@ namespace WaveHarmonic.Crest.Generated
}
namespace WaveHarmonic.Crest.Generated
{
enum LodQuerySource
{
/// <summary>
/// No query source.
/// </summary>
None,
/// <summary>
/// Uses AsyncGPUReadback to retrieve data from GPU to CPU.
/// </summary>
/// <remarks>
/// This is the most optimal approach.
/// </remarks>
GPU,
/// <summary>
/// Computes data entirely on the CPU.
/// </summary>
CPU,
}
}
namespace WaveHarmonic.Crest.Generated
{
enum LodTextureFormatMode
@@ -385,6 +415,68 @@ namespace WaveHarmonic.Crest.Generated
}
namespace WaveHarmonic.Crest.Generated
{
enum WaterCameraExclusion
{
/// <summary>
/// No exclusion rules applied.
/// </summary>
Nothing,
/// <summary>
/// Exclude hidden cameras.
/// </summary>
/// <remarks>
/// Does not affect reflection cameras, as they are typically always hidden. Use the Reflection flag for them.
/// </remarks>
Hidden,
/// <summary>
/// Exclude reflection cameras.
/// </summary>
Reflection,
/// <summary>
/// Exclude cameras not tagged as MainCamera.
/// </summary>
NonMainCamera,
/// <summary>
/// Apply all exclusion rules.
/// </summary>
Everything,
}
}
namespace WaveHarmonic.Crest.Generated
{
enum WaterDataBackgroundMode
{
/// <summary>
/// Always progress simulations in the background when camera does not render.
/// </summary>
Always,
/// <summary>
/// Progress simulations in the background when camera is inactive (ie !isActiveAndEnabled).
/// </summary>
Inactive,
/// <summary>
/// Progress simulations in the background when camera is disabled (ie !enabled).
/// </summary>
Disabled,
/// <summary>
/// Never progress simulations in the background.
/// </summary>
Never,
}
}
namespace WaveHarmonic.Crest.Generated
{
enum WaterInjectionPoint
@@ -451,3 +543,31 @@ namespace WaveHarmonic.Crest.Generated
Clip,
}
}
namespace WaveHarmonic.Crest.Generated
{
enum WaveSampling
{
/// <summary>
/// Automatically chooses the other options as needed (512+ resolution needs precision).
/// </summary>
Automatic,
/// <summary>
/// Reduces samples by copying waves from higher LODs to lower LODs.
/// </summary>
/// <remarks>
/// Best for resolutions lower than 512.
/// </remarks>
Performance,
/// <summary>
/// Samples directly from the wave buffers to preserve wave quality.
/// </summary>
/// <remarks>
/// Needed for higher resolutions (512+). Higher LOD counts can also benefit with this enabled.
/// </remarks>
Precision,
}
}

View File

@@ -48,11 +48,6 @@ namespace WaveHarmonic.Crest
[AddComponentMenu(Constants.k_MenuPrefixPhysics + "Floating Object")]
public sealed partial class FloatingObject : ManagedBehaviour<WaterRenderer>
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Tooltip("The rigid body to affect.\n\nIt will automatically get the sibling rigid body if not set.")]
[@GenerateAPI]
@@ -80,7 +75,7 @@ namespace WaveHarmonic.Crest
[@Label("Torque Strength")]
[Tooltip("Strength of torque applied to match boat orientation to water normal.")]
[@Predicated(nameof(_Model), inverted: true, nameof(FloatingObjectModel.AlignNormal), hide: true)]
[@Show(nameof(_Model), nameof(FloatingObjectModel.AlignNormal))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _BuoyancyTorqueStrength = 8f;
@@ -93,13 +88,13 @@ namespace WaveHarmonic.Crest
[@Label("Height Offset")]
[Tooltip("Height offset from transform center to bottom of boat (if any).\n\nDefault value is for a default sphere. Having this value be an accurate measurement from center to bottom is not necessary.")]
[@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal), hide: true)]
[@Show(nameof(_Model), nameof(FloatingObjectModel.AlignNormal))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _CenterToBottomOffset = -1f;
[Tooltip("Approximate hydrodynamics of 'surfing' down waves.")]
[@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal))]
[@Show(nameof(_Model), nameof(FloatingObjectModel.AlignNormal))]
[@Range(0, 1)]
[@GenerateAPI]
[SerializeField]
@@ -108,7 +103,9 @@ namespace WaveHarmonic.Crest
[UnityEngine.Space(10)]
[Tooltip("Query points for buoyancy.\n\nOnly applicable to Probes model.")]
[@Show(nameof(_Model), nameof(FloatingObjectModel.Probes))]
[@GenerateAPI]
[@DecoratedField]
[SerializeField]
internal FloatingObjectProbe[] _Probes = new FloatingObjectProbe[] { };
@@ -139,14 +136,14 @@ namespace WaveHarmonic.Crest
float _ObjectWidth = 3f;
[Tooltip("Computes a separate normal based on boat length to get more accurate orientations.\n\nRequires the cost of an extra collision sample.")]
[@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal), hide: true)]
[@Show(nameof(_Model), nameof(FloatingObjectModel.AlignNormal))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _UseObjectLength;
[Tooltip("Length dimension of boat.\n\nOnly used if Use Boat Length is enabled.")]
[@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal), hide: true)]
[@Predicated(nameof(_UseObjectLength))]
[@Show(nameof(_Model), nameof(FloatingObjectModel.AlignNormal))]
[@Enable(nameof(_UseObjectLength))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _ObjectLength = 3f;

View File

@@ -85,7 +85,10 @@ namespace WaveHarmonic.Crest
});
}
}
#endif
#if URP_COMPATIBILITY_MODE
#if UNITY_6000_0_OR_NEWER
[System.Obsolete]
#endif
public override void Execute(ScriptableRenderContext context, ref RenderingData data)
@@ -96,6 +99,7 @@ namespace WaveHarmonic.Crest
context.ExecuteCommandBuffer(buffer);
CommandBufferPool.Release(buffer);
}
#endif
}
}
}

View File

@@ -21,6 +21,7 @@ namespace WaveHarmonic.Crest
public static MaskRenderer Instantiate(WaterRenderer water)
{
#pragma warning disable format
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
@@ -40,6 +41,7 @@ namespace WaveHarmonic.Crest
{
return new MaskRendererBIRP(water);
}
#pragma warning restore format
}
// For PortalRenderer.
@@ -102,6 +104,10 @@ namespace WaveHarmonic.Crest
None,
Zero = 1 << 0,
Color = 1 << 1,
/// <summary>
/// Depth is used for the legacy mask or for stencil operations.
/// </summary>
Depth = 1 << 2,
Both = Color | Depth,
}
@@ -235,7 +241,7 @@ namespace WaveHarmonic.Crest
slices: TextureXR.slices,
dimension: TextureXR.dimension,
depthBufferBits: DepthBits.None,
colorFormat: GraphicsFormat.R16_SFloat,
colorFormat: Helpers.GetCompatibleTextureFormat(GraphicsFormat.R16_SFloat, randomWrite: true),
enableRandomWrite: true,
useDynamicScale: true,
name: k_MaskColor
@@ -251,7 +257,7 @@ namespace WaveHarmonic.Crest
scaleFactor: Vector2.one,
slices: TextureXR.slices,
dimension: TextureXR.dimension,
depthBufferBits: Helpers.k_DepthBits,
depthBufferBits: Rendering.GetDefaultDepthBufferBits(),
colorFormat: GraphicsFormat.None,
enableRandomWrite: false,
useDynamicScale: true,
@@ -277,7 +283,7 @@ namespace WaveHarmonic.Crest
if (_Inputs.HasFlag(MaskInput.Depth))
{
descriptor.graphicsFormat = GraphicsFormat.None;
descriptor.depthBufferBits = Helpers.k_DepthBufferBits;
descriptor.depthBufferBits = (int)Rendering.GetDefaultDepthBufferBits();
if (RenderPipelineCompatibilityHelper.ReAllocateIfNeeded(ref _DepthRTH, descriptor, name: k_MaskDepth))
{
@@ -288,7 +294,8 @@ namespace WaveHarmonic.Crest
if (_Inputs.HasFlag(MaskInput.Color))
{
// NOTE: Intel iGPU for Metal and DirectX both had issues with R16 (2021.11.18).
descriptor.graphicsFormat = GraphicsFormat.R16_SFloat;
descriptor.graphicsFormat = Helpers.GetCompatibleTextureFormat(GraphicsFormat.R16_SFloat, randomWrite: true);
descriptor.depthStencilFormat = GraphicsFormat.None;
descriptor.depthBufferBits = 0;
descriptor.enableRandomWrite = true;

View File

@@ -100,7 +100,10 @@ namespace WaveHarmonic.Crest
});
}
}
#endif
#if URP_COMPATIBILITY_MODE
#if UNITY_6000_0_OR_NEWER
[System.Obsolete]
#endif
public override void Execute(ScriptableRenderContext context, ref RenderingData data)
@@ -110,6 +113,7 @@ namespace WaveHarmonic.Crest
context.ExecuteCommandBuffer(buffer);
CommandBufferPool.Release(buffer);
}
#endif
}
}
}

View File

@@ -2,6 +2,7 @@
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
@@ -9,7 +10,7 @@ namespace WaveHarmonic.Crest
/// Renders the meniscus (waterline).
/// </summary>
[System.Serializable]
public sealed partial class Meniscus
public sealed partial class Meniscus : Versioned
{
[@Space(10)]
@@ -33,6 +34,14 @@ namespace WaveHarmonic.Crest
internal Material _Material;
[@Heading("Advanced")]
[Tooltip("Rules to exclude cameras from rendering the meniscus.\n\nThese are exclusion rules, so for all cameras, select Nothing. These rules are applied on top of the Layer rules.")]
[@DecoratedField]
[@GenerateAPI]
[SerializeField]
WaterCameraExclusion _CameraExclusions = WaterCameraExclusion.Hidden | WaterCameraExclusion.Reflection;
WaterRenderer _Water;
internal MeniscusRenderer Renderer { get; private set; }
@@ -76,6 +85,7 @@ namespace WaveHarmonic.Crest
return;
}
#pragma warning disable format
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
@@ -96,6 +106,17 @@ namespace WaveHarmonic.Crest
{
Renderer ??= new MeniscusRendererBIRP(water, this);
}
#pragma warning restore format
}
internal bool ShouldRender(Camera camera)
{
if (!Enabled)
{
return false;
}
return Renderer.ShouldExecute(camera);
}
}
@@ -131,11 +152,6 @@ namespace WaveHarmonic.Crest
private protected readonly WaterRenderer _Water;
internal readonly Meniscus _Meniscus;
static partial class ShaderIDs
{
public static readonly int s_HorizonNormal = Shader.PropertyToID("_Crest_HorizonNormal");
}
public abstract void OnBeginCameraRendering(Camera camera);
public abstract void OnEndCameraRendering(Camera camera);
@@ -174,24 +190,19 @@ namespace WaveHarmonic.Crest
return false;
}
// Meniscus is a product of the water surface.
if (!_Water.Surface.Enabled)
if (!WaterRenderer.ShouldRender(camera, _Meniscus.Layer, _Meniscus._CameraExclusions))
{
return false;
}
if (camera.cameraType is not CameraType.Game and not CameraType.SceneView)
{
return false;
}
if (!WaterRenderer.ShouldRender(camera, _Meniscus.Layer))
// Meniscus depends on both the surface and volume.
if (!_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.SurfaceAndVolume))
{
return false;
}
#if d_CrestPortals
if (_Water.Portals.Active)
if (_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Portal))
{
// Near surface check not compatible with portals.
return true;
@@ -211,13 +222,6 @@ namespace WaveHarmonic.Crest
internal void Execute<T>(Camera camera, T commands) where T : ICommandWrapper
{
// Project water normal onto camera plane.
_Meniscus.Material.SetVector(ShaderIDs.s_HorizonNormal, new Vector2
(
Vector3.Dot(Vector3.up, camera.transform.right),
Vector3.Dot(Vector3.up, camera.transform.up)
));
var isFullScreenRequired = true;
var isMasked = false;
var passOffset = 1;
@@ -225,7 +229,7 @@ namespace WaveHarmonic.Crest
#if d_CrestPortals
passOffset = (int)Portals.PortalRenderer.MeniscusPass.Length;
if (_Water.Portals.Active)
if (_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Portal))
{
isMasked = isFullScreenRequired = _Water._Portals.RenderMeniscus(commands, _Meniscus.Material);
}

View File

@@ -22,6 +22,7 @@ namespace WaveHarmonic.Crest
public static int s_WaterLineSnappedPosition = Shader.PropertyToID("_Crest_WaterLineSnappedPosition");
public static int s_WaterLineResolution = Shader.PropertyToID("_Crest_WaterLineResolution");
public static int s_WaterLineTexel = Shader.PropertyToID("_Crest_WaterLineTexel");
public static int s_WaterLineFlatWater = Shader.PropertyToID("_Crest_WaterLineFlatWater");
}
RenderTexture _HeightRT;
@@ -50,6 +51,13 @@ namespace WaveHarmonic.Crest
internal void UpdateDisplacedSurfaceData(Camera camera)
{
Helpers.SetGlobalBoolean(ShaderIDs.s_WaterLineFlatWater, IsQuadMesh);
if (IsQuadMesh)
{
return;
}
// World size of the texture. Formula should effectively cover the camera.
var size = 1f + (camera.nearClipPlane * 2f);
@@ -76,6 +84,7 @@ namespace WaveHarmonic.Crest
"_Crest_WaterLine",
ref _HeightRT,
texel: 0.0125f,
maximumResolution: 2048,
out _SurfaceDataParameters
);
@@ -84,9 +93,11 @@ namespace WaveHarmonic.Crest
BindDisplacedSurfaceData(wrapper);
var lod = (int)Builder.PatchType.Interior;
var mpb = _PerCascadeMPB.Current[lod];
var mpb = PerCascadeMPB[lod];
if (_Water.Viewpoint != camera.transform && Vector3.Distance(_Water.Viewpoint.position, camera.transform.position) > 0.01f)
var viewpoint = _Water.Viewpoint;
if (viewpoint == null || (viewpoint != camera.transform && Vector3.Distance(viewpoint.position, camera.transform.position) > 0.01f))
{
foreach (var chunk in _Water.Surface.Chunks)
{
@@ -125,22 +136,32 @@ namespace WaveHarmonic.Crest
Graphics.ExecuteCommandBuffer(commands);
}
internal void UpdateDisplacedSurfaceData(CommandBuffer commands, Bounds bounds, string name, ref RenderTexture target, float texel, out SurfaceDataParameters parameters)
internal void UpdateDisplacedSurfaceData(CommandBuffer commands, Bounds bounds, string name, ref RenderTexture target, float texel, int maximumResolution, out SurfaceDataParameters parameters)
{
var size = bounds.size.XZ();
var position = bounds.center.XZ();
var scale = size;
// TODO: texel needs to be calculates is clamped
// TODO: aspect ratio
var resolution = new Vector2Int
(
// TODO: Floor, Ceil or Round?
Mathf.CeilToInt(size.x / texel),
Mathf.CeilToInt(size.y / texel)
);
var largest = Mathf.Max(resolution.x, resolution.y);
if (largest > maximumResolution)
{
texel = Mathf.Max(size.x, size.y) / maximumResolution;
resolution = new Vector2Int
(
Mathf.CeilToInt(size.x / texel),
Mathf.CeilToInt(size.y / texel)
);
}
// Snapping for spatial stability. Different results, but could not tell which is
// more accurate. At higher resolution, appears negligable anyway.
var snapped = position - new Vector2(Mathf.Repeat(position.x, texel), Mathf.Repeat(position.y, texel));
@@ -153,11 +174,6 @@ namespace WaveHarmonic.Crest
_Texel = texel,
};
if (resolution.x > 2048 || resolution.y > 2048)
{
return;
}
// FIXME: LOD scale less than two has cut off and fall off at edges.
var view = WaterRenderer.CalculateViewMatrixFromSnappedPositionRHS(snapped.XNZ());
var projection = Matrix4x4.Ortho(size.x * -0.5f, size.x * 0.5f, size.y * -0.5f, size.y * 0.5f, 1f, 10000f + 10000f);

View File

@@ -27,6 +27,9 @@ namespace WaveHarmonic.Crest
case nameof(_Layer):
SetLayer((int)previous, _Layer);
break;
case nameof(_MeshType):
_Water.Rebuild();
break;
case nameof(_ChunkTemplate):
// We have to rebuild, as we instantiate entire GO. If we restricted it to just a
// MeshRenderer, then we could just replace those.
@@ -42,6 +45,9 @@ namespace WaveHarmonic.Crest
case nameof(_Debug) + "." + nameof(DebugFields._UniformTiles):
Rebuild();
break;
case nameof(_Debug) + "." + nameof(DebugFields._DrawRendererBounds):
foreach (var chunk in Chunks) chunk._DrawRenderBounds = _Debug._DrawRendererBounds;
break;
}
}
}

View File

@@ -62,18 +62,7 @@ namespace WaveHarmonic.Crest
var hdCamera = context.hdCamera;
var camera = hdCamera.camera;
if (!WaterRenderer.ShouldRender(camera, _Water.Surface.Layer))
{
return;
}
// Our reflections do not need them.
if (camera == WaterReflections.CurrentCamera)
{
return;
}
if (_Water.Surface.Material == null)
if (!_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Surface))
{
return;
}
@@ -114,6 +103,7 @@ namespace WaveHarmonic.Crest
var rld = new RendererListDesc(_ShaderTagID, context.cullingResults, camera)
{
layerMask = 1 << _Water.Surface.Layer,
// Required to set the pass. Use shader to keep WB material overrides.
overrideShader = _Water.Surface.Material.shader,
overrideShaderPassIndex = _Water.Surface.Material.FindPass("Forward"),
renderQueueRange = RenderQueueRange.transparent,

View File

@@ -22,6 +22,7 @@ namespace WaveHarmonic.Crest
}
CommandBuffer _DrawWaterSurfaceBuffer;
MaterialPropertyBlock _QuadMeshMPB;
void OnBeginCameraRenderingLegacy(Camera camera)
{
@@ -40,8 +41,6 @@ namespace WaveHarmonic.Crest
return;
}
camera.depthTextureMode |= DepthTextureMode.Depth;
_DrawWaterSurfaceBuffer ??= new() { name = WaterRenderer.k_DrawWater };
_DrawWaterSurfaceBuffer.Clear();
@@ -111,6 +110,15 @@ namespace WaveHarmonic.Crest
// Always enabled.
commands.SetShaderKeyword("LIGHTPROBE_SH", true);
if (IsQuadMesh)
{
_QuadMeshMPB ??= new();
new PropertyWrapperMPB(_QuadMeshMPB).SetSHCoefficients(Root.position);
Render(camera, commands, Material, pass: 0, mpb: _QuadMeshMPB);
commands.EndSample(k_DrawWaterSurface);
return;
}
UpdateChunkVisibility(camera);
foreach (var chunk in Chunks)
@@ -137,9 +145,7 @@ namespace WaveHarmonic.Crest
chunk.Bind();
}
var mpb = new PropertyWrapperMPB(chunk._MaterialPropertyBlock);
mpb.SetSHCoefficients(chunk.transform.position);
commands.DrawMesh(chunk._Mesh, chunk.transform.localToWorldMatrix, renderer.sharedMaterial, 0, 0, chunk._MaterialPropertyBlock);
commands.DrawRenderer(chunk.Rend, renderer.sharedMaterial, 0, 0);
}
commands.EndSample(k_DrawWaterSurface);

View File

@@ -1,6 +1,7 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if UNITY_EDITOR
#if d_UnityHDRP
using UnityEngine;
@@ -56,7 +57,12 @@ namespace WaveHarmonic.Crest
return;
}
if (camera.cameraType != CameraType.SceneView || camera != _Water.Viewer)
if (!_Water._CurrentSceneCameraHasMouseHover)
{
return;
}
if (camera.cameraType != CameraType.SceneView || (_Water.IsSingleViewpointMode && camera != _Water.Viewer))
{
return;
}
@@ -68,3 +74,4 @@ namespace WaveHarmonic.Crest
}
#endif // d_UnityHDRP
#endif

View File

@@ -1,6 +1,8 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.Rendering;
@@ -12,7 +14,12 @@ namespace WaveHarmonic.Crest
void OnPreRenderWaterLevelDepthTexture(Camera camera)
{
if (camera.cameraType != CameraType.SceneView || camera != _Water.Viewer)
if (!_Water._CurrentSceneCameraHasMouseHover)
{
return;
}
if (camera.cameraType != CameraType.SceneView || (_Water.IsSingleViewpointMode && camera != _Water.Viewer))
{
return;
}
@@ -38,3 +45,5 @@ namespace WaveHarmonic.Crest
}
}
}
#endif

View File

@@ -1,6 +1,8 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if UNITY_EDITOR
#if d_UnityURP
using UnityEngine;
@@ -37,7 +39,12 @@ namespace WaveHarmonic.Crest
return;
}
if (camera.cameraType != CameraType.SceneView || camera != _Water.Viewer)
if (camera.cameraType != CameraType.SceneView || (_Water.IsSingleViewpointMode && camera != _Water.Viewer))
{
return;
}
if (!_Water._CurrentSceneCameraHasMouseHover)
{
return;
}
@@ -69,7 +76,10 @@ namespace WaveHarmonic.Crest
});
}
}
#endif
#if URP_COMPATIBILITY_MODE
#if UNITY_6000_0_OR_NEWER
[System.Obsolete]
#endif
public override void Execute(ScriptableRenderContext context, ref RenderingData data)
@@ -79,8 +89,10 @@ namespace WaveHarmonic.Crest
context.ExecuteCommandBuffer(buffer);
CommandBufferPool.Release(buffer);
}
#endif
}
}
}
#endif // d_UnityURP
#endif

View File

@@ -1,7 +1,7 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// FIXME: Broken for BIRP on MacOS. Either platform specific problem or bug in Unity.
#if UNITY_EDITOR
using UnityEngine;
using UnityEngine.Rendering;
@@ -19,8 +19,18 @@ namespace WaveHarmonic.Crest
void ExecuteWaterLevelDepthTexture(Camera camera, CommandBuffer buffer)
{
Helpers.CreateRenderTargetTextureReference(ref _WaterLevelDepthTexture, ref _WaterLevelDepthTarget);
_WaterLevelDepthTexture.name = k_WaterLevelDepthTextureName;
// Currently, only used for painting which means only when mouse is over the view.
if (!_Water._CurrentSceneCameraHasMouseHover)
{
return;
}
if (_WaterLevelDepthTexture == null)
{
_WaterLevelDepthTexture = new(0, 0, 0);
}
WaterLevelDepthTexture.name = k_WaterLevelDepthTextureName;
if (_WaterLevelDepthMaterial == null)
{
@@ -41,11 +51,19 @@ namespace WaveHarmonic.Crest
// Depth texture.
// Always release to handle screen size changes.
_WaterLevelDepthTexture.Release();
WaterLevelDepthTexture.Release();
descriptor.graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R32_SFloat;
descriptor.depthBufferBits = 0;
Helpers.SafeCreateRenderTexture(ref _WaterLevelDepthTexture, descriptor);
_WaterLevelDepthTexture.Create();
WaterLevelDepthTexture.descriptor = descriptor;
WaterLevelDepthTexture.Create();
_WaterLevelDepthTarget = new
(
WaterLevelDepthTexture,
mipLevel: 0,
CubemapFace.Unknown,
depthSlice: -1 // Bind all XR slices.
);
// Convert.
Helpers.Blit(buffer, _WaterLevelDepthTarget, Rendering.BIRP.UtilityMaterial, (int)Rendering.BIRP.UtilityPass.Copy);
@@ -82,3 +100,5 @@ namespace WaveHarmonic.Crest
}
}
}
#endif

View File

@@ -24,9 +24,7 @@ namespace WaveHarmonic.Crest
{
_Water = water;
renderPassEvent = RenderPassEvent.BeforeRenderingTransparents;
// Copy color happens between "after skybox" and "before transparency".
ConfigureInput(ScriptableRenderPassInput.Color | ScriptableRenderPassInput.Depth);
ConfigureInput(ScriptableRenderPassInput.None);
}
public static void Enable(WaterRenderer water)
@@ -52,25 +50,18 @@ namespace WaveHarmonic.Crest
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (!WaterRenderer.ShouldRender(camera, Instance._Water.Surface.Layer))
if (!IsTransparent(_Water.Surface.Material))
{
return;
}
// Our reflections do not need them.
if (camera == WaterReflections.CurrentCamera)
{
return;
}
if (Instance._Water.Surface.Material == null)
{
return;
}
if (!IsTransparent(Instance._Water.Surface.Material))
{
return;
// Copy color happens between "after skybox" and "before transparency".
var pass = ScriptableRenderPassInput.Color | ScriptableRenderPassInput.Depth;
#if d_Crest_SimpleTransparency
pass = ScriptableRenderPassInput.None;
#endif
ConfigureInput(pass);
}
camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(Instance);
@@ -113,6 +104,7 @@ namespace WaveHarmonic.Crest
var rld = new RendererListDesc(_ShaderTagID, renderingData.cullResults, cameraData.camera)
{
layerMask = 1 << _Water.Surface.Layer,
// Required to set the pass. Use shader to keep WB material overrides.
overrideShader = _Water.Surface.Material.shader,
overrideShaderPassIndex = 0, // UniversalForward
renderQueueRange = RenderQueueRange.transparent,
@@ -129,7 +121,10 @@ namespace WaveHarmonic.Crest
});
}
}
#endif
#if URP_COMPATIBILITY_MODE
#if UNITY_6000_0_OR_NEWER
[System.Obsolete]
#endif
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
@@ -144,6 +139,7 @@ namespace WaveHarmonic.Crest
var rld = new RendererListDesc(_ShaderTagID, renderingData.cullResults, renderingData.cameraData.camera)
{
layerMask = 1 << _Water.Surface.Layer,
// Required to set the pass. Use shader to keep WB material overrides.
overrideShader = _Water.Surface.Material.shader,
overrideShaderPassIndex = 0, // UniversalForward
renderQueueRange = RenderQueueRange.transparent,
@@ -156,6 +152,7 @@ namespace WaveHarmonic.Crest
context.ExecuteCommandBuffer(buffer);
CommandBufferPool.Release(buffer);
}
#endif
}
}
}

View File

@@ -5,21 +5,25 @@ using System.Buffers;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Utility;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
enum WaterMeshType
{
[Tooltip("Chunks implemented as a clip-map.")]
Chunks,
[Tooltip("A single quad.\n\nOptimal for demanding platforms like mobile. Displacement will only contribute to normals.")]
Quad,
}
/// <summary>
/// Renders the water surface.
/// </summary>
[System.Serializable]
public sealed partial class SurfaceRenderer
public sealed partial class SurfaceRenderer : Versioned
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[@Space(10)]
[Tooltip("Whether the underwater effect is enabled.\n\nAllocates/releases resources if state has changed.")]
@@ -33,6 +37,27 @@ namespace WaveHarmonic.Crest
[SerializeField]
internal int _Layer = 4; // Water
[@Space(10)]
[@Label("Mesh")]
[Tooltip("The meshing solution for the water surface.")]
[@DecoratedField]
[@SerializeField]
WaterMeshType _MeshType;
[Tooltip("Template for water chunks as a prefab.\n\nThe only requirements are that the prefab must contain a MeshRenderer at the root and not a MeshFilter or WaterChunkRenderer. MR values will be overwritten where necessary and the prefabs are linked in edit mode.")]
[@PrefabField(title: "Create Chunk Prefab", name: "Water Chunk")]
[SerializeField]
internal GameObject _ChunkTemplate;
[Tooltip("Whether to support using the surface material with other renderers.\n\nAlso requires enabling Custom Mesh on the material.")]
[@GenerateAPI]
[@DecoratedField]
[@SerializeField]
bool _SupportCustomRenderers = true;
[@Space(10)]
[Tooltip("Material to use for the water surface.")]
[@AttachMaterialEditor(order: 0)]
[@MaterialField("Crest/Water", name: "Water", title: "Create Water Material")]
@@ -47,11 +72,6 @@ namespace WaveHarmonic.Crest
[SerializeField]
internal Material _VolumeMaterial = null;
[Tooltip("Template for water chunks as a prefab.\n\nThe only requirements are that the prefab must contain a MeshRenderer at the root and not a MeshFilter or WaterChunkRenderer. MR values will be overwritten where necessary and the prefabs are linked in edit mode.")]
[@PrefabField(title: "Create Chunk Prefab", name: "Water Chunk")]
[SerializeField]
internal GameObject _ChunkTemplate;
[@Space(10)]
[Tooltip("Have the water surface cast shadows for albedo (both foam and custom).")]
@@ -74,12 +94,18 @@ namespace WaveHarmonic.Crest
[@Heading("Advanced")]
[Tooltip("Rules to exclude cameras from surface rendering.\n\nThese are exclusion rules, so for all cameras, select Nothing. These rules are applied on top of the Layer rules.")]
[@DecoratedField]
[@GenerateAPI]
[SerializeField]
internal WaterCameraExclusion _CameraExclusions = WaterCameraExclusion.Hidden | WaterCameraExclusion.Reflection;
[Tooltip("How to handle self-intersections of the water surface.\n\nThey can be caused by choppy waves which can cause a flipped underwater effect. When not using the portals/volumes, this fix is only applied when within 2 metres of the water surface. Automatic will disable the fix if portals/volumes are used which is the recommend setting.")]
[@DecoratedField, SerializeField]
internal SurfaceSelfIntersectionFixMode _SurfaceSelfIntersectionFixMode = SurfaceSelfIntersectionFixMode.Automatic;
[Tooltip("Whether to allow sorting using the render queue.\n\nIf you need to change the minor part of the render queue (eg +100), then enable this option. As a side effect, it will also disable the front-to-back rendering optimization for Crest. This option does not affect changing the major part of the render queue (eg AlphaTest, Transparent), as that is always allowed.\n\nRender queue sorting is required for some third-party integrations.")]
[@Predicated(RenderPipeline.HighDefinition, inverted: true, hide: true)]
[@Hide(RenderPipeline.HighDefinition)]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal bool _AllowRenderQueueSorting;
@@ -108,6 +134,13 @@ namespace WaveHarmonic.Crest
[Tooltip("Disable generating a wide strip of triangles at the outer edge to extend water to edge of view frustum.")]
[@DecoratedField, SerializeField]
public bool _DisableSkirt;
#if !CREST_DEBUG
[HideInInspector]
#endif
[Tooltip("Toggle the Draw Renderer Bounds on each chunk.")]
[@DecoratedField, SerializeField]
public bool _DrawRendererBounds;
}
const string k_DrawWaterSurface = "Surface";
@@ -116,14 +149,15 @@ namespace WaveHarmonic.Crest
internal Transform Root { get; private set; }
internal List<WaterChunkRenderer> Chunks { get; } = new();
internal bool _Rebuild;
Renderer _RendererTemplate;
//
// Level of Detail
//
// Extra frame is for motion vectors.
internal BufferedData<MaterialPropertyBlock[]> _PerCascadeMPB = new(2, () => new MaterialPropertyBlock[Lod.k_MaximumSlices]);
readonly MaterialPropertyBlock[] _PerCascadeMPB = new MaterialPropertyBlock[Lod.k_MaximumSlices];
internal MaterialPropertyBlock[] PerCascadeMPB { get; private set; }
// We are computing these values to be optimal based on the base mesh vertex density.
float _LodAlphaBlackPointFade;
@@ -155,6 +189,7 @@ namespace WaveHarmonic.Crest
internal Material _MotionVectorMaterial;
internal Material AboveOrBelowSurfaceMaterial => _VolumeMaterial == null ? _Material : _VolumeMaterial;
internal bool IsQuadMesh => _MeshType == WaterMeshType.Quad;
//
@@ -200,30 +235,63 @@ namespace WaveHarmonic.Crest
public static readonly int s_ChunkGeometryGridWidth = Shader.PropertyToID("_Crest_ChunkGeometryGridWidth");
public static readonly int s_ChunkFarNormalsWeight = Shader.PropertyToID("_Crest_ChunkFarNormalsWeight");
public static readonly int s_ChunkNormalScrollSpeed = Shader.PropertyToID("_Crest_ChunkNormalScrollSpeed");
public static readonly int s_ChunkMeshScaleAlphaSource = Shader.PropertyToID("_Crest_ChunkMeshScaleAlphaSource");
public static readonly int s_ChunkGeometryGridWidthSource = Shader.PropertyToID("_Crest_ChunkGeometryGridWidthSource");
public static readonly int s_NormalMapParameters = Shader.PropertyToID("_Crest_NormalMapParameters");
// Visualizer
public static readonly int s_DataType = Shader.PropertyToID("_Crest_DataType");
public static readonly int s_Exposure = Shader.PropertyToID("_Crest_Exposure");
public static readonly int s_Range = Shader.PropertyToID("_Crest_Range");
public static readonly int s_Saturate = Shader.PropertyToID("_Crest_Saturate");
}
bool _ForceRenderingOff;
internal bool ForceRenderingOff
{
get => _ForceRenderingOff;
set
{
_ForceRenderingOff = value;
if (_Enabled)
{
Root.gameObject.SetActive(!_ForceRenderingOff && !IsQuadMesh);
}
}
}
Material _VisualizeDataMaterial;
internal Material VisualizeDataMaterial
{
get
{
if (_VisualizeDataMaterial == null)
{
_VisualizeDataMaterial = new(Shader.Find("Hidden/Crest/Debug/Visualize Data"));
}
return _VisualizeDataMaterial;
}
}
internal void Initialize()
{
Root = Builder.GenerateMesh(_Water, this, Chunks, _Water.LodResolution, _Water._GeometryDownSampleFactor, _Water.LodLevels);
if (_ChunkTemplate != null)
{
_RendererTemplate = _ChunkTemplate.GetComponent<Renderer>();
}
Root.position = _Water.Position;
Root.localScale = new(_Water.Scale, 1f, _Water.Scale);
// Populate MPBs with defaults.
for (var index = 0; index < _Water.LodLevels; index++)
{
for (var frame = 0; frame < 2; frame++)
{
var mpb = new MaterialPropertyBlock();
mpb.SetInteger(Lod.ShaderIDs.s_LodIndex, index);
mpb.SetFloat(ShaderIDs.s_ChunkFarNormalsWeight, 1f);
mpb.SetFloat(ShaderIDs.s_ChunkMeshScaleAlpha, 0f);
mpb.SetFloat(ShaderIDs.s_ChunkMeshScaleAlphaSource, 0f);
_PerCascadeMPB.Previous(frame)[index] = mpb;
}
}
// Populate MPBs with defaults. Protects against null exceptions etc.
PerCascadeMPB = _PerCascadeMPB;
NormalMapParameters = _NormalMapParameters;
_PreviousObjectToWorld = new Matrix4x4[Chunks.Count];
PreviousObjectToWorld = _PreviousObjectToWorld;
InitializeProperties();
// Resolution is 4 tiles across.
var baseMeshDensity = _Water.LodResolution * 0.25f / _Water._GeometryDownSampleFactor;
@@ -260,12 +328,18 @@ namespace WaveHarmonic.Crest
for (var i = 0; i < _Meshes?.Length; i++)
{
Helpers.Destroy(_Meshes[i]);
_Meshes[i] = null;
}
Chunks.Clear();
CoreUtils.Destroy(_MotionVectorMaterial);
CoreUtils.Destroy(_DisplacedMaterial);
// Clear camera data.
_PerCameraPerCascadeMPB.Clear();
_PerCameraNormalMapParameters.Clear();
_PerCameraPreviousObjectToWorld.Clear();
if (Root != null)
{
CoreUtils.Destroy(Root.gameObject);
@@ -297,6 +371,11 @@ namespace WaveHarmonic.Crest
return;
}
if (IsQuadMesh)
{
return;
}
GeometryUtility.CalculateFrustumPlanes(camera, _CameraFrustumPlanes);
foreach (var chunk in Chunks)
@@ -347,28 +426,38 @@ namespace WaveHarmonic.Crest
_Rebuild = false;
}
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
internal bool ShouldRender(Camera camera)
{
if (!WaterRenderer.ShouldRender(camera, Layer))
if (!_Enabled)
{
return;
return false;
}
if (!WaterRenderer.ShouldRender(camera, Layer, _CameraExclusions))
{
return false;
}
// Our planar reflection camera must never render the surface.
if (camera == WaterReflections.CurrentCamera)
if (camera == _Water.Reflections.ReflectionCamera)
{
return;
return false;
}
if (Material == null)
{
return;
return false;
}
return true;
}
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
WritePerCameraMaterialParameters(camera);
// Motion Vectors.
if (ShouldRenderMotionVectors(camera) && _QueueMotionVectors)
if (ShouldRenderMotionVectors(camera) && QueueMotionVectors)
{
UpdateChunkVisibility(camera);
@@ -378,6 +467,7 @@ namespace WaveHarmonic.Crest
}
}
#pragma warning disable format
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
@@ -393,29 +483,38 @@ namespace WaveHarmonic.Crest
{
OnBeginCameraRenderingLegacy(camera);
}
#pragma warning restore format
}
internal void OnEndCameraRendering(Camera camera)
{
_DoneChunkVisibility = false;
if (!WaterRenderer.ShouldRender(camera, Layer))
{
return;
}
// Our planar reflection camera must never render the surface.
if (camera == WaterReflections.CurrentCamera)
{
return;
}
if (RenderPipelineHelper.IsLegacy)
{
OnEndCameraRenderingLegacy(camera);
}
}
void InitializeProperties()
{
System.Array.Fill(NormalMapParameters, new Vector4(0, 0, 1, 0));
// Populate MPBs with defaults.
for (var index = 0; index < PerCascadeMPB.Length; index++)
{
var block = new MaterialPropertyBlock();
block.SetInteger(Lod.ShaderIDs.s_LodIndex, index);
block.SetFloat(ShaderIDs.s_ChunkFarNormalsWeight, 1f);
PerCascadeMPB[index] = block;
}
foreach (var chunk in Chunks)
{
PreviousObjectToWorld[chunk._SiblingIndex] = chunk.transform.localToWorldMatrix;
}
}
void WritePerCameraMaterialParameters(Camera camera)
{
if (Material == null)
@@ -424,7 +523,7 @@ namespace WaveHarmonic.Crest
}
// If no underwater, then no need for underwater surface.
if (!_Water.Underwater.Enabled)
if (!_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Volume) && _SurfaceSelfIntersectionFixMode == SurfaceSelfIntersectionFixMode.Automatic)
{
Shader.SetGlobalInteger(ShaderIDs.s_ForceUnderwater, (int)ForceFacing.AboveWater);
return;
@@ -442,14 +541,16 @@ namespace WaveHarmonic.Crest
var value = _SurfaceSelfIntersectionFixMode switch
{
SurfaceSelfIntersectionFixMode.On =>
height < -2f
!_Water._PerCameraHeightReady
? ForceFacing.None
: height < -2f
? ForceFacing.BelowWater
: height > 2f
? ForceFacing.AboveWater
: ForceFacing.None,
// Skip for portals as it is possible to see both sides of the surface at any position.
SurfaceSelfIntersectionFixMode.Automatic =>
_Water.Portaled
_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Portal) || !_Water._PerCameraHeightReady
? ForceFacing.None
: height < -2f
? ForceFacing.BelowWater
@@ -471,12 +572,55 @@ namespace WaveHarmonic.Crest
Rebuild();
}
if (_ForceRenderingOff)
{
return;
}
LoadCameraData(_Water.CurrentCamera);
Root.position = _Water.Position;
Root.localScale = new(_Water.Scale, 1f, _Water.Scale);
_PerCascadeMPB.Flip();
Root.gameObject.SetActive(!IsQuadMesh);
#if CREST_DEBUG
if (_Water._Debug._VisualizeData)
{
var material = _Water.Surface.VisualizeDataMaterial;
material.SetInteger(ShaderIDs.s_DataType, (int)_Water._Debug._VisualizeDataType);
material.SetBoolean(ShaderIDs.s_Saturate, _Water._Debug._VisualizeDataSaturate);
material.SetFloat(ShaderIDs.s_Exposure, _Water._Debug._VisualizeDataExposure);
material.SetFloat(ShaderIDs.s_Range, _Water._Debug._VisualizeDataRange);
}
#endif
if (Material != null)
{
// Cannot cache or receive the following on shader recompilation:
// Local keyword … comes from a different shader.
var keyword = Material.shader.keywordSpace.FindKeyword("_CREST_CUSTOM_MESH");
if (keyword.isValid)
{
Material.SetKeyword(keyword, IsQuadMesh);
}
}
WritePerCascadeInstanceData();
if (IsQuadMesh || _SupportCustomRenderers)
{
// For simple and custom meshes.
Shader.SetGlobalVectorArray(ShaderIDs.s_NormalMapParameters, NormalMapParameters);
}
if (IsQuadMesh)
{
LateUpdateQuadMesh();
return;
}
foreach (var chunk in Chunks)
{
chunk.UpdateMeshBounds(_Water, this);
@@ -506,60 +650,48 @@ namespace WaveHarmonic.Crest
{
var levels = _Water.LodLevels;
var texel = _Water.LodResolution * 0.25f / _Water._GeometryDownSampleFactor;
var mpbsCurrent = _PerCascadeMPB.Current;
var mpbsPrevious = _PerCascadeMPB.Previous(1);
// LOD 0
{
var mpb = mpbsCurrent[0];
if (_Water.WriteMotionVectors)
{
// NOTE: it may be more optimal to store in an array than fetching from MPB.
mpb.SetFloat(ShaderIDs.s_ChunkMeshScaleAlphaSource, mpbsPrevious[0].GetFloat(ShaderIDs.s_ChunkMeshScaleAlpha));
}
// Blend LOD 0 shape in/out to avoid pop, if scale could increase.
mpb.SetFloat(ShaderIDs.s_ChunkMeshScaleAlpha, _Water.ScaleCouldIncrease ? _Water.ViewerAltitudeLevelAlpha : 0f);
PerCascadeMPB[0].SetFloat(ShaderIDs.s_ChunkMeshScaleAlpha, _Water.ScaleCouldIncrease ? _Water.ViewerAltitudeLevelAlpha : 0f);
}
// LOD N
{
var mpb = mpbsCurrent[levels - 1];
// Blend furthest normals scale in/out to avoid pop, if scale could reduce.
mpb.SetFloat(ShaderIDs.s_ChunkFarNormalsWeight, _Water.ScaleCouldDecrease ? _Water.ViewerAltitudeLevelAlpha : 1f);
var weight = _Water.ScaleCouldDecrease ? _Water.ViewerAltitudeLevelAlpha : 1f;
PerCascadeMPB[levels - 1].SetFloat(ShaderIDs.s_ChunkFarNormalsWeight, weight);
NormalMapParameters[levels - 1] = new(0, 0, weight, 0);
}
for (var index = 0; index < levels; index++)
{
var mpbCurrent = mpbsCurrent[index];
var mpbPrevious = mpbsPrevious[index];
var mpb = PerCascadeMPB[index];
// geometry data
// compute grid size of geometry. take the long way to get there - make sure we land exactly on a power of two
// and not inherit any of the lossy-ness from lossyScale.
var scale = _Water._CascadeData.Current[index].x;
var scale = _Water.CascadeData.Current[index].x;
var width = scale / texel;
if (_Water.WriteMotionVectors)
{
// NOTE: it may be more optimal to store in an array than fetching from MPB.
mpbPrevious.SetFloat(ShaderIDs.s_ChunkGeometryGridWidthSource, mpbCurrent.GetFloat(ShaderIDs.s_ChunkGeometryGridWidth));
}
mpbCurrent.SetFloat(ShaderIDs.s_ChunkGeometryGridWidth, width);
mpb.SetFloat(ShaderIDs.s_ChunkGeometryGridWidth, width);
var mul = 1.875f; // fudge 1
var pow = 1.4f; // fudge 2
var texelWidth = width / _Water._GeometryDownSampleFactor;
mpbCurrent.SetVector(ShaderIDs.s_ChunkNormalScrollSpeed, new
var speed = new Vector2
(
Mathf.Pow(Mathf.Log(1f + 2f * texelWidth) * mul, pow),
Mathf.Pow(Mathf.Log(1f + 4f * texelWidth) * mul, pow),
0,
0
));
Mathf.Pow(Mathf.Log(1f + 4f * texelWidth) * mul, pow)
);
mpb.SetVector(ShaderIDs.s_ChunkNormalScrollSpeed, speed);
var normals = NormalMapParameters[index];
normals.x = speed.x;
normals.y = speed.y;
NormalMapParameters[index] = normals;
}
}
@@ -649,7 +781,7 @@ namespace WaveHarmonic.Crest
_CanSkipCulling = WaterBody.WaterBodies.Count == 0;
}
internal void Render(Camera camera, CommandBuffer buffer, Material material = null, int pass = 0, bool culled = false)
internal void Render(Camera camera, CommandBuffer buffer, Material material = null, int pass = 0, bool culled = false, MaterialPropertyBlock mpb = null)
{
var noMaterial = material == null;
@@ -658,6 +790,12 @@ namespace WaveHarmonic.Crest
return;
}
if (IsQuadMesh)
{
buffer.DrawMesh(Helpers.QuadMesh, Matrix4x4.TRS(Root.position, Quaternion.Euler(90f, 0, 0), new(10000, 10000, 1)), noMaterial ? Material : material, 0, shaderPass: pass, mpb);
return;
}
UpdateChunkVisibility(camera);
// Spends approx 0.2-0.3ms here on 2018 Dell XPS 15.
@@ -751,7 +889,11 @@ namespace WaveHarmonic.Crest
// Motion Vectors
partial class SurfaceRenderer
{
// Mostly to update the motion vector material only once.
bool _QueueMotionVectors;
bool QueueMotionVectors => _QueueMotionVectors && !IsQuadMesh;
Matrix4x4[] _PreviousObjectToWorld;
internal Matrix4x4[] PreviousObjectToWorld { get; private set; }
bool ShouldRenderMotionVectors(Camera camera)
{
@@ -799,7 +941,7 @@ namespace WaveHarmonic.Crest
{
var camera = cameras[i];
if (!WaterRenderer.ShouldRender(camera, _Layer))
if (!ShouldRender(camera))
{
continue;
}
@@ -817,7 +959,7 @@ namespace WaveHarmonic.Crest
void UpdateMotionVectorsMaterial(Material surface, ref Material motion)
{
if (!_QueueMotionVectors)
if (!QueueMotionVectors)
{
return;
}
@@ -855,4 +997,79 @@ namespace WaveHarmonic.Crest
motion.SetFloat(ShaderIDs.s_BuiltShadowCasterZTest, 1); // ZTest Never
}
}
partial class SurfaceRenderer
{
internal Dictionary<Camera, MaterialPropertyBlock[]> _PerCameraPerCascadeMPB = new();
internal Dictionary<Camera, Vector4[]> _PerCameraNormalMapParameters = new();
internal Dictionary<Camera, Matrix4x4[]> _PerCameraPreviousObjectToWorld = new();
void LoadCameraData(Camera camera)
{
if (_Water.IsSingleViewpointMode)
{
return;
}
if (!_PerCameraPerCascadeMPB.ContainsKey(camera))
{
PerCascadeMPB = new MaterialPropertyBlock[Lod.k_MaximumSlices];
_PerCameraPerCascadeMPB.Add(camera, PerCascadeMPB);
NormalMapParameters = new Vector4[Lod.k_MaximumSlices];
_PerCameraNormalMapParameters.Add(camera, NormalMapParameters);
PreviousObjectToWorld = new Matrix4x4[Chunks.Count];
_PerCameraPreviousObjectToWorld.Add(camera, PreviousObjectToWorld);
InitializeProperties();
}
else
{
PerCascadeMPB = _PerCameraPerCascadeMPB[camera];
NormalMapParameters = _PerCameraNormalMapParameters[camera];
PreviousObjectToWorld = _PerCameraPreviousObjectToWorld[camera];
}
}
internal void RemoveCameraData(Camera camera)
{
if (_PerCameraPerCascadeMPB.ContainsKey(camera))
{
_PerCameraPerCascadeMPB.Remove(camera);
_PerCameraNormalMapParameters.Remove(camera);
_PerCameraPreviousObjectToWorld.Remove(camera);
}
}
}
// Quad
partial class SurfaceRenderer
{
readonly Vector4[] _NormalMapParameters = new Vector4[Lod.k_MaximumSlices];
Vector4[] NormalMapParameters { get; set; }
void LateUpdateQuadMesh()
{
var scale = new Vector3(10000 * _Water.Scale, 10000 * _Water.Scale, 1);
var bounds = Helpers.QuadMesh.bounds;
bounds.Expand(scale);
Graphics.RenderMesh
(
new()
{
motionVectorMode = MotionVectorGenerationMode.Camera,
material = Material,
worldBounds = Root.TransformBounds(bounds),
layer = Layer,
shadowCastingMode = CastShadows ? ShadowCastingMode.On : ShadowCastingMode.Off,
lightProbeUsage = LightProbeUsage.Off,
reflectionProbeUsage = ReflectionProbeUsage.BlendProbesAndSkybox,
renderingLayerMask = _RendererTemplate != null ? _RendererTemplate.renderingLayerMask : 1,
},
Helpers.QuadMesh,
submeshIndex: 0,
Matrix4x4.TRS(Root.position, Quaternion.Euler(90f, 0, 0), scale)
);
UpdateMaterial(_Material, ref _MotionVectorMaterial);
}
}
}

View File

@@ -19,11 +19,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/WaterBodies.html")]
public sealed partial class WaterBody : ManagedBehaviour<WaterRenderer>
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Tooltip("Makes sure this water body is not clipped.\n\nIf clipping is enabled and set to clip everywhere by default, this option will register this water body to ensure its area does not get clipped.")]
[@GenerateAPI(name: "Clipped")]
[SerializeField]

View File

@@ -6,6 +6,7 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
@@ -455,11 +456,12 @@ namespace WaveHarmonic.Crest
var order = -lodCount + (patchTypes[i] == PatchType.Interior ? -1 : lodIndex);
var chunk = patch.AddComponent<WaterChunkRenderer>();
var mesh = meshData[(int)patchTypes[i]];
{
var mesh = meshData[(int)patchTypes[i]];
patch.AddComponent<MeshFilter>().sharedMesh = mesh;
var chunk = patch.AddComponent<WaterChunkRenderer>();
chunk._Water = water;
chunk._SortingOrder = order;
chunk._SiblingIndex = s_SiblingIndex++;
@@ -470,6 +472,8 @@ namespace WaveHarmonic.Crest
// be optimally sorted. We statically sort by LOD. Sub-sort is only done for LOD0,
// where interior tiles are placed first. Further sorting must be done dynamically.
tiles.Add(chunk);
chunk._DrawRenderBounds = water.Surface._Debug._DrawRendererBounds;
}
// Sorting order to stop unity drawing it back to front. Make the innermost four tiles draw first,
@@ -532,6 +536,14 @@ namespace WaveHarmonic.Crest
else
patch.transform.localRotation = Quaternion.FromToRotation(from, to);
}
// Pre-rotate bounds.
{
var bounds = mesh.bounds;
bounds = bounds.Rotate(chunk.transform.rotation);
chunk._LocalBounds = bounds;
chunk._LocalScale = chunk.transform.localScale.x;
}
}
}
}

View File

@@ -1,7 +1,6 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Collections.Generic;
using UnityEngine;
using WaveHarmonic.Crest.Internal;
@@ -9,12 +8,12 @@ namespace WaveHarmonic.Crest
{
interface IReportsHeight
{
bool ReportHeight(ref Rect bounds, ref float minimum, ref float maximum);
bool ReportHeight(WaterRenderer water, ref Rect bounds, ref float minimum, ref float maximum);
}
interface IReportsDisplacement
{
bool ReportDisplacement(ref Rect bounds, ref float horizontal, ref float vertical);
bool ReportDisplacement(WaterRenderer water, ref Rect bounds, ref float horizontal, ref float vertical);
}
/// <summary>
@@ -24,7 +23,7 @@ namespace WaveHarmonic.Crest
[AddComponentMenu("")]
#endif
[@ExecuteDuringEditMode]
sealed class WaterChunkRenderer : ManagedBehaviour<WaterRenderer>
sealed partial class WaterChunkRenderer : ManagedBehaviour<WaterRenderer>
{
[SerializeField]
internal bool _DrawRenderBounds = false;
@@ -46,7 +45,13 @@ namespace WaveHarmonic.Crest
internal Rect _UnexpandedBoundsXZ = new();
public Rect UnexpandedBoundsXZ => _UnexpandedBoundsXZ;
internal Bounds _LocalBounds;
internal float _LocalScale;
// WaterBody culling.
internal bool _Culled;
// Frustum visibility.
internal bool _Visible;
internal WaterRenderer _Water;
@@ -59,10 +64,8 @@ namespace WaveHarmonic.Crest
// contiguous surface.
internal bool _WaterDataHasBeenBound = true;
int _LodIndex = -1;
internal int _LodIndex = -1;
public static List<IReportsHeight> HeightReporters { get; } = new();
public static List<IReportsDisplacement> DisplacementReporters { get; } = new();
// There is a 1-frame delay with Initialized in edit mode due to setting
// enableInEditMode in EditorApplication.update. This only really affect this
@@ -73,7 +76,6 @@ namespace WaveHarmonic.Crest
_LodIndex = index;
Rend = renderer;
_Mesh = mesh;
_PreviousObjectToWorld = _CurrentObjectToWorld = transform.localToWorldMatrix;
_Transform = transform;
}
@@ -118,8 +120,9 @@ namespace WaveHarmonic.Crest
internal void OnLateUpdate()
{
_PreviousObjectToWorld = _CurrentObjectToWorld;
_PreviousObjectToWorld = _Water.Surface.PreviousObjectToWorld[_SiblingIndex];
_CurrentObjectToWorld = _Transform.localToWorldMatrix;
_Water.Surface.PreviousObjectToWorld[_SiblingIndex] = _CurrentObjectToWorld;
}
internal void RenderMotionVectors(SurfaceRenderer surface, Camera camera)
@@ -144,6 +147,7 @@ namespace WaveHarmonic.Crest
matProps = _MaterialPropertyBlock,
worldBounds = Rend.bounds,
layer = surface.Layer,
renderingLayerMask = (uint)surface.Layer,
receiveShadows = false,
shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off,
lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off,
@@ -158,37 +162,26 @@ namespace WaveHarmonic.Crest
{
s_UpdateMeshBoundsMarker.Begin(this);
var bounds = _Mesh.bounds;
var bounds = _LocalBounds;
if (WaterBody.WaterBodies.Count > 0)
bounds = ComputeBounds(_Transform, bounds);
_UnexpandedBoundsXZ = new(0, 0, bounds.size.x, bounds.size.z)
{
_UnexpandedBoundsXZ = ComputeBoundsXZ(_Transform, bounds);
}
center = bounds.center.XZ(),
};
bounds = ExpandBoundsForDisplacements(_Transform, bounds);
Rend.localBounds = bounds;
Rend.bounds = bounds;
s_UpdateMeshBoundsMarker.End();
}
public static Rect ComputeBoundsXZ(Transform transform, Bounds bounds)
{
// Since chunks are axis-aligned it is safe to rotate the bounds.
var center = transform.rotation * bounds.center * transform.lossyScale.x + transform.position;
var size = transform.rotation * bounds.size * transform.lossyScale.x;
// Rotation can make size negative.
return new(0, 0, Mathf.Abs(size.x), Mathf.Abs(size.z))
{
center = center.XZ(),
};
}
// Used by the water mask system if we need to render the water mask in situations
// where the water itself doesn't need to be rendered or has otherwise been disabled
internal void Bind()
{
_MaterialPropertyBlock = _Water.Surface._PerCascadeMPB.Current[_LodIndex];
_MaterialPropertyBlock = _Water.Surface.PerCascadeMPB[_LodIndex];
new PropertyWrapperMPB(_MaterialPropertyBlock).SetSHCoefficients(_Transform.position);
Rend.SetPropertyBlock(_MaterialPropertyBlock);
_WaterDataHasBeenBound = true;
@@ -197,6 +190,7 @@ namespace WaveHarmonic.Crest
void OnDestroy()
{
Helpers.Destroy(_Mesh);
_Mesh = null;
}
// Called when visible to a camera
@@ -207,6 +201,14 @@ namespace WaveHarmonic.Crest
return;
}
#if CREST_DEBUG
if (_Water._Debug._VisualizeData)
{
Rend.sharedMaterial = _Water.Surface.VisualizeDataMaterial;
MaterialOverridden = true;
}
#endif
if (!MaterialOverridden && Rend.sharedMaterial != _Water.Surface.Material)
{
Rend.sharedMaterial = _Water.Surface.Material;
@@ -217,13 +219,28 @@ namespace WaveHarmonic.Crest
{
Bind();
}
if (_DrawRenderBounds)
{
Rend.bounds.DebugDraw();
}
}
public Bounds ComputeBounds(Transform transform, Bounds bounds)
{
var extents = bounds.extents;
var center = bounds.center;
// Apply transform. Rotation already done.
var scale = _LocalScale * _Water.Scale;
extents.x *= scale;
extents.z *= scale;
center.x *= scale;
center.z *= scale;
center += transform.position;
bounds.center = center;
bounds.extents = extents;
return bounds;
}
// this is called every frame because the bounds are given in world space and depend on the transform scale, which
// can change depending on view altitude
public Bounds ExpandBoundsForDisplacements(Transform transform, Bounds bounds)
@@ -231,99 +248,53 @@ namespace WaveHarmonic.Crest
var extents = bounds.extents;
var center = bounds.center;
var scale = transform.lossyScale;
var rotation = transform.rotation;
var boundsPadding = _Water.MaximumHorizontalDisplacement;
var expandXZ = boundsPadding / scale.x;
var boundsY = _Water.MaximumVerticalDisplacement;
var rect = _UnexpandedBoundsXZ;
// Extend the kinematic bounds slightly to give room for dynamic waves.
if (_Water._DynamicWavesLod.Enabled)
{
boundsY += 5f;
}
// Extend bounds by global waves.
extents.x += expandXZ;
extents.y += boundsY;
extents.z += expandXZ;
// Get XZ bounds. Doing this manually bypasses updating render bounds call.
Rect rect;
{
var p1 = transform.position;
var p2 = rotation * new Vector3(center.x, 0f, center.z);
var s1 = scale;
var s2 = rotation * (extents.XNZ(0f) * 2f);
rect = new(0, 0, Mathf.Abs(s1.x * s2.x), Mathf.Abs(s1.z * s2.z))
{
center = new(p1.x + p2.x, p1.z + p2.z)
};
var settings = _Water.DynamicWavesLod.Settings;
extents.x += settings._HorizontalDisplace;
extents.y += settings._VerticalDisplacementCullingContributions;
extents.z += settings._HorizontalDisplace;
}
// Extend bounds by local waves.
{
var totalHorizontal = 0f;
var totalVertical = 0f;
var horizontal = 0f;
var vertical = 0f;
foreach (var reporter in DisplacementReporters)
foreach (var (key, input) in AnimatedWavesLod.s_Inputs)
{
var horizontal = 0f;
var vertical = 0f;
if (reporter.ReportDisplacement(ref rect, ref horizontal, ref vertical))
{
totalHorizontal += horizontal;
totalVertical += vertical;
}
input.DisplacementReporter?.ReportDisplacement(_Water, ref rect, ref horizontal, ref vertical);
}
boundsPadding = totalHorizontal;
expandXZ = boundsPadding / scale.x;
boundsY = totalVertical;
extents.x += expandXZ;
extents.y += boundsY;
extents.z += expandXZ;
extents.x += horizontal;
extents.y += vertical;
extents.z += horizontal;
}
// Expand and offset bounds by height.
{
var minimumWaterLevelBounds = 0f;
var maximumWaterLevelBounds = 0f;
var minimum = 0f;
var maximum = 0f;
foreach (var reporter in HeightReporters)
foreach (var (key, input) in LevelLod.s_Inputs)
{
var minimum = 0f;
var maximum = 0f;
if (reporter.ReportHeight(ref rect, ref minimum, ref maximum))
{
minimumWaterLevelBounds = Mathf.Max(minimumWaterLevelBounds, Mathf.Abs(Mathf.Min(minimum, _Water.SeaLevel) - _Water.SeaLevel));
maximumWaterLevelBounds = Mathf.Max(maximumWaterLevelBounds, Mathf.Abs(Mathf.Max(maximum, _Water.SeaLevel) - _Water.SeaLevel));
}
input.HeightReporter?.ReportHeight(_Water, ref rect, ref minimum, ref maximum);
}
minimumWaterLevelBounds *= 0.5f;
maximumWaterLevelBounds *= 0.5f;
extents.y += Mathf.Abs((minimum - maximum) * 0.5f);
boundsY = minimumWaterLevelBounds + maximumWaterLevelBounds;
extents.y += boundsY;
bounds.extents = extents;
var offset = maximumWaterLevelBounds - minimumWaterLevelBounds;
var offset = Mathf.Lerp(minimum, maximum, 0.5f);
center.y += offset;
bounds.center = center;
}
return bounds;
}
// Get XZ bounds. Doing this manually bypasses updating render bounds call.
bounds.center = center;
bounds.extents = extents;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void InitStatics()
{
HeightReporters.Clear();
DisplacementReporters.Clear();
return bounds;
}
}

View File

@@ -0,0 +1,117 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if d_UnityURP
#if UNITY_6000_0_OR_NEWER
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
namespace WaveHarmonic.Crest
{
partial class WaterReflections
{
CopyDepthRenderPass _CopyTargetsRenderPass;
void CaptureTargetDepth(ScriptableRenderContext context, Camera camera)
{
if (camera != ReflectionCamera)
{
return;
}
if (!RenderPipelineHelper.IsUniversal)
{
return;
}
#if URP_COMPATIBILITY_MODE
#if !UNITY_6000_4_OR_NEWER
if (GraphicsSettings.GetRenderPipelineSettings<RenderGraphSettings>().enableRenderCompatibilityMode)
{
return;
}
#endif
#endif
_CopyTargetsRenderPass ??= new(this);
var renderer = camera.GetUniversalAdditionalCameraData().scriptableRenderer;
renderer.EnqueuePass(_CopyTargetsRenderPass);
}
sealed class CopyDepthRenderPass : ScriptableRenderPass
{
readonly WaterReflections _Renderer;
RTHandle _Wrapper;
class CopyPassData
{
public TextureHandle _Source;
public TextureHandle _Target;
public int _Slice;
}
public CopyDepthRenderPass(WaterReflections renderer)
{
_Renderer = renderer;
renderPassEvent = RenderPassEvent.AfterRendering;
}
public void Dispose()
{
_Wrapper?.Release();
_Wrapper = null;
}
public override void RecordRenderGraph(RenderGraph graph, ContextContainer frame)
{
var resources = frame.Get<UniversalResourceData>();
var source = resources.cameraDepth;
if (!source.IsValid())
{
return;
}
// Create a wrapper. Does not appear to be anything heavy in here.
_Wrapper ??= RTHandles.Alloc(_Renderer._DepthTexture);
_Wrapper.SetRenderTexture(_Renderer._DepthTexture);
var texture = graph.ImportTexture(_Wrapper);
using var builder = graph.AddUnsafePass<CopyPassData>("Crest.CopyDepth", out var data);
data._Source = source;
data._Target = graph.ImportTexture(_Wrapper);
data._Slice = _Renderer._ActiveSlice;
builder.UseTexture(data._Source, AccessFlags.Read);
builder.UseTexture(data._Target, AccessFlags.Write);
// Unity's AddCopyPass cannot handle this it seems.
builder.SetRenderFunc((CopyPassData data, UnsafeGraphContext context) =>
{
RTHandle source = data._Source;
RTHandle target = data._Target;
// Just in case. Planar Reflections will work mostly without it.
if (source.rt == null)
{
return;
}
if (source.rt.graphicsFormat == target.rt.graphicsFormat && source.rt.depthStencilFormat == target.rt.depthStencilFormat)
{
context.cmd.m_WrappedCommandBuffer.CopyTexture(source.rt, 0, 0, target.rt, data._Slice, 0);
}
});
}
}
}
}
#endif
#endif

View File

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

View File

@@ -10,6 +10,7 @@ using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using UnityEngine.Rendering.Universal;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
@@ -36,13 +37,8 @@ namespace WaveHarmonic.Crest
/// Renders reflections for water. Currently on planar reflections.
/// </summary>
[Serializable]
public sealed partial class WaterReflections
public sealed partial class WaterReflections : Versioned
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[@Space(10)]
[@Label("Enable")]
@@ -55,7 +51,7 @@ namespace WaveHarmonic.Crest
[@Heading("Capture")]
[Tooltip("What side of the water surface to render planar reflections for.")]
[@GenerateAPI(name: "ReflectionSide")]
[@GenerateAPI(Setter.Custom, name: "ReflectionSide")]
[@DecoratedField, SerializeField]
internal WaterReflectionSide _Mode = WaterReflectionSide.Above;
@@ -69,10 +65,11 @@ namespace WaveHarmonic.Crest
[@Delayed, SerializeField]
int _Resolution = 256;
[Tooltip("Whether to render to the viewer camera only.\n\nWhen disabled, reflections will render for all cameras rendering the water layer, which currently this prevents Refresh Rate from working. Enabling will unlock the Refresh Rate heading.")]
[Tooltip("Overscan amount to capture off-screen content.\n\nRenders the reflections at a larger viewport size to capture off-screen content when the surface reflects off-screen. This avoids a category of artifacts - especially when looking down. This can be expensive, as the value is a multiplier to the capture size.")]
[@Range(1, 2)]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal bool _RenderOnlySingleCamera;
[SerializeField]
float _Overscan = 1.5f;
[@Space(10)]
@@ -88,7 +85,7 @@ namespace WaveHarmonic.Crest
#pragma warning disable 414
[Tooltip("Disables shadows.")]
[@GenerateAPI]
[@GenerateAPI(Setter.Custom)]
[@DecoratedField, SerializeField]
bool _DisableShadows = true;
#pragma warning restore 414
@@ -103,11 +100,6 @@ namespace WaveHarmonic.Crest
[@DecoratedField, SerializeField]
bool _Stencil = false;
[Tooltip("Whether to allow MSAA.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _AllowMSAA = false;
[@Space(10)]
[Tooltip("Overrides global quality settings.")]
@@ -144,11 +136,11 @@ namespace WaveHarmonic.Crest
[@Heading("Refresh Rate")]
[Tooltip("Refresh reflection every x frames (one is every frame)")]
[@Predicated(nameof(_RenderOnlySingleCamera))]
[@Enable(nameof(_RenderOnlySingleCamera))]
[@DecoratedField, SerializeField]
int _RefreshPerFrames = 1;
[@Predicated(nameof(_RenderOnlySingleCamera))]
[@Enable(nameof(_RenderOnlySingleCamera))]
[@DecoratedField, SerializeField]
int _FrameRefreshOffset = 0;
@@ -162,19 +154,34 @@ namespace WaveHarmonic.Crest
bool _UseObliqueMatrix = true;
[Tooltip("Planar relfections using an oblique frustum for better performance.\n\nThis can cause depth issues for TIRs, especially near the surface.")]
[@Predicated(nameof(_UseObliqueMatrix))]
[@Enable(nameof(_UseObliqueMatrix))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _NonObliqueNearSurface;
[Tooltip("If within this distance from the surface, disable the oblique matrix.")]
[@Predicated(nameof(_NonObliqueNearSurface))]
[@Predicated(nameof(_UseObliqueMatrix))]
[@Enable(nameof(_NonObliqueNearSurface))]
[@Enable(nameof(_UseObliqueMatrix))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _NonObliqueNearSurfaceThreshold = 0.05f;
[@Heading("Advanced")]
[Tooltip("Whether to render to the viewer camera only.\n\nWhen disabled, reflections will render for all cameras rendering the water layer, which currently this prevents Refresh Rate from working. Enabling will unlock the Refresh Rate heading.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal bool _RenderOnlySingleCamera;
[Tooltip("Renderer index for the reflection camera.")]
[@Show(RenderPipeline.Universal)]
[@Minimum(0)]
[@GenerateAPI(Setter.Custom)]
[@DecoratedField]
[@SerializeField]
int _RendererIndex;
[@Space(10)]
[@DecoratedField, SerializeField]
@@ -189,66 +196,91 @@ namespace WaveHarmonic.Crest
[Tooltip("Rendering reflections per-camera requires recursive rendering. Check this toggle if experiencing issues. The other downside without it is a one-frame delay.")]
[@DecoratedField, SerializeField]
internal bool _DisableRecursiveRendering;
[Tooltip("Whether to create a context more compatible for planar reflections camera. Try enabling this if you are getting exceptions.")]
[@Show(RenderPipeline.Universal)]
[@DecoratedField]
[SerializeField]
internal bool _ForceCompatibility;
}
/// <summary>
/// What side of the water surface to render planar reflections for.
/// </summary>
public WaterReflectionSide Mode { get => _Mode; set => _Mode = value; }
static class ShaderIDs
{
public static int s_ReflectionTexture = Shader.PropertyToID("_Crest_ReflectionTexture");
public static int s_ReflectionColorTexture = Shader.PropertyToID("_Crest_ReflectionColorTexture");
public static int s_ReflectionDepthTexture = Shader.PropertyToID("_Crest_ReflectionDepthTexture");
public static int s_ReflectionPositionNormal = Shader.PropertyToID("_Crest_ReflectionPositionNormal");
}
public static readonly int s_ReflectionMatrixIVP = Shader.PropertyToID("_Crest_ReflectionMatrixIVP");
public static readonly int s_ReflectionMatrixV = Shader.PropertyToID("_Crest_ReflectionMatrixV");
public static readonly int s_Crest_ReflectionOverscan = Shader.PropertyToID("_Crest_ReflectionOverscan");
// Checked in underwater to filter cameras.
internal static Camera CurrentCamera { get; private set; }
public static readonly int s_PlanarReflectionsApplySmoothness = Shader.PropertyToID("_Crest_PlanarReflectionsApplySmoothness");
}
internal WaterRenderer _Water;
internal UnderwaterRenderer _UnderWater;
RenderTexture _ReflectionTexture;
internal RenderTexture ReflectionTexture => _ReflectionTexture;
bool _ApplySmoothness;
RenderTexture _ColorTexture;
RenderTexture _DepthTexture;
internal RenderTexture ColorTexture => _ColorTexture;
internal RenderTexture DepthTexture => _DepthTexture;
readonly Vector4[] _ReflectionPositionNormal = new Vector4[2];
readonly Matrix4x4[] _ReflectionMatrixIVP = new Matrix4x4[2];
readonly Matrix4x4[] _ReflectionMatrixV = new Matrix4x4[2];
internal int _ActiveSlice;
Camera _CameraViewpoint;
Skybox _CameraViewpointSkybox;
Camera _CameraReflections;
Skybox _CameraReflectionsSkybox;
internal Camera ReflectionCamera => _CameraReflections;
int RefreshPerFrames => _RenderOnlySingleCamera ? _RefreshPerFrames : 1;
long _LastRefreshOnFrame = -1;
internal bool SupportsRecursiveRendering =>
#if !UNITY_6000_0_OR_NEWER
// HDRP cannot recursive render for 2022.
!RenderPipelineHelper.IsHighDefinition &&
#endif
!_Debug._DisableRecursiveRendering;
internal bool SupportsRecursiveRendering => _Water.SupportsRecursiveRendering && !_Debug._DisableRecursiveRendering;
readonly float[] _CullDistances = new float[32];
Texture _CameraDepthTexture;
/// <summary>
/// Invoked when the reflection camera is created.
/// </summary>
public static Action<Camera> OnCameraAdded { get; set; }
bool RequireTemporaryTargets =>
#if UNITY_6000_0_OR_NEWER && d_UnityURP
// As of Unity 6 we can write directly to a slice for URP.
!RenderPipelineHelper.IsUniversal &&
#endif
true;
internal void OnEnable()
{
_CameraViewpoint = _Water.Viewer;
_CameraViewpointSkybox = _CameraViewpoint.GetComponent<Skybox>();
// We initialized here previously to fix the first frame being black, but could not
// replicate anymore.
// This is called also called every frame, but was required here as there was a
// black reflection for a frame without this earlier setup call.
CreateWaterObjects(_CameraViewpoint);
#if d_UnityURP
#if UNITY_6000_0_OR_NEWER
RenderPipelineManager.beginCameraRendering -= CaptureTargetDepth;
RenderPipelineManager.beginCameraRendering += CaptureTargetDepth;
#endif
#endif
}
internal void OnDisable()
{
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, Texture2D.blackTexture);
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionColorTexture, Texture2D.blackTexture);
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionDepthTexture, Texture2D.blackTexture);
#if d_UnityURP
#if UNITY_6000_0_OR_NEWER
RenderPipelineManager.beginCameraRendering -= CaptureTargetDepth;
#endif
#endif
}
internal void OnDestroy()
@@ -259,18 +291,30 @@ namespace WaveHarmonic.Crest
_CameraReflections = null;
}
if (_ReflectionTexture)
if (_ColorTexture)
{
_ReflectionTexture.Release();
Helpers.Destroy(_ReflectionTexture);
_ReflectionTexture = null;
_ColorTexture.Release();
Helpers.Destroy(_ColorTexture);
_ColorTexture = null;
}
if (_DepthTexture)
{
_DepthTexture.Release();
Helpers.Destroy(_DepthTexture);
_DepthTexture = null;
}
}
bool ShouldRender(Camera camera)
internal bool ShouldRender(Camera camera)
{
if (!_Enabled)
{
return false;
}
// If no surface, then do not execute the reflection camera.
if (!WaterRenderer.ShouldRender(camera, _Water.Surface.Layer))
if (!_Water._ActiveModules.HasFlag(WaterRenderer.ActiveModules.Surface))
{
return false;
}
@@ -278,7 +322,7 @@ namespace WaveHarmonic.Crest
// This method could be executed twice: once by the camera rendering the surface,
// and once again by the planar reflection camera. For the latter, we do not want
// to proceed or infinite recursion. For safety.
if (camera == CurrentCamera)
if (camera == _CameraReflections)
{
return false;
}
@@ -294,11 +338,6 @@ namespace WaveHarmonic.Crest
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (!ShouldRender(camera))
{
return;
}
if (SupportsRecursiveRendering)
{
// This option only valid for recursive, otherwise, it is always single camera.
@@ -314,18 +353,44 @@ namespace WaveHarmonic.Crest
if (camera == _CameraViewpoint)
{
// TODO: Emit an event instead so WBs can listen.
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, _ReflectionTexture);
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionColorTexture, _ColorTexture);
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionDepthTexture, _DepthTexture);
}
}
internal void OnEndReflectionCameraRendering(Camera camera)
{
if (camera == ReflectionCamera)
{
// Appears to be the only reasonable way to get camera depth separately for SRPs.
_CameraDepthTexture = Shader.GetGlobalTexture(Crest.ShaderIDs.Unity.s_CameraDepthTexture);
}
}
internal void OnEndCameraRendering(Camera camera)
{
if (!ShouldRender(camera))
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionColorTexture, Texture2D.blackTexture);
}
internal void LateUpdate()
{
// Check if enabled for at least one material every frame.
_ApplySmoothness = false;
CheckSurfaceMaterial(_Water.Surface.Material);
foreach (var wb in WaterBody.WaterBodies)
{
CheckSurfaceMaterial(wb._Material);
}
if (SupportsRecursiveRendering)
{
return;
}
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, Texture2D.blackTexture);
// Passing a struct.
LateUpdate(new());
}
internal void LateUpdate(ScriptableRenderContext context)
@@ -376,19 +441,18 @@ namespace WaveHarmonic.Crest
UpdateCameraModes();
ForceDistanceCulling(_FarClipPlane);
_CameraReflections.targetTexture = _ReflectionTexture;
// TODO: Do not do this every frame.
if (_Mode != WaterReflectionSide.Both)
{
Helpers.ClearRenderTexture(_ReflectionTexture, Color.clear, depth: false);
Helpers.ClearRenderTexture(_ColorTexture, Color.clear, depth: true);
Helpers.ClearRenderTexture(_DepthTexture, Color.clear, depth: true);
}
var isActive = _Water.Surface.Root.gameObject.activeSelf;
// We do not want the water plane when rendering planar reflections.
_Water.Surface.Root.gameObject.SetActive(false);
CurrentCamera = _CameraReflections;
// Optionally disable pixel lights for reflection/refraction
var oldPixelLightCount = QualitySettings.pixelLightCount;
if (_DisablePixelLights)
@@ -436,8 +500,7 @@ namespace WaveHarmonic.Crest
_QualitySettingsOverride.Restore();
CurrentCamera = null;
_Water.Surface.Root.gameObject.SetActive(true);
_Water.Surface.Root.gameObject.SetActive(isActive);
// Remember this frame as last refreshed.
_LastRefreshOnFrame = Time.renderedFrameCount;
@@ -446,41 +509,60 @@ namespace WaveHarmonic.Crest
void Render(ScriptableRenderContext context)
{
#if UNITY_6000_0_OR_NEWER && d_UnityURP
_CameraReflections.targetTexture = _ReflectionTexture;
#else
var descriptor = _ReflectionTexture.descriptor;
descriptor.dimension = TextureDimension.Tex2D;
descriptor.volumeDepth = 1;
descriptor.useMipMap = false;
// No need to clear, as camera clears using the skybox.
var target = RenderTexture.GetTemporary(descriptor);
_CameraReflections.targetTexture = target;
#endif
var colorTarget = _ColorTexture;
var depthTarget = _DepthTexture;
if (RequireTemporaryTargets)
{
var descriptor = _ColorTexture.descriptor;
descriptor.dimension = TextureDimension.Tex2D;
descriptor.volumeDepth = 1;
descriptor.useMipMap = false;
// No need to clear, as camera clears using the skybox.
colorTarget = RenderTexture.GetTemporary(descriptor);
if (RenderPipelineHelper.IsLegacy)
{
descriptor = _DepthTexture.descriptor;
descriptor.dimension = TextureDimension.Tex2D;
descriptor.volumeDepth = 1;
descriptor.useMipMap = false;
// No need to clear, as camera clears using the skybox.
depthTarget = RenderTexture.GetTemporary(descriptor);
}
}
if (RenderPipelineHelper.IsLegacy)
{
// Not documented, but does not work for SRPs.
_CameraReflections.SetTargetBuffers(colorTarget.colorBuffer, depthTarget.depthBuffer);
}
else
{
_CameraReflections.targetTexture = colorTarget;
}
if (_Mode != WaterReflectionSide.Below)
{
_ReflectionPositionNormal[0] = ComputeHorizonPositionAndNormal(_CameraReflections, _Water.SeaLevel, 0.05f, false);
if (_UnderWater._Enabled)
{
// Disable underwater layer. It is the only way to exclude probes.
_CameraReflections.cullingMask = _Layers & ~(1 << _UnderWater.Layer);
}
_ActiveSlice = 0;
RenderCamera(context, _CameraReflections, Vector3.up, false, 0);
#if !(UNITY_6000_0_OR_NEWER && d_UnityURP)
Graphics.CopyTexture(target, 0, 0, _ReflectionTexture, 0, 0);
#endif
CopyTargets(colorTarget, depthTarget, 0);
_ReflectionPositionNormal[0] = ComputeHorizonPositionAndNormal(_CameraReflections, _Water.SeaLevel, 0.5f / _Resolution, false);
_CameraReflections.ResetProjectionMatrix();
}
if (_Mode != WaterReflectionSide.Above)
{
_ReflectionPositionNormal[1] = ComputeHorizonPositionAndNormal(_CameraReflections, _Water.SeaLevel, -0.05f, true);
if (_UnderWater._Enabled)
{
// Enable underwater layer.
@@ -489,22 +571,34 @@ namespace WaveHarmonic.Crest
_CameraReflections.depthTextureMode = DepthTextureMode.Depth;
}
_ActiveSlice = 1;
RenderCamera(context, _CameraReflections, Vector3.down, _NonObliqueNearSurface, 1);
#if !(UNITY_6000_0_OR_NEWER && d_UnityURP)
Graphics.CopyTexture(target, 0, 0, _ReflectionTexture, 1, 0);
#endif
CopyTargets(colorTarget, depthTarget, 1);
_ReflectionPositionNormal[1] = ComputeHorizonPositionAndNormal(_CameraReflections, _Water.SeaLevel, -0.05f, true);
_CameraReflections.ResetProjectionMatrix();
}
#if !(UNITY_6000_0_OR_NEWER && d_UnityURP)
RenderTexture.ReleaseTemporary(target);
if (RequireTemporaryTargets)
{
RenderTexture.ReleaseTemporary(colorTarget);
if (RenderPipelineHelper.IsLegacy) RenderTexture.ReleaseTemporary(depthTarget);
}
#if !d_Crest_DisablePlanarReflectionApplySmoothness
if (_ApplySmoothness)
{
// We are only using mip-maps if applying smoothness/roughness.
_ColorTexture.GenerateMips();
}
#endif
_ReflectionTexture.GenerateMips();
Shader.SetGlobalVectorArray(ShaderIDs.s_ReflectionPositionNormal, _ReflectionPositionNormal);
Shader.SetGlobalMatrixArray(ShaderIDs.s_ReflectionMatrixIVP, _ReflectionMatrixIVP);
Shader.SetGlobalMatrixArray(ShaderIDs.s_ReflectionMatrixV, _ReflectionMatrixV);
}
void RenderCamera(ScriptableRenderContext context, Camera camera, Vector3 planeNormal, bool nonObliqueNearSurface, int slice)
@@ -517,10 +611,9 @@ namespace WaveHarmonic.Crest
var viewpoint = _CameraViewpoint.transform;
if (offset == 0f && viewpoint.position.y == planePosition.y)
{
// Minor offset to prevent "Screen position out of view frustum". Smallest number
// to work with both above and below. Smallest number to work with both above and
// below. Could be BIRP only.
offset = 0.00001f;
// Minor offset to prevent "Screen position out of view frustum". Needs to scale
// with distance from center.
offset = viewpoint.position.magnitude >= 15000f ? 0.01f : 0.001f;
}
}
@@ -539,7 +632,12 @@ namespace WaveHarmonic.Crest
if (_UseObliqueMatrix && (!nonObliqueNearSurface || Mathf.Abs(_CameraViewpoint.transform.position.y - planePosition.y) > _NonObliqueNearSurfaceThreshold))
{
camera.projectionMatrix = _CameraViewpoint.CalculateObliqueMatrix(clipPlane);
var matrix = _CameraViewpoint.CalculateObliqueMatrix(clipPlane);
// Overscan.
var overscan = 1f - (_Overscan - 1f) * 0.5f;
matrix[0, 0] *= overscan;
matrix[1, 1] *= overscan;
camera.projectionMatrix = matrix;
}
// Set custom culling matrix from the current camera
@@ -550,9 +648,12 @@ namespace WaveHarmonic.Crest
camera.transform.eulerAngles = new(-euler.x, euler.y, euler.z);
camera.cullingMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
_ReflectionMatrixV[slice] = camera.worldToCameraMatrix;
_ReflectionMatrixIVP[slice] = (GL.GetGPUProjectionMatrix(camera.projectionMatrix, true) * camera.worldToCameraMatrix).inverse;
if (SupportsRecursiveRendering)
{
Helpers.RenderCamera(camera, context, slice);
Helpers.RenderCamera(camera, context, slice, _Debug._ForceCompatibility);
}
else
{
@@ -560,6 +661,35 @@ namespace WaveHarmonic.Crest
}
}
void CopyTargets(Texture color, Texture depth, int slice)
{
if (RequireTemporaryTargets)
{
Graphics.CopyTexture(color, 0, 0, 0, 0, _Resolution, _Resolution, _ColorTexture, slice, 0, 0, 0);
}
if (!RenderPipelineHelper.IsLegacy)
{
depth = _CameraDepthTexture;
}
if (Rendering.IsRenderGraph)
{
return;
}
// This can change between depth and R32 based on settings.
if (depth != null && depth.graphicsFormat != _DepthTexture.graphicsFormat)
{
RecreateDepth(depth);
}
if (depth != null && depth.width >= _Resolution)
{
Graphics.CopyTexture(depth, 0, 0, 0, 0, _Resolution, _Resolution, _DepthTexture, slice, 0, 0, 0);
}
}
/// <summary>
/// Limit render distance for reflection camera for first 32 layers
/// </summary>
@@ -611,6 +741,7 @@ namespace WaveHarmonic.Crest
{
// Destroy otherwise skybox will not render if empty.
Helpers.Destroy(_CameraViewpointSkybox);
_CameraViewpointSkybox = null;
}
}
@@ -623,54 +754,125 @@ namespace WaveHarmonic.Crest
_CameraReflections.orthographic = _CameraViewpoint.orthographic;
_CameraReflections.fieldOfView = _CameraViewpoint.fieldOfView;
_CameraReflections.orthographicSize = _CameraViewpoint.orthographicSize;
_CameraReflections.allowMSAA = _AllowMSAA;
_CameraReflections.allowMSAA = false;
_CameraReflections.aspect = _CameraViewpoint.aspect;
_CameraReflections.useOcclusionCulling = !_DisableOcclusionCulling && _CameraViewpoint.useOcclusionCulling;
_CameraReflections.depthTextureMode = _CameraViewpoint.depthTextureMode;
// Overscan
{
_CameraReflections.usePhysicalProperties = _Overscan > 1f;
var baseSensor = new Vector2(36f, 24f);
var focal = (baseSensor.y * 0.5f) / Mathf.Tan(_CameraViewpoint.fieldOfView * 0.5f * Mathf.Deg2Rad);
var overscan = 1f - (_Overscan - 1f) * 0.5f;
_CameraReflections.sensorSize = baseSensor / overscan;
_CameraReflections.focalLength = focal;
Shader.SetGlobalFloat(ShaderIDs.s_Crest_ReflectionOverscan, overscan);
}
}
void RecreateDepth(Texture depth)
{
if (_DepthTexture != null && _DepthTexture.IsCreated())
{
_DepthTexture.Release();
_DepthTexture.descriptor = depth.GetDescriptor();
}
else
{
_DepthTexture = new(depth.GetDescriptor());
}
_DepthTexture.name = "_Crest_ReflectionDepth";
_DepthTexture.width = _DepthTexture.height = _Resolution;
_DepthTexture.isPowerOfTwo = true;
_DepthTexture.useMipMap = false;
_DepthTexture.autoGenerateMips = false;
_DepthTexture.filterMode = FilterMode.Point;
_DepthTexture.volumeDepth = 2;
_DepthTexture.dimension = TextureDimension.Tex2DArray;
_DepthTexture.Create();
}
// On-demand create any objects we need for water
void CreateWaterObjects(Camera currentCamera)
{
var format = _HDR ? RenderTextureFormat.ARGBHalf : RenderTextureFormat.ARGB32;
var stencil = _Stencil ? 24 : 16;
// We cannot exclude stencil for URP, as the depth texture format always has it.
var colorFormat = Rendering.GetDefaultColorFormat(_HDR);
var depthFormat = Rendering.GetDefaultDepthFormat(_Stencil || RenderPipelineHelper.IsUniversal);
// Reflection render texture
if (!_ReflectionTexture || _ReflectionTexture.width != _Resolution || _ReflectionTexture.format != format || _ReflectionTexture.depth != stencil)
if (!_ColorTexture || _ColorTexture.width != _Resolution || _ColorTexture.graphicsFormat != colorFormat || _ColorTexture.depthStencilFormat != depthFormat)
{
if (_ReflectionTexture)
if (_ColorTexture)
{
Helpers.Destroy(_ReflectionTexture);
Helpers.Destroy(_ColorTexture);
Helpers.Destroy(_DepthTexture);
}
Debug.Assert(SystemInfo.SupportsRenderTextureFormat(format), "Crest: The graphics device does not support the render texture format " + format.ToString());
_ReflectionTexture = new(_Resolution, _Resolution, stencil, format)
var descriptor = new RenderTextureDescriptor(_Resolution, _Resolution)
{
name = "_Crest_WaterReflection",
isPowerOfTwo = true,
dimension = TextureDimension.Tex2DArray,
volumeDepth = 2,
depthStencilFormat = depthFormat,
msaaSamples = 1,
useMipMap = false,
};
_ColorTexture = new(descriptor)
{
name = "_Crest_ReflectionColor",
graphicsFormat = colorFormat,
isPowerOfTwo = true,
#if !d_Crest_DisablePlanarReflectionApplySmoothness
useMipMap = true,
#endif
autoGenerateMips = false,
filterMode = FilterMode.Trilinear,
};
_ReflectionTexture.Create();
_ColorTexture.Create();
_DepthTexture = new(descriptor)
{
name = "_Crest_ReflectionDepth",
graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.None,
isPowerOfTwo = true,
useMipMap = false,
autoGenerateMips = false,
filterMode = FilterMode.Point,
};
if (RenderPipelineHelper.IsHighDefinition)
{
_DepthTexture.graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R32_SFloat;
_DepthTexture.depthStencilFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.None;
}
_DepthTexture.Create();
}
var create = _CameraReflections == null;
// Camera for reflection
if (!_CameraReflections)
if (create)
{
var go = new GameObject("_Crest_WaterReflectionCamera");
go.transform.SetParent(_Water.Container.transform, worldPositionStays: true);
_CameraReflections = go.AddComponent<Camera>();
_CameraReflections.enabled = false;
_CameraReflections.cullingMask = _Layers;
_CameraReflections.cameraType = CameraType.Reflection;
_CameraReflections.backgroundColor = Color.clear;
if (RenderPipelineHelper.IsLegacy)
{
#pragma warning disable IDE0079
#pragma warning disable CS0618 // Type or member is obsolete
_CameraReflections.gameObject.AddComponent<FlareLayer>();
#pragma warning restore CS0618 // Type or member is obsolete
#pragma warning restore IDE0079
}
#if d_UnityHDRP
@@ -690,15 +892,34 @@ namespace WaveHarmonic.Crest
if (RenderPipelineHelper.IsUniversal)
{
var additionalCameraData = _CameraReflections.gameObject.AddComponent<UniversalAdditionalCameraData>();
additionalCameraData.renderShadows = !_DisableShadows;
additionalCameraData.requiresColorTexture = false;
additionalCameraData.requiresDepthTexture = false;
additionalCameraData.requiresDepthTexture = true;
}
#endif
OnCameraAdded?.Invoke(_CameraReflections);
_UpdateCamera = true;
}
_CameraReflections.gameObject.hideFlags = _Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
if (_UpdateCamera)
{
_CameraReflections.gameObject.hideFlags = _Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
var additionalCameraData = _CameraReflections.GetUniversalAdditionalCameraData();
additionalCameraData.SetRenderer(_RendererIndex);
additionalCameraData.renderShadows = !_DisableShadows; // Does not appear to work!
additionalCameraData.requiresColorTexture = _Mode != WaterReflectionSide.Above; // or incur assertions
}
#endif
_UpdateCamera = false;
}
if (create)
{
OnCameraAdded?.Invoke(_CameraReflections);
}
}
// Given position/normal of the plane, calculates plane in camera space.
@@ -856,14 +1077,34 @@ namespace WaveHarmonic.Crest
positionWS.Dispose();
}
normal = normal.normalized;
if (flipped)
{
normal = -normal;
}
else if (position.y == 0f)
{
// Sample anywhere if pointing downwards.
position.y = 1f;
}
return new(position.x, position.y, normal.x, normal.y);
}
void CheckSurfaceMaterial(Material material)
{
if (material == null)
{
return;
}
if (!_ApplySmoothness)
{
_ApplySmoothness = material.GetBoolean(ShaderIDs.s_PlanarReflectionsApplySmoothness);
}
}
void SetEnabled(bool previous, bool current)
{
if (previous == current) return;
@@ -871,6 +1112,26 @@ namespace WaveHarmonic.Crest
if (_Enabled) OnEnable(); else OnDisable();
}
bool _UpdateCamera;
void SetReflectionSide(WaterReflectionSide previous, WaterReflectionSide current)
{
if (previous == current) return;
_UpdateCamera = true;
}
void SetDisableShadows(bool previous, bool current)
{
if (previous == current) return;
_UpdateCamera = true;
}
void SetRendererIndex(int previous, int current)
{
if (previous == current) return;
_UpdateCamera = true;
}
#if UNITY_EDITOR
[@OnChange]
void OnChange(string propertyPath, object previousValue)
@@ -880,8 +1141,37 @@ namespace WaveHarmonic.Crest
case nameof(_Enabled):
SetEnabled((bool)previousValue, _Enabled);
break;
case nameof(_Debug) + "." + nameof(DebugFields._ShowHiddenObjects):
_UpdateCamera = true;
break;
case nameof(_Mode):
SetReflectionSide((WaterReflectionSide)previousValue, _Mode);
break;
case nameof(_DisableShadows):
SetDisableShadows((bool)previousValue, _DisableShadows);
break;
case nameof(_RendererIndex):
SetRendererIndex((int)previousValue, _RendererIndex);
break;
}
}
#endif
}
partial class WaterReflections
{
// MSAA would require separate textures to resolve to. Not worth the expense.
[HideInInspector]
[Obsolete("MSAA for the planar reflection camera is no longer supported. This setting will be ignored.")]
[Tooltip("Whether to allow MSAA.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _AllowMSAA;
/// <summary>
/// What side of the water surface to render planar reflections for.
/// </summary>
[Obsolete("Please use ReflectionSide instead.")]
public WaterReflectionSide Mode { get => _Mode; set => _Mode = value; }
}
}

View File

@@ -12,11 +12,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/TimeProviders.html#supporting-pause")]
public sealed partial class CustomTimeProvider : TimeProvider
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[Tooltip("Freeze progression of time. Only works properly in Play mode.")]
[@GenerateAPI]
[SerializeField]
@@ -24,22 +19,24 @@ namespace WaveHarmonic.Crest
[Tooltip("Whether to override the water simulation time.")]
[@GenerateAPI]
[SerializeField]
[@InlineToggle]
[@SerializeField]
bool _OverrideTime = false;
[Tooltip("The time override value.")]
[@Predicated(nameof(_OverrideTime))]
[@Enable(nameof(_OverrideTime))]
[@GenerateAPI(name: "TimeOverride")]
[@DecoratedField, SerializeField]
float _Time = 0f;
[Tooltip("Whether to override the water simulation time.\n\nThis in particular affects dynamic elements of the simulation like the foam simulation and the ripple simulation.")]
[@GenerateAPI]
[SerializeField]
[@InlineToggle]
[@SerializeField]
bool _OverrideDeltaTime = false;
[Tooltip("The delta time override value.")]
[@Predicated(nameof(_OverrideDeltaTime))]
[@Enable(nameof(_OverrideDeltaTime))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _DeltaTime = 0f;

View File

@@ -13,11 +13,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/TimeProviders.html#timelines-and-cutscenes")]
public sealed partial class CutsceneTimeProvider : TimeProvider
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
#if d_ModuleUnityDirector
[Tooltip("Playable Director to take time from.")]
[@GenerateAPI(symbol: "d_ModuleUnityDirector")]

View File

@@ -8,44 +8,7 @@ namespace WaveHarmonic.Crest
/// </summary>
sealed class DefaultTimeProvider : ITimeProvider
{
public float Time
{
get
{
#if UNITY_EDITOR
if (UnityEngine.Application.isPlaying)
{
return UnityEngine.Time.time;
}
else
{
return WaterRenderer.EditorTime;
}
#else
return UnityEngine.Time.time;
#endif
}
}
public float Delta
{
get
{
#if UNITY_EDITOR
if (UnityEngine.Application.isPlaying)
{
return UnityEngine.Time.deltaTime;
}
else
{
return WaterRenderer.EditorDeltaTime;
}
#else
return UnityEngine.Time.deltaTime;
#endif
;
}
}
public float Time => UnityEngine.Time.time;
public float Delta => UnityEngine.Time.deltaTime;
}
}

View File

@@ -17,11 +17,6 @@ namespace WaveHarmonic.Crest
[@HelpURL("Manual/TimeProviders.html#network-synchronisation")]
public sealed class NetworkedTimeProvider : TimeProvider
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
/// <summary>
/// If Time.time on this client is 1.5s ahead of the shared/server Time.time, set
/// this field to -1.5.

View File

@@ -21,6 +21,7 @@ namespace WaveHarmonic.Crest
public static readonly int s_TextureSize = Shader.PropertyToID("_Crest_TextureSize");
public static readonly int s_TexturePosition = Shader.PropertyToID("_Crest_TexturePosition");
public static readonly int s_TextureRotation = Shader.PropertyToID("_Crest_TextureRotation");
public static readonly int s_TextureResolution = Shader.PropertyToID("_Crest_TextureResolution");
public static readonly int s_Multiplier = Shader.PropertyToID("_Crest_Multiplier");
public static readonly int s_FeatherWidth = Shader.PropertyToID("_Crest_FeatherWidth");
public static readonly int s_NegativeValues = Shader.PropertyToID("_Crest_NegativeValues");

View File

@@ -14,22 +14,24 @@ namespace WaveHarmonic.Crest
{
[Tooltip("Whether to override the LOD bias.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
[@InlineToggle]
[@SerializeField]
internal bool _OverrideLodBias;
[Tooltip("Overrides the LOD bias for meshes.\n\nHighest quality is infinity.")]
[@Predicated(nameof(_OverrideLodBias))]
[@Enable(nameof(_OverrideLodBias))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal float _LodBias;
[Tooltip("Whether to override the maximum LOD level.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
[@InlineToggle]
[@SerializeField]
internal bool _OverrideMaximumLodLevel;
[Tooltip("Overrides the maximum LOD level.\n\nHighest quality is zero.")]
[@Predicated(nameof(_OverrideMaximumLodLevel))]
[@Enable(nameof(_OverrideMaximumLodLevel))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal int _MaximumLodLevel;
@@ -37,11 +39,12 @@ namespace WaveHarmonic.Crest
[Tooltip("Whether to override the terrain pixel error.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
[@InlineToggle]
[@SerializeField]
internal bool _OverrideTerrainPixelError;
[Tooltip("Overrides the pixel error value for terrains.\n\nHighest quality is zero.")]
[@Predicated(nameof(_OverrideTerrainPixelError))]
[@Enable(nameof(_OverrideTerrainPixelError))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal float _TerrainPixelError;

View File

@@ -5,11 +5,14 @@ using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("WaveHarmonic.Crest")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Integration.Gaia")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Shared.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Boats")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Editor")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Examples")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Ripples")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Waterfall")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.Samples.Submarine")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.CPUQueries")]
[assembly: InternalsVisibleTo("WaveHarmonic.Crest.CPUQueries.Editor")]

View File

@@ -99,11 +99,22 @@ namespace WaveHarmonic.Crest
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)]
abstract class Decorator : PropertyAttribute { }
[Conditional(Symbols.k_UnityEditor)]
sealed class Order : Decorator
{
public enum Placement { Heading, Below, Above }
public Order(string target, Placement placement = Placement.Heading) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Layer : Decorator { }
[Conditional(Symbols.k_UnityEditor)]
sealed class Stripped : Decorator { }
sealed class Stripped : Decorator
{
public enum Style { None, PlatformTab, }
public Stripped(Style style = Style.None, bool indent = false) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Delayed : Decorator { }
@@ -111,6 +122,18 @@ namespace WaveHarmonic.Crest
[Conditional(Symbols.k_UnityEditor)]
sealed class Disabled : Decorator { }
[Conditional(Symbols.k_UnityEditor)]
sealed class Required : Attribute { }
[Conditional(Symbols.k_UnityEditor)]
sealed class PlatformTabs : Attribute { }
[Conditional(Symbols.k_UnityEditor)]
sealed class CustomField : Attribute { }
[Conditional(Symbols.k_UnityEditor)]
sealed class CustomLabel : Attribute { }
[Conditional(Symbols.k_UnityEditor)]
sealed class AttachMaterialEditor : Attribute
{
@@ -153,25 +176,25 @@ namespace WaveHarmonic.Crest
{
[Flags]
public enum Clamp { None = 0, Minimum = 1, Maximum = 2, Both = Minimum | Maximum }
public Range(float minimum, float maximum, Clamp clamp = Clamp.Both, float scale = 1f, bool delayed = false, int step = 0, bool power = false) {}
public Range(float minimum, float maximum, Clamp clamp = Clamp.Both, float scale = 1f, bool delayed = false, int step = 0, bool power = false) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Minimum : Decorator
{
public Minimum(float minimum) {}
public Minimum(float minimum) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Maximum : Decorator
{
public Maximum(float maximum) {}
public Maximum(float maximum) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class WarnIfAbove : Decorator
{
public WarnIfAbove(float maximum) {}
public WarnIfAbove(float maximum) { }
}
[Conditional(Symbols.k_UnityEditor)]
@@ -207,12 +230,47 @@ namespace WaveHarmonic.Crest
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Predicated : Decorator
sealed class Show : Decorator
{
public Predicated(Type type, string member, bool inverted = false, bool hide = false) { }
public Predicated(Type type, bool inverted = false, bool hide = false) { }
public Predicated(string property, bool inverted = false, object disableValue = null, bool hide = false) { }
public Predicated(RenderPipeline rp, bool inverted = false, bool hide = false) { }
public Show(Type type, string member, object value) { }
public Show(Type type, string member) { }
public Show(Type type) { }
public Show(string property) { }
public Show(string property, object value) { }
public Show(RenderPipeline rp) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Hide : Decorator
{
public Hide(Type type, string member, object value) { }
public Hide(Type type, string member) { }
public Hide(Type type) { }
public Hide(string property) { }
public Hide(string property, object value) { }
public Hide(RenderPipeline rp) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Enable : Decorator
{
public Enable(Type type, string member, object value) { }
public Enable(Type type, string member) { }
public Enable(Type type) { }
public Enable(string property) { }
public Enable(string property, object value) { }
public Enable(RenderPipeline rp) { }
}
[Conditional(Symbols.k_UnityEditor)]
sealed class Disable : Decorator
{
public Disable(Type type, string member, object value) { }
public Disable(Type type, string member) { }
public Disable(Type type) { }
public Disable(string property) { }
public Disable(string property, object value) { }
public Disable(RenderPipeline rp) { }
}
[Conditional(Symbols.k_UnityEditor)]

View File

@@ -1,9 +1,10 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
#if UNITY_EDITOR
using MonoBehaviour = WaveHarmonic.Crest.Internal.EditorBehaviour;
#else
using MonoBehaviour = UnityEngine.MonoBehaviour;
@@ -14,7 +15,7 @@ namespace WaveHarmonic.Crest.Internal
/// <summary>
/// Implements logic to smooth out Unity's wrinkles.
/// </summary>
public abstract class CustomBehaviour : MonoBehaviour
public abstract partial class CustomBehaviour : MonoBehaviour
{
bool _AfterStart;
@@ -71,11 +72,46 @@ namespace WaveHarmonic.Crest.Internal
[InitializeOnEnterPlayMode]
static void OnEnterPlayModeInEditor(EnterPlayModeOptions options)
{
foreach (var @object in FindObjectsByType<CustomBehaviour>(FindObjectsInactive.Include, FindObjectsSortMode.None))
foreach (var @object in Helpers.FindObjectsByType<CustomBehaviour>(FindObjectsInactive.Include))
{
@object._AfterStart = false;
}
}
#endif
}
partial class CustomBehaviour : ISerializationCallbackReceiver
{
#pragma warning disable 414
[@SerializeField, @HideInInspector]
private protected int _Version;
#pragma warning restore 414
private protected virtual int Version => 0;
private protected CustomBehaviour()
{
// Sets the default version. Overriden by serialized field above.
_Version = Version;
}
private protected virtual void OnMigrate()
{
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
if (_Version < Version)
{
OnMigrate();
_Version = Version;
}
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
}
}
}

View File

@@ -0,0 +1,50 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
namespace WaveHarmonic.Crest.Internal
{
/// <summary>
/// Implements logic to smooth out Unity's wrinkles.
/// </summary>
public abstract partial class CustomScriptableObject : ScriptableObject
{
}
partial class CustomScriptableObject : ISerializationCallbackReceiver
{
#pragma warning disable 414
[@SerializeField, @HideInInspector]
private protected int _Version;
#pragma warning restore 414
private protected virtual int Version => 0;
private protected CustomScriptableObject()
{
// Sets the default version. Overriden by serialized field above.
_Version = Version;
}
private protected virtual void OnMigrate()
{
}
void ISerializationCallbackReceiver.OnBeforeSerialize()
{
if (_Version < Version)
{
OnMigrate();
_Version = Version;
}
}
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
}
}
}

View File

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

View File

@@ -98,7 +98,7 @@ namespace WaveHarmonic.Crest.Internal
// When singleton, destroy instance objects.
if (enableInEditMode && attribute._Options.HasFlag(ExecuteDuringEditMode.Options.Singleton) &&
FindObjectsByType(GetType(), FindObjectsSortMode.None).Length > 1)
Helpers.FindObjectsByType(GetType()).Length > 1)
{
enableInEditMode = false;
EditorApplication.update -= InternalDestroyNonSaveables;

View File

@@ -61,7 +61,7 @@ namespace WaveHarmonic.Crest.Internal
internal static void AfterScriptReload()
{
Instance = FindFirstObjectByType<T>();
Instance = FindAnyObjectByType<T>();
}
}

View File

@@ -10,7 +10,7 @@ using UnityEngine.Rendering.HighDefinition;
using UnityEngine.Rendering.Universal;
using WaveHarmonic.Crest.Internal;
#if !UNITY_2023_2_OR_NEWER
#if !UNITY_6000_0_OR_NEWER
using GraphicsFormatUsage = UnityEngine.Experimental.Rendering.FormatUsage;
#endif
@@ -21,18 +21,6 @@ namespace WaveHarmonic.Crest
/// </summary>
static partial class Helpers
{
// Adapted from:
// Packages/com.unity.render-pipelines.universal/Runtime/UniversalRenderer.cs
#if UNITY_SWITCH || UNITY_ANDROID || UNITY_EMBEDDED_LINUX || UNITY_QNX
internal const GraphicsFormat k_DepthStencilFormat = GraphicsFormat.D24_UNorm_S8_UInt;
internal const int k_DepthBufferBits = 24;
internal const DepthBits k_DepthBits = DepthBits.Depth24;
#else
internal const GraphicsFormat k_DepthStencilFormat = GraphicsFormat.D32_SFloat_S8_UInt;
internal const int k_DepthBufferBits = 32;
internal const DepthBits k_DepthBits = DepthBits.Depth32;
#endif
public static class ShaderIDs
{
public static readonly int s_MainTexture = Shader.PropertyToID("_Utility_MainTexture");
@@ -92,6 +80,24 @@ namespace WaveHarmonic.Crest
return result != 0 ? result : 1;
}
public static Object[] FindObjectsByType(System.Type type, FindObjectsInactive inactive = FindObjectsInactive.Exclude)
{
#if UNITY_6000_4_OR_NEWER
return Object.FindObjectsByType(type, inactive);
#else
return Object.FindObjectsByType(type, inactive, FindObjectsSortMode.None);
#endif
}
public static T[] FindObjectsByType<T>(FindObjectsInactive inactive = FindObjectsInactive.Exclude) where T : Object
{
#if UNITY_6000_4_OR_NEWER
return Object.FindObjectsByType<T>(inactive);
#else
return Object.FindObjectsByType<T>(inactive, FindObjectsSortMode.None);
#endif
}
static Vector2Int CalculateResolution(Vector2 resolution, int maximum)
{
// Enforce maximum and scale to maintain aspect ratio.
@@ -138,8 +144,17 @@ namespace WaveHarmonic.Crest
return (T)System.Attribute.GetCustomAttribute(type, typeof(T));
}
/// <inheritdoc cref="UnityEngine.WaitForEndOfFrame"/>
/// <remarks>
/// Does not work in batch mode or with the scene view.
/// </remarks>
public static WaitForEndOfFrame WaitForEndOfFrame { get; } = new();
public static float Fmod(float x, float y)
{
return x - y * (float)System.Math.Truncate(x / y);
}
// Taken from:
// https://github.com/Unity-Technologies/Graphics/blob/871df5563d88e1ba778c82a43f39c9afc95368e6/Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl#L1149-L1152
// Z buffer to linear 0..1 depth (0 at camera position, 1 at far plane).
@@ -374,6 +389,16 @@ namespace WaveHarmonic.Crest
return mask == (mask | (1 << layer));
}
// https://docs.unity3d.com/6000.3/Documentation/Manual/WebGPU-limitations.html
public static bool IsWebGPU =>
#if UNITY_6000_0_OR_NEWER
SystemInfo.graphicsDeviceType is GraphicsDeviceType.WebGPU;
#else
false;
#endif
public static bool RequiresCustomClear => IsWebGPU || Application.platform == RuntimePlatform.PS5;
// R16G16B16A16_SFloat appears to be the most compatible format.
// https://docs.unity3d.com/Manual/class-TextureImporterOverride.html#texture-compression-support-platforms
// https://learn.microsoft.com/en-us/windows/win32/direct3d12/typed-unordered-access-view-loads#supported-formats-and-api-calls
@@ -386,10 +411,44 @@ namespace WaveHarmonic.Crest
&& SystemInfo.SupportsRandomWriteOnRenderTextureFormat(rtFormat);
}
static GraphicsFormat GetWebGPUTextureFormat(GraphicsFormat format)
{
// WebGPU is very limited in format usage - especially read-write.
return GraphicsFormatUtility.GetComponentCount(format) switch
{
1 => GraphicsFormat.R32_SFloat,
_ => GraphicsFormat.R32G32B32A32_SFloat,
};
}
internal static readonly GraphicsFormatUsage s_DataGraphicsFormatUsage =
// Ensures a non compressed format is returned. This is a prerequisite for random
// write, but not random write itself according to a dubious comment in the
// Graphics repository. Search UUM-91313.
GraphicsFormatUsage.LoadStore |
// All these textures are sampled at some point.
GraphicsFormatUsage.Sample |
// Always use linear filtering.
GraphicsFormatUsage.Linear;
internal static GraphicsFormat GetCompatibleTextureFormat(GraphicsFormat format, GraphicsFormatUsage usage, string label, bool randomWrite = false)
{
var result = SystemInfo.GetCompatibleFormat(format, usage);
var useFallback = result == GraphicsFormat.None;
var isMetal = GraphicsDeviceType.Metal == SystemInfo.graphicsDeviceType;
#if CREST_DISABLE_PLATFORM_RTFORMAT_OVERRIDES
isMetal = false;
#endif
#if !UNITY_6000_0_OR_NEWER
// Weird bug on macOS where unknown format is returned, but it is really the same
// as the input (R32G32B32A32_SFloat).
if (isMetal && (int)result == 89)
{
result = GraphicsFormat.R32G32B32A32_SFloat;
}
#endif
#if CREST_DEBUG_LOG_FORMAT_CHANGES
if (useFallback)
@@ -402,29 +461,57 @@ namespace WaveHarmonic.Crest
}
#endif
// NOTE: Disabling for now. RenderTextureFormat is a subset of GraphicsFormat and
// there is not always an equivalent.
// if (!useFallback && randomWrite && !SupportsRandomWriteOnRenderTextureFormat(result))
// {
// Debug.Log($"Crest: The graphics device does not support the render texture format {result} with random read/write. Will attempt to use fallback.");
// useFallback = true;
// }
#if !CREST_DISABLE_RANDOMWRITE_CHECK
// Metal will return false for any two channel texture, as per the below link, but
// they work without issue. Lets trust the API, but only for other platforms.
// https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf
if (!isMetal && !useFallback && randomWrite && !SupportsRandomWriteOnRenderTextureFormat(result))
{
#if CREST_DEBUG_LOG_FORMAT_CHANGES
Debug.Log($"Crest: The graphics device does not support the render texture format {result} with random read/write. Will attempt to use fallback. ({label})");
#endif
useFallback = true;
}
#endif
#if CREST_DEBUG_LOG_FORMAT_CHANGES
// Check if fallback is compatible before using it.
if (useFallback && format == s_FallbackGraphicsFormat)
{
Debug.Log($"Crest: Fallback {s_FallbackGraphicsFormat} is not supported on this device. Please inform us. ({label})");
useFallback = false;
Debug.Log($"Crest: Fallback {s_FallbackGraphicsFormat} is not supported on this device. This may be a false positive. Please inform us if you have any issues. ({label})");
}
#endif
if (useFallback)
{
result = s_FallbackGraphicsFormat;
}
#if !CREST_DISABLE_PLATFORM_RTFORMAT_OVERRIDES
if (randomWrite && IsWebGPU)
{
// Pass the requested format otherwise GetCompatibleFormat may change the component
// count which we need to pick the right format.
result = GetWebGPUTextureFormat(format);
}
#endif
return result;
}
public static GraphicsFormat GetCompatibleTextureFormat(GraphicsFormat format, bool randomWrite)
{
#if !CREST_DISABLE_PLATFORM_RTFORMAT_OVERRIDES
if (randomWrite && IsWebGPU)
{
format = GetWebGPUTextureFormat(format);
}
#endif
return format;
}
public static void SetGlobalKeyword(string keyword, bool enabled)
{
if (enabled)
@@ -711,12 +798,12 @@ namespace WaveHarmonic.Crest
public static bool GetGlobalBoolean(int id)
{
return Shader.GetGlobalInteger(id) == 1;
return Shader.GetGlobalFloat(id) != 0f;
}
public static void SetGlobalBoolean(int id, bool value)
{
Shader.SetGlobalInteger(id, value ? 1 : 0);
Shader.SetGlobalFloat(id, value ? 1f : 0f);
}
#if d_UnityURP
@@ -731,6 +818,12 @@ namespace WaveHarmonic.Crest
internal static ScriptableRendererData[] UniversalRendererData(UniversalRenderPipelineAsset asset) =>
(ScriptableRendererData[])s_RenderDataListField.GetValue(asset);
#if UNITY_EDITOR
static readonly System.Type s_PlayModeViewType = typeof(UnityEditor.EditorWindow).Assembly.GetType("UnityEditor.PlayModeView");
static readonly FieldInfo s_RenderingViewField = s_PlayModeViewType.GetField("s_RenderingView", BindingFlags.Static | BindingFlags.NonPublic);
static readonly FieldInfo s_ShowGizmosField = s_PlayModeViewType.GetField("m_ShowGizmos", BindingFlags.NonPublic | BindingFlags.Instance);
#endif
internal static int GetRendererIndex(Camera camera)
{
var rendererIndex = (int)s_RendererIndex.GetValue(camera.GetUniversalAdditionalCameraData());
@@ -760,6 +853,92 @@ namespace WaveHarmonic.Crest
return false;
}
internal static void UniversalRenderCamera(ScriptableRenderContext context, Camera camera, int slice)
{
#if UNITY_EDITOR
// Fixes when using Render Graph:
// Attempting to render to a depth only surface with no dummy color attachment
var sceneView = UnityEditor.SceneView.currentDrawingSceneView;
var drawGizmos = false;
if (sceneView != null)
{
drawGizmos = sceneView.drawGizmos;
sceneView.drawGizmos = false;
}
var gameView = s_RenderingViewField.GetValue(null);
if (gameView != null)
{
drawGizmos = (bool)s_ShowGizmosField.GetValue(gameView);
s_ShowGizmosField.SetValue(gameView, false);
}
#endif
#if UNITY_6000_0_OR_NEWER
// SingleCameraRequest does not render the full camera stack, thus should exclude
// overlays which is likely desirable. Alternative approach worth investigating:
// https://docs.unity3d.com/6000.0/Documentation/Manual/urp/User-Render-Requests.html
// Setting destination silences a warning if Opaque Texture enabled.
s_RenderSingleCameraRequest.destination = camera.targetTexture;
s_RenderSingleCameraRequest.slice = slice;
UnityEngine.Rendering.RenderPipeline.SubmitRenderRequest(camera, s_RenderSingleCameraRequest);
#else
#pragma warning disable CS0618 // Type or member is obsolete
UniversalRenderPipeline.RenderSingleCamera(context, camera);
#pragma warning restore CS0618 // Type or member is obsolete
#endif
#if UNITY_EDITOR
if (sceneView != null)
{
sceneView.drawGizmos = drawGizmos;
}
if (gameView != null)
{
s_ShowGizmosField.SetValue(gameView, drawGizmos);
}
#endif
}
internal static void UniversalRenderCamera(ScriptableRenderContext context, Camera camera, int slice, bool noRenderFeatures)
{
// Get this every time as it could change.
var renderers = (ScriptableRendererData[])s_RenderDataListField.GetValue(UniversalRenderPipeline.asset);
var renderer = (UniversalRendererData)renderers[GetRendererIndex(camera)];
// On Unity 6.3, got exceptions if this was auto with SSAO enabled.
var safe = !noRenderFeatures && renderer.intermediateTextureMode == IntermediateTextureMode.Always;
if (!safe)
{
foreach (var feature in renderer.rendererFeatures)
{
// Null exception reported here. Might be null due to missing render features
if (feature == null) continue;
s_RenderFeatureActiveStates.Add(feature.isActive);
feature.SetActive(false);
}
}
UniversalRenderCamera(context, camera, slice);
if (!safe)
{
var index = 0;
foreach (var feature in renderer.rendererFeatures)
{
if (feature == null) continue;
feature.SetActive(s_RenderFeatureActiveStates[index++]);
}
s_RenderFeatureActiveStates.Clear();
}
}
internal static void RenderCameraWithoutCustomPasses(Camera camera)
{
// Get this every time as it could change.
@@ -791,24 +970,12 @@ namespace WaveHarmonic.Crest
static readonly UnityEngine.Rendering.RenderPipeline.StandardRequest s_RenderStandardRequest = new();
public static void RenderCamera(Camera camera, ScriptableRenderContext context, int slice)
public static void RenderCamera(Camera camera, ScriptableRenderContext context, int slice, bool noRenderFeatures = false)
{
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
#if UNITY_6000_0_OR_NEWER
// SingleCameraRequest does not render the full camera stack, thus should exclude
// overlays which is likely desirable. Alternative approach worth investigating:
// https://docs.unity3d.com/6000.0/Documentation/Manual/urp/User-Render-Requests.html
// Setting destination silences a warning if Opaque Texture enabled.
s_RenderSingleCameraRequest.destination = camera.targetTexture;
s_RenderSingleCameraRequest.slice = slice;
UnityEngine.Rendering.RenderPipeline.SubmitRenderRequest(camera, s_RenderSingleCameraRequest);
#else
#pragma warning disable CS0618 // Type or member is obsolete
UniversalRenderPipeline.RenderSingleCamera(context, camera);
#pragma warning restore CS0618 // Type or member is obsolete
#endif
UniversalRenderCamera(context, camera, slice, noRenderFeatures);
return;
}
#endif
@@ -894,7 +1061,7 @@ namespace WaveHarmonic.Crest
namespace Internal
{
static class Extensions
static partial class Extensions
{
// Swizzle
public static Vector2 XZ(this Vector3 v) => new(v.x, v.z);
@@ -1003,6 +1170,18 @@ namespace WaveHarmonic.Crest
return GeometryUtility.CalculateBounds(s_BoundsPoints, transform.localToWorldMatrix);
}
public static Bounds Rotate(this Bounds bounds, Quaternion rotation)
{
var center = rotation * bounds.center;
// Rotation each axis separately gets closer to Unity's, but the difference is only
// epsilon in magnitude.
var extents = rotation * bounds.extents * 2f;
// Rotation can produce negatives.
return new(center, new(Mathf.Abs(extents.x), Mathf.Abs(extents.y), Mathf.Abs(extents.z)));
}
public static bool IntersectsXZ(this Bounds a, Bounds b)
{
return a.min.x <= b.max.x && a.max.x >= b.min.x &&
@@ -1083,24 +1262,17 @@ namespace WaveHarmonic.Crest
public static bool GetBoolean(this Material material, int id)
{
return (material.HasInteger(id) ? material.GetInteger(id) : material.GetInt(id)) != 0;
return material.GetFloat(id) != 0f;
}
public static void SetBoolean(this Material material, int id, bool value)
{
if (material.HasInteger(id))
{
material.SetInteger(id, value ? 1 : 0);
}
else
{
material.SetInt(id, value ? 1 : 0);
}
material.SetFloat(id, value ? 1f : 0f);
}
public static void SetGlobalBoolean(this CommandBuffer buffer, int id, bool value)
{
buffer.SetGlobalInteger(id, value ? 1 : 0);
buffer.SetGlobalFloat(id, value ? 1f : 0f);
}
public static bool IsEmpty(this UnityEngine.Events.UnityEvent @event)
@@ -1112,6 +1284,30 @@ namespace WaveHarmonic.Crest
{
return @event.GetPersistentEventCount() == 0;
}
public static bool Encapsulates(this Rect r1, Rect r2)
{
return r1.Contains(r2.min) && r1.Contains(r2.max);
}
}
static partial class Extensions
{
#if !UNITY_6000_3_OR_NEWER
internal static int GetEntityId(this Object @this)
{
return @this.GetInstanceID();
}
#endif
internal static ulong GetRawSceneHandle(this UnityEngine.SceneManagement.Scene @this)
{
#if UNITY_6000_4_OR_NEWER
return @this.handle.GetRawData();
#else
return (ulong)@this.handle;
#endif
}
}
}

View File

@@ -25,7 +25,7 @@ namespace WaveHarmonic.Crest
public void SetVectorArray(int param, Vector4[] value) => Commands.SetGlobalVectorArray(param, value);
public void SetMatrix(int param, Matrix4x4 value) => Commands.SetGlobalMatrix(param, value);
public void SetInteger(int param, int value) => Commands.SetGlobalInteger(param, value);
public void SetBoolean(int param, bool value) => Commands.SetGlobalInteger(param, value ? 1 : 0);
public void SetBoolean(int param, bool value) => Commands.SetGlobalFloat(param, value ? 1f : 0f);
public void GetBlock() { }
public void SetBlock() { }
@@ -73,7 +73,7 @@ namespace WaveHarmonic.Crest
public void SetVectorArray(int param, Vector4[] value) => Commands.SetGlobalVectorArray(param, value);
public void SetMatrix(int param, Matrix4x4 value) => Commands.SetGlobalMatrix(param, value);
public void SetInteger(int param, int value) => Commands.SetGlobalInteger(param, value);
public void SetBoolean(int param, bool value) => Commands.SetGlobalInteger(param, value ? 1 : 0);
public void SetBoolean(int param, bool value) => Commands.SetGlobalFloat(param, value ? 1f : 0f);
public void GetBlock() { }
public void SetBlock() { }

View File

@@ -23,6 +23,11 @@ namespace WaveHarmonic.Crest
void SetBlock();
}
interface IPropertyWrapperVariants : IPropertyWrapper
{
void SetKeyword(in LocalKeyword keyword, bool value);
}
static class PropertyWrapperConstants
{
internal const string k_NoShaderMessage = "Cannot create required material because shader <i>{0}</i> could not be found or loaded."
@@ -40,7 +45,7 @@ namespace WaveHarmonic.Crest
public void SetVectorArray(int param, Vector4[] value) => Buffer.SetGlobalVectorArray(param, value);
public void SetMatrix(int param, Matrix4x4 value) => Buffer.SetGlobalMatrix(param, value);
public void SetInteger(int param, int value) => Buffer.SetGlobalInteger(param, value);
public void SetBoolean(int param, bool value) => Buffer.SetGlobalInteger(param, value ? 1 : 0);
public void SetBoolean(int param, bool value) => Buffer.SetGlobalFloat(param, value ? 1f : 0f);
public void GetBlock() { }
public void SetBlock() { }
@@ -65,14 +70,13 @@ namespace WaveHarmonic.Crest
public void SetVectorArray(int param, Vector4[] value) => PropertyBlock.SetVectorArray(param, value);
public void SetMatrix(int param, Matrix4x4 value) => PropertyBlock.SetMatrix(param, value);
public void SetInteger(int param, int value) => PropertyBlock.SetInteger(param, value);
public void SetBoolean(int param, bool value) => PropertyBlock.SetInteger(param, value ? 1 : 0);
public void SetBoolean(int param, bool value) => PropertyBlock.SetFloat(param, value ? 1f : 0f);
public void GetBlock() => Renderer.GetPropertyBlock(PropertyBlock);
public void SetBlock() => Renderer.SetPropertyBlock(PropertyBlock);
}
[System.Serializable]
readonly struct PropertyWrapperMaterial : IPropertyWrapper
readonly struct PropertyWrapperMaterial : IPropertyWrapperVariants
{
public Material Material { get; }
@@ -97,7 +101,7 @@ namespace WaveHarmonic.Crest
public void SetVectorArray(int param, Vector4[] value) => Material.SetVectorArray(param, value);
public void SetMatrix(int param, Matrix4x4 value) => Material.SetMatrix(param, value);
public void SetInteger(int param, int value) => Material.SetInteger(param, value);
public void SetBoolean(int param, bool value) => Material.SetInteger(param, value ? 1 : 0);
public void SetBoolean(int param, bool value) => Material.SetFloat(param, value ? 1f : 0f);
public void GetBlock() { }
public void SetBlock() { }
@@ -117,14 +121,13 @@ namespace WaveHarmonic.Crest
public void SetVectorArray(int param, Vector4[] value) => MaterialPropertyBlock.SetVectorArray(param, value);
public void SetMatrix(int param, Matrix4x4 value) => MaterialPropertyBlock.SetMatrix(param, value);
public void SetInteger(int param, int value) => MaterialPropertyBlock.SetInteger(param, value);
public void SetBoolean(int param, bool value) => MaterialPropertyBlock.SetInteger(param, value ? 1 : 0);
public void SetBoolean(int param, bool value) => MaterialPropertyBlock.SetFloat(param, value ? 1f : 0f);
public void GetBlock() { }
public void SetBlock() { }
}
[System.Serializable]
readonly struct PropertyWrapperCompute : IPropertyWrapper
readonly struct PropertyWrapperCompute : IPropertyWrapperVariants
{
readonly CommandBuffer _Buffer;
readonly ComputeShader _Shader;
@@ -140,7 +143,8 @@ namespace WaveHarmonic.Crest
public void SetFloat(int param, float value) => _Buffer.SetComputeFloatParam(_Shader, param, value);
public void SetFloatArray(int param, float[] value) => _Buffer.SetGlobalFloatArray(param, value);
public void SetInteger(int param, int value) => _Buffer.SetComputeIntParam(_Shader, param, value);
public void SetBoolean(int param, bool value) => _Buffer.SetComputeIntParam(_Shader, param, value ? 1 : 0);
public void SetIntegers(int param, params int[] value) => _Buffer.SetComputeIntParams(_Shader, param, value);
public void SetBoolean(int param, bool value) => _Buffer.SetComputeFloatParam(_Shader, param, value ? 1f : 0f);
public void SetTexture(int param, Texture value) => _Buffer.SetComputeTextureParam(_Shader, _Kernel, param, value);
public void SetTexture(int param, RenderTargetIdentifier value) => _Buffer.SetComputeTextureParam(_Shader, _Kernel, param, value);
public void SetBuffer(int param, ComputeBuffer value) => _Buffer.SetComputeBufferParam(_Shader, _Kernel, param, value);
@@ -156,8 +160,7 @@ namespace WaveHarmonic.Crest
public void Dispatch(int x, int y, int z) => _Buffer.DispatchCompute(_Shader, _Kernel, x, y, z);
}
[System.Serializable]
readonly struct PropertyWrapperComputeStandalone : IPropertyWrapper
readonly struct PropertyWrapperComputeStandalone : IPropertyWrapperVariants
{
readonly ComputeShader _Shader;
readonly int _Kernel;
@@ -171,7 +174,7 @@ namespace WaveHarmonic.Crest
public void SetFloat(int param, float value) => _Shader.SetFloat(param, value);
public void SetFloatArray(int param, float[] value) => _Shader.SetFloats(param, value);
public void SetInteger(int param, int value) => _Shader.SetInt(param, value);
public void SetBoolean(int param, bool value) => _Shader.SetBool(param, value);
public void SetBoolean(int param, bool value) => _Shader.SetFloat(param, value ? 1f : 0f);
public void SetTexture(int param, Texture value) => _Shader.SetTexture(_Kernel, param, value);
public void SetBuffer(int param, ComputeBuffer value) => _Shader.SetBuffer(_Kernel, param, value);
public void SetConstantBuffer(int param, ComputeBuffer value) => _Shader.SetConstantBuffer(param, value, 0, value.stride);

View File

@@ -61,6 +61,7 @@ namespace WaveHarmonic.Crest
cameraData = frameData.Get<UniversalCameraData>();
renderingData = frameData.Get<UniversalRenderingData>();
#if URP_COMPATIBILITY_MODE
if (builder == null)
{
#pragma warning disable CS0618 // Type or member is obsolete
@@ -69,6 +70,7 @@ namespace WaveHarmonic.Crest
#pragma warning restore CS0618 // Type or member is obsolete
}
else
#endif
{
colorTargetHandle = resources.activeColorTexture;
depthTargetHandle = resources.activeDepthTexture;

View File

@@ -93,6 +93,7 @@ namespace WaveHarmonic.Crest
return false;
}
// Modified from:
// https://github.com/Unity-Technologies/Graphics/blob/19ec161f3f752db865597374b3ad1b3eaf110097/Packages/com.unity.render-pipelines.universal/Runtime/RenderingUtils.cs#L697-L729
/// <summary>
@@ -108,7 +109,8 @@ namespace WaveHarmonic.Crest
/// <param name="mipMapBias">Bias applied to mipmaps during filtering.</param>
/// <param name="name">Name of the RTHandle.</param>
/// <returns>If the RTHandle should be re-allocated</returns>
public static bool ReAllocateIfNeeded(
public static bool ReAllocateIfNeeded
(
ref RTHandle handle,
Vector2 scaleFactor,
in RenderTextureDescriptor descriptor,
@@ -117,50 +119,28 @@ namespace WaveHarmonic.Crest
bool isShadowMap = false,
int anisoLevel = 1,
float mipMapBias = 0,
string name = "")
string name = ""
)
{
var usingConstantScale = handle != null && handle.useScaling && handle.scaleFactor == scaleFactor;
if (!usingConstantScale || RTHandleNeedsReAlloc(handle, descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name, true))
// Only HDRP seems to use this?
if (RenderPipelineHelper.IsHighDefinition)
{
handle?.Release();
handle = RTHandles.Alloc(scaleFactor, descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name);
return true;
var usingConstantScale = handle != null && handle.useScaling && handle.scaleFactor == scaleFactor;
if (!usingConstantScale || RTHandleNeedsReAlloc(handle, descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name, true))
{
handle?.Release();
handle = RTHandles.Alloc(scaleFactor, descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name);
return true;
}
}
return false;
}
// https://github.com/Unity-Technologies/Graphics/blob/19ec161f3f752db865597374b3ad1b3eaf110097/Packages/com.unity.render-pipelines.universal/Runtime/RenderingUtils.cs#L731-L764
/// <summary>
/// Re-allocate dynamically resized RTHandle if it is not allocated or doesn't match the descriptor
/// </summary>
/// <param name="handle">RTHandle to check (can be null)</param>
/// <param name="scaleFunc">Function used for the RTHandle size computation.</param>
/// <param name="descriptor">Descriptor for the RTHandle to match</param>
/// <param name="filterMode">Filtering mode of the RTHandle.</param>
/// <param name="wrapMode">Addressing mode of the RTHandle.</param>
/// <param name="isShadowMap">Set to true if the depth buffer should be used as a shadow map.</param>
/// <param name="anisoLevel">Anisotropic filtering level.</param>
/// <param name="mipMapBias">Bias applied to mipmaps during filtering.</param>
/// <param name="name">Name of the RTHandle.</param>
/// <returns>If an allocation was done</returns>
public static bool ReAllocateIfNeeded(
ref RTHandle handle,
ScaleFunc scaleFunc,
in RenderTextureDescriptor descriptor,
FilterMode filterMode = FilterMode.Point,
TextureWrapMode wrapMode = TextureWrapMode.Repeat,
bool isShadowMap = false,
int anisoLevel = 1,
float mipMapBias = 0,
string name = "")
{
var usingScaleFunction = handle != null && handle.useScaling && handle.scaleFactor == Vector2.zero;
if (!usingScaleFunction || RTHandleNeedsReAlloc(handle, descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name, true))
else
{
handle?.Release();
handle = RTHandles.Alloc(scaleFunc, descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name);
return true;
if (RTHandleNeedsReAlloc(handle, descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name, false))
{
handle?.Release();
handle = RTHandles.Alloc(descriptor, filterMode, wrapMode, isShadowMap, anisoLevel, mipMapBias, name);
return true;
}
}
return false;

View File

@@ -2,11 +2,18 @@
// Copyright © 2024 Wave Harmonic. All rights reserved.
// ENABLE_VR is defined if the platform supports XR.
// d_UnityModuleVR is defined if the VR module is installed.
// VR module depends on XR module (which does nothing by itself) so we only need to check the VR module.
#if ENABLE_VR && d_UnityModuleVR
// In Unity 6.5, the XR module replaced the VR module.
#if ENABLE_VR
#if UNITY_6000_5_OR_NEWER
#if d_UnityModuleXR
#define _XR_ENABLED
#endif
#else
#if d_UnityModuleVR
#define _XR_ENABLED
#endif
#endif
#endif
using System.Collections.Generic;
using System.Diagnostics;

View File

@@ -1,18 +1,102 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if ENABLE_VR && d_UnityModuleVR
#if ENABLE_VR
#if UNITY_6000_5_OR_NEWER
#if d_UnityModuleXR
#define _XR_ENABLED
#endif
#else
#if d_UnityModuleVR
#define _XR_ENABLED
#endif
#endif
#endif
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
using UnityEngine.Rendering;
using UnityEngine.XR;
namespace WaveHarmonic.Crest
{
static partial class Rendering
{
// Taken from Unity 6.5:
// Packages/com.unity.render-pipelines.core/Runtime/Utilities/CoreUtils.cs
/// <summary>
/// Return the GraphicsFormat of DepthStencil RenderTarget preferred for the current platform.
/// </summary>
/// <returns>The GraphicsFormat of DepthStencil RenderTarget preferred for the current platform.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphicsFormat GetDefaultDepthStencilFormat()
{
#if UNITY_SWITCH || UNITY_SWITCH2 || UNITY_EMBEDDED_LINUX || UNITY_QNX || UNITY_ANDROID
return GraphicsFormat.D24_UNorm_S8_UInt;
#else
return GraphicsFormat.D32_SFloat_S8_UInt;
#endif
}
// Taken from Unity 6.5:
// Packages/com.unity.render-pipelines.core/Runtime/Utilities/CoreUtils.cs
/// <summary>
/// Return the GraphicsFormat of Depth-only RenderTarget preferred for the current platform.
/// </summary>
/// <returns>The GraphicsFormat of Depth-only RenderTarget preferred for the current platform.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphicsFormat GetDefaultDepthOnlyFormat()
{
#if UNITY_SWITCH || UNITY_SWITCH2 || UNITY_EMBEDDED_LINUX || UNITY_QNX || UNITY_ANDROID
return GraphicsFormatUtility.GetDepthStencilFormat(24, 0); // returns GraphicsFormat.D24_UNorm when hardware supports it
#else
return GraphicsFormat.D32_SFloat;
#endif
}
// Taken from Unity 6.5:
// Packages/com.unity.render-pipelines.core/Runtime/Utilities/CoreUtils.cs
/// <summary>
/// Return the number of DepthStencil RenderTarget depth bits preferred for the current platform.
/// </summary>
/// <returns>The number of DepthStencil RenderTarget depth bits preferred for the current platform.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static DepthBits GetDefaultDepthBufferBits()
{
#if UNITY_SWITCH || UNITY_SWITCH2 || UNITY_EMBEDDED_LINUX || UNITY_QNX || UNITY_ANDROID
return DepthBits.Depth24;
#else
return DepthBits.Depth32;
#endif
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphicsFormat GetDefaultColorFormat(bool hdr)
{
return SystemInfo.GetGraphicsFormat(hdr ? DefaultFormat.HDR : DefaultFormat.LDR);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static GraphicsFormat GetDefaultDepthFormat(bool stencil)
{
return stencil ? GetDefaultDepthStencilFormat() : GetDefaultDepthOnlyFormat();
}
// URP_COMPATIBILITY_MODE = URP + UE < 6.4
public static bool IsRenderGraph => RenderPipelineHelper.IsUniversal
#if URP_COMPATIBILITY_MODE
#if !UNITY_6000_0_OR_NEWER
&& false
#else
&& !GraphicsSettings.GetRenderPipelineSettings<UnityEngine.Rendering.Universal.RenderGraphSettings>().enableRenderCompatibilityMode
#endif
#endif
;
public static partial class BIRP
{
static partial class ShaderIDs

Some files were not shown because too many files have changed in this diff Show More