// 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 { /// /// Vertical displacement which affects scale via DropDetailHeightBasedOnWaves. /// float ReportWaveDisplacement(WaterRenderer water, float displacement); } /// /// The main script for the water system. /// /// /// 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. /// public sealed partial class WaterRenderer : ManagerBehaviour { 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; } /// /// The current viewer (center of detail). /// internal Camera CurrentCamera { get; private set; } readonly SampleCollisionHelper _CenterOfDetailDisplacementCorrectionHelper = new(); // // Viewer Height // /// /// The water changes scale when viewer changes altitude, this gives the interpolation param between scales. /// internal float ViewerAltitudeLevelAlpha { get; private set; } /// /// Vertical offset of camera vs water surface. /// public float ViewerHeightAboveWater { get; private set; } /// /// Vertical offset of viewpoint vs water surface. /// public float ViewpointHeightAboveWater { get; private set; } /// /// Distance of camera to shoreline. Positive if over water and negative if over land. /// public float ViewerDistanceToShoreline { get; private set; } /// /// Smoothly varying version of viewpoint height to combat sudden changes in water level that are possible /// when there are local bodies of water /// 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; } /// /// Sea level is given by y coordinate of GameObject with WaterRenderer script. /// 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 // /// /// Loosely a stack for time providers. /// /// /// The last in the list is the active one. When a /// gets added to the stack, it is bumped to the top of /// the list. When a 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. /// public Utility.Internal.Stack TimeProviders { get; private set; } = new(); /// /// The current time provider. /// public ITimeProvider TimeProvider => TimeProviders.Peek(); internal float CurrentTime => TimeProvider.Time; internal float DeltaTime => TimeProvider.Delta; // // Environment // /// /// The primary light that affects the water. This should be a directional light. /// Light GetPrimaryLight() => _PrimaryLight == null ? RenderSettings.sun : _PrimaryLight; /// /// Physics gravity applied to water. /// 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(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 // /// /// Calculates the absorption value from the absorption color. /// /// The absorption color. /// The absorption value (XYZ value). 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 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 // /// /// Is runtime environment without graphics card /// 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; /// /// Is runtime environment non-interactive (not displaying to user). /// [System.Obsolete("We no longer care whether Unity is running in non-interactive mode.")] public static bool RunningHeadless => false; // // Frame Timing // /// /// The frame count for Crest. /// public static int FrameCount => Time.frameCount; // // Level of Detail // internal static System.Action s_OnBeforeBuildCommandBuffer; internal const string k_DrawLodData = "Crest.LodData"; internal CommandBuffer SimulationBuffer { get; private set; } // Scale, Weight, MaximumWaveLength, Unused BufferedData _CascadeData; internal BufferedData 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 // /// /// Current water scale (changes with viewer altitude). /// public float Scale { get; private set; } internal float CalcLodScale(float slice) => Scale * Mathf.Pow(2f, slice); internal float CalcGridSize(int slice) => CalcLodScale(slice) / LodResolution; /// /// Could the water horizontal scale increase (for e.g. if the viewpoint gains altitude). Will be false if water already at maximum scale. /// internal bool ScaleCouldIncrease => _ScaleRange.y == Mathf.Infinity || Scale < _ScaleRange.y * 0.99f; /// /// Could the water horizontal scale decrease (for e.g. if the viewpoint drops in altitude). Will be false if water already at minimum scale. /// internal bool ScaleCouldDecrease => Scale > _ScaleRange.x * 1.01f; internal int ScaleDifferencePower2 { get; private set; } // // Query Providers // /// /// Provides water shape to CPU. /// public ICollisionProvider CollisionProvider => AnimatedWavesLod?.Provider; /// /// Provides flow to the CPU. /// public IFlowProvider FlowProvider => FlowLod?.Provider; /// /// Provides water depth and distance to water edge to the CPU. /// 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(out _)) { gameObject.AddComponent().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 /// /// Notify water of origin shift /// 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 _CascadeData; // Non-Historic (for external access) public float _ViewerHeightAboveWater; public float _ViewerDistanceToShoreline; } internal static System.Action s_OnLoadCameraData; internal static System.Action s_OnStoreCameraData; internal static System.Action s_OnRemoveCameraData; readonly List _Cameras = new(); PerCameraData _CurrentPerCameraData; readonly Dictionary _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); } /// /// Cleans up data for a particular camera if no longer rendering water. /// /// The camera to clean up data for. 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 }