This commit is contained in:
zhangjiajia
2026-05-06 16:56:59 +08:00
parent 575626d3e1
commit 81ffaaeca6
1373 changed files with 145920 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
// Possible improvements:
// - Add quality property
// - Add water level separately (seems fine)
// - Loop over all chunks for larger near clip planes
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
internal const int k_SurfaceDataShaderPass = 2;
internal static partial class ShaderIDs
{
public static int s_WaterLine = Shader.PropertyToID("_Crest_WaterLine");
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");
}
RenderTexture _HeightRT;
internal RenderTexture HeightRT { get => _HeightRT; }
CommandBuffer _BeforeRenderingCommands;
Material _DisplacedMaterial;
internal struct SurfaceDataParameters
{
public Vector2 _SnappedPosition;
public Vector2 _Resolution;
public float _Texel;
}
internal SurfaceDataParameters _SurfaceDataParameters;
internal MaterialPropertyBlock _SurfaceDataMPB;
internal void BindDisplacedSurfaceData<T>(T properties) where T : IPropertyWrapper
{
properties.SetTexture(ShaderIDs.s_WaterLine, HeightRT);
properties.SetVector(ShaderIDs.s_WaterLineSnappedPosition, _SurfaceDataParameters._SnappedPosition);
properties.SetVector(ShaderIDs.s_WaterLineResolution, _SurfaceDataParameters._Resolution);
properties.SetFloat(ShaderIDs.s_WaterLineTexel, _SurfaceDataParameters._Texel);
}
internal void UpdateDisplacedSurfaceData(Camera camera)
{
// World size of the texture. Formula should effectively cover the camera.
var size = 1f + (camera.nearClipPlane * 2f);
// Do not use the water position. It will cause a mismatch when using displacement
// correction.
var bounds = new Bounds(camera.transform.position, Vector3.one * size);
if (_DisplacedMaterial == null)
{
_DisplacedMaterial = new(WaterResources.Instance.Shaders._UnderwaterMask);
}
_BeforeRenderingCommands ??= new();
var commands = _BeforeRenderingCommands;
commands.name = "Crest.DrawMask";
commands.Clear();
// TODO: add control so users can set this.
// Diminishing returns beyond 0.0125.
UpdateDisplacedSurfaceData
(
commands,
bounds,
"_Crest_WaterLine",
ref _HeightRT,
texel: 0.0125f,
out _SurfaceDataParameters
);
_SurfaceDataMPB ??= new();
var wrapper = new PropertyWrapperMPB(_SurfaceDataMPB);
BindDisplacedSurfaceData(wrapper);
var lod = (int)Builder.PatchType.Interior;
var mpb = _PerCascadeMPB.Current[lod];
if (_Water.Viewpoint != camera.transform && Vector3.Distance(_Water.Viewpoint.position, camera.transform.position) > 0.01f)
{
foreach (var chunk in _Water.Surface.Chunks)
{
if (!bounds.IntersectsXZ(chunk.Rend.bounds))
{
continue;
}
commands.DrawMesh
(
chunk._Mesh,
chunk.transform.localToWorldMatrix,
_DisplacedMaterial,
submeshIndex: 0,
shaderPass: k_SurfaceDataShaderPass,
chunk._MaterialPropertyBlock
);
}
}
else
{
for (var i = 0; i < 4; i++)
{
commands.DrawMesh
(
_Meshes[lod],
Root.localToWorldMatrix * Matrix4x4.TRS(Builder.s_OffsetsFirstLod[i].XNZ(), Quaternion.identity, Vector3.one),
_DisplacedMaterial,
submeshIndex: 0,
k_SurfaceDataShaderPass,
mpb
);
}
}
Graphics.ExecuteCommandBuffer(commands);
}
internal void UpdateDisplacedSurfaceData(CommandBuffer commands, Bounds bounds, string name, ref RenderTexture target, float texel, 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)
);
// 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));
// Store for binding later.
parameters = new()
{
_SnappedPosition = snapped,
_Resolution = resolution,
_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);
if (target == null)
{
target = new(resolution.x, resolution.y, 0)
{
name = name,
// Needs this precision.
graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R32_SFloat
};
}
else if (target.width != resolution.x || target.height != resolution.y)
{
target.Release();
target.width = resolution.x;
target.height = resolution.y;
}
if (!target.IsCreated())
{
target.Create();
}
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
var buffer = new UnityEngine.Rendering.HighDefinition.ShaderVariablesGlobal();
projection = GL.GetGPUProjectionMatrix(projection, true);
// If we want to use camera relative rendering, then we should not set the matrix
// position. Instead set _WorldSpaceCameraPos_Internal.
buffer._ViewProjMatrix = projection * view;
ConstantBuffer.PushGlobal(commands, buffer, Crest.ShaderIDs.Unity.s_ShaderVariablesGlobal);
}
else
#endif
{
commands.SetViewProjectionMatrices(view, projection);
}
commands.SetRenderTarget(target);
commands.ClearRenderTarget(true, true, Color.clear);
// For mask compute, meniscus etc.
commands.SetGlobalTexture(ShaderIDs.s_WaterLine, target);
commands.SetGlobalVector(ShaderIDs.s_WaterLineSnappedPosition, snapped);
commands.SetGlobalVector(ShaderIDs.s_WaterLineResolution, (Vector2)resolution);
commands.SetGlobalFloat(ShaderIDs.s_WaterLineTexel, texel);
}
}
}

View File

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

View File

@@ -0,0 +1,50 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
internal void Reset()
{
_Material = AssetDatabase.LoadAssetAtPath<Material>("Packages/com.waveharmonic.crest/Runtime/Materials/Water.mat");
_ChunkTemplate = AssetDatabase.LoadAssetAtPath<GameObject>("Packages/com.waveharmonic.crest/Runtime/Prefabs/Chunk.prefab");
}
[@OnChange]
void OnChange(string path, object previous)
{
switch (path)
{
case nameof(_Enabled):
SetEnabled((bool)previous, _Enabled);
break;
case nameof(_Layer):
SetLayer((int)previous, _Layer);
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.
Rebuild();
break;
case nameof(_CastShadows):
SetCastShadows((bool)previous, _CastShadows);
break;
case nameof(_AllowRenderQueueSorting):
SetAllowRenderQueueSorting((bool)previous, _AllowRenderQueueSorting);
break;
case nameof(_Debug) + "." + nameof(DebugFields._DisableSkirt):
case nameof(_Debug) + "." + nameof(DebugFields._UniformTiles):
Rebuild();
break;
}
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,133 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if d_UnityHDRP
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using UnityEngine.Rendering.RendererUtils;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
internal sealed class WaterSurfaceCustomPass : CustomPass
{
const string k_Name = "Water Surface";
static WaterSurfaceCustomPass s_Instance;
WaterRenderer _Water;
// We disable the pass we want, so target another.
ShaderTagId _ShaderTagID = new("DepthOnly");
static readonly RenderTargetIdentifier[] s_RenderTargets = new RenderTargetIdentifier[2];
public static void Enable(WaterRenderer renderer)
{
var gameObject = CustomPassHelpers.CreateOrUpdate
(
parent: renderer.Container.transform,
k_Name,
hide: !renderer._Debug._ShowHiddenObjects
);
CustomPassHelpers.CreateOrUpdate
(
gameObject,
ref s_Instance,
WaterRenderer.k_DrawWater,
CustomPassInjectionPoint.BeforeTransparent
);
s_Instance._Water = renderer;
s_Instance.targetColorBuffer = TargetBuffer.Camera;
s_Instance.targetDepthBuffer = TargetBuffer.Camera;
}
public static void Disable()
{
// It should be safe to rely on this reference for this reference to fail.
if (s_Instance != null && s_Instance._GameObject != null)
{
// Will also trigger Cleanup below.
s_Instance._GameObject.SetActive(false);
}
}
protected override void Execute(CustomPassContext context)
{
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)
{
return;
}
if (hdCamera.msaaEnabled)
{
WaterRenderer.s_CameraMSAA = true;
return;
}
var buffer = context.cmd;
buffer.BeginSample(k_DrawWaterSurface);
s_RenderTargets[0] = context.cameraColorBuffer;
s_RenderTargets[1] = context.cameraMotionVectorsBuffer;
CoreUtils.SetRenderTarget(buffer, s_RenderTargets, context.cameraDepthBuffer);
var apv = FrameSettingsField.
#if UNITY_6000_0_OR_NEWER
AdaptiveProbeVolume;
#else
ProbeVolume;
#endif
var rendererConfiguration = HDUtils.GetRendererConfiguration
(
context.hdCamera.frameSettings.IsEnabled(apv),
context.hdCamera.frameSettings.IsEnabled(FrameSettingsField.Shadowmask)
);
if (hdCamera.frameSettings.IsEnabled(FrameSettingsField.MotionVectors))
{
rendererConfiguration |= PerObjectData.MotionVectors;
}
var rld = new RendererListDesc(_ShaderTagID, context.cullingResults, camera)
{
layerMask = 1 << _Water.Surface.Layer,
overrideShader = _Water.Surface.Material.shader,
overrideShaderPassIndex = _Water.Surface.Material.FindPass("Forward"),
renderQueueRange = RenderQueueRange.transparent,
sortingCriteria = SortingCriteria.CommonOpaque,
excludeObjectMotionVectors = false,
rendererConfiguration = rendererConfiguration,
};
buffer.DrawRendererList(context.renderContext.CreateRendererList(rld));
buffer.EndSample(k_DrawWaterSurface);
}
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,243 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
partial class ShaderIDs
{
public static readonly int s_DummyTarget = Shader.PropertyToID("_Crest_DummyTarget");
public static readonly int s_WorldToShadow = Shader.PropertyToID("_Crest_WorldToShadow");
public static class Unity
{
public static readonly int s_BuiltInSurface = Shader.PropertyToID("_BUILTIN_Surface");
public static readonly int s_BuiltInTransparentReceiveShadows = Shader.PropertyToID("_BUILTIN_TransparentReceiveShadows");
}
}
CommandBuffer _DrawWaterSurfaceBuffer;
void OnBeginCameraRenderingLegacy(Camera camera)
{
_Water.UpdateMatrices(camera);
#if UNITY_EDITOR
if (!Application.isPlaying)
{
OnPreRenderWaterLevelDepthTexture(camera);
}
#endif
// Everything from here depends on the material being transparent.
if (!IsTransparent(Material))
{
return;
}
camera.depthTextureMode |= DepthTextureMode.Depth;
_DrawWaterSurfaceBuffer ??= new() { name = WaterRenderer.k_DrawWater };
_DrawWaterSurfaceBuffer.Clear();
// Create or update RT.
_Water.OnBeginCameraOpaqueTexture(camera);
SetUpShadows(camera);
if (_Water.RenderBeforeTransparency)
{
Draw(_DrawWaterSurfaceBuffer, camera);
}
camera.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, _DrawWaterSurfaceBuffer);
}
void OnEndCameraRenderingLegacy(Camera camera)
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
OnPostRenderWaterLevelDepthTexture(camera);
}
#endif
_Water.OnEndCameraOpaqueTexture(camera);
if (_DrawWaterSurfaceBuffer != null)
{
camera.RemoveCommandBuffer(CameraEvent.BeforeForwardAlpha, _DrawWaterSurfaceBuffer);
}
if (QualitySettings.shadows != ShadowQuality.Disable && _Water.PrimaryLight != null)
{
if (_ScreenSpaceShadowMapBuffer != null)
{
_Water.PrimaryLight.RemoveCommandBuffer(LightEvent.AfterScreenspaceMask, _ScreenSpaceShadowMapBuffer);
}
if (_DeferredShadowMapBuffer != null)
{
_Water.PrimaryLight.RemoveCommandBuffer(LightEvent.AfterShadowMap, _DeferredShadowMapBuffer);
}
}
Shader.SetGlobalTexture(Crest.ShaderIDs.Unity.s_ShadowMapTexture, Texture2D.whiteTexture);
}
// Draws the water surface including lighting.
internal void Draw(CommandBuffer commands, Camera camera)
{
commands.BeginSample(k_DrawWaterSurface);
CoreUtils.SetRenderTarget(commands, BuiltinRenderTextureType.CameraTarget);
var sun = RenderSettings.sun;
if (sun != null)
{
// Unity does not set up lighting for us so we will get the last value which could incorrect.
// SetGlobalColor is just an alias for SetGlobalVector (no color space conversion like Material.SetColor):
// https://docs.unity3d.com/2017.4/Documentation/ScriptReference/Shader.SetGlobalColor.html
commands.SetGlobalVector(Crest.ShaderIDs.Unity.s_LightColor0, sun.FinalColor());
commands.SetGlobalVector(Crest.ShaderIDs.Unity.s_WorldSpaceLightPos0, -sun.transform.forward);
}
// Always enabled.
commands.SetShaderKeyword("LIGHTPROBE_SH", true);
UpdateChunkVisibility(camera);
foreach (var chunk in Chunks)
{
var renderer = chunk.Rend;
if (chunk.Rend == null)
{
continue;
}
if (!chunk._Visible)
{
continue;
}
if (chunk._Culled)
{
continue;
}
if (!chunk._WaterDataHasBeenBound)
{
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.EndSample(k_DrawWaterSurface);
}
}
partial class SurfaceRenderer
{
Material _ForceShadowsMaterial;
ComputeBuffer _ShadowMatrixBuffer;
readonly Matrix4x4[] _ShadowMatrixDefaults = { Matrix4x4.zero, Matrix4x4.zero, Matrix4x4.zero, Matrix4x4.zero };
Material _CaptureShadowMatrices;
CommandBuffer _DeferredShadowMapBuffer;
CommandBuffer _ScreenSpaceShadowMapBuffer;
void LegacyOnEnable()
{
_ShadowMatrixBuffer ??= new(4, sizeof(float) * 16, ComputeBufferType.Structured);
_ShadowMatrixBuffer.SetData(_ShadowMatrixDefaults);
}
void LegacyOnDisable()
{
_ShadowMatrixBuffer?.Dispose();
_ShadowMatrixBuffer = null;
}
void SetUpShadows(Camera camera)
{
if (QualitySettings.shadows == ShadowQuality.Disable || _Water.PrimaryLight == null)
{
return;
}
var transform = camera.transform;
if (_ForceShadowsMaterial == null)
{
_ForceShadowsMaterial = new Material(WaterResources.Instance.Shaders._ForceShadows);
}
// Force shadows, as Unity ignores transparent shadow receivers, otherwise shadow
// passes will skip if caster or receiver out of view. ShadowLod also depends on this.
Graphics.RenderMesh
(
new(_ForceShadowsMaterial)
{
receiveShadows = true,
shadowCastingMode = ShadowCastingMode.Off,
},
mesh: Helpers.QuadMesh,
submeshIndex: 0,
objectToWorld: QualitySettings.shadowProjection == ShadowProjection.StableFit
? Matrix4x4.TRS(transform.position + transform.forward, Quaternion.LookRotation(transform.forward), Vector3.one * 0.01f)
// TODO: render water level inputs to support shadows for varying water level.
// Sort of works for close fit. But will decrease shadow quality.
: Matrix4x4.TRS(Vector3.up * _Water.SeaLevel, Quaternion.LookRotation(-Vector3.up), Vector3.one * 100f)
);
if (!Material.IsKeywordEnabled("_BUILTIN_TRANSPARENT_RECEIVES_SHADOWS"))
{
return;
}
if (_CaptureShadowMatrices == null)
{
_CaptureShadowMatrices = new Material(WaterResources.Instance.Shaders._CaptureShadowMatrices);
}
// Used ComputeBuffer must always be bound!
Shader.SetGlobalBuffer(ShaderIDs.s_WorldToShadow, _ShadowMatrixBuffer);
// Capture shadow matrices, as Unity clears all but the first cascade.
_ScreenSpaceShadowMapBuffer ??= new() { name = WaterRenderer.k_DrawWater };
_ScreenSpaceShadowMapBuffer.Clear();
// Cannot set target to None, as it will make some UI black (Unity bug?).
_ScreenSpaceShadowMapBuffer.GetTemporaryRT(ShaderIDs.s_DummyTarget, new RenderTextureDescriptor(4, 4));
CoreUtils.SetRenderTarget(_ScreenSpaceShadowMapBuffer, ShaderIDs.s_DummyTarget);
// Setting the buffer (SetGlobalBuffer) and writing to it only worked with Metal.
// For other graphics APIs, had to use SetRandomWriteTarget.
_ScreenSpaceShadowMapBuffer.ClearRandomWriteTargets();
_ScreenSpaceShadowMapBuffer.SetRandomWriteTarget(1, _ShadowMatrixBuffer);
_ScreenSpaceShadowMapBuffer.DrawProcedural(Matrix4x4.identity, _CaptureShadowMatrices, 0, MeshTopology.Triangles, 3);
_ScreenSpaceShadowMapBuffer.ClearRandomWriteTargets();
_ScreenSpaceShadowMapBuffer.ReleaseTemporaryRT(ShaderIDs.s_DummyTarget);
_Water.PrimaryLight.AddCommandBuffer(LightEvent.AfterScreenspaceMask, _ScreenSpaceShadowMapBuffer);
// Make shadow map available to transparents.
// Call this regardless of rendering path as it has no negative consequences for forward.
_DeferredShadowMapBuffer ??= new() { name = WaterRenderer.k_DrawWater };
_DeferredShadowMapBuffer.Clear();
_DeferredShadowMapBuffer.SetGlobalTexture(Crest.ShaderIDs.Unity.s_ShadowMapTexture, BuiltinRenderTextureType.CurrentActive);
_Water.PrimaryLight.AddCommandBuffer(LightEvent.AfterShadowMap, _DeferredShadowMapBuffer);
// Set up shadow keywords.
_DrawWaterSurfaceBuffer.SetKeyword(new("SHADOWS_SINGLE_CASCADE"), QualitySettings.shadowCascades == 1);
_DrawWaterSurfaceBuffer.SetKeyword(new("SHADOWS_SPLIT_SPHERES"), QualitySettings.shadowProjection == ShadowProjection.StableFit);
_DrawWaterSurfaceBuffer.SetKeyword(new("SHADOWS_SOFT"), QualitySettings.shadows == ShadowQuality.All);
}
}
}

View File

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

View File

@@ -0,0 +1,70 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if d_UnityHDRP
using UnityEngine;
using UnityEngine.Rendering.HighDefinition;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
sealed class WaterLevelDepthTextureHDRP : CustomPass
{
static WaterLevelDepthTextureHDRP s_Instance;
WaterRenderer _Water;
SurfaceRenderer _Surface;
internal static void Enable(WaterRenderer water, SurfaceRenderer surface)
{
var gameObject = CustomPassHelpers.CreateOrUpdate
(
parent: water.Container.transform,
k_WaterLevelDepthTextureName,
hide: !water._Debug._ShowHiddenObjects
);
CustomPassHelpers.CreateOrUpdate
(
gameObject,
ref s_Instance,
k_WaterLevelDepthTextureName,
CustomPassInjectionPoint.BeforeRendering
);
s_Instance._Water = water;
s_Instance._Surface = surface;
}
public static void Disable()
{
// It should be safe to rely on this reference for this reference to fail.
if (s_Instance != null && s_Instance._GameObject != null)
{
// Will also trigger Cleanup below.
s_Instance._GameObject.SetActive(false);
}
}
protected override void Execute(CustomPassContext context)
{
var camera = context.hdCamera.camera;
if (Application.isPlaying)
{
return;
}
if (camera.cameraType != CameraType.SceneView || camera != _Water.Viewer)
{
return;
}
_Surface.ExecuteWaterLevelDepthTexture(camera, context.cmd);
}
}
}
}
#endif // d_UnityHDRP

View File

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

View File

@@ -0,0 +1,40 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using UnityEngine.Rendering;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
CommandBuffer _WaterLevelDepthBuffer;
void OnPreRenderWaterLevelDepthTexture(Camera camera)
{
if (camera.cameraType != CameraType.SceneView || camera != _Water.Viewer)
{
return;
}
_WaterLevelDepthBuffer ??= new() { name = k_WaterLevelDepthTextureName };
_WaterLevelDepthBuffer.Clear();
ExecuteWaterLevelDepthTexture(camera, _WaterLevelDepthBuffer);
// Both forward and deferred.
camera.AddCommandBuffer(CameraEvent.BeforeDepthTexture, _WaterLevelDepthBuffer);
camera.AddCommandBuffer(CameraEvent.BeforeGBuffer, _WaterLevelDepthBuffer);
}
void OnPostRenderWaterLevelDepthTexture(Camera camera)
{
if (_WaterLevelDepthBuffer != null)
{
// Both forward and deferred.
camera.RemoveCommandBuffer(CameraEvent.BeforeDepthTexture, _WaterLevelDepthBuffer);
camera.RemoveCommandBuffer(CameraEvent.BeforeGBuffer, _WaterLevelDepthBuffer);
}
}
}
}

View File

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

View File

@@ -0,0 +1,86 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if d_UnityURP
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
sealed class WaterLevelDepthTextureURP : ScriptableRenderPass
{
internal static WaterLevelDepthTextureURP s_Instance;
WaterRenderer _Water;
SurfaceRenderer _Surface;
internal WaterLevelDepthTextureURP()
{
// Will always execute and matrices will be ready.
renderPassEvent = RenderPassEvent.BeforeRenderingPrePasses;
}
internal static void Enable(WaterRenderer water, SurfaceRenderer surface)
{
s_Instance ??= new();
s_Instance._Water = water;
s_Instance._Surface = surface;
}
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (Application.isPlaying)
{
return;
}
if (camera.cameraType != CameraType.SceneView || camera != _Water.Viewer)
{
return;
}
// Enqueue the pass. This happens every frame.
camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(this);
}
#if UNITY_6000_0_OR_NEWER
class PassData
{
public UniversalCameraData _CameraData;
public SurfaceRenderer _Surface;
}
public override void RecordRenderGraph(UnityEngine.Rendering.RenderGraphModule.RenderGraph graph, ContextContainer frame)
{
using (var builder = graph.AddUnsafePass<PassData>(k_WaterLevelDepthTextureName, out var data))
{
builder.AllowPassCulling(false);
data._CameraData = frame.Get<UniversalCameraData>();
data._Surface = _Surface;
builder.SetRenderFunc<PassData>((data, context) =>
{
var buffer = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
_Surface.ExecuteWaterLevelDepthTexture(data._CameraData.camera, buffer);
});
}
}
[System.Obsolete]
#endif
public override void Execute(ScriptableRenderContext context, ref RenderingData data)
{
var buffer = CommandBufferPool.Get(k_WaterLevelDepthTextureName);
_Surface.ExecuteWaterLevelDepthTexture(data.cameraData.camera, buffer);
context.ExecuteCommandBuffer(buffer);
CommandBufferPool.Release(buffer);
}
}
}
}
#endif // d_UnityURP

View File

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

View File

@@ -0,0 +1,86 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// FIXME: Broken for BIRP on MacOS. Either platform specific problem or bug in Unity.
using UnityEngine;
using UnityEngine.Rendering;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
RenderTexture _WaterLevelDepthTexture;
internal RenderTexture WaterLevelDepthTexture => _WaterLevelDepthTexture;
RenderTargetIdentifier _WaterLevelDepthTarget;
Material _WaterLevelDepthMaterial;
const string k_WaterLevelDepthTextureName = "Crest Water Level Depth Texture";
void ExecuteWaterLevelDepthTexture(Camera camera, CommandBuffer buffer)
{
Helpers.CreateRenderTargetTextureReference(ref _WaterLevelDepthTexture, ref _WaterLevelDepthTarget);
_WaterLevelDepthTexture.name = k_WaterLevelDepthTextureName;
if (_WaterLevelDepthMaterial == null)
{
_WaterLevelDepthMaterial = new(Shader.Find("Hidden/Crest/Editor/Water Level (Depth)"));
}
var descriptor = new RenderTextureDescriptor(camera.pixelWidth, camera.pixelHeight)
{
graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.None,
depthBufferBits = 32,
};
// Depth buffer.
buffer.GetTemporaryRT(Helpers.ShaderIDs.s_MainTexture, descriptor);
CoreUtils.SetRenderTarget(buffer, Helpers.ShaderIDs.s_MainTexture, ClearFlag.Depth);
Render(camera, buffer, _WaterLevelDepthMaterial);
Render(camera, buffer, _WaterLevelDepthMaterial);
// Depth texture.
// Always release to handle screen size changes.
_WaterLevelDepthTexture.Release();
descriptor.graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R32_SFloat;
descriptor.depthBufferBits = 0;
Helpers.SafeCreateRenderTexture(ref _WaterLevelDepthTexture, descriptor);
_WaterLevelDepthTexture.Create();
// Convert.
Helpers.Blit(buffer, _WaterLevelDepthTarget, Helpers.UtilityMaterial, (int)Helpers.UtilityPass.Copy);
buffer.ReleaseTemporaryRT(Helpers.ShaderIDs.s_MainTexture);
}
void EnableWaterLevelDepthTexture()
{
if (Application.isPlaying) return;
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
WaterLevelDepthTextureURP.Enable(_Water, this);
}
#endif
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
WaterLevelDepthTextureHDRP.Enable(_Water, this);
}
#endif
}
void DisableWaterLevelDepthTexture()
{
if (Application.isPlaying) return;
#if d_UnityHDRP
WaterLevelDepthTextureHDRP.Disable();
#endif
}
}
}

View File

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

View File

@@ -0,0 +1,163 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
#if d_UnityURP
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RendererUtils;
using UnityEngine.Rendering.Universal;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
internal sealed class WaterSurfaceRenderPass : ScriptableRenderPass
{
readonly WaterRenderer _Water;
public static WaterSurfaceRenderPass Instance { get; set; }
// We disable the pass we want, so target another.
ShaderTagId _ShaderTagID = new("DepthOnly");
public WaterSurfaceRenderPass(WaterRenderer water)
{
_Water = water;
renderPassEvent = RenderPassEvent.BeforeRenderingTransparents;
// Copy color happens between "after skybox" and "before transparency".
ConfigureInput(ScriptableRenderPassInput.Color | ScriptableRenderPassInput.Depth);
}
public static void Enable(WaterRenderer water)
{
#if UNITY_EDITOR
var data = water.Viewer != null ? water.Viewer.GetUniversalAdditionalCameraData() : null;
// Type is internal.
if (data != null && data.scriptableRenderer.GetType().Name == "Renderer2D")
{
UnityEditor.EditorUtility.DisplayDialog
(
"Crest Error!",
"The project has been detected as a URP 2D project. Crest only supports 3D projects. " +
"You may see errors from Crest in the console, and other issues.",
"Ok"
);
}
#endif
Instance = new WaterSurfaceRenderPass(water);
}
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (!WaterRenderer.ShouldRender(camera, Instance._Water.Surface.Layer))
{
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;
}
camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(Instance);
}
#if UNITY_6000_0_OR_NEWER
class PassData
{
public UnityEngine.Rendering.RenderGraphModule.RendererListHandle _RendererList;
}
readonly RenderGraphHelper.PassData _PassData = new();
public override void RecordRenderGraph(UnityEngine.Rendering.RenderGraphModule.RenderGraph graph, ContextContainer frame)
{
if (!_Water.RenderBeforeTransparency)
{
return;
}
using (var builder = graph.AddRasterRenderPass<PassData>("Crest.DrawWater/Surface", out var data))
{
var resourceData = frame.Get<UniversalResourceData>();
var cameraData = frame.Get<UniversalCameraData>();
var renderingData = frame.Get<UniversalRenderingData>();
// Make inputs show in RG viewer. We configure them already which makes them
// available, but that might change when Unity removes compatibility mode. If that
// happens, we also have to reconsider pass culling to ensure inputs are available
// when rendering to transparent pass.
builder.UseTexture(resourceData.cameraDepthTexture, UnityEngine.Rendering.RenderGraphModule.AccessFlags.Read);
builder.UseTexture(resourceData.cameraOpaqueTexture, UnityEngine.Rendering.RenderGraphModule.AccessFlags.Read);
// We do not want to use the back buffers, as it will prevent merging?
// This is recommended. Back buffers are used at end of frame typically.
builder.SetRenderAttachment(resourceData.activeColorTexture, 0);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
var rld = new RendererListDesc(_ShaderTagID, renderingData.cullResults, cameraData.camera)
{
layerMask = 1 << _Water.Surface.Layer,
overrideShader = _Water.Surface.Material.shader,
overrideShaderPassIndex = 0, // UniversalForward
renderQueueRange = RenderQueueRange.transparent,
sortingCriteria = SortingCriteria.CommonOpaque,
rendererConfiguration = renderingData.perObjectData,
};
data._RendererList = graph.CreateRendererList(rld);
builder.UseRendererList(data._RendererList);
builder.SetRenderFunc<PassData>((data, context) =>
{
context.cmd.DrawRendererList(data._RendererList);
});
}
}
[System.Obsolete]
#endif
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (!_Water.RenderBeforeTransparency)
{
return;
}
var buffer = CommandBufferPool.Get("Crest.DrawWater/Surface");
var rld = new RendererListDesc(_ShaderTagID, renderingData.cullResults, renderingData.cameraData.camera)
{
layerMask = 1 << _Water.Surface.Layer,
overrideShader = _Water.Surface.Material.shader,
overrideShaderPassIndex = 0, // UniversalForward
renderQueueRange = RenderQueueRange.transparent,
sortingCriteria = SortingCriteria.CommonOpaque,
rendererConfiguration = renderingData.perObjectData,
};
buffer.DrawRendererList(context.CreateRendererList(rld));
context.ExecuteCommandBuffer(buffer);
CommandBufferPool.Release(buffer);
}
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,858 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Buffers;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Utility;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Renders the water surface.
/// </summary>
[System.Serializable]
public sealed partial class SurfaceRenderer
{
[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.")]
[@GenerateAPI(Getter.Custom, Setter.Custom)]
[@DecoratedField, SerializeField]
internal bool _Enabled = true;
[Tooltip("The water chunk renderers will have this layer.")]
[@Layer]
[@GenerateAPI]
[SerializeField]
internal int _Layer = 4; // Water
[Tooltip("Material to use for the water surface.")]
[@AttachMaterialEditor(order: 0)]
[@MaterialField("Crest/Water", name: "Water", title: "Create Water Material")]
[@GenerateAPI]
[SerializeField]
internal Material _Material = null;
[Tooltip("Underwater will copy from this material if set.\n\nUseful for overriding properties for the underwater effect. To see what properties can be overriden, see the disabled properties on the underwater material. This does not affect the surface.")]
[@AttachMaterialEditor(order: 1)]
[@MaterialField("Crest/Water", name: "Water (Below)", title: "Create Water Material", parent: "_Material")]
[@GenerateAPI]
[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).")]
[@GenerateAPI(Getter.Custom)]
[@DecoratedField, SerializeField]
internal bool _CastShadows;
[@Heading("Culling")]
[Tooltip("Whether 'Water Body' components will cull the water tiles.\n\nDisable if you want to use the 'Material Override' feature and still have an ocean.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal bool _WaterBodyCulling = true;
[Tooltip("How many frames to distribute the chunk bounds calculation.\n\nThe chunk bounds are calculated per frame to ensure culling is correct when using inputs that affect displacement. Some performance can be saved by distributing the load over several frames. The higher the frames, the longer it will take - lowest being instant.")]
[@Range(1, 30, Range.Clamp.Minimum)]
[@GenerateAPI]
[SerializeField]
internal int _TimeSliceBoundsUpdateFrameCount = 1;
[@Heading("Advanced")]
[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)]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal bool _AllowRenderQueueSorting;
[@Space(10)]
#if !CREST_DEBUG
[HideInInspector]
#endif
[@DecoratedField, SerializeField]
internal DebugFields _Debug = new();
[System.Serializable]
internal sealed class DebugFields
{
#if !CREST_DEBUG
[HideInInspector]
#endif
[Tooltip("Whether to generate water geometry tiles uniformly (with overlaps).")]
[@DecoratedField, SerializeField]
public bool _UniformTiles;
#if !CREST_DEBUG
[HideInInspector]
#endif
[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;
}
const string k_DrawWaterSurface = "Surface";
internal WaterRenderer _Water;
internal Transform Root { get; private set; }
internal List<WaterChunkRenderer> Chunks { get; } = new();
internal bool _Rebuild;
//
// Level of Detail
//
// Extra frame is for motion vectors.
internal BufferedData<MaterialPropertyBlock[]> _PerCascadeMPB = new(2, () => new MaterialPropertyBlock[Lod.k_MaximumSlices]);
// We are computing these values to be optimal based on the base mesh vertex density.
float _LodAlphaBlackPointFade;
float _LodAlphaBlackPointWhitePointFade;
//
// Culling
//
internal readonly Plane[] _CameraFrustumPlanes = new Plane[6];
bool _CanSkipCulling;
internal bool _DoneChunkVisibility;
//
// Events
//
/// <summary>
/// Invoked after water chunk modification.
/// </summary>
/// <remarks>
/// Gives an opportunity to modify the renderer.
/// </remarks>
public static System.Action<Renderer> OnCreateChunkRenderer { get; set; }
internal Material _MotionVectorMaterial;
internal Material AboveOrBelowSurfaceMaterial => _VolumeMaterial == null ? _Material : _VolumeMaterial;
//
// Facing
//
internal enum SurfaceSelfIntersectionFixMode
{
[Tooltip("Uses VFACE/IsFrontFace.")]
Off,
[Tooltip("Force entire water surface to render as below water.")]
ForceBelowWater,
[Tooltip("Force entire water surface to render as above water.")]
ForceAboveWater,
[Tooltip("Force entire water surface to render as above or below water if beyond a distance from surface, otherwise use mask/facing.")]
On,
[Tooltip("Force entire water surface to render as above or below water if beyond a distance from surface (except in special circumstances like Portals).")]
Automatic,
}
enum ForceFacing
{
None,
BelowWater,
AboveWater,
Facing,
}
static partial class ShaderIDs
{
public static readonly int s_ForceUnderwater = Shader.PropertyToID("g_Crest_ForceUnderwater");
public static readonly int s_LodAlphaBlackPointFade = Shader.PropertyToID("g_Crest_LodAlphaBlackPointFade");
public static readonly int s_LodAlphaBlackPointWhitePointFade = Shader.PropertyToID("g_Crest_LodAlphaBlackPointWhitePointFade");
public static readonly int s_BuiltShadowCasterZTest = Shader.PropertyToID("_Crest_BUILTIN_ShadowCasterZTest");
public static readonly int s_ChunkMeshScaleAlpha = Shader.PropertyToID("_Crest_ChunkMeshScaleAlpha");
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");
}
internal void Initialize()
{
Root = Builder.GenerateMesh(_Water, this, Chunks, _Water.LodResolution, _Water._GeometryDownSampleFactor, _Water.LodLevels);
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;
}
}
// Resolution is 4 tiles across.
var baseMeshDensity = _Water.LodResolution * 0.25f / _Water._GeometryDownSampleFactor;
// 0.4f is the "best" value when base mesh density is 8. Scaling down from there produces results similar to
// hand crafted values which looked good when the water is flat.
_LodAlphaBlackPointFade = 0.4f / (baseMeshDensity / 8f);
_LodAlphaBlackPointWhitePointFade = 1f - _LodAlphaBlackPointFade - _LodAlphaBlackPointFade;
Shader.SetGlobalFloat(ShaderIDs.s_LodAlphaBlackPointFade, _LodAlphaBlackPointFade);
Shader.SetGlobalFloat(ShaderIDs.s_LodAlphaBlackPointWhitePointFade, _LodAlphaBlackPointWhitePointFade);
UpdateMaterial(_Material, ref _MotionVectorMaterial);
_CanSkipCulling = false;
if (RenderPipelineHelper.IsLegacy)
{
LegacyOnEnable();
}
#if UNITY_EDITOR
EnableWaterLevelDepthTexture();
#endif
}
internal void OnDestroy()
{
#if UNITY_EDITOR
DisableWaterLevelDepthTexture();
#endif
// Clean up everything created through the Water Builder.
// Not every mesh is assigned to a chunk thus we should destroy all of them here.
for (var i = 0; i < _Meshes?.Length; i++)
{
Helpers.Destroy(_Meshes[i]);
}
Chunks.Clear();
CoreUtils.Destroy(_MotionVectorMaterial);
CoreUtils.Destroy(_DisplacedMaterial);
if (Root != null)
{
CoreUtils.Destroy(Root.gameObject);
Root = null;
}
if (RenderPipelineHelper.IsLegacy)
{
LegacyOnDisable();
}
}
void ShowHiddenObjects(bool show)
{
foreach (var chunk in Chunks)
{
chunk.gameObject.hideFlags = show ? HideFlags.DontSave : HideFlags.HideAndDontSave;
}
}
// Chunk Visibility.
// check if needed here
// complicated. cos we would have to either check everything that may need it
// or have a loop going over an abstraction
internal void UpdateChunkVisibility(Camera camera)
{
if (_DoneChunkVisibility)
{
return;
}
GeometryUtility.CalculateFrustumPlanes(camera, _CameraFrustumPlanes);
foreach (var chunk in Chunks)
{
var renderer = chunk.Rend;
// Can happen in edit mode.
if (renderer == null) continue;
chunk._Visible = GeometryUtility.TestPlanesAABB(_CameraFrustumPlanes, renderer.bounds);
}
_DoneChunkVisibility = true;
}
internal void UpdateMaterial(Material material, ref Material motion)
{
if (material == null)
{
return;
}
var enable = !_Water.RenderBeforeTransparency;
material.SetShaderPassEnabled("Forward", enable);
material.SetShaderPassEnabled("ForwardAdd", enable);
material.SetShaderPassEnabled("ForwardBase", enable);
material.SetShaderPassEnabled("UniversalForward", enable);
// HDRP will automatically disable this pass for unknown reasons. It might be that
// we are sampling from the depth texture which does not work with shadow casting.
if (RenderPipelineHelper.IsHighDefinition)
{
material.SetShaderPassEnabled("ShadowCaster", _CastShadows);
}
UpdateMotionVectorsMaterial(material, ref motion);
}
internal static bool IsTransparent(Material material)
{
return RenderPipelineHelper.IsLegacy
? material.IsKeywordEnabled("_BUILTIN_SURFACE_TYPE_TRANSPARENT")
: material.IsKeywordEnabled("_SURFACE_TYPE_TRANSPARENT");
}
void Rebuild()
{
OnDestroy();
Initialize();
_Rebuild = false;
}
internal void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (!WaterRenderer.ShouldRender(camera, Layer))
{
return;
}
// Our planar reflection camera must never render the surface.
if (camera == WaterReflections.CurrentCamera)
{
return;
}
if (Material == null)
{
return;
}
WritePerCameraMaterialParameters(camera);
// Motion Vectors.
if (ShouldRenderMotionVectors(camera) && _QueueMotionVectors)
{
UpdateChunkVisibility(camera);
foreach (var chunk in Chunks)
{
chunk.RenderMotionVectors(this, camera);
}
}
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
#if UNITY_EDITOR
WaterLevelDepthTextureURP.s_Instance?.OnBeginCameraRendering(context, camera);
#endif
WaterSurfaceRenderPass.Instance?.OnBeginCameraRendering(context, camera);
}
else
#endif
if (RenderPipelineHelper.IsLegacy)
{
OnBeginCameraRenderingLegacy(camera);
}
}
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 WritePerCameraMaterialParameters(Camera camera)
{
if (Material == null)
{
return;
}
// If no underwater, then no need for underwater surface.
if (!_Water.Underwater.Enabled)
{
Shader.SetGlobalInteger(ShaderIDs.s_ForceUnderwater, (int)ForceFacing.AboveWater);
return;
}
_Water.UpdatePerCameraHeight(camera);
// Override isFrontFace when camera is far enough from the water surface to fix self-intersecting waves.
// Hack - due to SV_IsFrontFace occasionally coming through as true for back faces,
// add a param here that forces water to be in underwater state. I think the root
// cause here might be imprecision or numerical issues at water tile boundaries, although
// i'm not sure why cracks are not visible in this case.
var height = _Water._ViewerHeightAboveWaterPerCamera;
var value = _SurfaceSelfIntersectionFixMode switch
{
SurfaceSelfIntersectionFixMode.On =>
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
? ForceFacing.None
: height < -2f
? ForceFacing.BelowWater
: height > 2f
? ForceFacing.AboveWater
: ForceFacing.None,
// Always use facing (VFACE).
SurfaceSelfIntersectionFixMode.Off => ForceFacing.Facing,
_ => (ForceFacing)_SurfaceSelfIntersectionFixMode,
};
Shader.SetGlobalInteger(ShaderIDs.s_ForceUnderwater, (int)value);
}
internal void LateUpdate()
{
if (_Rebuild)
{
Rebuild();
}
Root.position = _Water.Position;
Root.localScale = new(_Water.Scale, 1f, _Water.Scale);
_PerCascadeMPB.Flip();
WritePerCascadeInstanceData();
foreach (var chunk in Chunks)
{
chunk.UpdateMeshBounds(_Water, this);
}
ApplyWaterBodyCulling();
LateUpdateMotionVectors();
UpdateMaterial(_Material, ref _MotionVectorMaterial);
foreach (var body in WaterBody.WaterBodies)
{
if (body._Material != null)
{
UpdateMaterial(body._Material, ref body._MotionVectorMaterial);
}
}
foreach (var chunk in Chunks)
{
chunk.OnLateUpdate();
}
}
void WritePerCascadeInstanceData()
{
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);
}
// 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);
}
for (var index = 0; index < levels; index++)
{
var mpbCurrent = mpbsCurrent[index];
var mpbPrevious = mpbsPrevious[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 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);
var mul = 1.875f; // fudge 1
var pow = 1.4f; // fudge 2
var texelWidth = width / _Water._GeometryDownSampleFactor;
mpbCurrent.SetVector(ShaderIDs.s_ChunkNormalScrollSpeed, new
(
Mathf.Pow(Mathf.Log(1f + 2f * texelWidth) * mul, pow),
Mathf.Pow(Mathf.Log(1f + 4f * texelWidth) * mul, pow),
0,
0
));
}
}
void ApplyWaterBodyCulling()
{
var canSkipCulling = WaterBody.WaterBodies.Count == 0 && _CanSkipCulling;
// Chunk bounds needs to be up-to-date at this point.
foreach (var tile in Chunks)
{
if (tile.Rend == null)
{
continue;
}
tile._Culled = false;
tile.MaterialOverridden = false;
// If there are local bodies of water, this will do overlap tests between the water tiles
// and the water bodies and turn off any that don't overlap.
if (!canSkipCulling)
{
var chunkBounds = tile.Rend.bounds;
var chunkUndisplacedBoundsXZ = tile.UnexpandedBoundsXZ;
var largestOverlap = 0f;
var overlappingOne = false;
foreach (var body in WaterBody.WaterBodies)
{
// If tile has already been excluded from culling, then skip this iteration. But finish this
// iteration if the water body has a material override to work out most influential water body.
if (overlappingOne && body.AboveSurfaceMaterial == null)
{
continue;
}
var bounds = body.AABB;
var overlapping =
bounds.max.x > chunkBounds.min.x && bounds.min.x < chunkBounds.max.x &&
bounds.max.z > chunkBounds.min.z && bounds.min.z < chunkBounds.max.z;
if (overlapping)
{
overlappingOne = true;
if (body.AboveSurfaceMaterial != null)
{
var overlap = 0f;
{
// Use the unexpanded bounds to prevent leaking as generally this feature will be
// for an inland body of water where hopefully there is attenuation between it and
// the water to handle the water's displacement. The inland water body will unlikely
// have large displacement but can be mitigated with a decent buffer zone.
var xMin = Mathf.Max(bounds.min.x, chunkUndisplacedBoundsXZ.min.x);
var xMax = Mathf.Min(bounds.max.x, chunkUndisplacedBoundsXZ.max.x);
var zMin = Mathf.Max(bounds.min.z, chunkUndisplacedBoundsXZ.min.y);
var zMax = Mathf.Min(bounds.max.z, chunkUndisplacedBoundsXZ.max.y);
if (xMin < xMax && zMin < zMax)
{
overlap = (xMax - xMin) * (zMax - zMin);
}
}
// If this water body has the most overlap, then the chunk will get its material.
if (overlap > largestOverlap)
{
tile.MaterialOverridden = true;
tile.Rend.sharedMaterial = body.AboveSurfaceMaterial;
tile._MotionVectorMaterial = body._MotionVectorMaterial;
largestOverlap = overlap;
}
}
else
{
tile.MaterialOverridden = false;
}
}
}
tile._Culled = _WaterBodyCulling && !overlappingOne && WaterBody.WaterBodies.Count > 0;
}
tile.Rend.enabled = !tile._Culled;
}
// Can skip culling next time around if water body count stays at 0
_CanSkipCulling = WaterBody.WaterBodies.Count == 0;
}
internal void Render(Camera camera, CommandBuffer buffer, Material material = null, int pass = 0, bool culled = false)
{
var noMaterial = material == null;
if (noMaterial && Material == null)
{
return;
}
UpdateChunkVisibility(camera);
// Spends approx 0.2-0.3ms here on 2018 Dell XPS 15.
foreach (var chunk in Chunks)
{
var renderer = chunk.Rend;
// Can happen in edit mode.
if (renderer == null)
{
continue;
}
if (!chunk._Visible)
{
continue;
}
if (culled && chunk._Culled)
{
continue;
}
// Make sure properties are bound for this frame.
if (!chunk._WaterDataHasBeenBound)
{
chunk.Bind();
}
if (noMaterial)
{
material = renderer.sharedMaterial;
}
buffer.DrawRenderer(renderer, material, submeshIndex: 0, pass);
}
}
}
// API
partial class SurfaceRenderer
{
bool GetEnabled()
{
return _Enabled && !_Water.IsRunningWithoutGraphics;
}
void SetEnabled(bool previous, bool current)
{
if (previous == current) return;
if (_Water == null || !_Water.isActiveAndEnabled) return;
if (_Enabled) Initialize(); else OnDestroy();
}
void SetLayer(int previous, int current)
{
if (previous == current) return;
foreach (var chunk in Chunks)
{
chunk.gameObject.layer = current;
}
}
bool GetCastShadows()
{
return _CastShadows;
}
void SetCastShadows(bool previous, bool current)
{
if (previous == current) return;
foreach (var chunk in Chunks)
{
chunk.Rend.shadowCastingMode = current ? ShadowCastingMode.On : ShadowCastingMode.Off;
}
}
void SetAllowRenderQueueSorting(bool previous, bool current)
{
if (previous == current) return;
foreach (var chunk in Chunks)
{
chunk.Rend.sortingOrder = current ? chunk._SortingOrder : 0;
}
}
}
// Motion Vectors
partial class SurfaceRenderer
{
bool _QueueMotionVectors;
bool ShouldRenderMotionVectors(Camera camera)
{
// Unity enables this when motion vectors are used - even for SRPs.
if (!camera.depthTextureMode.HasFlag(DepthTextureMode.MotionVectors))
{
return false;
}
return true;
}
void LateUpdateMotionVectors()
{
_QueueMotionVectors = false;
// Handled by Unity.
if (RenderPipelineHelper.IsHighDefinition)
{
return;
}
if (!Application.isPlaying)
{
return;
}
if (!_Water.WriteMotionVectors)
{
return;
}
// This will not support WBs with material overrides, but mixing opaque and
// transparent would be odd.
if (!IsTransparent(Material))
{
return;
}
var pool = ArrayPool<Camera>.Shared;
var cameras = pool.Rent(Camera.allCamerasCount);
Camera.GetAllCameras(cameras);
for (var i = 0; i < Camera.allCamerasCount; i++)
{
var camera = cameras[i];
if (!WaterRenderer.ShouldRender(camera, _Layer))
{
continue;
}
if (!ShouldRenderMotionVectors(camera))
{
continue;
}
_QueueMotionVectors = true;
}
pool.Return(cameras);
}
void UpdateMotionVectorsMaterial(Material surface, ref Material motion)
{
if (!_QueueMotionVectors)
{
return;
}
if (motion == null || motion.shader != surface.shader)
{
CoreUtils.Destroy(motion);
motion = CoreUtils.CreateEngineMaterial(surface.shader);
// BIRP
motion.SetShaderPassEnabled("ForwardBase", false);
motion.SetShaderPassEnabled("ForwardAdd", false);
motion.SetShaderPassEnabled("Deferred", false);
// URP
motion.SetShaderPassEnabled("UniversalForward", false);
motion.SetShaderPassEnabled("UniversalGBuffer", false);
motion.SetShaderPassEnabled("Universal2D", false);
motion.SetShaderPassEnabled("ShadowCaster", false);
motion.SetShaderPassEnabled("DepthOnly", false);
motion.SetShaderPassEnabled("DepthNormals", false);
motion.SetShaderPassEnabled("Meta", false);
motion.SetShaderPassEnabled("SceneSelectionPass", false);
motion.SetShaderPassEnabled("Picking", false);
motion.SetShaderPassEnabled("MotionVectors", true);
}
motion.CopyMatchingPropertiesFromMaterial(surface);
motion.renderQueue = (int)RenderQueue.Geometry;
motion.SetOverrideTag("RenderType", "Opaque");
motion.SetFloat(Crest.ShaderIDs.Unity.s_Surface, 0); // SurfaceType.Opaque
motion.SetFloat(Crest.ShaderIDs.Unity.s_SrcBlend, 1);
motion.SetFloat(Crest.ShaderIDs.Unity.s_DstBlend, 0);
motion.SetFloat(ShaderIDs.s_BuiltShadowCasterZTest, 1); // ZTest Never
}
}
}

View File

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

View File

@@ -0,0 +1,215 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Demarcates an AABB area where water is present in the world.
/// </summary>
/// <remarks>
/// If present, water tiles will be culled if they don't overlap any WaterBody.
/// </remarks>
[@ExecuteDuringEditMode]
[AddComponentMenu(Constants.k_MenuPrefixScripts + "Water Body")]
[@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]
bool _Clip = true;
[Tooltip("Water chunks that overlap this waterbody area will be assigned this material.\n\nThis is useful for varying water appearance across different water bodies. If no override material is specified, the default material assigned to the WaterRenderer component will be used.")]
[@AttachMaterialEditor]
[@GenerateAPI(name: "AboveSurfaceMaterial")]
[@MaterialField("Crest/Water", name: "Water", title: "Create Water Material"), SerializeField]
internal Material _Material = null;
[Tooltip("Overrides the property on the Water Renderer with the same name when the camera is inside the bounds.")]
[@AttachMaterialEditor]
[@GenerateAPI]
[@MaterialField("Crest/Water", name: "Water (Below)", title: "Create Water Material", parent: nameof(_Material)), SerializeField]
internal Material _BelowSurfaceMaterial;
[Tooltip("Overrides the Water Renderer's volume material when the camera is inside the bounds.")]
[@MaterialField(UnderwaterRenderer.k_ShaderNameEffect, name: "Underwater", title: "Create Underwater Material")]
[@AttachMaterialEditor]
[@GenerateAPI]
[SerializeField]
internal Material _VolumeMaterial;
bool _RecalculateRect = true;
bool _RecalculateBounds = true;
internal Material _MotionVectorMaterial;
sealed class ClipInput : ILodInput
{
readonly WaterBody _Owner;
readonly Transform _Transform;
public bool Enabled => WaterRenderer.Instance != null && WaterRenderer.Instance._ClipLod._DefaultClippingState == DefaultClippingState.EverythingClipped;
public bool IsCompute => true;
public int Pass => -1;
// TODO: Expose serialized queue.
public int Queue => 0;
public MonoBehaviour Component => _Owner;
public Rect Rect => _Owner.Rect;
public ClipInput(WaterBody owner)
{
_Owner = owner;
_Transform = owner.transform;
}
public void Draw(Lod simulation, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1f, int slices = -1)
{
var wrapper = new PropertyWrapperCompute(buffer, WaterResources.Instance.Compute._ClipPrimitive, 0);
wrapper.SetMatrix(ShaderIDs.s_Matrix, _Transform.worldToLocalMatrix);
// For culling.
wrapper.SetVector(ShaderIDs.s_Position, _Transform.position);
wrapper.SetFloat(ShaderIDs.s_Diameter, _Transform.lossyScale.Maximum());
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveInverted, true);
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveSphere, false);
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveCube, false);
wrapper.SetKeyword(WaterResources.Instance.Keywords.ClipPrimitiveRectangle, true);
wrapper.SetTexture(ShaderIDs.s_Target, target);
var threads = simulation.Resolution / Lod.k_ThreadGroupSize;
wrapper.Dispatch(threads, threads, slices);
}
public float Filter(WaterRenderer water, int slice)
{
return 1f;
}
}
internal static List<WaterBody> WaterBodies { get; } = new();
Bounds _Bounds;
internal Bounds AABB
{
get
{
if (_RecalculateBounds)
{
CalculateBounds();
_RecalculateBounds = false;
}
return _Bounds;
}
}
Rect _Rect;
Rect Rect
{
get
{
if (_RecalculateRect)
{
_Rect = AABB.RectXZ();
_RecalculateRect = false;
}
return _Rect;
}
}
internal Material AboveOrBelowSurfaceMaterial => _BelowSurfaceMaterial == null ? _Material : _BelowSurfaceMaterial;
ClipInput _ClipInput;
private protected override void Initialize()
{
base.Initialize();
CalculateBounds();
WaterBodies.Add(this);
HandleClipInputRegistration();
}
private protected override void OnDisable()
{
base.OnDisable();
WaterBodies.Remove(this);
if (_ClipInput != null)
{
ILodInput.Detach(_ClipInput, ClipLod.s_Inputs);
_ClipInput = null;
}
}
internal void CalculateBounds()
{
var bounds = new Bounds();
bounds.center = transform.position;
bounds.Encapsulate(transform.TransformPoint(Vector3.right / 2f + Vector3.forward / 2f));
bounds.Encapsulate(transform.TransformPoint(Vector3.right / 2f - Vector3.forward / 2f));
bounds.Encapsulate(transform.TransformPoint(-Vector3.right / 2f + Vector3.forward / 2f));
bounds.Encapsulate(transform.TransformPoint(-Vector3.right / 2f - Vector3.forward / 2f));
_Bounds = bounds;
}
void HandleClipInputRegistration()
{
var registered = _ClipInput != null;
var shouldBeRegistered = _Clip;
if (registered != shouldBeRegistered)
{
if (shouldBeRegistered)
{
_ClipInput = new(this);
ILodInput.Attach(_ClipInput, ClipLod.s_Inputs);
}
else
{
ILodInput.Detach(_ClipInput, ClipLod.s_Inputs);
_ClipInput = null;
}
}
}
private protected override System.Action<WaterRenderer> OnUpdateMethod => OnUpdate;
void OnUpdate(WaterRenderer water)
{
if (transform.hasChanged)
{
_RecalculateRect = _RecalculateBounds = true;
}
}
private protected override System.Action<WaterRenderer> OnLateUpdateMethod => OnLateUpdate;
void OnLateUpdate(WaterRenderer water)
{
transform.hasChanged = false;
}
}
}

View File

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

View File

@@ -0,0 +1,539 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
//#define PROFILE_CONSTRUCTION
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace WaveHarmonic.Crest
{
partial class SurfaceRenderer
{
// Keep references to meshes so they can be cleaned up later.
readonly Mesh[] _Meshes = new Mesh[(int)Builder.PatchType.Count];
/// <summary>
/// Instantiates all the water geometry, as a set of tiles.
/// </summary>
static class Builder
{
// The comments below illustrate case when BASE_VERT_DENSITY = 2. The water mesh is built up from these patches. Rotational symmetry
// is used where possible to eliminate combinations. The slim variants are used to eliminate overlap between patches.
internal enum PatchType
{
/// <summary>
/// Adds no skirt. Used in interior of highest detail LOD (0)
///
/// 1 -------
/// | | |
/// z -------
/// | | |
/// 0 -------
/// 0 1
/// x
///
/// </summary>
Interior,
/// <summary>
/// Adds a full skirt all of the way around a patch
///
/// -------------
/// | | | | |
/// 1 -------------
/// | | | | |
/// z -------------
/// | | | | |
/// 0 -------------
/// | | | | |
/// -------------
/// 0 1
/// x
///
/// </summary>
Fat,
/// <summary>
/// Adds a skirt on the right hand side of the patch
///
/// 1 ----------
/// | | | |
/// z ----------
/// | | | |
/// 0 ----------
/// 0 1
/// x
///
/// </summary>
FatX,
/// <summary>
/// Adds a skirt on the right hand side of the patch, removes skirt from top
/// </summary>
FatXSlimZ,
/// <summary>
/// Outer most side - this adds an extra skirt on the left hand side of the patch,
/// which will point outwards and be extended to Zfar
///
/// 1 --------------------------------------------------------------------------------------
/// | | | |
/// z --------------------------------------------------------------------------------------
/// | | | |
/// 0 --------------------------------------------------------------------------------------
/// 0 1
/// x
///
/// </summary>
FatXOuter,
/// <summary>
/// Adds skirts at the top and right sides of the patch
/// </summary>
FatXZ,
/// <summary>
/// Adds skirts at the top and right sides of the patch and pushes them to horizon
/// </summary>
FatXZOuter,
/// <summary>
/// One less set of verts in x direction
/// </summary>
SlimX,
/// <summary>
/// One less set of verts in both x and z directions
/// </summary>
SlimXZ,
/// <summary>
/// One less set of verts in x direction, extra verts at start of z direction
///
/// ----
/// | |
/// 1 ----
/// | |
/// z ----
/// | |
/// 0 ----
/// 0 1
/// x
///
/// </summary>
SlimXFatZ,
/// <summary>
/// Number of patch types
/// </summary>
Count,
}
// Instance Indices:
// 00 01 02 03
// 04 05
// 06 07
// 08 09 10 11
static readonly Vector2[] s_Offsets =
{
new(-1.5f, +1.5f), new(-0.5f, +1.5f), new(+0.5f, +1.5f), new(+1.5f, +1.5f),
new(-1.5f, +0.5f), new(+1.5f, +0.5f),
new(-1.5f, -0.5f), new(+1.5f, -0.5f),
new(-1.5f, -1.5f), new(-0.5f, -1.5f), new(+0.5f, -1.5f), new(+1.5f, -1.5f),
};
// First LOD has inside bit as well:
// 00 01 02 03
// 04 05 06 07
// 08 09 10 11
// 12 13 14 15
internal static readonly Vector2[] s_OffsetsFirstLod =
{
// Interior first for sorted rendering.
new(-0.5f, +0.5f), new(+0.5f, +0.5f), new(-0.5f, -0.5f), new(+0.5f, -0.5f),
// Exterior.
new(-1.5f, +1.5f), new(-0.5f, +1.5f), new(+0.5f, +1.5f), new(+1.5f, +1.5f),
new(-1.5f, +0.5f), new(+1.5f, +0.5f),
new(-1.5f, -0.5f), new(+1.5f, -0.5f),
new(-1.5f, -1.5f), new(-0.5f, -1.5f), new(+0.5f, -1.5f), new(+1.5f, -1.5f),
};
// Usually rings have an extra side of vertices that point inwards. The outermost
// ring has both the inward vertices and also an additional outwards set of
// vertices that go to the horizon.
static readonly PatchType[] s_PatchTypes =
{
PatchType.SlimXFatZ, PatchType.SlimX, PatchType.SlimX, PatchType.SlimXZ,
PatchType.FatX, PatchType.SlimX,
PatchType.FatX, PatchType.SlimX,
PatchType.FatXZ, PatchType.FatX, PatchType.FatX, PatchType.FatXSlimZ,
};
// All interior - the "side" types have an extra skirt that points inwards - this
// means that this inner most section does not need any skirting. This is good, as
// this is the highest density part of the mesh.
static readonly PatchType[] s_PatchTypesFirstLod =
{
PatchType.Interior, PatchType.Interior, PatchType.Interior, PatchType.Interior,
PatchType.SlimXFatZ, PatchType.SlimX, PatchType.SlimX, PatchType.SlimXZ,
PatchType.FatX, PatchType.SlimX,
PatchType.FatX, PatchType.SlimX,
PatchType.FatXZ, PatchType.FatX, PatchType.FatX, PatchType.FatXSlimZ,
};
static readonly PatchType[] s_PatchTypesLastLod =
{
PatchType.FatXZOuter, PatchType.FatXOuter, PatchType.FatXOuter, PatchType.FatXZOuter,
PatchType.FatXOuter, PatchType.FatXOuter,
PatchType.FatXOuter, PatchType.FatXOuter,
PatchType.FatXZOuter, PatchType.FatXOuter, PatchType.FatXOuter, PatchType.FatXZOuter,
};
static int s_SiblingIndex;
public static Transform GenerateMesh(WaterRenderer water, SurfaceRenderer surface, List<WaterChunkRenderer> tiles, int lodDataResolution, int geoDownSampleFactor, int lodCount)
{
if (lodCount < 1)
{
Debug.LogError("Crest: Invalid LOD count: " + lodCount.ToString(), water);
return null;
}
#if PROFILE_CONSTRUCTION
var sw = new System.Diagnostics.Stopwatch();
sw.Start();
#endif
s_SiblingIndex = 0;
var root = new GameObject("Root");
Debug.Assert(root != null, "Crest: The water Root transform could not be immediately constructed. Please report this issue to the Crest developers via our support email or GitHub at https://github.com/wave-harmonic/crest/issues .");
root.hideFlags = water._Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
root.transform.parent = water.Container.transform;
root.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
root.transform.localScale = Vector3.one;
// create mesh data
// 4 tiles across a LOD, and support lowering density by a factor
var tileResolution = Mathf.Round(0.25f * lodDataResolution / geoDownSampleFactor);
for (var i = 0; i < (int)PatchType.Count; i++)
{
surface._Meshes[i] = BuildPatch(water, (PatchType)i, tileResolution);
}
for (var i = 0; i < lodCount; i++)
{
CreateLOD(water, surface, tiles, root.transform, i, lodCount, surface._Meshes, lodDataResolution, geoDownSampleFactor, surface.Layer);
}
#if PROFILE_CONSTRUCTION
sw.Stop();
Debug.Log( "Crest: Finished generating " + lodCount.ToString() + " LODs, time: " + (1000.0*sw.Elapsed.TotalSeconds).ToString(".000") + "ms" );
#endif
return root.transform;
}
static Mesh BuildPatch(WaterRenderer water, PatchType pt, float vertDensity)
{
var verts = new List<Vector3>();
var indices = new List<int>();
// stick a bunch of verts into a 1m x 1m patch (scaling happens later)
var dx = 1f / vertDensity;
//////////////////////////////////////////////////////////////////////////////////
// verts
// see comments within PatchType for diagrams of each patch mesh
// skirt widths on left, right, bottom and top (in order)
float skirtXminus = 0f, skirtXplus = 0f;
float skirtZminus = 0f, skirtZplus = 0f;
// set the patch size
if (pt == PatchType.Fat) { skirtXminus = skirtXplus = skirtZminus = skirtZplus = 1f; }
else if (pt is PatchType.FatX or PatchType.FatXOuter) { skirtXplus = 1f; }
else if (pt is PatchType.FatXZ or PatchType.FatXZOuter) { skirtXplus = skirtZplus = 1f; }
else if (pt == PatchType.FatXSlimZ) { skirtXplus = 1f; skirtZplus = -1f; }
else if (pt == PatchType.SlimX) { skirtXplus = -1f; }
else if (pt == PatchType.SlimXZ) { skirtXplus = skirtZplus = -1f; }
else if (pt == PatchType.SlimXFatZ) { skirtXplus = -1f; skirtZplus = 1f; }
var sideLength_verts_x = 1f + vertDensity + skirtXminus + skirtXplus;
var sideLength_verts_z = 1f + vertDensity + skirtZminus + skirtZplus;
var start_x = -0.5f - skirtXminus * dx;
var start_z = -0.5f - skirtZminus * dx;
var end_x = 0.5f + skirtXplus * dx;
var end_z = 0.5f + skirtZplus * dx;
// With a default value of 100, this will reach the horizon at all levels at
// a far plane of 200k.
var extentsMultiplier = water._ExtentsSizeMultiplier * (Lod.k_MaximumSlices + 1 - water.LodLevels);
for (float j = 0; j < sideLength_verts_z; j++)
{
// interpolate z across patch
var z = Mathf.Lerp(start_z, end_z, j / (sideLength_verts_z - 1f));
// push outermost edge out to horizon
if (pt == PatchType.FatXZOuter && j == sideLength_verts_z - 1f)
z *= extentsMultiplier;
for (float i = 0; i < sideLength_verts_x; i++)
{
// interpolate x across patch
var x = Mathf.Lerp(start_x, end_x, i / (sideLength_verts_x - 1f));
// push outermost edge out to horizon
if (i == sideLength_verts_x - 1f && (pt == PatchType.FatXOuter || pt == PatchType.FatXZOuter))
x *= extentsMultiplier;
// could store something in y, although keep in mind this is a shared mesh that is shared across multiple lods
verts.Add(new(x, 0f, z));
}
}
//////////////////////////////////////////////////////////////////////////////////
// indices
var sideLength_squares_x = (int)sideLength_verts_x - 1;
var sideLength_squares_z = (int)sideLength_verts_z - 1;
for (var j = 0; j < sideLength_squares_z; j++)
{
for (var i = 0; i < sideLength_squares_x; i++)
{
var flipEdge = false;
if (i % 2 == 1) flipEdge = !flipEdge;
if (j % 2 == 1) flipEdge = !flipEdge;
var i0 = i + j * (sideLength_squares_x + 1);
var i1 = i0 + 1;
var i2 = i0 + (sideLength_squares_x + 1);
var i3 = i2 + 1;
if (!flipEdge)
{
// tri 1
indices.Add(i3);
indices.Add(i1);
indices.Add(i0);
// tri 2
indices.Add(i0);
indices.Add(i2);
indices.Add(i3);
}
else
{
// tri 1
indices.Add(i3);
indices.Add(i1);
indices.Add(i2);
// tri 2
indices.Add(i0);
indices.Add(i2);
indices.Add(i1);
}
}
}
//////////////////////////////////////////////////////////////////////////////////
// create mesh
var mesh = new Mesh();
if (verts != null && verts.Count > 0)
{
var arrV = new Vector3[verts.Count];
verts.CopyTo(arrV);
var arrI = new int[indices.Count];
indices.CopyTo(arrI);
mesh.SetIndices(null, MeshTopology.Triangles, 0);
mesh.vertices = arrV;
// HDRP needs full data. Do this on a define to keep door open to runtime changing of RP.
#if d_UnityHDRP
var norms = new Vector3[verts.Count];
for (var i = 0; i < norms.Length; i++) norms[i] = Vector3.up;
var tans = new Vector4[verts.Count];
for (var i = 0; i < tans.Length; i++) tans[i] = new(1, 0, 0, 1);
mesh.normals = norms;
mesh.tangents = tans;
#else
mesh.normals = null;
#endif
mesh.SetIndices(arrI, MeshTopology.Triangles, 0);
// recalculate bounds. add a little allowance for snapping. in the chunk renderer script, the bounds will be expanded further
// to allow for horizontal displacement
mesh.RecalculateBounds();
var bounds = mesh.bounds;
// Increase snapping allowance (see #1148). Value was chosen by observation with a
// custom debug mode to show pixels that were out of bounds.
dx *= 3f;
bounds.extents = new(bounds.extents.x + dx, bounds.extents.y, bounds.extents.z + dx);
mesh.bounds = bounds;
mesh.name = pt.ToString();
}
return mesh;
}
static void CreateLOD(WaterRenderer water, SurfaceRenderer surface, List<WaterChunkRenderer> tiles, Transform parent, int lodIndex, int lodCount, Mesh[] meshData, int lodDataResolution, int geoDownSampleFactor, int layer)
{
var horizScale = Mathf.Pow(2f, lodIndex);
var isBiggestLOD = lodIndex == lodCount - 1;
var generateSkirt = isBiggestLOD;
#if CREST_DEBUG
generateSkirt = generateSkirt && !surface._Debug._DisableSkirt;
#endif
Vector2[] offsets;
PatchType[] patchTypes;
if (lodIndex != 0)
{
offsets = s_Offsets;
patchTypes = generateSkirt ? s_PatchTypesLastLod : s_PatchTypes;
}
else
{
offsets = s_OffsetsFirstLod;
patchTypes = s_PatchTypesFirstLod;
}
#if CREST_DEBUG
// debug toggle to force all patches to be the same. they'll be made with a surrounding skirt to make sure patches
// overlap
if (surface._Debug._UniformTiles)
{
patchTypes = new PatchType[patchTypes.Length];
System.Array.Fill(patchTypes, PatchType.Fat);
}
#endif
// create the water patches
for (var i = 0; i < offsets.Length; i++)
{
// instantiate and place patch
var patch = surface._ChunkTemplate
? Helpers.InstantiatePrefab(surface._ChunkTemplate)
: new();
// Also applying the hide flags to the chunk will prevent it from being pickable in the editor.
patch.hideFlags = water._Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
patch.name = $"Tile_L{lodIndex}_{patchTypes[i]}";
patch.layer = layer;
patch.transform.parent = parent;
var pos = offsets[i];
patch.transform.localPosition = horizScale * new Vector3(pos.x, 0f, pos.y);
// scale only horizontally, otherwise culling bounding box will be scaled up in y
patch.transform.localScale = new(horizScale, 1f, horizScale);
if (!patch.TryGetComponent<MeshRenderer>(out var mr))
{
mr = patch.AddComponent<MeshRenderer>();
// I don't think one would use light probes for a purely specular water surface? (although diffuse
// foam shading would benefit).
mr.lightProbeUsage = LightProbeUsage.Off;
}
var order = -lodCount + (patchTypes[i] == PatchType.Interior ? -1 : lodIndex);
{
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++;
chunk.Initialize(lodIndex, mr, mesh);
// When custom rendering, we loop over chunks to render, which means these need to
// 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);
}
// Sorting order to stop unity drawing it back to front. Make the innermost four tiles draw first,
// followed by the rest of the tiles by LOD index.
if (RenderPipelineHelper.IsHighDefinition)
{
// HDRP has a different rendering priority system:
// https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@10.10/manual/Renderer-And-Material-Priority.html#sorting-by-renderer
mr.rendererPriority = order;
}
else if (!water.Surface.AllowRenderQueueSorting)
{
// Sorting order to stop unity drawing it back to front. make the innermost 4 tiles draw first, followed by
// the rest of the tiles by LOD index. all this happens before layer 0 - the sorting layer takes priority over the
// render queue it seems! ( https://cdry.wordpress.com/2017/04/28/unity-render-queues-vs-sorting-layers/ ). This pushes
// water rendering way early, so transparent objects will by default render afterwards, which is typical for water rendering.
mr.sortingOrder = order;
}
mr.shadowCastingMode = water.Surface.CastShadows ? ShadowCastingMode.On : ShadowCastingMode.Off;
// This setting is ignored by Unity for the transparent water shader.
mr.receiveShadows = false;
mr.motionVectorGenerationMode = !water.WriteMotionVectors
? MotionVectorGenerationMode.ForceNoMotion
: MotionVectorGenerationMode.Object;
mr.material = water.Surface.Material;
OnCreateChunkRenderer?.Invoke(mr);
// rotate side patches to point the +x side outwards
var rotateXOutwards = patchTypes[i] is PatchType.FatX or PatchType.FatXOuter or PatchType.SlimX or PatchType.SlimXFatZ;
if (rotateXOutwards)
{
if (Mathf.Abs(pos.y) >= Mathf.Abs(pos.x))
patch.transform.localEulerAngles = 90f * Mathf.Sign(pos.y) * -Vector3.up;
else
patch.transform.localEulerAngles = pos.x < 0f ? Vector3.up * 180f : Vector3.zero;
}
// rotate the corner patches so the +x and +z sides point outwards
var rotateXZOutwards = patchTypes[i] is PatchType.FatXZ or PatchType.SlimXZ or PatchType.FatXSlimZ or PatchType.FatXZOuter;
if (rotateXZOutwards)
{
// xz direction before rotation
var from = new Vector3(1f, 0f, 1f).normalized;
// target xz direction is outwards vector given by local patch position - assumes this patch is a corner (checked below)
var to = patch.transform.localPosition.normalized;
if (Mathf.Abs(patch.transform.localPosition.x) < 0.0001f || Mathf.Abs(Mathf.Abs(patch.transform.localPosition.x) - Mathf.Abs(patch.transform.localPosition.z)) > 0.001f)
{
Debug.LogWarning("Crest: Skipped rotating a patch because it isn't a corner, click here to highlight.", patch);
continue;
}
// Detect 180 degree rotations as it doesn't always rotate around Y
if (Vector3.Dot(from, to) < -0.99f)
patch.transform.localEulerAngles = Vector3.up * 180f;
else
patch.transform.localRotation = Quaternion.FromToRotation(from, to);
}
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,357 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Collections.Generic;
using UnityEngine;
using WaveHarmonic.Crest.Internal;
namespace WaveHarmonic.Crest
{
interface IReportsHeight
{
bool ReportHeight(ref Rect bounds, ref float minimum, ref float maximum);
}
interface IReportsDisplacement
{
bool ReportDisplacement(ref Rect bounds, ref float horizontal, ref float vertical);
}
/// <summary>
/// Sets shader parameters for each geometry tile/chunk.
/// </summary>
#if !CREST_DEBUG
[AddComponentMenu("")]
#endif
[@ExecuteDuringEditMode]
sealed class WaterChunkRenderer : ManagedBehaviour<WaterRenderer>
{
[SerializeField]
internal bool _DrawRenderBounds = false;
internal const string k_UpdateMeshBoundsMarker = "Crest.WaterChunkRenderer.UpdateMeshBounds";
static readonly Unity.Profiling.ProfilerMarker s_UpdateMeshBoundsMarker = new(k_UpdateMeshBoundsMarker);
internal Transform _Transform;
internal Mesh _Mesh;
public Renderer Rend { get; private set; }
internal MaterialPropertyBlock _MaterialPropertyBlock;
Matrix4x4 _CurrentObjectToWorld;
Matrix4x4 _PreviousObjectToWorld;
internal Material _MotionVectorMaterial;
internal int _SortingOrder;
internal int _SiblingIndex;
internal Rect _UnexpandedBoundsXZ = new();
public Rect UnexpandedBoundsXZ => _UnexpandedBoundsXZ;
internal bool _Culled;
internal bool _Visible;
internal WaterRenderer _Water;
public bool MaterialOverridden { get; set; }
// We need to ensure that all water data has been bound for the mask to
// render properly - this is something that needs to happen irrespective
// of occlusion culling because we need the mask to render as a
// contiguous surface.
internal bool _WaterDataHasBeenBound = true;
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
// component as it is instantiate via script, and is partial driven externally.
// So instead, call this after instantiation.
internal void Initialize(int index, Renderer renderer, Mesh mesh)
{
_LodIndex = index;
Rend = renderer;
_Mesh = mesh;
_PreviousObjectToWorld = _CurrentObjectToWorld = transform.localToWorldMatrix;
_Transform = transform;
}
private protected override void OnStart()
{
base.OnStart();
UpdateMeshBounds();
}
internal void UpdateMeshBounds(WaterRenderer water, SurfaceRenderer surface)
{
_WaterDataHasBeenBound = false;
var count = surface.TimeSliceBoundsUpdateFrameCount;
// Time slice update to distribute the load.
if (count <= 1 || !(_SiblingIndex % count != Time.frameCount % surface.Chunks.Count % count))
{
// This needs to be called on Update because the bounds depend on transform scale which can change. Also OnWillRenderObject depends on
// the bounds being correct. This could however be called on scale change events, but would add slightly more complexity.
UpdateMeshBounds();
}
}
bool ShouldRender(bool culled)
{
// Is visible to camera.
if (!_Visible)
{
return false;
}
// If including culling, is it culled.
if (culled && _Culled)
{
return false;
}
return true;
}
internal void OnLateUpdate()
{
_PreviousObjectToWorld = _CurrentObjectToWorld;
_CurrentObjectToWorld = _Transform.localToWorldMatrix;
}
internal void RenderMotionVectors(SurfaceRenderer surface, Camera camera)
{
if (!ShouldRender(culled: true))
{
return;
}
// RenderMesh will copy properties immediately, thus we need them bound.
if (!_WaterDataHasBeenBound)
{
Bind();
}
var material = MaterialOverridden ? _MotionVectorMaterial : surface._MotionVectorMaterial;
var parameters = new RenderParams(material)
{
motionVectorMode = MotionVectorGenerationMode.Object,
material = material,
matProps = _MaterialPropertyBlock,
worldBounds = Rend.bounds,
layer = surface.Layer,
receiveShadows = false,
shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off,
lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off,
reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off,
camera = camera,
};
Graphics.RenderMesh(parameters, _Mesh, 0, _CurrentObjectToWorld, _PreviousObjectToWorld);
}
void UpdateMeshBounds()
{
s_UpdateMeshBoundsMarker.Begin(this);
var bounds = _Mesh.bounds;
if (WaterBody.WaterBodies.Count > 0)
{
_UnexpandedBoundsXZ = ComputeBoundsXZ(_Transform, bounds);
}
bounds = ExpandBoundsForDisplacements(_Transform, bounds);
Rend.localBounds = 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];
Rend.SetPropertyBlock(_MaterialPropertyBlock);
_WaterDataHasBeenBound = true;
}
void OnDestroy()
{
Helpers.Destroy(_Mesh);
}
// Called when visible to a camera
void OnWillRenderObject()
{
if (Rend == null)
{
return;
}
if (!MaterialOverridden && Rend.sharedMaterial != _Water.Surface.Material)
{
Rend.sharedMaterial = _Water.Surface.Material;
_MotionVectorMaterial = _Water.Surface._MotionVectorMaterial;
}
if (!_WaterDataHasBeenBound)
{
Bind();
}
if (_DrawRenderBounds)
{
Rend.bounds.DebugDraw();
}
}
// 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)
{
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;
// 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)
};
}
// Extend bounds by local waves.
{
var totalHorizontal = 0f;
var totalVertical = 0f;
foreach (var reporter in DisplacementReporters)
{
var horizontal = 0f;
var vertical = 0f;
if (reporter.ReportDisplacement(ref rect, ref horizontal, ref vertical))
{
totalHorizontal += horizontal;
totalVertical += vertical;
}
}
boundsPadding = totalHorizontal;
expandXZ = boundsPadding / scale.x;
boundsY = totalVertical;
extents.x += expandXZ;
extents.y += boundsY;
extents.z += expandXZ;
}
// Expand and offset bounds by height.
{
var minimumWaterLevelBounds = 0f;
var maximumWaterLevelBounds = 0f;
foreach (var reporter in HeightReporters)
{
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));
}
}
minimumWaterLevelBounds *= 0.5f;
maximumWaterLevelBounds *= 0.5f;
boundsY = minimumWaterLevelBounds + maximumWaterLevelBounds;
extents.y += boundsY;
bounds.extents = extents;
var offset = maximumWaterLevelBounds - minimumWaterLevelBounds;
center.y += offset;
bounds.center = center;
}
return bounds;
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
static void InitStatics()
{
HeightReporters.Clear();
DisplacementReporters.Clear();
}
}
static class BoundsHelper
{
internal static void DebugDraw(this Bounds b)
{
var xmin = b.min.x;
var ymin = b.min.y;
var zmin = b.min.z;
var xmax = b.max.x;
var ymax = b.max.y;
var zmax = b.max.z;
Debug.DrawLine(new(xmin, ymin, zmin), new(xmin, ymin, zmax));
Debug.DrawLine(new(xmin, ymin, zmin), new(xmax, ymin, zmin));
Debug.DrawLine(new(xmax, ymin, zmax), new(xmin, ymin, zmax));
Debug.DrawLine(new(xmax, ymin, zmax), new(xmax, ymin, zmin));
Debug.DrawLine(new(xmin, ymax, zmin), new(xmin, ymax, zmax));
Debug.DrawLine(new(xmin, ymax, zmin), new(xmax, ymax, zmin));
Debug.DrawLine(new(xmax, ymax, zmax), new(xmin, ymax, zmax));
Debug.DrawLine(new(xmax, ymax, zmax), new(xmax, ymax, zmin));
Debug.DrawLine(new(xmax, ymax, zmax), new(xmax, ymin, zmax));
Debug.DrawLine(new(xmin, ymin, zmin), new(xmin, ymax, zmin));
Debug.DrawLine(new(xmax, ymin, zmin), new(xmax, ymax, zmin));
Debug.DrawLine(new(xmin, ymax, zmax), new(xmin, ymin, zmax));
}
}
}

View File

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

View File

@@ -0,0 +1,887 @@
// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
// This script originated from the unity standard assets. It has been modified heavily to be camera-centric (as opposed to
// geometry-centric) and assumes a single main camera which simplifies the code.
using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.HighDefinition;
using UnityEngine.Rendering.Universal;
namespace WaveHarmonic.Crest
{
/// <summary>
/// What side of the water surface to render planar reflections for.
/// </summary>
[@GenerateDoc]
public enum WaterReflectionSide
{
/// <inheritdoc cref="Generated.WaterReflectionSide.Both"/>
[Tooltip("Both sides. Most expensive.")]
Both,
/// <inheritdoc cref="Generated.WaterReflectionSide.Above"/>
[Tooltip("Above only. Typical for planar reflections.")]
Above,
/// <inheritdoc cref="Generated.WaterReflectionSide.Below"/>
[Tooltip("Below only. For total internal reflections.")]
Below,
}
/// <summary>
/// Renders reflections for water. Currently on planar reflections.
/// </summary>
[Serializable]
public sealed partial class WaterReflections
{
[SerializeField, HideInInspector]
#pragma warning disable 414
int _Version = 0;
#pragma warning restore 414
[@Space(10)]
[@Label("Enable")]
[Tooltip("Whether planar reflections are enabled.\n\nAllocates/releases resources if state has changed.")]
[@GenerateAPI(Setter.Custom)]
[@DecoratedField, SerializeField]
internal bool _Enabled;
[@Heading("Capture")]
[Tooltip("What side of the water surface to render planar reflections for.")]
[@GenerateAPI(name: "ReflectionSide")]
[@DecoratedField, SerializeField]
internal WaterReflectionSide _Mode = WaterReflectionSide.Above;
[Tooltip("The layers to rendering into reflections.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
LayerMask _Layers = 1; // Default
[Tooltip("Resolution of the reflection texture.")]
[@GenerateAPI]
[@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.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal bool _RenderOnlySingleCamera;
[@Space(10)]
[Tooltip("Whether to render the sky or fallback to default reflections.\n\nNot rendering the sky can prevent other custom shaders (like tree leaves) from being in the final output. Enable for best compatibility.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
internal bool _Sky = true;
[Tooltip("Disables pixel lights (BIRP only).")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DisablePixelLights = true;
#pragma warning disable 414
[Tooltip("Disables shadows.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DisableShadows = true;
#pragma warning restore 414
[Tooltip("Whether to allow HDR.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _HDR = true;
[Tooltip("Whether to allow stencil operations.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _Stencil = false;
[Tooltip("Whether to allow MSAA.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _AllowMSAA = false;
[@Space(10)]
[Tooltip("Overrides global quality settings.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
QualitySettingsOverride _QualitySettingsOverride = new()
{
_OverrideLodBias = false,
_LodBias = 0.5f,
_OverrideMaximumLodLevel = false,
_MaximumLodLevel = 1,
_OverrideTerrainPixelError = false,
_TerrainPixelError = 10,
};
[@Heading("Culling")]
[Tooltip("The near clip plane clips any geometry before it, removing it from reflections.\n\nCan be used to reduce reflection leaks and support varied water level.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _ClipPlaneOffset;
[Tooltip("Anything beyond the far clip plane is not rendered.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _FarClipPlane = 1000;
[Tooltip("Disables occlusion culling.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _DisableOcclusionCulling = true;
[@Heading("Refresh Rate")]
[Tooltip("Refresh reflection every x frames (one is every frame)")]
[@Predicated(nameof(_RenderOnlySingleCamera))]
[@DecoratedField, SerializeField]
int _RefreshPerFrames = 1;
[@Predicated(nameof(_RenderOnlySingleCamera))]
[@DecoratedField, SerializeField]
int _FrameRefreshOffset = 0;
[@Heading("Oblique Matrix")]
[@Label("Enable")]
[Tooltip("An oblique matrix will clip anything below the surface for free.\n\nDisable if you have problems with certain effects. Disabling can cause other artifacts like objects below the surface to appear in reflections.")]
[@GenerateAPI]
[@DecoratedField, SerializeField]
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))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
bool _NonObliqueNearSurface;
[Tooltip("If within this distance from the surface, disable the oblique matrix.")]
[@Predicated(nameof(_NonObliqueNearSurface))]
[@Predicated(nameof(_UseObliqueMatrix))]
[@GenerateAPI]
[@DecoratedField, SerializeField]
float _NonObliqueNearSurfaceThreshold = 0.05f;
[@Space(10)]
[@DecoratedField, SerializeField]
DebugFields _Debug = new();
[Serializable]
sealed class DebugFields
{
[@DecoratedField, SerializeField]
internal bool _ShowHiddenObjects;
[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;
}
/// <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_ReflectionPositionNormal = Shader.PropertyToID("_Crest_ReflectionPositionNormal");
}
// Checked in underwater to filter cameras.
internal static Camera CurrentCamera { get; private set; }
internal WaterRenderer _Water;
internal UnderwaterRenderer _UnderWater;
RenderTexture _ReflectionTexture;
internal RenderTexture ReflectionTexture => _ReflectionTexture;
readonly Vector4[] _ReflectionPositionNormal = new Vector4[2];
Camera _CameraViewpoint;
Skybox _CameraViewpointSkybox;
Camera _CameraReflections;
Skybox _CameraReflectionsSkybox;
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;
readonly float[] _CullDistances = new float[32];
/// <summary>
/// Invoked when the reflection camera is created.
/// </summary>
public static Action<Camera> OnCameraAdded { get; set; }
internal void OnEnable()
{
_CameraViewpoint = _Water.Viewer;
_CameraViewpointSkybox = _CameraViewpoint.GetComponent<Skybox>();
// 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);
}
internal void OnDisable()
{
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, Texture2D.blackTexture);
}
internal void OnDestroy()
{
if (_CameraReflections)
{
Helpers.Destroy(_CameraReflections.gameObject);
_CameraReflections = null;
}
if (_ReflectionTexture)
{
_ReflectionTexture.Release();
Helpers.Destroy(_ReflectionTexture);
_ReflectionTexture = null;
}
}
bool ShouldRender(Camera camera)
{
// If no surface, then do not execute the reflection camera.
if (!WaterRenderer.ShouldRender(camera, _Water.Surface.Layer))
{
return false;
}
// 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)
{
return false;
}
// Avoid these types for now.
if (camera.cameraType == CameraType.Reflection)
{
return false;
}
return true;
}
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.
if (_RenderOnlySingleCamera && camera != _Water.Viewer)
{
return;
}
_CameraViewpoint = camera;
LateUpdate(context);
}
if (camera == _CameraViewpoint)
{
// TODO: Emit an event instead so WBs can listen.
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, _ReflectionTexture);
}
}
internal void OnEndCameraRendering(Camera camera)
{
if (!ShouldRender(camera))
{
return;
}
Shader.SetGlobalTexture(ShaderIDs.s_ReflectionTexture, Texture2D.blackTexture);
}
internal void LateUpdate(ScriptableRenderContext context)
{
// Frame rate limiter.
if (_LastRefreshOnFrame > 0 && RefreshPerFrames > 1)
{
// Check whether we need to refresh the frame.
if (Math.Abs(_FrameRefreshOffset) % _RefreshPerFrames != Time.renderedFrameCount % _RefreshPerFrames)
{
return;
}
}
if (_Water == null)
{
return;
}
if (!SupportsRecursiveRendering)
{
_CameraViewpoint = _Water.Viewer;
}
if (_CameraViewpoint == null)
{
return;
}
#if UNITY_EDITOR
// Fix "Screen position out of view frustum" when 2D view activated.
{
var sceneView = UnityEditor.SceneView.lastActiveSceneView;
if (sceneView != null && sceneView.in2DMode && sceneView.camera == _CameraViewpoint)
{
return;
}
}
#endif
CreateWaterObjects(_CameraViewpoint);
if (!_CameraReflections)
{
return;
}
UpdateCameraModes();
ForceDistanceCulling(_FarClipPlane);
_CameraReflections.targetTexture = _ReflectionTexture;
// TODO: Do not do this every frame.
if (_Mode != WaterReflectionSide.Both)
{
Helpers.ClearRenderTexture(_ReflectionTexture, Color.clear, depth: false);
}
// 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)
{
QualitySettings.pixelLightCount = 0;
}
// Optionally disable shadows.
var oldShadowQuality = QualitySettings.shadows;
if (_DisableShadows)
{
QualitySettings.shadows = UnityEngine.ShadowQuality.Disable;
}
_QualitySettingsOverride.Override();
// Invert culling because view is mirrored. Does not work for HDRP (handled elsewhere).
var oldCulling = GL.invertCulling;
GL.invertCulling = !oldCulling;
#if UNITY_EDITOR
try
#endif
{
Render(context);
}
#if UNITY_EDITOR
// Ensure that any global settings are restored.
finally
#endif
{
GL.invertCulling = oldCulling;
// Restore shadows.
if (_DisableShadows)
{
QualitySettings.shadows = oldShadowQuality;
}
// Restore pixel light count
if (_DisablePixelLights)
{
QualitySettings.pixelLightCount = oldPixelLightCount;
}
_QualitySettingsOverride.Restore();
CurrentCamera = null;
_Water.Surface.Root.gameObject.SetActive(true);
// Remember this frame as last refreshed.
_LastRefreshOnFrame = Time.renderedFrameCount;
}
}
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
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);
}
RenderCamera(context, _CameraReflections, Vector3.up, false, 0);
#if !(UNITY_6000_0_OR_NEWER && d_UnityURP)
Graphics.CopyTexture(target, 0, 0, _ReflectionTexture, 0, 0);
#endif
_CameraReflections.ResetProjectionMatrix();
}
if (_Mode != WaterReflectionSide.Above)
{
_ReflectionPositionNormal[1] = ComputeHorizonPositionAndNormal(_CameraReflections, _Water.SeaLevel, -0.05f, true);
if (_UnderWater._Enabled)
{
// Enable underwater layer.
_CameraReflections.cullingMask = _Layers | (1 << _UnderWater.Layer);
// We need the depth texture for underwater.
_CameraReflections.depthTextureMode = DepthTextureMode.Depth;
}
RenderCamera(context, _CameraReflections, Vector3.down, _NonObliqueNearSurface, 1);
#if !(UNITY_6000_0_OR_NEWER && d_UnityURP)
Graphics.CopyTexture(target, 0, 0, _ReflectionTexture, 1, 0);
#endif
_CameraReflections.ResetProjectionMatrix();
}
#if !(UNITY_6000_0_OR_NEWER && d_UnityURP)
RenderTexture.ReleaseTemporary(target);
#endif
_ReflectionTexture.GenerateMips();
Shader.SetGlobalVectorArray(ShaderIDs.s_ReflectionPositionNormal, _ReflectionPositionNormal);
}
void RenderCamera(ScriptableRenderContext context, Camera camera, Vector3 planeNormal, bool nonObliqueNearSurface, int slice)
{
// Find out the reflection plane: position and normal in world space
var planePosition = _Water.Position;
var offset = _ClipPlaneOffset;
{
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;
}
}
// Reflect camera around reflection plane
var distance = -Vector3.Dot(planeNormal, planePosition) - offset;
var reflectionPlane = new Vector4(planeNormal.x, planeNormal.y, planeNormal.z, distance);
var reflection = Matrix4x4.zero;
CalculateReflectionMatrix(ref reflection, reflectionPlane);
camera.worldToCameraMatrix = _CameraViewpoint.worldToCameraMatrix * reflection;
// Setup oblique projection matrix so that near plane is our reflection
// plane. This way we clip everything below/above it for free.
var clipPlane = CameraSpacePlane(camera, planePosition, planeNormal, 1.0f);
if (_UseObliqueMatrix && (!nonObliqueNearSurface || Mathf.Abs(_CameraViewpoint.transform.position.y - planePosition.y) > _NonObliqueNearSurfaceThreshold))
{
camera.projectionMatrix = _CameraViewpoint.CalculateObliqueMatrix(clipPlane);
}
// Set custom culling matrix from the current camera
camera.cullingMatrix = _CameraViewpoint.projectionMatrix * _CameraViewpoint.worldToCameraMatrix;
camera.transform.position = reflection.MultiplyPoint(_CameraViewpoint.transform.position);
var euler = _CameraViewpoint.transform.eulerAngles;
camera.transform.eulerAngles = new(-euler.x, euler.y, euler.z);
camera.cullingMatrix = camera.projectionMatrix * camera.worldToCameraMatrix;
if (SupportsRecursiveRendering)
{
Helpers.RenderCamera(camera, context, slice);
}
else
{
camera.Render();
}
}
/// <summary>
/// Limit render distance for reflection camera for first 32 layers
/// </summary>
/// <param name="farClipPlane">reflection far clip distance</param>
void ForceDistanceCulling(float farClipPlane)
{
// Cannot use spherical culling with SRPs. Will error.
if (!RenderPipelineHelper.IsLegacy)
{
return;
}
for (var i = 0; i < _CullDistances.Length; i++)
{
// The culling distance
_CullDistances[i] = farClipPlane;
}
_CameraReflections.layerCullDistances = _CullDistances;
_CameraReflections.layerCullSpherical = true;
}
void UpdateCameraModes()
{
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
if (_CameraReflections.TryGetComponent(out HDAdditionalCameraData additionalCameraData))
{
additionalCameraData.clearColorMode = _Sky ? HDAdditionalCameraData.ClearColorMode.Sky :
HDAdditionalCameraData.ClearColorMode.Color;
}
}
else
#endif
{
_CameraReflections.clearFlags = _Sky ? CameraClearFlags.Skybox : CameraClearFlags.Color;
if (_Sky && _CameraViewpoint.TryGetComponent(out _CameraViewpointSkybox))
{
if (_CameraReflectionsSkybox == null)
{
_CameraReflectionsSkybox = _CameraReflections.gameObject.AddComponent<Skybox>();
}
_CameraReflectionsSkybox.enabled = _CameraViewpointSkybox.enabled;
_CameraReflectionsSkybox.material = _CameraViewpointSkybox.material;
}
else
{
// Destroy otherwise skybox will not render if empty.
Helpers.Destroy(_CameraViewpointSkybox);
}
}
// Update other values to match current camera.
// Even if we are supplying custom camera&projection matrices,
// some of values are used elsewhere (e.g. skybox uses far plane).
_CameraReflections.farClipPlane = _CameraViewpoint.farClipPlane;
_CameraReflections.nearClipPlane = _CameraViewpoint.nearClipPlane;
_CameraReflections.orthographic = _CameraViewpoint.orthographic;
_CameraReflections.fieldOfView = _CameraViewpoint.fieldOfView;
_CameraReflections.orthographicSize = _CameraViewpoint.orthographicSize;
_CameraReflections.allowMSAA = _AllowMSAA;
_CameraReflections.aspect = _CameraViewpoint.aspect;
_CameraReflections.useOcclusionCulling = !_DisableOcclusionCulling && _CameraViewpoint.useOcclusionCulling;
_CameraReflections.depthTextureMode = _CameraViewpoint.depthTextureMode;
}
// 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;
// Reflection render texture
if (!_ReflectionTexture || _ReflectionTexture.width != _Resolution || _ReflectionTexture.format != format || _ReflectionTexture.depth != stencil)
{
if (_ReflectionTexture)
{
Helpers.Destroy(_ReflectionTexture);
}
Debug.Assert(SystemInfo.SupportsRenderTextureFormat(format), "Crest: The graphics device does not support the render texture format " + format.ToString());
_ReflectionTexture = new(_Resolution, _Resolution, stencil, format)
{
name = "_Crest_WaterReflection",
isPowerOfTwo = true,
dimension = TextureDimension.Tex2DArray,
volumeDepth = 2,
useMipMap = true,
autoGenerateMips = false,
filterMode = FilterMode.Trilinear,
};
_ReflectionTexture.Create();
}
// Camera for reflection
if (!_CameraReflections)
{
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)
{
_CameraReflections.gameObject.AddComponent<FlareLayer>();
}
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
var additionalCameraData = _CameraReflections.gameObject.AddComponent<HDAdditionalCameraData>();
additionalCameraData.invertFaceCulling = true;
additionalCameraData.defaultFrameSettings = FrameSettingsRenderType.RealtimeReflection;
additionalCameraData.backgroundColorHDR = Color.clear;
additionalCameraData.customRenderingSettings = true;
additionalCameraData.renderingPathCustomFrameSettingsOverrideMask.mask[(uint)FrameSettingsField.CustomPass] = true;
additionalCameraData.renderingPathCustomFrameSettings.SetEnabled(FrameSettingsField.CustomPass, true);
}
#endif
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
var additionalCameraData = _CameraReflections.gameObject.AddComponent<UniversalAdditionalCameraData>();
additionalCameraData.renderShadows = !_DisableShadows;
additionalCameraData.requiresColorTexture = false;
additionalCameraData.requiresDepthTexture = false;
}
#endif
OnCameraAdded?.Invoke(_CameraReflections);
}
_CameraReflections.gameObject.hideFlags = _Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave;
}
// Given position/normal of the plane, calculates plane in camera space.
Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign)
{
var offset = _ClipPlaneOffset;
{
var viewpoint = _CameraViewpoint.transform;
if (offset == 0f && viewpoint.position.y == 0f && viewpoint.rotation.eulerAngles.y == 0f)
{
// 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;
}
}
var offsetPos = pos + normal * offset;
var m = cam.worldToCameraMatrix;
var cpos = m.MultiplyPoint(offsetPos);
var cnormal = m.MultiplyVector(normal).normalized * sideSign;
return new(cnormal.x, cnormal.y, cnormal.z, -Vector3.Dot(cpos, cnormal));
}
// Calculates reflection matrix around the given plane
static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane)
{
reflectionMat.m00 = 1F - 2F * plane[0] * plane[0];
reflectionMat.m01 = -2F * plane[0] * plane[1];
reflectionMat.m02 = -2F * plane[0] * plane[2];
reflectionMat.m03 = -2F * plane[3] * plane[0];
reflectionMat.m10 = -2F * plane[1] * plane[0];
reflectionMat.m11 = 1F - 2F * plane[1] * plane[1];
reflectionMat.m12 = -2F * plane[1] * plane[2];
reflectionMat.m13 = -2F * plane[3] * plane[1];
reflectionMat.m20 = -2F * plane[2] * plane[0];
reflectionMat.m21 = -2F * plane[2] * plane[1];
reflectionMat.m22 = 1F - 2F * plane[2] * plane[2];
reflectionMat.m23 = -2F * plane[3] * plane[2];
reflectionMat.m30 = 0F;
reflectionMat.m31 = 0F;
reflectionMat.m32 = 0F;
reflectionMat.m33 = 1F;
}
/// <summary>
/// Compute intersection between the frustum far plane and given plane, and return view space
/// position and normal for this horizon line.
/// </summary>
static Vector4 ComputeHorizonPositionAndNormal(Camera camera, float positionY, float offset, bool flipped)
{
var position = Vector2.zero;
var normal = Vector2.zero;
// Set up back points of frustum.
var positionNDC = new NativeArray<Vector3>(4, Allocator.Temp);
var positionWS = new NativeArray<Vector3>(4, Allocator.Temp);
try
{
var farPlane = camera.farClipPlane;
positionNDC[0] = new(0f, 0f, farPlane);
positionNDC[1] = new(0f, 1f, farPlane);
positionNDC[2] = new(1f, 1f, farPlane);
positionNDC[3] = new(1f, 0f, farPlane);
// Project out to world.
for (var i = 0; i < positionWS.Length; i++)
{
// Eye parameter works for BIRP. With it we could skip setting matrices.
// In HDRP it doesn't work for XR MP. And completely breaks horizon in XR SPI.
positionWS[i] = camera.ViewportToWorldPoint(positionNDC[i]);
}
var intersectionsScreen = new NativeArray<Vector2>(2, Allocator.Temp);
// This is only used to disambiguate the normal later. Could be removed if we were
// more careful with point order/indices below.
var intersectionsWorld = new NativeArray<Vector3>(2, Allocator.Temp);
try
{
var count = 0;
// Iterate over each back point
for (var i = 0; i < 4; i++)
{
// Get next back point, to obtain line segment between them.
var next = (i + 1) % 4;
// See if one point is above and one point is below sea level - then sign of the two differences
// will be different, and multiplying them will give a negative.
if ((positionWS[i].y - positionY) * (positionWS[next].y - positionY) < 0f)
{
// Proportion along line segment where intersection occurs.
var proportion = Mathf.Abs((positionY - positionWS[i].y) / (positionWS[next].y - positionWS[i].y));
intersectionsScreen[count] = Vector2.Lerp(positionNDC[i], positionNDC[next], proportion);
intersectionsWorld[count] = Vector3.Lerp(positionWS[i], positionWS[next], proportion);
count++;
}
}
// Two distinct results - far plane intersects water.
if (count == 2)
{
position = intersectionsScreen[0];
var tangent = intersectionsScreen[0] - intersectionsScreen[1];
normal.x = -tangent.y;
normal.y = tangent.x;
// Disambiguate the normal. The tangent normal might go from left to right or right
// to left since we do not handle ordering of intersection points.
if (Vector3.Dot(intersectionsWorld[0] - intersectionsWorld[1], camera.transform.right) > 0f)
{
normal = -normal;
}
// Invert the normal if camera is upside down.
if (camera.transform.up.y <= 0f)
{
normal = -normal;
}
// The above will sometimes produce a normal that is inverted around 90° along the
// Z axis. Here we are using world up to make sure that water is world down.
{
var cameraFacing = Vector3.Dot(camera.transform.right, Vector3.up);
var normalFacing = Vector2.Dot(normal, Vector2.right);
if (cameraFacing > 0.75f && normalFacing > 0.9f)
{
normal = -normal;
}
else if (cameraFacing < -0.75f && normalFacing < -0.9f)
{
normal = -normal;
}
}
// Minor offset helps.
position += normal.normalized * offset;
}
}
finally
{
intersectionsScreen.Dispose();
intersectionsWorld.Dispose();
}
}
finally
{
positionNDC.Dispose();
positionWS.Dispose();
}
if (flipped)
{
normal = -normal;
}
return new(position.x, position.y, normal.x, normal.y);
}
void SetEnabled(bool previous, bool current)
{
if (previous == current) return;
if (_Water == null || !_Water.isActiveAndEnabled) return;
if (_Enabled) OnEnable(); else OnDisable();
}
#if UNITY_EDITOR
[@OnChange]
void OnChange(string propertyPath, object previousValue)
{
switch (propertyPath)
{
case nameof(_Enabled):
SetEnabled((bool)previousValue, _Enabled);
break;
}
}
#endif
}
}

View File

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