Retour à l'accueil

Contour Unity 6 URP : Render Graph sans artefacts

L'article décrit l'implémentation de l'effet de contour dans Unity 6 URP avec Render Graph. Résout les problèmes de cassures aux coins par dilatation et assure une épaisseur constante à différentes résolutions. Code shader et ScriptableRendererFeature C# complet.

Unity 6 : Contour parfait en URP sans cassures aux coins
Advertisement 728x90

# Contours Unity 6 URP : Render Graph et correction des artefacts aux coins

Dans Unity 6 avec URP 17+, les post-effets nécessitent une refonte complète pour le Render Graph. La méthode Execute est dépréciée, fullscreenMesh est remplacée par Blitter. Les effets de contour traditionnels via ScriptableRendererFeature se cassent à cause des changements d'architecture de rendu.

L'approche standard en deux passes : créer un masque de silhouette des objets en blanc, puis détection de contours plein écran pour dessiner le contour. Nous corrigeons les problèmes clés :

  • Les trous aux coins nets des modèles.
  • L'épaisseur du contour qui varie avec la résolution d'écran.

Shader de masque de silhouette

La première passe génère une texture avec un remplissage blanc pour les objets cibles. Pas d'éclairage, d'ombres ou de textures — juste de la géométrie.

Google AdInline article slot

Créez 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 de contour : Dilatation et indépendance de résolution

La seconde passe analyse le masque. Vérifier seulement 4 voisins cause des ruptures aux coins. Nous utilisons la dilatation : échantillonner 8 voisins (4 cardinaux + 4 diagonaux), étendre le masque et soustraire l'original.

L'épaisseur est liée à une résolution de référence (1080p). Elle s'adapte via 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
        }
    }
}

Intégration Render Graph : Implémentation C#

OutlineFeature utilise RecordRenderGraph. TextureDesc remplace GetTemporaryRT, Blitter.BlitTexture gère le rendu plein écran.

Changements clés :

Google AdInline article slot
  • CreateTexture(texDesc) pour _OutlineRenderTexture.
  • AddRasterRenderPass<PassData> avec SetRenderAttachment.
  • CreateRendererList pour filtrage LayerMask.
  • SetGlobalTextureAfterPass pour passer au shader.

Script complet 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);
        }
    }
}

Configuration dans l'éditeur

  • Ajoutez OutlineFeature à vos données de rendu URP.
  • Assignez SilhouetteMaterial et OutlineMaterial.
  • Définissez LayerMask pour les objets à contourner.
  • Ajustez _Thickness et _OutlineColor dans l'inspecteur du matériau HardOutline.

Points clés

  • Dilatation corrige les trous aux coins : 8 voisins au lieu de 4 assurent des bords lisses aux coins nets.
  • Indépendance de résolution : Épaisseur de contour constante via mise à l'échelle sur référence 1080p.
  • Optimisation Render Graph : Gestion automatique de la mémoire avec TextureDesc au lieu des RT temporaires.
  • Performances : Deux passes, sans flou — idéal pour mobile et 4K.
  • Compatibilité : URP 17+, Unity 6 uniquement.

— Editorial Team

Advertisement 728x90

Lire ensuite