-100p

-10p

+10p

+100p

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

Unityシェーダーの基礎知識。Unityで最もシンプルなuGUI用Shaderを作る。
一行ずつコード解説。

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

今では少し分かってきてUnityのuGUI用シェーダーであれば作れるようになってきたので、その解説。

一番シンプルな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 : 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"
{
    SubShader
    {
最初の
 Shader "Test/Test2DShader" 
が、Materialから指定するパスになる。

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

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

 Cull off 
は、カリングというポリゴン描画の処理負荷を軽減する機能を無効にする命令。
uGUIではカリングの必要性はない。
むしろカリングを有効にしてしまうと、Scaleをマイナスにした時に画像が全く描画されなくなる。

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

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


        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
 Pass 
の{}で囲まれた部分が1ピクセルごとに実行するシェーダー処理。ひとつのSubShader内に複数のPassを記述することもできる。
 CGPROGRAM 
はPassとセットになるコード。大体の場合は、Pass内の一番最初にCGPROGRAMを記述し、一番最後にENDを記述する。

 #pragma vertex vert 
は頂点シェーダーの関数名を宣言している。
 #pragma fragment frag 
はフラグメントシェーダーの関数名を宣言している。
シェーダーは、この頂点シェーダーとフラグメントシェーダーの処理が少なくとも各1つは定義されていないと動かない。
ピクセルごとの座標や色の情報がまず頂点シェーダーに送られて、頂点シェーダーで編集した情報がフラグメントシェーダーに送られる。
最後に、フラグメントシェーダーで編集した情報が画面に反映されるイメージ。
uGUIの場合最後のフラグメントシェーダーだけ弄れば大体事足りると思う。

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

            struct v2f
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : POSITION;
                fixed4 color : COLOR;
            };
C言語系ではおなじみの構造体の定義。
上の
 appdata 
が頂点シェーダーに送られる構造体、下の
 v2f 
がフラグメントシェーダーに送られる構造体。
このシェーダーでは両方とも同じ情報を持たされている。

 fixed2 
,
 fixed4 
はUnity C#でいうVector2、Vector4に近い。それぞれ2つの浮動小数点数、4つの浮動小数点数を扱う変数になる。
似たようなものでhalf2, float2や、half4, float4もある。
もちろんfixed3, half3, float3もある。
fixedは最も低い精度の浮動小数点数で、掛け算や割り算をすると誤差が広がっていくが、その分処理負荷はわずかに下がる。
halfは中間程度の精度で、floatが最も精度の高い浮動小数点数になる。
色々試したけど見た目の違いは確認できなかったので、2Dシェーダーであればfixedで十分と思う。

 uv : TEXCOORD0; 
 vertex : POSITION; 
は両方とも座標に関する情報になる。
uvは画像内のピクセル位置、vertexは画像自体の3D空間上の位置。
 color : COLOR; 
は色情報。uGUIの場合、このcolorは画像自体のピクセルの色ではなく、Image ComponentのColor値になる。
「:」から続くコード部分はセマンティクスと呼ばれるもので、その値の使用方法を伝達するために必要な文字列。
頂点シェーダーとフラグメントシェーダーが受け取る構造体、それと最後にフラグメントシェーダーが返す色情報は、セマンティクスの指定が必要。

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

頂点シェーダー

            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);
すると下の画像のように少し右上にスクリーン上の座標がずれる。


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

                o.uv = fixed2(v.uv.x + 0.25, v.uv.y);
すると下の画像のように画像内のすべてのピクセル位置が左にずれる。

ずれた分の元画像には存在していないピクセル部分は、元画像の端のピクセルがコピーされ続けるらしい。

 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));


                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使いなら馴染みある結果だと思う。


                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

-100p

-10p

+10p

+100p