シェーダー基礎、最もシンプルな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.55 : シェーダー基礎、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