Создание 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.

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



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


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

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

OutlinePass и OutlinePassImpl
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 отлично расширяется, что позволяет создавать интересные эффекты очень просто.

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

Similar posts

Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 7

    0

    А как там дела обстоят с нормальным outline'ом, ну, который блюром делается?

      0
      Так же как и в простом варианте. Все дополнительные pass также добавляются на камеру. Grab texture нет в LWRP, но там сделали аналог Opaque texture(нужно включать в настройках рендеринга) и она доступна в pass после opaque pass.
      0
      Как бы, это тот же подход что и в стандартном «Toon/Basic Outline» и «Toon/Lighted Outline» (со времен появления unity), только там ни какие фильтры не требуются. Тут же дополнительный проход на отрисовку сетки.
        0
        Так и есть. Я просто показал как это сделать в рамках LWRP.
        0
        Что то последняя картинка c 4 кубами на outline вообще не похожа, почему нельзя сделать отступ в пиксельном пространстве экрана, рассчитав нормализованное направление проекции вектора нормали?
          0
          Конечно можно, но задача статьи не показать как делать сам effect, а как сделать его в рамках single pass LWRP. Не запрещается заменить vertex шейдер в outline pass на любой другой найденный на просторах интернета.
          0
          А в этой реализации обязательно стенсил нужен?
          или можно обойтись например очередью или ztesтом?

          Only users with full accounts can post comments. Log in, please.