Я поведаю о том, как создать простой outline effect на новом Lightweight Render Pipeline(LWRP) в Unity. Для этого нужна версия Unity 2018.3 и выше, а так же LWRP версии 4.0.0 и выше.
Классический outline состоит из двух-проходного шейдера (two pass shader), но LWRP поддерживает только одно-проходные шейдера (single pass shader). Для исправления этого недостатка в LWRP появилась возможность добавлять пользовательские pass в определенные этапы рендеринга, используя интерфейсы:
IAfterDepthPrePass IAfterOpaquePass IAfterOpaquePostProcess IAfterSkyboxPass IAfterTransparentPass IAfterRender
Подготовка
Нам потребуется два шейдера.
Первым я буду использовать Unlit Color. Вместо него можно использовать другой, главное добавить в шейдер конструкцию Stencil.
Shader "Unlit/SimpleColor" { SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { Tags { "LightMode" = "LightweightForward" } Stencil { Ref 2 Comp always Pass replace } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.lightweight/ShaderLibrary/Core.hlsl" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; v2f vert (appdata v) { v2f o; o.vertex = TransformObjectToHClip(v.vertex.xyz); return o; } half4 frag (v2f i) : SV_Target { return half4(0.5h, 0.0h, 0.0h, 1.0h); } ENDHLSL } } }
Второй — непосредственно простейший outline шейдер.
Shader "Unlit/SimpleOutline" { SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { Stencil { Ref 2 Comp notequal Pass keep } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.lightweight/ShaderLibrary/Core.hlsl" struct appdata { float4 vertex : POSITION; }; struct v2f { float4 vertex : SV_POSITION; }; half4 _OutlineColor; v2f vert (appdata v) { v2f o; v.vertex.xyz += 0.2 * normalize(v.vertex.xyz); o.vertex = TransformObjectToHClip(v.vertex.xyz); return o; } half4 frag (v2f i) : SV_Target { return _OutlineColor; } ENDHLSL } } }
Пользовательский Pass
Написание пользовательского pass начинается с создания обычного MonoBehaviour и реализации в нем одного из интерфейсов, указанных выше. Используем IAfterOpaquePass, так как outline будет применяться только к оpaque объектам.
public class OutlinePass : MonoBehaviour, IAfterOpaquePass { public ScriptableRenderPass GetPassToEnqueue(RenderTextureDescriptor baseDescriptor, RenderTargetHandle colorAttachmentHandle, RenderTargetHandle depthAttachmentHandle) { //... } }
Этот скрипт должен быть добавлен на камеру. И через него мы будем организовывать взаимодействие нашего прохода с игровой логикой, но об этом чуть позже.
Теперь приступим к написанию самого прохода. Для этого создадим класс, наследуемый от ScriptableRenderPass
public class OutlinePassImpl : ScriptableRenderPass { public OutlinePassImpl() { //... } public override void Execute(ScriptableRenderer renderer, ScriptableRenderContext context, ref RenderingData renderingData) { //... } }
В конструкторе мы зарегистрируем имя прохода, создадим материал и настройки для фильтрации видимых объектов после кулинга. В фильтре установим только opaque объекты, так как свой проход добавим после Opaque pass.
Функция Execute — это функция рендеринга для прохода. В ней мы создаём настройки для рендерига, устанавливаем материал, созданный в конструкторе, и рендерим все видимые объекты, удовлетворяющие созданному фильтру.
public class OutlinePassImpl : ScriptableRenderPass { private Material outlineMaterial; private FilterRenderersSettings m_OutlineFilterSettings; private int OutlineColorId; public OutlinePassImpl(Color outlineColor) { // Должно совпадать с тегом прохода шейдера, висящем на объекте, как в шейдере // SimpleColor RegisterShaderPassName("LightweightForward"); // Соответствует имени outline shader, указанному выше outlineMaterial = CoreUtils.CreateEngineMaterial("Unlit/SimpleOutline"); OutlineColorId = Shader.PropertyToID("_OutlineColor"); outlineMaterial.SetColor(OutlineColorId, outlineColor); m_OutlineFilterSettings = new FilterRenderersSettings(true) { renderQueueRange = RenderQueueRange.opaque, }; } public override void Execute(ScriptableRenderer renderer, ScriptableRenderContext context, ref RenderingData renderingData) { Camera camera = renderingData.cameraData.camera; SortFlags sortFlags = renderingData.cameraData.defaultOpaqueSortFlags; // Создaём настройки для рендерига для текущей камеры DrawRendererSettings drawSettings = CreateDrawRendererSettings(camera, sortFlags, RendererConfiguration.None, renderingData.supportsDynamicBatching); drawSettings.SetOverrideMaterial(outlineMaterial, 0); context.DrawRenderers(renderingData.cullResults.visibleRenderers, ref drawSettings, m_OutlineFilterSettings); } }
Теперь дополним класс OutlinePass. Тут все очень просто создаем экземпляр класса OutlinePassImpl и через ссылку можно будут взаимодействовать с пользовательским pass в режиме runtime. Например для изменения цвета outline.
public class OutlinePass : MonoBehaviour, IAfterOpaquePass { public Color OutlineColor; private OutlinePassImpl outlinePass; public ScriptableRenderPass GetPassToEnqueue(RenderTextureDescriptor baseDescriptor, RenderTargetHandle colorAttachmentHandle, RenderTargetHandle depthAttachmentHandle) { return outlinePass ?? (outlinePass = new OutlinePassImpl(OutlineColor)); } }
Теперь настроим сцену для теста.
- Создадим материал из шейдера SimpleColor
- Создадим куб и навесим на него материал
- На камеру добавим OutlinePass скрипт и установим цвет
- И нажимаем плей
Outline будет виден только в Game View.
Вот такой результат должен получиться.

Бонус: подсветка типа друг-враг
Используя настройку для фильтрации видимых объектов, можно указать слой или рендерный слой для того, чтобы применить этот проход для конкретного объекта или группы объектов и связать её с логикой игры.
Изменим наш pass так, что все объекты со слоем «Friend» будут иметь зеленый outline, а со слоем «Enemy» красный.
public class OutlinePass : MonoBehaviour, IAfterOpaquePass { [System.Serializable] public class OutlineData { public Color Color; public LayerMask Layer; } public List<OutlineData> outlineDatas = new List<OutlineData>(); private OutlinePassImpl outlinePass; public ScriptableRenderPass GetPassToEnqueue(RenderTextureDescriptor baseDescriptor, RenderTargetHandle colorAttachmentHandle, RenderTargetHandle depthAttachmentHandle) { return outlinePass ?? (outlinePass = new OutlinePassImpl(outlineDatas)); } } public class OutlinePassImpl : ScriptableRenderPass { private Material[] outlineMaterial; private FilterRenderersSettings[] m_OutlineFilterSettings; public OutlinePassImpl(List<OutlinePass.OutlineData> outlineDatas) { RegisterShaderPassName("LightweightForward"); outlineMaterial = new Material[outlineDatas.Count]; m_OutlineFilterSettings = new FilterRenderersSettings[outlineDatas.Count]; Shader outlineShader = Shader.Find("Unlit/SimpleOutline"); int OutlineColorId = Shader.PropertyToID("_OutlineColor"); for (int i = 0; i < outlineDatas.Count; i++) { OutlinePass.OutlineData outline = outlineDatas[i]; Material material = CoreUtils.CreateEngineMaterial(outlineShader); material.SetColor(OutlineColorId, outline.Color); outlineMaterial[i] = material; m_OutlineFilterSettings[i] = new FilterRenderersSettings(true) { renderQueueRange = RenderQueueRange.opaque, layerMask = outline.Layer }; } } public override void Execute(ScriptableRenderer renderer, ScriptableRenderContext context, ref RenderingData renderingData) { Camera camera = renderingData.cameraData.camera; SortFlags sortFlags = renderingData.cameraData.defaultOpaqueSortFlags; DrawRendererSettings drawSettings = CreateDrawRendererSettings(camera, sortFlags, RendererConfiguration.None, renderingData.supportsDynamicBatching); for (int i = 0; i < outlineMaterial.Length; i++) { drawSettings.SetOverrideMaterial(outlineMaterial[i], 0); context.DrawRenderers(renderingData.cullResults.visibleRenderers, ref drawSettings, m_OutlineFilterSettings[i]); } } }
На сцене добавим слои «Friend» и «Enemy», продублируем куб несколько раз, назначим им слои на «Friend» или «Enemy», настроим Outline Pass и запустим.

И вот что получим.

Заключение
Новый рендериг в Unity отлично расширяется, что позволяет создавать интересные эффекты очень просто.
Надеюсь статья оказалась полезной для прочтения. Если у кого возникнут вопросы — до встречи в комментах.

