Если вы недавно обновились до Unity 6 (или URP 17+) и попытались перенести свои старые пост-эффекты, то наверняка столкнулись с красной консолью и предупреждениями об устаревших методах.
Старые туториалы по созданию эффекта обводки (Outline) через ScriptableRendererFeature больше не работают "из коробки". Unity полностью изменила архитектуру рендера, внедрив Render Graph. Старый добрый метод Execute канул в Лету, а fullscreenMesh заменили на Blitter.
В этой статье мы не просто перепишем классический эффект Outline под новые реалии Unity 6. Мы решим две главные проблемы подобных шейдеров, о которых часто умалчивают в гайдах:
Разорванные углы: как сделать так, чтобы на острых краях моделей обводка не прерывалась.
Зависимость от разрешения: как заставить контур выглядеть одинаково и на Full HD мониторе, и на 4K-экране мобильного телефона.

Как это работает?
Мы будем использовать классический подход в два прохода (без эффекта размытия, нам нужен строгий хард-эдж контур):
Маска: Рисуем нужные объекты сплошным белым цветом в невидимую текстуру.
Контур: Полноэкранным проходом анализируем эту текстуру. Если пиксель находится на границе белого цвета и пустоты - рисуем там цветную обводку.
Шаг 1. Шейдер силуэта (Маска)
Сначала создадим простейший шейдер, который зальет наши объекты сплошным цветом в маску. Никакого освещения, теней или текстур - только цвет.
Создайте файл Silhouette.shader:
High-level shader language
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 } } }
Шаг 2. Умный шейдер контура (Решаем проблемы углов и разрешения)
Обычно в туториалах для поиска границ проверяют 4 соседних пикселя (верх, низ, лево, право). Это дает хороший результат на прямых линиях, но на острых внешних углах модели обводка "рвется".

Чтобы это исправить, мы применим технику Dilation (Расширение): будем проверять 8 соседей (включая диагонали) и "выращивать" маску наружу, а затем вычтем из нее оригинальный силуэт.
Вторая проблема - масштабирование. Мы привяжем толщину обводки к эталонному разрешению экрана (например, 1080p), чтобы на 4K-экранах контур не превращался в невидимую ниточку.
Создайте файл HardOutline.shader:
High-level shader language
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 } } }
Шаг 3. C# скрипт и магия Render Graph
Самое интересное. В Unity 6 мы больше не управляем очередью команд напрямую. Вместо этого мы создаем граф рендеринга (RecordRenderGraph), а движок сам решает, как оптимально выделить память.
Обратите внимание на TextureDesc (заменил старый GetTemporaryRT) и Blitter.BlitTexture (пришел на смену устаревшему полноэкранному мешу).
Создайте скрипт OutlineFeature.cs:
C#
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); } } }
Настройка в Unity
Создайте два материала:
Mat_Silhouette (шейдер:
Hidden/OutlineSilhouette)Mat_HardOutline (шейдер:
Custom/HardOutline)
Найдите ваш файл
UniversalRendererData(лежит в папке настроек URP).В самом низу нажмите Add Renderer Feature и выберите Outline Feature.
В настройках фичи:
Укажите Layer Mask (слой объектов, которые нужно обводить). Не забудьте назначить этот же слой вашим объектам на сцене!
Перетащите в слоты созданные материалы.
Убедитесь, что на Main Camera включена галочка Post Processing.

Готово! Теперь у вас есть полностью рабочий, оптимизированный под современный пайплайн эффект обводки с идеальными углами, который не сломается при изменении разрешения экрана.
Полный проект и исходники доступны в моем репозитории на GitHub👉
Надеюсь, эта статья сэкономит вам пару часов дебаггинга в новом Unity 6. Пишите в комментариях, как прошел ваш переезд на Render Graph!
