Unity Shader (HLSL) の書き方を基礎の基礎レベルで学んだ

# 概要

業務で「2つのテクスチャをブレンドするシェーダー」と「クロマキー合成を行うシェーダー」を合成したシェーダーを作ることになり、そこで学んだことを書いておく。

- [【Unity】2つのテクスチャを合成するシェーダー - Qiita](https://qiita.com/broken55/items/9debc09b7ea15907fa6d)
- [Unity でクロマキーシェーダを作ってみた - 凹みTips](https://tips.hecomi.com/entry/2018/10/21/022200)

# シェーダーファイルの構造

テンプレートになりそうな内容を元に説明をコメントに追記した。

```hlsl
Shader "シェーダの種類/シェーダーの名前"
{
    // Unityインスペクタに表示される項目 (マテリアルプロパティ)
    Properties
    {
        // テクスチャの設定
        _MainTex("Texture", 2D) = "white" {}

        // 色の設定
        _Color ("Main Color", Color) = (1,1,1,1)
    }

    SubShader
    {
        // どのようにレンダリングするか
        Tags 
        {
            "Queue" = "Transparent"
            "RenderType" = "Transparent" 
            "IgnoreProjector" = "True" 
            "PreviewType" = "Plane"
        }

        // レンダリングを行う
        Pass
        {            
            // 実際の処理を書く (HLSL)
            CGPROGRAM

            // vert関数を頂点シェーダーとして利用する
            #pragma vertex vert

            // frag関数をフラグメントシェーダーとして利用する
            #pragma fragment frag

            // 頂点シェーダーへの入力の構造体を定義
            struct appdata
            {
                // 頂点座標
                float4 vertex : POSITION;

                // テクスチャ座標
                float2 uv : TEXCOORD0;
            };
            
            // フラグメントシェーダーへの入力の構造体を定義
            // 頂点シェーダーで生成して、フラグメントシェーダーの引数で受け取る
            struct v2f
            {
                // クリップスペース位置
                float4 vertex : SV_POSITION;

                // テクスチャ座標
                float2 uv : TEXCOORD0;
            };

            // マテリアルプロパティを変数に割り当てる
            sampler2D _MainTex; // メインテクスチャ
            float4 _Color; // 色
            
            // 頂点シェーダーの処理
            // 3Dモデルの拡張点で実行されるプログラム (頂点の位置をオブジェクト空間からクリップスペースに変換する)
            v2f vert(appdata v)
            {
                // フラグメントシェーダーの入力になる v2f 構造体変数の定義
                // 変数名は Output の o だと思う
                v2f o;

                // オブジェクト空間の座標からクリップスペースへの変換を行う処理
                // クリップスペースはGPU が画面上のオブジェクトをラスタライズするために使用するもの
                o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);

                // テクスチャ座標を定義
                o.uv = v.uv;
                
                return o;
            }
            
            // フラグメントシェーダーの処理
            // 画面上に見えるすべてのピクセルで実行されるプログラム (各ピクセルの色を計算する)
            // これの最適化がパフォーマンスで重要
            fixed4 frag(v2f i) : SV_Target
            {
                // ここではマテリアルプロパティで設定された色を返しているだけ
                return _Color
            }
            ENDCG
        }
    }
}
```

# HLSLの処理の流れ

1. 頂点シェーダー/フラグメントシェーダーに使う関数名を指定 (vert/frag)
1. 頂点シェーダー/フラグメントシェーダーの入力構造体を定義 (appdata/v2f)
1. マテリアルプロパティを受け取る変数を定義 (_MainTex, Color)
1. 頂点シェーダーの処理を実装 (入力はappdata, 出力はv2f)
1. フラグメントシェーダーの処理を実装 (入力はv2f, 出力はfloat4のColor情報)

# どのようにブレンドシェーダーとクロマキーシェーダーを合成したのか？

最初は単純に2つのシェーダーの「pass」を並べて書いたが、これは上手く動作しなかった。
シェーダーの仕組みを理解すれば単純だが、実装するにはフラグメントシェーダーで「ブレンドした結果の色情報にクロマキー合成処理をする」必要がある。

また、2つのシェーダーはコードをよく見ると「マテリアルプロパティ」と「フラグメントシェーダー」以外の部分はほとんど同じことが書かれていたので、差異があった2つの箇所を上手く組み合わせた。

ここで問題になったのはクロマキーシェーダーの実際の処理が「.cginc」という別ファイルに定義されていたため、ブレンドシェーダー側のフラグメントシェーダーにクロマキーシェーダー側のフラグメントシェーダーの処理を直接追記する必要があった。

コード自体はそれぞれ別の開発者様が実装し、自分はそれを組み合わせただけなので、ここではフラグメントシェーダー部分のみ簡単に記載しておく。

```hlsl
fixed4 frag(v2f i) : SV_Target
{
    // メインテクスチャの色情報を取得
    fixed4 main = tex2D(_MainTex, i.uv);

    // サブテクスチャの色情報を取得
    fixed4 sub = tex2D(_SubTex, i.uv);

    // メインテクスチャとサブテクスチャをブレンドした結果の色情報を取得
    fixed4 col = (main * (1-_Blend) + sub * _Blend) * _Color;

    // クロマキー合成した結果の色情報を取得 (実装は ChromaKey.cginc にある)
    ChromaKeyApplyAlpha(col);

    // FOGを適用する
    UNITY_APPLY_FOG(i.fogCoord, col);

    // 計算した結果の色情報を返す
    return col;
}
```

その結果、ブレンドした結果を正しくクロマキー合成することに成功した。

![352b7539e8a32e3aa64098d204c5e359.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44388/55d0bdf1-32b1-5b8d-6e04-ab0674919f15.gif)

# まとめ

Unity は学び始めてもう4年になるが、シェーダーコードをしっかり読んで手を加えたのは初めてだったので良い経験になった。実際にHLSLを読むことで、シェーダーは基本的に以下の処理をしていることを理解した。

- 頂点座標の変換
- ピクセル情報の変換

ここから更に踏み込むともう少し複雑な世界があるとは思うが、現状の業務では必要十分の知識を得ることができたので満足した。もしも記事に間違い等があればコメントでご指摘していただけますと幸いです。

# 参考資料

- [シェーダー: 頂点とフラグメントプログラム - Unityマニュアル](https://docs.unity3d.com/ja/2018.4/Manual/ShaderTut2.html)
- [頂点シェーダーとフラグメントシェーダーの例 - Unityマニュアル](https://docs.unity3d.com/ja/2019.4/Manual/SL-VertexFragmentShaderExamples.html)