// Copyright (C) 2019-2021 Alexander Bogarsukov. All rights reserved. // See the LICENSE.md file in the project root for more information. using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.XR; namespace UnityFx.Outline { /// /// Helper class for outline rendering with . /// /// /// The class can be used on its own or as part of a higher level systems. It is used /// by higher level outline implementations ( and /// ). It is fully compatible with Unity post processing stack as well. /// The class implements to be used inside /// block as shown in the code samples. Disposing does not dispose /// the corresponding . /// Command buffer is not cleared before rendering. It is user responsibility to do so if needed. /// /// /// var commandBuffer = new CommandBuffer(); /// /// using (var renderer = new OutlineRenderer(commandBuffer, resources)) /// { /// renderer.Render(renderers, settings); /// } /// /// camera.AddCommandBuffer(CameraEvent.BeforeImageEffects, commandBuffer); /// /// public readonly struct OutlineRenderer : IDisposable { #region data private readonly TextureDimension _rtDimention; private readonly RenderTargetIdentifier _rt; private readonly RenderTargetIdentifier _depth; private readonly CommandBuffer _commandBuffer; private readonly OutlineResources _resources; #endregion #region interface /// /// A default outline rendering should be assosiated with. /// public const CameraEvent RenderEvent = CameraEvent.AfterSkybox; /// /// A default render texture format for the outline effect. /// public const RenderTextureFormat RtFormat = RenderTextureFormat.R8; /// /// Initializes a new instance of the struct. /// /// A to render the effect to. It should be cleared manually (if needed) before passing to this method. /// Outline resources. /// Thrown if is . public OutlineRenderer(CommandBuffer cmd, OutlineResources resources) : this(cmd, resources, BuiltinRenderTextureType.CameraTarget, BuiltinRenderTextureType.Depth, Vector2Int.zero) { } /// /// Initializes a new instance of the struct. /// /// A to render the effect to. It should be cleared manually (if needed) before passing to this method. /// Outline resources. /// The rendering path of target camera (). /// Thrown if is . public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderingPath renderingPath) : this(cmd, resources, BuiltinRenderTextureType.CameraTarget, GetBuiltinDepth(renderingPath), Vector2Int.zero) { } /// /// Initializes a new instance of the struct. /// /// A to render the effect to. It should be cleared manually (if needed) before passing to this method. /// Outline resources. /// Render target. /// Thrown if is . public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst) : this(cmd, resources, dst, BuiltinRenderTextureType.Depth, Vector2Int.zero) { } /// /// Initializes a new instance of the struct. /// /// A to render the effect to. It should be cleared manually (if needed) before passing to this method. /// Render target. /// The rendering path of target camera (). /// Thrown if is . public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst, RenderingPath renderingPath, Vector2Int rtSize) : this(cmd, resources, dst, GetBuiltinDepth(renderingPath), rtSize) { } /// /// Initializes a new instance of the struct. /// /// A to render the effect to. It should be cleared manually (if needed) before passing to this method. /// Outline resources. /// Render target. /// Depth dexture to use. /// Thrown if is . public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst, RenderTargetIdentifier depth, Vector2Int rtSize) { if (cmd is null) { throw new ArgumentNullException(nameof(cmd)); } if (resources is null) { throw new ArgumentNullException(nameof(resources)); } if (rtSize.x <= 0) { rtSize.x = -1; } if (rtSize.y <= 0) { rtSize.y = -1; } if (XRSettings.enabled) { var rtDesc = XRSettings.eyeTextureDesc; rtDesc.shadowSamplingMode = ShadowSamplingMode.None; rtDesc.depthBufferBits = 0; rtDesc.colorFormat = RtFormat; cmd.GetTemporaryRT(resources.MaskTexId, rtDesc, FilterMode.Bilinear); cmd.GetTemporaryRT(resources.TempTexId, rtDesc, FilterMode.Bilinear); _rtDimention = rtDesc.dimension; } else { cmd.GetTemporaryRT(resources.MaskTexId, rtSize.x, rtSize.y, 0, FilterMode.Bilinear, RtFormat); cmd.GetTemporaryRT(resources.TempTexId, rtSize.x, rtSize.y, 0, FilterMode.Bilinear, RtFormat); _rtDimention = TextureDimension.Tex2D; } _rt = dst; _depth = depth; _commandBuffer = cmd; _resources = resources; } /// /// Initializes a new instance of the struct. /// /// A to render the effect to. It should be cleared manually (if needed) before passing to this method. /// Outline resources. /// Render target. /// Depth dexture to use. /// Render texture decsriptor. /// Thrown if is . public OutlineRenderer(CommandBuffer cmd, OutlineResources resources, RenderTargetIdentifier dst, RenderTargetIdentifier depth, RenderTextureDescriptor rtDesc) { if (cmd is null) { throw new ArgumentNullException(nameof(cmd)); } if (resources is null) { throw new ArgumentNullException(nameof(resources)); } if (rtDesc.width <= 0) { rtDesc.width = -1; } if (rtDesc.height <= 0) { rtDesc.height = -1; } if (rtDesc.dimension == TextureDimension.None || rtDesc.dimension == TextureDimension.Unknown) { rtDesc.dimension = TextureDimension.Tex2D; } rtDesc.shadowSamplingMode = ShadowSamplingMode.None; rtDesc.depthBufferBits = 0; rtDesc.colorFormat = RtFormat; rtDesc.msaaSamples = 1; cmd.GetTemporaryRT(resources.MaskTexId, rtDesc, FilterMode.Bilinear); cmd.GetTemporaryRT(resources.TempTexId, rtDesc, FilterMode.Bilinear); _rtDimention = rtDesc.dimension; _rt = dst; _depth = depth; _commandBuffer = cmd; _resources = resources; } /// /// Renders outline around a single object. /// /// An object to be outlined. /// public void Render(OutlineRenderObject obj) { Render(obj.Renderers, obj.OutlineSettings, obj.Tag); } /// /// Renders outline around multiple . /// /// An object to be outlined. /// Thrown if is . /// public void Render(IReadOnlyList objects) { if (objects is null) { throw new ArgumentNullException(nameof(objects)); } for (var i = 0; i < objects.Count; i++) { Render(objects[i]); } } /// /// Renders outline around multiple . /// /// One or more renderers representing a single object to be outlined. /// Outline settings. /// Optional name of the sample (visible in profiler). /// Thrown if any of the arguments is . /// public void Render(IReadOnlyList renderers, IOutlineSettings settings, string sampleName = null) { if (renderers is null) { throw new ArgumentNullException(nameof(renderers)); } if (settings is null) { throw new ArgumentNullException(nameof(settings)); } if (renderers.Count > 0) { // NOTE: Remove BeginSample/EndSample for now (https://github.com/Arvtesh/UnityFx.Outline/issues/44). //if (string.IsNullOrEmpty(sampleName)) //{ // sampleName = renderers[0].name; //} //_commandBuffer.BeginSample(sampleName); { RenderObjectClear(settings.OutlineRenderMode); for (var i = 0; i < renderers.Count; ++i) { DrawRenderer(renderers[i], settings); } RenderOutline(settings); } //_commandBuffer.EndSample(sampleName); } } /// /// Renders outline around a single . /// /// A representing an object to be outlined. /// Outline settings. /// Optional name of the sample (visible in profiler). /// Thrown if any of the arguments is . /// public void Render(Renderer renderer, IOutlineSettings settings, string sampleName = null) { if (renderer is null) { throw new ArgumentNullException(nameof(renderer)); } if (settings is null) { throw new ArgumentNullException(nameof(settings)); } // NOTE: Remove BeginSample/EndSample for now (https://github.com/Arvtesh/UnityFx.Outline/issues/44). //if (string.IsNullOrEmpty(sampleName)) //{ // sampleName = renderer.name; //} // NOTE: Remove this for now (https://github.com/Arvtesh/UnityFx.Outline/issues/44). //_commandBuffer.BeginSample(sampleName); { RenderObjectClear(settings.OutlineRenderMode); DrawRenderer(renderer, settings); RenderOutline(settings); } //_commandBuffer.EndSample(sampleName); } /// /// Specialized render target setup. Do not use if not sure. /// public void RenderObjectClear(OutlineRenderFlags flags) { // NOTE: Use the camera depth buffer when rendering the mask. Shader only reads from the depth buffer (ZWrite Off). if ((flags & OutlineRenderFlags.EnableDepthTesting) != 0) { if (_rtDimention == TextureDimension.Tex2DArray) { // NOTE: Need to use this SetRenderTarget overload for XR, otherwise single pass instanced rendering does not function properly. _commandBuffer.SetRenderTarget(_resources.MaskTex, _depth, 0, CubemapFace.Unknown, -1); } else { _commandBuffer.SetRenderTarget(_resources.MaskTex, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, _depth, RenderBufferLoadAction.Load, RenderBufferStoreAction.DontCare); } } else { if (_rtDimention == TextureDimension.Tex2DArray) { _commandBuffer.SetRenderTarget(_resources.MaskTex, 0, CubemapFace.Unknown, -1); } else { _commandBuffer.SetRenderTarget(_resources.MaskTex, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store); } } _commandBuffer.ClearRenderTarget(false, true, Color.clear); } /// /// Renders outline. Do not use if not sure. /// public void RenderOutline(IOutlineSettings settings) { var mat = _resources.OutlineMaterial; var props = _resources.GetProperties(settings); _commandBuffer.SetGlobalFloatArray(_resources.GaussSamplesId, _resources.GetGaussSamples(settings.OutlineWidth)); if (_rtDimention == TextureDimension.Tex2DArray) { // HPass _commandBuffer.SetRenderTarget(_resources.TempTex, 0, CubemapFace.Unknown, -1); Blit(_resources.MaskTex, OutlineResources.OutlineShaderHPassId, mat, props); // VPassBlend _commandBuffer.SetRenderTarget(_rt, 0, CubemapFace.Unknown, -1); Blit(_resources.TempTex, OutlineResources.OutlineShaderVPassId, mat, props); } else { // HPass _commandBuffer.SetRenderTarget(_resources.TempTex, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store); Blit(_resources.MaskTex, OutlineResources.OutlineShaderHPassId, mat, props); // VPassBlend _commandBuffer.SetRenderTarget(_rt, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store); Blit(_resources.TempTex, OutlineResources.OutlineShaderVPassId, mat, props); } } #endregion #region IDisposable /// /// Finalizes the effect rendering and releases temporary textures used. Should only be called once. /// public void Dispose() { _commandBuffer.ReleaseTemporaryRT(_resources.TempTexId); _commandBuffer.ReleaseTemporaryRT(_resources.MaskTexId); } #endregion #region implementation private void DrawRenderer(Renderer renderer, IOutlineSettings settings) { if (renderer && renderer.enabled && renderer.isVisible && renderer.gameObject.activeInHierarchy) { // NOTE: Accessing Renderer.sharedMaterials triggers GC.Alloc. That's why we use a temporary // list of materials, cached with the outline resources. renderer.GetSharedMaterials(_resources.TmpMaterials); if (_resources.TmpMaterials.Count > 0) { if (settings.IsAlphaTestingEnabled()) { for (var i = 0; i < _resources.TmpMaterials.Count; ++i) { var mat = _resources.TmpMaterials[i]; // Use material cutoff value if available. if (mat.HasProperty(_resources.AlphaCutoffId)) { _commandBuffer.SetGlobalFloat(_resources.AlphaCutoffId, mat.GetFloat(_resources.AlphaCutoffId)); } else { _commandBuffer.SetGlobalFloat(_resources.AlphaCutoffId, settings.OutlineAlphaCutoff); } _commandBuffer.SetGlobalTexture(_resources.MainTexId, _resources.TmpMaterials[i].mainTexture); _commandBuffer.DrawRenderer(renderer, _resources.RenderMaterial, i, OutlineResources.RenderShaderAlphaTestPassId); } } else { for (var i = 0; i < _resources.TmpMaterials.Count; ++i) { _commandBuffer.DrawRenderer(renderer, _resources.RenderMaterial, i, OutlineResources.RenderShaderDefaultPassId); } } } else { // NOTE: No materials set for renderer means we should still render outline for it. _commandBuffer.DrawRenderer(renderer, _resources.RenderMaterial, 0, OutlineResources.RenderShaderDefaultPassId); } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Blit(RenderTargetIdentifier src, int shaderPass, Material mat, MaterialPropertyBlock props) { // Set source texture as _MainTex to match Blit behavior. _commandBuffer.SetGlobalTexture(_resources.MainTexId, src); // NOTE: SystemInfo.graphicsShaderLevel check is not enough sometimes (esp. on mobiles), so there is SystemInfo.supportsInstancing // check and a flag for forcing DrawMesh. if (SystemInfo.graphicsShaderLevel >= 35 && SystemInfo.supportsInstancing && !_resources.UseFullscreenTriangleMesh) { _commandBuffer.DrawProcedural(Matrix4x4.identity, mat, shaderPass, MeshTopology.Triangles, 3, 1, props); } else { _commandBuffer.DrawMesh(_resources.FullscreenTriangleMesh, Matrix4x4.identity, mat, 0, shaderPass, props); } } private static RenderTargetIdentifier GetBuiltinDepth(RenderingPath renderingPath) { return (renderingPath == RenderingPath.DeferredShading || renderingPath == RenderingPath.DeferredLighting) ? BuiltinRenderTextureType.ResolvedDepth : BuiltinRenderTextureType.Depth; } #endregion } }