Intégration et Maîtrise du RenderGraph avec URP 17 sous Unity 6

Introduction aux Nouveautés d'URP 17

La sortie de Unity 6 s'accompagne de la version 17 de l'Universal Render Pipeline (URP), introduisant des changements architecturaux majeurs. Parmi les ajouts notables, on retrouve le système RenderGraph, le Spatial-Temporal Post-Processing (STP), le Foveated Rendering et le GPU Resident Drawer. Si certaines de ces fonctionnalités s'activent par de simples cases à cocher, d'autres, comme le GPU Resident Drawer (évolution du BatchRendererGroup) et le RenderGraph, nécessitent une compréhension approfondie.

Le changement le plus impactant pour les développeurs est l'obligation d'utiliser le RenderGraph pour créer des fonctionnalités de rendu personnalisées (Renderer Features).

Ressources et Projets de Référence

Pour explorer ces nouveaux concepts, plusieurs projets officiels sont disponibles :

  • Les exemples intégrés directement dans le package URP 17.
  • Le projet Fantasy Kingdom disponible sur l'Asset Store.
  • Le template Universal 3D téléchargeable via le Unity Hub.

Comprendre le RenderGraph

Le RenderGraph optimise l'allocation des ressources et l'ordonnancement des passes de rendu. Dans l'outil d'analyse Render Graph Viewer, les éléments visuels indiquent l'état des textures :

  • Les icônes spécifiques signalent les RenderTextures externes.
  • Les indicateurs rouges et verts représentent respectivement les opérations d'écriture et de lecture.
  • Les marqueurs globaux identifient les textures accessibles à l'échelle du frame.

Transition vers RecordRenderGraph

Dans URP 17, bien que l'ancien système de compatibilité existe encore, la création de passes repose désormais sur la méthode RecordRenderGraph. Les méthodes Configure et Execute sont reléguées au mode de compatibilité.

public class CustomRenderPass : ScriptableRenderPass
{
    private class PassExecutionData
    {
        public Renderer targetMesh;
        public Material effectMaterial;
    }

    // Méthode principale pour le système RenderGraph
    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameContext)
    {
        // Logique d'enregistrement des passes
    }
}

Gestion des Textures et Optimisations

Le système gère automatiquement le cycle de vie des textures. Les anciens RTHandle doivent être convertis en TextureHandle via l'API du RenderGraph. De plus, le système élimine automatiquement les passes inutilisées (culling). Si une pass ne semble avoir aucun effet visible mais est nécessaire, elle doit être explicitement marquée pour éviter d'être ignorée.

RenderTextureDescriptor desc = new RenderTextureDescriptor(width, height, RenderTextureFormat.DefaultHDR, 0);
TextureHandle customTexture = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "CustomEffectRT", false);

Implémentation d'une Pass de Flou d'Écran

La règle d'or en SRP est de minimiser les changements de Render Targets (RT) pour éviter les coûts de performance. Voici comment implémenter un effet de flou en utilisant le RenderGraph, en combinant des passes non sécurisées (pour les opérations de Blit) et des passes rasterisées.

Contrôleur de Scène

Ce script permet d'injecter dynamiquement la pass de rendu dans la caméra active.

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

public class BlurEffectController : MonoBehaviour
{
    public Material blurMaterial;
    [Range(1, 10)] public int iterationCount = 4;
    [Range(0, 3)] public int resolutionDownscale = 1;
    [Range(0.0f, 5f)] public float blurSpread = 0.5f;

    public RenderPassEvent executionTiming = RenderPassEvent.BeforeRenderingPostProcessing;
    public CameraType targetCameraType = CameraType.Game;

    private ScreenBlurRenderPass blurPassInstance;

    private void OnEnable()
    {
        InitializePass();
        RenderPipelineManager.beginCameraRendering += InjectPassToCamera;
    }

    private void OnDisable()
    {
        RenderPipelineManager.beginCameraRendering -= InjectPassToCamera;
    }

    private void InitializePass()
    {
        blurPassInstance = new ScreenBlurRenderPass();
        blurPassInstance.renderPassEvent = executionTiming;
        blurPassInstance.ConfigureInput(ScriptableRenderPassInput.Color);
    }

    private void InjectPassToCamera(ScriptableRenderContext context, Camera camera)
    {
        if (blurPassInstance == null || blurMaterial == null) return;
        if ((camera.cameraType & targetCameraType) == 0) return;

        blurPassInstance.UpdateParameters(blurMaterial, iterationCount, resolutionDownscale, blurSpread);
        camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(blurPassInstance);
    }
}

Logique de la Pass de Rendu

La classe suivante orchestre les allocations de textures et les exécutions de shader via le RenderGraph.

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

public class ScreenBlurRenderPass : ScriptableRenderPass
{
    private class BlurPassData
    {
        public TextureHandle sourceTex;
        public TextureHandle destinationTex;
        public TextureHandle outputTex;
        public int iterations;
        public Material shaderMaterial;
    }

    private Material effectMat;
    private int passes;
    private int downscale;
    private float spread;

    public void UpdateParameters(Material mat, int p, int d, float s)
    {
        effectMat = mat;
        passes = p;
        downscale = d;
        spread = s;
    }

    public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
    {
        var resources = frameData.Get<universalresourcedata>();

        int scaledWidth = Screen.width >> downscale;
        int scaledHeight = Screen.height >> downscale;

        var desc = new RenderTextureDescriptor(scaledWidth, scaledHeight, RenderTextureFormat.Default, 0);
        var tempBufferA = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "BlurBufferA", false);
        var tempBufferB = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "BlurBufferB", false);

        // Pass initiale : Copie et première application
        using (var builder = renderGraph.AddUnsafePass<blurpassdata>("Initial Blur Copy", out var data))
        {
            data.sourceTex = resources.activeColorTexture;
            data.destinationTex = tempBufferA;
            data.shaderMaterial = effectMat;

            builder.UseTexture(resources.activeColorTexture, AccessFlags.Read);
            builder.UseTexture(tempBufferA, AccessFlags.Write);
            builder.SetRenderFunc((BlurPassData d, UnsafeGraphContext ctx) => PerformBlit(d, ctx));
        }

        effectMat.SetFloat("_BlurOffset", spread);

        // Pass itérative : Ping-pong entre les buffers
        using (var builder = renderGraph.AddUnsafePass<blurpassdata>("Iterative Blur", out var data))
        {
            data.sourceTex = tempBufferA;
            data.destinationTex = tempBufferB;
            data.shaderMaterial = effectMat;
            data.iterations = passes;

            builder.UseTexture(tempBufferA, AccessFlags.ReadWrite);
            builder.UseTexture(tempBufferB, AccessFlags.ReadWrite);
            builder.SetRenderFunc((BlurPassData d, UnsafeGraphContext ctx) => PerformIterativeBlit(d, ctx));
        }

        // Pass finale : Affichage à l'écran via Rasterization
        using (var builder = renderGraph.AddRasterRenderPass<blurpassdata>("Final Screen Draw", out var data))
        {
            data.outputTex = tempBufferA;
            data.shaderMaterial = effectMat;

            builder.UseTexture(tempBufferA, AccessFlags.Read);
            builder.SetRenderAttachment(resources.activeColorTexture, 0);

            builder.SetRenderFunc((BlurPassData d, RasterGraphContext ctx) =>
            {
                var props = new MaterialPropertyBlock();
                props.SetTexture("_BlitTexture", d.outputTex);
                props.SetVector("_BlitScaleBias", new Vector4(1, 1, 0, 0));
                ctx.cmd.DrawProcedural(Matrix4x4.identity, d.shaderMaterial, 0, MeshTopology.Triangles, 3, 1, props);
            });
        }
    }

    private static void PerformBlit(BlurPassData data, UnsafeGraphContext context)
    {
        var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
        Blitter.BlitCameraTexture(cmd, data.sourceTex, data.destinationTex, 
            RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, data.shaderMaterial, 0);
    }

    private static void PerformIterativeBlit(BlurPassData data, UnsafeGraphContext context)
    {
        var cmd = CommandBufferHelpers.GetNativeCommandBuffer(context.cmd);
        var src = data.sourceTex;
        var dst = data.destinationTex;

        for (int i = 0; i < data.iterations; i++)
        {
            Blitter.BlitCameraTexture(cmd, src, dst, 
                RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, data.shaderMaterial, 0);
            
            // Swap pour le ping-pong
            var temp = src;
            src = dst;
            dst = temp;
        }
    }
}
</blurpassdata></blurpassdata></blurpassdata></universalresourcedata>

Pour finaliser l'effet, configurez le Shader Graph en utilisant les propriétés _BlitTexture et _BlitScaleBias. L'échantillonnage de la texture doit être réalisé via le nœud SAMPLE_TEXTURE2D. Une fois le matériau et le contrôleur assignés dans la scène, l'effet de flou sera actif.

Étiquettes: Unity6 URP17 RenderGraph SRP ShaderGraph

Publié le 19 juin à 01h27