【Unity】シェーダーの半透過ブレンドモードに関する考察

# はじめに
Unity Recorderではカメラのレンダリングの結果を画像や映像素材として出力することができます。このとき半透過のオブジェクトがある場合、出力されるレンダリング結果はシェーダーのブレンドモードで挙動が変化します。ここではUnity Recorderと半透過にまつわるブレンドモードについて考察を行っていきます。


# ブレンド係数
UnityのShaderLabにはシェーダーで出力する色と既に描画されているフレームバッファとの色の混ぜ具合(ブレンドモード)を指定できるコマンドがあります。

```
Blend SrcFactor DstFactor
Blend SrcFactor DstFactor, SrcFactorA, DstFactorA
```
第二引数を指定することで色とアルファそれぞれのブレンドモードを指定が可能

https://docs.unity3d.com/ja/2019.4/Manual/SL-Blend.html

SrcFactorとDstFactorには以下の係数を乗算指定することができます。
| 係数 | 機能 |
|:-:|:-:|
| Off  | ブレンドの無効化 |
| One  | 1 |
| Zero | 0 |
| SrcColor | シェーダーのColor  |
| SrcAlpha | シェーダーのAlpha  |
| DstColor | フレームバッファのColor |
| DstAlpha  | フレームバッファのAlpha |
| OneMinusSrcColor | 1 - Source Color |
| OneMinusSrcAlpha | 1 - Source Alpha |
| OneMinusDstColor | 1 - Destination Color |
| OneMinusDstAlpha | 1 - Destination Alpha |

これらの係数を組み合わせることで最終的な出力値が決まります。

```
finalValue = sourceFactor * sourceValue operation destinationFactor * destinationValue
```

- sourceValue: シェーダーで計算した色
- destinationValue: フレームバッファの色
- opreraton: ブレンドの操作方法

# ブレンド操作
```
BlendOp OpColor
BlendOp OpColor, OpAlpha // ColorとAlpha別々で指定する場合
```
ブレンド係数と同様に第二引数を指定することでColorとAlphaそれぞれのoperationを指定できます。operationの代表的なものとして以下があります。
| operation | 機能 |
|:-:|:-:|
| Add | sourceとdestinationを加算(デフォルト) |
| Sub | sourceからdestinationを減算 |
| RevSub | destinationからsourceを減算 |
| Min | sourceとdestinationの小さい方を使用 |
| Max | sourceとdestinationの大きい方を使用 |

特にBlendOpを指定しない場合はAddが適用され、最終的な出力式は以下のようになります。
```
finalValue = sourceFactor * sourceValue + destinationFactor * destinationValue
```


# ブレンドモードによる半透過描画
ブレンド操作をおさらいしたところで早速、ブレンドモードの組み合わせをみていきます。以下のシェーダーを使ってオブジェクトをシーン上に適当に配置します。

```hlsl
Shader "Unlit/BlendOp"
{
    Properties
    {
        _Color ("Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent"}
        Blend SrcAlpha OneMinusSrcAlpha
//        Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha
//        Blend SrcAlpha OneMinusSrcAlpha, One One
//        BlendOp Add, Max
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
            };

            fixed4 _Color;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                return _Color;
            }
            ENDCG
        }
    }
}

```

#### GameView
![GameView_SrcAlpha_OneMinusSrcAlpha_One_One.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/ec52a267-4a6e-12f0-e440-c40064c5305a.png)

#### SceneView
![スクリーンショット-2024-12-22-220922.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/7f08d196-5f04-f6be-3d29-e26be6046c5b.jpeg)

カメラを基準に手前から奥に①→②→③→④の順に配置しています。Alpha値はそれぞれ0.4とし、黄色のみ1.0(不透過)としてあります。括弧は(R, G, B, A)


# Blend SrcAlpha OneMinusSrcAlpha
一般的な半透過のブレンドモードで、ColorとAlphaは同じ式でブレンドされます。
(AlphaはSrcAlpha * sourceValueの項があるため線形補間とならない)
```
finalValue = SrcAlpha * sourceValue + (1 - SrcAlpha) * destinationValue
```

#### GameView
![GameView_SrcAlpha_OneMinusSrcAlpha.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/98356129-6806-27de-dc43-9c2a7d9edef2.png)
描画は問題なし

#### Unity Recorder PNG出力
![ps_SrcAlpha_OneMinusSrcAlpha.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/b1cee7d8-1a5c-1691-f173-8ab9af5c697f.jpeg)

重なっている部分のAlpha値を計算してみます。

```math
1枚:0.4\times 0.4+( 1-0.4) \times 0=0.16\ (16\%)
```
```math
2枚:0.4\times 0.4+( 1-0.4) \times 0.16 \fallingdotseq 0.25\ (25\%)
```
```math
3枚:0.4\times 0.4+( 1-0.4) \times 0.25 \fallingdotseq 0.31\ (31\%)
```
```math
1枚+不透明:0.4\times 0.4+( 1-0.4) \times 1 = 0.76\ (76\%)
```

出力されたPNG画像をPhotoShop等で開いてAlpha値を確認すると計算値と一致していました。`Blend SrcAlpha OneMinusSrcAlpha`のときは不透過と重なる部分が透けてしまっていることがわかります。


# Blend SrcAlpha OneMinusSrcAlpha / BlendOp Add, Max
半透過のブレンドモードに加え、Alphaはsourceとdestinationの大きい方を使用するパターンです。
```
finalValue = SrcAlpha * sourceValue + (1 - SrcAlpha) * destinationValue
BlendOp Add, Max
```

![GameView_SrcAlpha_OneMinusSrcAlpha_BlendOp_Add_Max.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/d459d734-c671-9f99-3a34-0e04897c24d6.png)
描画は問題なし

![ps_SrcAlpha_OneMinusSrcAlpha_BlendOp_Add_Max.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/6e2e16b5-4719-35c6-2dfc-4ae1cd1bbf53.jpeg)

```math
0.4 = 0.4 \rightarrow 40\%
```
```math
0.4 < 1.0 \rightarrow 100\%
```

不透過と重なる部分は大きいAlpha値が優先されるため透けずに出力されていることがわかります。ただし、半透過で重なる部分は全て同じ値Alpha値となっています。(半透過の3枚は同じAlpha値のため)



# Blend SrcAlpha OneMinusSrcAlpha, One One
Colorは線形補間、AlphaはsourceAlphaとdestinationAlphaの加算でブレンドします。
```
finalColor = SrcAlpha * sourceValue + (1 - SrcAlpha) * destinationValue
finalAlpha = 1 * sourceAlpha + 1 * destinationAlpha
```

![GameView_SrcAlpha_OneMinusSrcAlpha_One_One.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/8e785777-abcb-fe3e-aa74-fccf8060284c.png)
描画は問題なし

![ps_SrcAlpha_OneMinusSrcAlpha_One_One.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/ef61a2a7-5a7e-45d8-8d58-6decf049fbd3.jpeg)

```math
1枚:1 \times 0.4 + 1 \times 0 = 0.4\ (40\%)
```
```math
2枚:1 \times 0.4 + 1 \times 0.4 = 0.8\ (80\%)
```
```math
3枚:1 \times 0.4 + 1 \times 0.8 = 1.2 \rightarrow 1.0\ (100\%)
```
```math
1枚+不透明:1 \times 0.4 + 1 \times 1.0 = 1.4 \rightarrow 1.0\ (100\%)
```

3枚重なる部分と不透過と半透過が重なる部分のAlpha値は1を超えますが1でクリップされます。


# Blend SrcAlpha OneMinusSrcAlpha, One OneMinusSrcAlpha
ColorとAlphaをSrcAlphaで線形補間します。
```
finalColor = SrcAlpha * sourceValue + (1 - SrcAlpha) * destinationValue
finalAlpha = 1 * sourceAlpha + 1 * destinationAlpha
```
![GameView_SrcAlpha_OneMinusSrcAlpha_One_OneMinuSrcAlpha.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/180bccd2-dd91-6206-2ab5-762c65c370cd.png)
描画は問題なし

![ps_SrcAlpha_OneMinusSrcAlpha_One_OneMinuSrcAlpha.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70925/1850d5e8-87cb-1b89-9c2c-641a48f2fa60.jpeg)

```math
1枚:1\times 0.4+( 1-0.4) \times 0=0.4\ (40\%)
```
```math
2枚:1\times 0.4+( 1-0.4) \times 0.4 = 0.64\ (64\%)
```
```math
3枚:1\times 0.4+( 1-0.4) \times 0.64 \fallingdotseq 0.78\ (78\%)
```
```math
1枚+不透明:1\times 0.4+( 1-0.4) \times 1 = 1.0\ (100\%)
```


# まとめ
今回試したブレンドモードのパターン間で描画の差はありませんでしたが、Unity Recorderの出力結果には差が確認できました。どのブレンドモードが正しいというものではなく、求める用途や表現によってブレンドモードを適切に使い分けることが大切であると言えるでしょう。

Unity内で完結する描画ではあまり半透過のブレンドはそこまで意識する機会は少なそうですが、Unityの描画出力を素材として扱う場合は重要になっていることが知見として得られました。もし半透過ブレンドでお困りの際はこの記事が少しでも参考になれば幸いです。🌱


# 参考リンク
https://docs.unity3d.com/ja/2019.4/Manual/SL-Blend.html
https://docs.unity3d.com/Manual/SL-Blend.html
https://github.com/Santarh/MToon/issues/10
https://discussions.unity.com/t/dstalpha-alpha-blendmode-alpha-blendop/694629/2