'push'
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8d53923414758449c896818d2fa45191
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 909d252964c5e41a8893e54eda8d7199
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 144e7e4c014db437fbf04fcce4ae4aac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 729ce6767111740d78a05608c8fb03c5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bc32c22349e84f57b3571b886bc8b63
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fb2445377c8974383bd1b3b4afa85646
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8926d03a42e31434eb70caddd59f0466
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b48c0a50256ea4177ba95870d7a211b2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ea612be74ded47e5a970d7e4e8d540a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 212222792a1a241d889bed1dc799ba37
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12fa5fcd0e5ac436b8581c4441a2683e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74a58ee5c012f4e38be0d38da61de24a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 391e7ec2ec9194dbeb14b0b0af03a29f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b8b8696e988b24f1e832400fdd148451
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 205
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user