6. 立体視表示

3DS に搭載されている 2 つの LCD のうち、上画面の LCD は特殊な器具を用いることなく裸眼による立体視表示を行うことができます。

立体視表示を行うには左目用と右目用の 2 枚の画像をレンダリングする必要がありますが、CTR-SDK では通常の表示で作成したカメラ行列から 2 つの画像をレンダリングするためのカメラ行列を計算する ULCD ライブラリを用意しています。様々なアプリケーションで立体視表示を実現する方法を統一するためにも、レンダリング時に ULCD ライブラリで算出したカメラ行列を使用してください。

この章では、立体視表示の原理、アプリケーションでの実装方法、ステレオカメラとの連動について説明します。

便宜上、以下の立体視表示に関する用語を以降の説明で使用しています。

現実空間

立体視表示には、プレイヤーと上画面の LCD 表面との距離などが関係しています。それらとアプリケーション内で構築する空間とを区別するために、プレイヤーや 3DS が存在する空間のことを現実空間と呼びます。

仮想空間

現実空間に対して、アプリケーション内で構築される空間のことを仮想空間と呼びます。

ベースカメラ

アプリケーションがシーンに応じて設定・作成するカメラをベースカメラと呼びます。ベースカメラの情報をもとに、ULCD ライブラリは左目用、右目用のカメラ行列を算出します。

基準面

立体視表示時の、仮想空間における 3DS の LCD 表面上に像を結ぶ平面を基準面と呼びます。基準面は、カメラのビューボリュームの断面のひとつです。

3D ボリューム

立体視表示の強度を調節するために 3DS に搭載されているスイッチです。立体視が苦手な方でも快適に立体視表示ができるように、また長時間のプレイで疲れを感じたときなどに映像を調節することができるように、スライド式のスイッチとなっています。ULCD ライブラリでは、その入力値が仮想空間上に生成した左右 2 つのカメラ間の距離を調整するために用いられます。

最適視認位置

立体視表示でプレイヤーが最適な立体感を得ることのできる視聴位置のことです。

6.1. 立体視表示の原理

立体視表示は、基本的に左右の目の視差を考慮してオブジェクトをレンダリングすることで、オブジェクトと視点との距離感を生み出しています。ULCD ライブラリには、視差を考慮したレンダリングを行うためのカメラ行列を算出する方法として、ベースカメラの設定を極力維持する方法(アプリケーション優先)とベースカメラの設定を必要に応じて自動的に変更する方法(現実感優先)の 2 種類を用意しています。

この節ではそれぞれの算出方法の原理について説明します。

6.1.1. 前提条件

ULCD ライブラリは、立体視表示で使用するカメラ行列を以下の条件を前提にして計算しています。

  • プレイヤーの両目の間隔 Disteye を 62 mm と仮定します。
  • LCD 表面(中心)とプレイヤーの両目との距離を Diste2d とします。
  • 先行研究によって得られている知見により、人間の目にとって自然に奥行きを感じることができる限界の深さを Depthltd とします。また、この深さを表現するために必要となる、現実空間における限界視差(基準面上で必要な視差)を Pr_ltd とします。プレイヤーから見て、奥方向(奥行き)と手前方向(飛び出し)それぞれの視差の上限はガイドラインで定められています。
  • 上画面 LCD の短辺の長さを Lendisp とします。

6.1.2. 左右のカメラ間の距離と各カメラのビューボリュームの算出方法

カメラ行列を計算するための入力として、以下の情報を必要とします。

  • 立体視用の視差画像を生成するためのベースとなるビュー行列 Viewbase
  • 左目と右目用カメラのビューボリュームを生成するためのベースとなるプロジェクション行列 Projbase
  • 仮想空間において、カメラ位置から LCD 表面上に位置させたい点までの距離 Dlevel
  • 立体具合を調整するための係数 Dr(値の範囲は 0.0 ~ 1.0)

Projbase から逆算できるビューボリュームのパラメータ(left, right, bottom, top, near, far)をそれぞれ lbaserbasebbasetbasenbasefbase としたとき、これらのパラメータと Dlevel から、基準面の幅と高さを求めることができます。

基準面の幅:

Wlevel = | rbase - lbase | × Dlevel / nbase

基準面の高さ:

Hlevel = | tbase - bbase | × Dlevel / nbase

この基準面の幅と高さ、そして LCD の実際の寸法から、現実空間のスケールを仮想空間のものに変換する係数を求めておきます。

Scaler2v = Hlevel / Lendisp

6.1.2.1. アプリケーション優先の算出方法

ベースカメラによる見え方(アプリケーションが本来想定している見え方)を極力維持して、左目用と右目用の画像を描画するためのカメラ行列を生成します。

前提条件から、現実空間における限界視差(不自然に見えない状態を維持する、最も大きい視差)Pr_ltd を算出することができます。

Pr_ltd = Disteye × ( Depthltd / ( Diste2d + Depthltd ) )

下図に計算式中の項目と位置関係を示します。

図 6-1. 現実空間における限界視差

Scaler2v を用いて Pr_ltd を仮想空間における限界視差 Pv_ltd に変換することができます。

Pv_ltd = Pr_ltd × Scaler2v

ベースカメラに設定されている奥行き方向の位置を変えずに、ファークリップ面のオブジェクトを表示するための基準面における視差がこの限界視差となるような右目用・左目用カメラの間隔 I を求めます。この計算には Dlevel を利用します。なお、ファークリップ面が異常な位置の場合(基準面よりも手前になるなど)は 0 とします。

I を求める計算式は以下のとおりです。

I = Pv_ltd × ( fbase / ( fbaseDlevel ) )

それらの関係を図示します。

図 6-2. 仮想空間における左右のカメラの間隔

この算出方法ではベースカメラの位置を動かさないため、ビューボリュームに関するパラメータは変化しません。

ニアクリップ面の幅: Wn = | rbase - lbase |

ニアクリップ面の高さ: Hn = | tbase - bbase |

6.1.2.2. 現実感優先の算出方法

基準面のオブジェクトの見え方を現実空間における見え方に合わせ、プレイヤーが LCD から Diste2d 離れた位置から見て自然な形で立体視を行うことができるような、左目用と右目用の画像を描画するためのカメラ行列を生成します。

現実空間におけるプレイヤーの両眼の間隔と LCD の位置関係を、仮想空間の左右 2 つのカメラと基準面の位置関係に反映させます。その過程でベースカメラに設定された各パラメータは自動的に変更されることになります。

LCD 表面とプレイヤーの両目との距離 Diste2d を仮想空間におけるスケールに変換した Dlevel_new を求めます。

Dlevel_new = Diste2d × Scaler2v

求めた距離と Dlevel 、そして Projbase から得られる各クリップ面への距離から、新しく生成するカメラのクリップ面への距離 nnewfnew を求めることができます。

nnew = Dlevel_new - ( Dlevel - nbase ) , fnewDlevel_new + ( fbaseDlevel )

計算の結果、新しいニアクリップ面がカメラよりも奥に位置した場合は以下のように補正します。

nnew = Dlevel_new × 0.01

また、ファークリップ面がニアクリップ面よりも手前に位置した場合は以下のように補正します。

fnew = nnew × 2.0

基準面は動かさないため、これまでに求めた値を満たすようにベースカメラを前後に動かすことになります。

次に、動かした結果変化した、ニアクリップ面の大きさ(幅、高さ)を求めます。さらに、ニアクリップ面の範囲(left, right, top, bottom)を再計算します。

ニアクリップ面の幅

Wn_new = Wlevel × nnew / Dlevel_new

ニアクリップ面の高さ

Hn_new = Hlevel × nnew / Dlevel_new

ニアクリップ面の範囲

lnew = tmp × lbase , rnewtmp × rbase ,

tnew = tmp × tbase , bnewtmp × bbase

tmpHn_new / | tbasebbase |

プレイヤーの両目の間隔を仮想空間における左右 2 つのカメラ間距離 I に反映させます。

IDisteye × Scaler2v

下図に計算式中の項目と位置関係を示します。

図 6-3. 現実感優先の算出方法

6.1.3. プロジェクション行列の生成

計算で求めた左右のカメラ間距離 I に対して、調整係数 Dr と 3DS 本体の 3D ボリュームの入力値 vol を反映させます。3D ボリュームを最低値にした場合は立体感がなくなり、左右のカメラはベースカメラと一致します。

I = I × vol × Dr  (0.0 ≤ vol ≤ 1.0)

ベースカメラのビューボリュームに関するパラメータはどちらの算出方法を利用したかで異なりますが、便宜上以降の計算式ではニアクリップ面の幅を Wn、ニアクリップ面の高さを Hn、ベースカメラから基準面への距離を Dlevel、ビューボリュームに関するパラメータ(left、right、top、bottom、near、far)を l, r, t, b, n, f のように、統一して表記しています。左右のカメラそれぞれのビューボリュームを求めるために、ニアクリップ面での視差 Pn を求めます。ただし前提条件として、左右のカメラがベースカメラの位置から均等に離れていると仮定しています。

Pn = I × 0.5 × (( Dlevel - n ) / Dlevel )

この視差を元に、左右のカメラそれぞれのビューボリュームにおけるニアクリップ面の位置を求めます。左右のカメラは横方向のみに動かしたものであるため、top、bottom、near、far は共通です。

左カメラのニアクリップ面位置: lleft = ( l - Pn ) + I × 0.5, rleft = ( r - Pn ) + I ×0.5

右カメラのニアクリップ面位置: lright = ( l + Pn ) - I × 0.5, rright = ( r + Pn ) - I ×0.5

以上から導かれたパラメータに基づき、左右のカメラに関してプロジェクション行列を計算します。下図は、今回求めたビューボリュームを図示したものです。立体視を行うためには、ビューボリュームが重なった領域(図内中央)にオブジェクトを配置する必要があります。

図 6-4. 左右カメラのビューボリューム

6.1.4. ビュー行列の生成

左右のカメラ間距離 IViewbase から導かれるベースカメラの情報を用いて、左右のカメラそれぞれのビュー行列を生成します。導かれるベースカメラの情報は、位置 Posbase、向き Dirbase、右手方向 Eright の 3 つで、これらは三次元ベクトルであり、DirbaseEright は長さ 1 の単位ベクトルです。

現実感優先の算出方法でカメラ間距離を求めていた場合、左右のカメラの位置がベースカメラに対して前後(奥行き方向)に動いている可能性があります。従ってベースカメラの位置は、以下のとおりとなります。

Posbase = Posbase - ( Dlevel_newDlevel ) × Dirbase

ベースカメラの位置は左右のカメラの中間であることから、ベースカメラの位置に対して左右のカメラ間距離の半分を加えたもの、あるいは引いたものが左右のカメラの位置となります。左カメラ、右カメラの位置をそれぞれ PosleftPosright とし、注視点の方向(カメラの向きさえ分かればよいため、注視点の位置は厳密に求める必要はありません)をそれぞれ TgtleftTgtright とすると、

PosleftPosbaseI × 0.5 × ErightTgtleftPosleft + Dirbase

PosrightPosbase + I × 0.5 × ErightTgtrightPosright + Dirbase

となります。以上から、左右のカメラそれぞれのビュー行列を生成することができます。

6.1.5. オブジェクトを任意の位置に表示するために必要な視差

ULCD ライブラリで算出した行列を利用すれば、透視射影で描画した 3D オブジェクトに関しては視差をどのくらいつければよいかを意識せずに立体視表示を行うことができます。しかし、2D イメージあるいは正射影で描画した 3D オブジェクトを表示したい場合は、左目用と右目用の絵をアプリケーションで計算して作らなければなりません。

ライブラリによって生成された左右のカメラを用いて、LCD よりも手前にオブジェクトを表示する場合、または奥に表示する場合につけるべき視差を下図に示します。

図 6-5. 任意の位置にオブジェクトを表示するために必要な視差

ファークリップ面 基準面 ニアクリップ面 Left Base Right

カメラから d1 離れたオブジェクト M1 を LCD より手前に表示するためには、カメラと M1 の位置を結ぶ直線と基準面の交点(右目は R1、左目は L1 の位置)にそれぞれ M1 を描画します。つまり R1L1 の視差が必要ということになります。この長さは、左右のカメラ間距離 I とカメラから基準面までの距離 Dlevel を用いると、三角比で計算することができます。

R1L1 = I × ( Dleveld1 ) / d1  ただし、Dleveld1

同様に、LCD よりも奥に位置するオブジェクト M2(カメラからの距離 d2)は R2L2 の視差をつけることで表現することができます。

R2L2 = I × ( d2 - Dlevel ) / d2  ただし、Dleveld2

オブジェクト M3 のように、左右カメラどちらのビューボリュームにも含まれないような位置では基準面上に像を結ばないため、立体視表示に必要な視差を実現することができません。

6.1.6. 無限遠におけるオブジェクトのずれ

2D イメージあるいは正射影で描画したオブジェクトを無限遠(最奥)に位置するように見せるために、左目用の絵と右目用の絵に表示するオブジェクトをどのくらいずらして描画するべきかについて説明します。

無限遠に位置するオブジェクトは、左右の絵を限界視差 Pr_ltd(基準面となる LCD 上での実寸値)ずらして描画することで実現することができます。つまり、立体視表示が有効な状態で遠景を表現するためには、左目用と右目用のレンダーバッファへ元の位置からそれぞれ限界視差の半分だけずらして描画してください。ただし、実際に描画する際には Pr_ltd を仮想空間における限界視差 Pv_ltd に変換する必要があります。

原理上は手前方向に最も飛び出るようなオブジェクトの立体表示を実現することもできますが、図 6-5 で示すように、表現可能な範囲が奥方向の範囲と比べて狭いことに注意してください。

6.2. アプリケーションでの実装方法

アプリケーションで立体視表示を実現するためには、以下のような実装手順が必要となります。

  • 上画面の表示モードを立体視表示のモードに設定する。
  • 右目用の画像を表示するためのディスプレイバッファを用意する。
  • 透視射影ならば、ULCD ライブラリを使用して左目用と右目用のカメラ行列を算出する。
  • 左目用と右目用の画像をレンダリングし、それぞれ対応するディスプレイバッファに転送する。
  • ディスプレイバッファの内容を LCD に表示する。

 

6.2.1. 表示モードの設定

nngxSetDisplayMode() を呼び出して表示モードの設定を行うことで、上画面での立体視表示の有効・無効を制御することができます。表示中に状態を切り替えるときは VSync 直後に行ってください。

コード 6-1. 表示モードの設定
void nngxSetDisplayMode(GLenum mode);

mode NN_GX_DISPLAYMODE_STEREO を渡すと立体視表示が有効になります。立体視表示を無効にする場合は NN_GX_DISPLAYMODE_NORMAL を渡してください。それ以外を渡した場合は GL_ERROR_9003_DMP のエラーが生成されます。デフォルトは NN_GX_DISPLAYMODE_NORMAL を指定した状態(立体視表示が無効)です。

立体視表示が有効になると、上画面には左目用と右目用の 2 つの画面(LCD)が存在することになります。2 つの画面の解像度、フォーマット、領域を確保するメモリ(メインメモリ、VRAM-A/B)は同じでなければなりません。異なる場合は nngxSwapBuffers() の呼び出しでエラーが生成されます。下画面への影響はありません。

左目用の画面を指定する場合は通常時と同じ NN_GX_DISPLAY0 を使用します。右目用の画面の指定には、追加された NN_GX_DISPLAY0_EXT を使用します。右目用の画面でも、ほかの画面と同じように画面の指定(nngxActiveDisplay())、ディスプレイバッファのバインド(nngxBindDisplayBuffer())、LCD へ出力する際のオフセット(nngxDisplayEnv())など、一連の処理を行わなければなりません。

左目用と右目用、どちらの画面を指定するのかを分かりやすくするために、別名として NN_GX_DISPLAY0 に対して NN_GX_DISPLAY0_LEFT が、NN_GX_DISPLAY0_EXT に対して NN_GX_DISPLAY0_RIGHT が定義されています。

6.2.2. ディスプレイバッファの確保

立体視表示が有効になると、上画面の LCD では左右の目で異なる映像が見えるようになります。アプリケーションでは、左目で見る映像を映し出している部分と右目で見る部分とを別の LCD であるかのように扱います。そのため、関数の呼び出しで上画面を指定するときに使用されていた NN_GX_DISPLAY0 は左目用の LCD の指定に使われ、右目用の LCD を指定するためには追加された NN_GX_DISPLAY0_EXT を使います。

別々の LCD として扱われますので、左目用と右目用それぞれにディスプレイバッファが必要になります。マルチバッファリングを行う場合、必要なディスプレイバッファの数もそれだけ増えることに注意してください。

以下のコード例では、上画面の LCD サイズのディスプレイバッファを右目用にダブルバッファリングで確保しています。

コード 6-2. 右目用のディスプレイバッファの確保
GLuint m_Display0BuffersExt[2];
// For right eye - Upper (DISPLAY0_EXT)
nngxActiveDisplay(NN_GX_DISPLAY0_EXT);
nngxGenDisplaybuffers(2, m_Display0BuffersExt);
nngxBindDisplaybuffer(m_Display0BuffersExt[0]);
nngxDisplaybufferStorage(GL_RGB8_OES, 
    NN_GX_DISPLAY0_WIDTH, NN_GX_DISPLAY0_HEIGHT, NN_GX_MEM_FCRAM);
nngxBindDisplaybuffer(m_Display0BuffersExt[1]);
nngxDisplaybufferStorage(GL_RGB8_OES, 
    NN_GX_DISPLAY0_WIDTH, NN_GX_DISPLAY0_HEIGHT, NN_GX_MEM_FCRAM);
nngxDisplayEnv(0, 0);

6.2.3. ULCD ライブラリの使用方法

レンダリングで透視射影を使用している場合、ベースとなるカメラ情報から立体視表示で使用することのできる左右のカメラ情報を算出する ULCD ライブラリを利用することで、アプリケーションは立体視に必要な視差を意識することなく立体視表示を実現することができます。

ULCD ライブラリでは、視差を考慮したカメラ行列を作成するクラス nn::ulcd::StereoCamera を用意しています。このクラスをアプリケーションで使用するには、ヘッダファイル "nn/ulcd.h" をインクルードしなければなりません。また、3D ボリュームの値を取得する関数は立体視表示に副次的な視覚効果を与えることを想定したものです。そのため、このクラスを使用しなければ 3D ボリュームを立体視表示の強度調整に使うことができません。

補足:

3D ボリュームの値を取得する関数を使用する場合は事前連絡が必要です。

6.2.3.1. 初期化処理

nn::ulcd::StereoCamera クラスのインスタンスを生成し、メンバの Initialize() を呼び出すことで初期化を行うことができます。

コード 6-3. 初期化処理
void Initialize(void);

初期化処理では内部で保持している情報の初期化が行われます。

6.2.3.2. 限界視差の設定と取得

限界視差の設定および取得をメンバ関数の SetLimitParallax()GetLimitParallax() で行うことができます。

コード 6-4. 限界視差の設定と取得
void SetLimitParallax(const f32 limit);
f32  GetLimitParallax(void) const;

limit には設定する限界視差を mm(ミリメートル)単位で指定します。限界視差の上限はガイドラインで定められており、奥方向の上限までの正値ならば自由に指定することができます。設定された限界視差はカメラ行列の計算結果に反映されます。

GetLimitParallax() で、限界視差の現在の設定値を取得することができます。一度も SetLimitParallax() で限界視差を設定していない場合は、ガイドラインで定められている上限値(奥方向)を返します。

6.2.3.3. ベースカメラの情報

ベースとなるカメラの情報をメンバ関数の SetBaseFrustum()SetBaseCamera() で設定します。

コード 6-5. ベースカメラ情報の設定
void SetBaseFrustum(const nn::math::Matrix44 *proj);
void SetBaseFrustum(const f32 left, const f32 right, const f32 bottom, 
                    const f32 top, const f32 near, const f32 far);
void SetBaseCamera(const nn::math::Matrix34 *view);
void SetBaseCamera(const nn::math::Vector3 *position, 
                   const nn::math::Vector3 *rightDir, 
                   const nn::math::Vector3 *upDir, 
                   const nn::math::Vector3 *targetDir);

SetBaseFrustum() は主に、ニアとファーの両クリップ面の情報を設定します。それぞれのパラメータを個別に設定する関数と、nn::math::MTX44Frustum()nn::math::MTX44Perspective() で作成したプロジェクション行列からパラメータを逆算する関数の 2 種類が用意されています。nn::math::Matrix44 構造体でプロジェクション行列を指定する場合は、3DS のビューボリュームの定義(Z 座標が 0 から –Wc でクリップされる)に基づいて算出されたものでなければ正常に計算が行われません。

SetBaseCamera() は主に、ベースカメラの位置情報などを設定します。それぞれのパラメータを個別に設定する関数と、nn::math::MTX34LookAt() などで作成したビュー行列からパラメータを逆算する関数の 2 種類が用意されています。ビュー行列またはベクトルには、3DS の座標系である右手座標系に基づいたものを指定してください。

6.2.3.4. 左右のカメラの行列計算

6.1. 立体視表示の原理」で紹介したように左右それぞれのカメラ行列を計算する方法として、ベースカメラの設定を極力維持する手法(アプリケーション優先の算出方法)とベースカメラの設定を自動的に変更する手法(現実感優先の算出方法)の 2 つの計算方法を用意しています。

メンバ関数の CalculateMatrices() がアプリケーション優先の算出方法で、CalculateMatricesReal() が現実感優先の算出方法です。

コード 6-6. 左右のカメラ行列の計算
void CalculateMatrices(
        nn::math::Matrix44* projL, nn::math::Matrix34* viewL,
        nn::math::Matrix44* projR, nn::math::Matrix34* viewR,
        const f32 depthLevel, const f32 factor,
        const nn::math::PivotDirection pivot = nn::math::PIVOT_UPSIDE_TO_TOP,
        const bool update3DVolume = true);
void CalculateMatricesReal(
        nn::math::Matrix44* projL, nn::math::Matrix34* viewL,
        nn::math::Matrix44* projR, nn::math::Matrix34* viewR,
        const f32 depthLevel, const f32 factor,
        const nn::math::PivotDirection pivot = nn::math::PIVOT_UPSIDE_TO_TOP,
        const bool update3DVolume = true);

どちらの関数も引数は同じで、計算結果だけが異なります。

projL viewL には左目用の、projRviewR には右目用のプロジェクション行列とビュー行列の格納場所を指定します。値が書き込まれますので、それぞれの構造体には実体が必要です。

depthLevel には基準面までのカメラからの距離 Dlevel を、factor には立体具合の調整係数 Dr を指定します。factor は、計算結果を補正するために使用されます。0.0 を渡すと視差がなくなり、1.0 で補正なしの状態になります。この値以外にも、3D ボリュームの入力値が計算結果に影響を与えます。

pivot で指定された方向にカメラの上方向が向くように、出力されるプロジェクション行列の出力に対して回転行列が乗算されます。デフォルトは nn::math::PIVOT_UPSIDE_TO_TOP で、カメラの上方向が LCD の上方向(下画面が配置されていない側の長辺の方向)に向くような回転行列が乗算されます。ベースカメラの設定で回転が考慮されているなど、回転処理が不要な場合は nn::math::PIVOT_NONE を指定してください。

update3DVolume は行列計算時点の 3D ボリュームの値を取得して利用するかどうかを指定します。引数を指定しなければ、利用する(デフォルト:true)になります。 行列計算時点の 3D ボリュームの値を利用する場合、1 フレーム内に何度も上記関数を用いて行列を計算する実装では 3D ボリュームの値が途中で変化し、描画結果に影響を与える可能性があります。

そのような実装では update3DVolume を利用しない(false)に指定し、各フレームの冒頭でメンバ関数 Update3DVolume() を呼び出すようにしてください。呼び出さなければ、3D ボリュームの操作に描画結果が連動しません。

コード 6-7. 3D ボリューム値を更新
void Update3DVolume(void);

6.2.3.5. 視差情報の取得

Initialize() で初期化された場合を除き、最後に行列計算を行った結果に基づいて、カメラから指定した距離に像を結ぶために必要な視差を取得するためのメンバ関数が用意されています。

コード 6-8. 視差情報の取得
f32 GetParallax(const f32 distance) const;
f32 GetMaxParallax(void) const;

GetParallax() はカメラから distance 離れた位置、GetMaxParallax() は無限遠にあるオブジェクトを描画するために必要な視差を画面幅に対する割合(1.0 が 100 %)で返します。返り値に LCD の解像度を乗算することで、(ベースカメラでの表示位置から)左右それぞれに何ピクセルずらして描画すればよいかを得ることができます。

distance で指定した位置が基準面より手前ならば負の値が返され、基準面より奥ならば正の値が返されます。distance に負の値を渡した場合は 0 が返されます。

透視射影で 3D オブジェクトを描画する場合はライブラリによって生成された行列を利用することで視差を意識する必要はありませんでしたが、2D のオブジェクトを描画する場合や正射影で描画する場合はオブジェクトごとに視差計算を行う必要があります。しかし、描画するオブジェクトごとに GetParallax() で視差を計算すると、CPU の処理負荷が高くなってしまいます。そこで、以下の関数で視差計算の途中の値を取得し、残りの計算を頂点シェーダで行わせることで高速化することができます。

コード 6-9. 視差計算の途中経過の取得
f32 GetCoefficientForParallax(void) const;

この関数で返される値に ((distance-depthLevel)/distance) を乗算することで、GetParallax() が返す値と同じ値を求めることができます。

視差情報と同じように、計算結果に基づいて、ベースカメラから基準面、ニアクリップ面、ファークリップ面それぞれまでの距離を取得するためのメンバ関数が用意されています。

コード 6-10. ベースカメラから基準面、ニアクリップ面、ファークリップ面までの距離の取得
f32 GetDistanceToLevel(void) const;
f32 GetDistanceToNearClip(void) const;
f32 GetDistanceToFarClip(void) const;

これらの関数は、主に現実感優先の算出方法で変更されたベースカメラの情報を得るために使用します。アプリケーション優先の算出方法で計算した場合、これらの関数は計算のために渡した情報そのままを返します。

6.2.3.6. 終了

nn::ulcd::StereoCamera クラスのインスタンスが不要になった場合は、メンバ関数の Finalize() を呼び出してください。

コード 6-11. 終了処理
void Finalize(void);

この関数はデストラクタで呼び出されますが、必ずアプリケーションからも明示的に呼び出してください。

6.2.4. レンダリングとディスプレイバッファへの転送

透視射影で 3D オブジェクトの描画を行っている場合は、ULCD ライブラリで算出した左右のカメラ用のプロジェクション行列とビュー行列を使用することで、視差を意識せずにレンダリングすることができます。しかし、2D オブジェクトや正射影で 3D オブジェクトを描画する場合は、アプリケーションで視差を考慮しながら左目用と右目用の画像をレンダリングしなければなりません。2D オブジェクトも透視射影で描画すれば視差を意識せずにレンダリングすることができますが、オブジェクト配置の前後関係には配慮しなければなりません。

カラーバッファにレンダリングされた画像を LCD に表示するためにディスプレイバッファへ転送しますが、左目用と右目用で異なるディスプレイバッファに転送しなければならないことに注意してください。

6.2.5. LCD への表示

左目用と右目用のディスプレイバッファを、それぞれ NN_GX_DISPLAY0NN_GX_DISPLAY0_EXT で指定する画面に関連付けます。それからディスプレイバッファのスワップを行いますが、立体視表示時の右目用ディスプレイバッファは上画面(NN_GX_DISPLAY0)が指定されたときに同時に行われるため、nngxSwapBuffers()NN_GX_DISPLAY0_EXT を含めて渡す必要はありません。

コード 6-12. ディスプレイバッファの関連付けとバッファスワップ
// UpperLCD for Left Eye
nngxActiveDisplay(NN_GX_DISPLAY0);
nngxBindDisplaybuffer(m_Display0Buffers[m_CurrentDispBuf0]);
// UpperLCD for Right Eye
nngxActiveDisplay(NN_GX_DISPLAY0_EXT);
nngxBindDisplaybuffer(m_Display0BufferExt[m_CurrentDispBuf0Ext]);
// Swap buffers
nngxSwapBuffers(NN_GX_DISPLAY0);

6.2.6. 3D ボリュームの入力について

最大にした状態が立体視に特に支障を感じない方に対する「お薦め位置」となるように調整してください。

3D ボリュームを下げて立体視表示(3D 表示)から通常表示(2D 表示)に切り替わったときは、システム側で自動的に LCD のシャッターをオフにします。また、2D 表示と 3D 表示の切り替えや液晶バックライトの輝度についてもシステムが自動的に制御を行いますので、デバッグ用途を除いてはアプリケーションから制御を行う必要はありません。

6.2.7. 立体視表示の無効化について

本体設定の保護者による使用制限で 3D 映像の表示を制限している場合や 3D ボリュームを 0(最低値)に下げている場合は、強制的に立体視表示が無効となり、表示モードの設定や右目用の LCD への表示は無視され、必ず通常表示(2D 表示)となります。

立体視表示が許可されている状態であるかどうかは、nn::gx::IsStereoVisionAllowed() で確認することができます。この関数は立体視表示が許可されている状態のときに true を返し、強制的に立体視表示が無効化される状態のときに false を返します。この判定に表示モードの設定は影響しません。

6.2.8. 左右を反転する場合

立体視表示で左右反転の表示をする場合、プロジェクション行列の X 軸を反転させるだけでは正しく表示されず、奥行きの位置が基準面を境に反転してしまいます。

この問題を解消するには、X 軸を反転させたプロジェクション行列で立体視表示のプロジェクション行列とモデルビュー行列を生成し、その左右を入れ替えて表示します。

コード 6-13. 立体視表示で左右を反転する場合
nn::math::Matrix44 proj,rev;
nn::math::Matrix44 projL, projR;
nn::math::Matrix34 viewL, viewR;
// SetBaseFrustum
MTX44Frustum(&proj, l, r, b, t, n, f);
MTX44Identity(&rev);
rev.m[0][0] = -1.0f;
MTX44Mult(&proj, &proj, &rev);         // X軸で反転
s_StereoCamera.SetBaseFrustum(&proj);
// SetBaseCamera
nn::math::Matrix34 cam;
nn::math::Vector3 camUp(0.f, 1.f, 0.f);
nn::math::MTX34LookAt(&cam, &camPos, &camUp, &focus);
s_StereoCamera.SetBaseCamera(&cam);
// CalcurateMatrices
s_StereoCamera.CalculateMatrices(
    &projR, &viewR, &projL, &viewL,    // 左右の行列を逆に指定
    depthLevel, factor, nn::math::PIVOT_UPSIDE_TO_TOP);

下図は、立体視表示での左右反転の原理を示したものです。

図 6-6. 立体視表示での左右反転の原理

左目 右目 プロジェクション行列のX軸を反転 生成された行列の左右を入れ替える 正しい奥行きで左右反転される 奥行きが反転してしまう!

6.3. 予約フラグメントシェーダを使用する場合

シャドウを使う場合、シャドウテクスチャはライトからの深度情報ですので、シャドウテクスチャの生成時には立体視表示によるビュー行列の変化は関係しません。オブジェクトのレンダリング時にはビュー行列が関係するため、右目用と左目用に 2 回のレンダリングが必要となります。その際、シャドウテクスチャを適用するためのテクスチャ座標の変換行列には注意が必要です。

ガスレンダリングを使う場合、第 1 パスでデプス情報を生成する時点でビュー行列が関わりますので、立体視表示をするにはすべてのパスを左右別々に実行しなければなりません。

フォグを使う場合、参照テーブルへの入力値はウィンドウ座標系でのデプス値ですので、立体視表示でプロジェクション行列が変更になると参照テーブルを再作成しなければなりません。アプリケーション優先の算出方法であれば、立体視表示でもベースカメラのプロジェクション行列で参照テーブルを作成することができます。しかし、現実感優先の算出方法ではニアクリップ面とファークリップ面がベースカメラの値から変化するため、3D ボリュームで調整が行われるだけでも参照テーブルを再作成しなければなりません。

フラグメントライティング全般では、どのように座標系を統一するかにもよりますが、ライト位置がビュー行列の影響を受ける場合、左右それぞれのビュー行列を考慮したライト位置でレンダリングしなければなりません。

6.4. ステレオカメラとの連動

左右のカメラは 2 つあるポートにそれぞれ接続されていますが、ポート 1 が右目用(NN_GX_DISPLAY0_EXT)で、ポート 2 が左目用(NN_GX_DISPLAY0)の画像をキャプチャしていることに注意してください。また、カメラは 2 つのポートからそれぞれのバッファへと画像データを出力しますが、YUVtoRGB 回路は 1 つですので、YUV フォーマットで取得したカメラからのキャプチャ画像を RGB フォーマットに変換するときにはアプリケーションで排他処理をするなどの工夫が必要です。また、左右の画像が同じフレームで取得されたものでなければ、うまく立体的に表示されない場合があります。

左右のカメラで得られた画像をそのまま表示することでも立体的に表示されますが、カメラの間隔と両目の間隔が同一ではないことに注意しなければなりません。

6.4.1. ステレオカメラのキャリブレーション

外側にある 2 つのカメラは水平かつ 35 mm 離れて配置されるように設計されていますが、製造段階での取り付け精度によって左右のカメラで多少の誤差が発生し、左右のカメラ画像にずれが生じます。取り付け誤差による画像のずれは、カメラライブラリ内では自動的に補正されませんので、アプリケーション側で補正しなければなりません。

補足:

完全には補正できず、補正後の画像の周辺部に多少のずれが残る場合があります。

ステレオカメラのキャリブレーションデータは nn::camera::GetStereoCameraCalibrationData() で取得することができます。キャリブレーションデータは nn::camera::StereoCameraCalibrationData 構造体に格納されます。

コード 6-14. ステレオカメラのキャリブレーションデータの取得
void nn::camera::GetStereoCameraCalibrationData(
        nn::camera::StereoCameraCalibrationData * pDst);

基本的にキャリブレーションデータの各メンバ変数は、左のカメラ画像を右のカメラ画像に合わせるために必要な補正(拡大縮小、光軸の回転、並進移動)と補正値の測定条件を示しています。

表 6-1. キャリブレーションデータが取り得る値の範囲
項目 メンバ 取り得る値の範囲
拡大縮小 scale 0.9604 ~ 1.0405
Z 軸(光軸)の回転角度 rotationZ -1.6 ~ +1.6(単位:度)
X(水平)方向の並進量 translationX -154 ~ -19(単位:ピクセル)
Y(垂直)方向の並進量 translationY -70 ~ +70(単位:ピクセル)

2 つの並進量が取り得る値の範囲の幅が広いため、そのまま左のカメラの画像を移動させて補正すると画像の端が表示画面内に入ってしまい、表示画面の端が欠ける可能性があります。キャリブレーションデータを立体視表示に利用する場合は、CAMERA ライブラリで用意されている補正行列の計算のための関数を使用してください。取り付け誤差が限度値であっても、画像の端が欠ける場合には拡大表示によって表示の画像サイズとなるように対応した関数が用意されています。立体視表示以外の処理(画像認識処理など)に利用する場合は、キャリブレーションデータの値によって処理の精度が落ちるなどの不具合が起こらないように注意してください。

以下の関数を利用して、左のカメラ画像に乗算しなければならない補正行列をキャリブレーションデータから計算することができます。

コード 6-15. 補正行列の計算
void nn::camera::GetStereoCameraCalibrationMatrix(
        nn::math::MTX34 * pDst, 
        const nn::camera::StereoCameraCalibrationData & cal, 
        const f32 translationUnit, const bool isIncludeParallax = true);
void nn::camera::GetStereoCameraCalibrationMatrixEx(
        nn::math::MTX34 * pDstR, nn::math::MTX34 * pDstL, f32 * pDstScale, 
        const nn::camera::StereoCameraCalibrationData & cal, 
        const f32 translationUnit, const f32 parallax, 
        const s16 orgWidth, const s16 orgHeight,
        const s16 dstWidth, const s16 dstHeight);
f32 nn::camera::GetParallax(
        const nn::camera::StereoCameraCalibrationData & cal, f32 distance);

nn::camera::GetStereoCameraCalibrationMatrix() では、cal にキャリブレーションデータ、translationUnit に 3D 空間上での並進移動の単位量(補足を参照)を渡すことで、pDst に補正行列を取得することができます。isIncludeParallaxtrue を渡すと、補正行列に測定チャート上の視差が含まれ、カメラから 250 mm 離れた対象に焦点が合うようになります。取り付け誤差だけを補正する場合には isIncludeParallaxfalse を渡してください。

補足:

translationUnit には、並進移動量(カメラ画像を 1 ピクセル移動させるのに必要な 3D 空間上での移動量)に VGA 画像と表示画像の大きさ(幅)の比をかけた値を指定します。ただし、これは VGA 画像と異なるサイズでそのまま表示する場合や補正行列をかけてから画像を拡大する場合の指定方法です。補正行列をかける前に画像が貼られるオブジェクトを拡大し、VGA 画像に pixel by pixel で表示された被写体と同じ大きさに画像内の被写体が表示される場合は並進移動量そのままを指定してください。

nn::camera::GetStereoCameraCalibrationMatrixEx() では、取り付け誤差が限度値になる本体で画面端が立体視できない問題に対応した補正行列を計算します。また、nn::camera::GetParallax() で取得する視差を利用していますので、計算される補正行列は特定の距離に焦点を合わせたものになります。以上のことから、特別な理由がない限り、補正行列の計算にはこちらの関数を使用することを推奨します。

引数 caltranslationUnit に渡す値は前述の関数と同じです。parallax に渡す VGA 画像における視差(ピクセル単位)の計算には GetParallax() を使用します。視差は引数 distance で指定された距離(メートル単位)に焦点が合うように計算されます。orgWidthorgHeight にはカメラ画像の(トリミング後の)幅と高さをピクセル単位で指定します。dstWidthdstHeight にはレンダリングに必要な幅と高さをピクセル単位で指定します。

補正行列は pDstRpDstL に左右のカメラそれぞれに乗算する行列が返され、pDstScale にはレンダリングのサイズにするために必要な拡大縮小率が返されます。

補足:

補正行列の計算の詳細については関数リファレンスを参照してください。

nn::camera::GetParallaxOnChart() は、測定チャート上での視差(カメラから 250 mm 離れた対象に焦点が合うような視差)をピクセル単位で取得することができます。

コード 6-16. 測定チャート上での視差の取得
f32 nn::camera::GetParallaxOnChart(
        const nn::camera::StereoCameraCalibrationData & cal);

取り付け誤差による水平方向のずれは、キャリブレーションデータの translationX メンバからこの関数で取得した値を減算したものと同じです。

6.4.2. ステレオカメラの明るさを連動させる

ステレオカメラでは、自動露出などの内部処理が左右のカメラで独立しているため、被写体によっては画像の明るさが左右で異なる場合があります。これに対応するため、CAMERA ライブラリにはステレオカメラの画像の明るさを自動的に連動させる nn::camera::SetBrightnessSynchronization() が用意されています。

コード 6-17. ステレオカメラの明るさを連動させる
nn::Result nn::camera::SetBrightnessSynchronization(bool enable);

enable true を指定することで連動する設定になります。デフォルトの設定は無効(false)です。また、ステレオカメラ以外の組み合わせでは機能しません。

この関数は外側カメラ(R)と外側カメラ(L)を起動させていない状態でも呼び出すことができます。連動を有効にした場合は、そのあとでカメラをスタンバイ状態にしても設定は記憶されたままとなり、再度ステレオカメラを起動させたときには連動状態が継続します。

この機能は、定期的に外側カメラ(R)の露出に関する設定を外側カメラ(L)に書き込むことで、ステレオカメラの画像の明るさを連動させます。特に自動露出が有効であるときは、外側カメラ(R)の露出量の変化に追従して外側カメラ(L)の露出量が変化します。設定の連動はライブラリが作成した低優先度のスレッド(アプリケーションのリソースを消費しません)で行なわれるため、処理が重いときに連動が遅れることがあります。

カメラの設定が以下のいずれかに該当すると、この関数の実行に失敗します。また、連動を有効にしたあとは、以下のいずれかの設定を行うと実行に失敗します。

  • ホワイトバランス設定が WHITE_BALANCE_NORMAL 以外
  • オートホワイトバランス設定が無効
  • 撮影モードが PHOTO_MODE_LANDSCAPE
  • コントラスト設定が CONTRAST_PATTERN_10

 

注意:

カメラの再起動処理中に呼び出したときは、ライブラリ内で処理が長時間ブロックされることがありますので注意してください。

6.5. 注意点など

補足:

この節の内容は、別途検討中のガイドラインに従って変更・追加される可能性があります。

6.5.1. 手前へのオブジェクト配置

立体視表現を用いて LCD 表面よりも手前に飛び出すようにオブジェクトを配置する場合は、飛び出したオブジェクトが LCD の縁にかからないように配置してください。(見かけ上)奥にあるはずの画面の縁によって、手前にあるはずのものが隠されるといった不自然な状態になってしまい、プレイヤーが正しい立体感を得ることができません。

また、ニアクリップ面をどこに置くかには注意が必要です。ニアクリップ面はカメラにごく近い位置に置くことが一般的ですが、その場合ニアクリップ面への距離はプレイヤーに正しい立体感を与える手前方向の限界距離とは一致しません。そのため、オブジェクトがカメラに近寄り過ぎると、立体視ができなくなります。

しかし、単純にニアクリップ面をカメラから遠ざければよいという問題ではないため、それぞれのアプリケーションにおいてオブジェクトがカメラに近寄り過ぎないように配慮する必要があります。

6.5.2. 2D オブジェクトとの混合配置

2D のオブジェクトを 3D オブジェクトと同時に表示するために、正射影と透視射影のように異なるカメラ設定でレンダリングした結果を合成する場合は、2D と 3D のオブジェクトが正しい前後関係となるように 2D オブジェクトの描画位置やサイズを調整する必要があります。

2D オブジェクトのレンダリングに対しても透視射影によってレンダリングすれば、2D オブジェクト自身の調整を意識する必要はありませんが、3D オブジェクトと 2D オブジェクトの配置の前後関係に配慮が必要な点は変わりません。3D オブジェクトによって 2D オブジェクトが隠れてしまうことが起こりえます。

6.5.3. 本体を傾けたときの対処

立体視表示を行っているときに、本体を手前や奥の方向への傾けてもあまり問題にはなりませんが、本体を左右に傾けたり回転させたりすると、LCD 面がユーザーと正対していないために立体視が困難になる可能性があります。

この問題への対処として、本体に内蔵されている「顔シューティング」ではジャイロセンサーで本体が傾いているかどうかを判断し、本体が傾いていることを検出したときに立体具合の調整係数が弱くなる(立体視の強度を下げる)ように実装されています。動きの激しいアクションゲームなどで立体視表示を行う場合は、ジャイロセンサーで本体の傾きを検知して立体視の強度を調整することを推奨します。

6.5.4. 表示モードで立体視表示を無効にしたときの注意点

アプリケーションが nngxSetDisplayMode(NN_GX_DISPLAYMODE_NORMAL) で立体視表示を無効にしていても、nn::ulcd::CalculateMatrices() は 3D ボリュームに連動した値を返します。そのため、ULCD ライブラリで計算された行列をそのまま表示に使用していると、3D ランプが消えている状態でも、上画面の表示が 3D ボリュームに連動して変化してしまいます。

nngxSetDisplayMode() で立体視表示を無効にしているときは CalculateMatrices() を使用しないか、factor に 0.0 を渡してください。

この現象は「6.2.7. 立体視表示の無効化について」のように強制的に立体視表示が無効化されるときには起こりません。