Volver al inicio

Contorno Unity 6 URP: Render Graph sin artefactos

El artículo describe la implementación del efecto de contorno en Unity 6 URP con Render Graph. Resuelve problemas de rupturas en esquinas mediante dilatación y asegura un grosor constante en diferentes resoluciones. Código completo de shader y ScriptableRendererFeature en C#.

Unity 6: Contorno perfecto en URP sin rupturas en esquinas
Advertisement 728x90

# Unity 6 URP: Contornos con Render Graph y corrección de artefactos en esquinas

En Unity 6 con URP 17+, los post-efectos necesitan una renovación completa para Render Graph. El método Execute está obsoleto, fullscreenMesh se reemplaza por Blitter. Los efectos de contorno tradicionales vía ScriptableRendererFeature fallan por los cambios en la arquitectura de renderizado.

El enfoque estándar de dos pasadas: crear una máscara de silueta de los objetos en blanco, luego detección de bordes en pantalla completa para dibujar el contorno. Solucionamos problemas clave:

  • Huecos en las esquinas afiladas del modelo.
  • Grosor del contorno que varía con la resolución de pantalla.

Shader de máscara de silueta

La primera pasada genera una textura con relleno blanco para los objetos objetivo. Sin iluminación, sombras ni texturas: solo geometría.

Google AdInline article slot

Crea 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 contorno: Dilatación e independencia de resolución

La segunda pasada analiza la máscara. Verificar solo 4 vecinos causa rupturas en esquinas. Usamos dilatación: muestreamos 8 vecinos (4 cardinales + 4 diagonales), expandimos la máscara y restamos la original.

El grosor se ata a una resolución de referencia (1080p). Escala vía 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
        }
    }
}

Integración con Render Graph: Implementación en C#

OutlineFeature usa RecordRenderGraph. TextureDesc reemplaza GetTemporaryRT, Blitter.BlitTexture maneja el renderizado en pantalla completa.

Cambios clave:

Google AdInline article slot
  • CreateTexture(texDesc) para _OutlineRenderTexture.
  • AddRasterRenderPass<PassData> con SetRenderAttachment.
  • CreateRendererList para filtrado por LayerMask.
  • SetGlobalTextureAfterPass para pasar al shader.

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

Configuración en el Editor

  • Añade OutlineFeature a tus datos del renderizador URP.
  • Asigna SilhouetteMaterial y OutlineMaterial.
  • Configura LayerMask para los objetos con contorno.
  • Ajusta _Thickness y _OutlineColor en el inspector del material HardOutline.

Lecciones clave

  • Dilatación corrige huecos en esquinas: 8 vecinos en lugar de 4 aseguran bordes suaves en esquinas afiladas.
  • Independencia de resolución: Grosor de contorno consistente mediante escalado de referencia 1080p.
  • Optimización Render Graph: Gestión automática de memoria con TextureDesc sobre manejadores RT.
  • Rendimiento: Dos pasadas, sin desenfoque — ideal para móviles y 4K.
  • Compatibilidad: URP 17+, solo Unity 6.

— Editorial Team

Advertisement 728x90

Leer después