using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Rendering.RenderGraphModule.Util; using UnityEngine.Rendering.Universal; namespace Ilumisoft.Rendering { [SupportedOnRenderer(typeof(UniversalRendererData))] [DisallowMultipleRendererFeature("Outline")] public class OutlineRendererFeature : ScriptableRendererFeature { public enum InjectionPoint { BeforeRenderingTransparents = 450, BeforeRenderingPostProcessing = 550 } public enum OutlineMode { DepthOnly, [InspectorName("Depth + Normal")] DepthNormal } public struct Settings { public OutlineMode Mode; public bool ScaleWithResolution; public int ReferenceHeight; } class OutlineRenderPass : ScriptableRenderPass { private readonly int LUMINANCE_CONTRAST = Shader.PropertyToID("_LuminanceContrast"); private readonly int LUMINANCE_POWER = Shader.PropertyToID("_LuminancePower"); private readonly int BACKGROUND_COLOR = Shader.PropertyToID("_BackgroundColor"); private readonly int BACKGROUND_COLOR_OPACITY = Shader.PropertyToID("_BackgroundColorOpacity"); private readonly int DEPTH_EDGE = Shader.PropertyToID("_DepthEdge"); private readonly int NORMAL_EDGE = Shader.PropertyToID("_NormalEdge"); private readonly int IS_FADE_ENABLED = Shader.PropertyToID("_IsFadeEnabled"); private readonly int FADE_START = Shader.PropertyToID("_FadeStart"); private readonly int FADE_END = Shader.PropertyToID("_FadeEnd"); private readonly int REFERENCE_HEIGHT = Shader.PropertyToID("_ReferenceHeight"); private readonly int OUTLINE_THICKNESS_ID = Shader.PropertyToID("_OutlineThickness"); private readonly int OUTLINE_COLOR_ID = Shader.PropertyToID("_OutlineColor"); Material material; Settings settings; public void Setup(Material material, Settings settings) { this.material = material; this.settings = settings; } public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { var volumeComponent = VolumeManager.instance.stack.GetComponent(); // Get settings from volume component int thickness = volumeComponent.thickness.value; Color outlineColor = volumeComponent.outlineColor.value; Color backgroundColor = volumeComponent.backgroundColor.value; bool isDistanceFadeEnabled = volumeComponent.distanceFade.value; float distanceFadeStart = volumeComponent.fadeStart.value; float distanceFadeDistance = volumeComponent.fadeDistance.value; bool backgroundFill = volumeComponent.backgroundFill.value; Vector2 depthEdge = volumeComponent.depthSmoothstep.value; Vector2 normalEdge = volumeComponent.normalSmoothstep.value; float luminancePower = volumeComponent.luminanceDetail.value; float luminanceContrast = volumeComponent.luminanceContrast.value; int referenceHeight = settings.ReferenceHeight; // Cancel if outline are disabled if (thickness == 0) { return; } UniversalResourceData resourceData = frameData.Get(); var sourceHandle = resourceData.activeColorTexture; var descriptor = sourceHandle.GetDescriptor(renderGraph); descriptor.clearBuffer = false; descriptor.name = "_CameraColorOutline"; if (!sourceHandle.IsValid()) { return; } // Get current image height if outlines should not be scaled by a reference resolution if (!settings.ScaleWithResolution) { referenceHeight = descriptor.height; } // Set material properties material.SetColor(OUTLINE_COLOR_ID, outlineColor); material.SetInt(OUTLINE_THICKNESS_ID, thickness); material.SetInt(REFERENCE_HEIGHT, referenceHeight); material.SetInt(IS_FADE_ENABLED, isDistanceFadeEnabled ? 1 : 0); material.SetFloat(FADE_START, distanceFadeStart); material.SetFloat(FADE_END, distanceFadeStart + distanceFadeDistance); material.SetColor(BACKGROUND_COLOR, backgroundColor); material.SetFloat(BACKGROUND_COLOR_OPACITY, backgroundFill ? 1 : 0); material.SetVector(DEPTH_EDGE, depthEdge); material.SetVector(NORMAL_EDGE, normalEdge); material.SetFloat(LUMINANCE_POWER, luminancePower); material.SetFloat(LUMINANCE_CONTRAST, luminanceContrast); // Outline blit TextureHandle targetHandle = renderGraph.CreateTexture(descriptor); var parameters = new RenderGraphUtils.BlitMaterialParameters(sourceHandle, targetHandle, material, (int)settings.Mode); renderGraph.AddBlitPass(parameters, passName: "Draw Outlines"); resourceData.cameraColor = targetHandle; } } [Tooltip("Specifies where in the frame this pass will be injected.")] public InjectionPoint injectionPoint = InjectionPoint.BeforeRenderingTransparents; [Tooltip("Determines which scene data is used for edge detection. 'Depth Only' uses depth differences. 'Depth + Normal' uses surface angle changes as well.")] public OutlineMode mode = OutlineMode.DepthNormal; [Tooltip("When enabled, outline thickness scales proportionally with screen resolution to maintain visual consistency across different screen resolutions.")] public bool scaleWithResolution = true; [Tooltip("The vertical resolution used as a baseline for outline thickness scaling. A value of 1080 means the effect appears 1:1 at 1080p.")] [Min(720)] public int referenceHeight = 1080; public bool showInSceneView = false; Shader shader; Material material; OutlineRenderPass renderPass; public override void Create() { shader = Shader.Find("Hidden/Ilumisoft/PostProcessing/Outline"); if (shader == null) { Debug.LogWarning("Outline Renderer Feature: Could not find outline shader"); return; } material = CoreUtils.CreateEngineMaterial(shader); renderPass = new OutlineRenderPass(); } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (renderPass == null || material == null) { return; } if (renderingData.cameraData.cameraType == CameraType.Preview || renderingData.cameraData.cameraType == CameraType.Reflection || UniversalRenderer.IsOffscreenDepthTexture(ref renderingData.cameraData)) { return; } if (renderingData.cameraData.cameraType != CameraType.Game && !(showInSceneView && renderingData.cameraData.cameraType == CameraType.SceneView)) { return; } var inputRequirements = ScriptableRenderPassInput.Depth; if (mode == OutlineMode.DepthNormal) { inputRequirements |= ScriptableRenderPassInput.Normal; } renderPass.ConfigureInput(inputRequirements); renderPass.requiresIntermediateTexture = true; renderPass.renderPassEvent = injectionPoint switch { InjectionPoint.BeforeRenderingTransparents => RenderPassEvent.BeforeRenderingTransparents, InjectionPoint.BeforeRenderingPostProcessing => RenderPassEvent.BeforeRenderingPostProcessing, _ => RenderPassEvent.BeforeRenderingTransparents, }; renderPass.Setup(material, new Settings() { Mode = mode, ReferenceHeight = referenceHeight, ScaleWithResolution = scaleWithResolution }); renderer.EnqueuePass(renderPass); } protected override void Dispose(bool disposing) { CoreUtils.Destroy(material); renderPass = null; } } }