【Unity】Shaderの半透明とDstAlphaを調べてみた

## はじめに
Shaderのブレンド設定では出力したAlphaを__SrcAlpha__、背景側のAlphaを__DstAlpha__と書きます。
半透明はShaderの出力と背景の色をAlpha値の比で合成していて、ほとんどは__SrcAlpha__を使用しています。
調べている過程で__DstAlpha__に興味を持ったので調べてみました。
この記事の前半はよく見かけるShaderの__半透明__について、後半は__DstAlpha__について説明していきます。
もしまちがっている所がありましたらやさしく教えていただければと思います。
※記事を書いた時のUnityのバージョンは2020.1.17f1になります。

## 半透明表示の仕組みについて（前半）
半透明はかなり幅広い知識が必要で詳しくない方に説明するのは難しいですが、なるべく全体をふんわりと多くの方が理解できることを目指して説明していきます。
画面表示の仕組みをモニター出力側から__逆順__に見ていくと理解がしやすいと思います。

#### フレームバッファ
各オブジェクトから複数のShaderの処理を通してフレームバッファを更新しています。
フレームバッファはピクセルの集合体のデータでそれぞれの__RGBA__チャンネルを持ち、値は`0～1`の範囲になります。
フレームバッファに書き込まれた内容はモニターの更新タイミングに合わせて画面に表示されます。
※カメラの設定によって最終出力はモニターではなくレンダーテクスチャに出力する場合もあります。

#### Shader（フォワードレンダリング）
Shaderには順番に処理する`レンダリングパイプライン`というものが存在します。
いくつかの__ステージ__に処理が分かれていて順番にShaderに書かれた内容で__GPU__が実行していきます。
簡単にですがそれぞれの流れを説明しておきます。

<img width=30% alt="ForwardRendaringPipline.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316385/41bcda95-3640-2efc-0f4a-e6c818fb1d13.png">

1. 頂点シェーダー
__各ポリゴンの頂点ごとに実行__し、頂点位置をクリップ空間座標に変換して出力します。
この処理はShaderにプログラムを書いて実行します。

1. ラスタライズ
ラスタライズ処理でポリゴンの位置から描画するピクセルが割り当てられます。
この処理はGPUが自動で行います。

1. フラグメントシェーダー（ピクセルシェーダー）
__各ピクセルごとに実行__し、RGBAの各チャンネルの色データを出力します。
この処理はShaderにプログラムを書いて実行します。

1. ステンシルテスト
ステンシルバッファと設定した条件で比較して条件を満たせば出力します。

1. 深度テスト
デプスバッファとカメラまでの距離を設定した条件で比較して条件を満たせば出力します。

1. ブレンディング
Shaderで出力された色とフレームバッファをブレンドの設定で計算して出力します。

ステンシル、深度テスト、ブレンディングは出力マージャステージと呼ばれていて、それぞれレンダリングステートで設定します。
半透明の描画は出力マージャステージのブレンディングの設定が重要になります。

#### ブレンディングの設定と計算式
ブレンディングでShaderから`出力した色`と`現在のフレームバッファの色`をどのように合成するかはブレンド設定の組み合わせによって計算式が変わります。
よくあるブレンド設定の書き方は__Factor__が__２つ__で下記のように書きます。

```
Blend [SrcFactor] [DstFactor]
```
__Factor__はそれぞれ後で紹介するリストから選択します。
さらに計算式そのものを決めている`BlendOp`という設定もあります。
下記のように書きます。

```
BlendOp [Op]
```
ほとんどの場合は省略されていて`BlendOp Add`の設定になっています。
__BlendOp__の__Op__が`Add`のブレンドの計算式は下記になります。
`出力.rgba = (SrcColor.rgba  * SrcFactor) + (DstColor.rgba * DstFactor)`
__SrcColor__はShaderの出力で、__DstColor__は現在のフレームバッファです。
この計算式の__Factor__に代入してフレームバッファを更新しています。

上記の他にあまり使われない書き方ですが、__RGB__と__A__の計算を別にする書き方もあります。
__Factor__が__４つ__で下記のように書きます。
この記事ではこちらを使います。

```
Blend [SrcFactor] [DstFactor],[SrcFactorA] [DstFactorA]
```
__左2つのFactor__は__RGB__に対して、__右2つのFactor__は__A__に対しての設定になります。
__BlendOp__にも__RGB__と__A__の値を別の計算式にする書き方があります。

```
BlendOp [OpColor],[OpAlpha]
```

これを`BlendOp Add,Add`と書いた場合の計算式は下記になります。
`出力.rgb = (SrcColor.rgb  * SrcFactor.rgb) + (DstColor.rgb * DstFactor.rgb)`
`出力.a = (SrcColor.a * SrcFactorA.a) + (DstColor.a * DstFactorA.a)`

式に代入される__Factor__や__BlendOp__の詳細は以下を参照してください。
#### BlendのFactorの内容
 No  | Factor | Factorの内容
:-------:|:---------|:------
0 | Zero | 0
1 | One | 1
2 | DstColor | フレームバッファの色
3 | SrcColor   | Shader出力の色
4 | OneMinusDstColor | ( 1 - フレームバッファの色 )
5 | SrcAlpha | Shader出力のアルファ
6 | OneMinusSrcColor | ( 1 - Shader出力の色 )
7 | DstAlpha | フレームバッファのアルファ
8 | OneMinusDstAlpha | ( 1 - フレームバッファのアルファ )
9 | SrcAlphaSaturate | min(Shader出力のアルファ, 1 - フレームバッファのアルファ )
10 | OneMinusSrcAlpha | ( 1 - Shader出力のアルファ )

#### BlendOpのOpの内容
 No  | Op | ブレンド式の内容
:-------:|:---------|:------
0 | Add | (SrcColor * SrcFactor) + (DstColor * DstFactor)
1 | Subtract | (SrcColor * SrcFactor) - (DstColor * DstFactor)
2 | ReverseSubtract | (DstColor * DstFactor) - (SrcColor * SrcFactor)
3 | Min | min(SrcColor,DstColor) ※BlendのFactorは使用しない
4 | Max | max(SrcColor,DstColor) ※BlendのFactorは使用しない

※BlendOpはAdd、Subtract、ReverseSubtract、Min、Max以外にもありますが__DX11.1のみサポート__されており、環境によっては設定をしてもAddと同じになります。この記事では省略させて頂きます。

#### 半透明に使う画像について
よく使われるブレンド方法の計算式を説明した上で扱う画像について説明しておきます。
まず__アルファブレンド__といわれるよく使われる合成方法の計算は下記になります。
`フレームバッファへ出力 = (Shader出力 * Alpha) ＋ (現在のフレームバッファ * (1-Alpha))`
合計が１になる値をAlpha値で分けてそれぞれをShader出力とフレームバッファに乗算している計算です。

これに対して__プリマルチプライドアルファ（事前乗算アルファ）__と呼ばれる画像形式があります。
ブレンドする時の計算式は下記を使用します。
`フレームバッファへ出力 = Shaderの出力 ＋ (現在のフレームバッファ * (1-Alpha))`
__アルファブレンド__と比べると`(Shaderの出力 * Alpha)`のところが`Shaderの出力`になっていて事前にAlphaを乗算してある場合に合わせて省略されています。
テクスチャを作成するときに計算しておいて処理を軽くするなどの目的があります。
プリマルチプライドアルファのテクスチャを間違えてアルファブレンドの式で合成するとAlpha値を２回乗算してしまうので色が暗くなってしまいます。

#### 深度バッファ
カメラから描画するピクセルへの距離を深度バッファに書いておき、条件を満たすか判定してフレームバッファを更新するか決定する仕組みです。
不透明オブジェクトの描画をスキップして処理を軽くするなどにも利用されてます。
描画順と深度バッファの設定がかみ合っていないとおかしな描画の原因になります。

#### Stencilバッファ
Stencilバッファに`0～255`の整数の値を入れておき、値が条件を満たすか判定してフレームバッファを更新するか決定できる仕組みです。
深度バッファと似てますが、こちらは値をStencil自身で設定するのでマスク処理などに利用されてます。

#### clip()関数
フラグメントシェーダー内で使用する関数で`clip(x)`と書くと引数の`x`が`0`より小さい値の時はそのピクセルの描画をスキップします。
描画するかしないかの二択になるので不透明オブジェクトで画像をくり抜いて表示するような処理に使われています。
半透明と合わせると縁が汚くなるので少し扱いにくいところがあります。

#### 描画順
Shaderの__Render Queue__が2500以下の値は__不透明オブジェクト__、2500より大きい値は__透明オブジェクト__と扱われます。
__不透明オブジェクト__はカメラから見て一番手前を先に描画し、するその後ろは表示されないので処理を省略します。
無駄なShaderの処理を減らすのに都合がいいので手前から描画する順番にソートされます。
__透明オブジェクト__は背景に重ねて描画する必要があるので奥から描画する順番にソートされます。
他に__Sprite__は__SortingLeyer__や__OrderinLeyer__などでユーザーが好きなように描画順を入れ替える仕組みもあります。
__uGUI__は奥行など条件が同じであればヒエラルキーが親の上から順に描画されます。
半透明がおかしな描画になる原因のほとんどは描画順が間違っていることが多い印象です。
描画順がよくわからない時は__フレームデバッガー__を使うと確認できます。

## DstAlphaを使った半透明（後半）
ここまでは半透明を扱う際に必要な知識の説明でした。
ここからがこの記事の本番です。
あんまり使われていない__DstAlphaを使った半透明__について説明していきます。

#### DstAlphaについて
DstAlphaはフレームバッファに書き込まれた背景側のAlpha値のことです。
モニタ表示に使用される色は実際にはフレームバッファのRGBチャンネルのみで描画されていて、Aチャンネルは描画に影響していません。
Shaderのブレンディング時に使用する値なのですが、現状ではほぼ未使用なようでUnityの公式ページのブレンド設定を見てもDstAlphaはほとんど使用されていないことがわかります。
`Blend SrcAlpha OneMinusSrcAlpha // アルファブレンド`
`Blend One OneMinusSrcAlpha // プリマルチプライド`
`Blend One One // 加算`
`Blend OneMinusDstColor One // ソフトな追加`
`Blend DstColor Zero // 乗算`

__DstAlphaを使うShader__としては気にするところはありますが、他のShaderに対しては悪い影響がほぼ出ないと考えられます。
とりあえず、安全に利用できそうなのが分かったので使い方を考えてみます。

#### ColorとAlphaでブレンド設定を分けてみる
ブレンド方法でRGBとAを別に指定できるのでColorとAlphaをそれぞれ別々に更新できます。

- Aを更新するのブレンド設定例
RGBを変更しないでAの値だけ合成するのが目的です。

  - `Blend Zero One,One Zero //Alphaだけ上書き`
  - `Blend Zero One,Zero SrcAlpha //Alphaだけ乗算`
  - `BlendOp Add,Min //BlendOpを追加してAlphaだけ最小値`
  - `BlendOp Add,Max //BlendOpを追加してAlphaだけ最大値`

- RGBを更新するブレンド設定例
Alphaを変更しないでDstAlphaを使って色を合成するのが目的です。
  - `Blend DstAlpha OneMinusDstAlpha,Zero One // DstAlphaでアルファブレンド`
  - `Blend DstAlpha One,Zero One // DstAlphaでかけて加算`

## 実際にDstAlphaを使ってみた
この表示の構成は1の値のDstAlphaに__横縞__と__ひし形__の画像と__テキスト__を乗算していて値を徐々に変化させてます。
表示されている色は__壁っぽい画像__と__緑のテキスト__を背景の__Skybox__と__DstAlpha__でブレンドアルファで合成してます。
各要素が別々に設定できることを見せるために__壁画像__は下にスクロールさせてみました。
<img width=80% alt="DstAlphaBlend20201215.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316385/5af7dcdd-818d-d1ff-135e-9182c4c5b795.gif">

この動画を作っている時になんとなくパズルを解いているような感覚に近いものがありました。
ブレンド設定も組み合わせが多く複雑になりやすいです。
いろいろ試しやすいように[TransitionMask](https://github.com/A-rosuko/TransitionMask)という名前の自作Shaderを__GitHub__で公開していますのでよかったら使ってみてください。

## DstAlphaの特徴
全体的なイメージとしては事前に書き込んでその後の描画に使用するため、Stencilを使用したShaderと使用感が似ている気がします。
StencilのShaderも普段はあまりつかわれていないので同様に出番は少なそうです。
シーンの遷移エフェクトとして使うのは良さそうでした。
とりあえず使ってみた印象でDstAlphaの特徴をまとめておきます。

 - 良いところ
   - 複数のゲームオブジェクトを表示とアルファで役割を分けて配置できます。
   - 画像を組み合わせると面白い表現がしやすそうです。
   - ブレンド設定が特殊なだけでShader自体は単純です。
   - 軽いシーンでならuGUIと相性が良さそうです。

 - 悪いところ
   - マスクと表示用でマテリアルが増えるので管理が複雑になりがちです。
   - 普通の半透明と同じですが、重ねた分だけ処理が増えるので重くなります。
   - 操作しているのはAlpha値だけなので、できることはあんまり多くないです。

## おまけ
いろいろ調べている過程で気づいたことを載せておきます。
#### HDR設定
HDRモードで疑似HDRといわれる__R11G11B10__というフォーマットがあります。
__R11G11B10__は32bitの使われてないメモリを有効利用して処理を軽くする代わりにDstAlphaの値が`1`になるようです。
以下の設定をしていると__R11G11B10__になります。

- __URP__の場合
  - `Quality > HDRをtrue`
  - `Camera > Output > HDRをUse GraphicSettings`

- __Build-in RP__の場合
  - `ProjectSettings > Graphics Tier Settings > Use HDRをtrue`
  - `ProjectSettings > Graphics Tier Settings > HDRModeでR11G11B10を選択`
  - `Camera > Output > HDRをUse GraphicSettings`

- uGUIの表示はHDRに設定してもHDRにならないようです。
※HDRPは未確認です。

#### 毎フレームの描画時の背景のAlpha値
- カメラの設定で__SolidColor__の場合、Color設定の初期のAlpha値は0なので注意しましょう。
- カメラの設定で__SkyBox__の場合、始めに`[0,0,0,0]`にデータがクリアされて不透明オブジェクトの描画最後の`RenderQueue=2500`の後のタイミングで__SkyBox__が描画されてAlphaが`1`になります。
__SolidColor__とは描画順がちがうので注意しましょう。
不透明オブジェクトで描画した内容は深度バッファを更新していない場合はSkyBoxに上書きされます。

#### 3Dでポリゴンが交差している場合の半透明
交差しているポリゴンはどちらを先に描画しても正しく半透明に表示できないことがあります。
対処方法としてはShaderに下記のPassを追加して深度バッファだけ先に更新して、そのあとのPassでカメラに距離が近いピクセルのみを描画するという方法があります。

```
// 深度バッファのみ更新する追加パス
Pass {
    ZWrite On      // 深度バッファを更新する
    ColorMask 0    // カラーマスクによりフレームバッファを更新しない
}
```
※2Dでも設定次第で同じようなことができないかなと思っているのですが未確認です。

#### レンダリングステートのチートシート
自作Shaderなどは簡単にレンダリングステートを追加できます。
ShaderGraphの場合も出力ノードで右クリックして「Show Genarate Code」からプログラムを書き出す機能があります。
手間はかかりますが保存してからレンダリングステートを変更することができます。
必要なプロパティと設定をコピーして使ってください。

```
Shader "ShaderName"
{
    Properties
    {
      //プロパティ
      //ここにマテリアルのインスペクタに表示される内容を追加します
      
      //ブレンド設定用のプロパティを追加
      [Enum(UnityEngine.Rendering.BlendMode)]_BlendSrc("Blend Src", Float) = 5
      [Enum(UnityEngine.Rendering.BlendMode)]_BlendDst("Blend Dst", Float) = 10
      [Enum(UnityEngine.Rendering.BlendMode)]_BlendAlphaSrc("Blend Alpha Src", Float) = 5
      [Enum(UnityEngine.Rendering.BlendMode)]_BlendAlphaDst("Blend Alpha Dst", Float) = 10
      [Enum(UnityEngine.Rendering.BlendOp)]_BlendColorOp("BlendOp Color", float) = 0
      [Enum(UnityEngine.Rendering.BlendOp)]_BlendAlphaOp("BlendOp Alpha", float) = 0

      //CullModeのプロパティを追加
      [Enum(UnityEngine.Rendering.CullMode)]_CullMode("Cull Mode", Float) = 2

      //ZWriteとZTestModeのプロパティを追加
      [Toggle]_ZWriteParam("ZWrite", Float) = 1
      [Enum(UnityEngine.Rendering.CompareFunction)]_ZTestMode("ZTest Mode", Float) = 4

      //Stencilのプロパティを追加
      [Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp("Stencil Comp", Float) = 8
      _StencilRef("Stencil Ref", Range(0, 255)) = 0
      _StencilReadMask("Stencil Read Mask", Range(0, 255)) = 255
      _StencilWriteMask("Stencil Write Mask", Range(0, 255)) = 255
      [Enum(UnityEngine.Rendering.StencilOp)]_StencilPass("Stencil Pass", Float) = 0
      [Enum(UnityEngine.Rendering.StencilOp)]_StencilFail("Stencil Fail", Float) = 0
      [Enum(UnityEngine.Rendering.StencilOp)]_StencilZFail("Stencil ZFail", Float) = 0
    }

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

        Pass
        {            
            //ブレンド設定を追加
            Blend[_BlendSrc][_BlendDst],[_BlendAlphaSrc][_BlendAlphaDst]
            BlendOp[_BlendColorOp],[_BlendAlphaOp]
            
            //CullModeの設定を追加
            Cull[_CullMode]
            
            //ZTestModeとZWriteの設定を追加
            ZTest[_ZTestMode]
            ZWrite[_ZWriteParam]
            
            //Stencilの設定を追加
            Stencil
            {
                Ref[_StencilRef]
                Comp[_StencilComp]
                ReadMask[_StencilReadMask]
                WriteMask[_StencilWriteMask]
                Pass[_StencilPass]
                Fail[_StencilFail]
                ZFail[_StencilZFail]
            }

            //ここから下は触らない
            HLSLPROGRAM //Shaderのプログラムはここから
            ・・・

            ・・・
            ENDHLSL //Shaderのプログラムはここまで
        }
    }
}
```

## おわりに
__DstAlpha__の存在を知ったのはだいぶ昔で当時は使い道がさっぱりわかりませんでした。
調べる過程で背景側に透明度を設定できるのは面白いんじゃないかなと思いましたが、描画内容が決まっているならShaderを直接変更した方が早いかもしれないと思うところもありました。
もしかしたら良い使い方が見つかるかもしれないと思って今回記事にしました。

この記事を通して何か得るものがあれば幸いです。
少し長くなりましたが、最後まで読んで頂きありがとうございました。

## 参考
- [ShaderLab: Blending](https://docs.unity3d.com/ja/2020.1/Manual/SL-Blend.html)
- [シェーダーのデータ型と精度](https://docs.unity3d.com/ja/2020.1/Manual/SL-DataTypesAndPrecision.html)
- [uGUI 描画優先度のチートシート](https://developer.aiming-inc.com/ux/ugui-layer/)
- [【Unity】複数パーツで構成された半透明のSpriteを違和感無く表示する…際に出るであろう問題の対策](http://tsubakit1.hateblo.jp/entry/2017/11/25/200555)