【連載】Unity時代の3D入門 – 第4回「シェーダとは?」

こんにちは、クライアントサイドエンジニアの矢野です。

第3回までは、ポリゴンの描画やuv座標の設定などを学んできました。
第4回からは、一歩進んでシェーダについて学んでいきます。

シェーダとは

さて、シェーダとはなんでしょうか。

ポリゴンの頂点座標は(1.0, 2.0, 3.0)のような3次元の座標です。
しかし、PCやスマートフォンのディスプレイは640ピクセル×1136ピクセルのような2次元です。

そのため、ディスプレイにポリゴンを表示するためには、ある3次元上の頂点をディスプレイ上のどこに描画するか、という座標の計算が必要となります。
このような計算を座標変換といいます。

ディスプレイ上のどのピクセルに描画するかが決まったら、今度はピクセルを何色に塗るかを決める必要があります。
例えば物体の陰になっているピクセルは暗めの色に変えるなど、描画する色を計算して決めます。

このように、2次元への座標変換や、ピクセルの色を決める役割をするのがシェーダです。
座標変換を主な役割とするシェーダを頂点シェーダ(バーテックスシェーダ)、ピクセルの色を計算するシェーダをフラグメントシェーダ(ピクセルシェーダ)といいます。

シェーダとマテリアルを作成する

シェーダの概念がわかったところで、さっそくUnityでシェーダを作ってみましょう。
シェーダはProjectビューで右クリック -> Create -> Shader -> Unlit Shader で作成します。

次にマテリアルを作ります。

マテリアルは、シェーダに渡す情報を格納するオブジェクトです。
例えばシェーダでテクスチャを扱いたい場合、マテリアルがテクスチャの参照を持ちます。
そのため、シェーダを扱う際には必ずマテリアルが必要となります。

マテリアルはProjectビューで右クリック -> Create -> Material で作成できます。

マテリアルを作成したら、マテリアルにシェーダをドラッグ&ドロップして紐づけます。

最後にこのマテリアルをモデルに適用します。
新しい Scene でGameObject -> Sphere を作成し、MeshRenderer の Materials に先ほど作成したマテリアルをセットします。

次のように表示されればOKです。

まだシェーダを変更していないため、デフォルトの処理が行われた結果が描画されています。

最もシンプルなシェーダを書いてみる

次に、Unityにおけるシェーダの文法を説明します。

先ほど作ったシェーダを開いて、中身を変更していきます。
まず、全てを削除して下記のように記述してください。

Shader "Custom/MyShader"
{
    SubShader
    {
        Pass
        {
        }
    }
}

この部分は現時点ではいったん「おまじない」で済ませてしまっても構いませんが、以下で簡単に説明します。

まずShaderキーワードでシェーダのUnityにおけるメニュー名を定義しています。
これは任意で、シェーダのファイル名と一致する必要はありません。

次にSubShaderブロックを定義しています。
SubShaderは複数定義することでPCとスマートフォンなど、プラットフォームごとに処理を切り替えることができるものです。

最後にPassを定義しています。
Passは、オブジェクトのレンダリング処理を記述する部分です。
複数パスを定義することも可能で、その場合には複数回のレンダリング処理が走ることとなります。

この状態だとPassに何も処理を書いていないため、オブジェクトは白くレンダリングされます。

次に、Pass内のレンダリング処理を以下のように記述します。

Shader "Custom/MyShader"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                half4 vertex : POSITION;
            };

            struct v2f
            {
                half4 vertex : SV_POSITION;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }
            
            half4 frag (v2f i) : SV_Target
            {
                return half4(1.0, 0.0, 0.0, 1.0);
            }
            ENDCG
        }
    }
}

上記では、まず CGPROGRAM キーワードと ENDCG キーワードでシェーディング言語の開始と終了を定義しています。

CGPROGRAM
~~~
ENDCG

次に pragma ディレクティブを記述して、頂点シェーダとフラグメントシェーダの関数を指定します。
ここでは、vert と frag がそれぞれ頂点シェーダとフラグメントシェーダの関数名です。

#pragma vertex vert
#pragma fragment frag

次に、構造体 appdata を定義しています。
これは頂点シェーダに渡す情報を保持するためのものであり、頂点座標やuv座標、頂点カラーなどを持つことができます。

struct appdata
{
    half4 vertex : POSITION;
};

今回のように変数に POSITION セマンティクスを付けた場合には、その変数には頂点座標の情報が入ってきます。

頂点シェーダではこの appdata 構造体を引数にとっています。

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

頂点シェーダでやっている処理は UnityObjectToClipPos(v.vertex) のみです。
これはUnityであらかじめ定義された関数であり、最初の節で説明した「3次元上の頂点をディスプレイ上のどこに描画するか、という座標の計算」をしています。

また、頂点シェーダは v2f 構造体を返却します。

struct v2f
{
    half4 vertex : SV_POSITION;
};

SV_POSITION セマンティクスをつけた変数に頂点シェーダで計算した頂点座標を代入することで、この値をフラグメントシェーダなどの後工程に受け渡します。

フラグメントシェーダは、最初の節で説明した通り「ピクセルを何色に塗るかを決め」ます。
そのため、返却する値はRGBAの4次元の値になります。

half4 frag (v2f i) : SV_Target
{
    return half4(1.0, 0.0, 0.0, 1.0);
}

ここでは単純に、RGBA(1, 0, 0, 1)、つまり赤を返却しています。

このシェーダを適用すると、以下のように描画されます。

頂点シェーダを変更する

上記の頂点シェーダでは単純に座標変換のみを行なっているだけだったので、もう少しだけ複雑な処理をしてみます。

Shader "Custom/MyShader"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                half4 vertex : POSITION;
                // UV座標
                half2 uv : TEXCOORD;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                // 擬似乱数を生成
                half rand = frac(sin(dot(v.uv, fixed2(12.9898, 78.233))) * 43758.5453);
                // 頂点を押し出す
                o.pos = v.vertex * (1 + rand * 0.3);
                o.pos = UnityObjectToClipPos(o.pos);
                return o;
            }
            
            half4 frag (v2f i) : SV_Target
            {
                return half4(1.0, 0.0, 0.0, 1.0);
            }
            ENDCG
        }
    }
}

まず、appdata に uv という変数を追加しています。

// UV座標
half2 uv : TEXCOORD;

TEXCOORD セマンティクスを用いることで、uv座標を受け取ることができます。

また、頂点シェーダで頂点座標を書き換えています。

// 擬似乱数を生成
half rand = frac(sin(dot(v.uv, fixed2(12.9898, 78.233))) * 43758.5453);
// 頂点を押し出す
o.pos = v.vertex * (1 + rand * 0.3);
o.pos = UnityObjectToClipPos(o.pos);

まず、randにuv座標をシード値にした0〜1の擬似乱数を求めて格納しています。

次に、その乱数を0.3倍したものを頂点座標に掛けて、それを頂点座標に足し合わせています。
つまり、頂点をランダムで少しずつ外側に押し出しています。

最後に、上記のようにして計算した頂点座標を座標変換しています。

このシェーダを球体に適用すると、以下のようになります。

輪郭がギザギザになっていて、頂点がランダムで押し出されていることがわかります。
頂点シェーダでは、このように頂点座標を自由に変えることができます。

フラグメントシェーダを変更する

次に、今まで赤色を返すだけだったフラグメントシェーダの処理も変更してみます。

Shader "Custom/MyShader"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                half4 vertex : POSITION;
                half2 uv : TEXCOORD;
            };

            struct v2f
            {
                half4 pos : SV_POSITION;
                // 変換前の頂点座標
                half4 pos2 : TEXCOORD0;
            };
            
            v2f vert (appdata v)
            {
                v2f o;
                half rand = frac(sin(dot(v.uv, fixed2(12.9898,78.233))) * 43758.5453);
                o.pos = v.vertex * (1 + rand * 0.3);
                o.pos = UnityObjectToClipPos(o.pos);
                // 変換前の頂点座標を代入
                o.pos2 = v.vertex;
                return o;
            }
            
            half4 frag (v2f i) : SV_Target
            {
                // 頂点座標をそのまま色として返す
                return i.pos2;
            }
            ENDCG
        }
    }
}

まず、頂点シェーダからフラグメントシェーダに対して、変換前の頂点座標を受け渡しています。

// 変換前の頂点座標を代入
o.pos2 = v.vertex;

pos2 は、TEXCOORD0 セマンティクスを付けた変数として v2f 構造体に定義しました。
頂点シェーダからフラグメントシェーダに値を渡す場合には、このように TEXCOORD セマンティクスを付けた変数を使います。

また、フラグメントシェーダではこの頂点座標をそのまま色として返却しています。

// 頂点座標をそのまま色として返す
return i.pos2;

これは例えば、頂点座標が(0, 0, 0)の地点であればフラグメントシェーダの返す色はRGB(0,0,0)、つまり黒になり、頂点座標が(1,0,0)の地点はRGB(1,0,0)、つまり赤になるということになります。

これを適用したものが下図です。

頂点座標に応じて着色されていることがわかります。
このように、フラグメントシェーダでは出力する色を自由に決めることができます。

発展:ラスタライザ

ここまで座標変換をする頂点シェーダと、そのあとに処理されるフラグメントシェーダについて説明してきました。
しかし、より正確にはこの二つの処理の間にはラスタライザが行うラスタライズという処理が挟まれます。

ラスタライザの主な処理の1つ目は、レンダリングされるピクセルを決定することです。
頂点シェーダで変換された座標情報はあくまで頂点ごとの座標情報であるため、具体的にどのピクセルに描画されるのか、という情報は持っていません。
そこでラスタライザが、座標情報を元に描画するピクセルを計算します。

次に、頂点シェーダから渡された情報をラスタライザが補間します。

例えばある頂点から送られてきた色情報がRGBA(1, 0, 0, 1)、もう一つの頂点から送られてきた色情報がRGBA(0, 1, 0, 1)であったとします。
この二つの頂点のちょうど中間地点のピクセルの色情報を求める場合、上記二つの色情報を半分ずつ混ぜ合わせてRGBA(0.5, 0.5, 0, 1)とする計算を行う必要があります。

このように、頂点シェーダから送られてきた頂点ごとの情報を補間してピクセル単位の情報にする処理をラスタライザが行います。

まとめると、ラスタライザとは頂点シェーダとフラグメントシェーダの橋渡しのような役割をするものだといえます。

まとめ

今回は、以下のことを説明しました。

  • シェーダの概念
  • シェーダの文法
  • 頂点シェーダの例
  • フラグメントシェーダの例
  • ラスタライザ

次回からは、シェーダを使って光と陰を表現する処理、ライティングをしていきます。

バックナンバー