だんごのUnity開発メモ

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

【Unity SRP】鏡面反射の実装

はじめに

前回は不透明オブジェクトに拡散反射を実装しました。
今回は前回のモデルに鏡面反射を実装していきます。

s-dango.hatenablog.jp

目次

反射モデルとは

3Dグラフィックスにおいて、現実世界の光の挙動を表現するための計算式のことです。
モデリングされた物体のサーフェス(表面)の一点に陰影つけるために使用します。

鏡面反射光

物体の光沢やハイライトを表現することができます。

特徴

  • 物体の色の影響を受けず、光源と同じ色のまま反射する
  • 視点によって見える位置や大きさが変化する
  • 正反射方向に近づけば近づくほど、光が強くなる

フォン反射モデル

鏡面反射の光の挙動を表現するためのモデルです。
「光源の正反射ベクトル」と「視点ベクトル」との内積で算出することができます。

フォン反射モデルの式

N・・・正規化法線ベクトル
L・・・光源ベクトル
R・・・正反射ベクトル
V・・・視点ベクトル
k_s・・・鏡面反射係数(鏡面反射の反射率)
i_s・・・鏡面反射成分(光源の明るさ)
a・・・粗さ係数(物体の光沢度)


\begin{align*}
&R = 2 \times N \times \bigl( N \cdot L \bigr) - L \\
&I = k_s \times i_s \times \bigl( R \cdot V \bigr) ^ 2
\end{align*}

この視点方向と正反射方向が一致するときに明るさが最大になります。
光沢度を表す$a$の値が大きくなるにつれて、光沢の明るさも減少していきます。
f:id:s-dango:20220122194631j:plain:h300

実行サンプル

左:鏡面反射なし、右:フォン反射モデル f:id:s-dango:20220122194751p:plain:h300

カメラ情報の設定

今回はシェーダー側でカメラの方向ベクトルを使用するため、C#側からシェーダーへカメラベクトルを渡す必要があります。

前回のコードにシェーダー側へ渡すための処理を追加していきます。
※コードは URP の実装を参考にしています。

/// <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);

        // カメラの情報をシェーダー側に渡す
        SetPerCameraShaderVariables(cmd, camera);

        // ライト情報の設定
        SetupLights(context, cullResults);

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

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

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

    context.Submit();
}

/// <summary>
/// シェーダーに渡すカメラ情報の設定
/// </summary>
/// <param name="camera"></param>
void SetPerCameraShaderVariables(CommandBuffer cmd, Camera camera)
{
    cmd.SetGlobalVector("_WorldSpaceCameraPos", camera.transform.position);
}

SetPerCameraShaderVariables でカメラデータを設定しています。
今回はカメラの位置情報のみシェーダー側に渡しています。

cmd.SetGlobalVector("_WorldSpaceCameraPos", camera.transform.position);

シェーダーの実装

シェーダーではC#側で設定した _WorldSpaceCameraPos を使用して鏡面反射を実装しています。

前回のコードに鏡面反射の処理を追加します。
※コードは URP の実装を参考にしています。

Shader "Custom/Demo02_Specular_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
        _SpecularBrightness("Specular Brightness", Float) = 2.0

        // 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;
                float3 normal : NORMAL;
            };

            struct Varyings
            {
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : TEXCOORD1;
                float3 viewDir : TEXCOORD2;
            };

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

            float4x4 unity_ObjectToWorld;
            float4x4 unity_MatrixVP;

            float4 _MainLightPos;
            half4 _MainLightColor;

            float4 _WorldSpaceCameraPos;

            float _SpecularBrightness;

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

            float3 TransformObjectToWorldNormal(float3 normalOS) 
            {
                return mul(normalOS, (float3x3)unity_ObjectToWorld);
            }

            float3 GetWorldSpaceViewDir(float3 positionWS) 
            {
                return _WorldSpaceCameraPos.xyz - positionWS;
            }

            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);
                o.normal = TransformObjectToWorldNormal(i.normal);
                o.viewDir = GetWorldSpaceViewDir(positionWS);

                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;
                
                // 拡散反射(ランバート反射モデル)
                half3 lightDir = normalize(_MainLightPos.xyz);
                half3 normal = normalize(i.normal);
                half dotNL = saturate(dot(normal, lightDir));

                // 鏡面反射(フォン反射モデル)
                half3 viewDir = normalize(i.viewDir);
                half3 r = 2.0 * normal * dot(normal, lightDir) - lightDir;
                half specular = pow(saturate(dot(r, viewDir)), _SpecularBrightness);
                color.rgb *= dotNL + specular;

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

光源方向の正反射ベクトルと視点ベクトルの内積を算出したのち、光沢度を表す _SpecularBrightness でべき乗することで、鏡面反射を表現しています。
そして、最終的に拡散反射光と合わせることで最終的なサーフェスの色としています。

// 鏡面反射(フォン反射モデル)
half3 viewDir = normalize(i.viewDir);
half3 r = 2.0 * normal * dot(normal, lightDir) - lightDir;
half specular = pow(saturate(dot(r, viewDir)), _SpecularBrightness);
color.rgb *= dotNL + specular;

ブリン-フォン反射モデル

フォン反射モデルは正反射ベクトルを計算するのに複数の内積や乗算を行うため、処理負荷を減らすために考え出されたモデルです。
「光源ベクトル」と「視点ベクトル」の中間ベクトルである「ハーフベクトル」と「法線ベクトル」との内積で算出することができます。

ブリン-フォン反射モデルの式

N・・・正規化法線ベクトル
L・・・光源ベクトル
V・・・視点ベクトル
H・・・ハーフベクトル
a・・・粗さ係数(物体の光沢度)


\begin{align*}
&H=\frac{L+V}{|L+V|}\\
&I=\bigl( N \cdot H \bigr)^a
\end{align*}

正反射ベクトルを求めるフォン反射モデルとは違い、精度は落ちてしまいますが、その代わりに処理負荷が軽減されます。
f:id:s-dango:20220122195752j:plain:h300

以下は実装部分になります。
フォン反射モデルのコードがブリン-フォン反射モデルに置き換わっています。

// 鏡面反射(ブリン・フォン反射モデル)
half3 viewDir = normalize(i.viewDir);
half3 halfLV = normalize(lightDir + viewDir);
half specular = pow(saturate(dot(normal, halfLV)), _SpecularBrightness);
color.rgb *= dotNL + specular;
実行サンプル

左:鏡面反射無し 中央:ブリン-フォン反射モデル 右:フォン反射モデル
f:id:s-dango:20220122195846p:plain:h300

おわりに

今回は反射モデルの基本である、鏡面反射モデルをSRPで実装しました。
ここまでみていただいてありがとうございます。

【Unity SRP】ランバート拡散反射モデルの実装

はじめに

前回実装した不透明オブジェクトに陰影をつけていきます。
s-dango.hatenablog.jp

最終的な表示画面
f:id:s-dango:20211212181118p:plain:h300

頻出する語句

ベクトル・・・・・・・向きと大きさを持つ量(速度や力など)
ベクトルの内積・・・・2つのベクトルにの計算によって得られる値
ベクトルの正規化・・・ベクトルの方向を維持しつつ大きさを1にすること
法線・・・・・・・・・3Dモデルにおいて、表面の一点に対して垂直なベクトル
陰影・・・・・・・・・光が当たらずにできる暗い部分(物体の裏側など、光が十分に届かずにできる陰と、他の物体に遮られてできる影がある)

目次

反射モデルとは

3Dグラフィックスにおいて、現実世界の光の挙動を表現するための計算式のことです。
モデリングされた物体のサーフェス(表面)の一点に陰影つけるために使用します。

拡散反射光

物体の陰を表現することができます。

特徴
  • 光源から向きが離れていくほど暗くなっていく
  • 視点によって色や大きさは変化しない
ランバート反射モデル

拡散反射光の挙動を表現するための計算モデルの一つです。
サーフェスの「法線ベクトル」と「光源方向ベクトル」との内積で算出することができます。

ランバート反射モデルの式

N・・・正規化法線ベクトル
L・・・光源ベクトル
C・・・拡散反射光の色
I_l・・・入射光の輝度


\begin{align*}
&N \cdot L = |N||L|cos\alpha \\
&I = \bigl( N \cdot L \bigr) \times C \times I_l
\end{align*}

最終的に正規化法線ベクトルと光源ベクトルのなす角θが算出され、この値がサーフェスの輝度となります。
\theta が 0.0 になればなるほど、サーフェスの輝度が 1.0 に近づいていきます。
f:id:s-dango:20211212235740j:plain:h300

実行サンプル

左がライティング無し、右がランバート反射を適用したモデルです。
f:id:s-dango:20211212235959p:plain:h300

ライトの実装

ランバート拡散反射を実装するためには、まずライトの方向ベクトルを取得する必要があります。
以下の実装では、Hierarchy内で一番明るいディレクショナルライトをメインライトとして、方向ベクトルをシェーダー側に渡しています。

前回のC#コードに追記する形になります。
※コードはURPの実装を参考にしています。

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

        // ライト情報の設定 <- ここでライトのセットアップを行っています
        SetupLights(context, cullResults);

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

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

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

    context.Submit();
}
void SetupLights(ScriptableRenderContext context, CullingResults cullResults)
{
    CommandBuffer cmd = CommandBufferPool.Get();

    ProfilingSampler sampler = new ProfilingSampler("Setup Light Contents");
    using (new ProfilingScope(cmd, sampler))
    {
        NativeArray<VisibleLight> visibleLights = cullResults.visibleLights;

        // メインライト検索
        int lightIndex = -1;
        float brightestLightIntensity = 0f;
        for (int i = 0; i < visibleLights.Length; i++)
        {
            VisibleLight visibleLight = visibleLights[i];
            Light light = visibleLight.light;

            if (light == null)
            {
                break;
            }

            if (visibleLight.lightType == LightType.Directional)
            {
                // 最も明るいディレクショナルライトを使用
                if (light.intensity > brightestLightIntensity)
                {
                    brightestLightIntensity = light.intensity;
                    lightIndex = i;
                }
            }
        }

        // シェーダーに渡すライトデータの作成
        if (lightIndex < 0)
        {
            return;
        }

        VisibleLight mainVisibleLight = visibleLights[lightIndex];
        Vector4 dir = -mainVisibleLight.localToWorldMatrix.GetColumn(2);
        Vector4 lightPos = new Vector4(dir.x, dir.y, dir.z, 0);

        cmd.SetGlobalVector("_MainLightPos", lightPos);
    }

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

シェーダーの実装

シェーダー側では、C#コード側で登録した _MainLightsPos プロパティを使用して、ランバート拡散反射を実装しています。
前回のシェーダーコードに追記する形になります。
※コードはURPの実装を参考にしています。

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;
                float3 normal : NORMAL;
            };

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

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

            float4x4 unity_ObjectToWorld;
            float4x4 unity_MatrixVP;

            float4 _MainLightPos;   // C#側で登録したプロパティを使用しています

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

            float3 TransformObjectToWorldNormal(float3 normalOS) 
            {
                return mul(normalOS, (float3x3)unity_ObjectToWorld);
            }

            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);
                o.normal = TransformObjectToWorldNormal(i.normal);

                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;
                
                // 正規化法線ベクトル・ライト方向ベクトルを使用して、ランバート拡散反射を実装しています
                half3 lightDir = normalize(_MainLightPos.xyz);
                half3 normal = normalize(i.normal);
                half dotNL = max(0, dot(normal, lightDir));
                color.rgb *= dotNL;

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

正規化法線ベクトル・ライト方向ベクトルを内積して、その算出結果(0.0~1.0)をサーフェスの輝度としています。

half3 lightDir = normalize(_MainLightPos.xyz);
half3 normal = normalize(i.normal);
half dotNL = max(0, dot(normal, lightDir));
color.rgb *= dotNL;

終結

シェーダーを設定したマテリアルをオブジェクトに適用すると、以下のような表示になると思います。
f:id:s-dango:20211212181118p:plain:h300

おわりに

今回は反射モデルの基本である、ランバート拡散反射モデルをSRPで実装しました。
ここまでみていただいてありがとうございます。
なにか間違い等あればご指摘いただけると嬉しいです。

【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で不透明オブジェクトの描画」を行いました。

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

Universal Render Pipeline Assetまとめ

Universal Render Pipeline Asset(以下URPアセット)の作成~各設定項目を簡単にまとめていきます。
詳しくは公式のUniversal Render Pipeline Assetにまとめられているので、こちらも参考にしてください。

目次

       
  1. URPアセットとは
  2. URPアセットの作成手順
  3. 作成されるアセットについて
  4. URPアセットの設定項目
  5. まとめ

1.URPアセットとは

  • URPの描画機能と品質の設定をおこなえる
  • SRPのRenderPipelineAssetを継承したスクリプト可能なオブジェクト

2.URPアセットの作成手順

Projectウィンドウの「Create」->「Rendering」->「Universal Render Pipeline」->「Pipeline Asset (Forward Renderer)」を選択することで、アセットを作成できます。

「UniversalRenderPipelineAsset」「UniversalRenderPipelineAsset_Renderer」という名前のアセットが作成されます。

3.作成されるアセットについて

UniversalRenderPipelineAsset

  • Universal Render Pipelineの描画機能と品質の設定をおこなえます
  • SRPのRenderPipelineAssetを継承したスクリプト可能なオブジェクトです
  • 描画設定の項目に作成したアセットを割り当てることで、組み込みのレンダーパイプラインからURPに切り替わります

UniversalRenderPipelineAsset_Renderer

  • UniversalRenderPipelineAssetに登録して使用します
  • ScriptableRendererDataを継承したForwardRendererDataクラスであり、URPのフォワードレンダリングパスを実行するためのアセットです
  • 作成されるレンダラーはデフォルトのForwardRendererです

URPアセットの設定項目

General

Renderer List

フィールド m_RendererDataList : ScriptableRendererData[]
デフォルト ForwardRenderer
説明
  • ForwardRendererDataクラスを登録する項目です
  • URPアセットで使用するすべてのレンダラーをここで設定できます
  • 0番目に設定されているレンダラーがデフォルトのレンダラーとして使用されます(SetDefaultを選択することでデフォルトのレンダラーを切り替えることができます)

Depth Texture

フィールド m_RequireDepthTexture : bool
デフォルト false
説明
  • シェーダ側で_CameraDepthTextureを使用できるようになります
  • シーン内のすべてのカメラにデフォルトで使用されます(カメラのインスペクタで個々の設定を反映可能です)

Opaque Texture

フィールド m_RequireOpaqueTexture : bool
デフォルト false
説明
  • シェーダ側で_CameraOpaqueTextureを使用できるようになります
  • すべての不透明(Opaque)なオブジェクト描画後のレンダリング結果が書き込まれます(旧パイプラインのGrabPassに相当する機能です)
  • シーン内のすべてのカメラにデフォルトで使用されます(カメラのインスペクタで個々の設定を反映可能です)

Opaque Downsampling

フィールド m_OpaqueDownsampling : DownSampling(enum)
デフォルト _2xBilinear
説明
  • Opaque Textureがtrueの時に設定可能です
  • Opaque Textureのダウンサンプリング方式を設定できます

Downsampling

名称 説明 方式
None ダウンサンプリングを行わず、カメラと同じ解像度でコピーされたテクスチャが作成されます  
_2xBilinear 1/2の解像度でコピーされたテクスチャが作成されます バイリニアフィルタリング
_4xBox 1/4の解像度でコピーされたテクスチャが作成されます ボックスフィルタリング
_4xBilinear 1/4の解像度でコピーされたテクスチャが作成されます バイリニアフィルタリング

Terrain Holes

フィールド m_SupportsTerrainHoles : bool
デフォルト true
説明
  • テレインホールの使用有無を設定できます
  • 無効にすることで、ビルド時にテレインホールシェーダが削除され、ビルド時間が短縮されます

Quality

HDR

フィールド m_SupportsHDR : bool
デフォルト false
説明
  • HDR(High Dynamic Range)の使用有無を設定できますす
  • 広範囲の照明効果を表現したい場合に使用します
  • 有効にすることでレンダーテクスチャのカラーフォーマットがHDRになります
  • ローエンドのハードウェアをターゲットにしている場合は無効にしておくことでパフォーマンスを向上できます
  • シーン内のすべてのカメラにデフォルトで使用されます(カメラのインスペクタで個々の設定を反映可能です)

Anti Aliasing(MSAA)

フィールド m_MSAA : MsaaQuality(enum)
デフォルト Disabled
説明
  • MSAA(Multi-Sample Anti-Aliasing)のサンプル数(Disabled, 2x, 4x, 8x)を設定できます
  • サンプル数が多いほどエッジが滑らかになりますが、同時に負荷も大きくなります
  • MSAAを使用しない場合や2DゲームでMSAAの計算が不要な場合は設定を無効(Disabled)にしておいた方がいいです
  • シーン内のすべてのカメラにデフォルトで使用されます(カメラのインスペクタで個々の設定を反映可能です)

Render Scale

フィールド m_RenderScale : float
デフォルト 1.0
説明
  • レンダーテクスチャの解像度を設定できます(デバイスの解像度ではない点に注意)
  • 値を下げるほどパフォーマンスは向上しますが、描画品質は低下します
  • 値を上げるほど描画品質が向上しますが、パフォーマンスが低下します
  • UIの表示はデバイスの解像度で表示されます

Lighting

Main Light

フィールド m_MainLightRenderingMode : LightRenderingMode(enum)
デフォルト PerPixel
説明
  • メインのDirefctional Lightの計算方式を設定(disabled, Per Pixel)できます
  • メインライトの設定は「Lighting Settings」の「Sun Source」で設定できます
  • Sun sourceの値がからの場合はシーン内で最も明るいDirectional Lightをメインライトとして設定されます

Cast Shadows

フィールド m_MainLightShadowsSupported : bool
デフォルト true
説明
  • ONにすることでメインライトの影を描画できます
  • OFFにすることでメインライトの影が描画されなくなります

Shadow Resolution

フィールド m_MainLightShadowmapResolution : ShadowResolution(enum)
デフォルト _2048
説明
  • メインライトから受ける影の解像度を設定します
  • 値を下げるほどパフォーマンスが向上しますが、影が粗くなります
  • 値を上げるほど影は詳細になりますが、パフォーマンスが低下します

Additional Lights

フィールド m_AdditionalLightsRenderingMode : LightRenderingMode(enum)
デフォルト PerPixel
説明
  • メインライト以外に追加するライトの計算方法(Disable, Per Vertex, Per Pixel)を設定します

LightRenderingMode

名称 説明
Double 追加ライトの計算を無効にします
Per Vertex 頂点ごとに計算をおこないます
Per Pixel ピクセルごとに計算をおこないます

Per Object Limit

フィールド m_AdditionalLightsPerObjectLimit : int
デフォルト 4
説明
  • メインライト以外に追加できるライトの最大数を設定できます

Cast Shadows

フィールド m_AdditionalLightShadowsSupported : bool
デフォルト false
説明
  • ONにすることで追加ライトの影を描画できます
  • OFFにすることで追加ライトの影が描画されなくなります

Shadow Resolution

フィールド m_AdditionalLightsShadowmapResolution : ShadowResolution(enum)
デフォルト false
説明
  • 追加ライトから受ける影の解像度を設定します
  • 値を下げるほどパフォーマンスが向上しますが、影が粗くなります
  • 値を上げるほど影は詳細になりますが、パフォーマンスが低下します

Shadows

Distance

フィールド m_ShadowDistance : float
デフォルト 50
説明
  • カメラからどれだけの距離の範囲に影を描画するかを設定できます
  • 値より先にいるオブジェクトの影は描画されません
  • 影の描画範囲が広がるほど大量のメモリを消費するので、広大なフィールド等では値を調整する必要があります(影の表示範囲が限定されている見下ろし型のゲームでも活用できます)

Cascades

フィールド m_ShadowCascades : ShadowCascadesOption(enum)
デフォルト NoCascades
説明
  • シャドウカスケードの設定ができます
  • シャドウマップの数を増やすほどパフォーマンスは低下します

ShadowCascadesOption

名称 説明
NoCascades 一つのシャドウマップを使用します(距離に応じてシャドウマップが切り替わることはありません)
Two Cascades 2つのシャドウマップを使用します
Four Cascades 4つのシャドウマップを使用します

Depth Bias

フィールド m_ShadowDepthBias : float
デフォルト 1.0
説明

Normal Bias

フィールド m_ShadowNormalBia : float
デフォルト 1.0
説明

Soft Shadows

フィールド m_SoftShadowsSupported : bool
デフォルト false
説明
  • ソフトシャドウの使用有無を設定できます
  • シャドウマップにスムーズなフィルタリングを適用することによって影を滑らかに表現できます

Post-processing

Grading Mode

フィールド m_ColorGradingMode : ColorGradingMode(enum)
デフォルト LowDynamicRange
説明
  • プロジェクトに使用するカラーグレーディングモード(LDR, HDR)を設定できます
  • この処理はトーンマッピングの後に実行されます

ColorGradingMode

名称 説明
LowDynamicRange
  • 通常のカラーグレーディングモードです
  • モバイル等のローエンド向けに使用します
HighDynamicRange
  • 高精度なカラーグレーディングモードです
  • コンシューマ等のハイエンド向けに使用します

LUT Size

フィールド m_ColorGradingLutSize : int
デフォルト 32
説明
  • カラーグレーディングに使用するLookup textureの値を設定できます
  • 値が大きいほど精度が上がりますが、パフォーマンスとメモリ使用量に負荷がかかります
  • 現状デフォルト値の32が最もバランスのとれた値になっているとのことです

Advanced

SRP Batcher

フィールド m_UseSRPBatcher : bool
デフォルト true
説明
  • SRP Batcherの使用有無を設定できます
  • ONにすることでCPUレンダリング速度を高速化できます
  • 同じシェーダを使用する大量のマテリアルがある場合に役立ちます

Dynamic Batching

フィールド m_SupportsDynamicBatching : bool
デフォルト false
説明

Mixed Lighting

フィールド m_MixedLightingSupported : bool
デフォルト true
説明
  • 混合照明の使用有無を設定できます
  • ONにすることでビルドに混合照明シェーダバリアントが含まれるようになります

Debug Level

フィールド m_DebugLevel : PipelineDebugLevel(enum)
デフォルト Disabled
説明
  • レンダーパイプラインが生成するデバッグ情報レベル(Disabled, Profiling)を設定できます

PipelineDebugLevel

名称 説明
Disabled ログの記録を無効にします
Profiling
  • レンダーパイプラインに詳細情報タグを提供させます
  • 詳細情報はFrameDebuggerで確認できます

Shader Variant Log Level

フィールド m_ShaderVariantLogLevel : ShaderVariantLogLevel(enum)
デフォルト Disabled
説明
  • ビルド完了時にコンソールパネルに表示するシェーダーストリッピングとシェーダーバリアントの情報レベル(Disabled, OnlyUniversalRPShaders, AllShaders)を設定できます

ShaderVariantLogLevel

名称 説明
Disabled ログの記録を無効にします
OnlyUniversalRPShaders すべてのURPシェーダの情報をログに記録します
AllShaders ビルド内のすべてのシェーダの情報をログに記録します

まとめ

URPアセットの各設定項目を簡単にまとめました。
URPの設定は従来のBuilt-in Render Pipelineの時とは違い、設定項目がアセットにまとめられているのでかなり扱いやすくなっていました。
これからもURPについて調べたことや気づいた点などあればまとめていくのでよろしくお願いします。
また本記事の内容にどこか間違い等あればご指摘いただけると嬉しいです。

参考記事

URPを覚えよう ~特徴からプロジェクト作成まで~

この記事はURPを勉強し始めた筆者のメモ書きみたいなものです

 

 

目次

 

 

はじめに

最近始動したプロジェクトでURPを使用することになりました。
実は前々から興味はあれど手をつけずにいたので、この機会にちゃんと勉強します。

 

1.Universal Render Pipline ( URP ) って何?

Universal Render Pipeline、通称URPは(長いのでURPで統一します)、Unity従来のレンダリングパイプラインである「Built-in」に変わる新しい機能として提供されているレンダリングパイプラインです。
URPはオープンソースとなっており、現在もGitHubにて開発が進められています。

  • UnityデフォルトのパイプラインであるBuilt-in Render Piplineの後継として採用されることが決まっている新しいレンダーパイプラインの事
  • 今まではLighht Weight Render Pipline、通称LWRPとして配布されていたが、unity2018以降、URPに改名

 

2.レンダリングパイプラインとは

レンダリングパイプラインとは、ゲーム空間上に配置したオブジェクトが最終的に画面に表示されるまでに行われる作業の工程の事です。

この工程で行われる各処理の事をPASSといい、パイプラインに渡されてきたオブジェクトは各PASSを通り、最終的に画面上に絵として表示されます。

PASSによって行われる処理の例

  • カリング処理
  • 陰影処理
  • ラスタライズ
  • ポストプロセス など

以下は、レンダリングパイプラインの作業工程のレ例図です。

【CEDEC2020 Unityの次の標準レンダリングパイプライン Universal Render Pipeline は何がどう変わるのか】より引用

f:id:s-dango:20210131055544j:plain

 

そして、このパイプラインの各PASSをカスタマイズできるようになったのが、次に説明するScriptable Render Piplineになります

 

3.Scriptable Render Pipline ( SRP ) とURP

Scriptable Render Pipeline、通称SRPは(長いのでSRPで統一します)、はUnity5から試験的に導入されることとなり、Unity2018から正式版として組み込まれることになりました。
前項で触れた、レンダリングパイプラインのPASSを自由に組み替えられるパッケージとなります。
ただ、SRP単体では何も表示されない状態なので、自分で全て組み立てる必要があります。

Unity公式マニュアルの説明によると、完全にカスタマイズ可能とのことです。

f:id:s-dango:20210131055541j:plain

SRP自体は「Package Manager」または開発中のGitHubからインポートすることで使用可能となります。
リスト一番上の「Core RP Library」がSRPのパッケージです。(ちなみに一つ下が本記事で説明している「URP」パッケージ)

f:id:s-dango:20210329182035j:plain

具体的な導入や使用方法は、また別の記事でやろうと思います。
興味がある方は勉強してみてもいいかもしれません。

SRPは自由度の高いパッケージですが、正直パイプラインを1から全て作成しようと思うと、かなりの時間と手間が必要になってきます。
そこでUnityさんが、最初から誰でも使えるように組み立ててくれた2つのパイプラインがあります。
その一つがこの記事で説明していくUniversal Render Pipeline ( URP ) です。

 

4.URPの特徴

以下にURPの特徴やURPでできることをまとめていきます

特徴

  • URPはモバイルからハイエンドなコンソールゲームまで、幅広い表現を可能としている
  • 「シングルパス・フォワードレンダリング」というレンダリング方式を採用しており、ドローコール数を抑える事が可能
  • カメラごとに「2D」「3D」のレンダラーを分けて使用することが可能(それぞれ実行されるパスが異なる)
    • Forward Renderer : シングルパス・フォワードレンダリング
    • 2D Renderer : 2D専用のレンダリング(この機能はURPのみに採用されている)
    • Custom Renderer : 独自にカスタムしたレンダラーに切り替えることも可能

URPで使用できるシェーダ

URPではビルトインに変わる標準シェーダとして以下が提供用意されています

  • Lit : 物理ベースのシェーダ
  • Simple Lit : 非物理ベースのシェーダ
  • Baked Lit : 動的なライティングを行わないシェーダ

ビルトインから用意されているシェーダでは、以下が使用可能となっています

  • Unlit : ライトの影響を受けないシェーダ

URPのデメリット

  • ビルトインの標準シェーダであった「Surface」が使用できないため、アセットストアなどからインポートしてきたアセットはURPに対応させる必要がある
  • 「Shadow Mask」「Point Light Shadow」などの影の表現が使えない
  • SSR」「Auto Exposure」「SSAO」などの一部のポストプロセスが使えないため、自作する必要がある 下2つについては、近いうちに実装されていく予定らしいのです。
    どうしても使いたい場合、今はまだ自作する必要があります。

URPの導入方法

新規プロジェクトを作成する場合

新規でプロジェクトを作成する場合は、作成画面の「Universal Render Pipline」を選択します。

f:id:s-dango:20210329182229j:plain

 

既存プロジェクトに導入する場合

既存のプロジェクトに導入する場合は、PackageManagerを使用します。

f:id:s-dango:20210329182335j:plain

 

パッケージのインポート後、Projectウィンドウから「Create」->「Rendering」->「Universal Render Pipeline」->「Pipline Asset(Forward Renderer)」を選択してアセットを作成します。
この時点で、Pipline Assetとそのアセットで使用するレンダラー用アセットの2つが作成されます。

f:id:s-dango:20210329182409j:plain

作成したPipeline Assetを、「Project Settings」->「Graphics」の「Scriptable Render Pipleline Settings」項目に設定します。

f:id:s-dango:20210329182445j:plain

シェーダとマテリアルについて

【2.URPで使用できるシェーダ】でも説明しましたが、URPではBuilt-inで使用されていた「Surface Shader」が使えません。
なのでマテリアルの設定にはURP用のシェーダを設定する必要があります。(Standardを使用するとUnityユーザーさんおなじみピンクモデルが表示されます)

f:id:s-dango:20210329182623j:plain
f:id:s-dango:20210329182619j:plain

 

URPは使うべきか

近いうちにBuilt-inから完全に置き換わる事を考えると、これから新規で動くプロジェクトはURPを使用したほうがよさそう
(公式によると、Unity2021を目標にBuilt-inの上位互換となる予定らしい)

適している開発

  • これから新しく動くプロジェクト
  • モバイル・コンソール・PCまで幅広く展開したい人
  • フォトグラフィックな表現を必要としない開発
  • アニメ調など、独自の表現を行いたい人
  • 「Shader Graph」「VFX Graph」といったアーティスト向けツールを使用したい人

 

5.まとめ

URPとは

  • SRPをベースとした拡張可能なレンダリングパイプライン
  • 高速で、幅広い表現が可能

URPで何が変わるのか

  • パフォーマンスの向上
  • レンダラーを使った、少し変わった表現が可能になる

URPを拡張するには

  • アセットストアで探す
  • 頑張って自作

以上となります。
今回はURPとは何なのか~URPの導入までを書きました。
間違っている箇所等があれば、コメント欄にて指摘していただけると嬉しいです。
今後も調べた事や勉強したことなどを記事にしていくので、よろしくお願いします。

参考サイト

【Unity】Unityの次の標準レンダリングパイプライン Universal Render Pipeline は何がどう変わるのか
https://learning.unity3d.jp/4966/

【Unity】軽量レンダーパイプラインからユニバーサルレンダーパイプラインへ
https://blogs.unity3d.com/jp/2019/09/20/how-the-lightweight-render-pipeline-is-evolving/

【Unity】スクリプタブルレンダーパイプラインの概要
https://blogs.unity3d.com/jp/2018/01/31/srp-overview/

【Unity】Universal Render Pipeline overview
https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@10.3/manual/index.html

【npaka 様】Unityのレンダーパイプライン
https://note.com/npaka/n/n5c7c11512999

参考書籍

染井吉野ゲームズ 様】Unityシェーダープログラミングの教科書4 SRP[1]UniversalRP/Litシェーダー解説編