using System; using UnityEngine; using UnityEngine.Rendering; #if UPPipeline using UnityEngine.Experimental.Rendering; using UnityEngine.Rendering.Universal; #endif namespace Gaia { [ExecuteAlways] public class GaiaPlanarReflections : MonoBehaviour { [Serializable] public class PlanarReflectionSettings { public bool m_enableReflections = true; public GaiaConstants.ResolutionMulltiplier m_ResolutionMultiplier = GaiaConstants.ResolutionMulltiplier.Half; public float m_ClipPlaneOffset = 0f; public LayerMask m_ReflectLayers = -1; public bool m_Shadows = false; public bool m_enableRenderDistance = false; public bool m_enableRenderDistances = false; public float m_customRenderDistance = 500f; public float[] m_customRenderDistances = new float[32]; public int m_textureResolution = 512; } public class PlanarReflectionSettingData { private bool _fog; private int _maxLod; private float _lodBias; public void Get() { _fog = RenderSettings.fog; _maxLod = QualitySettings.maximumLODLevel; _lodBias = QualitySettings.lodBias; } public void Set() { GL.invertCulling = true; RenderSettings.fog = true; // disable fog for now as it's incorrect with projection QualitySettings.maximumLODLevel = 1; QualitySettings.lodBias = _lodBias * 0.5f; } public void Restore() { GL.invertCulling = false; RenderSettings.fog = _fog; QualitySettings.maximumLODLevel = _maxLod; QualitySettings.lodBias = _lodBias; } } public PlanarReflectionSettings m_settings = new PlanarReflectionSettings(); public PlanarReflectionSettingData m_reflectionData = new PlanarReflectionSettingData(); public static event Action BeginPlanarReflections; private static Camera m_reflectionCamera; private RenderTexture m_reflectionTexture = null; [SerializeField] private float[] m_distances = new float[32]; #region Unity Functions /// /// Load on enable /// private void OnEnable() { RenderPipelineManager.beginCameraRendering += ExecutePlanarReflections; } /// /// Execute on disable /// private void OnDisable() { Cleanup(); } /// /// Execute when the object is destroyed /// private void OnDestroy() { Cleanup(); } #endregion #region Planar Reflection Functions /// /// Calculates the matrix of the reflection plane view /// /// /// private static void CalculateReflectionMatrix(ref Matrix4x4 reflectionMat, Vector4 plane) { reflectionMat.m00 = (1f - 2f * plane[0] * plane[0]); reflectionMat.m01 = (-2f * plane[0] * plane[1]); reflectionMat.m02 = (-2f * plane[0] * plane[2]); reflectionMat.m03 = (-2f * plane[3] * plane[0]); reflectionMat.m10 = (-2f * plane[1] * plane[0]); reflectionMat.m11 = (1f - 2f * plane[1] * plane[1]); reflectionMat.m12 = (-2f * plane[1] * plane[2]); reflectionMat.m13 = (-2f * plane[3] * plane[1]); reflectionMat.m20 = (-2f * plane[2] * plane[0]); reflectionMat.m21 = (-2f * plane[2] * plane[1]); reflectionMat.m22 = (1f - 2f * plane[2] * plane[2]); reflectionMat.m23 = (-2f * plane[3] * plane[2]); reflectionMat.m30 = 0f; reflectionMat.m31 = 0f; reflectionMat.m32 = 0f; reflectionMat.m33 = 1f; } /// /// Updates position /// /// /// private static Vector3 ReflectPosition(Vector3 pos) { var newPos = new Vector3(pos.x, -pos.y, pos.z); return newPos; } /// /// Updates the camera planes based on it's current view /// /// /// /// /// /// private Vector4 CameraSpacePlane(Camera cam, Vector3 pos, Vector3 normal, float sideSign) { var offsetPos = pos + normal * m_settings.m_ClipPlaneOffset; var m = cam.worldToCameraMatrix; var cameraPosition = m.MultiplyPoint(offsetPos); var cameraNormal = m.MultiplyVector(normal).normalized * sideSign; return new Vector4(cameraNormal.x, cameraNormal.y, cameraNormal.z, -Vector3.Dot(cameraPosition, cameraNormal)); } /// /// Builds the reflection texture if null or new resolution has been set /// private void PlanarReflectionTexture() { if (m_reflectionTexture == null) { #if UPPipeline var res = ReflectionResolution(m_settings.m_textureResolution, UniversalRenderPipeline.asset.renderScale); const RenderTextureFormat hdrFormat = RenderTextureFormat.DefaultHDR; m_reflectionTexture = RenderTexture.GetTemporary(res.x, res.y, 24, GraphicsFormatUtility.GetGraphicsFormat(hdrFormat, true)); #endif } else { if (m_reflectionTexture.width != m_settings.m_textureResolution) { #if UPPipeline var res = ReflectionResolution(m_settings.m_textureResolution, UniversalRenderPipeline.asset.renderScale); const RenderTextureFormat hdrFormat = RenderTextureFormat.DefaultHDR; m_reflectionTexture = RenderTexture.GetTemporary(res.x, res.y, 24, GraphicsFormatUtility.GetGraphicsFormat(hdrFormat, true)); #endif } } m_reflectionCamera.targetTexture = m_reflectionTexture; } /// /// Gets the reflection resolution /// /// /// /// private Vector2Int ReflectionResolution(int textureSize, float scale) { var x = (int)(textureSize * scale * GetScaleValue()); var y = (int)(textureSize * scale * GetScaleValue()); return new Vector2Int(x, y); } /// /// Executes the planar reflections functions /// /// /// private void ExecutePlanarReflections(ScriptableRenderContext context, Camera cam) { if (cam.cameraType == CameraType.Reflection || cam.cameraType == CameraType.Preview) { return; } UpdateReflectionCamera(cam); PlanarReflectionTexture(); if (m_reflectionData == null) { m_reflectionData = new PlanarReflectionSettingData(); } m_reflectionData.Get(); m_reflectionData.Set(); BeginPlanarReflections?.Invoke(context, m_reflectionCamera); #if UPPipeline UniversalRenderPipeline.RenderSingleCamera(context, m_reflectionCamera); #endif Shader.SetGlobalTexture(GaiaShaderID.m_globalReflectionTexture, m_reflectionTexture); m_reflectionData.Restore(); } #endregion #region Utils /// /// Cleans up planar reflections /// private void Cleanup() { RenderPipelineManager.beginCameraRendering -= ExecutePlanarReflections; if(m_reflectionCamera != null) { m_reflectionCamera.targetTexture = null; SafeDestroy(m_reflectionCamera.gameObject); } if (m_reflectionTexture != null) { RenderTexture.ReleaseTemporary(m_reflectionTexture); } } /// /// Creates the reflection camera /// /// private Camera CreateMirrorObjects() { var go = new GameObject("Gaia Planar Reflections Camera",typeof(Camera)); #if UPPipeline var cameraData = go.AddComponent(typeof(UniversalAdditionalCameraData)) as UniversalAdditionalCameraData; cameraData.requiresColorOption = CameraOverrideOption.Off; cameraData.requiresDepthOption = CameraOverrideOption.Off; cameraData.SetRenderer(0); #endif var t = transform; var reflectionCamera = go.GetComponent(); reflectionCamera.transform.SetPositionAndRotation(t.position, t.rotation); reflectionCamera.depth = -10; reflectionCamera.enabled = false; go.hideFlags = HideFlags.HideAndDontSave; return reflectionCamera; } /// /// Destory the object /// /// private static void SafeDestroy(GameObject obj) { if (Application.isEditor) { DestroyImmediate(obj); } else { Destroy(obj); } } /// /// Updates the reflection camera settings /// /// /// private void UpdateCamera(Camera src, Camera dest) { if (dest == null) return; dest.CopyFrom(src); dest.useOcclusionCulling = true; SetCameraBackgroundType(dest, CameraClearFlags.SolidColor); #if UPPipeline if (dest.gameObject.TryGetComponent(out UniversalAdditionalCameraData camData)) { camData.SetRenderer(0); camData.renderShadows = m_settings.m_Shadows; // turn off shadows for the reflection camera } #endif } /// /// /// /// private void SetCameraBackgroundType(Camera camera, CameraClearFlags backgroundRenderMode) { if (camera == null) { return; } camera.clearFlags = backgroundRenderMode; camera.backgroundColor = RenderSettings.fogColor; } /// /// Updates the reflection camera for rendering /// /// private void UpdateReflectionCamera(Camera realCamera) { if (m_reflectionCamera == null) { m_reflectionCamera = CreateMirrorObjects(); } Vector3 pos = Vector3.zero; Vector3 normal = Vector3.up; UpdateCamera(realCamera, m_reflectionCamera); var d = Vector3.Dot(normal, pos) - m_settings.m_ClipPlaneOffset; var reflectionPlane = new Vector4(normal.x, normal.y, normal.z, d); var reflection = Matrix4x4.identity; reflection = Matrix4x4.Scale(new Vector3(1, 1, 1)); CalculateReflectionMatrix(ref reflection, reflectionPlane); var oldPosition = realCamera.transform.position - new Vector3(0, pos.y, 0); var newPosition = ReflectPosition(oldPosition); m_reflectionCamera.transform.forward = Vector3.Scale(realCamera.transform.forward, new Vector3(1, 1, 1)); m_reflectionCamera.worldToCameraMatrix = realCamera.worldToCameraMatrix * reflection; var clipPlane = CameraSpacePlane(m_reflectionCamera, pos - Vector3.up * 0.1f, normal, 1.0f); var projection = realCamera.CalculateObliqueMatrix(clipPlane); m_reflectionCamera.projectionMatrix = projection; if (m_settings.m_enableReflections) { m_reflectionCamera.cullingMask = ~(1 << 4) & m_settings.m_ReflectLayers; } else { m_reflectionCamera.cullingMask = 0; } if (m_distances.Length != 32) { m_distances = new float[32]; } if (m_settings.m_enableRenderDistance) { if (m_settings.m_enableRenderDistances) { m_reflectionCamera.layerCullDistances = m_settings.m_customRenderDistances; m_reflectionCamera.layerCullSpherical = true; } else { for (int idx = 0; idx < m_distances.Length; idx++) { m_distances[idx] = m_settings.m_customRenderDistance; } m_reflectionCamera.layerCullDistances = m_distances; m_reflectionCamera.layerCullSpherical = true; } } else { for (int idx = 0; idx < m_distances.Length; idx++) { m_distances[idx] = 0f; } m_reflectionCamera.layerCullDistances = m_distances; m_reflectionCamera.layerCullSpherical = true; } m_reflectionCamera.transform.position = newPosition; } /// /// Gets the multiplier scale /// /// private float GetScaleValue() { switch(m_settings.m_ResolutionMultiplier) { case GaiaConstants.ResolutionMulltiplier.Full: return 1f; case GaiaConstants.ResolutionMulltiplier.Half: return 0.5f; case GaiaConstants.ResolutionMulltiplier.Third: return 0.33f; case GaiaConstants.ResolutionMulltiplier.Quarter: return 0.25f; default: return 0.5f; } } #endregion } }