// Crest Water System // Copyright © 2024 Wave Harmonic. All rights reserved. using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.HighDefinition; using WaveHarmonic.Crest.Internal; using WaveHarmonic.Crest.RelativeSpace; using WaveHarmonic.Crest.Utility; namespace WaveHarmonic.Crest { /// /// 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 { [SerializeField, HideInInspector] #pragma warning disable 414 int _Version = 0; #pragma warning restore 414 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_LodAlphaBlackPointFade = Shader.PropertyToID("g_Crest_LodAlphaBlackPointFade"); public static readonly int s_LodAlphaBlackPointWhitePointFade = Shader.PropertyToID("g_Crest_LodAlphaBlackPointWhitePointFade"); public static readonly int s_ForceUnderwater = Shader.PropertyToID("g_Crest_ForceUnderwater"); // 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_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"); // High Definition Render Pipeline public static readonly int s_PrimaryLightDirection = Shader.PropertyToID("g_Crest_PrimaryLightDirection"); public static readonly int s_PrimaryLightIntensity = Shader.PropertyToID("g_Crest_PrimaryLightIntensity"); // URP Motion Vectors public static readonly int s_Surface = Shader.PropertyToID("_Surface"); public static readonly int s_SrcBlend = Shader.PropertyToID("_SrcBlend"); public static readonly int s_DstBlend = Shader.PropertyToID("_DstBlend"); } // // Viewer // Transform GetViewpoint() { #if UNITY_EDITOR if (!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; } Camera GetViewer() { #if UNITY_EDITOR if (!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; } // Cache the ViewCamera property for internal use. Camera _ViewCameraCached; // // 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(); 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 // // Serialized Fields // internal float WindSpeedKPH => _WindSpeed; // // Transform // internal Transform Root { get; private set; } internal GameObject Container { get; private set; } /// /// Sea level is given by y coordinate of GameObject with WaterRenderer script. /// public float SeaLevel => Root.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 // internal Material AboveOrBelowSurfaceMaterial => _VolumeMaterial == null ? _Material : _VolumeMaterial; #if UNITY_6000_0_OR_NEWER internal Material _MotionVectorsMaterial; #endif enum SurfaceSelfIntersectionFixMode { Off, ForceBelowWater, ForceAboveWater, On, Automatic, } #if d_CrestPortals bool Portaled => _Portals.Active; #else bool Portaled => false; #endif bool GetCastShadows() => _CastShadows && !RenderPipelineHelper.IsLegacy; bool GetWriteMotionVectors() => _WriteMotionVectors && #if UNITY_6000_0_OR_NEWER !RenderPipelineHelper.IsLegacy; #else RenderPipelineHelper.IsHighDefinition; #endif // // 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(); // // Water Chunks // internal List Chunks { get; } = new(); // // Water Chunk Culling // bool _CanSkipCulling; // // 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). /// public static bool RunningHeadless => Application.isBatchMode || (Instance != null && Instance._Debug._ForceBatchMode); internal bool IsRunningHeadless => Application.isBatchMode || _Debug._ForceBatchMode; // // Frame Timing // internal int LastUpdateFrame { get; private set; } = -1; /// /// The frame count for Crest. /// public static int FrameCount { get { #if UNITY_EDITOR if (!Application.isPlaying) { return s_EditorFrames; } else #endif { return Time.frameCount; } } } // // Level of Detail // // We are computing these values to be optimal based on the base mesh vertex density. float _LodAlphaBlackPointFade; float _LodAlphaBlackPointWhitePointFade; internal CommandBuffer SimulationBuffer { get; private set; } internal BufferedData _CascadeData; internal struct PerCascadeInstanceData { public float _MeshScaleLerp; public float _FarNormalsWeight; public float _GeometryGridWidth; public Vector2 _NormalScrollSpeeds; } internal BufferedData _PerCascadeInstanceData; internal int BufferSize { get; private set; } internal float MaximumWavelength(int slice) { var maximumDiameter = 4f * Scale * Mathf.Pow(2f, slice); // 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 || Root.localScale.x < _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 => Root.localScale.x > _ScaleRange.x * 1.01f; internal int ScaleDifferencePower2 { get; private set; } // // Displacement Reporting // /// /// User shape inputs can report in how far they might displace the shape horizontally and vertically. The max value is /// saved here. Later the bounding boxes for the water tiles will be expanded to account for this potential displacement. /// internal void ReportMaximumDisplacement(float horizontal, float vertical, float verticalFromWaves) { MaximumHorizontalDisplacement += horizontal; MaximumVerticalDisplacement += vertical; _MaximumVerticalDisplacementFromWaves += verticalFromWaves; } float _MaximumVerticalDisplacementFromWaves = 0f; /// /// The maximum horizontal distance that the shape scripts are displacing the shape. /// internal float MaximumHorizontalDisplacement { get; private set; } /// /// The maximum height that the shape scripts are displacing the shape. /// internal float MaximumVerticalDisplacement { 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(); _IsFirstFrameSinceEnabled = true; _ViewCameraCached = Viewer; if (_Initialized) { Enable(); return; } _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 = "Crest.LodData", }; 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(); } // TODO: Have a BufferCount which will be the run-time buffer size or prune data. // NOTE: Hardcode minimum (2) to avoid breaking server builds and LodData* toggles. // Gather the buffer size for shared data. BufferSize = 2; foreach (var simulation in Simulations) { if (!simulation.Enabled) continue; BufferSize = Mathf.Max(BufferSize, simulation.BufferCount); } _PerCascadeInstanceData = new(BufferSize, () => new PerCascadeInstanceData[Lod.k_MaximumSlices]); // The extra LOD accounts for reading off the cascade (eg CurrentIndex + LodChange + 1). _CascadeData = new(BufferSize, () => new Vector4[Lod.k_MaximumSlices + 1]); _ProjectionMatrix = new Matrix4x4[LodLevels]; // Resolution is 4 tiles across. var baseMeshDensity = _Resolution * 0.25f / _GeometryDownSampleFactor; // 0.4f is the "best" value when base mesh density is 8. Scaling down from there produces results similar to // hand crafted values which looked good when the water is flat. _LodAlphaBlackPointFade = 0.4f / (baseMeshDensity / 8f); // We could calculate this in the shader, but we can save two subtractions this way. _LodAlphaBlackPointWhitePointFade = 1f - _LodAlphaBlackPointFade - _LodAlphaBlackPointFade; #if CREST_DEBUG if (_Debug._DisableChunks) { Root = new GameObject("Root").transform; } else #endif { Root = WaterBuilder.GenerateMesh(this, Chunks, _Resolution, _GeometryDownSampleFactor, _Slices); } Root.SetParent(Container.transform, worldPositionStays: false); if (Application.isPlaying && _Debug._AttachDebugGUI && !TryGetComponent(out _)) { gameObject.AddComponent().hideFlags = HideFlags.DontSave; } _CanSkipCulling = false; _GeneratedSettingsHash = CalculateSettingsHash(); // Prevent MVs from popping on first frame. if (!_Debug._DisableFollowViewpoint && _ViewCameraCached != null) { LateUpdatePosition(); LateUpdateScale(); } #if UNITY_6000_0_OR_NEWER // Set up motion vector material. if (RenderPipelineHelper.IsUniversal && WriteMotionVectors && _Material != null) { SetUpMotionVectorsMaterial(); } #endif 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 #if UNITY_EDITOR // Don't run immediately if in edit mode - need to count editor frames so this is run through EditorUpdate() if (Application.isPlaying) #endif { RunUpdate(); } } // // Methods // private protected override void Enable() { base.Enable(); foreach (var simulation in Simulations) { simulation.SetGlobals(enable: true); if (!simulation.Enabled) continue; simulation.Enable(); } #if d_WaveHarmonic_Crest_ShiftingOrigin ShiftingOrigin.OnShift -= OnOriginShift; ShiftingOrigin.OnShift += OnOriginShift; #endif // Needs to be first or will get assertions etc. Unity bug likely. RenderPipelineManager.activeRenderPipelineTypeChanged -= OnActiveRenderPipelineTypeChanged; RenderPipelineManager.activeRenderPipelineTypeChanged += OnActiveRenderPipelineTypeChanged; 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 -= OnPreRenderCamera; Camera.onPreCull += OnPreRenderCamera; Camera.onPostRender -= OnPostRenderCamera; Camera.onPostRender += OnPostRenderCamera; } #if d_UnityURP else if (RenderPipelineHelper.IsUniversal) { ConfigureUniversalRenderer.Enable(this); UniversalCopyWaterSurfaceDepth.Enable(this); } #endif #if UNITY_EDITOR EnableWaterLevelDepthTexture(); #endif Container.SetActive(true); if (_Underwater._Enabled) { _Underwater.OnEnable(); } #if d_CrestPortals if (_Portals._Enabled) { _Portals.OnEnable(); } #endif if (_Reflections._Enabled) { _Reflections.OnEnable(); } #if UNITY_EDITOR EditorApplication.update -= EditorUpdate; EditorApplication.update += EditorUpdate; #endif } internal ScriptableRenderContext _Context; void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) { #if UNITY_EDITOR UpdateLastActiveSceneCamera(camera); #endif _Context = context; OnBeginCameraRendering(camera); } void OnBeginCameraRendering(Camera camera) { if (_Underwater._Enabled) { _Underwater.OnBeforeCameraRender(camera); } #if d_UnityHDRP // This use to be in OnBeginContextRendering with comment: // "Most compatible with lighting options if computed here" // Cannot remember what that meant… if (RenderPipelineHelper.IsHighDefinition) { var lightDirection = Vector3.zero; var lightIntensity = Color.black; var sun = PrimaryLight; if (sun != null && sun.isActiveAndEnabled && sun.TryGetComponent(out var data)) { lightDirection = -sun.transform.forward; lightIntensity = Color.clear; // It was reported that Light.intensity causes flickering when updated with // HDAdditionalLightData.SetIntensity, unless we get intensity from there. { // Adapted from Helpers.FinalColor. var light = data; var linear = GraphicsSettings.lightsUseLinearIntensity; var color = linear ? light.color.linear : light.color; #if UNITY_6000_0_OR_NEWER color *= sun.intensity; #else color *= light.intensity; #endif if (linear && light.useColorTemperature) color *= Mathf.CorrelatedColorTemperatureToRGB(sun.colorTemperature); if (!linear) color = color.MaybeLinear(); lightIntensity = linear ? color.MaybeGamma() : color; } // Transmittance is for Physically Based Sky. var hdCamera = HDCamera.GetOrCreate(camera); var settings = SkyManager.GetSkySetting(hdCamera.volumeStack); var transmittance = settings != null ? settings.EvaluateAtmosphericAttenuation(lightDirection, hdCamera.camera.transform.position) : Vector3.one; lightIntensity *= transmittance.x; lightIntensity *= transmittance.y; lightIntensity *= transmittance.z; } Shader.SetGlobalVector(ShaderIDs.s_PrimaryLightDirection, lightDirection); Shader.SetGlobalVector(ShaderIDs.s_PrimaryLightIntensity, lightIntensity); } #endif if (_Reflections._Enabled) { _Reflections.OnPreRenderCamera(camera); } if (!Helpers.MaskIncludesLayer(camera.cullingMask, Layer)) { return; } var viewpoint = camera.transform.position; _SampleHeightHelperPerCamera.SampleHeight(System.HashCode.Combine(GetHashCode(), camera.GetHashCode()), viewpoint, out var height, allowMultipleCallsPerFrame: true); _ViewerHeightAboveWaterPerCamera = viewpoint.y - height; WritePerCameraMaterialParameters(); } void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) { OnEndCameraRendering(camera); } void OnEndCameraRendering(Camera camera) { if (_Underwater._Enabled) { _Underwater.OnAfterCameraRender(camera); } if (_Reflections._Enabled) { _Reflections.OnAfterCameraRender(camera); } } void OnActiveRenderPipelineTypeChanged() { if (isActiveAndEnabled) { Disable(); Enable(); } } internal void Rebuild() { Disable(); Destroy(); OnEnable(); } bool VerifyRequirements() { if (!RunningWithoutGraphics) { if (Application.platform == RuntimePlatform.WebGLPlayer) { 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(_Layer, ref settingsHash); Hash.AddInt(_Resolution, ref settingsHash); Hash.AddInt(_GeometryDownSampleFactor, ref settingsHash); Hash.AddInt(_Slices, ref settingsHash); Hash.AddFloat(_ExtentsSizeMultiplier, ref settingsHash); Hash.AddBool(AllowRenderQueueSorting, ref settingsHash); Hash.AddBool(CastShadows, ref settingsHash); Hash.AddBool(WriteMotionVectors, ref settingsHash); Hash.AddBool(_Debug._ForceBatchMode, ref settingsHash); Hash.AddBool(_Debug._ForceNoGraphics, ref settingsHash); Hash.AddBool(_Debug._ShowHiddenObjects, ref settingsHash); #if CREST_DEBUG Hash.AddBool(_Debug._DisableChunks, ref settingsHash); Hash.AddBool(_Debug._DisableSkirt, ref settingsHash); Hash.AddBool(_Debug._UniformTiles, ref settingsHash); #endif if (_ChunkTemplate != null) { Hash.AddObject(_ChunkTemplate, ref settingsHash); } return settingsHash; } void RunUpdate() { UnityEngine.Profiling.Profiler.BeginSample("Crest.WaterRenderer.RunUpdate"); // Rebuild if needed. Needs to run in builds (for MVs at the very least). if (CalculateSettingsHash() != _GeneratedSettingsHash) { Rebuild(); } _ViewCameraCached = Viewer; // Reset displacement reporting values. // This is written to in Update, and read in LateUpdate (chunk) and LateUpdateScale. MaximumHorizontalDisplacement = MaximumVerticalDisplacement = _MaximumVerticalDisplacementFromWaves = 0f; BroadcastUpdate(); if (!_Debug._DisableFollowViewpoint && _ViewCameraCached != null) { LateUpdatePosition(); LateUpdateViewerHeight(); LateUpdateScale(); } // Set global shader params Shader.SetGlobalFloat(ShaderIDs.s_Time, CurrentTime); Shader.SetGlobalFloat(ShaderIDs.s_LodCount, LodLevels); Shader.SetGlobalFloat(ShaderIDs.s_LodAlphaBlackPointFade, _LodAlphaBlackPointFade); Shader.SetGlobalFloat(ShaderIDs.s_LodAlphaBlackPointWhitePointFade, _LodAlphaBlackPointWhitePointFade); // HDRP will automatically disable this pass for the water shader for unknown reasons. It might be that we // are sampling from the depth texture does not work with shadow casting. if (RenderPipelineHelper.IsHighDefinition && Material != null) { Material.SetShaderPassEnabled("ShadowCaster", CastShadows); } #if UNITY_6000_0_OR_NEWER // Motion Vectors (URP). Shader Graph introduced the necessary toggle in U6. if (Application.isPlaying && RenderPipelineHelper.IsUniversal && WriteMotionVectors && _Material != null) { if (_MotionVectorsMaterial.shader != _Material.shader) { SetUpMotionVectorsMaterial(); } _MotionVectorsMaterial.CopyMatchingPropertiesFromMaterial(_Material); _MotionVectorsMaterial.renderQueue = (int)RenderQueue.Geometry; _MotionVectorsMaterial.SetOverrideTag("RenderType", "Opaque"); _MotionVectorsMaterial.SetFloat(ShaderIDs.s_Surface, 0); // SurfaceType.Opaque _MotionVectorsMaterial.SetFloat(ShaderIDs.s_SrcBlend, 1); _MotionVectorsMaterial.SetFloat(ShaderIDs.s_DstBlend, 0); } #endif // Needs updated transform values like scale. WritePerFrameMaterialParams(); SimulationBuffer.Clear(); // Construct the command buffer and attach it to the camera so that it will be executed in the render. #if UNITY_EDITOR if (Application.isPlaying || !_ShowWaterProxyPlane) #endif { foreach (var simulation in Simulations) { if (!simulation.Enabled) continue; simulation.BuildCommandBuffer(this, SimulationBuffer); } // This will execute at the beginning of the frame before the graphics queue. Graphics.ExecuteCommandBuffer(SimulationBuffer); base.LateUpdate(); // Call after LateUpdate so chunk bounds are updated. LateUpdateTiles(); if (_Underwater._Enabled) { _Underwater.LateUpdate(); } if (_Reflections._Enabled && !_Reflections.SupportsRecursiveRendering) { _Reflections.LateUpdate(); } LastUpdateFrame = FrameCount; } // 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.CollisionSource == CollisionSource.CPU) { AnimatedWavesLod.Provider?.UpdateQueries(this); } _IsFirstFrameSinceEnabled = false; UnityEngine.Profiling.Profiler.EndSample(); } void WritePerCameraMaterialParameters() { if (Material != null) { // Override isFrontFace when camera is far enough from the water surface to fix self-intersecting waves. // Hack - due to SV_IsFrontFace occasionally coming through as true for back faces, // add a param here that forces water to be in underwater state. I think the root // cause here might be imprecision or numerical issues at water tile boundaries, although // i'm not sure why cracks are not visible in this case. var height = _ViewerHeightAboveWaterPerCamera; var value = (int)_SurfaceSelfIntersectionFixMode; switch (_SurfaceSelfIntersectionFixMode) { case SurfaceSelfIntersectionFixMode.On: value = height < -2f ? 1 : height > 2f ? 2 : 0; break; case SurfaceSelfIntersectionFixMode.Automatic: // Skip for portals as it is possible to see both sides of the surface at any position. value = Portaled ? 0 : height < -2f ? 1 : height > 2f ? 2 : 0; break; } Shader.SetGlobalInteger(ShaderIDs.s_ForceUnderwater, value); } } void WritePerFrameMaterialParams() { _CascadeData.Flip(); // Update rendering parameters. { for (var slice = 0; slice < LodLevels; slice++) { var scale = CalcLodScale(slice); _CascadeData.Current[slice].x = scale; _CascadeData.Current[slice].y = 1f; _CascadeData.Current[slice].z = MaximumWavelength(slice); _ProjectionMatrix[slice] = Matrix4x4.Ortho(-2f * scale, 2f * scale, -2f * scale, 2f * scale, 1f, k_RenderAboveSeaLevel + k_RenderBelowSeaLevel); if (slice == 0) Shader.SetGlobalFloat(ShaderIDs.s_Scale, scale); } // Duplicate last element so that things can safely read off the end of the cascades _CascadeData.Current[LodLevels] = _CascadeData.Current[LodLevels - 1]; _CascadeData.Current[LodLevels].y = 0f; } Shader.SetGlobalVectorArray(ShaderIDs.s_CascadeData, _CascadeData.Current); Shader.SetGlobalVectorArray(ShaderIDs.s_CascadeDataSource, _CascadeData.Previous(1)); _PerCascadeInstanceData.Flip(); WritePerCascadeInstanceData(_PerCascadeInstanceData); } void WritePerCascadeInstanceData(BufferedData instanceData) { for (var lodIdx = 0; lodIdx < LodLevels; lodIdx++) { // blend LOD 0 shape in/out to avoid pop, if the water might scale up later (it is smaller than its maximum scale) var needToBlendOutShape = lodIdx == 0 && ScaleCouldIncrease; instanceData.Current[lodIdx]._MeshScaleLerp = needToBlendOutShape ? ViewerAltitudeLevelAlpha : 0f; // blend furthest normals scale in/out to avoid pop, if scale could reduce var needToBlendOutNormals = lodIdx == LodLevels - 1 && ScaleCouldDecrease; instanceData.Current[lodIdx]._FarNormalsWeight = needToBlendOutNormals ? ViewerAltitudeLevelAlpha : 1f; // geometry data // compute grid size of geometry. take the long way to get there - make sure we land exactly on a power of two // and not inherit any of the lossy-ness from lossyScale. var scale = CalcLodScale(lodIdx); instanceData.Current[lodIdx]._GeometryGridWidth = scale / (0.25f * _Resolution / _GeometryDownSampleFactor); var mul = 1.875f; // fudge 1 var pow = 1.4f; // fudge 2 var texelWidth = instanceData.Current[lodIdx]._GeometryGridWidth / _GeometryDownSampleFactor; instanceData.Current[lodIdx]._NormalScrollSpeeds[0] = Mathf.Pow(Mathf.Log(1f + 2f * texelWidth) * mul, pow); instanceData.Current[lodIdx]._NormalScrollSpeeds[1] = Mathf.Pow(Mathf.Log(1f + 4f * texelWidth) * mul, pow); } } #if UNITY_6000_0_OR_NEWER void SetUpMotionVectorsMaterial() { if (_MotionVectorsMaterial != null) Helpers.Destroy(_MotionVectorsMaterial); _MotionVectorsMaterial = CoreUtils.CreateEngineMaterial(_Material.shader); _MotionVectorsMaterial.SetShaderPassEnabled("UniversalForward", false); _MotionVectorsMaterial.SetShaderPassEnabled("UniversalGBuffer", false); _MotionVectorsMaterial.SetShaderPassEnabled("ShadowCaster", false); _MotionVectorsMaterial.SetShaderPassEnabled("DepthOnly", false); _MotionVectorsMaterial.SetShaderPassEnabled("DepthNormals", false); _MotionVectorsMaterial.SetShaderPassEnabled("Meta", false); _MotionVectorsMaterial.SetShaderPassEnabled("SceneSelectionPass", false); _MotionVectorsMaterial.SetShaderPassEnabled("Picking", false); _MotionVectorsMaterial.SetShaderPassEnabled("Universal2D", false); _MotionVectorsMaterial.SetShaderPassEnabled("MotionVectors", true); } #endif void LateUpdatePosition() { var pos = Viewpoint.position; // maintain y coordinate - sea level pos.y = Root.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(pos.x * 60f - Mathf.Round(pos.x * 60f)) < 0.001f) { pos.x += 0.002f; } if (Mathf.Abs(pos.z * 60f - Mathf.Round(pos.z * 60f)) < 0.001f) { pos.z += 0.002f; } Shader.SetGlobalVector(ShaderIDs.s_CenterDelta, pos - Root.position); Root.position = pos; Shader.SetGlobalVector(ShaderIDs.s_Center, Root.position); } void LateUpdateScale() { var viewerHeight = _ViewpointHeightAboveWaterSmooth; // 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 += _MaximumVerticalDisplacementFromWaves * _DropDetailHeightBasedOnWaves; 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); // scale water mesh based on camera distance to sea level, to keep uniform detail. var level = camDistance; level = Mathf.Max(level, _ScaleRange.x); if (_ScaleRange.y < Mathf.Infinity) level = Mathf.Min(level, 1.99f * _ScaleRange.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}"); } #endif #endif } Scale = newScale; Root.localScale = new(Scale, 1f, Scale); // 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; _SampleHeightHelper.SampleHeight(viewpoint.position, out var waterHeight); ViewerHeightAboveWater = ViewpointHeightAboveWater = viewpoint.position.y - waterHeight; var viewerHeightAboveWaterOrTerrain = ViewpointHeightAboveWater; if (viewpoint != _ViewCameraCached.transform) { var viewer = _ViewCameraCached.transform; // Reuse sampler. Combine hash codes to avoid pontential conflict. _SampleHeightHelper.SampleHeight(System.HashCode.Combine(GetHashCode(), viewer.GetHashCode()), viewpoint.position, out waterHeight, allowMultipleCallsPerFrame: true); ViewerHeightAboveWater = viewer.position.y - waterHeight; } // 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; } } // 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.05f ); #if CREST_DEBUG if (_Debug._IgnoreWavesForScaleChange) { _ViewpointHeightAboveWaterSmooth = Viewpoint.transform.position.y - SeaLevel; } #endif _SampleDepthHelper.SampleDistanceToWaterEdge(_ViewCameraCached.transform.position, out var distance); ViewerDistanceToShoreline = distance; } void LateUpdateTiles() { var canSkipCulling = WaterBody.WaterBodies.Count == 0 && _CanSkipCulling; foreach (var tile in Chunks) { if (tile.Rend == null) { continue; } tile._Culled = false; tile.MaterialOverridden = false; // If there are local bodies of water, this will do overlap tests between the water tiles // and the water bodies and turn off any that don't overlap. if (!canSkipCulling) { var chunkBounds = tile.Rend.bounds; var chunkUndisplacedBoundsXZ = tile.UnexpandedBoundsXZ; var largestOverlap = 0f; var overlappingOne = false; foreach (var body in WaterBody.WaterBodies) { // If tile has already been excluded from culling, then skip this iteration. But finish this // iteration if the water body has a material override to work out most influential water body. if (overlappingOne && body.AboveSurfaceMaterial == null) { continue; } var bounds = body.AABB; var overlapping = bounds.max.x > chunkBounds.min.x && bounds.min.x < chunkBounds.max.x && bounds.max.z > chunkBounds.min.z && bounds.min.z < chunkBounds.max.z; if (overlapping) { overlappingOne = true; if (body.AboveSurfaceMaterial != null) { var overlap = 0f; { // Use the unexpanded bounds to prevent leaking as generally this feature will be // for an inland body of water where hopefully there is attenuation between it and // the water to handle the water's displacement. The inland water body will unlikely // have large displacement but can be mitigated with a decent buffer zone. var xMin = Mathf.Max(bounds.min.x, chunkUndisplacedBoundsXZ.min.x); var xMax = Mathf.Min(bounds.max.x, chunkUndisplacedBoundsXZ.max.x); var zMin = Mathf.Max(bounds.min.z, chunkUndisplacedBoundsXZ.min.y); var zMax = Mathf.Min(bounds.max.z, chunkUndisplacedBoundsXZ.max.y); if (xMin < xMax && zMin < zMax) { overlap = (xMax - xMin) * (zMax - zMin); } } // If this water body has the most overlap, then the chunk will get its material. if (overlap > largestOverlap) { tile.Rend.sharedMaterial = body.AboveSurfaceMaterial; tile.MaterialOverridden = true; largestOverlap = overlap; } } else { tile.MaterialOverridden = false; } } } tile._Culled = _WaterBodyCulling && !overlappingOne && WaterBody.WaterBodies.Count > 0; } tile.Rend.enabled = !tile._Culled; } // Can skip culling next time around if water body count stays at 0 _CanSkipCulling = WaterBody.WaterBodies.Count == 0; } void Destroy() { foreach (var simulation in Simulations) { if (!simulation.Enabled) continue; simulation.Destroy(); } Simulations.Clear(); // Clean up modules. #if d_CrestPortals _Portals.OnDestroy(); #endif _Underwater.OnDestroy(); _Reflections.OnDestroy(); // Clean up everything created through the Water Builder. WaterBuilder.CleanUp(this); Root = null; if (Container) { Helpers.Destroy(Container); Container = null; } Chunks.Clear(); #if UNITY_6000_0_OR_NEWER Helpers.Destroy(_MotionVectorsMaterial); #endif _Initialized = false; } private protected override void Disable() { 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. OnPostRenderCamera(Viewer); } #if UNITY_EDITOR EditorApplication.update -= EditorUpdate; #endif Camera.onPreCull -= OnPreRenderCamera; Camera.onPostRender -= OnPostRenderCamera; #if d_UnityURP ConfigureUniversalRenderer.Disable(); UniversalCopyWaterSurfaceDepth.Disable(); #endif #if d_WaveHarmonic_Crest_ShiftingOrigin ShiftingOrigin.OnShift -= OnOriginShift; #endif RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; RenderPipelineManager.endCameraRendering -= OnEndCameraRendering; RenderPipelineManager.activeRenderPipelineTypeChanged -= OnActiveRenderPipelineTypeChanged; #if d_CrestPortals if (_Portals._Enabled) _Portals.OnDisable(); #endif if (_Underwater._Enabled) _Underwater.OnDisable(); if (_Reflections._Enabled) _Reflections.OnDisable(); #if UNITY_EDITOR DisableWaterLevelDepthTexture(); #endif if (Container != null) { Container.SetActive(false); } base.Disable(); } /// /// Notify water of origin shift /// void OnOriginShift(Vector3 newOrigin) { foreach (var simulation in Simulations) { if (!simulation.Enabled) continue; simulation.SetOrigin(newOrigin); } } /// /// Clears persistent LOD data. Some simulations have persistent data which can linger for a little while after /// being disabled. This will manually clear that data. /// void ClearLodData() { foreach (var simulation in Simulations) { if (!simulation.Enabled) continue; simulation.ClearLodData(); } } } #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 }