Powrót do strony głównej

Obwódka Unity 6 URP: Render Graph bez artefaktów

Artykuł opisuje implementację efektu obwódki w Unity 6 URP z Render Graph. Rozwiązuje problemy przerw na rogach poprzez dilation i zapewnia stałą grubość na różnych rozdzielczościach. Pełny kod shaderów i C# ScriptableRendererFeature.

Unity 6: Idealna obwódka w URP bez przerw na rogach
Advertisement 728x90

Obwódka w Unity 6 URP: Render Graph i eliminacja artefaktów na krawędziach

W Unity 6 z URP 17+ efekty post-processingu wymagają całkowitej przebudowy pod Render Graph. Metoda Execute jest przestarzała, fullscreenMesh zastąpiono Blitter. Klasyczne efekty obwódki przez ScriptableRendererFeature psują się z powodu zmian w architekturze renderingu.

Standardowe podejście w dwa przejścia: tworzenie maski sylwetki obiektów białym kolorem, następnie pełnoekranowa analiza krawędzi do rysowania konturu. Rozwiązujemy kluczowe wady:

  • Zerwania na ostrych krawędziach modeli.
  • Zmiana grubości konturu przy zmianie rozdzielczości ekranu.

Shader maski sylwetki

Pierwsze przejście tworzy teksturę z białym wypełnieniem obiektów docelowych. Bez oświetlenia, cieni, tekstur — tylko geometria.

Google AdInline article slot

Utwórz Silhouette.shader:

Shader "Hidden/OutlineSilhouette"
{
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Pass
        {
            ZWrite On
            ZTest LEqual
            Cull Back

            HLSLPROGRAM
            #pragma vertex Vert
            #pragma fragment Frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            struct Attributes {
                float4 positionOS : POSITION;
            };

            struct Varyings {
                float4 positionCS : SV_POSITION;
            };

            Varyings Vert(Attributes input) {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                return output;
            }

            half4 Frag(Varyings input) : SV_Target {
                return half4(1, 1, 1, 1);
            }
            ENDHLSL
        }
    }
}

Shader konturu: dylatacja i niezmienność względem skali

Drugie przejście analizuje maskę. Zwykła kontrola 4 sąsiadów rwie kontur na krawędziach. Stosujemy dylatację: sprawdzamy 8 sąsiadów (4 główne + 4 diagonalne), rozszerzamy maskę, odejmujemy oryginał.

Grubość powiązana z rozdzielczością referencyjną (1080p). Skalowana przez scaleFactor = _ScreenParams.y / 1080.

Google AdInline article slot

HardOutline.shader:

Shader "Custom/HardOutline"
{
    Properties
    {
        _OutlineColor ("Outline Color", Color) = (1, 0, 0, 1)
        _Thickness ("Outline Thickness", Range(1, 10)) = 1.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" "RenderPipeline"="UniversalPipeline" }
        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ZWrite Off ZTest Always Cull Off

            HLSLPROGRAM
            #pragma vertex Vert 
            #pragma fragment Frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

            TEXTURE2D_X(_OutlineRenderTexture);
            SAMPLER(sampler_OutlineRenderTexture);

            float4 _OutlineColor;
            float _Thickness;

            half4 Frag(Varyings input) : SV_Target {
                float2 uv = input.texcoord;

                half centerAlpha = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv).a;

                float2 texelSize = float2(1.0 / _ScreenParams.x, 1.0 / _ScreenParams.y);
                float referenceHeight = 1080.0;
                float scaleFactor = _ScreenParams.y / referenceHeight;
                float scaledThickness = _Thickness * scaleFactor;
                float2 offset = texelSize * scaledThickness;

                half up = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(0, offset.y)).a;
                half down = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(0, -offset.y)).a;
                half left = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(-offset.x, 0)).a;
                half right = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(offset.x, 0)).a;

                half topLeft = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(-offset.x, offset.y)).a;
                half topRight = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(offset.x, offset.y)).a;
                half bottomLeft = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(-offset.x, -offset.y)).a;
                half bottomRight = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv + float2(offset.x, -offset.y)).a;

                half maxNeighbors = max(max(up, down), max(left, right));
                half maxDiagonals = max(max(topLeft, topRight), max(bottomLeft, bottomRight));
                half dilated = max(centerAlpha, max(maxNeighbors, maxDiagonals));

                half edge = saturate(dilated - centerAlpha);

                return half4(_OutlineColor.rgb, edge * _OutlineColor.a);
            }
            ENDHLSL
        }
    }
}

Integracja przez Render Graph: implementacja w C#

OutlineFeature korzysta z RecordRenderGraph. TextureDesc zastępuje GetTemporaryRT, Blitter.BlitTexture — pełnoekranowy rendering.

Kluczowe zmiany:

Google AdInline article slot
  • CreateTexture(texDesc) dla _OutlineRenderTexture.
  • AddRasterRenderPass<PassData> z SetRenderAttachment.
  • CreateRendererList do filtrowania po LayerMask.
  • SetGlobalTextureAfterPass do przekazywania do shadera.

Pełny skrypt OutlineFeature.cs:

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Experimental.Rendering;

public class OutlineFeature : ScriptableRendererFeature
{
    [Serializable]
    public class OutlineSettings
    {
        public LayerMask LayerMask;
        public Material SilhouetteMaterial;
        public Material OutlineMaterial;
    }

    public OutlineSettings settings = new OutlineSettings();

    class RenderSilhouettePass : ScriptableRenderPass
    {
        private Material silhouetteMaterial;
        private FilteringSettings filteringSettings;
        private static readonly int SilhouetteTextureID = Shader.PropertyToID("_OutlineRenderTexture");

        public RenderSilhouettePass(LayerMask layerMask, Material mat)
        {
            silhouetteMaterial = mat;
            filteringSettings = new FilteringSettings(RenderQueueRange.opaque, layerMask);
        }

        private class PassData
        {
            public RendererListHandle rendererList;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            if (silhouetteMaterial == null) return;

            UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
            UniversalRenderingData renderingData = frameData.Get<UniversalRenderingData>();
            UniversalLightData lightData = frameData.Get<UniversalLightData>();

            TextureDesc texDesc = new TextureDesc(cameraData.cameraTargetDescriptor.width, cameraData.cameraTargetDescriptor.height)
            {
                colorFormat = GraphicsFormat.R8G8B8A8_UNorm,
                clearBuffer = true,
                clearColor = Color.clear,
                name = "_OutlineRenderTexture"
            };
            TextureHandle silhouetteTexture = renderGraph.CreateTexture(texDesc);

            using (var builder = renderGraph.AddRasterRenderPass<PassData>("Render Silhouette", out var passData))
            {
                var sortingCriteria = cameraData.defaultOpaqueSortFlags;
                var drawingSettings = CreateDrawingSettings(new ShaderTagId("UniversalForward"), renderingData, cameraData, lightData, sortingCriteria);
                drawingSettings.overrideMaterial = silhouetteMaterial;

                RendererListParams listParams = new RendererListParams(renderingData.cullResults, drawingSettings, filteringSettings);
                passData.rendererList = renderGraph.CreateRendererList(listParams);
                builder.UseRendererList(passData.rendererList);

                builder.SetRenderAttachment(silhouetteTexture, 0, AccessFlags.Write);
                builder.AllowPassCulling(false);

                builder.SetGlobalTextureAfterPass(silhouetteTexture, SilhouetteTextureID);

                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    context.cmd.DrawRendererList(data.rendererList);
                });
            }
        }
    }

    class DrawOutlinePass : ScriptableRenderPass
    {
        private Material outlineMaterial;

        public DrawOutlinePass(Material mat)
        {
            outlineMaterial = mat;
            requiresIntermediateTexture = true; 
        }

        private class PassData
        {
            public Material material;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            if (outlineMaterial == null) return;

            UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
            TextureHandle activeColorTexture = resourceData.activeColorTexture;

            using (var builder = renderGraph.AddRasterRenderPass<PassData>("Draw Outline", out var passData))
            {
                passData.material = outlineMaterial;

                builder.SetRenderAttachment(activeColorTexture, 0);
                builder.AllowPassCulling(false);

                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    Blitter.BlitTexture(context.cmd, new Vector2(1, 1), data.material, 0);
                });
            }
        }
    }

    private RenderSilhouettePass silhouettePass;
    private DrawOutlinePass outlinePass;

    public override void Create()
    {
        silhouettePass = new RenderSilhouettePass(settings.LayerMask, settings.SilhouetteMaterial)
        {
            renderPassEvent = RenderPassEvent.AfterRenderingOpaques
        };

        outlinePass = new DrawOutlinePass(settings.OutlineMaterial)
        {
            renderPassEvent = RenderPassEvent.AfterRenderingSkybox
        };
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (settings.SilhouetteMaterial != null && settings.OutlineMaterial != null)
        {
            renderer.EnqueuePass(silhouettePass);
            renderer.EnqueuePass(outlinePass);
        }
    }
}

Konfiguracja w edytorze

  • Dodaj OutlineFeature do URP Renderer Data.
  • Przypisz materiały SilhouetteMaterial i OutlineMaterial.
  • Wskaż LayerMask dla obiektów z obwódką.
  • _Thickness i _OutlineColor konfiguruj w inspektorze HardOutline.

Co ważne

  • Dylatacja rozwiązuje zerwania krawędzi: 8 sąsiadów zamiast 4 zapewnia ciągłość na ostrych krawędziach.
  • Niezmienność względem skali: Grubość konturu stała dzięki powiązaniu z 1080p.
  • Optymalizacja Render Graph: Automatyczne zarządzanie pamięcią, TextureDesc zamiast uchwytów RT.
  • Wydajność: Dwa przejścia bez rozmycia, nadaje się na mobile/4K.
  • Kompatybilność: URP 17+, tylko Unity 6.

— Editorial Team

Advertisement 728x90

Czytaj dalej