Criando um efeito de estrutura de tópicos no pipeline de renderização universal do Unity

No Universal Render Pipeline, criando sua própria RendererFeature, você pode expandir facilmente os recursos de renderização. Adicionar novas passagens ao pipeline de renderização permite criar vários efeitos. Neste artigo, usando ScriptableRendererFeature e ScriptableRenderPass, criaremos o efeito Estrutura de tópicos do objeto e consideraremos alguns recursos de sua implementação.

Efeito de estrutura de tópicos


Introdução ou algumas palavras sobre o Render Pipeline


O Pipeline de renderização com script permite controlar a renderização de gráficos por meio de scripts em C # e controlar a ordem de processamento de objetos, luzes, sombras e muito mais. O Universal Render Pipeline é um Pipeline de renderização programável por script, desenvolvido pela Unity e projetado para substituir o antigo RP interno.

Os recursos do Universal RP podem ser expandidos criando e adicionando seus próprios passes de desenho (ScriptableRendererFeature e ScriptableRenderPass). Este será o artigo atual. Será útil para quem vai mudar para o Universal RP e, talvez, ajude a entender melhor o trabalho do ScriptableRenderPass'ov existente no Universal RP.

Este artigo foi escrito no Unity 2019.3 e no Universal RP 7.1.8.

Plano de ação


Vamos entender como ScriptableRendererFeature e ScriptableRenderPass funcionam no exemplo da criação do efeito de traçado de objetos opacos.

Para fazer isso, crie um ScriptableRendererFeature que execute as seguintes ações:

  • desenhando objetos especificados
  • desfocando objetos desenhados
  • obtenção de contornos de objetos a partir de imagens obtidas em passagens anteriores

Quadro de origem -

E a sequência de resultados que devemos alcançar:


No decorrer do trabalho, criaremos um sombreador, nas propriedades globais das quais os resultados da primeira e segunda passagens serão salvos. A última passagem exibirá o resultado do shader em si na tela.

Propriedades globais
, , Properties. — .

— . .

Criar estrutura de tópicos


ScriptableRendererFeature é usado para adicionar passagens de renderização personalizadas (ScriptableRenderPass) ao Universal RP. Crie uma classe OutlineFeature que herda de ScriptableRenderFeature e implemente seus métodos.

using UnityEngine;
using UnityEngine.Rendering.Universal;

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

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

O método Create () é usado para criar e configurar passes. E o método AddRenderPasses () para injetar as passagens criadas na fila de renderização.

argumentos AddRenderPasses ()
ScriptableRenderer — Universal RP. Universal RP Forward Rendering.

RenderingData — , , .

Agora vamos começar a criar as passagens de renderização e retornaremos à classe atual após a implementação de cada uma delas.

Objetos de renderização passam


A tarefa desta passagem é desenhar objetos de uma camada específica com a substituição do material na propriedade de textura global do shader. Essa será uma versão simplificada da passagem RenderObjectsPass disponível no Universal RP, com a única diferença no destino (RenderTarget) em que a renderização será executada.

Crie uma classe MyRenderObjectsPass herdada de ScriptableRenderPass. Implementamos o método Execute (), que conterá toda a lógica da passagem, além de redefinir o método 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)
        { }
    }
}

O método Configure () é usado para especificar a finalidade de renderizar e criar texturas temporárias. Por padrão, o alvo é o alvo da câmera atual e, após concluir o passe, será indicado por padrão novamente. Este método é chamado antes da lógica base ser executada.

Substituição de destino de renderização


Declare um RenderTargetHandle para um novo destino de renderização. Utilizando-o, crie uma textura temporária e indique-a como destino. RenderTargetHandle contém o identificador da RenderTexture temporária usada. Também permite obter um RenderTargetIdentifier, usado para identificar um destino de renderização que pode ser definido, por exemplo, como RenderTexture, Objeto de textura, RenderTexture temporário ou embutido (usado pela câmera ao renderizar um quadro).

Um objeto RenderTargetHandle será criado no OutlineFeature e passado para o nosso passe quando ele for criado.

private RenderTargetHandle _destination;

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

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

O método GetTemporaryRT () cria uma RenderTexture temporária com os parâmetros especificados e a define como uma propriedade global de sombreador com o nome especificado (o nome será definido no recurso).

Remover RenderTexture
ReleaseTemporaryRT() RenderTexture. Execute() FrameCleanup.
, RenderTexture, , .

Para criar uma RenderTexture temporária, usamos o descritor da câmera atual que contém informações sobre o tamanho, formato e outros parâmetros do alvo da câmera.

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

O objetivo do destino e sua limpeza deve ocorrer apenas em Configure () usando os métodos ConfigureTarget () e ClearTarget ().

Render


Não consideraremos a renderização em detalhes, porque isso pode nos levar muito longe do tópico principal. Para renderização, usaremos o método ScriptableRenderContext.DrawRenderers (). Crie configurações para renderizar apenas objetos opacos apenas das camadas especificadas. A máscara de camada será passada para o construtor.

...
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);
}

Muito brevemente sobre os parâmetros
CullingResults — ( RenderingData)
FilteringSettings — .
DrawingSettings — .
RenderStateBlock — , (, ..)

Sobre o RenderObjectsPass concluído no UniversalRP
, . . RenderObjectsPass, Universal RP, . RenderFeature - :)

Substituição de material


Redefinimos os materiais utilizados para renderização, pois precisamos apenas dos contornos dos objetos.

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;
...
}

Shader para renderização


Crie um shader de material no ShaderGraph que será usado ao desenhar objetos na passagem atual.


Adicione uma passagem ao OutlineFeature


Voltar para OutlieFeature. Primeiro, crie uma classe para as configurações de nossa passagem.

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

Declare os campos para as configurações MyRenderPass e o nome da propriedade de textura global usada como destino de renderização por nossa passagem.

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

Crie um identificador para a propriedade de textura e uma instância de MyRenderPass.

private RenderTargetHandle _renderTexture;
private MyRenderObjectsPass _renderPass;

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

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

No método AddRendererPass, adicionamos nosso passe à fila de execução.

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

Olhando para o futuro
. , .

O resultado da passagem para a cena de origem deve ser o seguinte:


Depuração
Frame Debug (Windows-Analysis-FrameDebugger).

Passe de desfoque


O objetivo desta passagem é desfocar a imagem obtida na etapa anterior e configurá-la para a propriedade global shader.

Para fazer isso, copiaremos a textura original várias vezes para uma textura temporária, usando o sombreador de desfoque. Nesse caso, a imagem original pode ser reduzida em tamanho (crie uma cópia reduzida), o que acelerará os cálculos e não afetará a qualidade do resultado.

Vamos criar a classe BlurPass herdada de ScriptableRenderPass.

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

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

Vamos obter as variáveis ​​para as texturas de origem, destino e temporárias (e seu 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;

Todos os IDs para RenderTexture são definidos através de Shader.PropertyID (). Isso não significa que em algum lugar essas propriedades do shader devam existir necessariamente.

Adicione campos para os parâmetros restantes, que inicializamos imediatamente no construtor.

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

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

_blurMaterial - material com um sombreamento de desfoque.
_downSample - coeficiente para reduzir o tamanho da textura
_passesCount - o número de passes de desfoque a serem aplicados.

Para criar texturas temporárias, crie um descritor com todas as informações necessárias sobre ele - tamanho, formato e muito mais. A altura e o tamanho serão redimensionados em relação ao descritor da câmera.

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);
Também criaremos os identificadores e a RenderTexture temporária.
    _tmpBlurRT1 = new RenderTargetIdentifier(_tmpBlurRTId1);
    _tmpBlurRT2 = new RenderTargetIdentifier(_tmpBlurRTId2);

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

Como alteramos o destino de renderização novamente, crie outra textura temporária e especifique-a como destino.

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

Borrão


Algumas tarefas de renderização podem ser executadas usando métodos especiais ScriptableRenderContext que configuram e adicionam comandos a ele. Para executar outros comandos, você precisará usar o CommandBuffer, que pode ser obtido no pool.
Após adicionar comandos e enviá-los ao contexto, o buffer precisará ser retornado de volta ao pool.

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

A implementação final do método Execute () será a seguinte.

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


Para desfocar, crie um sombreador simples que calcule a cor do pixel levando em consideração os vizinhos mais próximos (o valor médio da cor de cinco pixels).

Shader de desfoque
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
}
}
}

Por que não o ShaderGraph?
ShaderGraph , 13 :)

Adicione uma passagem ao OutlineFeature


O procedimento será semelhante ao adicionar nossa primeira passagem. Primeiro, crie as configurações.

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

Depois os campos.

[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);
}

E adicione à fila para execução.

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

Resultado do passe:


Passe de estrutura de tópicos


A imagem final com o traço dos objetos será obtida usando o shader. E o resultado de seu trabalho será exibido na parte superior da imagem atual na tela.

Abaixo está todo o código de acesso de uma só vez, como toda a lógica está em duas linhas.

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 retorna uma malha 1 por 1.

Shader


Crie um sombreador para obter o contorno. Ele deve conter duas propriedades de textura globais. _OutlineRenderTexture e _OutlineBluredTexture para a imagem dos objetos especificados e sua versão borrada.

código de sombreador
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..

O resultado do sombreador para duas imagens obtidas anteriormente:



Adicione uma passagem ao OutlineFeature


Todas as ações são semelhantes às passagens anteriores.

[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


Resta indicar quando os passes criados serão chamados. Para fazer isso, cada um deles precisa especificar o parâmetro renderPassEvent.

Lista de evento
Universal RP, .
BeforeRendering
BeforeRenderingShadows
AfterRenderingShadows
BeforeRenderingPrepasses
AfterRenderingPrePasses
BeforeRenderingOpaques
AfterRenderingOpaques
BeforeRenderingSkybox
AfterRenderingSkybox
BeforeRenderingTransparents
AfterRenderingTransparents
BeforeRenderingPostProcessing
AfterRenderingPostProcessing
AfterRendering

Crie o campo apropriado em OutlineFeature.

[SerializeField] private RenderPassEvent _renderPassEvent;

E vamos indicá-lo para todas as passagens criadas.

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

Costumização


Adicione uma camada de estrutura de tópicos e defina-a para os objetos que queremos circundar.

Vamos criar e configurar todos os ativos necessários: UniversalRendererPipelineAsset e ForwardRendererData.



Resultado


O resultado para o nosso quadro original será o seguinte!



Conclusão


Agora, o contorno do objeto estará sempre visível, mesmo através de outros objetos. Para que nosso efeito leve em consideração a profundidade da cena, várias alterações devem ser feitas.

RenderObjectsPass


Ao especificar o objetivo de nossa renderização, também devemos indicar o buffer de profundidade atual. Crie o campo e o método apropriados.

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

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

No método Configure (), especifique a profundidade na definição do destino de renderização.

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

Estrutura de tópicos


Em OutlineFeature, passaremos MyRenderObjectsPass a profundidade da cena atual.

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


No UniversalRenderPipelineAsset usado, marque a caixa ao lado de DepthTexture.


Resultado


Resultado excluindo profundidade:


Resultado com base na profundidade:



Total


ScriptableRendererFeature é uma ferramenta bastante conveniente para adicionar suas passagens ao RP.
Nele, você pode facilmente substituir RenderObjectsPass e usá-los em outro ScriptableRendererFeature. Você não precisa se aprofundar na implementação do Universal RP e alterar seu código para adicionar algo.

PS


Para tornar o algoritmo geral para trabalhar com ScriptableRendererFeature e ScriptableRenderPass mais claro e para que o artigo não cresça muito, tentei intencionalmente criar código de acesso simples, mesmo em detrimento de sua universalidade e otimização.

Referências


Código-fonte - link para o gitlab
Modelos e cenas tiradas do jogo Lander Missions: planet depths
A seguinte implementação de golpe foi tomada como base do exemplo - youtube link
Exemplos de implementação do RenderFeature da Unity - link para o github .

Uma série de lições sobre como criar seu próprio ScriptableRenderPipeline. Depois de ler a lógica geral do RP e dos shaders, o link para os tutoriais fica claro .

All Articles