Unityで頂点色を表示するシェーダを作る

はじめに

前回Meshクラスを利用してポリゴンデータを作成しました。 今回は、前回の記事で出てきた頂点カラーを表示するマテリアルのシェーダを作りながら解説をします。

実行結果

Unityのバージョン: 2018.2.5f1

Unityのシェーダ

Unityでマテリアルを作成するとShaderという欄があります。

マテリアルのShader欄

マテリアルとシェーダは一体どのような関係なのでしょうか。

Unityではシェーダとマテリアルは密接に関係しています。 マテリアルはシェーダの実際の処理が書かれたもので、 マテリアルはシェーダのインスタンスに当たります。 同一のシェーダからプロパティの違うマテリアルを複数作成できます。

マテリアルとシェーダの関係


Unityのシェーダは、ShaderLabというUnityのシェーダ独自の形式を用います。 実際のシェーダ部分はCg/HLSL言語で書きます。 CgとHLSLはどちらもシェーダ用の言語で非常によく似ています。 シェーダを書く際のドキュメントとしてはこちらのMicrosoftのHLSLのドキュメントが参考になります。

レンダリングパイプライン

GPUに頂点の情報を渡してから画面に描画されるまでにはいくつもの工程を経ます。 この工程のことをレンダリングパイプラインと呼びます。 シェーダを理解するにはこのレンダリングパイプラインの理解が欠かせません。 ここではレンダリングパイプラインについて軽く説明をします。

レンダリングパイプラインはいくつかのステージに分かれています。 簡略化したレンダリングパイプラインのステージを次の図に示します。

レンダリングパイプライン

本来はテッセレーションステージやジオメトリステージなど、さまざまなステージが存在しますが、 ここでは省略をします。

次に各ステージについて軽く解説をします。

入力アセンブラ

CPUから渡されたMeshクラスの頂点の座標や頂点カラーなどの頂点属性を 次のステージに渡す役割を担います。

前回の記事でMeshを作成し頂点色やUVを渡しました。 それらの情報が入力として次のステージに渡されます。

頂点シェーダ

頂点情報を受け取り、適切な座標変換を行い次のステージへ渡します。

ラスタライザ

渡された頂点のクリッピング座標系の座標から画面上のピクセルの情報に変換します。 頂点の間にあるピクセルについては値は線形補間されて次のステージに渡されます。

ラスタライザ

フラグメントシェーダ

各ピクセルごとに実行され、最終的に画面に表示するピクセルの色を決定します。 ピクセルシェーダと呼ばれることもあります。


この入力ステージのうち、プログラマがプログラムできるのは 頂点シェーダとフラグメントシェーダの部分になります。 画面に表示する部分ではZテストやアルファブレンディングなどの指定もShaderLabから行えますが、 今回はひとまず頂点シェーダとフラグメントシェーダにのみ触ってみます。

単色塗りつぶしシェーダの作成

実際にシェーダを書いてマテリアルを作ってみましょう。 最初は単色で塗りつぶす単純なシェーダを書きます。

まずはShaderを作成します。 Projectビューで右クリックをして「Create/Shader/Unlit Shader」を選択しましょう。

Unlit Shader

Shaderを作ったら次のように書き換えます。

Shader "MyShader/Red"
{
  SubShader
  {
    Pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
      };

      struct v2f
      {
        float4 vertex : SV_POSITION;
      };

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

      float4 frag (v2f i) : SV_Target
      {
        return float4(1, 0, 0, 1);
      }
      ENDCG
    }
  }
}

fogなどの不必要な情報を消し、シェーダに名前を与えています。

CGPROGRAMENDCGがシェーダのプログラムとなっています。

このシェーダはvertfragの2つの部分によってできています。 #pragma文で頂点シェーダとフラグメントシェーダに それぞれvertfragの関数を指定しています。 これによって、それぞれが頂点シェーダとフラグメントシェーダとなります。

セマンティクス

構造体の定義で書かれているコロンから続く: SV_POSITIONなどは セマンティクスと呼ばれています。 セマンティクスは関数の仮引数、関数の戻り値、構造体に対して与えられます。

レンダリングパイプラインとの値の受け渡しはセマンティクスをつけた変数を利用します。 レンダリングパイプラインの前のステージから受け取る値と、 次のステージへ受け渡す値には適切なセマンティクスをつけます。

各セマンティクスについてはこちらのドキュメントを参考にしてください。

頂点シェーダ

頂点シェーダでは座標情報をappdata構造体で受け取り、 座標変換して頂点のクリッピング座標をv2fで次のステージに渡します。

頂点シェーダ内に出てくるUnityObjectToClipPos(v)UnityShaderUtilities.cgincに定義されています。 UnityShaderUtilities.cgincUnityCG.cginc内部でインクルードされています。

UnityShaderUtilities.cgincは私の環境ではC:\Program Files\Unity\Hub\Editor\2018.2.5f1\Editor\Data\CGIncludes\UnityShaderUtilities.cgincに置かれていました。

UnityObjectToClipPos(v)の定義を抜粋すると次のようになっています。

// Tranforms position from object to homogenous space
inline float4 UnityObjectToClipPos(in float3 pos)
{
#if defined(STEREO_CUBEMAP_RENDER_ON)
    return UnityObjectToClipPosODS(pos);
#else
    // More efficient than computing M*VP matrix product
    return mul(UNITY_MATRIX_VP, mul(unity_ObjectToWorld, float4(pos, 1.0)));
#endif
}
inline float4 UnityObjectToClipPos(float4 pos) // overload for float4; avoids "implicit truncation" warning for existing shaders
{
    return UnityObjectToClipPos(pos.xyz);
}

#ifによる条件分岐はコンパイル時に条件分岐が行われます。

STEREO_CUBEMAP_RENDER_ONはステレオの360度画像のときに使われるようです。 VR用の360度動画の撮影の機能がUnityの2018.1から追加されました。 2017.4のUnityShaderUtilities.cgincを見るとこの条件分岐が存在していません。 このステレオ動画用の投影はGoogleのOmni-Directional Stereo(ODS)と呼ばれる 技術を使っているようですね。 特殊な投影を行うときにのみこちらの分岐に入るので、 通常時は#elseのほうが実行されるようです。

通常時のレンダリング処理では、引数として渡されたデータに対して Model行列とVP行列を掛けた値を返しています。 このMVP行列についてはまた別の記事で解説をする予定です。 ここでは画面上に正しく表示するための座標変換が行われている、 とだけ理解しておけば問題ありません。

(2018/08/30追記:MVP行列についての記事を書きました)

フラグメントシェーダ

SV_TARGETセマンティクスに対してfloat4(1, 0, 0, 1)を渡しています。 これはピクセルの色のRGBA成分に対して、それぞれ1、0、0、1の色を指定しています。 これで不透明の赤が指定されたことになります。

シェーダを利用する

シェーダが書き上がったので実際に利用してみましょう。

シェーダはマテリアルの形で利用します。 マテリアルを作成し、Shaderの欄から自分の作成したシェーダを選びます。

マテリアルで自分のシェーダを選ぶ

このマテリアルをMeshRendererに渡してやると、 このシェーダを利用して描画が行われます。

このシェーダの実行結果は次のとおりです。

実行結果

フラグメントシェーダで指定した色で塗りつぶされているのが確認できました。

型修飾子

先のシェーダでは次のステージへの値の受け渡しに関数の戻り値を利用していました。 これを出力引数にすることもできます。

関数の引数の定義の前にoutをつけることで出力引数となります。

このような引数の前につける修飾子を型修飾子といいます。 型修飾子にはininoutoutの3種類が存在します。 inは入力用引数、inoutは入力にも出力にも使う引数、outは出力に使う引数となります。 型修飾子を省略するとデフォルトでinを指定したことになるので、 通常はinを指定する必要はありません。

先のコードを出力引数を使って書き直してみましょう。 inはなくても動きますが、outと対称になってきれいなので 入力引数にはinをつけてみます。

Shader "MyShader/Red"
{
  SubShader
  {
    Pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
      };

      struct v2f
      {
        float4 vertex : SV_POSITION;
      };

      void vert (in appdata v, out v2f o)
      {
        o.vertex = UnityObjectToClipPos(v.vertex);
      }

      void frag (in v2f i, out float col : SV_Target)
      {
        col = float4(1, 0, 0, 1);
      }
      ENDCG
    }
  }
}

このように書き換えても、さきほどと同じように動作することが確認できます。


頂点カラーを表示するシェーダの作成

さきほどのシェーダを改造して頂点カラーを表示するシェーダを作ってみます。

Shader "MyShader/VertexColor"
{
  SubShader
  {
    Pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #include "UnityCG.cginc"

      struct appdata
      {
        float4 vertex : POSITION;
        float4 color: COLOR;
      };

      struct v2f
      {
        float4 vertex : SV_POSITION;
        float4 color : COLOR;
      };

      void vert (in appdata v, out v2f o)
      {
        o.vertex = UnityObjectToClipPos(v.vertex);
        o.color = v.color;
      }

      void frag (in v2f i, out float4 col : SV_Target)
      {
        col = i.color;
      }
      ENDCG
    }
  }
}

頂点シェーダの入力のappdataCOLORセマンティクスを追加しました。 これで頂点カラーの情報を受け取ることができます。

受け取った頂点カラーをフラグメントシェーダに渡すべく、 v2fにもCOLORセマンティクスを追加しました。

頂点シェーダでは、受け取った頂点カラーをそのままo.colorに受け渡しています。 これで頂点カラーがラスタライザで線形補間されてピクセルシェーダに渡されます。

ピクセルシェーダでは、ラスタライザから渡されたi.colorをそのまま返しています。

さきほどと同様にマテリアルを作成しMeshRendererに渡します。 今回は三角形の頂点に赤、青、緑を指定したMeshをレンダリングしてみました。 実行結果は次のようになります。

実行結果

ピクセルシェーダに渡された色は頂点カラーを線形補間したものになっていることがわかります。

おわりに

今回はレンダリングパイプラインを非常に大雑把に説明し、 頂点カラーを表示するシェーダの作成まで行いました。

今回は頂点シェーダでのUnityObjectToClipPos(v)について何も解説していません。 次回にUnityObjectToClipPos(v)で行われているMVP行列を掛ける処理について解説します。

ソースコードはこちらのリポジトリにおいておきます。

この記事についてなにか気づいた点があれば@MatchaChoco010まで。

  • Unity
  • Shader