Здравствуйте.
Я поведаю о том, как создать простой 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 отлично расширяется, что позволяет создавать интересные эффекты очень просто.
Надеюсь статья оказалась полезной для прочтения. Если у кого возникнут вопросы — до встречи в комментах.
Автор: ArtemSekretov