【URP / iPhoneX】半透明描画の負荷 と カメラテクスチャブレンド負荷の比較

2024/05/21に公開

はじめに

オブジェクトを半透明で描画させたい時に、二つの方法があります。

  1. Blendingを利用して、半透明描画する
  2. カメラのカラーテクスチャをシェーダー内で合成する

どちらの方が負荷が高くなるのか気になったので、Instrumentsを利用して計測してみることにしました。

環境

  • Unity2022.3.23f1
  • URP14.0.10

シーンのセットアップ

計測のためにUnlit Colorシェーダーを適用したSphereを画面に配置したシーンを用意しました。

計測1 : 半透明ブレンド

カメラの正面にQuadを配置し、半透明ブレンドを行うシェーダーを適用します。

シェーダー

半透明ブレンドを行うシェーダーを使用します。

Blend SrcAlpha OneMinusSrcAlpha
シェーダーコード (Transparent.shader)
Transparent.shader
Shader "Test102/Transparent"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100
        
        Blend SrcAlpha OneMinusSrcAlpha
        ZTest Always

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

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

            struct appdata
            {
                float4 positionOS : POSITION;
            };

            struct v2f
            {
                float4 positionCS : SV_POSITION;
            };


            CBUFFER_START(UnityPerMaterial)
            half4 _Color;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.positionCS = TransformObjectToHClip(v.positionOS);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                return _Color;
            }
            ENDHLSL
        }
    }
}

計測結果

iPhoneX上でのGPU負荷は以下のような値となりました。

fragment(ms) vertertex(ms)
3.78 1.47

計測2 : CameraOpaqueTextureを合成する

CameraOpaqueTexture シェーダー内で合成することを考えます。

シェーダー

Blend One Zero

half4 frag (Varyings i) : SV_Target
{
    const float2 screenPos = i.screenPosition.xy / i.screenPosition.w;
    
    half4 color;
    color.rgb = lerp(SampleSceneColor(screenPos).rgb, _Color.rgb, _Color.a);
    color.a = 1;
    return color;
}
シェーダーコード (BlendSceneColor.shader)
BlendSceneColor.shader
Shader "Test102/BlendSceneColor"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 100
        
        Blend One Zero
        ZTest Always

        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareOpaqueTexture.hlsl"
            
            struct appdata
            {
                float4 positionOS : POSITION;
            };

            struct v2f
            {
                float4 positionCS : SV_POSITION;
                float4 screenPosition : TEXCOORD0;
            };


            CBUFFER_START(UnityPerMaterial)
            half4 _Color;
            CBUFFER_END

            v2f vert (appdata v)
            {
                v2f o;
                o.positionCS = TransformObjectToHClip(v.positionOS);
                o.screenPosition = ComputeScreenPos(o.positionCS);
                return o;
            }

            half4 frag (v2f i) : SV_Target
            {
                const float2 screenPos = i.screenPosition.xy / i.screenPosition.w;
                
                half4 color;
                color.rgb = lerp(SampleSceneColor(screenPos).rgb, _Color.rgb, _Color.a);
                color.a = 1;
                return color;
            }
            ENDHLSL
        }
    }
}

計測結果

CameraOpaqueTextureのDownSamplingを変えた時の負荷は以下のようになりました。

DownSampling fragment(ms) vertertex(ms)
None 6.98 2.43
2x Bilinear 6.39 2.31
4x Box 6.34 2.59
4x Bilinear 6.3 2.51


Downsampling

計測3 : CameraOpaqueTexture有効化 + Transparentシェーダー

Case2の計測結果には、CameraOpaqueTexture生成負荷が入っているため、
純粋にシェーダーのブレンディングの負荷の比較とはなりません。

そこで、CameraOpqaueTextureを有効にした状態で、Transparent.shaderを使用した時の負荷を計測してみます。
CameraOpqaueTextureは描画されますが、シェーダー内では使用されない状態です。

計測結果

DownSampling fragment(ms) vertertex(ms)
Downsampling : None 7.01 2.51
Downsampling : 2x Bilinear 6.55 2.3
Downsampling : 4x Box 6.57 2.5
Downsampling : 4x Bilinear 6.49 2.45

計測結果

計測結果を一つにまとめてみたものは以下になります。

fragment (ms) vertex (ms) fragment + vertex
Opaque None 6.98327 2.43479 9.41806
Opaque_2xBilinear 6.38886 2.309 8.69786
Opaque_4xBox 6.33768 2.59228 8.92996
Opaque_4xBilinear 6.29805 2.51337 8.81142
Transparent_None 7.01027 2.51285 9.52312
Transparent_2xBilinear 6.55254 2.29557 8.84811
Transparent_4xBox 6.56381 2.4936 9.05741
Transparent_4xBilinear 6.48919 2.45124 8.94043

まとめ

  • CameraOpaqueTextureを無効にして半透明描画するのが最も軽い
  • CameraOpaqueTexutreを有効化すると、fragment負荷が2.7 ~ 3.3 ms ほど負荷が上がる
  • CameraOpaqueTextureが有効な場合では、半透明描画より不透明描画の方が軽い
    • 負荷の差分 : 0.03 ~ 0.19 ms
  • 不透明描画かつ Downsampling を 2x Bilinear にしたケースが シェーダー負荷(Vertex+Fragment)が最も軽かった

おまけ : より詳細な比較

負荷のどこが増加しているのか気になったので、GPUカウンタを計測してみました。

GPUカウンタの値
fragment (ms) vertex (ms) fragment + vertex F32 Utilization F16 Utilization Texture Sample Limiter Texture Filtering Limiter Texture Cache Limiter Texture Write Limiter Buffer Read Limiter Buffer Write Limiter Threadgroup/Imageblock Load Limiter Threadgroup/Imageblock Store Limiter Fragment Input Interpolation Limiter GPU Last Level Cache Limiter Vertex Occupancy Fragment Occupancy Compute Occupancy GPU Read Bandwidth GPU Write Bandwidth MMU Limiter MMU Utilization Partial Renders Count
Opaque_None 6.98327 2.43479 9.41806 3.45764 0.03034 16.53634 27.14459 28.57466 52.41252 0.04555 0.00262 0.0184 18.70876 8.11922 64.08297 0.00761 45.52817 0 5.6927 8.97749 71.89764 35.44055 0
Opaque_2xBilinear 6.38886 2.309 8.69786 3.46131 0.03433 16.22136 21.57864 20.8543 48.13993 0.0504 0.00289 0.02131 16.50318 7.57701 63.23955 0.00878 40.68932 0 4.68195 8.74706 63.46532 32.44388 0
Opaque_4xBox 6.33768 2.59228 8.92996 4.33695 0.03358 17.98042 23.05799 21.64522 46.12128 0.05022 0.00276 0.02274 15.92012 7.06557 59.33887 0.00862 42.31889 0 4.79311 8.25396 60.02068 31.65525 0
Opaque_4xBilinear 6.29805 2.51337 8.81142 3.64138 0.03351 17.39104 21.35605 21.34439 46.24221 0.04869 0.00278 0.02176 15.78942 7.9389 60.45347 0.00816 43.00796 0 4.7833 8.17352 59.63258 31.44311 0
Transparent_None 7.01027 2.51285 9.52312 4.28611 0.76193 18.17287 25.89068 29.17183 53.42498 0.04531 0.00251 3.88581 22.52397 5.73857 63.56375 0.00746 52.8083 0 5.40715 9.04245 70.63753 34.83334 0
Transparent_2xBilinear 6.55254 2.29557 8.84811 4.38791 0.78213 17.00159 20.95917 23.15491 47.81116 0.05001 0.00286 3.97362 20.30112 5.02193 64.80396 0.00868 48.6866 0 5.42256 8.58455 67.07069 33.84298 0
Transparent_4xBox 6.56381 2.4936 9.05741 5.14535 0.82424 18.02937 21.69801 23.97855 46.37299 0.04924 0.00269 4.20084 20.15665 4.1413 63.34396 0.00842 49.61799 0 5.59458 8.3932 64.96178 33.75735 0
Transparent_4xBilinear 6.48919 2.45124 8.94043 4.42764 0.78846 16.75304 19.82471 23.0775 46.75546 0.05016 0.00278 4.00472 19.77895 5.00538 64.83689 0.00868 48.28427 0 5.42299 8.41546 63.7453 33.41775 0

不透明描画かつ、Downsampling を 2x Bilinearにしたときが Vertex と Fragment を合わせた時の負荷が最も小さくなっていました。

グラフ(GPUカウンタ)

大きな変化が見られたGPUカウンタをグラフにまとめてみました。

メモリアクセス

半透明描画は、不透明描画と比べてタイルメモリへのアクセス量が増えていました

  • Imageblock Load Limiter
  • Imageblock Store Limite
  • MMU Limiter
  • MMU Utilization

テクスチャキャッシュ

半透明描画は、不透明と比べてテクスチャキャッシュの数値も増えていました

  • Texture Cache Limiter

Texture read cache
Measures the time the GPU spends in the texture read cache.
https://developer.apple.com/documentation/xcode/reducing-shader-bottlenecks

グラフ(Bandwidth)

半透明描画は、Bandwidthも多く消費していました。

  • GPU Read Bandwidth
  • GPU Write Bandwidth

Downsampling無しの場合、不透明描画と半透明描画はBandwidthがほぼ同じですが、
テクスチャ解像度を下げると不透明描画の方のBandwidthが、半透明描画のものよりも低くなることが確認できます。

関連 (GPUカウンタ)

https://developer.apple.com/documentation/xcode/reducing-shader-bottlenecks

Discussion