Panorama180 Render : アルゴリズム

Developer : ft-lab (Yutaka Yoshisaka).
03/04/2019 - 06/14/2021.

戻る



パノラマのEquirectangular形式の展開は、以下のアルゴリズムで行っています。
VR180カメラのような展開と、左右を向いた場合の視差を考慮した展開の2つがあります。

Panorama180 Render ver.2.0.0で追加されたパノラマ360-3Dの展開でも基本部分は同様のアルゴリズムを使っていますが、まだアルゴリズムを記載していません。

VR180カメラのような展開

VR180カメラは、2つの魚眼レンズがIPD(瞳孔間距離)の距離分離れて配置されています。

IPDは、一般的には64mmを使用することが多いです。
左右の目を模して撮影した魚眼レンズからの映像を左右に並べる、 もしくはEquirectangular形式に変換したものがVR180カメラでよく使われる形式になります。
以下は、Equirectangular形式にしたものです。

VR180フォーマットでは、この映像に投影方式や配置方法などの情報を"メタデータ"として与えることになります。

レンダリングでEquirectangular形式の映像を生成するときに、以下のような手順で行いました。
構成は、-Z面を使用しないキューブマップになります。

上から見下ろすと以下のようになります。

カメラの中心から+Z/-X/+X方向、-Y/+Y方向を向いた5つのカメラでそれぞれレンダリングします。
カメラから半球状に走査し、カメラの投影面と交差する箇所の色を採用します。

視野角度は90度ではなく95度にし、境界部分が重なる箇所は滑らかに見せるように補正します。

左右の視点を考慮すると、+Z方向は以下のようにIPDの距離だけ離れて平行です。

-X方向は、カメラの位置は変わらず-X方向を向きます。

+X方向も同様です。

VR180カメラのようなレンダリングを行う際は、
左右のカメラの中心はどの方向を向いていても、IPDの距離分離れているだけで同じ位置になります。
これより、+Z/-Y/+Y面についてはIPD分の視差が発生しますが、 -X/+X面についてはカメラが互いに前後にずれているだけであるので視差が発生しないのが分かります。
これは立体視としてVRで見た場合に、前面や上下を向いた場合は視差が正しく反映されますが、 左右を向くにつれて視差がなくなる、という現象に結び付きます。
これは、物理的な魚眼レンズ2つのみで撮影する場合でも同様の現象になります。

レンダリング結果は、以下のようにEquirectangular形式になります。

分かりやすいように、-X/+Xを赤色、-Y/+Yを緑色にしています。
このレンダリングの場合は、左右それぞれのカメラ位置が同じであるので 境界部分をスティッチしなくても問題ありません。

境界部分の補正

キューブマップ面それぞれにPost Processingを割り当てている場合には、 それぞれの面の境界部でつながらない部分が出てきます。
これを緩和するために、境界部分はウエイト値を与えて補正するようにします。
以下は境界部分を赤く可視化したものになります。

境界部は、あらかじめ固定の位置(球投影での角度)を与えています。

単純にキューブマップ面を合成しただけの場合は、 Ambient OcclusionやBloomの影響が大きいときに境界部分の切れ目が見えてしまいます。
これを解決するために、境界部にウエイト値を与えて補間することで境界が目立ちにくいようにします。


下画像の場合は、境界部分で+Z面に接する箇所はウエイト値が1.0に近づき、 -Y面に接する箇所は0.0に近づきます。

境界でない箇所はウエイト値を0.0としました。

境界部分では、+Z/-Y面がそれぞれ重なる箇所になるため、 +Z面上のピクセル色(col1)と-Y面上のピクセル色(col2)をあらかじめ取得できます。
ウエイト値をwとすると、以下の式で補正します。
float3 col = col2 * (1.0 - w) + col1 * w;
この補正は、+Z面に接する-Y/+Y/-X/+X面、 -Y面に接する-X/+X面、+Y面に接する-X/+X面 にそれぞれ境界部を与えて行うことになります。
こうすることで、ある程度のPost Processingの境界部の色差を緩和できます。

左右の視差を考慮した展開

この手法では、キューブマップのRGBテクスチャとdepthテクスチャが必要になります。

前述したVR180カメラのような展開を行う場合、左右を向くにつれて視差がなくなります。
ここでは、左右を向いても視差が出るように補正します。
左右の目の中心を回転の中心とし、向きを変えた後にIPD分ずらします。

"L"がLeft、"R"がRightです。

キューブマップとしての+Z/-Y/+Y面は、VR180カメラのような展開の時と同じになり、左と右のカメラの中心は変わりません。
-X/+X面は、向きについてはVR180カメラのような展開の時と同じですが、左右それぞれのカメラの中心が変化しています。

キューブマップの構成

また、+Z/-Y/+Y面と-X/+X面で視差が大きく変わることになるため、その境界では変化が急になります。
そのときに、VR-HMDで立体視として見た場合は目に負担がかかります。
これを緩和するために-X/+X面およびカメラの視線は内側に傾けています。

また、-X/+Xのキューブマップ面は接する+Z/-Y/+Y面と重なりができるように若干視野角度を大きくしてます。

境目のずれ

キューブマップからEquirectangular形式にレンダリングする際に カメラ位置が異なる場合、
それぞれのキューブマップ面の境目でずれが生じます。
以下の画像の場合、赤い箇所が境界部です。
-X/+X面の境目が補正が必要な箇所になります。

境界部はカメラが移動/回転してもスクリーンから見ると常に固定となります。
この部分は、あらかじめ固定の位置(球投影での角度)を与えています。
拡大すると、ずれている箇所を確認できます。

この部分をスティッチする必要があります。

境界のスティッチ処理

キューブマップ面をレンダリングするときに視野角度を90度より大きくして重なる箇所を設けているため、
重なる部分を合成することで境界を目立たなくできます。
単純に合成すると以下のようにぶれたようになります。

この状態は、VR-HMDで見たときに目立ってしまいます。

これを緩和するために、"境界部にWeightを与えて変位させる"ようにしました。
ここでは"Stitch with border weight"と呼ぶことにします。
境界部はスクリーンでのEquirectangularの球投影の角度で固定値を割り当てています。
下画像は-X/+X面に近いほど赤色に、+Z/-Y/+Y面に近いほど青色になるようにしています。

ウエイト値として、-X/+X面に近いほど1.0に近づき、+Z/-Y/+Y面に近いほど0.0に近づくとし、 境界部ではない箇所を0.0のウエイト値としました。
境界部ではない箇所(ウエイト値0.0の箇所)はキューブマップ面をそのまま採用とし、 境界部は補正処理を行います。

下画像は、上から見た図になります。

cPosが左目のカメラの中心。
キューブマップ面の境界となる部分を緑色にしています。
cPosを中心に、半球上の走査したワールド座標上の位置をwPosとします。このwPosは、cPosから十分遠い位置にあります。
cPosからwPosに向けた直線上と+Z面が交差する位置をpWzとします。

また、-Xの面を走査するとき、カメラの位置と向きが変わります。
この時のカメラ位置をcPosMXとします。

cPosMXからwPosに向けた直線上と-X面が交差する位置をpWxとします。

cPosから見た+Z面上の交点pWz、
cPosMXから見た-X面上の交点pWX、
この情報より、境界上のウエイト値を元に補間します。
ウエイト値が0.0に近い場合は、+Z面上の交点pWzのピクセル値を採用、
ウエイト値が1.0に近い場合は、-X面上の交点pWxのピクセル値を採用することになります。

pWzは+Z面、pWxは-X面への投影となるため、depthバッファを参照してワールド座標に変換します。
このとき、共に背景に到達する場合は単純な合成を行います。
float3 sPos1 = wPosをcPosから見た+Z面上のテクスチャ位置に変換;
float3 sPos2 = wPosをcPosMXから見た-X面上のテクスチャ位置に変換;
float3 col1 = tex2D(+Z面のテクスチャ, sPos1.xy).rgb; 
float3 col2 = tex2D(-X面のテクスチャ, sPos2.xy).rgb;
float3 col = (col1 + col2) * 0.5;
カメラから十分遠い背景のピクセルは、カメラの左右の視差やcPos/cPosMXなどの位置の違いの影響はほとんど受けません。

-X面上のdepthバッファより、 pWxの位置から物体と衝突する位置iwPosを計算します。
iwPosはワールド座標上の位置になります。

iwPosが+Z面上ではどの位置に投影されるかを計算します。
// カメラから十分遠い背景までの距離.
float _FarDistance = 500.0;

// カメラの中心の差分.
float3 dd = cPos - cPosMX;

// iwPosをcPos中心のカメラから見たワールド座標位置に変換.
float3 iwPos2 = normalize((iwPos - cPosMX) + dd) * _FarDistance + cPos;
-X面を投影するカメラ(中心cPosMX)では、 iwPosがiwPos2に推移することになります。
ただし、このiwPos2は推測された位置となり正しい位置とは限りません。

境界部のウエイト値をwとします。
ウエイト値は-X/+X面に近い箇所を1.0、+Z/-Y/+Y面に近い箇所を0.0としています。

このときのカメラ位置cPosMX(-X面を向く)での補間されたワールド座標を計算し、そのときのピクセル値も計算します。
// 境界上のワールド位置を補間.
float3 wPos2 = iwPos * w + iwPos2 * (1.0 - w);

float3 sPos = wPos2をcPosMXから見た-X面上のテクスチャ位置に変換;
float3 col2 = tex2D(-X面のテクスチャ, sPos.xy).rgb;
この時に計算できた色をcol2としました。
これは、ウエイト値が1.0から0.0に推移するときの色に相当します。
0.0に近づくにつれて正しくなくなりますので、次に逆方向の+Z面で同様の処理を行います。

+Z面上のdepthバッファより、 pWzの位置から物体と衝突する位置iwzPosを計算します。
iwzPosはワールド座標上の位置になります。

iwzPosが-X面上ではどの位置に投影されるかを計算します。
// カメラから十分遠い背景までの距離.
float _FarDistance = 500.0;

// カメラの中心の差分.
float3 dd = cPos - cPosMX;

// iwzPosをcPosMX中心のカメラから見たワールド座標位置に変換.
float3 iwzPos2 = normalize((iwzPos - cPos) - dd) * _FarDistance + cPosMX;
+Z面を投影するカメラ(中心cPos)では、 iwzPosがiwzPos2に推移することになります。
ただし、このiwzPos2は推測された位置となり正しい位置とは限りません。

このときのカメラ位置cPos(+Z面を向く)での補間されたワールド座標を計算し、そのときのピクセル値も計算します。
// 境界上のワールド位置を補間.
float3 wzPos2 = iwzPos * (1.0 - w) + iwzPos2 * w;

float3 sPos = wzPos2をcPosから見た+Z面上のテクスチャ位置に変換;
float3 col1 = tex2D(+Z面のテクスチャ, sPos.xy).rgb;
この時に計算できた色をcol1としました。
これは、ウエイト値が0.0から1.0に推移するときの色に相当します。
これで以下の2つの色が求められました。

これをウエイト値を元に合成します。
float3 col = col2 * w + col1 * (1.0 - w);			
計算されたcolが、境界上の補間された色情報に相当します。
補正前と後を比べると、以下のように変化しました。


これだけではカメラの近い位置では、ずれが残る場合があります。
+Z/-Y/+Y面での走査しているピクセル値をbaseColとしたとき、 wが0.5よりも小さいときに以下の合成処理を行って重ね合わせます。
float3 sPos1 = wPosをcPosから見た+Z面上のテクスチャ位置に変換;
float3 baseCol = tex2D(+Z面のテクスチャ, sPos1.xy).rgb; 

if (w < 0.5) {
  float w2 = w / 0.5;
  col = baseCol * (1.0 - w2) + col * w2;
}
以上のように、視差が異なる(カメラの中心が異なる)レンダリング情報(RGB + Depth)を使用することで、 ある程度はスティッチを滑らかにできます。
depthが計算できない背景があるときは、誤差が出る場合があります。

戻る