using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; namespace EPOOutline { /// /// The component that is responsible for rendering the ouline and holding its parameters. /// [ExecuteAlways] [RequireComponent(typeof(Camera))] public class Outliner : MonoBehaviour { #if UNITY_EDITOR private static GameObject lastSelectedOutliner; private static List outliners = new List(); #endif private static List temporaryOutlinables = new List(); private OutlineParameters parameters; private OutlineParameters Parameters => parameters ??= new OutlineParameters(new BasicCommandBufferWrapper(new CommandBuffer())); #if UNITY_EDITOR private OutlineParameters editorPreviewParameters; private OutlineParameters EditorPreviewParameters => editorPreviewParameters ??= new OutlineParameters(new BasicCommandBufferWrapper(new CommandBuffer())); #endif private Camera targetCamera; [SerializeField] private RenderStage stage = RenderStage.AfterTransparents; [SerializeField] private OutlineRenderingStrategy renderingStrategy = OutlineRenderingStrategy.Default; [SerializeField] private RenderingMode renderingMode; [SerializeField] private long outlineLayerMask = -1; [SerializeField] private BufferSizeMode primaryBufferSizeMode; [SerializeField] [Range(0.15f, 1.0f)] private float primaryRendererScale = 0.75f; [SerializeField] private int primarySizeReference = 800; [SerializeField] [Range(0.0f, 2.0f)] private float blurShift = 1.0f; [SerializeField] [Range(0.0f, 2.0f)] private float dilateShift = 1.0f; [SerializeField] private int dilateIterations = 1; [SerializeField] private DilateQuality dilateQuality; [SerializeField] private int blurIterations = 1; [SerializeField] private BlurType blurType = BlurType.Box; private RTHandle target; private RTHandle primaryBuffer; private RTHandle targetBuffer; private CameraEvent Event => stage == RenderStage.BeforeTransparents ? CameraEvent.AfterForwardOpaque : CameraEvent.BeforeImageEffects; /// /// Used to calculate the buffer size if or is set to . /// /// /// public int PrimarySizeReference { get => primarySizeReference; set => primarySizeReference = value < 10 ? 50 : value; } /// /// that will be used for this outliner. /// public BufferSizeMode PrimaryBufferSizeMode { get => primaryBufferSizeMode; set => primaryBufferSizeMode = value; } /// /// that is going to be used by this outliner. /// public OutlineRenderingStrategy RenderingStrategy { get => renderingStrategy; set => renderingStrategy = value; } /// /// to use for this outliner. /// public RenderStage RenderStage { get => stage; set => stage = value; } /// /// that is going to be used for this outliner. /// public DilateQuality DilateQuality { get => dilateQuality; set => dilateQuality = value; } /// /// that is going to be used for this outliner. For HDRP it's always . /// public RenderingMode RenderingMode { get => renderingMode; set => renderingMode = value; } /// /// Blur shift amount that is going to be applied to this outliner. The more, the higher the shift will be. /// public float BlurShift { get => blurShift; set => blurShift = Mathf.Clamp(value, 0, 2.0f); } /// /// Dilate shift amount that is going to be applied to this outliner. The more, the higher the dilate will be. /// public float DilateShift { get => dilateShift; set => dilateShift = Mathf.Clamp(value, 0, 2.0f); } /// /// The layer mask that will be used to filter the before rendering. /// /// public long OutlineLayerMask { get => outlineLayerMask; set => outlineLayerMask = value; } /// /// The value to scale the primary buffer size if is set to . /// public float PrimaryRendererScale { get => primaryRendererScale; set => primaryRendererScale = Mathf.Clamp(value, 0.1f, 1.0f); } /// /// The count of blur iterations to apply. /// public int BlurIterations { get => blurIterations; set => blurIterations = value > 0 ? value : 0; } /// /// to use for this outliner. /// public BlurType BlurType { get => blurType; set => blurType = value; } /// /// The count of dilate iterations to apply. /// public int DilateIterations { get => dilateIterations; set => dilateIterations = value > 0 ? value : 0; } private void OnValidate() { if (blurIterations < 0) blurIterations = 0; if (dilateIterations < 0) dilateIterations = 0; if (primarySizeReference < 10) primarySizeReference = 10; else if (primarySizeReference > 4096) primarySizeReference = 4096; primaryRendererScale = Mathf.Clamp(primaryRendererScale, 0.1f, 1.0f); if (blurType < BlurType.Box) blurType = BlurType.Box; if (blurType > BlurType.Gaussian13x13) blurType = BlurType.Gaussian13x13; } private void OnEnable() { if (targetCamera == null) targetCamera = GetComponent(); targetCamera.forceIntoRenderTexture = targetCamera.stereoTargetEye == StereoTargetEyeMask.None || !UnityEngine.XR.XRSettings.enabled; #if UNITY_EDITOR outliners.Add(this); #endif } private void OnDestroy() { #if UNITY_EDITOR EditorPreviewParameters.Dispose(); #endif Parameters.Dispose(); } private void OnDisable() { if (targetCamera != null) UpdateBuffer(targetCamera, Parameters.Buffer, true); #if UNITY_EDITOR RemoveFromAllSceneViews(); outliners.Remove(this); #endif #if UNITY_EDITOR if (RenderPipelineManager.currentPipeline != null) return; if (!(EditorPreviewParameters.Buffer is IUnderlyingBufferProvider bufferProvider)) return; var underlyingBuffer = bufferProvider.UnderlyingBuffer; foreach (var view in UnityEditor.SceneView.sceneViews) { var viewToUpdate = (UnityEditor.SceneView)view; viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, underlyingBuffer); viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, underlyingBuffer); } #endif } private void UpdateBuffer(Camera cameraToUpdate, CommandBufferWrapper buffer, bool removeOnly) { if (RenderPipelineManager.currentPipeline != null) return; if (!(buffer is IUnderlyingBufferProvider bufferProvider)) return; var underlyingBuffer = bufferProvider.UnderlyingBuffer; if (underlyingBuffer == null) return; cameraToUpdate.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, underlyingBuffer); cameraToUpdate.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, underlyingBuffer); if (removeOnly) return; cameraToUpdate.AddCommandBuffer(Event, underlyingBuffer); } private void OnPreRender() { if (PipelineFetcher.CurrentAsset != null) return; Parameters.OutlinablesToRender.Clear(); SetupOutline(targetCamera, Parameters, false); } private void SetupOutline(Camera cameraToUse, OutlineParameters parametersToUse, bool isEditor) { UpdateBuffer(cameraToUse, parametersToUse.Buffer, false); PrepareParameters(parametersToUse, cameraToUse, isEditor); parametersToUse.Buffer.Clear(); if (renderingStrategy == OutlineRenderingStrategy.Default) { OutlineEffect.SetupOutline(parametersToUse); parametersToUse.BlitMesh = null; parametersToUse.MeshPool.ReleaseAllMeshes(); } else { temporaryOutlinables.Clear(); temporaryOutlinables.AddRange(parametersToUse.OutlinablesToRender); parametersToUse.OutlinablesToRender.Clear(); parametersToUse.OutlinablesToRender.Add(null); foreach (var outlinable in temporaryOutlinables) { parametersToUse.OutlinablesToRender[0] = outlinable; OutlineEffect.SetupOutline(parametersToUse); parametersToUse.BlitMesh = null; } parametersToUse.MeshPool.ReleaseAllMeshes(); } } #if UNITY_EDITOR private void RemoveFromAllSceneViews() { if (RenderPipelineManager.currentPipeline != null) return; foreach (var view in UnityEditor.SceneView.sceneViews) { var viewToUpdate = (UnityEditor.SceneView)view; var eventTransferer = viewToUpdate.camera.GetComponent(); if (eventTransferer != null) eventTransferer.OnPreRenderEvent -= UpdateEditorCamera; if (!(EditorPreviewParameters.Buffer is IUnderlyingBufferProvider bufferProvider)) return; var underlyingBuffer = bufferProvider.UnderlyingBuffer; viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.BeforeImageEffects, underlyingBuffer); viewToUpdate.camera.RemoveCommandBuffer(CameraEvent.AfterForwardOpaque, underlyingBuffer); } } private void LateUpdate() { if (lastSelectedOutliner == null && outliners.Count > 0) lastSelectedOutliner = outliners[0].gameObject; var isSelected = Array.Find(UnityEditor.Selection.gameObjects, x => x == gameObject) ?? lastSelectedOutliner != null; if (isSelected) lastSelectedOutliner = gameObject; foreach (var view in UnityEditor.SceneView.sceneViews) { var viewToUpdate = (UnityEditor.SceneView)view; var eventTransferer = viewToUpdate.camera.GetComponent(); if (eventTransferer != null) eventTransferer.OnPreRenderEvent -= UpdateEditorCamera; UpdateBuffer(viewToUpdate.camera, EditorPreviewParameters.Buffer, true); } if (!isSelected) return; foreach (var view in UnityEditor.SceneView.sceneViews) { var viewToUpdate = (UnityEditor.SceneView)view; if (!viewToUpdate.sceneViewState.showImageEffects) continue; var eventTransferer = viewToUpdate.camera.GetComponent(); if (eventTransferer == null) eventTransferer = viewToUpdate.camera.gameObject.AddComponent(); eventTransferer.OnPreRenderEvent += UpdateEditorCamera; } } private void UpdateEditorCamera(Camera cameraToUpdate) { SetupOutline(cameraToUpdate, EditorPreviewParameters, true); } #endif public StereoTargetEyeMask GetTargetEyeMask(Camera cameraTarget) { return XRUtility.IsXRActive ? cameraTarget.stereoTargetEye : StereoTargetEyeMask.None; } public void UpdateSharedParameters(OutlineParameters parametersToUpdate, Camera cameraToUpdate, bool editorCamera, bool forceNative, bool forceHDR) { parametersToUpdate.DilateQuality = DilateQuality; parametersToUpdate.Camera = cameraToUpdate; parametersToUpdate.IsEditorCamera = editorCamera; parametersToUpdate.PrimaryBufferScale = forceNative ? 1.0f : primaryRendererScale; if (forceNative) parametersToUpdate.PrimaryBufferSizeMode = BufferSizeMode.Native; else { parametersToUpdate.PrimaryBufferSizeMode = primaryBufferSizeMode; parametersToUpdate.PrimaryBufferSizeReference = primarySizeReference; } parametersToUpdate.BlurIterations = blurIterations; parametersToUpdate.BlurType = blurType; parametersToUpdate.DilateIterations = dilateIterations; parametersToUpdate.BlurShift = blurShift; parametersToUpdate.DilateShift = dilateShift; parametersToUpdate.UseHDR = forceHDR || cameraToUpdate.allowHDR && (RenderingMode == RenderingMode.HDR); parametersToUpdate.EyeMask = GetTargetEyeMask(cameraToUpdate); parametersToUpdate.OutlineLayerMask = outlineLayerMask; parametersToUpdate.Prepare(); parametersToUpdate.TextureHandleMap.Clear(); foreach (var outlinable in parametersToUpdate.OutlinablesToRender) { for (var index = 0; index < outlinable.OutlineTargets.Count; index++) { var outlineTarget = outlinable.OutlineTargets[index]; if (outlineTarget.IsValidForCutout) { var cutoutTexture = outlineTarget.CutoutTexture; var rtHandle = parametersToUpdate.RTHandlePool.Allocate(cutoutTexture); parametersToUpdate.TextureHandleMap[cutoutTexture] = rtHandle; } if (outlineTarget.Renderer is SpriteRenderer spriteRenderer) { var texture = spriteRenderer.sprite.texture; var rtHandle = parametersToUpdate.RTHandlePool.Allocate(texture); parametersToUpdate.TextureHandleMap[texture] = rtHandle; } } } } public void ReplaceHandles(OutlineParameters parametersToUpdate) { Replace(ref parametersToUpdate.Handles.Target, parametersToUpdate.TargetWidth, parametersToUpdate.TargetHeight, parametersToUpdate, (width, height, outlineParameters) => RenderTargetUtility.GetRT(outlineParameters, width, height, "Target")); Replace(ref parametersToUpdate.Handles.InfoTarget, parametersToUpdate.TargetWidth, parametersToUpdate.TargetHeight, parametersToUpdate, (width, height, outlineParameters) => RenderTargetUtility.GetRT(outlineParameters, width, height, "Info target")); var (scaledWidth, scaledHeight) = parametersToUpdate.ScaledSize; Replace(ref parametersToUpdate.Handles.PrimaryTarget, scaledWidth, scaledHeight, parametersToUpdate, (width, height, outlineParameters) => RenderTargetUtility.GetRT(outlineParameters, width, height, "Primary target")); Replace(ref parametersToUpdate.Handles.SecondaryTarget, scaledWidth, scaledHeight, parametersToUpdate, (width, height, outlineParameters) => RenderTargetUtility.GetRT(outlineParameters, width, height, "Secondary target")); Replace(ref parametersToUpdate.Handles.PrimaryInfoBufferTarget, scaledWidth, scaledHeight, parametersToUpdate, (width, height, outlineParameters) => RenderTargetUtility.GetRT(outlineParameters, width, height, "Primary info target")); Replace(ref parametersToUpdate.Handles.SecondaryInfoBufferTarget, scaledWidth, scaledHeight, parametersToUpdate, (width, height, outlineParameters) => RenderTargetUtility.GetRT(outlineParameters, width, height, "Secondary info target")); } private static void Replace(ref RTHandle handle, int width, int height, OutlineParameters parameters, Func newHandle) { if (handle != null) { if (width == handle.rtHandleProperties.currentRenderTargetSize.x && height == handle.rtHandleProperties.currentRenderTargetSize.y && handle.rt.descriptor.msaaSamples == parameters.Antialiasing) return; handle.Release(); } handle = newHandle(width, height, parameters); handle.SetCustomHandleProperties(new RTHandleProperties() { currentRenderTargetSize = new Vector2Int(width, height) }); } private void PrepareParameters(OutlineParameters parametersToPrepare, Camera cameraToUse, bool editorCamera) { parametersToPrepare.RTHandlePool.ReleaseAll(); parametersToPrepare.DepthTarget = parametersToPrepare.RTHandlePool.Allocate(RenderTargetUtility.ComposeTarget(parametersToPrepare, BuiltinRenderTextureType.CameraTarget)); parametersToPrepare.Target = parametersToPrepare.RTHandlePool.Allocate(RenderTargetUtility.ComposeTarget(parametersToPrepare, BuiltinRenderTextureType.CameraTarget)); var targetTexture = cameraToUse.targetTexture == null ? cameraToUse.activeTexture : cameraToUse.targetTexture; if (XRUtility.IsUsingVR(parametersToPrepare)) { var descriptor = XRUtility.VRRenderTextureDescriptor; parametersToPrepare.TargetWidth = descriptor.width; parametersToPrepare.TargetHeight = descriptor.height; } else { parametersToPrepare.TargetWidth = targetTexture != null ? targetTexture.width : cameraToUse.scaledPixelWidth; parametersToPrepare.TargetHeight = targetTexture != null ? targetTexture.height : cameraToUse.scaledPixelHeight; } parametersToPrepare.Viewport = new Rect(0, 0, parametersToPrepare.TargetWidth, parametersToPrepare.TargetHeight); parametersToPrepare.Antialiasing = editorCamera ? (targetTexture == null ? 1 : targetTexture.antiAliasing) : CameraUtility.GetMSAA(targetCamera); parametersToPrepare.Camera = cameraToUse; var scaledSize = parametersToPrepare.ScaledSize; parametersToPrepare.ScaledBufferWidth = scaledSize.ScaledWidth; parametersToPrepare.ScaledBufferHeight = scaledSize.ScaledHeight; Outlinable.GetAllActiveOutlinables(parametersToPrepare.OutlinablesToRender); RendererFilteringUtility.Filter(parametersToPrepare.Camera, parametersToPrepare); UpdateSharedParameters(parametersToPrepare, cameraToUse, editorCamera, false, false); ReplaceHandles(parametersToPrepare); } } }