[Из песочницы] Создание outline на LWRP в Unity

Здравствуйте.

Я поведаю о том, как создать простой 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.

Unlit Color
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 шейдер.

Simple 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 — это функция рендеринга для прохода. В ней мы создаём настройки для рендерига, устанавливаем материал, созданный в конструкторе, и рендерим все видимые объекты, удовлетворяющие созданному фильтру.

OutlinePassImpl который получился у меня
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.

OutlinePass который получился у меня
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));
    }   
}



Теперь настроим сцену для теста.

  1. Создадим материал из шейдера SimpleColor
  2. Создадим куб и навесим на него материал
  3. На камеру добавим OutlinePass скрипт и установим цвет
  4. И нажимаем плей


Outline будет виден только в Game View.

Вот такой результат должен получиться.

tr6tcbfjlamo3ngawyfwcihffre.jpeg

Бонус: подсветка типа друг-враг


Используя настройку для фильтрации видимых объектов, можно указать слой или рендерный слой для того, чтобы применить этот проход для конкретного объекта или группы объектов и связать её с логикой игры.

Изменим наш pass так, что все объекты со слоем «Friend» будут иметь зеленый outline, а со слоем «Enemy» красный.

OutlinePass и OutlinePassImpl
public class OutlinePass : MonoBehaviour, IAfterOpaquePass
{
    [System.Serializable]
    public class OutlineData
    {
        public Color Color;
        public LayerMask Layer;
    }

    public List outlineDatas = new List();

    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 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 и запустим.

nopoqtnrzswkrg5c9vhut9ihiiw.jpeg

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

xrfclebp-d1obdcslg31rdvp54y.jpeg

Заключение


Новый рендериг в Unity отлично расширяется, что позволяет создавать интересные эффекты очень просто.

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

© Habrahabr.ru