Files
Fishing2/Packages/com.waveharmonic.crest/Runtime/Scripts/WaterRenderer.cs
2026-01-31 00:32:49 +08:00

2115 lines
68 KiB
C#

// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Rendering;
using WaveHarmonic.Crest.Internal;
using WaveHarmonic.Crest.RelativeSpace;
using WaveHarmonic.Crest.Utility;
namespace WaveHarmonic.Crest
{
interface IReportWaveDisplacement
{
/// <summary>
/// Vertical displacement which affects scale via DropDetailHeightBasedOnWaves.
/// </summary>
float ReportWaveDisplacement(WaterRenderer water, float displacement);
}
/// <summary>
/// The main script for the water system.
/// </summary>
/// <remarks>
/// Attach this to an object to create water. This script initializes the various
/// data types and systems and moves/scales the water based on the viewpoint. It
/// also hosts a number of global settings that can be tweaked here.
/// </remarks>
public sealed partial class WaterRenderer : ManagerBehaviour<WaterRenderer>
{
internal const string k_RunUpdateMarker = "Crest.WaterRenderer.RunUpdate";
static readonly Unity.Profiling.ProfilerMarker s_RunUpdateMarker = new(k_RunUpdateMarker);
internal static partial class ShaderIDs
{
public static readonly int s_Center = Shader.PropertyToID("g_Crest_WaterCenter");
public static readonly int s_Scale = Shader.PropertyToID("g_Crest_WaterScale");
public static readonly int s_Time = Shader.PropertyToID("g_Crest_Time");
public static readonly int s_CascadeData = Shader.PropertyToID("g_Crest_CascadeData");
public static readonly int s_CascadeDataSource = Shader.PropertyToID("g_Crest_CascadeDataSource");
public static readonly int s_LodChange = Shader.PropertyToID("g_Crest_LodChange");
public static readonly int s_MeshScaleLerp = Shader.PropertyToID("g_Crest_MeshScaleLerp");
public static readonly int s_LodCount = Shader.PropertyToID("g_Crest_LodCount");
public static readonly int s_WaterDepthAtViewer = Shader.PropertyToID("g_Crest_WaterDepthAtViewer");
public static readonly int s_MaximumVerticalDisplacement = Shader.PropertyToID("g_Crest_MaximumVerticalDisplacement");
public static readonly int s_HorizonNormal = Shader.PropertyToID("g_Crest_HorizonNormal");
// Shader Properties
public static readonly int s_AbsorptionColor = Shader.PropertyToID("_Crest_AbsorptionColor");
public static readonly int s_Absorption = Shader.PropertyToID("_Crest_Absorption");
public static readonly int s_Scattering = Shader.PropertyToID("_Crest_Scattering");
public static readonly int s_Anisotropy = Shader.PropertyToID("_Crest_Anisotropy");
public static readonly int s_AmbientTerm = Shader.PropertyToID("_Crest_AmbientTerm");
public static readonly int s_DirectTerm = Shader.PropertyToID("_Crest_DirectTerm");
public static readonly int s_ShadowsAffectsAmbientFactor = Shader.PropertyToID("_Crest_ShadowsAffectsAmbientFactor");
public static readonly int s_PlanarReflectionsEnabled = Shader.PropertyToID("_Crest_PlanarReflectionsEnabled");
public static readonly int s_Occlusion = Shader.PropertyToID("_Crest_Occlusion");
public static readonly int s_OcclusionUnderwater = Shader.PropertyToID("_Crest_OcclusionUnderwater");
// Motion Vectors
public static readonly int s_CenterDelta = Shader.PropertyToID("g_Crest_WaterCenterDelta");
public static readonly int s_ScaleChange = Shader.PropertyToID("g_Crest_WaterScaleChange");
// Underwater
public static readonly int s_VolumeExtinctionLength = Shader.PropertyToID("_Crest_VolumeExtinctionLength");
}
//
// Viewer
//
Transform GetViewpoint()
{
if (MultipleViewpoints)
{
return CurrentCamera == null ? null : CurrentCamera.transform;
}
#if UNITY_EDITOR
if (EditorMultipleViewpoints && CurrentCamera != null && CurrentCamera.cameraType == CameraType.SceneView)
{
return CurrentCamera.transform;
}
if (!EditorMultipleViewpoints && !Application.isPlaying && _FollowSceneCamera && SceneView.lastActiveSceneView != null && IsSceneViewActive)
{
return SceneView.lastActiveSceneView.camera.transform;
}
#endif
if (_Viewpoint != null)
{
return _Viewpoint;
}
// Even with performance improvements, it is still good to cache whenever possible.
var camera = Viewer;
if (camera != null)
{
return camera.transform;
}
return null;
}
internal Camera GetViewer(bool includeSceneCamera = true, bool initial = false)
{
if (!initial && MultipleViewpoints)
{
return CurrentCamera;
}
#if UNITY_EDITOR
if (!initial && EditorMultipleViewpoints && includeSceneCamera && CurrentCamera != null && CurrentCamera.cameraType == CameraType.SceneView)
{
return CurrentCamera;
}
if ((initial || !EditorMultipleViewpoints) && includeSceneCamera && !Application.isPlaying && _FollowSceneCamera && SceneView.lastActiveSceneView != null && IsSceneViewActive)
{
return SceneView.lastActiveSceneView.camera;
}
#endif
if (_Camera != null)
{
return _Camera;
}
// Unity has greatly improved performance of this operation in 2019.4.9.
return Camera.main;
}
/// <summary>
/// The current viewer (center of detail).
/// </summary>
internal Camera CurrentCamera { get; private set; }
readonly SampleCollisionHelper _CenterOfDetailDisplacementCorrectionHelper = new();
//
// Viewer Height
//
/// <summary>
/// The water changes scale when viewer changes altitude, this gives the interpolation param between scales.
/// </summary>
internal float ViewerAltitudeLevelAlpha { get; private set; }
/// <summary>
/// Vertical offset of camera vs water surface.
/// </summary>
public float ViewerHeightAboveWater { get; private set; }
/// <summary>
/// Vertical offset of viewpoint vs water surface.
/// </summary>
public float ViewpointHeightAboveWater { get; private set; }
/// <summary>
/// Distance of camera to shoreline. Positive if over water and negative if over land.
/// </summary>
public float ViewerDistanceToShoreline { get; private set; }
/// <summary>
/// Smoothly varying version of viewpoint height to combat sudden changes in water level that are possible
/// when there are local bodies of water
/// </summary>
float _ViewpointHeightAboveWaterSmooth;
readonly SampleCollisionHelper _SampleHeightHelper = new();
readonly SampleDepthHelper _SampleDepthHelper = new();
internal float _ViewerHeightAboveWaterPerCamera;
readonly SampleCollisionHelper _SampleHeightHelperPerCamera = new();
//
// Teleport Threshold
//
float _TeleportTimerForHeightQueries;
bool _IsFirstFrameSinceEnabled = true;
internal bool _HasTeleportedThisFrame;
Vector3 _OldViewpointPosition;
#if d_WaveHarmonic_Crest_ShiftingOrigin
Vector3 TeleportOriginThisFrame => ShiftingOrigin.ShiftThisFrame;
#else
Vector3 TeleportOriginThisFrame => Vector3.zero;
#endif
//
// Wind
//
internal float WindSpeedKPH => _WindSpeed;
bool WindSpeedOverriden => _WindZone == null || _OverrideWindZoneWindSpeed;
bool WindDirectionOverriden => _WindZone == null || _OverrideWindZoneWindDirection;
bool WindTurbulenceOverriden => _WindZone == null || _OverrideWindZoneWindTurbulence;
float GetWindSpeed()
{
return _OverrideWindZoneWindSpeed || _WindZone == null ? _WindSpeed : _WindZone.windMain * 3.6f;
}
float GetWindDirection()
{
var wind = _WindZone != null ? _WindZone.transform : null;
return _OverrideWindZoneWindDirection || wind == null
? _WindDirection
: Mathf.Atan2(wind.forward.z, wind.forward.x) * Mathf.Rad2Deg;
}
float GetWindTurbulence()
{
return _OverrideWindZoneWindTurbulence || _WindZone == null ? _WindTurbulence : _WindZone.windTurbulence;
}
//
// Transform
//
internal Vector3 Position { get; private set; }
internal GameObject Container { get; private set; }
/// <summary>
/// Sea level is given by y coordinate of GameObject with WaterRenderer script.
/// </summary>
public float SeaLevel => Position.y;
// Anything higher (minus 1 for near plane) will be clipped.
const float k_RenderAboveSeaLevel = 10000f;
// Anything lower will be clipped.
const float k_RenderBelowSeaLevel = 10000f;
Matrix4x4[] _ProjectionMatrix;
internal Matrix4x4 GetProjectionMatrix(int slice) => _ProjectionMatrix[slice];
internal static Matrix4x4 CalculateViewMatrixFromSnappedPositionRHS(Vector3 snapped)
{
return Helpers.CalculateWorldToCameraMatrixRHS(snapped + Vector3.up * k_RenderAboveSeaLevel, Quaternion.AngleAxis(90f, Vector3.right));
}
//
// Time Provider
//
/// <summary>
/// Loosely a stack for time providers.
/// </summary>
/// <remarks>
/// The last <see cref="TimeProvider"/> in the list is the active one. When a
/// <see cref="TimeProvider"/> gets added to the stack, it is bumped to the top of
/// the list. When a <see cref="TimeProvider"/> is removed, all instances of it are
/// removed from the stack. This is less rigid than a real stack which would be
/// harder to use as users have to keep a close eye on the order that things are
/// pushed/popped.
/// </remarks>
public Utility.Internal.Stack<ITimeProvider> TimeProviders { get; private set; } = new();
/// <summary>
/// The current time provider.
/// </summary>
public ITimeProvider TimeProvider => TimeProviders.Peek();
internal float CurrentTime => TimeProvider.Time;
internal float DeltaTime => TimeProvider.Delta;
//
// Environment
//
/// <summary>
/// The primary light that affects the water. This should be a directional light.
/// </summary>
Light GetPrimaryLight() => _PrimaryLight == null ? RenderSettings.sun : _PrimaryLight;
/// <summary>
/// Physics gravity applied to water.
/// </summary>
public float Gravity => _GravityMultiplier * Mathf.Abs(_OverrideGravity ? _GravityOverride : Physics.gravity.y);
//
// Rendering
//
// Used as an extra check to prevent null exceptions, as the events raised when an
// RP change happen too late for some things.
RenderPipeline _SetUpFor;
internal bool RenderBeforeTransparency =>
#if d_Crest_LegacyUnderwater
false;
#else
_InjectionPoint == WaterInjectionPoint.BeforeTransparent;
#endif
#if d_CrestPortals
internal bool Portaled => _Portals.Active;
#else
internal bool Portaled => false;
#endif
internal MaskRenderer _Mask;
// Flags
bool _DonePerCameraHeight;
internal bool _PerCameraHeightReady;
bool GetWriteMotionVectors() =>
#if !UNITY_6000_0_OR_NEWER
!RenderPipelineHelper.IsUniversal &&
#endif
_WriteMotionVectors;
bool GetWriteToColorTexture()
{
return (_WriteToColorTexture && RenderBeforeTransparency) || Meniscus.RequiresOpaqueTexture;
}
bool GetWriteToDepthTexture()
{
return _WriteToDepthTexture && Surface.Enabled;
}
internal static bool ShouldRender(Camera camera)
{
#if UNITY_EDITOR
// Preview camera are for preview game view, preview panes, material previews etc.
if (camera.cameraType == CameraType.Preview)
{
return false;
}
#endif
return true;
}
internal static bool ShouldRender(Camera camera, int layer)
{
if (!ShouldRender(camera))
{
return false;
}
if (!Helpers.MaskIncludesLayer(camera.cullingMask, layer))
{
return false;
}
return true;
}
internal static bool ShouldRender(Camera camera, WaterCameraExclusion exclusion)
{
if (camera.cameraType == CameraType.SceneView)
{
return true;
}
if (camera.TryGetComponent<WaterCamera>(out var wc) && wc.isActiveAndEnabled)
{
return true;
}
var exclude =
// Reflection cameras are all typically hidden. We have a separate flag for them.
(exclusion.HasFlag(WaterCameraExclusion.Hidden) && camera.hideFlags.HasFlag(HideFlags.HideInHierarchy) && camera.cameraType != CameraType.Reflection) ||
(exclusion.HasFlag(WaterCameraExclusion.Reflection) && camera.cameraType == CameraType.Reflection) ||
(exclusion.HasFlag(WaterCameraExclusion.NonMainCamera) && !camera.CompareTag("MainCamera"));
return !exclude;
}
internal static bool ShouldRender(Camera camera, int layer, WaterCameraExclusion exclusion)
{
if (!ShouldRender(camera, layer))
{
return false;
}
if (!ShouldRender(camera, exclusion))
{
return false;
}
return true;
}
bool ShouldExecute(Camera camera, int layer, WaterCameraExclusion exclusion)
{
if (SingleViewpoint)
{
return false;
}
#if UNITY_EDITOR
// Editor Multiple Viewpoints is for scene view and viewer only.
if (!MultipleViewpoints && camera.cameraType != CameraType.SceneView && camera != GetViewer(false))
{
return false;
}
#endif
if (!ShouldRender(camera, layer, exclusion))
{
return false;
}
return true;
}
//
// Material
//
/// <summary>
/// Calculates the absorption value from the absorption color.
/// </summary>
/// <param name="color">The absorption color.</param>
/// <returns>The absorption value (XYZ value).</returns>
public static Vector4 CalculateAbsorptionValueFromColor(Color color)
{
return UpdateAbsorptionFromColor(color);
}
internal static Vector4 UpdateAbsorptionFromColor(Color color)
{
var alpha = Vector3.zero;
alpha.x = Mathf.Log(Mathf.Max(color.r, 0.0001f));
alpha.y = Mathf.Log(Mathf.Max(color.g, 0.0001f));
alpha.z = Mathf.Log(Mathf.Max(color.b, 0.0001f));
// Magic numbers that make fog density easy to control using alpha channel
return (-color.a * 32f * alpha / 5f).XYZN(1f);
}
internal static void UpdateAbsorptionFromColor(Material material)
{
var fogColour = material.GetColor(ShaderIDs.s_AbsorptionColor);
var alpha = Vector3.zero;
alpha.x = Mathf.Log(Mathf.Max(fogColour.r, 0.0001f));
alpha.y = Mathf.Log(Mathf.Max(fogColour.g, 0.0001f));
alpha.z = Mathf.Log(Mathf.Max(fogColour.b, 0.0001f));
// Magic numbers that make fog density easy to control using alpha channel
material.SetVector(ShaderIDs.s_Absorption, UpdateAbsorptionFromColor(fogColour));
}
//
// Simulations
//
internal List<Lod> Simulations { get; } = new();
//
// Instance
//
bool _Initialized;
internal bool Active => enabled && this == Instance;
//
// Hash
//
// A hash of the settings used to generate the water, used to regenerate when necessary
int _GeneratedSettingsHash;
//
// Runtime Environment
//
/// <summary>
/// Is runtime environment without graphics card
/// </summary>
public static bool RunningWithoutGraphics
{
get
{
var noGPU = SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null;
var emulateNoGPU = Instance != null && Instance._Debug._ForceNoGraphics;
return noGPU || emulateNoGPU;
}
}
// No GPU or emulate no GPU.
internal bool IsRunningWithoutGraphics => SystemInfo.graphicsDeviceType == GraphicsDeviceType.Null || _Debug._ForceNoGraphics;
/// <summary>
/// Is runtime environment non-interactive (not displaying to user).
/// </summary>
[System.Obsolete("We no longer care whether Unity is running in non-interactive mode.")]
public static bool RunningHeadless => false;
//
// Frame Timing
//
/// <summary>
/// The frame count for Crest.
/// </summary>
public static int FrameCount => Time.frameCount;
//
// Level of Detail
//
internal static System.Action<WaterRenderer, Camera> s_OnBeforeBuildCommandBuffer;
internal const string k_DrawLodData = "Crest.LodData";
internal CommandBuffer SimulationBuffer { get; private set; }
// Scale, Weight, MaximumWaveLength, Unused
BufferedData<Vector4[]> _CascadeData;
internal BufferedData<Vector4[]> CascadeData { get; private set; }
// NOTE: hardcoded for now. There is typically at least one persistent simulation, and
// they never go beyond two frames.
internal int BufferSize => 2;
internal float MaximumWavelength(int slice)
{
return MaximumWavelength(CalcLodScale(slice));
}
internal float MaximumWavelength(float scale)
{
var maximumDiameter = 4f * scale;
// TODO: Do we need to pass in resolution? Could resolution mismatch with animated
// and dynamic waves be an issue?
var maximumTexelSize = maximumDiameter / LodResolution;
var texelsPerWave = 2f;
return 2f * maximumTexelSize * texelsPerWave;
}
//
// Scale
//
/// <summary>
/// Current water scale (changes with viewer altitude).
/// </summary>
public float Scale { get; private set; }
internal float CalcLodScale(float slice) => Scale * Mathf.Pow(2f, slice);
internal float CalcGridSize(int slice) => CalcLodScale(slice) / LodResolution;
/// <summary>
/// Could the water horizontal scale increase (for e.g. if the viewpoint gains altitude). Will be false if water already at maximum scale.
/// </summary>
internal bool ScaleCouldIncrease => _ScaleRange.y == Mathf.Infinity || Scale < _ScaleRange.y * 0.99f;
/// <summary>
/// Could the water horizontal scale decrease (for e.g. if the viewpoint drops in altitude). Will be false if water already at minimum scale.
/// </summary>
internal bool ScaleCouldDecrease => Scale > _ScaleRange.x * 1.01f;
internal int ScaleDifferencePower2 { get; private set; }
//
// Query Providers
//
/// <summary>
/// Provides water shape to CPU.
/// </summary>
public ICollisionProvider CollisionProvider => AnimatedWavesLod?.Provider;
/// <summary>
/// Provides flow to the CPU.
/// </summary>
public IFlowProvider FlowProvider => FlowLod?.Provider;
/// <summary>
/// Provides water depth and distance to water edge to the CPU.
/// </summary>
public IDepthProvider DepthProvider => DepthLod?.Provider;
//
// Component
//
// Drive state from OnEnable and OnDisable? OnEnable on RegisterLodDataInput seems to get called on script reload
private protected override void Initialize()
{
base.Initialize();
_SetUpFor = RenderPipelineHelper.RenderPipeline;
_IsFirstFrameSinceEnabled = true;
CurrentCamera = GetViewer(initial: true);
// Recompiled in play mode.
if (_Mask == null)
{
_Initialized = false;
}
if (_Initialized)
{
Enable();
return;
}
Utility.RTHandles.Initialize();
_Mask = MaskRenderer.Instantiate(this);
Meniscus.Initialize(this);
Surface._Water = this;
_Reflections._Water = this;
_Reflections._UnderWater = _Underwater;
_Underwater._Water = this;
#if d_CrestPortals
_Underwater._Portals = _Portals;
_Portals._Water = this;
_Portals._UnderWater = _Underwater;
#endif
_DepthLod._Water = this;
_LevelLod._Water = this;
_FlowLod._Water = this;
_DynamicWavesLod._Water = this;
_AnimatedWavesLod._Water = this;
_FoamLod._Water = this;
_ClipLod._Water = this;
_AbsorptionLod._Water = this;
_ScatteringLod._Water = this;
_AlbedoLod._Water = this;
_ShadowLod._Water = this;
// Add simulations to a list for common operations. Order is important.
Simulations.Clear();
Simulations.Add(_DepthLod);
Simulations.Add(_LevelLod);
Simulations.Add(_FlowLod);
Simulations.Add(_DynamicWavesLod);
Simulations.Add(_AnimatedWavesLod);
Simulations.Add(_FoamLod);
Simulations.Add(_AbsorptionLod);
Simulations.Add(_ScatteringLod);
Simulations.Add(_ClipLod);
Simulations.Add(_AlbedoLod);
Simulations.Add(_ShadowLod);
// Setup a default time provider, and add the override one (from the inspector)
TimeProviders.Clear();
// Put a base TP that should always be available as a fallback
TimeProviders.Push(new DefaultTimeProvider());
// Add the TP from the inspector
if (_TimeProvider != null)
{
TimeProviders.Push(_TimeProvider);
}
if (!VerifyRequirements())
{
enabled = false;
return;
}
SimulationBuffer ??= new()
{
name = k_DrawLodData,
};
Container = new()
{
name = "Container",
hideFlags = _Debug._ShowHiddenObjects ? HideFlags.DontSave : HideFlags.HideAndDontSave
};
Container.transform.SetParent(transform, worldPositionStays: false);
this.Manage(Container);
Scale = Mathf.Clamp(Scale, _ScaleRange.x, _ScaleRange.y);
foreach (var simulation in Simulations)
{
// Bypasses Enabled and has an internal check.
if (!simulation._Enabled) continue;
simulation.Initialize();
}
CascadeData = _CascadeData = new(BufferSize, () => new Vector4[Lod.k_MaximumSlices + 1]);
_ProjectionMatrix = new Matrix4x4[LodLevels];
if (Application.isPlaying && _Debug._AttachDebugGUI && !TryGetComponent<DebugGUI>(out _))
{
gameObject.AddComponent<DebugGUI>().hideFlags = HideFlags.DontSave;
}
_GeneratedSettingsHash = CalculateSettingsHash();
if (Surface.Enabled)
{
Surface.Initialize();
}
foreach (var body in WaterBody.WaterBodies)
{
if (body._Material != null)
{
Surface.UpdateMaterial(body._Material, ref body._MotionVectorMaterial);
}
}
Enable();
_Initialized = true;
}
void OnDisable()
{
Disable();
// Always clean up in OnDisable during edit mode as OnDestroy is not always called.
if (_Debug._DestroyResourcesInOnDisable || !Application.isPlaying)
{
Destroy();
}
}
void OnDestroy()
{
// Only clean up in OnDestroy when not in edit mode.
if (_Debug._DestroyResourcesInOnDisable || !Application.isPlaying)
{
return;
}
Destroy();
}
private protected override void LateUpdate()
{
#if CREST_DEBUG
#if UNITY_EDITOR
if (_SkipForTesting)
{
return;
}
#endif
#endif
// Queries keeps LateUpdate running regardless of editor settings. If queries
// stops, then the water will pause.
RunUpdate();
}
//
// Methods
//
private protected override void Enable()
{
base.Enable();
// For running the system in the background and fallbacks.
if (!SingleViewpoint && !RunningWithoutGraphics)
{
_EndOfFrame = StartCoroutine(UpdateSkippedCameras());
}
// Needs to be first or will get assertions etc. Unity bug likely.
RenderPipelineManager.activeRenderPipelineTypeChanged -= OnActiveRenderPipelineTypeChanged;
RenderPipelineManager.activeRenderPipelineTypeChanged += OnActiveRenderPipelineTypeChanged;
// Needs to run even without graphics to initialize provider.
foreach (var simulation in Simulations)
{
simulation.SetGlobals(enable: true);
if (!simulation.Enabled) continue;
simulation.Enable();
}
if (IsRunningWithoutGraphics)
{
// We need nothing from here on.
return;
}
#if d_WaveHarmonic_Crest_ShiftingOrigin
ShiftingOrigin.OnShift -= OnOriginShift;
ShiftingOrigin.OnShift += OnOriginShift;
#endif
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
// This event should not when not using the built-in renderer, but in some cases it can in the editor like
// when using scene filtering.
if (RenderPipelineHelper.IsLegacy)
{
Camera.onPreCull -= OnBeginCameraRendering;
Camera.onPreCull += OnBeginCameraRendering;
Camera.onPostRender -= OnEndCameraRendering;
Camera.onPostRender += OnEndCameraRendering;
}
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
// Always enable as it sets requirements.
SurfaceRenderer.WaterSurfaceRenderPass.Enable(this);
}
#endif
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
if (RenderBeforeTransparency)
{
SurfaceRenderer.WaterSurfaceCustomPass.Enable(this);
}
CrestInternalCopyToTextureCustomPass.Enable(this);
}
#if UNITY_EDITOR
if (RenderPipelineHelper.IsHighDefinition)
{
RenderPipelineManager.beginContextRendering -= OnBeginContextRendering;
RenderPipelineManager.beginContextRendering += OnBeginContextRendering;
}
#endif
#endif
Container.SetActive(true);
_Mask.Enable();
if (_Underwater._Enabled)
{
_Underwater.OnEnable();
}
if (Meniscus.Enabled)
{
Meniscus.Enable();
}
#if d_UnityURP
if (RenderPipelineHelper.IsUniversal)
{
if (WriteToColorTexture || WriteToDepthTexture)
{
CopyTargetsRenderPass.Enable(this);
}
}
#endif
#if d_CrestPortals
if (_Portals._Enabled)
{
_Portals.OnEnable();
}
#endif
if (_Reflections._Enabled)
{
_Reflections.OnEnable();
}
}
// Because we cannot pass null when using built-in render pipeline.
// Being a struct there should not be any side effects.
internal ScriptableRenderContext _Context = new();
void OnBeginCameraRendering(Camera camera)
{
#if CREST_DEBUG
if (_Debug._SimulatePaused)
{
return;
}
#endif
if (_SetUpFor != RenderPipelineHelper.RenderPipeline)
{
return;
}
if (!_Initialized)
{
return;
}
Utility.RTHandles.OnBeginCameraRendering(camera);
OnBeginCameraRendering(_Context, camera);
}
// OnBeginCameraRendering or OnPreCull
void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
#if CREST_DEBUG
if (_Debug._SimulatePaused)
{
return;
}
#endif
// Guard against being called before the RP change events are raised.
if (_SetUpFor != RenderPipelineHelper.RenderPipeline)
{
return;
}
if (!_Initialized)
{
return;
}
#if UNITY_EDITOR
UpdateLastActiveSceneCamera(camera);
#endif
if (!ShouldRender(camera))
{
return;
}
var noSurface = !Surface.Enabled || !ShouldRender(camera, Surface.Layer, Surface._CameraExclusions);
var noVolume = !Underwater.Enabled || !ShouldRender(camera, Underwater.Layer, Underwater._CameraExclusions);
// MainCamera is a requirement. Guard against this.
if ((!noSurface || !noVolume) && GetViewer(initial: true) == null)
{
noSurface = noVolume = true;
}
if (noSurface)
{
// For exclusion rules. Redundant in some cases.
Surface.ForceRenderingOff = true;
}
// Ensure TIRs render.
// TODO: do this check properly, as easy as this is.
if (camera == Reflections.ReflectionCamera && Underwater.Enabled && Helpers.MaskIncludesLayer(camera.cullingMask, Underwater.Layer))
{
noVolume = false;
}
// Nothing to render to this camera.
if (noSurface && noVolume)
{
return;
}
_HasAnyViewerRendered = true;
if (ShouldExecute(camera, Surface.Layer, _CameraExclusions))
{
if (!_Cameras.Contains(camera))
{
_Cameras.Add(camera);
}
_SeparateViewpoint = true;
RunUpdate(camera);
}
// Project water normal onto camera plane.
Shader.SetGlobalVector(ShaderIDs.s_HorizonNormal, new Vector2
(
Vector3.Dot(Vector3.up, camera.transform.right),
Vector3.Dot(Vector3.up, camera.transform.up)
));
// Must render first so that we do not overwrite work below for game camera.
// Reflections only make sense with an active surface.
if (_Reflections._Enabled && Surface.Enabled)
{
_Reflections.OnBeginCameraRendering(context, camera);
}
// Must render before the mask, but cannot execute in the mask pass.
if (Underwater.Enabled)
{
Underwater.ExecuteHeightField(camera);
}
if (_Mask.Enabled)
{
_Mask.OnBeginCameraRendering(camera);
}
// Water lighting etc.
#pragma warning disable format
#if d_UnityHDRP
if (RenderPipelineHelper.IsHighDefinition)
{
UpdateHighDefinitionLighting(camera);
}
else
#endif
if (RenderPipelineHelper.IsLegacy)
{
OnBeginCameraRenderingLegacy(camera);
}
#pragma warning restore format
// Always execute before surface, as order is only important when rendering volume
// before surface.
if (Underwater._Enabled)
{
Underwater.OnBeginCameraRendering(context, camera);
}
#if d_CrestPortals
// Call between volume and surface. Sets water line uniforms.
if (Portals.Enabled)
{
Portals.OnBeginCameraRendering(camera);
}
#endif
if (Surface.Enabled)
{
Surface.OnBeginCameraRendering(context, camera);
}
#pragma warning disable format
#if d_UnityURP
// Always execute after surface.
if (RenderPipelineHelper.IsUniversal)
{
CopyTargetsRenderPass.Instance?.OnBeginCameraRendering(context, camera);
}
else
#endif
if (RenderPipelineHelper.IsLegacy)
{
OnLegacyCopyPass(camera);
}
#pragma warning restore format
// Execute after copy pass in case refraction.
if (Meniscus.Enabled)
{
Meniscus.Renderer.OnBeginCameraRendering(camera);
}
if (_ShadowLod.Enabled)
{
_ShadowLod.OnBeginCameraRendering(context, camera);
}
}
void OnEndCameraRendering(Camera camera)
{
#if CREST_DEBUG
if (_Debug._SimulatePaused)
{
return;
}
#endif
OnEndCameraRendering(_Context, camera);
}
void OnEndCameraRendering(ScriptableRenderContext context, Camera camera)
{
#if CREST_DEBUG
if (_Debug._SimulatePaused)
{
return;
}
#endif
_DonePerCameraHeight = false;
_PerCameraHeightReady = false;
#if d_UnityHDRP
_DoneHighDefinitionLighting = false;
#endif
if (Reflections.ReflectionCamera != camera)
{
Surface.ForceRenderingOff = false;
}
if (RenderPipelineHelper.IsLegacy)
{
OnEndCameraRenderingLegacy(camera);
}
if (_Mask.Enabled)
{
_Mask.OnEndCameraRendering(camera);
}
if (Meniscus.Enabled)
{
Meniscus.Renderer.OnEndCameraRendering(camera);
}
if (Underwater._Enabled)
{
Underwater.OnEndCameraRendering(camera);
}
if (Surface.Enabled)
{
Surface.OnEndCameraRendering(camera);
}
if (_Reflections._Enabled)
{
_Reflections.OnEndCameraRendering(camera);
}
if (_ShadowLod.Enabled)
{
_ShadowLod.OnEndCameraRendering(camera);
}
#if d_CrestPortals
if (_Portals.Enabled)
{
_Portals.OnEndCameraRendering(camera);
}
#endif
if (camera == CurrentCamera)
{
_SeparateViewpoint = false;
_InCameraLoop = false;
CurrentCamera = null;
}
}
internal void UpdatePerCameraHeight(Camera camera)
{
if (_DonePerCameraHeight)
{
return;
}
// We have already sampled this height.
if ((MultipleViewpoints || camera == Viewer) && ShouldExecuteQueries(camera))
{
_ViewerHeightAboveWaterPerCamera = ViewerHeightAboveWater;
_PerCameraHeightReady = true;
_DonePerCameraHeight = true;
return;
}
// This will be 1-frame behind. We allow multiple calls because the camera may
// render multiple times per frame. One case is the scene camera when focus is
// gained or using the frame debugger.
var viewpoint = camera.transform.position;
_PerCameraHeightReady |= _SampleHeightHelperPerCamera.SampleHeight(System.HashCode.Combine(_SampleHeightHelperPerCamera, camera), viewpoint, out var height, allowMultipleCallsPerFrame: true);
_ViewerHeightAboveWaterPerCamera = viewpoint.y - height;
// Planar camera mirrors current camera, but treat at same location.
if (camera == Reflections.ReflectionCamera) _ViewerHeightAboveWaterPerCamera = -_ViewerHeightAboveWaterPerCamera;
_DonePerCameraHeight = true;
}
void OnActiveRenderPipelineTypeChanged()
{
_Mask?.Destroy();
_Mask = MaskRenderer.Instantiate(this);
Meniscus.OnActiveRenderPipelineTypeChanged();
if (isActiveAndEnabled)
{
// Must destroy as there is still some state left like buffer count.
Disable();
Destroy();
Initialize();
}
}
internal void Rebuild()
{
Disable();
Destroy();
OnEnable();
}
bool VerifyRequirements()
{
if (!RunningWithoutGraphics)
{
if (Application.platform == RuntimePlatform.WebGLPlayer && !Helpers.IsWebGPU)
{
Debug.LogError("Crest: Crest does not support WebGL backends.", this);
return false;
}
#if UNITY_EDITOR
if (SystemInfo.graphicsDeviceType is GraphicsDeviceType.OpenGLES3 or GraphicsDeviceType.OpenGLCore)
{
Debug.LogError("Crest: Crest does not support OpenGL backends.", this);
return false;
}
#endif
if (SystemInfo.graphicsShaderLevel < 45)
{
Debug.LogError("Crest: Crest requires graphics devices that support shader level 4.5 or above.", this);
return false;
}
if (!SystemInfo.supportsComputeShaders)
{
Debug.LogError("Crest: Crest requires graphics devices that support compute shaders.", this);
return false;
}
if (!SystemInfo.supports2DArrayTextures)
{
Debug.LogError("Crest: Crest requires graphics devices that support 2D array textures.", this);
return false;
}
}
return true;
}
int CalculateSettingsHash()
{
var settingsHash = Hash.CreateHash();
// Add all the settings that require rebuilding..
Hash.AddInt(_Resolution, ref settingsHash);
Hash.AddInt(_Slices, ref settingsHash);
Hash.AddBool(WriteMotionVectors, ref settingsHash);
Hash.AddBool(_Debug._ForceNoGraphics, ref settingsHash);
Hash.AddBool(_Debug._ShowHiddenObjects, ref settingsHash);
return settingsHash;
}
void RunUpdate()
{
// Rebuild if needed. Needs to run in builds (for MVs at the very least).
if (CalculateSettingsHash() != _GeneratedSettingsHash)
{
Rebuild();
}
if (RunningWithoutGraphics)
{
// All we need for servers.
BroadcastUpdate();
Position = new(0f, transform.position.y, 0f);
}
else
{
BroadcastUpdate();
if (SingleViewpoint)
{
RunUpdate(Viewer);
}
else
{
if (FallBackRequired)
{
_SeparateViewpoint = true;
RunUpdate(GetViewer(initial: true));
_SeparateViewpoint = false;
}
PruneCameraData();
_HasAnyViewpointExecuted = false;
_HasAnyViewerRendered = false;
}
}
// This use to execute after the system command buffer, after all properties had
// been updated. But none of the calls used any of that data.
base.LateUpdate();
// Run queries at end of update. For CollProviderBakedFFT calling this kicks off
// collision processing job, and the next call to Query() will force a complete, and
// we don't want that to happen until they've had a chance to run, so schedule them
// late.
if (AnimatedWavesLod.QuerySource == LodQuerySource.CPU)
{
AnimatedWavesLod.Provider?.UpdateQueries(this);
}
}
void RunUpdate(Camera camera)
{
if (camera == _Reflections.ReflectionCamera)
{
return;
}
// Only set to a camera which is a center of detail.
CurrentCamera = camera;
s_RunUpdateMarker.Begin(this);
LoadCameraData(camera);
if (!_Debug._DisableFollowViewpoint && CurrentCamera != null)
{
LateUpdatePosition();
LateUpdateViewerHeight();
LateUpdateScale();
}
else
{
Position = new(0f, transform.position.y, 0f);
}
// Set global shader params
Shader.SetGlobalFloat(ShaderIDs.s_Time, CurrentTime);
Shader.SetGlobalFloat(ShaderIDs.s_LodCount, LodLevels);
// Construct the command buffer and attach it to the camera so that it will be executed in the render.
{
SimulationBuffer.Clear();
// Needs updated transform values like scale.
WritePerFrameMaterialParams(SimulationBuffer);
s_OnBeforeBuildCommandBuffer?.Invoke(this, camera);
foreach (var simulation in Simulations)
{
if (!simulation.Enabled) continue;
if (_IsEndOfFrame && simulation.SkipEndOfFrame) continue;
simulation.BuildCommandBuffer(this, SimulationBuffer);
}
// This will execute at the beginning of the frame before the graphics queue.
Graphics.ExecuteCommandBuffer(SimulationBuffer);
foreach (var simulation in Simulations)
{
if (!simulation.Enabled) continue;
if (_IsEndOfFrame && simulation.SkipEndOfFrame) continue;
simulation.AfterExecute();
}
}
// Call after LateUpdate so chunk bounds are updated.
if (Surface.Enabled)
{
Surface.LateUpdate();
}
if (_Reflections._Enabled)
{
_Reflections.LateUpdate();
}
_IsFirstFrameSinceEnabled = false;
_HasAnyViewpointExecuted = true;
StoreCameraData(camera);
s_RunUpdateMarker.End();
}
void WritePerFrameMaterialParams(CommandBuffer commands)
{
CascadeData.Flip();
var current = CascadeData.Current;
// Update rendering parameters.
{
var levels = LodLevels;
for (var slice = 0; slice < levels; slice++)
{
var scale = CalcLodScale(slice);
current[slice] = new Vector4(scale, 1f, MaximumWavelength(scale), 0f);
_ProjectionMatrix[slice] = Matrix4x4.Ortho(-2f * scale, 2f * scale, -2f * scale, 2f * scale, 1f, k_RenderAboveSeaLevel + k_RenderBelowSeaLevel);
if (slice == 0) commands.SetGlobalFloat(ShaderIDs.s_Scale, scale);
}
// Duplicate last element so that things can safely read off the end of the cascades
current[levels] = current[levels - 1].XNZW(0f);
}
commands.SetGlobalVectorArray(ShaderIDs.s_CascadeData, current);
commands.SetGlobalVectorArray(ShaderIDs.s_CascadeDataSource, CascadeData.Previous(1));
}
void LateUpdatePosition()
{
var position = Viewpoint.position;
var hash = System.HashCode.Combine(_CenterOfDetailDisplacementCorrectionHelper, Viewpoint);
// This will cause artifacts in motion vectors debug view, but are likely negligible.
if (_CenterOfDetailDisplacementCorrection && _CenterOfDetailDisplacementCorrectionHelper.SampleDisplacement(hash, position, out var displacement, allowMultipleCallsPerFrame: true))
{
position = new(position.x - displacement.x, position.y, position.z - displacement.z);
}
// maintain y coordinate - sea level
position.y = transform.position.y;
// Don't land very close to regular positions where things are likely to snap to, because different tiles might
// land on either side of a snap boundary due to numerical error and snap to the wrong positions. Nudge away from
// common by using increments of 1/60 which have lots of factors.
// :WaterGridPrecisionErrors
if (Mathf.Abs(position.x * 60f - Mathf.Round(position.x * 60f)) < 0.001f)
{
position.x += 0.002f;
}
if (Mathf.Abs(position.z * 60f - Mathf.Round(position.z * 60f)) < 0.001f)
{
position.z += 0.002f;
}
Shader.SetGlobalVector(ShaderIDs.s_CenterDelta, (position - Position).XZ());
Position = position;
Shader.SetGlobalVector(ShaderIDs.s_Center, Position);
}
void LateUpdateScale()
{
var viewerHeight = _ViewpointHeightAboveWaterSmooth;
// Drop Detail Height Based On Waves.
{
var displacement = 0f;
foreach (var (_, input) in AnimatedWavesLod.s_Inputs)
{
if (input.WaveDisplacementReporter == null)
{
continue;
}
displacement = input.WaveDisplacementReporter.ReportWaveDisplacement(this, displacement);
}
// Reach maximum detail at slightly below sea level. this should combat cases where visual range can be lost
// when water height is low and camera is suspended in air. i tried a scheme where it was based on difference
// to water height but this does help with the problem of horizontal range getting limited at bad times.
viewerHeight += displacement * _DropDetailHeightBasedOnWaves;
Shader.SetGlobalFloat(ShaderIDs.s_MaximumVerticalDisplacement, displacement);
}
var camDistance = Mathf.Abs(viewerHeight);
// offset level of detail to keep max detail in a band near the surface
camDistance = Mathf.Max(camDistance - 4f, 0f);
var range = _ScaleRange;
#if CREST_DEBUG
if (_Debug._OverrideScale)
{
range = Vector2.one * _Debug._ScaleOverride;
}
#endif
// scale water mesh based on camera distance to sea level, to keep uniform detail.
var level = camDistance;
level = Mathf.Max(level, range.x);
if (range.y < Mathf.Infinity) level = Mathf.Min(level, 1.99f * range.y);
var l2 = Mathf.Log(level) / Mathf.Log(2f);
var l2f = Mathf.Floor(l2);
ViewerAltitudeLevelAlpha = l2 - l2f;
var newScale = Mathf.Pow(2f, l2f);
if (Scale > 0f)
{
var ratio = newScale / Scale;
var ratioL2 = Mathf.Log(ratio) / Mathf.Log(2f);
ScaleDifferencePower2 = Mathf.RoundToInt(ratioL2);
Shader.SetGlobalFloat(ShaderIDs.s_LodChange, ScaleDifferencePower2);
Shader.SetGlobalFloat(ShaderIDs.s_ScaleChange, ratio);
#if UNITY_EDITOR
#if CREST_DEBUG
if (ratio != 1f)
{
EditorApplication.isPaused = EditorApplication.isPaused || _Debug._PauseOnScaleChange;
if (_Debug._LogScaleChange) Debug.Log($"Scale Change: {newScale} / {Scale} = {ratio}. LOD Change: {ScaleDifferencePower2}");
}
#endif
#endif
}
Scale = newScale;
// LOD 0 is blended in/out when scale changes, to eliminate pops. Here we set it as
// a global, whereas in WaterChunkRenderer it is applied to LOD0 tiles only through
// instance data. This global can be used in compute, where we only apply this
// factor for slice 0.
Shader.SetGlobalFloat(ShaderIDs.s_MeshScaleLerp, ScaleCouldIncrease ? ViewerAltitudeLevelAlpha : 0f);
}
void LateUpdateViewerHeight()
{
var viewpoint = Viewpoint;
var viewpointHashCode = System.HashCode.Combine(_SampleHeightHelper, viewpoint);
_SampleHeightHelper.SampleHeight(viewpointHashCode, viewpoint.position, out var waterHeight, allowMultipleCallsPerFrame: true);
ViewerHeightAboveWater = ViewpointHeightAboveWater = viewpoint.position.y - waterHeight;
var viewerHeightAboveWaterOrTerrain = ViewpointHeightAboveWater;
if (viewpoint != CurrentCamera.transform)
{
var viewer = CurrentCamera.transform;
// Reuse sampler. Combine hash codes to avoid pontential conflict.
_SampleHeightHelper.SampleHeight(System.HashCode.Combine(_SampleHeightHelper, viewer), viewpoint.position, out waterHeight, allowMultipleCallsPerFrame: true);
ViewerHeightAboveWater = viewer.position.y - waterHeight;
}
#if d_Unity_Terrain
// Also use terrain height for scale. Viewpoint is absolute if set.
if (_SampleTerrainHeightForScale && LevelLod.Enabled && _Viewpoint == null)
{
var viewerPosition = viewpoint.position;
var viewerHeight = viewerPosition.y;
var viewerHeightAboveTerrain = Mathf.Infinity;
var terrain = Helpers.GetTerrainAtPosition(viewerPosition.XZ());
if (terrain != null)
{
var terrainHeight = terrain.GetPosition().y + terrain.SampleHeight(viewerPosition);
var heightAbove = viewerHeight - terrainHeight;
// Ignore if viewer is under terrain.
if (heightAbove >= 0f)
{
viewerHeightAboveTerrain = heightAbove;
}
}
if (viewerHeightAboveTerrain < Mathf.Abs(viewerHeightAboveWaterOrTerrain))
{
viewerHeightAboveWaterOrTerrain = viewerHeightAboveTerrain;
}
}
#endif // d_Unity_Terrain
// Calculate teleport distance and create window for height queries to return a height change.
{
if (_TeleportTimerForHeightQueries > 0f)
{
_TeleportTimerForHeightQueries -= Time.deltaTime;
}
var hasTeleported = _IsFirstFrameSinceEnabled;
if (!_IsFirstFrameSinceEnabled)
{
// Find the distance. Adding the FO offset will exclude FO shifts so we can determine a normal teleport.
// FO shifts are visually the same position and it is incorrect to treat it as a normal teleport.
var teleportDistanceSqr = (_OldViewpointPosition - viewpoint.position - TeleportOriginThisFrame).sqrMagnitude;
// Threshold as sqrMagnitude.
var thresholdSqr = _TeleportThreshold * _TeleportThreshold;
hasTeleported = teleportDistanceSqr > thresholdSqr;
}
if (hasTeleported)
{
// Height queries can take a few frames so a one second window should be plenty.
_TeleportTimerForHeightQueries = 1f;
}
_HasTeleportedThisFrame = hasTeleported;
_OldViewpointPosition = viewpoint.position;
}
// Smoothly varying version of viewer height to combat sudden changes in water level that are possible
// when there are local bodies of water
_ViewpointHeightAboveWaterSmooth = Mathf.Lerp
(
_ViewpointHeightAboveWaterSmooth,
viewerHeightAboveWaterOrTerrain,
_TeleportTimerForHeightQueries > 0f || !(_ForceScaleChangeSmoothing || (LevelLod.Enabled && !_SampleTerrainHeightForScale)) ? 1f : 0.01f
);
#if CREST_DEBUG
if (_Debug._IgnoreWavesForScaleChange)
{
_ViewpointHeightAboveWaterSmooth = Viewpoint.transform.position.y - SeaLevel;
}
#endif
_SampleDepthHelper.Sample(System.HashCode.Combine(_SampleDepthHelper, CurrentCamera), CurrentCamera.transform.position, out var result, allowMultipleCallsPerFrame: true);
ViewerDistanceToShoreline = result.y;
Shader.SetGlobalFloat(ShaderIDs.s_WaterDepthAtViewer, result.x);
}
void Destroy()
{
foreach (var simulation in Simulations)
{
if (!simulation.Enabled) continue;
simulation.Destroy();
}
Simulations.Clear();
_Mask?.Destroy();
Meniscus.Destroy();
// Clean up modules.
#if d_CrestPortals
_Portals.OnDestroy();
#endif
_Underwater.OnDestroy();
_Reflections.OnDestroy();
Surface.OnDestroy();
if (Container)
{
Helpers.Destroy(Container);
Container = null;
}
_Cameras.Clear();
_PerCameraData.Clear();
_Initialized = false;
}
private protected override void Disable()
{
if (_EndOfFrame != null)
{
StopCoroutine(_EndOfFrame);
}
foreach (var simulation in Simulations)
{
simulation.SetGlobals(enable: false);
if (!simulation.Enabled) continue;
simulation.Disable();
}
if (RenderPipelineHelper.IsLegacy && Viewer != null)
{
// Need to call to prevent crash.
OnEndCameraRenderingLegacy(Viewer);
}
Camera.onPreCull -= OnBeginCameraRendering;
Camera.onPostRender -= OnEndCameraRendering;
#if d_UnityHDRP
SurfaceRenderer.WaterSurfaceCustomPass.Disable();
CrestInternalCopyToTextureCustomPass.Disable();
#if UNITY_EDITOR
RenderPipelineManager.beginContextRendering -= OnBeginContextRendering;
#endif
#endif
#if d_WaveHarmonic_Crest_ShiftingOrigin
ShiftingOrigin.OnShift -= OnOriginShift;
#endif
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
RenderPipelineManager.activeRenderPipelineTypeChanged -= OnActiveRenderPipelineTypeChanged;
_Mask?.Disable();
#if d_CrestPortals
if (_Portals._Enabled) _Portals.OnDisable();
#endif
if (_Underwater._Enabled) _Underwater.OnDisable();
if (_Reflections._Enabled) _Reflections.OnDisable();
if (Meniscus.Enabled)
{
Meniscus.Disable();
}
if (Container != null)
{
Container.SetActive(false);
}
base.Disable();
}
#if d_WaveHarmonic_Crest_ShiftingOrigin
/// <summary>
/// Notify water of origin shift
/// </summary>
void OnOriginShift(Vector3 newOrigin)
{
foreach (var simulation in Simulations)
{
if (!simulation.Enabled) continue;
simulation.SetOrigin(newOrigin);
}
}
#endif
}
// Per Camera Data.
partial class WaterRenderer
{
class PerCameraData
{
// Historic
public bool _RenderedThisFrame;
public bool _ExecutedThisFrame;
public float _Scale = 1f;
public Vector3 _Position;
public Vector3 _OldViewpointPosition;
public float _ViewpointHeightAboveWaterSmooth;
public bool _IsFirstFrameSinceEnabled = true;
public BufferedData<Vector4[]> _CascadeData;
// Non-Historic (for external access)
public float _ViewerHeightAboveWater;
public float _ViewerDistanceToShoreline;
}
internal static System.Action<Camera> s_OnLoadCameraData;
internal static System.Action<Camera> s_OnStoreCameraData;
internal static System.Action<Camera> s_OnRemoveCameraData;
readonly List<Camera> _Cameras = new();
PerCameraData _CurrentPerCameraData;
readonly Dictionary<Camera, PerCameraData> _PerCameraData = new();
Coroutine _EndOfFrame;
bool _IsEndOfFrame;
internal bool _InCameraLoop;
// Fallback Flags
// If a camera is rendering the surface and/or volume, then it needs a viewpoint to
// have executed or there will be NaNs and possibly null exceptions.
bool _HasAnyViewpointExecuted;
bool _HasAnyViewerRendered;
bool FallBackRequired => _HasAnyViewerRendered && !_HasAnyViewpointExecuted;
// Set when we execute for the current camera.
// We need this flag due to camera exclusions.
bool _SeparateViewpoint;
internal bool SeparateViewpoint => _SeparateViewpoint && !SingleViewpoint;
internal bool SingleViewpoint => !MultipleViewpoints && !EditorMultipleViewpoints;
internal bool SupportsRecursiveRendering =>
#if !UNITY_6000_0_OR_NEWER
// HDRP cannot recursive render for 2022.
!RenderPipelineHelper.IsHighDefinition &&
#endif
true;
bool EditorMultipleViewpoints =>
#if UNITY_EDITOR
(_EditorMultipleViewpoints && SupportsRecursiveRendering) ||
#endif
false;
internal bool MultipleViewpoints => _MultipleViewpoints && SupportsRecursiveRendering;
bool ShouldExecuteSkippedFrame(Camera camera)
{
if (!MultipleViewpoints)
{
return false;
}
if (_DataBackgroundMode == WaterDataBackgroundMode.Never)
{
return false;
}
// Always execute for the scene camera.
if (camera.cameraType == CameraType.SceneView)
{
return true;
}
if (_DataBackgroundMode == WaterDataBackgroundMode.Always)
{
return true;
}
if (_DataBackgroundMode == WaterDataBackgroundMode.Inactive && camera.isActiveAndEnabled)
{
return true;
}
if (_DataBackgroundMode == WaterDataBackgroundMode.Disabled && !camera.enabled)
{
return true;
}
return false;
}
internal bool ShouldExecuteQueries(Camera camera)
{
return camera != null && _PerCameraData.ContainsKey(camera) && _PerCameraData[camera]._ExecutedThisFrame;
}
System.Collections.IEnumerator UpdateSkippedCameras()
{
while (true)
{
yield return Helpers.WaitForEndOfFrame;
if (SingleViewpoint)
{
// This should not happen, as enumerator not registered.
continue;
}
_IsEndOfFrame = true;
_HasAnyViewpointExecuted = false;
foreach (var camera in _Cameras)
{
if (camera == null) continue;
if (!_PerCameraData.ContainsKey(camera)) continue;
var data = _PerCameraData[camera];
data._ExecutedThisFrame = data._RenderedThisFrame;
_HasAnyViewpointExecuted |= data._RenderedThisFrame;
if (!data._RenderedThisFrame && ShouldExecuteSkippedFrame(camera))
{
RunUpdate(camera);
}
data._RenderedThisFrame = false;
}
_IsEndOfFrame = false;
}
}
void LoadCameraData(Camera camera)
{
if (SingleViewpoint)
{
return;
}
if (camera == null)
{
return;
}
if (!_PerCameraData.ContainsKey(camera))
{
_PerCameraData.Add(camera, new()
{
// The extra LOD accounts for reading off the cascade (eg CurrentIndex + LodChange + 1).
_CascadeData = new(BufferSize, () => new Vector4[Lod.k_MaximumSlices + 1]),
});
}
_CurrentPerCameraData = _PerCameraData[camera];
CascadeData = _CurrentPerCameraData._CascadeData;
Scale = _CurrentPerCameraData._Scale;
Position = _CurrentPerCameraData._Position;
_OldViewpointPosition = _CurrentPerCameraData._OldViewpointPosition;
_ViewpointHeightAboveWaterSmooth = _CurrentPerCameraData._ViewpointHeightAboveWaterSmooth;
_IsFirstFrameSinceEnabled = _CurrentPerCameraData._IsFirstFrameSinceEnabled;
foreach (var simulation in Simulations)
{
if (!simulation.Enabled) continue;
simulation.LoadCameraData(camera);
}
_CurrentPerCameraData._RenderedThisFrame = true;
_CurrentPerCameraData._ExecutedThisFrame = true;
// We are in the camera loop AND there is additional camera data.
_InCameraLoop = true;
s_OnLoadCameraData?.Invoke(camera);
}
void StoreCameraData(Camera camera)
{
if (SingleViewpoint)
{
return;
}
_CurrentPerCameraData._Scale = Scale;
_CurrentPerCameraData._Position = Position;
_CurrentPerCameraData._OldViewpointPosition = _OldViewpointPosition;
_CurrentPerCameraData._ViewpointHeightAboveWaterSmooth = _ViewpointHeightAboveWaterSmooth;
_CurrentPerCameraData._IsFirstFrameSinceEnabled = _IsFirstFrameSinceEnabled;
_CurrentPerCameraData._ViewerHeightAboveWater = ViewerHeightAboveWater;
_CurrentPerCameraData._ViewerDistanceToShoreline = ViewerDistanceToShoreline;
foreach (var simulation in Simulations)
{
if (!simulation.Enabled) continue;
simulation.StoreCameraData(camera);
}
s_OnStoreCameraData?.Invoke(camera);
}
/// <summary>
/// Cleans up data for a particular camera if no longer rendering water.
/// </summary>
/// <param name="camera">The camera to clean up data for.</param>
void RemoveCameraData(Camera camera)
{
// NOTE: Handles all camera data. Add more as needed!
Surface.RemoveCameraData(camera);
foreach (var lods in Simulations)
{
lods.RemoveCameraData(camera);
}
if (_PerCameraData.ContainsKey(camera))
{
_PerCameraData.Remove(camera);
}
s_OnRemoveCameraData?.Invoke(camera);
}
void PruneCameraData()
{
var length = _Cameras.Count;
for (var i = length - 1; i >= 0; i--)
{
var camera = _Cameras[i];
// Check against surface rendering and whether we executed, as if we did not, then
// the data is no longer synced anyway.
if (camera == null || !ShouldRender(camera, Surface._Layer, _CameraExclusions) || !_PerCameraData[camera]._ExecutedThisFrame)
{
// Do not prune the fallback camera!
if (FallBackRequired && GetViewer(initial: true) == camera)
{
continue;
}
RemoveCameraData(camera);
_Cameras.RemoveAt(i);
}
}
// Load single camera data to prevent null exceptions.
if (_Cameras.Count <= 0)
{
CascadeData = _CascadeData;
}
}
internal bool GetViewerHeightAboveWater(Camera camera, out float height)
{
height = SeaLevel;
if (!MultipleViewpoints)
{
height = ViewerHeightAboveWater;
return true;
}
if (!_PerCameraData.ContainsKey(camera))
{
return false;
}
height = _PerCameraData[camera]._ViewerHeightAboveWater;
return true;
}
internal bool GetViewerDistanceToShoreline(Camera camera, out float distance)
{
distance = SeaLevel;
if (!MultipleViewpoints)
{
distance = ViewerDistanceToShoreline;
return true;
}
if (!_PerCameraData.ContainsKey(camera))
{
return false;
}
distance = _PerCameraData[camera]._ViewerDistanceToShoreline;
return true;
}
internal Transform GetClosestViewpoint(Vector3 position)
{
if (!MultipleViewpoints)
{
return Viewpoint;
}
var furthest = Mathf.Infinity;
Camera result = null;
foreach (var camera in _Cameras)
{
if (camera == null)
{
continue;
}
var distance = Mathf.Abs((camera.transform.position - position).sqrMagnitude);
if (distance < furthest)
{
result = camera;
furthest = distance;
}
}
if (result == null)
{
return null;
}
return result.transform;
}
internal bool IsClosestViewpoint(Vector3 position)
{
if (!MultipleViewpoints)
{
return true;
}
var viewpoint = Viewpoint;
return viewpoint == GetClosestViewpoint(position);
}
}
#if CREST_DEBUG
#if UNITY_EDITOR
// Tests.
partial class WaterRenderer
{
internal bool _SkipForTesting;
private protected override void FixedUpdate()
{
if (_SkipForTesting)
{
return;
}
base.FixedUpdate();
}
internal void TestFixedUpdate()
{
_SkipForTesting = false;
FixedUpdate();
_SkipForTesting = true;
}
internal void TestLateUpdate()
{
_SkipForTesting = false;
LateUpdate();
_SkipForTesting = true;
}
}
#endif
#endif
}