読者です 読者をやめる 読者になる 読者になる

しゅみぷろ

プログラミングとか

ハイトマップから法線情報の生成

はじめに

esprog.hatenablog.com

以前の記事で、ハイトマップを参照して動的に変形するオブジェクトのサンプルを作りました。

サンプルでは、シェーディング計算が適当でしたが、今回はちゃんとシェーディングを行うために必要になる法線情報を計算してみます。

なぜ計算が必要なのか

以前の例ではハイトマップに高さの情報を書き込みそれを元にして頂点を移動させていますが、シェーダー内で変形させているだけで、頂点の持つ法線方向には書き込みを行っているわけではありません。なので、変形させているにも関わらずCPUから送られてくる法線データには変化がありません。当然ですが・・・。

法線データへの書き込みが必要なら、ハイトマップの変更のときに法線マップにも変更を加える・・・というちょっと面倒な手順を踏む必要があります。 なので今回は、ハイトマップから法線がどの方向を向いているのかを計算させることにします。

この方法であれば法線マップが必要なくなる上、計算ミスなどによるデータの齟齬も防げます。

f:id:es_program:20161024144710g:plain:w500

形状の変形と、それによる法線の変化をテストした例です(本来なら青っぽい色が表示されるのですが、目に優しくないのでちょっといじってます...ということにしておいてください...)。 元々の形状はただのPlane(板)ですが、とりあえずハイトマップだけ与えて変形させ、黒い四角形(プレイヤーの操作するキャラクターを模したもの)の歩いた場所を凹ませています。

形状が変化しても法線情報が正しく得られているかと思います。

計算方法

ハイトマップのテクスチャ座標のx,yに関する高さの偏微分を取ることで、接ベクトルを求めます。 求めた接ベクトルの外積の方向ベクトルが法線になります。

コードはこんな感じになります。

float2 shiftX = { _ParallaxMap_TexelSize.x,  0 };
float2 shiftZ = { 0, _ParallaxMap_TexelSize.y };

shiftX *= _ParallaxScale * _NormalScaleFactor;
shiftZ *= _ParallaxScale * _NormalScaleFactor;

float3 texX = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy + shiftX,0,0)) - 1;
float3 texx = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy - shiftX,0,0)) - 1;
float3 texZ = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy + shiftZ,0,0)) - 1;
float3 texz = 2 * tex2Dlod(_ParallaxMap, float4(uv.xy - shiftZ,0,0)) - 1;

float3 du = { 1, 0, _NormalScaleFactor * (texX.x - texx.x) };
float3 dv = { 0, 1, _NormalScaleFactor * (texZ.x - texz.x)};


float3 normal = normalize(cross(du, dv)) * 0.5 + 0.5;

_ParallaxMapがハイトマップ、_ParallaxMap_TexelSizeのx、yはそれぞれハイトマップのテクスチャサイズの横、縦サイズの逆数。 _ParallaxScaleはハイトマップによる変位の係数、_NormalScaleFactorは法線方向を計算するのに使用する係数です。 今回はハイトマップのx(赤色成分)から計算していますが、たとえば以下のような感じでrgbからいい感じにモノクロ抽出してそれを高さとして取ったほうが良いかもしれません。

height = 0.299 * r + 0.587 * g + 0.114 * b;

法線さえしっかり取れていると、テッセレーションなしでも割りとそれっぽく見えます。

f:id:es_program:20161024214302g:plain:w500

ちなみに法線を取ってきて、その法線情報を法線マップに書き込む場合、Textureの形式が問題になる場合があります。たとえばDXT_5nm形式に書き込む場合

float4 PackNormal(float3 normal) {
#if defined(UNITY_NO_DXT5nm)
    return float4(normal);
#else
    return float4(normal.y, normal.y, normal.y, normal.x);
#endif
}

といった感じで、書き込むための法線情報を変換する必要がありそうです。

参考

t-pot『動的法線マップ』