素材としての「空間キャッシュ」を使い、
背景画像として球体に対してEquirectangularの180度パノラマを半球部分にUVマッピングします。
背景としてEquirectangularとして球にUVマッピングしたパノラマ画像を描画する際に、fragment Shaderで1ピクセルごとに走査することになります。
このときに、UV値よりShaderの180度パノラマの1方向を計算することができます。
float3 calcVDir (float2 _uv) {
float theta = UNITY_PI2 * (_uv.x - 0.5);
float phi = UNITY_PI * (_uv.y - 0.5);
float sinP = sin(phi);
float cosP = cos(phi);
float sinT = sin(theta);
float cosT = cos(theta);
float3 vDir = float3(cosP * sinT, sinP, cosP * cosT);
return vDir;
}
UNITY_PIはπ(3.141592...)の定数値です。UNITY_PI2は(UNITY_PI * 2.0)です。
極座標から方向ベクトルを計算しています。
UVから計算された方向ベクトルvDirを使用してカメラc1とc2からレイを飛ばし、
Depthを参照してワールド座標での交差位置を計算します。
ここでの「レイを飛ばす」とはレイシューティングするわけではなく、
方向ベクトルから視線方向のUVを逆算し、その位置でのDepth値を取得する処理になります。

ワールド位置からUVを計算するには、以下の処理を行います。
前述の「UV値よりShaderの180度パノラマの1方向を計算」の操作の逆を行うことになります。
float2 calcWPosToUV (float3 wPos, float3 centerPos) {
float3 vDir = normalize(wPos - centerPos);
float sinP = vDir.y;
float phi = asin(sinP); // from -π/2 to +π/2 (-90 to +90 degrees).
float cosP = cos(phi);
if (abs(cosP) < 1e-5) cosP = 1e-5;
float sinT = vDir.x / cosP;
float cosT = vDir.z / cosP;
sinT = max(sinT, -1.0);
sinT = min(sinT, 1.0);
cosT = max(cosT, -1.0);
cosT = min(cosT, 1.0);
float a_s = asin(sinT);
float a_c = acos(cosT);
float theta = (a_s >= 0.0) ? a_c : (UNITY_PI2 - a_c);
float2 uv = float2((theta / UNITY_PI2) + 0.5, (phi / UNITY_PI) + 0.5);
if (uv.x < 0.0) uv.x += 1.0;
if (uv.x > 1.0) uv.x -= 1.0;
return uv;
}
Depth値は、DepthテクスチャからUVを使用して取得することができます。
float depth1 = tex2D(_TexDepth1, uv).r;
float depth2 = tex2D(_TexDepth2, uv).r;
_TexDepth1はc1のカメラ位置でのDepthテクスチャです。
_TexDepth2はc2のカメラ位置でのDepthテクスチャです。
これは、RGBテクスチャから色を取得する場合と同じ手順です。ただし、Depth値取得の場合はピクセル間の線形補間をしないようにします。
Depthテクスチャでの1ピクセルの値は0.0-1.0の間の値が入っており、これを以下の式でビュー座標でのZ距離に変換します。
float zDist1 = (depth1 >= 0.99999) ? farPlane : (nearPlane / (1.0 - depth1));
float zDist2 = (depth2 >= 0.99999) ? farPlane : (nearPlane / (1.0 - depth2));
Z距離よりワールド座標位置を計算します。
float3 wPos1 = (vDir * zDist1) + c1;
float3 wPos2 = (vDir * zDist2) + c2;
UV値から、指定のカメラでのDepthを考慮したワールド座標での衝突位置を計算する関数を以下のようにまとめました。
/**
* UV位置と方向ベクトルより、衝突するワールド座標位置を計算.
* @param[in] depthTex depthテクスチャ.
* @param[in] uv UV値.
* @param[in] cPos カメラのワールド座標での中心.
* @param[in] vDir 視線ベクトル.
*/
float3 calcUVToWorldPos (sampler2D depthTex, float2 uv, float3 cPos, float3 vDir) {
float depth = tex2D(depthTex, uv).r;
// depth値から、カメラからの距離に変換.
depth = (depth >= 0.99999) ? farPlane : (nearPlane / (1.0 - depth));
depth = min(depth, farPlane);
// 衝突したワールド座標位置.
return (vDir * depth) + cPos;
}
wPos1からwPos2の間が一直線につながる場合(線形に遷移する場合)、Bの位置での交差位置は以下のように計算できます。
float3 wPosC = lerp(wPos1, wPos2, B);

このwPosCは、この段階では正しい値かどうかは不明です。
このときwPosCが正しいかの確認は、Bの位置からvDirの方向に延ばした線分上にwPosCが存在し、かつ、カメラc1またはc2から見えているかどうか、で判断します。
float3 wPosC0 = lerp(c1, c2, B); // c1-c2直線上のBのワールド位置.
// ワールド座標でのwPosCを、c1のカメラでのUV値に変換.
float2 newUV1 = calcWPosToUV(wPosC, c1);
// ワールド座標でのwPosCを、c2のカメラでのUV値に変換.
float2 newUV2 = calcWPosToUV(wPosC, c2);
// UV値より、それぞれのワールド座標位置を計算.
float3 wPosA = calcUVToWorldPos(_TexDepth1, newUV1, c1, normalize(wPosC - c1));
float3 wPosB = calcUVToWorldPos(_TexDepth2, newUV2, c2, normalize(wPosC - c2));
float angle1 = dot(normalize(wPosA - wPosC0), vDir);
float angle2 = dot(normalize(wPosB - wPosC0), vDir);
ここで計算されたangle1とangle2が限りなく1.0に近い場合は、
wPosCがvDirの直線上にあり、c1またはc2のカメラ位置から見えている衝突である、ということになります。
つまり、ここで計算されたnewUV1とnewUV2のUV値からピクセル色を取得して採用できる、ということになります。
// c1でのRGBテクスチャを_Tex1、c2でのRGBテクスチャを_Tex2とする.
// 計算されたUVより、ピクセル色を取得.
float4 col1 = tex2D(_Tex1, newUV1);
float4 col2 = tex2D(_Tex2, newUV2);
float4 col = float4(0, 0, 0, 1);
if (angle1 > 0.99999 && angle2 > 0.99999) {
col = lerp(col1, col2, B);
}
wPos1-wPos2の間は直線状に遷移するとは限りません。
以下のように凸凹がある場合は、
Depthを考慮して計算されたwPosAとwPosBは、wPosC0からvDir方向に伸ばした直線上に存在しないというのを確認できます。

また、c1のカメラから見たwPosAのみwPosC0からvDir方向に伸ばした直線上に存在する、という場合(またはその逆)もあります。

この場合は、c1のカメラから見えているサンプリング情報はあるがc2のカメラからは見えていない、ということになります。
この場合は、c1のカメラでのRGBテクスチャからピクセル色を採用します。
// 計算されたUVより、ピクセル色を取得.
float4 col1 = tex2D(_Tex1, newUV1);
float4 col2 = tex2D(_Tex2, newUV2);
float4 col = float4(0, 0, 0, 1);
if (angle1 > 0.99999 && angle2 <= 0.99999) {
col = col1;
} else if (angle2 > 0.99999 && angle1 <= 0.99999) {
col = col2;
}
まとめると、推測された交点位置wPosCが正しいかの判断は、以下の4パターンがあることになります。
- 成功 : wPosCが、c1/c2のどちらのカメラ位置からも見えている。
- 成功 : wPosCが、c1のカメラ位置から見えている。
- 成功 : wPosCが、c2のカメラ位置から見えている。
- 失敗 : wPosCが、c1/c2のどちらのカメラ位置からも見えていない。
以上は、c1のカメラからvDir方向の衝突をwPos1、c2のカメラからvDir方向の衝突をwPos2、としたときに、
wPos1からwPos2が線形に遷移する場合にうまくいくパターンになります。
ピクセルごとの空間補間の大部分は、この方法で正しく補間されたピクセル色を推測できます。
以下は、エラーとなる部分を赤色にしたものです。

ここでエラーになったピクセルは、補間をもう少し詰めます。
wPos1とwPos2を計算したときに使用したDepth値から計算されたZ距離で、
小さいほうの距離(minDepth)と大きいほうの距離(maxDepth)を元に、
wPosC0からvDir方向の直線上の位置を計算し、wPosC1、wPosC2とします。

このwPosC1とwPosC2が、c1またはc2のカメラから見て見えているかを計算します。
上画像の場合は、wPosCよりもwPosC2のほうが求める値に近いことになります。
wPosCで推測値が正しいかチェック、wPosC1で推測値が正しいかチェック、wPosC2で推測値が正しいかチェック、としたとき、
残りのエラーになる部分を赤色で表現すると、以下のようにエラーの箇所は減りました。

エラーの箇所は残り少ないため、計算されたUV値から得られる2つのピクセル色を使い、
合成値Bにより単純合成とした場合は以下のようになりました。
別案として、カメラがc1からc2に移動する場合のピクセルごとの遷移が線形でない場合は、荒いルックアップテーブル(テクスチャ)を前処理で作成して参照、という手段もとれそうです。