Unityで頂点単位のADSシェーディングを実装する
前回は頂点単位の拡散反射を行うシェーディングを実装しました。 今回はそれに環境光と鏡面反射を加えてADSシェーディングを実装してみます。
Unityのバージョン: 2018.2.6f1
ADSシェーディング
Ambient(環境光)とDiffuse(拡散反射)、Specular(鏡面反射)の頭文字をとったもので、 その名のとおり環境光と拡散反射と鏡面反射を組み合わせたものです。 このモデルはBui Tuong PhongにちなんでPhong反射モデルとも呼ばれます。
環境光
前回作成した拡散反射のシェーディングでは平行光源のみを考えました。 本来は光源から直接物体に向かう光だけではなく、 他の物体にあたって反射してから物体に向かう光も考える必要があります。 それらの光を再現するにはレイトレーシングなどの方法もありますが、 ここではもっと単純にシーンの全方向から一様に光が当たるものと仮定してしまいます。
拡散反射は法線ベクトルとライトベクトルの角度によって明るさが変わりましたが、 環境光は法線によって明るさは変わりません。 今回はどこでも一律に同じ明るさを加算することで表現します。
環境光を受けて反射して出ていく光の強さをとし、環境光の光源の強度を、 物体の環境光の反射率をとしたときに次の式でモデル化できます。
拡散反射
これは前回説明しました。
拡散反射する光の強さをとし、光源の強さを、反射率をとします。 法線ベクトルを、光源に対するベクトルをとしたときに、 拡散反射は次の式でモデル化できます。
鏡面反射
これはいわゆるハイライトというやつです。 反射は鏡のような完全反射の方向でもっとも強くなります。
この図においてが入射光に対する純粋な反射のベクトルを表し、が法線です。 は次の式で計算できます。
反射の強さは視点に対するベクトルとが一致するときに最大で、 から離れるに連れて急速に小さくなる必要があります。 これはとの間の角度の余弦を何乗かしたものでモデル化できます。
の値は一般的に1から200の間で選びます。 の値が大きいほど、との間の角度が増えるに連れて値は0に速やかに近づきます。
このモデルの式ではを求める必要がありますが、 の計算には内積と2回の乗算が必要です。 ハーフベクトルを使うことで計算を少しだけ効率化できます。 ハーフベクトルとは視線ベクトルとライトベクトルを足して正規化したベクトルです。
とが揃うときとが一致することを利用します。 これによって鏡面反射の計算は次のようにできます。
の計算に必要な操作はの計算よりも少ないので、 ハーフベクトルによっていくらか効率の改善が期待できます。
これらのすべてをまとめることで次の式が得られます。
シェーダの実装
上記のモデルを実装します。
必要なパラメータをすべてプロパティで渡します。
Shader "MyShader/VertexADS"
{
Properties
{
[Header(Light)]
_LightDirection ("Light Direction", Vector) = (1.0, 1.0, -1.0, 1.0)
_Lambient ("Lambient", Vector) = (0.5, 0.5, 0.5, 1.0)
_Ldiffuse ("Ldiffuse", Vector) = (1.0, 1.0, 1.0, 1.0)
_Lspecular ("Lspecular", Vector) = (1.0, 1.0, 1.0, 1.0)
[Space]
[Header(Material)]
_Kambient ("Kambient", Vector) = (0.1, 0.1, 0.1, 1.0)
_Kdiffuse ("Kdiffuse", Vector) = (0.5, 0.5, 0.5, 1.0)
_Kspecular ("Kspecular", Vector) = (0.5, 0.5, 0.5, 1.0)
_Shininess ("Shininess", float) = 150
}
SubShader
{
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
...
uniform float3 _LightDirection;
uniform float3 _Lambient;
uniform float3 _Ldiffuse;
uniform float3 _Lspecular;
uniform float3 _Kambient;
uniform float3 _Kdiffuse;
uniform float3 _Kspecular;
uniform float _Shininess;
...
ENDCG
}
}
}
プロパティは[Header(name)]
や[Space]
を使ってわかりやすく分類しています。
struct appdata
{
float4 vertex : POSITION;
float3 normal : NORMAL;
};
struct v2f
{
float4 vertex : SV_POSITION;
float3 color : COLOR;
};
パイプラインとの値の受け渡しは上記の構造体を利用します。
前回と変わりはありませんね。
頂点シェーダの入力ではNORMAL
で法線を受け取ります。
頂点シェーダの出力として、計算した色をCOLOR
で次のステージに渡します。
void vert (in appdata v, out v2f o) {
o.vertex = UnityObjectToClipPos(v.vertex);
float3 normal = UnityObjectToWorldNormal(v.normal);
float3 l = normalize(_LightDirection);
float3 view = normalize(_WorldSpaceCameraPos - o.vertex);
float3 h = normalize(l + view);
float3 Iambient = _Lambient * _Kambient;
float3 Idiffuse = _Ldiffuse * _Kdiffuse * max(dot(normal, _LightDirection), 0);
float3 Ispecular = _Lspecular * _Kspecular * pow(max(dot(normal, h), 0), _Shininess);
o.color = Iambient + Idiffuse + Ispecular;
}
頂点シェーダは上記のとおりです。
最初に座標変換を行っています。 これについては以前説明をしました。
Unity組み込みの変数である_WorldSpaceCameraPos
を使って視点へのベクトルを求めています。
Unityの組み込みのシェーダ変数はドキュメントのこちらにまとまっています。
それぞれ環境光反射、拡散反射、鏡面反射を計算してそれらを足し合わせたものを 最終的な色として渡しています。
void frag (in v2f i, out float4 col : SV_Target)
{
col = float4(i.color, 1);
}
フラグメントシェーダでは、頂点シェーダから渡された色を単に出力します。
実行結果
頂点ごとに色を決定し、その間で補間しているのが はっきりと見て取れます。
おわりに
簡単なシェーディングモデルとしてADSシェーダを作りました。 今回の実装は頂点シェーダ内で行ったので実行結果ではポリゴン感が出てしまいました。 次回はフラグメントごとにシェーディングの計算を行うシェーダを解説する予定です。
(2018/09/08追記:フラグメントごとのシェーディングの記事を書きました)
ソースコードはこちらのリポジトリにおいておきます。