//Stylized Water 2: Underwater Rendering extension //Staggart Creations (http://staggart.xyz) //Copyright protected under Unity Asset Store EULA using System; using UnityEngine; using UnityEngine.Rendering; #if URP using UnityEngine.Rendering.Universal; namespace StylizedWater2.UnderwaterRendering { #if UNITY_2021_1_OR_NEWER [DisallowMultipleRendererFeature] #endif public class UnderwaterRenderFeature : ScriptableRendererFeature { //Shared resources, ensures they're included in a build when the render feature is in use public UnderwaterResources resources; [Serializable] public class Settings { public bool allowBlur = true; public bool allowDistortion = true; public enum DistortionMode { [InspectorName("Screen-space (Fastest)")] ScreenSpace, [InspectorName("Camera-space (Nicest)")] CameraSpace } [Tooltip("Screen-space mode is faster, but distortion will appear to move with the camera\n\n" + "Camera-space mode looks better, but requires more calculations")] public DistortionMode distortionMode = DistortionMode.CameraSpace; [Tooltip("Limit caustics only to parts of a surface where sun light hits it")] public bool directionalCaustics; [Tooltip("(Requires Unity 2020.2+) Use the depth normals texture created from the Depth Normals pre-pass." + "\n\nThis can negatively impact performance if the game isn't already optimized for draw calls!" + "\n\nIf disabled, normals will be reconstructed from the depth texture")] public bool accurateDirectionalCaustics = false; [Tooltip("Attempts to create a glass-like appearance by refracting the scene geometry behind the water line. Note this does not refract the water surface behind it")] public bool waterlineRefraction = true; } public Settings settings = new Settings(); private UnderwaterMaskPass maskPass; private UnderwaterLinePass waterLinePass; private UnderwaterShadingPass shadingPass; private DistortionSpherePass distortionSpherePass; private UnderwaterPost postProcessingPass; public UnderwaterRenderer.KeywordStates keywordStates; private void Reset() //Note: editor-only { if (!resources) resources = UnderwaterResources.Find(); //Recommended fastest settings #if UNITY_IOS || UNITY_TVOS || UNITY_ANDROID || UNITY_SWITCH settings.directionalCaustics = false; settings.accurateDirectionalCaustics = false; settings.allowBlur = false; settings.allowDistortion = false; settings.distortionMode = Settings.DistortionMode.ScreenSpace; settings.waterlineRefraction = false; #endif } public override void Create() { #if UNITY_EDITOR if (!resources) resources = UnderwaterResources.Find(); #endif maskPass = new UnderwaterMaskPass(this); maskPass.renderPassEvent = RenderPassEvent.BeforeRenderingTransparents; shadingPass = new UnderwaterShadingPass(this); shadingPass.renderPassEvent = RenderPassEvent.BeforeRenderingTransparents; distortionSpherePass = new DistortionSpherePass(resources); distortionSpherePass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; postProcessingPass = new UnderwaterPost(this); postProcessingPass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; waterLinePass = new UnderwaterLinePass(this); waterLinePass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing; } private void OnDisable() { maskPass.Dispose(); shadingPass.Dispose(); distortionSpherePass.Dispose(); postProcessingPass.Dispose(); } private bool cameraIntersecting; private bool cameraSubmerged; private bool RequiresPostProcessingPass(UnderwaterRenderer renderer) { return (renderer.enableBlur && settings.allowBlur) || (renderer.enableDistortion && settings.allowDistortion); } private bool InvalidRenderingContext(CameraData cameraData) { #if SWS_DEV //Debug.Log($"Name:{cameraData.camera.name} Type:{cameraData.cameraType} Enabled:{cameraData.camera.enabled}"); #endif //Likely a planar reflections camera or otherwise if (cameraData.camera.cameraType != CameraType.SceneView && cameraData.camera.enabled == false) return true; //Camera stacking and depth-based post processing is essentially non-functional. //All effects render twice to the screen, causing double brightness. Next to fog causing overlay objects to appear transparent //- Best option is to not render anything for overlay cameras //- Reflection probes do not capture the water line correctly //- Preview cameras end up rendering the effect into asset thumbnails if (cameraData.renderType == CameraRenderType.Overlay || cameraData.camera.cameraType == CameraType.Reflection || cameraData.camera.cameraType == CameraType.Preview) return true; #if UNITY_EDITOR //Skip if post-processing is disabled in scene-view if (cameraData.camera.cameraType == CameraType.SceneView && UnityEditor.SceneView.lastActiveSceneView && !UnityEditor.SceneView.lastActiveSceneView.sceneViewState.showImageEffects) return true; #endif //Skip hidden or off-screen cameras. if (cameraData.camera.cameraType == CameraType.Game && cameraData.camera.hideFlags != HideFlags.None) return true; #if UNITY_EDITOR //Skip rendering if editing a prefab #if UNITY_2021_2_OR_NEWER if (UnityEditor.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage() != null) return true; #else if (UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetCurrentPrefabStage() != null) return true; #endif #endif return false; } private int _FullySubmerged = Shader.PropertyToID("_FullySubmerged"); private int _UnderwaterRenderingEnabled = Shader.PropertyToID("_UnderwaterRenderingEnabled"); public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (!UnderwaterRenderer.EnableRendering) return; if (UnderwaterRenderer.Instance == null) return; if (UnderwaterRenderer.Instance.waterMaterial == null) return; if (InvalidRenderingContext(renderingData.cameraData)) return; //Only render if the bottom of the screen touches the water or is below the water level cameraIntersecting = UnderwaterRenderer.Instance.CameraIntersectingWater(renderingData.cameraData.camera); if (cameraIntersecting) { Shader.SetGlobalInt(_UnderwaterRenderingEnabled, 1); keywordStates = UnderwaterRenderer.Instance.materialKeywordStates; cameraSubmerged = UnderwaterRenderer.Instance.CameraSubmerged(renderingData.cameraData.camera); //Once submerged, the pass stops executing. At which point the water mask buffer will be left entirely filled if (!cameraSubmerged) { maskPass.Setup(settings, renderer); } //Note: Previously was assigning Texture2D.redTexture as the water mask, but this breaks in VR since the texture isn't a texture array //Instead return a full white value in the shader function Shader.SetGlobalInt(_FullySubmerged, cameraSubmerged ? 1 : 0); shadingPass.Setup(settings, renderer); if (RequiresPostProcessingPass(UnderwaterRenderer.Instance)) { if (UnderwaterRenderer.Instance.enableDistortion && settings.allowDistortion && settings.distortionMode == UnderwaterRenderFeature.Settings.DistortionMode.CameraSpace) { renderer.EnqueuePass(distortionSpherePass); } postProcessingPass.Setup(settings, renderer); } //No need to render this if the water line won't be visible if (!cameraSubmerged) { renderer.EnqueuePass(waterLinePass); } } else { Shader.SetGlobalInt(_UnderwaterRenderingEnabled, 0); } } public static Material CreateMaterial(string profilerTag, Shader shader) { #if UNITY_EDITOR || DEVELOPMENT_BUILD if (!shader) { //Debug.LogError("[" + profilerTag + "] Shader could not be found, ensure all files are imported"); return null; } #endif return CoreUtils.CreateEngineMaterial(shader); } } } #endif