【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が実行していきます。
簡単にですがそれぞれの流れを説明しておきます。

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

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

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

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

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

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

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

ブレンディングの設定と計算式

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

Blend [SrcFactor] [DstFactor]

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

BlendOp [Op]

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

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

Blend [SrcFactor] [DstFactor],[SrcFactorA] [DstFactorA]

左2つのFactorRGBに対して、右2つのFactorAに対しての設定になります。
BlendOpにもRGBAの値を別の計算式にする書き方があります。

BlendOp [OpColor],[OpAlpha]

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

式に代入されるFactorBlendOpの詳細は以下を参照してください。

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))
合計が1になる値をAlpha値で分けてそれぞれをShader出力とフレームバッファに乗算している計算です。

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

深度バッファ

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

Stencilバッファ

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

clip()関数

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

描画順

ShaderのRender Queueが2500以下の値は不透明オブジェクト、2500より大きい値は透明オブジェクトと扱われます。
不透明オブジェクトはカメラから見て一番手前を先に描画し、するその後ろは表示されないので処理を省略します。
無駄なShaderの処理を減らすのに都合がいいので手前から描画する順番にソートされます。
透明オブジェクトは背景に重ねて描画する必要があるので奥から描画する順番にソートされます。
他にSpriteSortingLeyerOrderinLeyerなどでユーザーが好きなように描画順を入れ替える仕組みもあります。
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に横縞ひし形の画像とテキストを乗算していて値を徐々に変化させてます。
表示されている色は壁っぽい画像緑のテキストを背景のSkyboxDstAlphaでブレンドアルファで合成してます。
各要素が別々に設定できることを見せるために壁画像は下にスクロールさせてみました。

この動画を作っている時になんとなくパズルを解いているような感覚に近いものがありました。
ブレンド設定も組み合わせが多く複雑になりやすいです。
いろいろ試しやすいように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を直接変更した方が早いかもしれないと思うところもありました。
もしかしたら良い使い方が見つかるかもしれないと思って今回記事にしました。

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

参考