Creación de un efecto de esquema en Unity Universal Render Pipeline

En la canalización de renderizado universal, al crear su propia RendererFeature, puede ampliar fácilmente las capacidades de renderizado. Agregar nuevos pases a la tubería de renderizado le permite crear varios efectos. En este artículo, utilizando ScriptableRendererFeature y ScriptableRenderPass, crearemos el efecto Esquema del objeto y consideraremos algunas características de su implementación.

Efecto de esquema


Introducción o algunas palabras sobre Render Pipeline


Scriptable Render Pipeline le permite controlar la representación de gráficos a través de scripts en C # y controlar el orden de procesamiento de objetos, luces, sombras y más. Universal Render Pipeline es una tubería de renderizado programable ya preparada, desarrollada por Unity y diseñada para reemplazar el antiguo RP incorporado.

Las capacidades de Universal RP se pueden ampliar creando y agregando sus propios pases de dibujo (ScriptableRendererFeature y ScriptableRenderPass). Este será el artículo actual. Será útil para aquellos que van a cambiar a Universal RP y, tal vez, ayudarán a comprender mejor el trabajo de ScriptableRenderPass'ov existente en Universal RP.

Este artículo fue escrito en Unity 2019.3 y Universal RP 7.1.8.

Plan de ACCION


Entenderemos cómo ScriptableRendererFeature y ScriptableRenderPass funcionan en el ejemplo de crear el efecto de trazo de objetos opacos.

Para hacer esto, cree una ScriptableRendererFeature que realice las siguientes acciones:

  • dibujar objetos especificados
  • desenfocar objetos dibujados
  • obtener contornos de objetos a partir de imágenes obtenidas en pasadas anteriores

Marco fuente

Y la secuencia de resultados que debemos lograr:


En el curso del trabajo, crearemos un sombreador, en cuyas propiedades globales se guardarán los resultados de la primera y segunda pasada. La última pasada mostrará el resultado del sombreador en la pantalla.

Propiedades globales
, , Properties. — .

— . .

Crear esquema Característica


ScriptableRendererFeature se utiliza para agregar pasajes de representación personalizados (ScriptableRenderPass) a Universal RP. Cree una clase OutlineFeature que herede de ScriptableRenderFeature e implemente sus 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)
    { }
}

El método Create () se usa para crear y configurar pases. Y el método AddRenderPasses () para inyectar los pases creados en la cola de representación.

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

RenderingData — , , .

Ahora comencemos a crear los pases de renderizado, y volveremos a la clase actual después de implementar cada uno de ellos.

Los objetos de render pasan


La tarea de este pasaje es dibujar objetos de una capa específica con el reemplazo del material en la propiedad de textura global del sombreador. Esta será una versión simplificada del pasaje RenderObjectsPass disponible en Universal RP, con la única diferencia en el objetivo (RenderTarget) donde se realizará el renderizado.

Cree una clase MyRenderObjectsPass heredada de ScriptableRenderPass. Implementamos el método Execute (), que contendrá toda la lógica del pasaje, así como redefiniremos el 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)
        { }
    }
}

El método Configure () se usa para especificar el propósito de renderizar y crear texturas temporales. Por defecto, el objetivo es el objetivo de la cámara actual, y después de completar el pase, se volverá a indicar por defecto. Este método se llama antes de que se ejecute la lógica base.

Representación de reemplazo de objetivos


Declare un RenderTargetHandle para un nuevo objetivo de representación. Utilizándolo, cree una textura temporal e indíquelo como el objetivo. RenderTargetHandle contiene el identificador de la RenderTexture temporal utilizada. También le permite obtener un RenderTargetIdentifier, que se utiliza para identificar un objetivo de renderizado que se puede establecer, por ejemplo, como RenderTexture, objeto Texture, RenderTexture temporal o incorporado (utilizado por la cámara al renderizar un marco).

Se creará un objeto RenderTargetHandle en OutlineFeature y se pasará a nuestro pase cuando se cree.

private RenderTargetHandle _destination;

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

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

El método GetTemporaryRT () crea una RenderTexture temporal con los parámetros especificados y la establece como una propiedad de sombreador global con el nombre especificado (el nombre se establecerá en la función).

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

Para crear una RenderTexture temporal, utilizamos el descriptor de la cámara actual que contiene información sobre el tamaño, el formato y otros parámetros del objetivo de la cámara.

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

El propósito del objetivo y su limpieza solo debe ocurrir en Configure () usando los métodos ConfigureTarget () y ClearTarget ().

Hacer


No consideraremos la representación en detalle, porque Esto nos puede llevar muy lejos del tema principal. Para la representación, utilizaremos el método ScriptableRenderContext.DrawRenderers (). Cree configuraciones para representar solo objetos opacos solo de las capas especificadas. La máscara de capa se pasará al constructor.

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

Muy brevemente sobre los parámetros
CullingResults — ( RenderingData)
FilteringSettings — .
DrawingSettings — .
RenderStateBlock — , (, ..)

Acerca del RenderObjectsPass terminado en UniversalRP
, . . RenderObjectsPass, Universal RP, . RenderFeature - :)

Reemplazo de material


Redefinimos los materiales utilizados para el renderizado, ya que solo necesitamos los contornos de los 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 renderizar


Cree un sombreador de material en ShaderGraph que se usará al dibujar objetos en el pase actual.


Agregar un pasaje a OutlineFeature


Volver a OutlieFeature. Primero, cree una clase para la configuración de nuestro pasaje.

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

Declare los campos para la configuración de MyRenderPass y el nombre de la propiedad de textura global utilizada como objetivo de representación por nuestro pase.

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

Cree un identificador para la propiedad de textura y una instancia de MyRenderPass.

private RenderTargetHandle _renderTexture;
private MyRenderObjectsPass _renderPass;

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

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

En el método AddRendererPass, agregamos nuestro pase a la cola de ejecución.

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

Mirando hacia el futuro
. , .

El resultado del pase para la escena fuente debe ser el siguiente:


Depuración
Frame Debug (Windows-Analysis-FrameDebugger).

Pase de desenfoque


El propósito de este pasaje es desenfocar la imagen obtenida en el paso anterior y establecerla en la propiedad de sombreador global.

Para hacer esto, copiaremos la textura original varias veces a una temporal, usando el sombreador de desenfoque. En este caso, la imagen original se puede reducir de tamaño (crear una copia reducida), lo que acelerará los cálculos y no afectará la calidad del resultado.

Creemos la clase BlurPass heredada de ScriptableRenderPass.

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

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

Consigamos las variables para las texturas de origen, destino y temporales (y su 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 los ID de RenderTexture se configuran a través de Shader.PropertyID (). Esto no significa que en algún lugar tales propiedades de sombreador necesariamente existan.

Agregue campos para los parámetros restantes, que inicializamos inmediatamente en el constructor.

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 con un sombreador borroso.
_downSample: coeficiente para reducir el tamaño de la textura
_passesCount: la cantidad de pases de desenfoque que se aplicarán.

Para crear texturas temporales, cree un descriptor con toda la información necesaria sobre él: tamaño, formato y más. La altura y el tamaño se escalarán en relación con el descriptor de la cámara.

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);
También crearemos los identificadores y la RenderTexture temporal ellos mismos.
    _tmpBlurRT1 = new RenderTargetIdentifier(_tmpBlurRTId1);
    _tmpBlurRT2 = new RenderTargetIdentifier(_tmpBlurRTId2);

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

Cambiamos el objetivo de representación nuevamente, así que cree otra textura temporal y especifíquela como el objetivo.

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

Difuminar


Algunas tareas de renderizado se pueden realizar utilizando métodos especiales de ScriptableRenderContext que configuran y le agregan comandos. Para ejecutar otros comandos, necesitará usar CommandBuffer, que se puede obtener del grupo.
Después de agregar comandos y enviarlos al contexto, será necesario devolver el búfer al grupo.

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

La implementación final del método Execute () será la siguiente.

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 desenfocar, cree un sombreador simple que calcule el color del píxel teniendo en cuenta sus vecinos más cercanos (el valor de color promedio de cinco píxeles).

Sombreador de desenfoque
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 qué no ShaderGraph?
ShaderGraph , 13 :)

Agregar un pasaje a OutlineFeature


El procedimiento será similar a agregar nuestro primer pase. Primero, cree la configuración.

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

Luego los 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);
}

Y agregue a la cola para su ejecución.

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

Resultado del pase:


Pase de esquema


La imagen final con el trazo de los objetos se obtendrá con el sombreador. Y el resultado de su trabajo se mostrará en la parte superior de la imagen actual en la pantalla.

A continuación se muestra el código de acceso completo a la vez, como Toda la lógica está en dos líneas.

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 devuelve una malla 1 por 1.

Shader


Crea un sombreador para obtener el contorno. Debe contener dos propiedades de textura global. _OutlineRenderTexture y _OutlineBluredTexture para la imagen de los objetos especificados y su versión borrosa.

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

El resultado del sombreador para dos imágenes obtenidas previamente:



Agregar un pasaje a OutlineFeature


Todas las acciones son similares a los pases 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


Queda por indicar cuándo se llamarán los pases creados. Para hacer esto, cada uno de ellos necesita especificar el parámetro renderPassEvent.

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

Cree el campo apropiado en OutlineFeature.

[SerializeField] private RenderPassEvent _renderPassEvent;

Y lo indicaremos a todos los pasajes creados.

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

Personalización


Agregue una capa de esquema y configúrela para los objetos que queremos rodear.

Crearemos y configuraremos todos los activos necesarios: UniversalRendererPipelineAsset y ForwardRendererData.



Resultado


¡El resultado para nuestro marco original será el siguiente!



Terminación


Ahora el contorno del objeto siempre será visible, incluso a través de otros objetos. Para que nuestro efecto tenga en cuenta la profundidad de la escena, se deben realizar varios cambios.

RenderObjectsPass


Al especificar el propósito de nuestro render, también debemos indicar el búfer de profundidad actual. Cree el campo y método apropiados.

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

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

En el método Configure (), especifique la profundidad en la configuración del objetivo de renderizado.

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

Esquema


En OutlineFeature, pasaremos MyRenderObjectsPass la profundidad de la escena actual.

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


En el UniversalRenderPipelineAsset usado, marque la casilla junto a DepthTexture.


Resultado


Resultado sin profundidad:


Resultado basado en la profundidad:



Total


ScriptableRendererFeature es una herramienta bastante conveniente para agregar sus pasajes al RP.
En él, puede reemplazar fácilmente RenderObjectsPass y usarlos en otra ScriptableRendererFeature. No necesita profundizar en la implementación de Universal RP y cambiar su código para agregar algo.

PD


Para que el algoritmo general de trabajar con ScriptableRendererFeature y ScriptableRenderPass sea más claro, y para que el artículo no crezca demasiado, intenté crear un código de acceso simple, incluso en detrimento de su universalidad y óptima.

Referencias


El código fuente es un enlace a gitlab. Los
modelos y la escena están tomados de Lander Missions: juego de las profundidades del planeta.
La siguiente implementación de trazo se tomó como base del ejemplo: enlace de youtube.
Ejemplos de implementación de RenderFeature de Unity: enlace a github .

Una serie de lecciones sobre cómo crear su propio ScriptableRenderPipeline. Después de leer la lógica general del RP y los sombreadores, el enlace a los tutoriales se vuelve claro .

All Articles