はじめに
Unityのシェーダーを勉強しはじめた時、公式サイトを眺めてみてもイマイチ全体像が把握出来ないなぁと感じていました。公式サイトは機能単位で記述されているので順番に学んでいくのには適していないんですね。
そこで、この記事ではシェーダーを書くための必要最低限な情報に絞って記述していきたいと思います。できるだけ詳細を省き、とにかく簡単なシェーダーを書くことを目標とします。
最初にUnityのシェーダーについて概要を把握し、その後に簡単なシェーダーを作成していきます。
参考
GitHub
今回使用するコードなどはこちらからダウンロードできます。
https://github.com/kakureusagi/UnityShaderTest/tree/master/Assets/SimplestShader
シェーダーの種類
Unityでシェーダを記述する方法は3つあります。
サーフェイスシェーダー(Surface Shader)
サーフェイスシェーダーはライティングや影を扱う場合に使用します。ライティング計算は複雑なものになりますが、このサーフェイスシェーダーを使用することで簡単に記述することができるようになります。
ShaderLab+Cg/HLSLで記述します。ShaderLabとCg/HLSLについては後述します。
コンパイル後に頂点・フラグメントシェーダーが生成されます。
頂点・フラグメントシェーダー(Vertex and Fragment Shader)
最も柔軟に記述することができます。頂点シェーダーで頂点の位置情報などを加工したあと、フラグメントシェーダーで最終的なピクセルの色を決定します。ライティング計算を行う必要がないものや、より高度なエフェクト・表現を作成したい場合に使用します。
フラグメントシェーダーはDirectXではピクセルシェーダーとも呼ばれます。
ShaderLab+Cg/HLSLで記述します。
固定関数シェーダー
固定関数シェーダーはGPUがプログラマブルシェーダーをサポートしていない場合に使用します。単純な機能のみを提供するようです。
ShaderLabに沿って記述します。
この記事では解説しません。
参考
シェーダーを書く(Unity)
ShaderLab と固定関数シェーダー
Cg/HLSL
Unityで採用されているシェーダー言語です。
CgはNVIDIAが開発したもの、HLSLはNVIDIAとMicrosoftが共同で開発したもので、実際的に使用する分には両者はほぼ同等のようです。どちらもC言語風に記述をしていきます。
ここでは深掘りせずにサンプルを見ながら覚えていきます。基本的なプログラムが分かればスタート時点の知識としては十分だと思います。
参考
Unityで使用するシェーダー言語(Unity公式)
Cg(プログラミング言語)(Wikipedia)
HLSL(Wikipedia)
ShaderLab
ShaderLabはUnityでシェーダーを記述するための言語です。
UnityでMonoBehaviorを継承したクラスを作成することでインスペクターにフィールドを表示できるように、ShaderLabに沿ってシェーダーを記述することでマテリアルインスペクターにシェーダーのプロパティーを表示することができます。
具体的には以下のような書き方をします。
//ここにシェーダーの名前を記述する
Shader "Custom/Sample" {
Properties {
//ここにマテリアルインスペクターに表示したいプロパティを記述する
}
SubShader {
//ここにシェーダー本体を記述する
}
}
ポイントは大きく3つあります。順番に見ていきます。
Shaderの名前
Custom/Sample
の部分がシェーダーの名前になります。
この名前でマテリアルにシェーダーを割り当てることが出来ます。
Propertiesブロック
この部分に記述したプロパティーがマテリアルインスペクターに表示されます。
Unityとシェーダー内のプロパティーを繋ぐ部分になります。
ここに記述しないプロパティーもシェーダー内に記述することはできますが、マテリアル内には値が保存されません。
SubShaderブロック
ここには実際のシェーダー本体を記述します。次の項で説明していきます。
参考
ShaderLab(Unity)
ShaderLab と固定関数シェーダー
サーフェイスシェーダーを作成する
それでは実際に簡単なサーフェイスシェーダーを作成していきます。
頂点・フラグメントシェーダーはその後作成します。
シェーダーファイルを作成する
[プロジェクトビューで右クリック] → [Create] → [Shader] → [Standard Surface Shader]
シェーダーファイルを修正する
作成したファイルにはすでに使える状態のサーフェイスシェーダーが記述されています。
これはこのままで使えるのですが、今回は色を指定するだけのシンプルなシェーダーに変えます。
↓のシェーダーを見ると、ShaderLabに沿った記述になっていることが分かりますね。
Shader "Custom/SimplestSurfaceShader" {
Properties {
_MainColor("Main Color", Color) = (1,1,1,1)
}
SubShader {
Tags {
"Queue"="Geometry"
}
CGPROGRAM
float4 _MainColor;
#pragma surface surf Lambert
struct Input {
float4 vertexColor : COLOR;
};
void surf (Input IN, inout SurfaceOutput o) {
o.Albedo = _MainColor.rgb;
}
ENDCG
}
}
中身の解説
順番に中身を見ていきます。
Propertiesブロック
_MainColor("Main Color", Color) = (1,1,1,1)
ここでは色をマテリアルインスペクターで指定できるようにしています。左から
・_MainColorはシェーダー内で使用する変数名
・"Main Color"はインスペクター上に表示される名前
・Colorはシェーダー内での型
・(1, 1, 1, 1)はこのプロパティーのデフォルト値(白)
です。
他にもfloatやテクスチャなど色々と設定できるものがあります。
SubShaderブロック
Tags { "Queue"="Geometry" }
SubShaderタグを利用して、いつどのようにしてレンダリングするかを指定します。
ここではレンダリングの順番を決めるRenderQueueを指定しています。geometry(デフォルト)の他にTransparent、Overlayなどがあります。透明な物体やUIなど最前面に来るようなオブジェクトを扱うときにRenderQueueはとても重要になります。
タグはKey-Valueペアで指定します。複数個指定することもできます。
CGPROGRAM
~
ENDCG
シェーダーの記述範囲の宣言。この間にシェーダーを記述していきます。
サーフェイスシェーダーではSubShaderブロックの直下に配置しますが、頂点・フラグメントシェーダーでは1段階ネストの階層が深いところに配置します。
float4 _MainColor;
ここではfloatを4つ持つ型float4の変数を宣言しています。Propertiesブロックで宣言した名前と同じ名前を使用していることに注目してください。同じ名前で宣言することで、マテリアルインスペクターで設定した値をシェーダー本体で使用できるようになります。
PropertiesブロックではColor型として宣言していましたがCg/HLSLではfloat4として宣言します。中身はどちらもfloat4つ分の型になります。このようにPropertiesブロックとCg/HLSL言語で型が異なるものが他にもありますが、その辺りは別の記事でまとめたいと思います。
#pragma surface surf Lambert
ここではサーフェイスシェーダーの関数の宣言をしています。
surfの部分が使用する関数名で任意の名前を使用できます。
Lambertの部分ががライティングのモデルで、LambertかBlinnPhong、Standard、StandardSpecularあるいは自作のライティングモデルを指定します。
更にオプションを指定してアルファを使用するかどうかや頂点データの変更などを行うかどうかを指定できます。
オプションはライティングモデルの後に続けて半角スペース区切りで複数指定できます。
アルファを使用する時の例:#pragma surface surf Lambert alpha
struct Input {
float4 vertexColor : COLOR;
};
サーフェイスシェーダー関数の引数としてUnityから受け取る構造体を宣言する必要があるのですが、その中身をここに記述します。
構造体の名前は"Input"で固定です。
ここでは頂点カラーを使用するためにfloat4型のvertexColor変数を宣言しています。:COLORの部分が見慣れない記述ですが、これはvertexColorを頂点カラーとして使いますよ、ということをGPUに伝えています。型と名前だけではこの変数が頂点カラーなのか頂点座標なのかGPUが判断出来ないため、宣言する必要があるんですね。ちなみにこの宣言のことをセマンティクスと呼びます。
今回はインスペクターで指定した色を直接物体の表面の色として扱うためこのvertexColorは使用しません。
他にも頂点のワールド座標や視線ベクトルなど、Unityから受け取れる変数は色々あります。
void surf (Input IN, inout SurfaceOutput o){
o.Albedo = _MainColor.rgb;
}
ここがサーフェイスシェーダー関数本体です。戻り値と引数の型の書き方は決まっています。
ここではマテリアルインスペクターで指定した色をそのままオブジェクトの表面色(Albedo)とします。その他にも鏡面反射や発光具合など設定できます。
普通のプログラミングと比較して、代入部分がやや特殊な記述になっています。Albedoの型はfloat3ですが、_MainColorの型はfloat4です。_MainColorの最初の3成分だけをAlbedoに代入しているわけですね。ちなみにfloat4は.rgbaと.xyzwのどちらの記述でもアクセスできます。
PBR(物理ベースレンダリング)を扱う場合にはSurfaceOuput構造体の代わりにSurfaceOutputStandard構造体やSurfaceOutputStandardSpecular構造体など使うようです。
参考
ShaderLab :プロパティー(Unity)
サーフェイスシェーダーの記述(Unity)
サーフェイスシェーダーライティングの例(Unity)
シェーダーセマンティクス(Unity)
SubShader 内の Tags(Unity)
セマンティクス(DirectX HLSL)
完成したサーフェイスシェーダーを確認する
完成したシェーダーをマテリアルで指定すれば完成です!
ライティングの計算などはシェーダーに記述していませんが、ライティングが施されているのが分かりますね。
今回のシェーダーでは透過するようにはしていないので、アルファ値を変更しても透けたりはしません。
頂点・フラグメントシェーダーを作成する
サーフェイスシェーダーに続いて、頂点・フラグメントシェーダーを作成していきます。
シェーダーファイルを作成する
[プロジェクトビューで右クリック] → [Create] → [Shader] → [Unlit Shader]
シェーダーファイルを修正する
作成した頂点・フラグメントシェーダーはすでに使える状態になっていますが、話を簡単にするために色を変更するだけのシェーダーに変えていきます。
Shader "Unlit/SimplestFlagmentShader" {
Properties {
_MainColor("Main Color", Color) = (1, 1, 1, 1)
}
SubShader {
Tags {
"Queue"="Geometry"
}
Pass {
CGPROGRAM
#include "UnityCG.cginc"
#pragma vertex vert
#pragma fragment frag
float4 _MainColor;
struct appdata {
float4 vertex : POSITION;
};
struct v2f {
float4 vertex : SV_POSITION;
};
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
return _MainColor;
}
ENDCG
}
}
}
中身の説明
順番に見ていきます。
Propertiesブロック
ここはサーフェイスシェーダーの時と同じです。インスペクターでColorを1つだけ指定できるようにしています。
SubShaderブロック
Pass {
}
SubShaderブロックの直下にはPassブロックを配置します。1つのシェーダーで複数回レンダリングを行う場合にはこのブロックを複数回記述しますが、今回のようなシンプルなものであれば1つで十分です。
サーフェイスシェーダーにはこのブロックは登場しませんが、頂点・フラグメントシェーダーでは必須です。サーフェイスシェーダーでは、コンパイル時にUnityがPassブロックを生成してくれていると考えられます。
#include "UnityCG.cginc"
Unity組み込みの関数や便利な構造体などがあらかじめ定義されています。基本的に書いておけばよいでしょう。
CGPROGRAM
~
ENDCG
サーフェイスシェーダーと同じく、ここにシェーダーを記述します。
#pragma vertex vert
#pragma fragment frag
サーフェイスシェーダーと同じく使用する関数を宣言しています。
ここでは頂点シェーダー関数vertとフラグメントシェーダー関数fragを宣言しています。
float4 _MainColor;
サーフェイスシェーダーと同じで、ここではインスペクターで指定したColorをシェーダー内で使用できるように宣言しています。
struct appdata {
float4 vertex : POSITION;
};
struct v2f {
float4 vertex : SV_POSITION;
};
ここでは2つの構造体(appdata、v2f)を宣言しています。それぞれ頂点シェーダのvert関数、フラグメントシェーダーのfrag関数の引数に使われます。各構造体の名前は自由につけることが出来ます。
appdata構造体にはUnityから受け取る値を記述します。ここでは頂点のローカル座標のみを受け取っています。サーフェイスシェーダーの時には:COLORを記述して頂点カラーを受け取ることを宣言していましたが、今回は:POSITIONと記述してvertexが頂点座標であることをUnityに伝えています。(セマンティクスというんでしたね)
v2f構造体にはフラグメントシェーダーで使用する値を記述します。vert関数でこの構造体を返すことによって、frag関数に渡されます。ここでは:SV_POSITIONと記述してピクセル座標を受け取ることを宣言しています。今回のサンプルでは位置によらずに色を決定するので、この変数は使用しません。
v2f vert (appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
return o;
}
fixed4 frag (v2f i) : SV_Target {
return _MainColor;
}
ここが頂点シェーダーのvert関数とフラグメントシェーダーのfrag関数の実装部分になります。
vert関数ではフラグメントシェーダーで必要なパラメータを返します。少なくともピクセル座標を出力する必要があります。ここではローカル座標をカメラから見た座標へ変換しています。UnityObjectToClipPos関数はUnity内のシェーダーに組み込まれている関数(上述したUnityCG.cgincをインクルードすることで使えます)で、この処理を担当してくれます。座標変換については説明を省きますので、参考URLなどご覧ください。
frag関数では最終的なピクセルの色を返します。ここではインスペクターで指定した色をそのまま返しています。ここでもSV_Targetを指定して、色を返すことを宣言しています。
参考
頂点シェーダーとフラグメントシェーダーのプログラミング(Unity)
頂点シェーダーとフラグメントシェーダーの例(Unity)
シェーダーセマンティクス(Unity)
その7 3Dオブジェクト描画のおさらい(○×(まるぺけ)つくろーどっとコム)
完成した頂点・フラグメントシェーダーを確認する
完成したシェーダーをマテリアルで指定すれば完成です!
今回のシェーダーでは透過するようにはしていないので、アルファ値を変更しても透けたりはしません。
おわりに
これでやっとシェーダーの基礎を勉強できました。
今後はUnityの公式サイトを見ながら、各機能の詳細の理解を深めていきたいと思います。