サイト内を検索

Unity、URP、RenderFeature を用いたマルチパスレンダリング【 実践編 】

こんにちは。

Cocone Engineering(以下、CE)でグラフィックエンジニア兼テクニカルアーティストのTechLeadとして、データパイプラインやレンダーパイプラインの開発、アセット制作環境の整備等に携わっている高野正也たかのまさやと申します。

以前の記事では、Unity URPにおけるRenderFeatureを使ったマルチパスレンダリングの基礎について解説しました。

Unity、URP、RenderFeatureを用いたマルチパスレンダリング【実装編】

Unity、URP、RenderFeatureを用いたマルチパスレンダリング【解説編】

今回は実践編として、マルチパスレンダリングの技術を活用した 「半透明オブジェクトが重なった時に半透明の最前面のみを描画するシステム」 の実装を解説します。実際のゲーム開発で遭遇する課題を解決する、より実用的な内容となっています。

1. 機能概要

この 最前面半透明描画機能 は半透明が重なった部分について、最前面の半透明のみを描画します。キャラクター全体を半透明にした時に内部構造が見えてしまう問題を改善します。

半透明が重なった描画

半透明の最前面のみ描画

Unity 6000.2.12f1、URP 17.2.0、Forward Rendering Pathで動作確認していますが、Unity 6000系(Unity 6)であれば問題なく動作します。

2. 実装手順

① ForegroundTransparentFeature.cs、ForegroundTransparentPass.cs、ForegroundTransparent.shaderの3つのファイルをUnityプロジェクト内に作り、中身をコピー&ペーストして保存してください。

ForegroundTransparentFeature.cs
using System;
using UnityEngine;
using UnityEngine.Rendering.Universal;

namespace TechBlog.Rendering
{
    /// <summary>
    /// ForegroundTransparentFeature
    ///
    /// 半透明オブジェクトを最前面に描画する ScriptableRendererFeature。
    /// 深度プリパスを行うことで、半透明オブジェクト同士の奥行きを正しく制御します。
    ///
    /// 構成:
    /// ① _depthPass : DepthOnly パスで深度プリパス
    /// ② _colorPass : カラー描画パス(SRPDefaultUnlit / UniversalForward)
    ///
    /// すべて RenderPassEvent の順序で実行されます。
    /// </summary>
    public class ForegroundTransparentFeature : ScriptableRendererFeature
    {
        [Serializable]
        public class Settings
        {
            [Tooltip("描画対象のレイヤーマスク")]
            public LayerMask layerMask = 0;

            [HideInInspector]
            public string[] depthTagNames = { "DepthOnly" };

            [HideInInspector]
            public string[] colorTagNames = { "UniversalForward", "SRPDefaultUnlit" };

            [HideInInspector]
            public string passTagPrefix = "ForegroundTransparent";
        }

        public Settings settings = new();

        // 各パス
        private ForegroundTransparentPass _depthPass;
        private ForegroundTransparentPass _colorPass;

        public override void Create()
        {
            // ① DepthOnly プリパス
            _depthPass = new ForegroundTransparentPass(
                $"{settings.passTagPrefix}_Depth",
                settings.depthTagNames,
                RenderPassEvent.BeforeRenderingTransparents,
                settings.layerMask,
                ForegroundTransparentPass.PassMode.DepthOnly
            );

            // ② カラー描画パス(Unlit + Lit両対応)
            _colorPass = new ForegroundTransparentPass(
                $"{settings.passTagPrefix}_Color",
                settings.colorTagNames,
                RenderPassEvent.BeforeRenderingTransparents + 1,
                settings.layerMask,
                ForegroundTransparentPass.PassMode.Color
            );
        }

        public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
        {
            // Previewカメラではスキップ
            if (renderingData.cameraData.cameraType == CameraType.Preview)
            {
                return;
            }

            // RenderPassEventの昇順で自動実行される
            renderer.EnqueuePass(_depthPass);
            renderer.EnqueuePass(_colorPass);
        }
    }
}


ForegroundTransparentPass.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RendererUtils;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;

namespace TechBlog.Rendering
{
    /// <summary>
    /// 前景半透明オブジェクト用の汎用レンダーパス
    /// モードによって深度のみ書き込み、またはカラー+深度書き込みを切り替え可能
    /// </summary>
    public class ForegroundTransparentPass : ScriptableRenderPass
    {
        /// <summary>
        /// パスモード
        /// </summary>
        public enum PassMode
        {
            DepthOnly,  // 深度のみ書き込み
            Color,      // カラー+深度書き込み
        }

        private readonly string _profilerTag;
        private readonly ProfilingSampler _profilingSampler;
        private readonly LayerMask _layerMask;
        private readonly RenderStateBlock _renderStateBlock;
        private readonly List<ShaderTagId> _shaderTagIds = new();
        private readonly PassMode _mode;

        public ForegroundTransparentPass(
            string tag,
            string[] drawTagNames,
            RenderPassEvent passEvent,
            LayerMask layerMask,
            PassMode mode
        )
        {
            _profilerTag = tag;
            _profilingSampler = new ProfilingSampler(tag);
            renderPassEvent = passEvent;
            _layerMask = layerMask;
            _mode = mode;

            // レンダーステートのカスタマイズが必要な場合はここで設定
            _renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);

            if (drawTagNames != null)
            {
                foreach (var name in drawTagNames)
                {
                    if (!string.IsNullOrEmpty(name))
                    {
                        _shaderTagIds.Add(new ShaderTagId(name));
                    }
                }
            }
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            var cameraData = frameData.Get<UniversalCameraData>();
            var renderingData = frameData.Get<UniversalRenderingData>();
            var resourceData = frameData.Get<UniversalResourceData>();

            // 透明オブジェクトの標準的なソート順
            var sorting = SortingCriteria.CommonTransparent;

            // Lit シェーダで必要な PerObject 情報を有効化
            // (Unlit の場合は不要な情報は無視される)
            var perObject = PerObjectData.Lightmaps
                            | PerObjectData.LightProbe
                            | PerObjectData.LightProbeProxyVolume
                            | PerObjectData.ReflectionProbes
                            | PerObjectData.ShadowMask
                            | PerObjectData.OcclusionProbe
                            | PerObjectData.OcclusionProbeProxyVolume
                            | PerObjectData.LightData
                            | PerObjectData.ReflectionProbeData;

            foreach (var shaderTag in _shaderTagIds)
            {
                using var builder = renderGraph.AddRasterRenderPass<PassData>(
                    $"{_profilerTag}_{shaderTag.name}", out var passData, _profilingSampler);

                // モードに応じてアタッチメントを設定
                if (_mode == PassMode.Color)
                {
                    // カラー+深度書き込み
                    passData.Color = resourceData.activeColorTexture;
                    builder.SetRenderAttachment(passData.Color, 0);
                    builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
                }
                else // DepthOnly
                {
                    // カラーアタッチメントを読み取り専用でアタッチ
                    // (RenderGraphのバリデーション要件を満たすため)
                    builder.SetRenderAttachment(resourceData.activeColorTexture, 0, AccessFlags.Read);
                    // 深度アタッチメントには書き込み
                    builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Write);
                }

                var rendererListDesc = new RendererListDesc(shaderTag, renderingData.cullResults, cameraData.camera)
                {
                    sortingCriteria = sorting,
                    renderQueueRange = RenderQueueRange.all,
                    layerMask = _layerMask,
                    stateBlock = _renderStateBlock,
                    rendererConfiguration = perObject,
                };

                passData.RendererList = renderGraph.CreateRendererList(rendererListDesc);
                builder.UseRendererList(passData.RendererList);

                // プロファイル上で可視化するため、パスカリングを無効化
                builder.AllowPassCulling(false);

                builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
                {
                    context.cmd.DrawRendererList(data.RendererList);
                });
            }
        }

        private class PassData
        {
            public TextureHandle Color;
            public RendererListHandle RendererList;
        }
    }
}


ForegroundTransparent.shader
Shader "TechBlog/ForegroundTransparent"
{
    Properties
    {
        _BaseColor("Base Color", Color) = (1, 1, 1, 1)
    }

    SubShader
    {
        Tags
        {
            "RenderType" = "Transparent"
            "Queue" = "Transparent"
        }

        Pass
        {
            Name "Unlit"
            Tags { "LightMode" = "SRPDefaultUnlit" }

            Blend SrcAlpha One
            ZWrite Off
            ZTest LEqual
            Cull Back

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

            struct Attributes
            {
                float4 positionOS : POSITION;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
            };

            CBUFFER_START(UnityPerMaterial)
                half4 _BaseColor;
            CBUFFER_END

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

            half4 frag(Varyings input) : SV_Target
            {
                return _BaseColor;
            }
            ENDHLSL
        }

        Pass
        {
            Name "DepthPrepass"
            Tags { "LightMode" = "DepthOnly" }

            ZWrite On
            ColorMask 0
            ZTest LEqual
            Cull Back

            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

            struct Attributes
            {
                float4 positionOS : POSITION;
            };

            struct Varyings
            {
                float4 positionCS : SV_POSITION;
            };

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

            half4 frag(Varyings input) : SV_Target
            {
                return half4(0, 0, 0, 0);
            }
            ENDHLSL
        }
    }

    FallBack "Hidden/Universal Render Pipeline/FallbackError"
}


② 描画モデルのマテリアルにForegroundTransparent.shaderシェーダを割り当てます。また、この処理を適用する専用のレイヤー(例:ForegroundTransparent)をUnityのProject Settings > Tags and Layersから追加し、モデルに割り当てます。

③ 使用しているUniversalRenderDataの下部にあるAdd Render Featureボタンを押しForegroundTransparentFeatureを追加します。(モバイルのデフォルト設定だとAssets/URP-Balanced-Rendererを使用)Layer Maskに追加したForegroundTransparentLayerのみを設定してください。

④ 最後に使用しているUniversalRenderDataのTransparent Layer MaskからForegroundTransparentLayerのチェックを外します。これは、標準の透明描画パスで二重に描画されることを防ぐためです。ForegroundTransparentFeatureで専用に処理するため、標準パスからは除外する必要があります。

以上で、実装は完了です。正常に処理出来ていれば、半透明部分の最前面のみが描画されているはずです。

3. 実装内容の説明

この機能は深度プリパス(Depth Prepass) と カラー描画パス の2パスレンダリングで実現しています。以下、各ファイルの役割と実装の詳細を説明します。

3.1 ForegroundTransparentFeature.cs – レンダリングパイプラインへの統合

ForegroundTransparentFeatureは、URPのScriptableRendererFeatureを継承したクラスで、レンダリングパイプラインに独自の描画処理を追加します。

主な役割:

・2つのレンダーパス(深度プリパスとカラー描画パス)を生成・管理

・レイヤーマスクによる描画対象の制御

・パスの実行タイミングをRenderPassEvent.BeforeRenderingTransparentsに設定

// 深度プリパス(先に深度情報のみ書き込み)
_depthPass = new ForegroundTransparentPass(
    $"{settings.passTagPrefix}_Depth",
    settings.depthTagNames,
    RenderPassEvent.BeforeRenderingTransparents,
    settings.layerMask,
    ForegroundTransparentPass.PassMode.DepthOnly
);

// カラー描画パス(深度テストを使って最前面のみ描画)
_colorPass = new ForegroundTransparentPass(
    $"{settings.passTagPrefix}_Color",
    settings.colorTagNames,
    RenderPassEvent.BeforeRenderingTransparents + 1,
    settings.layerMask,
    ForegroundTransparentPass.PassMode.Color
);

深度パスが先に実行され、その後カラーパスが実行されることで、半透明オブジェクトの最前面のみが描画される仕組みです。

3.2 ForegroundTransparentPass.cs – 描画ロジックの実装

ForegroundTransparentPassは、ScriptableRenderPassを継承し、実際の描画処理を担当します。

Unity 6のRenderGraphについて: Unity 6からはRenderGraphベースの実装が推奨されており、この実装も RecordRenderGraph メソッドを使用しています。 RenderGraphは、レンダリングパスのリソース依存関係を自動的に管理し、最適化を行うシステムです。

主な特徴:

1.パスモードの切り替え

 ・DepthOnly: 深度バッファのみに書き込み(ColorMask 0相当)

 ・Color: カラーと深度の両方に書き込み

2.リソース管理の最適化

 ・RenderGraphによる自動的なリソース依存関係の管理

 ・テクスチャアタッチメントの明示的な定義

3.Lit/Unlitシェーダー両対応

 ・UniversalForwardタグ(Litシェーダー)とSRPDefaultUnlitタグ(Unlitシェーダー)の両方をサポート

 ・PerObjectDataを適切に設定し、ライティング情報を提供

// モードに応じてアタッチメントを設定if (_mode == PassMode.Color)
{
// カラー+深度書き込み
    passData.Color = resourceData.activeColorTexture;
    builder.SetRenderAttachment(passData.Color, 0);
    builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
}
else// DepthOnly
{
// 深度のみ書き込み(カラーは読み取り専用)
    builder.SetRenderAttachment(resourceData.activeColorTexture, 0, AccessFlags.Read);
    builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture, AccessFlags.Write);
}

3.3 ForegroundTransparent.shader – シェーダー実装

このシェーダーは、2つのパスを持つマルチパスシェーダーです。

Pass 1: Unlitパス(カラー描画)

LightModeSRPDefaultUnlit

ZWrite: Off(深度書き込みなし)

ZTest: LEqual(既存の深度と比較)

Blend: SrcAlpha One(加算的な半透明)

Pass
{
    Name "Unlit"
    Tags { "LightMode" = "SRPDefaultUnlit" }

    Blend SrcAlpha One
    ZWrite Off      // 深度は書き込まない
    ZTest LEqual    // 深度プリパスで書き込んだ深度と比較
    Cull Back
}

ブレンドモードについて:

このシェーダーでは Blend SrcAlpha One を使用しています。これは加算ブレンド(Additive Blending) と呼ばれる方式で、通常の半透明描画とは異なる特性があります。

加算ブレンド (Blend SrcAlpha One):

  ◦ ソースカラーのアルファ値で乗算した色を、背景色に加算します◦

  ◦ 発光エフェクトやグローのような明るい表現に適しています

  ◦ 重ねるほど明るくなります

通常の半透明 (Blend SrcAlpha OneMinusSrcAlpha):

  ◦ ソースカラーと背景色をアルファ値で線形補間します

  ◦ 一般的な透明ガラスや水のような表現に使用されます

  ◦ 背景色と混ざり合います

このサンプルでは加算ブレンドを使用していますが、用途に応じてブレンドモードを変更することができます。例えば、通常の半透明表現が必要な場合は、シェーダーの Blend SrcAlpha One を Blend SrcAlpha OneMinusSrcAlpha に変更してください。

Pass 2: DepthPrepassパス(深度のみ)

LightModeDepthOnly

ZWrite: On(深度書き込みあり)

ColorMask: 0(カラーバッファに書き込まない)

Pass
{
    Name "DepthPrepass"
    Tags { "LightMode" = "DepthOnly" }

    ZWrite On       // 深度のみ書き込む
    ColorMask 0     // カラーには書き込まない
    ZTest LEqual
    Cull Back
}

3.4 レンダリングフロー全体像

実際のレンダリングは以下の順序で実行されます:

1.深度プリパス(ForegroundTransparent_Depth)

  ◦ DepthOnlyパスで半透明オブジェクトの深度情報のみをデプスバッファに書き込み

  ◦ この時点でカラーバッファには何も描画されない

2.カラー描画パス(ForegroundTransparent_Color)

  ◦ SRPDefaultUnlitパスで半透明オブジェクトを描画

  ◦ ZTest LEqualにより、深度プリパスで書き込んだ最前面のピクセルのみが描画される

  ◦ 奥にあるピクセルは深度テストで弾かれる

3.結果

  ◦ 半透明オブジェクトが重なっている部分では、最前面のオブジェクトのみが描画される

  ◦ 内部構造が透けて見える問題が解消される

この仕組みにより、通常の半透明描画では避けられない「半透明が重なると内部が見える」問題を、追加の深度情報を活用して解決しています。

まとめ

今回はマルチパスレンダリングを使用した、具体的な実装例を紹介させて頂きました。 この記事が皆様のお役に立てましたら幸いです。

Tag

Category

Tag