前回の記事ではShader Graphを用いて、Unity Shaderを実装する超基本的な解説をしました。
しかし、Shader Graphだけを使っていては実現できない表現や、最適な実装方法がわからなくなるケースが必ず出てきます。
今回はそうした場面にも対応できるように、ShaderLab+HLSLによってUnlit Shaderを手書きで実装する方法を、構文の意味と役割を1つずつ丁寧に解説していきます。
Shader Graphでつまずいたときのヒントを得たい方、Custom Functionノードを使いたい方、Shaderの本質的な仕組みを理解したい方にとって、役立つ内容になっています。
なぜShaderLabを学ぶのか?
さて、Shader Graphさえ学んでおけばいいんじゃないか?と言いたくなると思うのですが、私はShaderLabも合わせて学んだ方がいいと考えています。
過去の記事でShader GraphとShaderLabのどちらを選択すべきかを記載しましたが、これに加えてShader Graphの実装力も確実に上がると思っているからです。
主に2つの理由です。
ShaderLabの実装からShader Graphの実装のヒントを得られる
Shader Graphで実装中に、どうしても実装したい表現があるのに実装方法がわからない、といったケースはあります。
そんな時にShaderLabならどう実装するのか?を調査することで、Shader Graphの実装の足がかりにできます。
ShaderLabの実装方法を調べ、そこで得た知見でそれをShader Graphで実装するには?という順番に検索することでShader Graphへの実装へと繋げられるからです。
Custom Functionノードの実装ができる
Custom Functionノードというのは、Shader Graphの中にHLSLのコードを埋め込むことができるノードです。
このノードを使うことでShader Graphだとノードを繋いで繋いで繋いで実現しなければならない大変な表現も1つのノードで実現できるかもしれません。
そんな時にHLSLを知らないと開発効率が悪くなってしまいます。
作業効率が落ちるだけならまだしも、大規模なShader Graphを描こうとすると動作が重くなることで、フラストレーションが溜まる状況に陥ってしまうかもしれません。
Unityも日々アップデートをしているので、その頻度は減っていくでしょうが、対策方法を知っておくことでそういう状況をさらに軽減できるようになります。
Unlit Shaderで見るShaderLab構文と実行構造
以下のコードがUnlit Shaderで「常に同じ色になる」シェーダーです(Unlit、つまりは光の影響は受けません)。
Shader "Custom/UnlitColor" // Shader名。マテリアル上での選択に使う { Properties { _MainColor ("Main Color", Color) = (1,0,0,1) // マテリアル側で操作できるパラメータ } SubShader { Tags { "RenderType"="Opaque" } // 描画順やZバッファの扱いに関係 Pass { CGPROGRAM #pragma vertex vert // 頂点シェーダー関数 #pragma fragment frag // フラグメントシェーダー関数 struct appdata { float4 vertex : POSITION; // モデルの頂点情報 }; struct v2f { float4 pos : SV_POSITION; // 最終描画位置 }; fixed4 _MainColor; // Propertiesで定義した色 v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); // MVP変換(モデル→ビュー→投影) return o; } fixed4 frag (v2f i) : SV_Target { return _MainColor; // フラグメントシェーダーで色を出力 } ENDCG } } }
このシェーダーをベースに解説していきます。
ShaderLabの各ブロックの説明
Shader名
Shader "Custom/UnlitColor"
この名前はUnityのマテリアルInspector上で表示される階層になります。
Custom/UnlitColorとすれば、「Custom → UnlitColor」と選択される構造になります。
超重要な注意点が一つ。
この名前は重複しないようにしてください!
重複してもエラーにならない上に、シェーダーを選択する一覧に両方表示され、しかもどちらを選択したのか不定になってしまうので、非常に危険な状態になってしまいます。
Propertiesブロック
Properties { _MainColor ("Main Color", Color) = (1,0,0,1) }
マテリアルから変更可能なパラメータを定義します。ここで定義した変数は、HLSL側で使用できるグローバル変数になります。
- "Main Color" はインスペクターに表示されるラベル
- Color はデータ型(他に2D, Float, Range などもある)
- (1,0,0,1) はデフォルト値(この場合は赤)
この設定によって、マテリアルのカラーピッカーから色を操作できるようになります。
SubShaderブロック
SubShader { // この中に描画手順を定義 }
SubShaderはPassブロックやCGPROGRAM〜ENDCGによるシェーダーコードブロックを記載して、実際の描画処理を定義するセクションです。
Unityは環境やレンダーパイプラインに応じて、SubShaderの中から最適なものを選んで実行します。
複数のSubShaderを用意することで、例えば高性能GPU向け/低性能GPU向けのシェーダーをそれぞれ記載することができたりします。
Tagブロック
Tags { "RenderType"="Opaque" }
これは「このシェーダーはUnity内部的に“どういう種類の描画物”として扱われるか」を宣言する分類用メタ情報です。
実際の動作上だと省略してもいいのですが、書かないことで、透明物が前に出ない/ポストプロセス効かない、などの事例が起こり得ます 。
描画順・フィルタリング・SRP処理など、Unity内部処理に大きな影響を与えるので、記載することを推奨します。
上記で指定してるRenderType = Opaqueは不透明なオブジェクト(後ろのオブジェクトが見えない)であることを指定しています。
Passブロック
Pass { CGPROGRAM // この中にシェーダーコードを記載する ENDCG }
SubShaderブロックの中にPassブロックを記載します。
Passブロックは、実際に描画処理を1回実行する単位で、Passの中にシェーダーコードブロックをHLSLで記載します。
HLSLによるシェーダーコードブロックの解説
前置きがめっちゃ長くなった気がしますが、いよいよHLSLによるシェーダーコードブロックの解説です。
#pragmaディレクティブ
#pragmaディレクティブとは、コンパイラやシェーダーコンパイラに対して特定の命令や設定を指示するための命令です。
UnityのShaderにおける#pragmaは、特定の関数をシェーダーの「エントリーポイント」として指定したり、特定の処理を有効にしたりするために使います。
下記の例では、#pragmaディレクティブで、vertを頂点シェーダーの関数名、fragをフラグメントシェーダーの関数名としています。
#pragma vertex vert #pragma fragment frag
各シェーダーの入力値の構造体定義
struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; };
appdata構造体を頂点シェーダーの入力値の構造体として、v2fがフラグメントシェーダーの入力値の構造体として定義しています。
重要なことはPOSITION、SV_POSITIONです。
これはセマンティクと呼ばれるもので、GPUから値を受け取るために指定するものです。
POSITIONはモデルの頂点座標を示し、SV_POSITIONはスクリーンの描画座標を示します。
v2f vert (appdata v) { // 頂点シェーダーの処理 } fixed4 frag (v2f i) : SV_Target { // フラグメントシェーダーの処理 }
各シェーダーの定義は上記のようになっており、vert関数の引数にはappdata構造体を受け取り、戻り値としてv2f構造体を戻しています。
そしてfrag関数の引数にv2f構造体の入力を受け付けるようになっています。
そして同じセマンティクスが一致する構造体が出力と入力として指定されているので、頂点シェーダーの出力をフラグメントシェーダーの入力して、自動で繋げてくれます。
つまり厳密にいうなら、同じ構造体じゃなくてもセマンティクスが完全に一致すれば、自動で接続してくれるので、下記のようなシェーダーでもちゃんと動きます。
struct OutData { float4 foo : SV_POSITION; }; struct InData { float4 bar : SV_POSITION; }; OutData vert(appdata v) { // 頂点シェーダーの処理 } fixed4 frag(InData i) : SV_Target { // フラグメントシェーダーの処理 }
Propertiesとのリンク
fixed4 _MainColor; // Propertiesで定義した色
Propertiesブロックに_MainColorという値を定義し、インスペクター上で入力できるようにしています。
これをSubShaderのシェーダーブロック内で使用するためには、下記のように同じ名前で定義し直す必要があります。
各シェーダーの処理
頂点シェーダーの処理
v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); // MVP変換(モデル→ビュー→投影) return o; }
GPUにより、v.vertexに入力されたモデル座標が入力として渡され、UnityObjectToClipPos関数により、クリップ空間の投資投影込みの座標に変換されます。
変数posはSV_POSITIONセマンティックが指定されているため、次のfragメソッドに渡される時に、posの値はスクリーン空間の座標に裏で自動変換されて渡されることになります。
vはモデル座標ごとにある値なので、vert関数はモデルの頂点の数だけ実行されることとなります。
フラグメントシェーダーの処理
fixed4 frag (v2f i) : SV_Target { return _MainColor; // フラグメントシェーダーで色を出力 }
スクリーン座標に変換された座標に対して、出力する色を戻り値で示します。
SV_Targetセマンティックを指定していることでGPUに「色を出力する」というのを伝えています。
シェーダーファイルの作成とオブジェクトへの適用
さて先ほどのシェーダーファイルを作って、オブジェクトに適用していきましょう。
Shaderファイルは、プロジェクトの右クリックメニューから、Create > Shader > Unlit Shaderを選択すれば、ファイルを作れます。
デフォルトで作成されるUnlit Shaderはテクスチャを適用するシェーダーで、後ほど解説します。
今は先ほど解説した色を指定して表示するだけのシェーダーを適用してみましょう。
というわけで、作成したUnlit Shaderを開いて、以下のシェーダー(再掲)をコピペしましょう。
Shader "Custom/UnlitColor" // Shader名。マテリアル上での選択に使う { Properties { _MainColor ("Main Color", Color) = (1,0,0,1) // マテリアル側で操作できるパラメータ } SubShader { Tags { "RenderType"="Opaque" } // 描画順やZバッファの扱いに関係 Pass { CGPROGRAM #pragma vertex vert // 頂点シェーダー関数 #pragma fragment frag // フラグメントシェーダー関数 struct appdata { float4 vertex : POSITION; // モデルの頂点情報 }; struct v2f { float4 pos : SV_POSITION; // 最終描画位置 }; fixed4 _MainColor; // Propertiesで定義した色 v2f vert (appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); // MVP変換(モデル→ビュー→投影) return o; } fixed4 frag (v2f i) : SV_Target { return _MainColor; // フラグメントシェーダーで色を出力 } ENDCG } } }
次にマテリアルを作成し、マテリアルのシェーダーを指定するところから、「Custom > UnlitColor」を選択します。
ちなみにデフォルトで表示されている「Test Shader」については気にしなくていいです。
関連記事:Unityでシェーダーに「Test Shader」があるんだが?
作成してマテリアルを適用してGameビューを確認すると、下記のようにCubeが表示されていると思います。
ここでインスペクター上にあるMainColorを変えれば、それに応じてCubeの色も変わりますね!
このシェーダーは光の影響を受けないので、影(陰)が全くできないので、のっぺりした表示なっていると思います。
注意点としては、色を確認するときはSceneビューではなく、Gameビューで確認するようにしましょう。
ライティングやスカイボックスの影響で色が異なって見えることがあります!
シェーダーでテクスチャを扱う
次に先ほど後回しにした、デフォルトで作られるUnlit Shaderの解説をします。
デフォルトでは下記のようなシェーダーが作られていると思います。(バージョによって違うかも・・・)
Shader "Unlit/NewUnlitShader" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag // make fog work #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; UNITY_FOG_COORDS(1) float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } fixed4 frag (v2f i) : SV_Target { // sample the texture fixed4 col = tex2D(_MainTex, i.uv); // apply fog UNITY_APPLY_FOG(i.fogCoord, col); return col; } ENDCG } } }
先に解説したものとの違いを見ながら、解説していきます。
テクスチャーをオブジェクトに適用する
このシェーダーでインスペクターで指定した画像をオブジェクトに適用するには下記のステップを実施しています。
- Propertiesで画像を指定できるようにする
- 各シェーダーの入力にUV座標を加える。
- 頂点シェーダーで、マテリアルで設定されたスケールとオフセットを反映させたテクスチャ画像の、どの位置を参照するかを確定するための処理を行う。
- フラグメントシェーダーで、スクリーン座標に対して、テクスチャ画像のUV座標の色を確定するための処理を行う。
以下、解説します。
Propertiesで画像を指定できるようにする
Properties { _MainTex ("Texture", 2D) = "white" {} }
_MainTexという名前で、2D型(つまりは2D Texture)を指定しており、whiteについてはデフォルト値です。
このシェーダーではインスペクターで画像を指定して使用しますが、Unityがデフォルトで用意している内部的なテクスチャをデフォルトで指定します。
通常は空文字でも問題ありません。
最後の{}は追加オプションを指定する部分で、特殊な状況でしか使わないので、ここは空のままで問題ありません。
SubShader内での定義
sampler2D _MainTex; float4 _MainTex_ST;
Propertiesで定義したものをSubShader内で使えるように定義します。
ここで_MainTexは同じ名前を使用するのは先ほどの例と同じですが、ここに追加で_MainTex_STというものが定義されています。
_MainTex_STにはTilingとOffsetの情報がバインディングされます。
そのため(name)_STという命名規則は守る必要があります。
各シェーダーの入力にUV座標を加える
struct appdata { float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; }
UV座標のみだけだと上記となります。
TEXCOORD0セマンティックで、GPUによって補間した値を入れる変数であることを示します。
頂点シェーダーの処理
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
頂点シェーダーは、このマクロでインスペクターで設定しているテクスチャ画像とセットになっているtilingとoffsetの情報を使って、UV座標をずらして、オブジェクトの頂点ごとにテクスチャのどの座標を使うかを決めています。
フラグメントシェーダーの処理
fixed4 col = tex2D(_MainTex, i.uv);
最後にフラグメントシェーダーで、頂点シェーダーで求めたUV座標からピクセルごとに補間されたUV座標とテクスチャ画像から、色を決めます。
fogの影響を与える
テクスチャ画像とUV画像から色が決まりますが、そこにfogの影響を与えるためには、下記の4ステップが必要です。
- fog用のディレクティブの定義
- fog用のvarying変数を定義
- 頂点シェーダーでフォグの影響度の元になる距離する
- フラグメントシェーダーでフォグの影響度を反映させた色を決める
順番に説明しましょう。
fog用のディレクティブの定義
#pragma multi_compile_fog
これは「このShaderでフォグ対応もしたいから、フォグあり・なし両方のバージョンを用意しておいてくれ」と命令するディレクティブ(プリプロセッサ命令)です。
要はフォグの影響を受けるオブジェクトにしたいなら、この一行をまず入れておく、ということですね。
fog用のvarying変数を定義
UNITY_FOG_COORDS(1)
頂点シェーダーからフラグメントシェーダーに渡す値を、上記のような記述で定義します。
このマクロを記載することでv2f構造体にfogcoordという名前のvarying変数を定義できます。
1はvarying変数TEXCOORD1 に fogCoord を割り当てるということです。
※頂点シェーダーからフラグメントシェーダーに渡す変数のこと、セマンティクスにより補間するようにGPUに認識させている変数をvarying変数と呼びます。
頂点シェーダーでフォグの影響度の元になる距離する
UNITY_TRANSFER_FOG(o,o.vertex);
このマクロによりSV_POSITION から、フォグの影響計算に必要な「カメラとの距離」を計算し、fogCoord に代入しています。
フラグメントシェーダーでフォグの影響度を反映させた色を決める
UNITY_APPLY_FOG(i.fogCoord, col);
このマクロにより、頂点シェーダーから渡されたfogCoord を使い、オブジェクトの色にフォグの色を適用させます。
処理としてはlerp関数をコールしていると考えればわかりやすいかと思います。
フォグの影響度が高ければフォグの色になり、フォグの影響度が低ければオブジェクトの色になるイメージです。
#include "UnityCG.cginc"
って何よ?
ここで一つだけ飛ばしている内容がありますね。
それは#include "UnityCG.cginc"
です。
これは下記のようなUnityが用意しているビルトインシェーダー関数を使うためのものです。
- UnityObjectToClipPos
- UNITY_FOG_COORDS
- UNITY_TRANSFER_FOG
- TRANSFORM_TEX
- UNITY_APPLY_FOG
UnityCG.cgincがなくてもシェーダー自体は組めますが、せっかくあるものなので使ったほうがいいですね。
バージョンアップで変更が入っても、吸収してくれますし。
とりあえず入れとけ、って感じの1文です。
マテリアルに適用して動かす
デフォルトのままでインスペクターで画像を適用してみると、以下のようになっています。
TilingやOffsetを動かしてみて、シェーダーでどう処理しているのか、イメージしながら動かしてみてくださいね。
まとめと次回予告
今回の記事では、Unity Shaderにおける手書き実装の第一歩として、Unlit Shaderを例にShaderLabとHLSLの基本構文を解説しました。
構文ごとの役割を理解することで、Shader Graphで表現しづらい処理のヒントが得られるだけでなく、Custom Functionノードの活用やShaderの自作に踏み出せるようになります。
次回は、Unlit Shaderにテクスチャを適用したShaderをベースに、より柔軟な表現力を手に入れる実装に進んでいきます。