動的双放物面環境マッピング


~2chでリクエストされてたし~




■はじめに

今回は、動的双放物面環境マッピングです。ちまたで (Dynamic) Dual-Paraboloid Environment Mapping と呼ばれているやつです。
はるか昔に(Kanoさんが作られたころですが)、2ch で、”IF氏がつくってるよ”とか書き込みがされていたのですが、 そんなリクエストを思い出して作りました。
ちなみに、上の画像は、上部に小さな絵が張られて醜くなっていますが、

1番左外側の車のワールド空間+z軸方向からのマップ
2番左外側の車のワールド空間-z軸方向からのマップ
2番右外側の車のワールド空間+z軸方向からのマップ
1番右外側の車のワールド空間-z軸方向からのマップ

の環境マップテクスチャそのものになっています。

まぁ、いつものように適当にファイルが入っています。

fish.vsh環境マップ生成頂点シェーダー。
fish.psh環境マップ生成ピクセルシェーダー。
dpem.vsh環境マップを張る頂点シェーダー。
dpem.psh環境マップを張るピクセルシェーダー。
vs.vsh普通の頂点シェーダー(地面描画)。
ps.psh普通のピクセルシェーダー(地面描画)。
draw.cppメインの描画部分。
draw.h描画の各関数の定義。
bg.cpp背景の描画。
main.h基本的な定数など。
main.cpp描画に関係しないシステム的な部分。
load.cppロード。
load.hロードのインターフェイス。
tile.bmp (床デカール)
sky.bmp (空デカール)

あと、モデルのファイル、実行ファイル及び、プロジェクトファイルが入っています。

■やってること

前回の魚眼レンズで不必要なほど詳細にレンダリング方法を解説したのは、 今回のテクスチャーに使うためでした(そうでなければ、適当にゆがめた方が綺麗な結果が得られます)。
前回のテクスチャーを視線を前と後ろに向けてレンダリングすると次の2枚のテクスチャーが得られます。

ここで、それぞれの赤い色の円の部分を見てください。張り合わせたら丁度重なります。
つまり、この2枚のテクスチャで全景を覆うことになります。

環境マップを行うためには、反射ベクトルを求め、反射ベクトルの対応するテクスチャ座標を計算する必要があります。
反射ベクトルは、視線ベクトルと法線ベクトルから求められます。

反射ベクトルのxy平面への射影からxy平面での距離が求まります。
その原点からの距離からテクスチャ座標を求めます。

テクスチャ座標を求めるために、特殊な量を計算しましょう。
なぜ、計算するかというと、いろいろ計算した結果、その量が必要だったからです。
まず、下の図を見てください。

原点を通る(反射)ベクトルとz軸のなす角をθとします。
このθの定義から、tanθ=rXY/rZになります。
rXYをθに関して方程式を解きます。
放物線の方程式と角度の式を連立させると、

rXY    2rXY
― = ――― = tanθ
rZ    1 - rXY2

↓rXYで展開して

tanθ rXY2 + 2 rXY - tanθ = 0

↓解の公式

      -1+√(1+tan2θ)
rXY = ――――――――
           tanθ

↓倍角の公式

      -1 + 1/cosθ
rXY = ――――――
         tanθ

↓tanの公式

      1 - cosθ
rXY = ――――
       sinθ

 rXY      1 - cosθ    1 - cosθ      1
――― = ――――― = ――――― = ――――
 sinθ     sin2θ     1 - cos2θ   1 + cosθ

さて、以上で必要な量の計算は終わりました。いよいよテクスチャ座標を求めましょう。
テクスチャ座標は、反射ベクトルの大きさ rXY をx軸、y軸に振った量になります。

    1          rx            1           rXY
u = - (1 + ――――― rXY) = - (1 + rx ―――――)
    2      √(rx2+ry2)        2        √(rx2+ry2)
    1          ry            1           rXY
v = - (1 + ――――― rXY) = - (1 + ry ―――――)
    2      √(rx2+ry2)        2        √(rx2+ry2)

rx, ryは、反射ベクトルのx、y成分です。
0.5を掛けて足すのは、テクセル中心が(0.5,0.5)にあるからです。

ここで、最後の項に注目しましょう。 cosθが反射ベクトルとz軸のなす角なので、 √(rx2+ry2) はsinθに他なりません (rXYは、放物線の式から求めた値で、sinθとは異なります)。
ここに、先ほど求めた式を使うと、

    1          rXY      1         rx         1        rx
u = - (1 + rx ―――) = - (1 + ―――――) = - (1 + ―――)
    2          sinθ    2      1 + cosθ    2      1 + rz
    1          rXY      1         ry         1        ry
v = - (1 + ry ―――) = - (1 + ―――――) = - (1 + ―――)
    2          sinθ    2      1 + cosθ    2      1 + rz

この式が実際に使える公式です。
反射ベクトルの量で、テクスチャ座標が求まりました。
反射ベクトルのz座標が負の時は、反射ベクトルと交差する放物線が別のテクスチャのものになるので、 zを負にした放物線に関するテクスチャー座標を使います。

反射ベクトルのz座標の正負でテクスチャを判定する方法で綺麗につながるのかといえば、綺麗につながります。
例えば、球をレンダリングすれば分かりやすいでしょうか。
球をレンダリングすると、次の結果が得られます。

この球に、2枚の画像が張られていることが分かりやすいように2枚のテクスチャーの空の色を変えると、

と、途中で張り合わされていることが明らかになります。
しかし、途中の継ぎ目は色が同じならば、ほとんど見えません。
ということで、かなり「使える」方法であることが確認できると思います。

■環境マップの作成

今回、特別なプログラムは、主に2つのシェーダプログラムで出来ています。
一つ目は、前回やった環境マップを作るためのシェーダと、もう一つは環境マップを張るためのシェーダです。
環境マップを張るシェーダは次のようになります。

fish.vsh
0001: ; c0-3   -- world + ビュー 行列
0002: ; c12    -- {0.0, 0.5, 1.0, 2.0}
0003: ; c13    -- ライトのベクトル (w成分は環境光の強さ)
0004: ; c14    -- モデルの色
0005: ; c15    -- 深度調整
0006: ;
0007: ; v0    頂点の座標値
0008: ; v7    テクスチャ座標
0009: 
0010: vs.1.0
0011: 
0012: ;座標変換
0013: m4x4 r0,  v0,   c0
0014: 
0015: dp3 r0.w, r0, r0
0016: rsq r1.w, r0.w
0017: mul r0.w, r0.w, r1.w
0018: mov oPos.w, r0.w                ; oPos.w = r0.w = ||v0||
0019: 
0020: mad oPos.z,  c15.x, r0.w, c15.y ; oPos.z = z_max*(||v0||-z_min)/(z_max-z_min)
0021: 
0022: add r1.w, r0.w, -r0.z           
0023: dp4 r1.z, r0.xxyy, r0.xxyy
0024: rcp r1.z, r1.z
0025: mul r0, r0, r0.w
0026: mul r0, r0, r1.w
0027: mul r0, r0, c12.w
0028: mul oPos.xy, r0, r1.z           ; oPos.xy = r0.xy*||v0||*(||v0||-r0.z)/(v0.x^2+v0.y^2)
0029: 
0034: ; ((L・N) + L.w)*c14 (平行光源のライティング)
0035: dp4 r0.w,   v3,   c13
0036: mul oD0,    r0.w, c14
0037: 
0038: ; テクスチャーを張る
0039: mov oT0,    v7

前回のもの、そのままです(というより、今回のために前回のシェーダを作ってのですが)。
ピクセルシェーダも使っていますが、普通に表示するだけなので、固定ファンクションでも十分です。

重要なのは、ビュー行列です。
ビュー行列は車の位置(を少し浮かした重心的な場所)を視点として、注目点を視点の前後にしました。
これらのベクトルからビュー行列を作成し、環境マップを作ります。

0245:         eye.x = mWorld0._41;
0246:         eye.y = mWorld0._42+0.4f*MeshRadius;
0247:         eye.z = mWorld0._43;
0248:         lookAt.x = eye.x;
0249:         lookAt.y = eye.y;
0250:         if(back){
0251:             lookAt.z = eye.z-1.0f;
0252:         }else{
0253:             lookAt.z = eye.z+1.0f;
0254:         }

0273:     D3DXMatrixLookAtLH(&mView, &eye, &lookAt, &up);

■マッピング

いよいよ環境マップを張ります。
簡単なプログラムになっているピクセルシェーダを最初に見ましょう。
テクスチャの1番目と2番目に表と裏の環境マップをロードします。
頂点色のα成分にどちらのテクスチャを使うのか0.5以上か以下かで入れておきます。
後は、cnd 命令でどちらかのテクスチャを引っ張り、適当に強さを調整して頂点色に加算します。

dpem.psh
0002: ps.1.1
0003: 
0005: def c0, 0.3f, 0.3f, 0.3f, 1.0f
0006: 
0007: ; テクスチャの色を引っ張ってくる
0008: tex t0          ; 表のテクスチャ
0009: tex t1          ; 裏のテクスチャ
0010: 
0011: mov r0.a, v0.a
0012: cnd r0, r0.a, t0, t1    ; テクスチャの選択
0014: mul r0, r0, c0          ; 色を薄くする
0015: add r0, v0, r0          ; 頂点色とテクスチャの色の加算が表示色
0016: 

あっ、mad 命令で1命令削れる。

さて、気を取り直して頂点シェーダです。
頂点シェーダでは、適当なテクスチャ座標やどちらのテクスチャを使うかを出力しなくてはなりません。

dpem.vsh
0001: ; c0-3   -- world + ビュー 行列
0002: ; c8-11   -- world
0003: ; c12    -- {1/sqrt(2), 0.5, 1.0, 2.0}
0004: ; c13    -- ライトのベクトル (w成分は環境光の強さ)
0005: ; c14    -- モデルの色
****: ; c16    -- 視点
0006: ;
0007: ; v0    頂点の座標値
****: ; v3    法線ベクトル
0008: ; v7    テクスチャ座標
0009: 
0010: vs.1.1
0011: 
0012: ; 通常レンダリング
0013: m4x4   r0,  v0,   c0
0014: m4x4 oPos,  r0,   c4
0015: 
0016: ; ((L・N) + L.w)*c14 (平行光源のライティング)
0017: dp4 r0.w,   v3,   c13
0018: mul oD0,    r0.w, c14
0019: 
0020: m4x4   r0,  v0,   c8
0021: add    r0, c16,   r0        ; 視線ベクトル 
0022: dp3 r0.w, r0, r0            ; 規格化
0023: rsq r0.w,  r0.w
0024: mul r0.xyz,  r0.xyz,  r0.w
0025: 
0026: dp3 r1.x,  v3,   c8         ; ワールド空間での法線
0027: dp3 r1.y,  v3,   c9
0028: dp3 r1.z,  v3,   c10
0032: 
0033: dp3 r1.w, r0, r1
0034: add r1.w, r1.w, r1.w
0035: mad r0.xyz, r1.w, r1.xyz, -r0.xyz; 反射ベクトル
0036: 
0037: add oD0.w, r0.z,   c12.y ; nzがz軸のどちらを向いているか
0038: 
0039: ; テクスチャーを張る
0040: add r2.z, c12.z, r0.z
0041: rcp r2.z, r2.z
0042: mul r2.x,  r0, r2.z
0043: mul r2.y, -r0, r2.z
0044: mad oT0.xy, c12.y, r2, c12.y
0045: 
0046: add r2.z, c12.z, -r0.z
0047: rcp r2.z, r2.z
0048: mul r2.xy, -r0, r2.z
0049: mad oT1.xy, c12.y, r2, c12.y

座標変換や頂点色は普通と同じ物です。座標変換は命令数を減らせるので、適当に修正してください。 今回は、環境マップを作る行列と同じ行列を使いまわしたのでこうなっています。
テクスチャ座標とどちらのテクスチャを使うのか選択するアルファ値として、反射ベクトルを計算します。
反射ベクトルは視線ベクトル(=視点-頂点の位置)と法線ベクトルから計算します。
どちらのテクスチャを使うかどうかは、反射ベクトルのz軸方向が正か負かで決まるので、

頂点色のα=視線ベクトルのZ成分+0.5

で、ピクセルシェーダにどちらの色を渡すか決定します。

残るはテクスチャ座標ですが、

    1      rx
u = - (1+―――)
    2     1+rz
    1      ry
v = - (1+―――)
    2     1+rz

の公式を使って、計算します。
裏面はz軸の正負が反転します。
また、張り合わせている関係で、一枚(ここでは表面)のx軸が反転します。
これらの向きは実は目で見てあわせました。そんなもんです…

■最後に

てなわけで、動的双放物面環境マッピングをしました。
作ってて気が付いたのですが、視線方向からのテクスチャーを作れば1枚のテクスチャーで環境マップが出来ますね。
視線方向に依存するようになるのですが、球マップよりもゆがみが小さくなるので、悪くない方法だと思います。
今回やってみて(特に球のモデルにマッピングするときに感じたのですが)、テクスチャーがでこぼこと張られてしまうのが気になります。 ミップマップなどの適当な方法で綺麗になるのかは分かりませんが、何とかしたいものです。




もどる

imagire@gmail.com