[Unity] シェーダを書く準備

  • 77
    Like
  • 0
    Comment
More than 1 year has passed since last update.

どうやらUnityではやや特殊な仕組みがあるらしい。
それがShaderLabと呼ばれるもの。

これらの知識はこちらの記事を参考にしました。


Unityで使えるシェーダの種類

WebGL(やOpenGL)などではシェーダを書き、頂点データやマトリクスなどはメインの言語で生成し、それをシェーダに送る、という仕組みで動作しています。
当然、固定パイプラインでなければライティングなど様々な表現を自分で書く必要があります。

しかしUnityの場合は、この ShaderLab が色々とめんどくさいところをやってくれているよう。
Unityで書けるシェーダにはいくつか種類があり、

  1. Fixed Function Shaders(固定機能シェーダ)
  2. Surface Shaders(サーフェイスシェーダ)
  3. Vertex and Fragment Shaders(頂点・フラグメントシェーダ)

の3つ。
上から順に難易度というか、やるべきことが増えていきます。
メインで使うのは後半ふたつ。
そして(おそらく)一番書くであろう部分が真ん中のSurface Shadersになると思われます。

固定機能シェーダ

シェーダをサポートしていない端末の場合に利用されるもので、基本は使わないものらしいです。いわゆる代替手段。

サーフェイス・シェーダ

Unityには通常のパイプラインの他に、ライティングパイプラインという別のフェーズがあるみたい。
詳しくは分からないけど、推測では通常のパイプラインの1フェーズとして存在し、その中でライティングやシェーディングを行う?
そしてその隙間に入れるシェーダが2番目のSurface Shadersってことなんだと理解しました。

頂点・フラグメントシェーダ

こちらはいわゆる普通のシェーダ。
ライティングやシェーディングもすべて行える。柔軟だけど、上のライティングなども自分で実装しないとなので、その分やることが増えます。完全なプログラマブルシェーダですね。(当然と言えば当然)

ShaderLab

ShaderLab はUnityとシェーダとの仲立ちをする部分。
これにも書式があって、ここにシェーダで利用する変数の定義や、UnityのGUIエディタ側に出す項目などを制御することができます。
また、SubShaderを複数定義でき、これは端末ごとに差異のあるシェーダを出し分ける仕組み。
つまり、色々な端末向けに作る場合は必要な分だけシェーダを書かないとならないことを意味しています。

(ただ、通常はCg/HLSLで記述して、GLSLにトランスコードするのが基本らしいので、Unityでシェーダを書く場合はCg/HLSLで書くのがよさそう)

書式

ごく簡単な書式は以下の感じ。

Shader "Path/To/ShaderName" {
    Properties {
        //...
    }
    SubShader {
        //...
    }
    SubShader {
        //...
    }
    SubShader {
        //...
    }
    //以下、必要なだけ繰り返し

    FallBack "Diffuse"
}

Shaderで囲まれた部分がひとつのシェーダを定義し、その中で、シェーダで使う変数やUnityのエディタに出す名前などを定義、関連付けと実際のシェーダコードを書いていきます。

Properties

Propertiesはシェーダで使う変数とUnityエディタとの関連付けを行います。
ここで定義したものがシェーダ内で利用できる、というわけです。
使えるものは決まっていて、以下のような形で定義します。
Unityの公式サイトから引用すると以下のような定義になります。

Properties { Property [Property ...] }
Defines the property block. Inside braces multiple properties are defined as follows.

プロパティの定義ブロック。ブラケットの中は、以下のように複数のプロパティが定義されます。

name ("display name", Range (min, max)) = number
Defines a float property, represented as a slider from min to max in the inspector.

float型のプロパティ定義。minからmaxまでのスライダーをインスペクタに表示します。

name ("display name", Color) = (number,number,number,number)
Defines a color property.

カラープロパティ定義。

name ("display name", 2D) = "name" { options }
Defines a 2D texture property.

2Dテクスチャプロパティ定義。

name ("display name", Rect) = "name" { options }
Defines a rectangle (non power of 2) texture property.

矩形のプロパティ定義。

name ("display name", Cube) = "name" { options }
Defines a cubemap texture property.

キューブマップテクスチャのプロパティ定義。

name ("display name", Float) = number
Defines a float property.

float型のプロパティ定義。

name ("display name", Vector) = (number,number,number,number)
Defines a four component vector property.

4成分のベクトルプロパティの定義。

boolean風のプロパティ定義(チェックボックス)

[MaterialToggle] name ("display name", Float) = 0 // 0 is false, 1 is true

nameはシェーダ内の変数名、display nameはUnityのインスペクタに表示される名前です。
続く部分がいわゆる型になっていて、例えばVectorとするとUnityのインスペクタ側には4つの入力が表示される、という具合です。

SubShader(サーフェイス・シェーダ)

サーフェイス・シェーダは冒頭で書いた通り、ライティングなどをUnity側でよしなにしてくれるタイプのシェーダです。

書式

Shader "Custom/SampleShader" {
    Properties {
        _MainTex ("Base", 2D) = "white" {}
    }

    SubShader {
        Tags { "RenderType"="Transparent" }

        CGPROGRAM

        #pragma surface surf Lambert

        struct Input {
            float2 uv_MainTex;
            float4 vtxColor : COLOR;
        };

        void surf(Input IN, inout SurfaceOutput o) {
            half4 color = tex2D(_MainTex, IN.uv_MainTex);
            o.Albedo = color.rgb;
        }

        ENDCG
    }
}
#pragma文

#pragmaの行は、サーフェイスシェーダについての設定です。
surfaceというキーワードで始まり、次のsurfは、シェーダのエントリ関数の名前を指定します。
(その下にsurf関数が定義されているのが分かるかと思います)

最後のLambertはランバート反射のことです。いくつかライティングについての設定ができるようです。
上記ではランバート反射を採用していることを通知しているわけですね。

ちなみにサンプルには書いていませんが、Lambertのあとにオプションを指定することもできるようです。

Tagsブロック

Tagsブロックは「いつ」「どのように」レンダリングを行うかを指定します。(いわゆる「Key-Value」型で)

Input構造体

以下のサンプルを見てください。

Shader "Custom/sample" {
    Properties {
        _MainColor("Color", Color) = (1, 1, 1)
        _MainTex("Texture", 2D) = "white" {}
    }
    SubShader {
        Tags { "RenderType"="Transparent" }

        CGPROGRAM

        float4 _MainColor;
        sampler2D _MainTex;

        #pragma surface surf Lambert alpha

        struct Input {
            float4 color : COLOR;
            float2 uv_MainTex;
        };

        void surf (Input IN, inout SurfaceOutput o) {
            o.Albedo = tex2D(_MainTex, IN.uv_MainTex).rgb * _MainColor.rgb * half3(1, 0.5, 0.5);
            o.Alpha = 0.5;
        }
        ENDCG
    } 
    FallBack "Diffuse"
}

中央付近にInput構造体が定義されているのが分かるかと思います。
ここは書式がいくらか決まっていて、Unity側からシェーダへデータのインプットをするための定義です。

上記サンプルでは、Unityのインスペクタからカラーとテクスチャをそれぞれ受け取るようになっています。
(しっかりドキュメント読んでいませんが、どうやら変数名のprefixに_は必須のようです(_MainTexなど))

インスペクタから受け取ったテクスチャ情報は、Input構造体で定義されているuv_MainTexメンバに引数として渡ってきます。
(どうやらこちらのuvプレフィクスも決まりがあるようです)

頂点シェーダ/フラグメントシェーダ

サーフェイスシェーダはライティングなどの基本的なことをさっと作るのには向いているシェーダ(だと思ってます)。
頂点シェーダ/フラグメントシェーダは、いわゆる普通のプログラマブルシェーダです。

以下のコードサンプルのように、#pragma文で頂点シェーダとフラグメントシェーダのエントリ関数名を指定します。

Shader "shaderName" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragument frag

            float4 vert(float4 v:POSITION) : SV_POSITION {
                return mul(UNITY_MATRIX_MVP, v);
            }

            fixed4 frag() : COLOR {
                return fixed4(1.0, 0.0, 0.0, 1.0);
            }

            ENDCG
        }
    }
}

ちなみにUNITY_MATRIX_MVPは、Unity側から渡される座標変換行列です。
UNITY_MATRIX_MUNITY_MATRIX_VUNITY_MATRIX_PUNITY_MATRIX_MVなど、それぞれ個別の行列や掛け合わされた行列が準備されているようです。
(組み込み値の種類はこちらを参照:ShaderLab built-in values

セマンティクス

上記のサンプルで、:POSITION:SV_POSITIONなどの記述があります。
これはなにかというと、「頂点シェーダ入力(出力)セマンティクス」というものらしいです。
セマンティクス詳細についてはこちら

※ ちなみにSV_はおそらくSystem Value(システム値)の略。

頂点シェーダでは通常、「頂点位置」以外にも「頂点色」や「頂点法線」など様々な入力があります。
そうした中で、float4(float型の4次元ベクトル)だけでは、どの情報なのか識別できません。
そこで、この「セマンティクス」を付与することで、「この変数は頂点位置だ」ということを意味付けしているようです。

ちなみに「頂点シェーダ出力セマンティクス」には以下のものが利用できるようです。

  • 位置座標(POSIITON)
  • ポイントサイズ(PSIZE)
  • 頂点フォグ(FOG)
  • 色(COLOR[n]:nはオプションの整数でCOLOR0などのように使う)
  • テクスチャ座標(TEXCOORD[n])([n]はテクスチャ番号?)

ビルトイン シェーダ includeファイル / Built-in shader include files

Unityのサイト:ビルトイン シェーダ includeファイル / Built-in shader include files

Unityでは、いくつかの便利なヘルパー関数や、予め使える構造体などを定義したファイルを用意してくれているようです。

以下のようにすることで、ファイルを利用することが出来ます。

Shader "shaderName" {
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 3.0

            #include "UnityCG.cginc"

            float4 vert(appdata_base v) : POSITION {
                return mul(UNITY_MATRIX_MVP, v.vertex);
            }

            fixed4 frag(float4 sp:WPOS) : COLOR {
                fixed2 red_green = sp.xy / _ScreenParams.xy;
                fixed blue  = 0.0;
                fixed alpha = 1.0f;
                return fixed4(red_green, blue, alpha);
            }

            ENDCG
        }
    }
}

ちなみに、appdata_baseは以下のような構造体です。

struct appdata_base {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 texcoord : TEXCOORD0;
};

なお、セマンティクスがあるため、変数名は特に決まったルールはなさそうです。
:POSITIONなどを付けておけば自動的に適切に値が渡ってくる)

インスペクタから値を設定する

Unityの優れている点は、外部から値を設定するのが簡単なところでしょう。
頂点シェーダ/フラグメントシェーダでも当然、外部から値を渡すことが可能です。

Shader "shaderName" {
    Properties {
        _RangeValue("Value", Range(0.0, 1.0)) = 0.2
        _MainTex("Texture", 2D) = "white" {}
    }
    SubShader {
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            uniform float _RangeValue;
            uniform sampler2D _MainTex;

            // シェーダソース

            ENDCG
        }
    }
}

例のように、Propertiesでプロパティを定義し、シェーダ側ではuniform修飾子を付けることで参照できるようになります。

色々と覚えることが多くて大変そうですが、なんとなくUnityでシェーダを書く準備が整った気がします。
今後はごりごりシェーダを書いてみたいですね。(まだまだ勉強しないとならないことがたくさんありますが・・)

ちなみに、本記事の大部分はhecomiさんの記事を参考にさせていただきました。
上記記事では、Meshを使った、蝶が羽ばたくParticleを作成しています。一見の価値アリです。