視差マッピング (Parallax Mapping) の実装

DirectX で視差マッピング (Parallax Mapping) の実装をしました。自分の理解の内容を記録しておくと共に、Parallax Occlusion Mapping (POM) の情報は多くあるのに Parallax Mapping の情報は少ないなという気持ちになったのです。

Parallax Mapping とは

既にこのページを見ている人は説明不要かもしれません。視差マッピングとは、高さマップ(ハイトマップ)の情報を使って、より立体的な表現を可能とするものです。法線マップを用いての陰影付けにプラスして使われることも多かったと思います。

法線マップのみでポリゴン表面に陰影が付くのですが、正面から見ているときはともかく、斜めから見ている時には少々違和感のある描画結果となります。視差マッピングを使うとこの違和感を減らすことが可能となります。

仕組み

視差マッピングはとても簡単です。高さ情報に応じて、参照するテクスチャの情報をずらすだけです。コードを見るととても簡単な記述で出来てしまうので、その背景がなかなか分かりづらいと感じました。そのため、これについて説明をしておきたいと思います。

ポリゴン面には、テクスチャを貼るための UV が設定されているとします。これから使用するマップは全て同じ UV を使うとします。

高さに応じて参照する位置を変える、というテクニックがどうして上手く作用するのかを、上記の図を用いて説明します。もし高さゼロの場合、すなわち高さの影響を受けない場合は、視点から見えるディフューズテクスチャの参照すべき位置はポリゴン面に記録されている値(点 “A” )そのままです。

次に、高さ情報を考慮した場合についてです。ここではポリゴン面の点 “B” の箇所への視線を考えてみます。この位置の高さマップを参照すると、一定の高さが記録されていたとします。このとき、本来見えて欲しい箇所というのはズレている、ということがこの図からわかります。ここではズレて欲しい分を Offset として表示しています。この分ズレた位置の UV 値を使ってディフューズテクスチャを参照することで、立体感を感じる絵になります (UV としては点 “C” 箇所をフェッチ)。

この Offset としてずらす量を高さ情報によって変化させます。視差マッピングでは高さは緩やかに変化していること、という条件があるので、このように少しずらして見せかけるということができます。

図ではわざと視線と高さの交点までの Offset 量としていません。これは実際のコードでも同様です。そのため、本来見えるべき点とはズレが生じるのですが、この辺は仕様となります。そもそも視差マッピング自体が見せかけの技術ですし、妥協も必要でしょう。

実装

先の説明で、 フェッチする UV をずらす、という話をしました。しかしこの処理のためには、接空間でずらす量を求める作業が必要です。そのため、まずは頂点情報から接空間に変換するための関数を実装します。ここでは、 tangent の第4要素に binormal の符号が格納されているものとして処理を行っています。 (gltf モデルを Blender でエクスポートしたときにはそのようになっていたのでした)

float3 ToTangentSpace(float3 normal, float4 tangent, float3 vect)
{
	float3 binormal = normalize(cross(tangent.xyz, normal) * tangent.w);
	float3x3 mTangentTransform = float3x3(tangent.xyz, binormal, normal);

	float3 vectorTangentSpace = mul(mTangentTransform, vect);
	return vectorTangentSpace;
}

まずは高さマップから現在の高さを取得して、係数・バイアスなどで補正を掛けます。それから、接空間での視点方向を計算して、接空間でのオフセット分を求めます。下記のコードでは toViewDir は接空間でのベクトルになるため、xy の座標が uv 空間の uv と一致します。また視点との角度の影響を考慮するため、 z 要素での除算も適用しています。

// 高さマップ取得.
float height = texHeightMap.Sample(sLinearSamp, input.uv0).r;
height = height * gHeightScale + gHeightBias;

float3 toViewDir = ToTangentSpace(worldNormal.xyz, input.worldTangent, toCameraDirection.xyz);

// フェッチすべき UV を求める.
float2 offset = height * toViewDir.xy / toViewDir.z;
float2 uv = input.uv0.xy + offset;

接空間にベクトルを変換して処理する過程が、手間の掛かる部分だったり、わかりにくい部分だったりかなと思いました。

あとは求まった UV 値で、法線マップを参照、ディフューズテクスチャを参照などしたりして、ライティングを行います。以下の図は処理後の結果を示しています。

静止画では分かりづらいですが、視点を移動すると法線マップだけのときと比べると、立体感が強く感じることができます。うまく見えるように、高さ分の影響度を係数やバイアスで調整することが必要です。

参考

以下の情報を参考にしました。特に床井研究室 のページは本ページで説明するにあたって、背景を理解するのに大変役立ちました。

関連記事

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA


このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください