using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal; #if ENABLE_VR using UnityEngine.XR; #endif namespace Golems { public class OutlineScriptableRenderFeature : ScriptableRendererFeature { [SerializeField] private bool m_IsEnabled = true; [SerializeField] private Material m_SolidUnlitMaterial = default; [SerializeField] private Material m_BlurMaterial = default; private OutlineScriptableRenderPass m_Pass = default; [SerializeField] private RenderPassEvent m_RenderPassEvent = RenderPassEvent.AfterRenderingOpaques; /// /// The number of Blur passes performed. /// This effects ALL objects rendered /// [Tooltip("How many times we Blur all the objects. Note, the bigger the number, the bigger the performance hit")] [Range(1, 10)][SerializeField] private int m_BlurPasses = 1; /// /// Effects how far off centre each Blur sample becomes /// [Tooltip("Effects how far off centre each Blur sample becomes. Note, the bigger the number, the bigger the pixelation")] [Range(0.0f, 10.0f)] [SerializeField] private float m_BlurRadius = .5f; /// /// Index into the m_BlurTextureSizes /// [Range(0,4)] [SerializeField] private int m_BlurTextureSize = 3; /// /// Texture size of the blur textures /// private int[] m_BlurTextureSizes = new int[5] { 128, 256, 512, 1024, 2048 }; [Header("Outlines Occluders")] /// /// Material used to render the occluders /// [Tooltip("Material used to render the occluders")] [SerializeField] Material m_OutlinesOccludersMat; /// /// Used to set the objects that will be part of the outlines occluders /// [Tooltip("Used to set the objects that will be part of the outlines occluders")] [SerializeField] LayerMask m_OccludersMask; /// /// Initializes this feature's resources. /// public override void Create() { m_Pass = new OutlineScriptableRenderPass(m_SolidUnlitMaterial, m_BlurMaterial, m_OutlinesOccludersMat, m_OccludersMask); } private void DebugSetEnabledState(bool state) { m_IsEnabled = state; } private bool DebugGetEnabledState() { return m_IsEnabled; } /// /// Injects the outline ScriptableRenderPass into the renderer. /// public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (!m_IsEnabled) { return; } // // Zeroing this here in case the pass is not added // OutlineCollection.NumberOfObjectsRendered = 0; var outlineRenderers = OutlineCollection.RendererBundles; if (outlineRenderers == null || outlineRenderers.Count <= 0) return; m_Pass.Setup( m_RenderPassEvent, renderingData.cameraData.camera, outlineRenderers, m_BlurPasses, m_BlurRadius, m_BlurTextureSizes[Mathf.Max(0, Mathf.Min(m_BlurTextureSize, m_BlurTextureSizes.Length-1))]); renderer.EnqueuePass(m_Pass); } } public class OutlineScriptableRenderPass : ScriptableRenderPass { private const string k_ProfilerIdent = "Outline"; #region Texture Consts private const string k_DefaultDrawObjectsTex = "_DefaultDrawObjects"; private const string k_SolidDrawObjectsTex = "_SolidDrawObjects"; private const string k_BlurTex = "_BlurTex"; private const string k_MainText = "_MainTex"; private const string k_TempTex = "_TempTex"; #endregion Texture Consts #region Shader Fields private readonly int m_ColourID = Shader.PropertyToID("_Color"); private readonly int m_AlphaID = Shader.PropertyToID("_Alpha"); private readonly int m_BlurResolution = Shader.PropertyToID("_BlurResolution"); private readonly int m_BlurRadius = Shader.PropertyToID("_BlurRadius"); private readonly int m_BlurDirectionX = Shader.PropertyToID("_BlurDirectionX"); private readonly int m_BlurDirectionY = Shader.PropertyToID("_BlurDirectionY"); #endregion Shader Fields #region Render Texture IDs private readonly int m_DefaultDrawObjects = Shader.PropertyToID(k_DefaultDrawObjectsTex); private readonly int m_SolidDrawObjects = Shader.PropertyToID(k_SolidDrawObjectsTex); private readonly int m_BlurPassRenderTexID = Shader.PropertyToID(k_BlurTex); private readonly int m_TempRenderTexID = Shader.PropertyToID(k_TempTex); #endregion private Camera m_Camera; private IList m_Renderers; private Material m_SolidUnlitMaterial; private Material m_BlurMaterial; /// /// The uniform Blur texture size, calculated from the current screen size /// private int m_BlurRes; /// /// Effects how far off centre each Blur sample becomes /// private float m_BlurRad = 1.0f; /// /// The number of Blur passes performed. /// This effects ALL objects rendered /// private int m_BlurPasses = 1; /// /// Used in DrawObjectsXYZ. We use this in conjunction with GetSharedMaterials. /// Avoids GC alloc from .sharedMaterials /// private readonly List m_SharedMaterials = new List(); /// /// Used to Get and Store the each Outline instances Color /// private Color m_OutlineColour = new Color(1,1,1,1); /// /// Occluders shader tags list /// private List m_ShaderTagsList = new List(); /// /// Filter settings for occluders rendering /// private FilteringSettings m_FilteringSettings; /// /// Material used to render the occluders /// private Material m_OccludersOverrideMaterial; #if UNITY_EDITOR private readonly HashSet m_EditorLoggedNullMaterialOutlines = new HashSet(); private readonly HashSet m_EditorLoggedNullMaterialRenderers = new HashSet(); #endif public OutlineScriptableRenderPass(Material solidUnlitMaterial, Material blurMaterial, Material occludersOverrideMaterial, LayerMask occludersMask) { m_BlurMaterial = blurMaterial; m_SolidUnlitMaterial = solidUnlitMaterial; m_OccludersOverrideMaterial = occludersOverrideMaterial; // Initialise occluders related properties m_ShaderTagsList.Add(new ShaderTagId("SRPDefaultUnlit")); m_ShaderTagsList.Add(new ShaderTagId("UniversalForward")); m_ShaderTagsList.Add(new ShaderTagId("UniversalForwardOnly")); m_ShaderTagsList.Add(new ShaderTagId("ForwardLit")); m_FilteringSettings = new FilteringSettings(RenderQueueRange.opaque, occludersMask); } public void Setup(RenderPassEvent whenToRender, Camera camera, IList renderers, int blurPasses, float blurRadius, int blurTextureSize) { renderPassEvent = whenToRender; m_Renderers = renderers; m_Camera = camera; m_BlurPasses = blurPasses; m_BlurRad = blurRadius; m_BlurRes = blurTextureSize; } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { // // Attempt to make RTs for XR, otherwise use Default Settings // if (!ConfigureTemporaryRenderTexturesForXR(in cmd)) ConfigureTemporaryRenderTextures(in cmd); } public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { ConfigureTarget(m_SolidDrawObjects); ConfigureClear(ClearFlag.All, Color.clear); } /// /// Execute the pass. This is where custom rendering occurs. Specific details are left to the implementation /// public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { // // Always make sure we reset, this is the begining of a new Outline process // OutlineCollection.NumberOfObjectsRendered = 0; var cmd = CommandBufferPool.Get(k_ProfilerIdent); cmd.Clear(); // // First render objects using their default materials // DrawObjectsDefault(cmd); // // OutlineCollection.NumberOfObjectsRendered is incremented // inside DrawObjectsDefault. If it's 0 we rendered nothing. // if (OutlineCollection.NumberOfObjectsRendered > 0) { DrawOccluders(context, ref renderingData); DrawObjectsSolid(cmd); cmd.SetGlobalFloat(m_BlurResolution, m_BlurRes); cmd.SetGlobalFloat(m_BlurRadius, m_BlurRad); DrawBlur(in cmd); context.ExecuteCommandBuffer(cmd); } cmd.Clear(); CommandBufferPool.Release(cmd); } /// /// Draw outlines occluders /// private void DrawOccluders(ScriptableRenderContext context, ref RenderingData renderingData) { // Draw the occluders into m_SolidDrawObjects // This pass will fill in the depth buffer so that the outlines could get occluded by the soldiers or other occluders as needed DrawingSettings drawingSettings = CreateDrawingSettings(m_ShaderTagsList, ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags); drawingSettings.overrideMaterial = m_OccludersOverrideMaterial; context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref m_FilteringSettings); } /// /// Draw all objects using their default material/s /// private void DrawObjectsDefault(in CommandBuffer commandBuffer) { CoreUtils.SetRenderTarget(commandBuffer, m_DefaultDrawObjects, ClearFlag.All, Color.clear); commandBuffer.ClearRenderTarget(true, true, Color.clear); for (int i = 0, counti = m_Renderers.Count; i < counti; ++i) { var outline = m_Renderers[i]; // // Don't waste time drawing objects which are currently invisible // if (outline.Alpha <= 0) continue; ++OutlineCollection.NumberOfObjectsRendered; var outlineRenderers = outline.Renderers; for (int j = 0; j < outlineRenderers.Length; j++) { var renderer = outlineRenderers[j]; if (renderer == null) continue; m_SharedMaterials.Clear(); renderer.GetSharedMaterials(m_SharedMaterials); if (m_SharedMaterials.Count > 0) { for (int m = 0, countm = m_SharedMaterials.Count; m < countm; ++m) { if (m_SharedMaterials[m] == null) { #if UNITY_EDITOR if (!m_EditorLoggedNullMaterialOutlines.Contains(outline)) { Debug.LogErrorFormat(outline, "Outline {0} has a renderer {1} with null material", outline.name, renderer.name); m_EditorLoggedNullMaterialOutlines.Add(outline); } if (!m_EditorLoggedNullMaterialRenderers.Contains(renderer)) { Debug.LogErrorFormat(outline, "Outline {0} has a renderer {1} with null material", outline.name, renderer.name); m_EditorLoggedNullMaterialRenderers.Add(renderer); } #endif continue; } commandBuffer.DrawRenderer(renderer, m_SharedMaterials[m], m, 0); } } else { commandBuffer.DrawRenderer(renderer, renderer.material); } } } } /// /// Draw all objects using the special Solid Unlit shader /// private void DrawObjectsSolid(in CommandBuffer commandBuffer) { CoreUtils.SetRenderTarget(commandBuffer, m_SolidDrawObjects, ClearFlag.None, Color.clear); //commandBuffer.ClearRenderTarget(true, true, Color.clear); for (int i = 0, counti = m_Renderers.Count; i < counti; ++i) { var outline = m_Renderers[i]; if (outline.Alpha <= 0) continue; outline.GetColour(out m_OutlineColour); commandBuffer.SetGlobalColor(m_ColourID, m_OutlineColour); commandBuffer.SetGlobalFloat(m_AlphaID, outline.Alpha); var outlineRenderers = outline.Renderers; for (int j = 0; j < outlineRenderers.Length; ++j) { var renderer = outlineRenderers[j]; if (renderer == null) continue; m_SharedMaterials.Clear(); renderer.GetSharedMaterials(m_SharedMaterials); if (m_SharedMaterials.Count > 0) { for (int m = 0, countm = m_SharedMaterials.Count; m < countm; ++m) { commandBuffer.DrawRenderer(renderer, m_SolidUnlitMaterial, m, 0); } } else { commandBuffer.DrawRenderer(renderer, m_SolidUnlitMaterial); } } } } /// /// Creates the Blur mask RT. Blur the Outline objects /// private void DrawBlur(in CommandBuffer commandBuffer) { commandBuffer.Blit(m_SolidDrawObjects, m_BlurPassRenderTexID); for (int i = 0, counti = m_BlurPasses; i < counti; ++i) { commandBuffer.SetGlobalFloat(m_BlurDirectionX, 0); commandBuffer.SetGlobalFloat(m_BlurDirectionY, 1); commandBuffer.SetGlobalTexture(k_MainText, m_BlurPassRenderTexID); commandBuffer.Blit(m_BlurPassRenderTexID, m_TempRenderTexID, m_BlurMaterial, 0); commandBuffer.SetGlobalFloat(m_BlurDirectionX, 1); commandBuffer.SetGlobalFloat(m_BlurDirectionY, 0); commandBuffer.SetGlobalTexture(k_MainText, m_TempRenderTexID); commandBuffer.Blit(m_TempRenderTexID, m_BlurPassRenderTexID, m_BlurMaterial, 0); } } /// /// Configure Temporary RTs for XR /// Relies on ENABLE_VR /// /// SRF CommandBuffer /// True if RTs were setup using XRSettings, Otherwise False. private bool ConfigureTemporaryRenderTexturesForXR(in CommandBuffer cmd) { #if ENABLE_VR if (!XRSettings.enabled) return false; var renderTextureDescriptor = XRSettings.eyeTextureDesc; cmd.GetTemporaryRT(m_DefaultDrawObjects, renderTextureDescriptor, FilterMode.Bilinear); cmd.GetTemporaryRT(m_SolidDrawObjects, renderTextureDescriptor, FilterMode.Bilinear); renderTextureDescriptor.depthBufferBits = 0; renderTextureDescriptor.width = renderTextureDescriptor.height = m_BlurRes; cmd.GetTemporaryRT(m_BlurPassRenderTexID, renderTextureDescriptor, FilterMode.Bilinear); cmd.GetTemporaryRT(m_TempRenderTexID, renderTextureDescriptor, FilterMode.Bilinear); return true; #endif return false; } /// /// Configure Temporary RTs for SRF. /// /// SRF CommandBuffer private void ConfigureTemporaryRenderTextures(in CommandBuffer cmd) { var width = m_Camera.scaledPixelWidth; var height = m_Camera.scaledPixelHeight; var aa = Mathf.Max(1, QualitySettings.antiAliasing); // // We can't really down sample the Solid Render. This is the render of the // cmd.GetTemporaryRT(m_DefaultDrawObjects, width, height, 24, FilterMode.Bilinear, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default, aa); cmd.GetTemporaryRT(m_SolidDrawObjects, width >> 1, height >> 1, 24, FilterMode.Bilinear, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default,aa); cmd.GetTemporaryRT(m_BlurPassRenderTexID, m_BlurRes, m_BlurRes, 0, FilterMode.Bilinear); cmd.GetTemporaryRT(m_TempRenderTexID, m_BlurRes, m_BlurRes, 0, FilterMode.Bilinear); } } }