在Unity Universal Render管道中创建轮廓效果

在通用渲染管道中,通过创建自己的RendererFeature,可以轻松扩展渲染功能。在渲染管线中添加新的通道可以创建各种效果。在本文中,我们将使用ScriptableRendererFeature和ScriptableRenderPass创建对象的Outline效果,并考虑其实现的一些功能。

轮廓效果


关于渲染管道的简介或几句话


可脚本化的渲染管线允许您通过C#中的脚本控制图形的渲染,并控制对象,灯光,阴影等的处理顺序。Universal Render Pipeline是由Unity开发的现成的可编写脚本的Render Pipeline,旨在替代旧的内置RP。

可以通过创建和添加自己的图形传递(ScriptableRendererFeature和ScriptableRenderPass)来扩展Universal RP的功能。这将是当前文章。这将对那些打算转用Universal RP的人很有用,也许有助于更好地了解Universal RP中现有ScriptableRenderPass'ov的工作。

本文是在Unity 2019.3和Universal RP 7.1.8中编写的。

行动计划


我们将了解ScriptableRendererFeature和ScriptableRenderPass如何在创建不透明对象的笔触效果的示例中工作。

为此,请创建一个ScriptableRendererFeature来执行以下操作:

  • 绘制指定对象
  • 模糊绘制的对象
  • 从先前通过的图像中获取物体的轮廓

源框架-

我们必须达到的结果顺序是:


在工作过程中,我们将创建一个着色器,在该着色器的全局属性中,将保存第一遍和第二遍的结果。最后一遍将在屏幕上显示着色器本身的结果。

全局属性
, , Properties. — .

— . .

创建OutlineFeature


ScriptableRendererFeature用于将自定义渲染段落(ScriptableRenderPass)添加到Universal RP。创建从ScriptableRenderFeature继承的OutlineFeature类,并实现其方法。

using UnityEngine;
using UnityEngine.Rendering.Universal;

public class OutlineFeature : ScriptableRendererFeature
{
    public override void Create()
    { }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    { }
}

Create()方法用于创建和配置过程。然后将用于将创建的通行证注入渲染队列的AddRenderPasses()方法。

参数AddRenderPasses()
ScriptableRenderer — Universal RP. Universal RP Forward Rendering.

RenderingData — , , .

现在开始创建渲染过程,在实现每个过程之后,我们将返回当前类。

渲染对象通过


此段落的任务是从特定图层绘制对象,并替换着色器的全局纹理属性中的材质。这将是Universal RP中可用的RenderObjectsPass通道的简化版本,只有执行渲染的目标(RenderTarget)不同。

创建一个从ScriptableRenderPass继承的MyRenderObjectsPass类。我们实现Execute()方法,其中将包含段落的所有逻辑,并重新定义Configure()方法。

using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class MyRenderObjectsPass : ScriptableRenderPass
{
    {
        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        { }
        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        { }
    }
}

Configure()方法用于指定渲染和创建临时纹理的目的。默认情况下,目标是当前摄像机的目标,并且在完成传递之后,默认情况下将再次指示该目标。在基本逻辑运行之前调用此方法。

渲染目标替换


为新的渲染目标声明一个RenderTargetHandle。使用它,创建一个临时纹理并将其指定为目标。RenderTargetHandle包含使用的临时RenderTexture的标识符。它还允许您获取RenderTargetIdentifier,该标识符用于标识可以设置的渲染目标,例如,可以设置为RenderTexture,Texture对象,临时RenderTexture或内置(渲染帧时由相机使用)。

一个RenderTargetHandle对象将在OutlineFeature中创建,并在创建时传递给我们的通道。

private RenderTargetHandle _destination;

public MyRenderObjectsPass(RenderTargetHandle destination)
{
    _destination = destination;
}

public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{  
    cmd.GetTemporaryRT(_destination.id, cameraTextureDescriptor);
}

GetTemporaryRT()方法使用指定的参数创建一个临时的RenderTexture,并将其设置为具有指定名称的全局着色器属性(名称将在功能部件中设置)。

删除RenderTexture
ReleaseTemporaryRT() RenderTexture. Execute() FrameCleanup.
, RenderTexture, , .

要创建临时的RenderTexture,我们使用当前摄像机的描述符,其中包含有关摄像机目标的大小,格式和其他参数的信息。

public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{        
    cmd.GetTemporaryRT(_destination.id, cameraTextureDescriptor);
    ConfigureTarget(_destination.Identifier());
    ConfigureClear(ClearFlag.All, Color.clear);
}

目标的用途及其清除仅应在使用ConfigureTarget()和ClearTarget()方法的Configure()中发生。

渲染


我们将不考虑详细渲染,因为 这可以使我们远离主要话题。对于渲染,我们将使用ScriptableRenderContext.DrawRenderers()方法。创建仅用于渲染来自指定图层的不透明对象的设置。图层蒙版将传递给构造函数。

...
private List<ShaderTagId> _shaderTagIdList = new List<ShaderTagId>() { new ShaderTagId("UniversalForward") };
private FilteringSettings _filteringSettings;
private RenderStateBlock _renderStateBlock;
...
public MyRenderObjectsPass(RenderTargetHandle destination, int layerMask)
{
    _destination = destination;

    _filteringSettings = new FilteringSettings(RenderQueueRange.opaque, layerMask);
    _renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
}

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    SortingCriteria sortingCriteria = renderingData.cameraData.defaultOpaqueSortFlags;
    DrawingSettings drawingSettings = CreateDrawingSettings(_shaderTagIdList, ref renderingData, sortingCriteria);

    context.DrawRenderers(renderingData.cullResults, ref drawingSettings, ref _filteringSettings, ref _renderStateBlock);
}

关于参数非常简短
CullingResults — ( RenderingData)
FilteringSettings — .
DrawingSettings — .
RenderStateBlock — , (, ..)

关于UniversalRP中完成的RenderObjectsPass
, . . RenderObjectsPass, Universal RP, . RenderFeature - :)

材料更换


由于我们仅需要对象的轮廓,因此我们重新定义了用于渲染的材料。

private Material _overrideMaterial;

public MyRenderObjectsPass(RenderTargetHandle destination, int layerMask,, Material overrideMaterial)
{
...
    _overrideMaterial = overrideMaterial;
...
}

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
...
    DrawingSettings drawingSettings = CreateDrawingSettings(_shaderTagIdList, ref renderingData, sortingCriteria);
    drawingSettings.overrideMaterial = _overrideMaterial;
...
}

渲染着色器


在ShaderGraph中创建一个材质着色器,该着色器将在当前通道中绘制对象时使用。


在OutlineFeature中添加一段


回到OutlieFeature。首先,为我们的段落设置创建一个类。

public class OutlineFeature : ScriptableRendererFeature
{
    [Serializable]
    public class RenderSettings
    {
        public Material OverrideMaterial = null;
        public LayerMask LayerMask = 0;
    }
    ...
}

声明MyRenderPass设置的字段以及我们的遍历用作渲染目标的全局纹理属性的名称。

[SerializeField] private string _renderTextureName;
[SerializeField] private RenderSettings _renderSettings;

创建纹理属性的标识符和MyRenderPass的实例。

private RenderTargetHandle _renderTexture;
private MyRenderObjectsPass _renderPass;

public override void Create()
{
    _renderTexture.Init(_renderTextureName);

    _renderPass = new MyRenderObjectsPass(_renderTexture, _renderSettings.LayerMask, _renderSettings.OverrideMaterial);
}

在AddRendererPass方法中,我们将通行证添加到执行队列中。

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    renderer.EnqueuePass(_renderPass);
}

展望未来
. , .

源场景的传递结果应如下所示:


调试
Frame Debug (Windows-Analysis-FrameDebugger).

模糊通行证


此段落的目的是使在上一步中获得的图像模糊,并将其设置为global shader属性。

为此,我们将使用原始材质上的模糊着色器多次将其复制到一个临时材质上。在这种情况下,可以缩小原始图像的尺寸(创建缩小的副本),这将加快计算速度,并且不会影响结果的质量。

让我们创建从ScriptableRenderPass继承的BlurPass类。

using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class BlurPass : ScriptableRenderPass
{
    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    { }
}

让我们获取源纹理,目标纹理和临时纹理(及其ID)的变量。

private int _tmpBlurRTId1 = Shader.PropertyToID("_TempBlurTexture1");
private int _tmpBlurRTId2 = Shader.PropertyToID("_TempBlurTexture2");

private RenderTargetIdentifier _tmpBlurRT1;
private RenderTargetIdentifier _tmpBlurRT2;

private RenderTargetIdentifier _source;
private RenderTargetHandle _destination;

RenderTexture的所有ID都是通过Shader.PropertyID()设置的。这并不意味着必须在某些地方存在此类着色器属性。

为其余参数添加字段,我们将立即在构造函数中对其进行初始化。

private int _passesCount;
private int _downSample;
private Material _blurMaterial;

public BlurPass(Material blurMaterial, int downSample, int passesCount)
{
    _blurMaterial = blurMaterial;
    _downSample = downSample;
    _passesCount = passesCount;
}

_blurMaterial-具有模糊着色器的材质。
_downSample-减少纹理大小的系数
_passesCount-要应用的模糊遍数。

要创建临时纹理,请创建一个描述符,其中包含有关它的所有必要信息-大小,格式等。高度和尺寸将相对于摄像机描述符缩放。

public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
    var width = Mathf.Max(1, cameraTextureDescriptor.width >> _downSample);
    var height = Mathf.Max(1, cameraTextureDescriptor.height >> _downSample);
    var blurTextureDesc = new RenderTextureDescriptor(width, height, RenderTextureFormat.ARGB32, 0, 0);
我们还将自己创建标识符和临时RenderTexture。
    _tmpBlurRT1 = new RenderTargetIdentifier(_tmpBlurRTId1);
    _tmpBlurRT2 = new RenderTargetIdentifier(_tmpBlurRTId2);

    cmd.GetTemporaryRT(_tmpBlurRTId1, blurTextureDesc, FilterMode.Bilinear);
    cmd.GetTemporaryRT(_tmpBlurRTId2, blurTextureDesc, FilterMode.Bilinear);

我们再次更改渲染目标,因此创建另一个临时纹理并将其指定为目标。

    cmd.GetTemporaryRT(_destination.id, blurTextureDesc, FilterMode.Bilinear);
    ConfigureTarget(_destination.Identifier());
}

模糊


可以使用特殊的ScriptableRenderContext方法执行一些渲染任务,这些方法配置并向其中添加命令。要执行其他命令,您将需要使用CommandBuffer,可以从池中获取。
添加命令并将其发送到上下文后,需要将缓冲区返回到池中。

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    var cmd = CommandBufferPool.Get("BlurPass");
    ...
    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
}

Execute()方法的最终实现如下。

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
    var cmd = CommandBufferPool.Get("BlurPass");

    if (_passesCount > 0)
    {
        cmd.Blit(_source, _tmpBlurRT1, _blurMaterial, 0);
        for (int i = 0; i < _passesCount - 1; i++)
        {
            cmd.Blit(_tmpBlurRT1, _tmpBlurRT2, _blurMaterial, 0);
            var t = _tmpBlurRT1;
            _tmpBlurRT1 = _tmpBlurRT2;
            _tmpBlurRT2 = t;
        }
        cmd.Blit(_tmpBlurRT1, _destination.Identifier());
    }
    else
        cmd.Blit(_source, _destination.Identifier());
    context.ExecuteCommandBuffer(cmd);
    CommandBufferPool.Release(cmd);
}

Blit
Blit() .

着色器


要进行模糊处理,请创建一个简单的着色器,该着色器将考虑像素的最近邻居(五个像素的平均颜色值)来计算像素的颜色。

模糊着色器
Shader "Custom/Blur"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
}

SubShader
{
HLSLINCLUDE

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 positionCS : SV_POSITION;
float2 uv : TEXCOORD0;
};

TEXTURE2D_X(_MainTex);
SAMPLER(sampler_MainTex);
float4 _MainTex_TexelSize;

Varyings Vert(Attributes input)
{
Varyings output;
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
output.uv = input.uv;
return output;
}

half4 Frag(Varyings input) : SV_Target
{
float2 offset = _MainTex_TexelSize.xy;
float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);

half4 color = SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2(-1, 1) * offset);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2( 1, 1) * offset);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2( 1,-1) * offset);
color += SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, input.uv + float2(-1,-1) * offset);

return color/5.0;
}

ENDHLSL

Pass
{
HLSLPROGRAM

#pragma vertex Vert
#pragma fragment Frag

ENDHLSL
}
}
}

为什么不使用ShaderGraph?
ShaderGraph , 13 :)

在OutlineFeature中添加一段


该过程将类似于添加我们的第一遍。首先,创建设置。

    [Serializable]
    public class BlurSettings
    {
        public Material BlurMaterial;
        public int DownSample = 1;
        public int PassesCount = 1;
    }

然后是田野。

[SerializeField] private string _bluredTextureName;
[SerializeField] private BlurSettings _blurSettings;
private RenderTargetHandle _bluredTexture;
private BlurPass _blurPass;

...

public override void Create()
{
    _bluredTexture.Init(_bluredTextureName);

    _blurPass = new BlurPass(_blurSettings.BlurMaterial, _blurSettings.DownSample, _blurSettings.PassesCount);
}

并添加到队列中执行。

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    renderer.EnqueuePass(_renderPass);
    renderer.EnqueuePass(_blurPass);
}

通过结果:


轮廓通行证


带有对象笔触的最终图像将使用着色器获得。并且他的工作结果将显示在屏幕上当前图像的顶部。

下面是一次完整的密码,如下 所有逻辑都在两行中。

public class OutlinePass : ScriptableRenderPass
{
    private string _profilerTag = "Outline";
    private Material _material;

    public OutlinePass(Material material)
    {
        _material = material;
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        var cmd = CommandBufferPool.Get(_profilerTag);

        using (new ProfilingSample(cmd, _profilerTag))
        {
            var mesh = RenderingUtils.fullscreenMesh;
            cmd.DrawMesh(mesh, Matrix4x4.identity, _material, 0, 0);
        }

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }
}

RenderingUtils.fullscreenMesh返回1 x 1网格。

着色器


创建一个着色器以获取轮廓。它应该包含两个全局纹理属性。_OutlineRenderTexture和_OutlineBluredTexture用于指定对象的图像及其模糊版本。

着色器代码
Shader "Custom/Outline"
{
    Properties
    {
        _Color ("Glow Color", Color ) = ( 1, 1, 1, 1)
        _Intensity ("Intensity", Float) = 2
        [Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
        [Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
    }
    
    SubShader
    {       
        HLSLINCLUDE
 
        #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
        
        struct Attributes
        {
            float4 positionOS   : POSITION;
            float2 uv           : TEXCOORD0;
        };
        
        struct Varyings
        {
            half4 positionCS    : SV_POSITION;
            half2 uv            : TEXCOORD0;
        };
 
        TEXTURE2D_X(_OutlineRenderTexture);
        SAMPLER(sampler_OutlineRenderTexture);
 
        TEXTURE2D_X(_OutlineBluredTexture);
        SAMPLER(sampler_OutlineBluredTexture);
 
        half4 _Color;
        half _Intensity;
 
        Varyings Vertex(Attributes input)
        {
            Varyings output;
            output.positionCS = float4(input.positionOS.xy, 0.0, 1.0); 
            output.uv = input.uv;
            if (_ProjectionParams.x < 0.0) 
                output.uv.y = 1.0 - output.uv.y;    
            return output;      
        }
 
        half4 Fragment(Varyings input) : SV_Target
        {
            float2 uv = UnityStereoTransformScreenSpaceTex(input.uv);
            half4 prepassColor = SAMPLE_TEXTURE2D_X(_OutlineRenderTexture, sampler_OutlineRenderTexture, uv);
            half4 bluredColor = SAMPLE_TEXTURE2D_X(_OutlineBluredTexture, sampler_OutlineBluredTexture,uv);
            half4 difColor = max( 0, bluredColor - prepassColor);
            half4 color = difColor* _Color * _Intensity;
            color.a = 1;    
            return color;
        }
        
        ENDHLSL        
     
        Pass
        {
            Blend [_SrcBlend] [_DstBlend]
            ZTest Always    //  ,      
            ZWrite Off      //      
            Cull Off        //    
 
            HLSLPROGRAM
           
            #pragma vertex Vertex
            #pragma fragment Fragment        
 
            ENDHLSL         
        }
    }
}

, . Unity . , _ProjectionParams..

两个先前获得的图像的着色器结果:



在OutlineFeature中添加一段


所有操作均与之前的操作相似。

[SerializeField] private Material _outlineMaterial;
private OutlinePass _outlinePass;

public override void Create()
{
    ...
    _outlinePass = new OutlinePass(_outlineMaterial);
    ....
}

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
    renderer.EnqueuePass(_renderPass);
    renderer.EnqueuePass(_blurPass);
    renderer.EnqueuePass(_outlinePass);
}

RenderPassEvent


它仍然指示何时调用创建的通行证。为此,他们每个人都需要指定renderPassEvent参数。

活动列表
Universal RP, .
BeforeRendering
BeforeRenderingShadows
AfterRenderingShadows
BeforeRenderingPrepasses
AfterRenderingPrePasses
BeforeRenderingOpaques
AfterRenderingOpaques
BeforeRenderingSkybox
AfterRenderingSkybox
BeforeRenderingTransparents
AfterRenderingTransparents
BeforeRenderingPostProcessing
AfterRenderingPostProcessing
AfterRendering

在OutlineFeature中创建适当的字段。

[SerializeField] private RenderPassEvent _renderPassEvent;

我们将在所有已创建的段落中指出这一点。

public override void Create()
{
    ...
    _renderPass.renderPassEvent = _renderPassEvent;
    _blurPass.renderPassEvent = _renderPassEvent;
    _outlinePass.renderPassEvent = _renderPassEvent;
}

客制化


添加轮廓层并将其设置为我们要圈出的对象。

我们将创建并配置所有必需的资产:UniversalRendererPipelineAsset和ForwardRendererData。



结果


我们原始框架的结果将如下所示!



完成时间


现在,即使通过其他对象,该对象的轮廓也将始终可见。为了使我们的效果考虑场景的深度,必须进行一些更改。

RenderObjectsPass


在指定渲染目的时,我们还应该指出当前的深度缓冲区。创建适当的字段和方法。

public class MyRenderObjectsPass : ScriptableRenderPass
{
    ...
    private RenderTargetIdentifier _depth;

    public void SetDepthTexture(RenderTargetIdentifier depth)
    { _depth = depth; }
    ...
}

在Configure()方法中,指定设置渲染目标的深度。

public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
    cmd.GetTemporaryRT(_destination.id, cameraTextureDescriptor);
    ConfigureTarget(_destination.Identifier(), _depth);
    ConfigureClear(ClearFlag.Color, Color.clear);
}

轮廓特征


在OutlineFeature中,我们将传递MyRenderObjectsPass当前场景深度。

public class OutlineFeature : ScriptableRendererFeature
{
    ...
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        var depthTexture = renderer.cameraDepth;
        _renderPass.SetDepthTexture(depthTexture);

        renderer.EnqueuePass(_renderPass);
        renderer.EnqueuePass(_blurPass);
        renderer.EnqueuePass(_outlinePass);
    }
    ...
}

UniversalRenderPipelineAsset


在使用的UniversalRenderPipelineAsset中,选中“ DepthTexture”旁边的框。


结果


结果不包括深度:


基于深度的结果:




ScriptableRendererFeature是用于将段落添加到RP的便捷工具。
在其中,您可以轻松替换RenderObjectsPass,并在其他ScriptableRendererFeature中使用它们。您无需深入研究Universal RP的实施,也无需更改其代码即可添加一些东西。

聚苯乙烯


为了使使用ScriptableRendererFeature和ScriptableRenderPass的通用算法更加清晰,并且使本文不会增加太多,我特意尝试简化密码,甚至损害其通用性和最佳性。

参考文献


源代码是一个以gitlab链接时,
模型和场景是从兰德任务采取:地球深处的比赛。
下面的行程实施被作为例子的基础- YouTube的链接
团结自己的RenderFeature实现示例- 链接到github上

有关创建自己的ScriptableRenderPipeline的一系列课程。阅读RP和着色器的一般逻辑后,指向教程链接将变得清晰

All Articles