だんごのUnity開発メモ

Unityでの開発や勉強内容をまとめています!

【Unity SRP】不透明オブジェクトを描画する

はじめに

Unityのレンダリングパイプラインについての勉強メモです。
今回はSRPで不透明オブジェクトを描画していきます。

ちなみにSRPとは Scriptable Render Pipeline の訳です。(長いのでSRPで統一します)

f:id:s-dango:20211108005130p:plain

環境

Unity2020.3.15f2
com.unity.render-pipelines.core@10.5.1

目次

SRPの導入

SRPは以下の方法で導入できます。

  • 新規でプロジェクトを作成する場合はテンプレートから「URP」または「HDRP」を選択
  • 既にプロジェクトを作成している場合は「Windows/Package Manager」から「Core RP Library」をインポート

パイプラインの作成

不透明オブジェクトを描画するためのパイプラインを作成します。
独自パイプラインを作成するためには以下のクラスが必要になります。

  • 「RenderPipelineAsset」を継承したクラス
  • 「RenderPipeline」を継承したクラス

以下が実際のソースコードです。
ソースコード自体はUnityのURPの実装を参考にしています。

BasicRenderPipelineAsset.cs

using UnityEngine;
using UnityEngine.Rendering;

public class BasicRenderPipelineAsset : RenderPipelineAsset
{
#if UNITY_EDITOR
    [UnityEditor.MenuItem( "Project/Create/Render Pipeline Asset/Basic" )]
    public static void CreateAsset()
    {
        BasicRenderPipelineAsset instance = CreateInstance<BasicRenderPipelineAsset>();
        UnityEditor.AssetDatabase.CreateAsset( instance, "Assets/Rendering/BasicRenderPipeline/BasicPipelineAsset.asset" );
    }
#endif

    protected override RenderPipeline CreatePipeline()
    {
        return new BasicRenderPipeline();
    }
}

BasicRenderPipeline.cs

using UnityEngine;
using UnityEngine.Rendering;
using Unity.Collections;

public class BasicRenderPipeline : RenderPipeline
{
    protected override void Render( ScriptableRenderContext context, Camera[] cameras )
    {
        for( int i = 0; i < cameras.Length; i++ )
        {
            RenderSingleCamera(context, cameras[i]);
        }
    }

    /// <summary>
    /// 単一カメラごとのレンダリング
    /// </summary>
    /// <param name="context"></param>
    /// <param name="camera"></param>
    void RenderSingleCamera(ScriptableRenderContext context, Camera camera)
    {
        ScriptableCullingParameters cullParams;
        if (camera.TryGetCullingParameters(out cullParams) == false)
        {
            return;
        }

        CommandBuffer cmd = CommandBufferPool.Get();

        ProfilingSampler sampler = new ProfilingSampler($"{nameof(BasicRenderPipeline)}.{nameof(RenderSingleCamera)}.{camera.name}");
        using (new ProfilingScope(cmd, sampler))
        {
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            // カメラプロパティ設定
            context.SetupCameraProperties(camera, false);

            // カリング設定
            CullingResults cullResults = context.Cull(ref cullParams);

            // 不透明オブジェクト描画
            DrawObjectsOpaque(context, camera, cullResults);
        }

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

        context.Submit();
    }

    /// <summary>
    /// 不透明オブジェクト描画
    /// </summary>
    /// <param name="context"></param>
    /// <param name="camera"></param>
    /// <param name="cullResults"></param>
    void DrawObjectsOpaque(ScriptableRenderContext context, Camera camera, CullingResults cullResults)
    {
        CommandBuffer cmd = CommandBufferPool.Get();

        ProfilingSampler sampler = new ProfilingSampler(nameof(DrawObjectsOpaque));
        using (new ProfilingScope(cmd, sampler))
        {
            context.ExecuteCommandBuffer(cmd);
            cmd.Clear();

            // ソート設定
            SortingSettings sortingSettings = new SortingSettings(camera);
            sortingSettings.criteria = SortingCriteria.CommonOpaque;

            // 実行するシェーダーパスを設定
            ShaderTagId shaderTagId = new ShaderTagId("OpaquePass");

            // OpaquePass のみ描画するように設定
            DrawingSettings drawingSettings = new DrawingSettings(shaderTagId, sortingSettings);

            // RenderQueue が Opaque の場合のみ描画
            FilteringSettings filteringSettings = new FilteringSettings(RenderQueueRange.opaque);

            // 不透明オブジェクトを描画する
            context.DrawRenderers(cullResults, ref drawingSettings, ref filteringSettings);
        }

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

RenderPipelineを継承したクラスは「Render」関数を定義する必要があります。
この関数はレンダリングのエントリポイントとなり、 レンダリングのコンテキストカメラの配列を引数として受け取ります。
以下がRender関数内の最小構成となり、ここにカメラごとの処理を追加していきます。

protected override void Render(ScriptableRenderContext context, Camera[] cameras)
    {
        foreach (var camera in cameras)
        {
        }
    }

CommandBufferの作成

レンダリングコンテキストにコマンドを発行するために、新しくコマンドバッファを作成します。
コマンドバッファとは、レンダリングコマンドリストを保持しており、レンダリングパイプラインの様々なタイミングでコマンドを実行できる機能です。
https://docs.unity3d.com/ja/2018.4/Manual/GraphicsCommandBuffers.html

カメラ設定

以下でカメラプロパティ(ビュー行列、プロジェクション行列、シェーダーのグローバル変数など)を設定しています。

context.SetupCameraProperties(camera);

カリング処理

カリングとは、画面に映さないポリゴンをピクセルパイプラインに渡す前に破棄するプロセスのことです。
以下の処理では context.Cull にカリングパラメータを渡すことで、カリング処理の結果を受け取っています。

ScriptableCullingParameters cullingParameters;
if (camera.TryGetCullingParameters(false, out cullingParameters) == false)
{
    continue;
}

CullingResults cullResults = context.Cull(ref cullingParameters);

フィルタリング処理

描画するオブジェクトのフィルタリングを以下の2つで設定しています。

  • ShaderTagId
  • RenderQueueRange

ShadertagId

シェーダー側の「LightMode」タグで指定することで、指定されたパスが実行されるようになります。
今回は「BasicPass」を指定しているので、シェーダー側で { "LightMode" = "BasicPass" } を指定したパスのみが実行されることになります。

ShaderTagId shaderTagId = new ShaderTagId("BasicPass");

RenderQueueRange

オブジェクトが描画されるタイミングをキューの範囲で指定します。
今回は不透明オブジェクトを描画するため、RenderQueueを「opaque」に設定しています。

FilteringSettings filteringSettings = new FilteringSettings(RenderQueueRange.opaque);

最後にコンテキストの Submit 関数を呼び出してパイプラインの処理は完了です。
「Submit」関数は「ExecuteCommandBuffer」にてキューに登録された全てのレンダリングコマンドを実行しています。

context.Submit();

シェーダーの作成

パイプラインの作成が完了したので、次はシェーダー側で描画の中身を作成していきます。
コード自体はURPの Unlit.shader を参考にしています。

Basic.shader

Shader "Custom/Basic"
{
    Properties
    {
        [MainTexture] _BaseMap("Base Map", 2D) = "white" {}
        [MainColor] _BaseColor("Base Color", Color) = (1, 1, 1, 1)
        _Cutoff("Alpha Cutout", Range(0.0, 1.0)) = 0.5

        // ObsoleteProperties
        [HideInInspector] _MainTex("Texture", 2D) = "white" {}
        [HideInInspector] _Color("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType" = "Opaque" }
        LOD 100

        Pass
        {
            Name "BasicPass"
            Tags { "LightMode" = "BasicPass" }

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5

            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl"

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

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

            TEXTURE2D(_BaseMap);
            SAMPLER(sampler_BaseMap);
            float4 _BaseMap_ST;
            float4 _BaseColor;
            float _Cutoff;

            float4x4 unity_ObjectToWorld;
            float4x4 unity_MatrixVP;

            float3 TransformObjectToWorld(float3 positionOS)
            {
                return mul(unity_ObjectToWorld, float4(positionOS, 1.0)).xyz;
            }

            float4 TransformWorldToHClip(float3 positionWS)
            {
                return mul(unity_MatrixVP, float4(positionWS, 1.0));
            }

            Varyings vert(Attributes i)
            {
                Varyings o = (Varyings)0;

                float3 positionWS = TransformObjectToWorld(i.positionOS.xyz);
                float4 positionCS = TransformWorldToHClip(positionWS);
                o.vertex = positionCS;
                o.uv = TRANSFORM_TEX(i.uv, _BaseMap);

                return o;
            }

            half4 frag(Varyings i) : SV_Target
            {
                float2 uv = i.uv;
                float4 texColor = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, uv);
                float3 color = texColor.rgb * _BaseColor.rgb;
                half alpha = texColor.a * _BaseColor.a;

                return half4(color, alpha);
            }
            ENDHLSL
        }
    }
}

「LightMode」タグに「BasicPass」を指定することでパスが実行されるようになります。

Name "Basic"
Tags { "LightMode" = "BasicPass" }

ここまでで全ての工程が完了しました。
一度Unityエディタに戻ってオブジェクトを作成します。

  • 作成した「BasicRenderPipelineAsset」を「Edit/Project Settings/Graphics/Scriptable Render Pipeline Settings」に登録
  • マテリアルを作成して、先ほど作成したシェーダーを指定
  • オブジェクトを作成して、作成したマテリアルを適用

これで画面に不透明オブジェクトが描画されることが確認できました。

f:id:s-dango:20211108005130p:plain

ついでに...
コンテキストの「DrawSkybox」を呼び出すことで不透明オブジェクトの描画後にSkyboxを描画することができます。

// 不透明オブジェクト描画
DrawObjectsOpaque(context, camera, cullResults);

// Skybox描画
context.DrawSkybox(camera);

なぜSkyboxを不透明オブジェクトの後に描画するのか

Skyboxは画面全体に描画されるため、不透明オブジェクトより先に描画してしまうと、後から不透明オブジェクトを描画するときにSkyboxで描画した部分を塗りつぶしてしまいます。
そのため、先に不透明オブジェクトを描画しておくことでSkyboxを描画するときに不透明オブジェクトで塗られた部分を無視するため、処理負荷が軽くなるためらしいです。

描画順番としては、「不透明オブジェクト > Skybox > 半透明オブジェクト」という順番がUnityの標準となっています。

まとめ

今回は「SRPで不透明オブジェクトの描画」を行いました。

ここまで見てくださってありがとうございます。
内容に間違い等ありましたら遠慮なくご指摘いただけますと幸いです。