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.
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.
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:
CreateTexture(texDesc)dla_OutlineRenderTexture.AddRasterRenderPass<PassData>zSetRenderAttachment.CreateRendererListdo filtrowania poLayerMask.SetGlobalTextureAfterPassdo 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
OutlineFeaturedo URP Renderer Data. - Przypisz materiały
SilhouetteMaterialiOutlineMaterial. - Wskaż
LayerMaskdla obiektów z obwódką. _Thicknessi_OutlineColorkonfiguruj w inspektorzeHardOutline.
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ą,
TextureDesczamiast uchwytów RT. - Wydajność: Dwa przejścia bez rozmycia, nadaje się na mobile/4K.
- Kompatybilność: URP 17+, tylko Unity 6.
— Editorial Team
Brak komentarzy.