シェーダー基礎、最もシンプルなuGUI用Shader

シェーダー基礎、最もシンプルなuGUI用Shader

最もシンプルなUnity uGUI用シェーダーの作り方。
一行ずつコード解説。

関連ページ
参考URL
趣味や業務内でC,C++,C#,Java,PHPさまざまな言語を扱ってきたけど、これらは結局C言語の文化圏で、ひとつを覚えれば他も大体分かる。
ただシェーダー言語に初めて触れたときは全く異次元の言語に見えてけっこう絶望した。

今では少し分かってきてuGUI用シェーダーであれば作れるようになってきた。その解説をして知識を共有したい。
Unity uGUIは、シェーダー初学者にとってとても良い教材だと自分は思ってる。
最近ようやく日本語のシェーダー本も良いのが出てきたので、この記事もそれを参考に内容をアップデートした(2025年2月)。
今後も適時アップデートする予定。

一番シンプルな2Dシェーダー

2Dシェーダーは光の反射を考慮する必要がないし、平面なので3Dシェーダーよりよっぽどシンプルになる。
下のコードは機能を最小限に絞ったuGUI用シェーダーの全文。
Shader "Test/Test2DShader"
{
     SubShader
     {
         Tags { "Queue" = "Transparent" }
         Cull Off
         ZWrite Off
         Blend SrcAlpha OneMinusSrcAlpha

         Pass
         {
             CGPROGRAM
             #pragma vertex vert
             #pragma fragment frag

             struct appdata
             {
                 fixed2 uv : TEXCOORD0;
                 fixed4 vertex : POSITION;
                 fixed4 color : COLOR;
             };

             struct v2f
             {
                 fixed2 uv : TEXCOORD0;
                 fixed4 vertex : SV_POSITION;
                 fixed4 color : COLOR;
             };

             sampler2D _MainTex;

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

             fixed4 frag (v2f i) : SV_Target
             {
                 fixed4 col = tex2D(_MainTex, i.uv);
                 col *= i.color;
                 return col;
             }
             ENDCG
         }
     }
}
要点としては、2Dシェーダーでは一番下の関数のフラグメントシェーダーしかほぼ弄らない。
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                col *= i.color;
                return col;
            }

例えば、関数内の
 col *= i.color; 
というコードを削除すると、Image ComponentのColorを弄っても全く反映されなくなる。 シェーダーのコードを一部削除した結果

一行ずつ解説


Shader "Test/Test2DShader"
{
最初の
 Shader "Test/Test2DShader" 
が、Materialから指定するパスになる。 マテリアルからシェーダーを指定する方法
詳しくは右の記事参照。 p.40 : シェーダー基礎、Imageにシェーダーを反映させる方法

    SubShader
    {
 SubShader 
はUnityでシェーダーを作成する際はほぼ必須の行。
このブロックの中では、Unityのシステムとシェーダー言語であるHLSLを仲介する様々な機能が使える。
なおこの仲介役の、HLSL言語をラップした言語をShaderLabと言う。

         Tags { "Queue" = "Transparent" }
 Tags { "Queue" = "Transparent" } 
は描画の実行順で、何も指定しないと"Geometry"になる。
"Geometry"だとuGUIは描画順がおかしくなるので、"Transparent"に決め打ちで良い。

         Cull Off
 Cull off 
は、カリングというポリゴン描画の処理負荷を軽減する機能を無効にする命令。
一般的に3Dゲームでは、カメラに写ってない裏側のポリゴンはこのカリングで描写を省いてる事が多い。

uGUIではこのカリングの必要性はない。
むしろカリングを有効にしてしまうと、Scaleをマイナスにした時に画像が全く描画されなくなる。
uGUIでは、画像を上下反転、もしくは左右反転したい時にマイナスのScale値を使うことがよくある。

         ZWrite Off
 ZWrite Off 
は、カメラからの距離によって適切に描画順を管理する機能を無効にする命令。
通常uGUIでは、z軸の座標は関係なしにHierarchyが下の物ほど手前に表示する。
なのでこの機能はいらない。
むしろZWriteを有効にしてしまうと、Hierarchyでは下に配置してるのに、何故か手前に表示されない事がある。

         Blend SrcAlpha OneMinusSrcAlpha
 Blend SrcAlpha OneMinusSrcAlpha 
は画像にアルファ値を使う場合必ず必要な行。これを削除すると下のような謎の画像になる。
OneMinusSrcAlphaを削除した結果

        Pass
        {
 Pass 
の{}で囲まれた部分がシェーダーの具体的な処理を記述する部分。
実はフラグメントシェーダーの定義以外はPassの外に出すことも出来る。
その場合すこしコードが増えるので、今回は頂点シェーダーの処理もPass内に突っ込んでる。

            CGPROGRAM
 CGPROGRAM 
はPassとセットになるコード。大体の場合は、Pass内の一番最初にCGPROGRAMを記述し、一番最後にENDを記述する。

            #pragma vertex vert
 #pragma vertex vert 
は頂点シェーダーの関数名を宣言している。
複雑なシェーダー処理の中で唯2つ、頂点シェーダーとフラグメントシェーダーはプログラマの介入が許されている。
それと同時に、頂点シェーダーとフラグメントシェーダーの関数名はプログラマが明示的に宣言し、その処理を定義する必要がある。

頂点とは、ポリゴンを形成するのに必要な点の数で、平面的な三角形な3つ、四角形なら4つは最低必要になる。
立体的な三角柱なら6つ、四角柱なら8つ最低必要になる。
近年のゲームであれば、1つの3Dモデルで頂点数が10万を超えることも珍しくないらしい。

uGUIは平面的な四角形として扱われるので、1つの画像につき頂点数は4つになる。
一応UnityのWindow > Analysis > FrameDebuggerから、この頂点数が確認できる。初学においては特に気にする必要はない。
uGUIの頂点数

            #pragma fragment frag
 #pragma fragment frag 
はフラグメントシェーダーの関数名を宣言している。別名ピクセルシェーダーとも呼ばれる。
頂点シェーダーは頂点数ごとに呼ばれるが、フラグメントシェーダーは塗りつぶすピクセル数ごとに呼ばれる。
つまり画面上に300 x 300ピクセルの画像を表示する場合、毎フレーム9万回呼ばれることになる。
この塗りつぶすピクセルには、透明色も含まれる。

処理の流れとしては、頂点シェーダーでまず対象モデルの座標や色の情報を編集し、それをフラグメントシェーダーに送る。
情報を受け取ったフラグメントシェーダーが更に情報を編集して、画面に出力する感じ。
厳密には頂点シェーダーとフラグメントシェーダーの間に、ラスタライザーという処理が挟まっている。
近年のシェーダーは画面に映像が出力されるまでに15ぐらいのステップを踏んでいるが、特に重要な処理は次の4つ。

 入力アセンブラー 
: 頂点情報などをメモリからロードする。人間で言うとお絵描きするモデルを選ぶフェーズ。
 頂点シェーダー 
: 頂点情報をスクリーン座標に変換する。モデルを見ながらキャンバス上に丸で当たりをつけるフェーズ。
 ラスタライザー 
: 塗りつぶす必要があるピクセルを決定する。モデルをよく観察し、当たりから下書きに移行するフェーズ。
 フラグメントシェーダー 
: 各ピクセルのカラーを決定する。絵具でキャンバスに対象を描く。描き方は自由、下書きを無視しても良い。

上で述べたように、プログラマは頂点シェーダーとフラグメントシェーダーにしか介入できず、他は自動化されている。

uGUIの場合頂点シェーダーでやることはほぼ決まっていて、ここに頭を使う必要はない。
最後のフラグメントシェーダーだけ弄れば大体事足りると思う。

頂点構造体の定義

ここからちょっと話が複雑になるので、複数の題目に分けて解説する。

            struct appdata
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : POSITION;
                fixed4 color : COLOR;
            };

            struct v2f
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : SV_POSITION;
                fixed4 color : COLOR;
            };
C言語系ではおなじみの構造体の定義に似てるが、後ろに
 : TEXCOORD0 
などの変な文字列が入ってる。
これはセマンティクスと呼ばれるもので後ほど説明する。

 appdata 
が入力アセンブラーから頂点シェーダーに送られる構造体の定義になる。これを入力頂点構造体と呼ぶ、
 v2f 
がラスタライザからフラグメントシェーダーに送られる構造体の定義。これを出力頂点構造体と呼ぶ。
それぞれの構造体内で、どこからどこまでの情報をシステムから取得するかはプログラマの手に委ねられている。

ここではappdataとv2fという名前で定義しているが、実際のところはなんでもいい。
後述する頂点シェーダーとフラグメントシェーダーの引数の記述と一致していれば問題がない。
ちょっとコードを飛ばして頂点シェーダーとフラグメントシェーダーの定義を見てみると一致してるのが分かる。
             v2f vert (appdata v)
             {

             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

             fixed4 frag (v2f i) : SV_Target
             {

シェーダーの変数の型

構造体の定義内で、fixed2やfixed4といったC#プログラマには見慣れない宣言が見える。
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : POSITION;

fixedは浮動小数点数を扱う型で、floatの亜流と考えていい。
ShaderLabでは、高精度から低精度の順にfloat, half, fixedの3つの浮動小数点数の型が用意されている。
端末のGPUごとに少し内容が異なる概ね下のような性能。
 float 
: 32ビット浮動小数点数
 half 
: 16ビット浮動小数点数
 fixed 
: 11ビット浮動小数点数

fixedは最も低い精度で、掛け算や割り算をすると誤差が広がっていくが、その分処理負荷は下がる。
スマホ開発に特化したUnityらしさを感じる型。
もっとも最近はスマホも高性能化してきて、fixedをサポートしている端末は少なく、内部的にはhalfとして処理されるらしい。

厄介なことに、PCのEditor上で開発してるときは常に内部的にはfloatとして処理される。
実際の挙動はビルドして確かめる必要がある。
ともあれ、uGUIのシェーダー開発であれば最低精度でも十分事足りる。

 fixed2 
というのは、UnityでいうとVector2の感覚に近い。配列で内部に2つの浮動小数点数を持っている。
 fixed4 
はVector4に近い感覚で、配列で内部に4つの浮動小数点を持っている。
ここでは使ってないが、fixed3もあるし、他の精度のfloat2やhalf2もある。

入力セマンティクス

変数の宣言の後ろに
 : TEXCOORD0 
 : POSITION 
といった謎の文字列が確認できる。
まずappdataから見ていく。
            struct appdata
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : POSITION;
                fixed4 color : COLOR;
            };
上で触れた通りこれはセマンティクスと呼ばれるもので、直前のシェーダー処理のステージからどの情報を取得するかを表す。
入力頂点構造体で使うセマンティクスを、入力セマンティクスという。

 : TEXCOORD0 
はその頂点のuv座標。
UV座標は画像内の座標のことで、x軸もy軸も、0~1の値に正規化されている。
どんなに横長の画像でも、どんなに縦長の画像でも最小値は0、最大値は1となる。

頂点シェーダーは頂点数ごとに呼ばれるが、uGUIは1画像につき4つしか頂点がない。
つまり四角形の4隅の頂点に対応する、(0, 0), (0, 1), (1, 0), (1, 1)のいずれかの値が渡されることになる。

 : POSITION; 
はその頂点のワールド座標。
最終的にゲームプレイヤーが見るのは、ワールド座標ではなくカメラに映りこむスクリーン座標になる。
なのでこの情報のままラスタライザー及びフラグメントシェーダーに値を渡しても意味がない。
プログラマは頂点シェーダー内で明示的にこのワールド座標をスクリーン座標に変換する必要がある。

 : COLOR; 
はその頂点の色情報。
3Dモデルであれば、頂点ごとに異なった色情報を格納することが可能になっている。
異なる頂点色は頂点間で線形補完され、最終的に画面に映る色に影響を与える。

uGUIの場合もっとシンプルで、頂点ごとに異なる色情報を持つことは基本的にない。
4つの頂点すべてに同じカラー値が渡される。
そのカラー値はどこを参照してるかと言うと、Image ComponentのColorの部分になる。


出力セマンティクス

次にv2fを見ていく。
            struct v2f
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : SV_POSITION;
                fixed4 color : COLOR;
            };
出力頂点構造体で使うセマンティクスを、出力セマンティクスという。

 : TEXCOORD0 
はそのピクセルのUV座標。
appdataでは(0, 0), (0, 1), (1, 0), (1, 1)の4パターンしか無かったが、出力セマンティクスであるv2fでは相当多いパターンが考えられる。

フラグメントシェーダーは描画するピクセルごとに呼ばれる。
もし300 x 300pxの画像を描画する場合、x軸を0~1まで300分割、y軸を0~1まで300分割で、9万パターンが想定される。
例: (0, 0), (0.003, 0), (0.006, 0), (0, 0.003), (0.003, 0.003)など。

 : SV_POSITION; 
はエンジニアによって加工済みの頂点座標。
POSITIONと似た意味のセマンティクス。POSITIONとの違いは、その頂点座標が未加工であるか加工済みであるかの違い。
uGUIシェーダーにおいては、頂点シェーダー内でワールド座標からスクリーン座標に書き換えてるのでこちらを使う。

しかし実際のところ、Unity uGUI用シェーダー開発においてはSV_POSITIONをPOSITIONに置き換えても普通に動作する。
Unity以外の開発環境においては、出力セマンティクスでPOSITIONを使うと誤動作するらしいが、この辺は自分の方では調査できてない。

 : COLOR; 
は対象UV座標の色情報。
上で述べた通り、この色情報は画像内のピクセルの色ではなくImage Component内での指定値になる。
画像内のピクセル色は、後述するフラグメントシェーダー内の
 tex2D(_MainTex, i.uv); 
で取得している。
自分の知る範囲だとUnityで使うセマンティクスは以下の通り。
 POSITION 
: 未加工の頂点座標、uGUIにおいてはワールド空間の頂点座標。
 SV_POSITION 
: 頂点シェーダーによって加工された頂点座標。
 SV_TARGET 
: 最終的に画面に出力される、フラグメントシェーダーで加工されたピクセル色。
 TEXCOORD0 
: 1番目のテクスチャUV座標。uGUIにおいてはImageコンポーネントのSourceImageにアタッチされた画像内の座標。
 TEXCOORD1 
: 2番目のテクスチャUV座標。uGUIでは使わない。
 TEXCOORD2 
: 3番目のテクスチャUV座標。uGUIでは使わない。
 TEXCOORD3 
: 4番目のテクスチャUV座標。uGUIでは使わない。
 NORMAL 
: 頂点の法線ベクトル、光の反射方向などに使う値。uGUIでは使わない。
 TANGENT 
: 頂点の接ベクトル。一般にカメラに対して水平の値が代入され、法線ベクトルの基準になる(らしい)。uGUIでは使わない。
 COLOR 
: 頂点が内包するカラー。uGUIにおいてはImageコンポーネントのColor値。

_MainTexについて

            sampler2D _MainTex;
 sampler2D 
は平面テクスチャ画像を扱うための型。
 _MainTex 
はShaderLabで既に定義されている変数で、ここにImage ComponentのSource Imageにアタッチされたテクスチャ情報が代入される。
シェーダー内で明示的に宣言することで、後のフラグメントシェーダーで使えるようになる。
一文字でも違うとバグになるので注意。

今回は使ってないが、ShaderLabには _GrabTexture という画面全体をスクリーンショットしてくれるような便利な変数もある。
他にも_MainTex_TexelSizeという変数を宣言すると、アタッチされた画像のサイズの取得できる。
シェーダー開発にハマっていくとこういのも自然に使えるようになると思う。

頂点シェーダー

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.color = v.color;
                return o;
            }
頂点シェーダーの実際の処理部分。
関数名と引数は事前の
 #pragma vertex vert 
の宣言と、
 struct appdata 
の定義通り。
戻り値の型は、フラグメントシェーダーに渡すために、フラグメントシェーダーの引数と一致させる。
ここでは事前に定義した
 struct v2f 
を当てている。

 v2f o; 
でまず変数を用意。
 o.vertex = UnityObjectToClipPos(v.vertex); 
の部分は、対象ピクセルのワールド座標をカメラに投影されるスクリーン座標に置き換えている。
UnityObjectToClipPos関数はフラグメントシェーダーでは機能しなかったので、座標変換は頂点シェーダー内で行う必要があるみたい。

ここで例えば、変換された座標を少しずらす処理を入れてみる。
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.vertex = fixed4(o.vertex.x + 0.1, o.vertex.y + 0.1, o.vertex.z, o.vertex.w);
すると下の画像のように少し右上にスクリーン上の座標がずれる。
vertexを弄った結果

 o.uv = v.uv; 
では、対象ピクセルのUV座標を代入している。
例えばこのUV座標を少しずらす処理に置き換えてみる。
                
//o.uv = v.uv;

                o.uv = fixed2(v.uv.x + 0.25, v.uv.y);
すると下の画像のように画像内のすべてのピクセル位置が左にずれる。
uv座標を弄った結果
ずれた分の元画像には存在していないピクセル部分は、元画像の端のピクセルがコピーされ続けるらしい。

 o.color = v.color; 
でImage ComponentをColorを代入。
最終的に完成した変数を
 return o; 
でフラグメントシェーダーに渡している。

フラグメントシェーダー

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                col *= i.color;
                return col;
            }
本丸のフラグメントシェーダーの処理。
関数名と引数は事前の
 #pragma fragment frag 
の宣言と、
 struct v2f 
の定義通り。
関数の戻り値はピクセルの色を表すRGBAである必要があるので、float4、half4、fixed4のいずれかを指定する。
この戻り値には
 : SV_Target 
のセマンティクスが必要になる。

 fixed4 col = tex2D(_MainTex, i.uv); 
で、Source Imageにアタッチされた画像の特定のピクセルを取得している。
tex2D(_MainTex, i.uv)の第1引数がアタッチされている画像、第2引数がその画像のUV座標になる。

この段階でもUV座標をずらすことは可能で、例えば下のコードに置き換えてみると画像がズレるのが確認できる。
                
//fixed4 col = tex2D(_MainTex, i.uv);

                fixed4 col = tex2D(_MainTex, fixed2(i.uv.x + 0.25, i.uv.y));
uv座標を弄った結果

                col *= i.color;
 col *= i.color; 
で画像の全てのピクセル色に対し、ImageのColorで設定した値を掛け算で加えている。

シェーダーではRGBAの値は0~255ではなく0~1の範囲で扱う。
例えばアルファを1として、黒なら(0, 0, 0, 1)、青なら(0, 0, 1, 1)、グレーなら(0.5, 0.5, 0.5, 1)になる。
ここでアルファが1とする真っ黄色なピクセル(1, 1, 0, 1)があるとして、Imageのカラー設定がグレー(0.5, 0.5, 0.5, 1)だとする。
この2つの色情報を掛け算すると、それぞれのRGBAの値が掛け算され、(0.5, 0.5, 0, 1)という淀んだ黄色が生まれる。
これがUnity標準のImage Componentのカラー処理になる。
下の画像は、MaterialがアタッチされていないImageのColorを灰色にしたもので、Unity使いなら馴染みある結果だと思う。
RGBの掛け算の結果

                return col;
            }
            ENDCG
        }
    }
}
最後に、編集したピクセルの色情報を返して終了。ENDCGはすでに説明したCGPROGRAMと対になるコード。

色相反転シェーダーを作ってみる

遊びでフラグメントシェーダーを弄ってRGBの値を反転するシェーダーを作ってみる。いわゆるネガポジ反転と言われるもの。
単純化するため、ImageのColor値の反映はコメントアウトで無視している。
            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                
//col *= i.color;

                col.r = 1 - col.r;
                col.g = 1 - col.g;
                col.b = 1 - col.b;
                return col;
            }
下はその結果の画像。
ネガポジ反転の結果

今回記事改訂につき参考した本はこちら。
1
1