視点とビューアー: WebXR でのカメラのシミュレーション

アプリケーションで視点とカメラを管理するためのコードを検討する際に理解する最初で最も重要なことは、WebXR にカメラがないことです。 WebGL または WebXR API によって提供される、回転して移動するだけで画面に表示されるものを自動的に変更できるビューアーを表す魔法のオブジェクトはありません。 このガイドでは、カメラを動かさずに WebGL を使用してカメラの動きをシミュレートする方法を示します。 これらの手法は、任意の WebGL(または WebXR)プロジェクトで使用できます。

3D グラフィックスのアニメーション化は、コンピューターサイエンス、数学、芸術、グラフィックデザイン、運動学、解剖学、生理学、物理学、および映画撮影における複数の分野を統合するソフトウェア開発の領域です。 私たちは実際のカメラを持っていないので、実際にユーザーをシーン内で動かすことなく、カメラを持っているかのような効果を再現するカメラを想像します。

WebGL と WebXR の背後にある基本的な数学、幾何学、およびその他の概念についての記事がいくつかあります。 これは、この記事を読む前または読んでいるときに役立つかもしれません。

編注: この記事で使用されているほとんどの図は、標準的な動作を実行しながらカメラがどのように動くかを示すために、FilmmakerIQ ウェブサイトの記事から取られました。 つまり、ウェブ全体で見られるこの画像からです。 それらは頻繁に再利用されるため、許可されたライセンスの下で利用できると想定しているため、所有権は不明です。 私たちはそれが自由に使えることを望みます。 そうでない場合で、あなたが所有者である場合はお知らせください。 新しい図を検索または作成します。 または、画像の使用を継続してよろしければ、適切にクレジットできるようにお知らせください。

カメラと相対運動

古典的な実写映画を撮影する時は、俳優はセットにいて、演技しながらセットを動きまわり、1 台以上のカメラでその動きを見守ります。 カメラは固定することもできますが、動き回るように設定したり、パフォーマーの動きを追跡したり、ドリーイン/アウトして感情的な影響を与えたりすることもできます。

仮想カメラ

WebGL(そして、拡張によって、WebXR)では、移動および回転できるカメラオブジェクトがないため、これらの動きのふりをする方法を見つける必要があります。 カメラがないので、そのふりをする方法を見つけなければなりません。 幸いなことに、ガリレオ、ニュートン、ローレンツ、アインシュタインのような物理学者は私たちに**相対性原理**を与えてくれました。 それは物理学の法則がすべての参照系で同じ形であると述べています。 つまり、どこに立っていても、物理法則は同じように機能します。

つまり、あなたと他の人が固い石の何もないフィールドに立っていて、目に見える限り他のものが見えない場合、あなたが他の人に向かって 3 メートル移動すると、他の人があなたに向かって 3 メートル移動した場合と同じ結果になります。 どちらにも違いを見分ける方法はありません。 第三者は違いを見分けることができますが、2 人にはできません。 カメラの場合は、カメラを動かすか、カメラの周りのすべてを動かすことで、同じ視覚結果を得ることができます。

そしてそれが私たちの解決策です。 カメラを動かすことができないので、カメラの周りの世界を動かします。 レンダラーは、カメラがどこにあると想像するかを知っている必要があります。 次に、すべての可視オブジェクトの位置を変更して、その位置と方向をシミュレートします。 したがって、実際のカメラオブジェクトを参照する代わりに、WebGL および WebXR プログラミングではカメラ(camera)という用語を使用して、3D 空間に実際のオブジェクトが存在するかどうかにかかわらず、シーンの仮定のビューアーの位置と視線方向を表すオブジェクトを参照します。

視点

カメラは仮想オブジェクトであり、必ずしも仮想世界の物理的なオブジェクトを表すのではなく、ビューアーの位置と視線方向を表すため、カメラの使用を必要とする状況の種類について考えることは役立ちます。 ゲーム関連の状況は、多くの場合ゲーム固有の特殊なケースであるため、個別にリストされていますが、これらのパースペクティブのいずれも 3D グラフィックシーンに適用される場合があります。

一般化されたカメラ

一般に、仮想カメラはシーン内の物理オブジェクトに組み込まれる場合と組み込まれない場合があります。 実際、3D ゲームの範囲外では、カメラがシーンに表示されるオブジェクトとまったく対応しない可能性がはるかに高くなります。 次が 3D カメラの使用例です。

  • アニメーションをレンダリングするとき — 映画制作のために、またはプレゼンテーションやゲームのコンテキスト内で使用するために — 仮想カメラは、実世界のフィルムカメラのように使用されます。 視聴者(ビューアー)はおそらくそれらの手法を使用して映画を鑑賞して成長しており、映画やアニメーションがそれらの方法に従うという潜在意識の期待があるため、可能な限り、標準の映画撮影手法が使用されます。 それらから逸脱すると、視聴者をその瞬間から引き離すことができます。
  • ビジネスアプリケーションでは、3D カメラを使用して、グラフやチャートなどをレンダリングするときに、見かけの大きさとパースペクティブを簡単に設定できます。
  • 地図アプリケーションでは、カメラをシーンの真上に配置するか、さまざまな角度を使用してパースペクティブを表現できます。 3D GPS ソリューションの場合、カメラはユーザーの周囲の領域を表示するように配置され、ディスプレイの大部分はユーザーの移動経路の前方の領域を示します。
  • WebGL を使用して 2D グラフィックス描画を高速化する場合、通常、カメラはシーンの中心の真上に配置され、距離と視野はシーン全体を表示できるように設定されます。
  • ビットマップグラフィックスを高速化する場合、レンダラーは 2D 画像を WebGL テクスチャーのバッファーに描画し、テクスチャーを再描画して画面をリフレッシュします。 これは基本的に、2D グラフィックアプリケーションでマルチプルバッファリング(multiple buffering)を実行するためのバックバッファーとしてテクスチャーを使用します。

ゲームにおけるカメラ

ゲームにはさまざまな種類があり、カメラをゲームで使用するにはいくつかの方法があります。 一般的な状況は次のとおりです。

  • ファーストパーソンゲームでは、カメラはプレイヤーのアバターの頭の中にあり、アバターの目と同じ方向を向いています。 このようにして、プレイヤーの画面またはヘッドセットに表示されるビューは、アバターが見るものです。
  • 一部のサードパーソンゲームでは、カメラがプレイヤーのアバターや乗り物の後ろの少し離れたところにあり、ゲームの世界を移動するときに後ろから見ています。 これは、多くのマルチプレイヤーオンラインロールプレイングゲーム、特定のシューティングゲームなどで使用されます。 人気のある例には、World of Warcraftトゥームレイダーフォートナイト などがあります。 このカテゴリーには、カメラがプレイヤーの肩の真上に配置されるゲームも含まれます。
  • 一部の 3D ゲームは、フライトシミュレーターで航空機のさまざまなウィンドウを見る、またはステージ(game level)内のすべての防犯カメラからのビューを見るなど、視点を変更する能力を提供します(スパイとステルスベースのゲームの一般的な機能)。 この能力は、スコープ付きの武器を提供するゲームでも使用されます。 この場合、ビューは、もはや頭の位置に完全に基づいていません。
  • 3D ゲームは、目に見えない種類のアバターを配置するか、固定の仮想カメラを選択して監視することにより、非プレイヤーがアクションを観察する能力も提供します。
  • 高度な 3D ゲームでは、カメラまたはカメラのようなオブジェクトを使用して、プレイヤーキャラクターが使用できるものと同じレンダリングエンジンおよび物理エンジンによって、非プレイヤーキャラクターが何を見ることができるかを決定できます。
  • シングルスクリーン 2D ゲームでは、カメラはプレイヤーやゲーム内の他のキャラクターに直接関連付けられていませんが、代わりにゲームプレイエリアの上または横に固定されているか、アクションがスクロールするゲーム世界を動き回るときにアクションに従います。 例えば、パックマンなどの古典的なアーケードゲームは固定されたゲームマップ上で行われるため、カメラはマップ上の設定距離に固定され、常にゲームの世界を真下に向けます。
  • スーパーマリオブラザーズなどの横スクロールゲームまたは縦スクロールゲームでは、カメラは左右(または上下、あるいはその両方)に沿って移動するため、ステージがビューポートよりはるかに大きくても、アクションは表示されたままになります。

カメラの配置

WebGL または WebXR には標準のカメラオブジェクトがないため、カメラを自分でシミュレートする必要があります。 そうする前に、そしてカメラの動きをシミュレートする前に、実際に仮想カメラとその動きを最も基本的なレベルで見てみましょう。 すべてのものと同様に、空間内のオブジェクトの位置(position)は、たとえ仮想空間であっても、原点を基準にした位置を示す 3 つの数値を使用して表すことができ、原点の位置は (0, 0, 0) と定義されています。

オブジェクトと空間の原点との空間関係には、考慮する必要のある別の側面があります。 それはパースペクティブ(perspective)です。 パースペクティブは、シーン内のオブジェクトに適切に適用されると、通常の 2D 画面と同じくらい平面に見えるシーンを取り、まるで 3D であるかのように飛び出させることができます。 パースペクティブにはいくつかの種類があります。 それらは定義されており、それらの数学は WebGL のモデル、ビュー、投影の記事で説明されています。 重要なのは、ベクトルに対するパースペクティブの効果は、ベクトルに w と呼ばれる 4 番目のパースペクティブ成分を追加することによって表すことができるということです。

w の値は、他の 3 つの成分のそれぞれをそれで除算することによって適用され、最終的な位置またはベクトルを取得します。 つまり、(x, y, z, w) として与えられた座標の場合、3D 空間の点は実際には (x/w, y/w, z/w, 1) または単に (x/w, y/w, z/w) です。 パースペクティブを使用していない場合、w は常に 1 です。 その場合、(1, 0, 3) にあるオブジェクトの完全な座標は (1, 0, 3, 1) になります。

ただし、3D 空間のオブジェクトを表現するには、場所だけでは不十分です。 なぜなら、空間内のオブジェクトの状態は、その位置だけでなく、その回転(rotation)または向き(facing direction)としても知られる方向(orientation)に関するものだからです。 方向は 3D ベクトルを使用して表すことができ、これは通常、長さが 1.0 になるように正規化されています。 例えば、オブジェクトが (3, 1, -2) にあるオブジェクトに向いている場合、つまり、原点から 3 メートル右、1 メートル上、2 メートル向こうに離れているオブジェクトに向いている場合、結果は次のようになります。

[ 3 1 - 2 ] \left [ \begin{matrix} 3 \ 1 \ -2 \end{matrix} \right ]

これは次のように配列として表すこともできます。

js
let directionVector = [3, 1, -2];

座標と向きのベクトルの両方を含む操作を実行するために、ベクトルには w 成分を含める必要があります。 ベクトルの w の値は常に 0 であるため、前述のベクトルは [3, 1, -2, 0] または次のように表すこともできます。

[ 3 1 - 2 0 ] \left [ \begin{matrix} 3 \ 1 \ -2 \ 0 \end{matrix} \right ]

WebXR は、ベクトルを 1 メートルの長さに自動的に正規化します。 ただし、正規化を繰り返し実行する必要はないため、計算のパフォーマンスを向上させるなど、さまざまな理由で自分で行う方が理にかなっている場合があります。

カメラにさせたい動きの組み合わせを表す行列を決定したら、カメラは動かせないため、それを逆にする必要があります。 実際にはカメラ以外のすべてのものを移動しているので、変換行列の逆行列を取って、逆変換行列を取得します。 次に、この逆行列を世界のオブジェクトに適用して、オブジェクトの位置と方向を変更し、希望するカメラ位置をシミュレートできます。

これが、WebXR が変換を表すために使用する XRRigidTransform オブジェクトに、inverse プロパティが含まれている理由です。 inverse プロパティは、親変換の逆行列である別の XRRigidTransform オブジェクトです。 ビューを表す XRView には、カメラビューを提供する XRRigidTransform である transform プロパティがあるため、次のように、モデルビュー行列(世界を移動して目的のカメラ位置をシミュレートするために必要な変換行列)を取得できます。

js
let viewMatrix = view.transform.inverse.matrix;

使用しているライブラリが XRRigidTransform オブジェクトを直接受け入れる場合は、ビュー行列を表す配列だけを取り出すのではなく、代わりに view.transform.inverse を取得できます。

複数の変換の合成

カメラが同時にズームやパンなどの複数の変換を実行する必要がある場合は、変換行列を乗算して、両方の変更を一度に適用する単一の行列に合成できます。 これを実行する明確で読みやすい関数については、ウェブの行列計算の記事の2 つの行列の乗算を参照してください。 あるいは、glMatrix などのお好みの行列計算ライブラリを使用して処理してください。

乗算が可換である(つまり、左から右に乗算しても右から左に乗算しても同じ答えが得られる)典型的な算術とは異なり、行列の乗算は可換ではないことに注意してください! これは、各変換がオブジェクトの位置と、場合によっては座標系自体に影響を与え、実行される次の操作の結果を劇的に変える可能性があるためです。 そのため、複合変換を作成するとき(または変換を順番に直接適用するとき)は、変換を適用する順序に注意する必要があります。

変換の適用

変換を適用するには、点またはベクトルに変換または変換の合成を掛けます。

これは、物理的な場所、方向または向き、およびパースペクティブの観点から見た位置の概念の概要です。 このテーマの詳細については、幾何学と参照空間WebGL のモデル、ビュー、投影、およびウェブの行列計算の記事を参照してください。

古典的な映画撮影のシミュレーション

映画撮影は、カメラの動きを設計、計画、そして実行することで、アニメーションまたはフィルムのシーンに望ましい外観と感情を生み出す芸術です。 主にカメラの動きについて理解するのに役立つ用語がいくつかありますが、これらの用語を、仮想カメラを使って設計された視点の変更を説明するために使用します。 これらの動きを同時に複数実行することも完全に可能です。 例えば、シーンをズームインしながらカメラをパンできます。

カメラの動きの大部分は、カメラの参照空間を基準にして記述されていることに注意してください。

行列を格納するための形式は、通常、列優先でフラット化された配列です。 つまり、行列の値は、左上隅から下に向かって書き込まれ、次に右に 1 行移動して、すべての値が配列になるまで繰り返されます。

したがって、次のような行列になります。

[ a 1 a 5 a 9 a 13 a 2 a 6 a 10 a 14 a 3 a 7 a 11 a 15 a 4 a 8 a 12 a 16 ] \left [ \begin{matrix} a_{1} & a_{5} & a_{9} & a_{13} \ a_{2} & a_{6} & a_{10} & a_{14} \ a_{3} & a_{7} & a_{11} & a_{15} \ a_{4} & a_{8} & a_{12} & a_{16} \end{matrix} \right ]

これは、次のように配列形式で表されます。

js
let matrixArray = [
  a1,
  a2,
  a3,
  a4,
  a5,
  a6,
  a7,
  a8,
  a9,
  a10,
  a11,
  a12,
  a13,
  a14,
  a15,
  a16,
];

この配列では、左端の列にエントリ a1、a2、a3、a4 が含まれています。 一番上の行には、エントリ a1、a5、a9、a13 が含まれています。

ほとんどの WebGL および WebXR のプログラミングは、サードパーティライブラリを使用して行われることを覚えておいてください。 サードパーティライブラリは、コアとなる行列やその他の操作だけでなく、多くの場合、これらの標準的な映画撮影手法のシミュレーションをより簡単にするルーチンを追加することにより、WebGL の基本機能を拡張します。 WebGL を直接使用するのではなく、その 1 つを使用することを強くお勧めします。 このガイドでは WebGL を直接使用しています。 これは、その裏で何が行われているのかをある程度理解し、ライブラリの開発を支援したり、コードを最適化するのに役立つためです。

メモ: 「カメラを動かす」などのフレーズを使用していますが、実際に行っているのは、カメラの周りの世界全体を動かすことです。 これは、特定の値が機能する方法に影響を与えます。 これらの値については、後で説明します。

ズーム

最もよく知られているカメラ効果には、ズーム(zoom)があります。 ズームは、レンズの焦点距離を変更することにより、物理的なカメラで実行されます。 これは、レンズ自体の中心とカメラの光センサーの間の距離です。 したがって、ズームは実際にはカメラを動かすことをまったく含みません。 代わりに、ズームショットは、時間の経過とともにカメラの倍率を変化させて、実際にカメラを物理的に動かさなくても、焦点領域がビューアーに近づいたり遠ざかったりするように見せます。 ゆっくりとした動きはシーンに動き、気軽さ、または集中の感覚をもたらすことができ、急速なズームは不安、驚き、または緊張の感覚を生み出すことができます。

ズームはカメラの位置を動かさないので、結果として生じる効果は不自然です。 人間の目にはズームレンズがありません。 物を遠ざけたり、近づけたりして、物を小さくしたり大きくしたりします。 映画撮影では、それはドリーショットと呼ばれます。

3D グラフィックスにも 2 つの手法があり、同一ではありませんが類似した結果を作成でき、その方法はさまざまな状況でより簡単に適用できます。

視野調整によるズーム

カメラの視野(field of view、FOV)を変更することで、真の「ズーム」に似たことができます。 視野とは、一度に見る必要のある、カメラを囲む可視領域全体の円弧の長さを定義する角度です。 これは実際のカメラの焦点距離の効果であり、真のカメラがないため、FOV を変更することは無難な代案です。

円の円周は 2π⋅r ラジアン(360°)であることを思い出してください。 そのため、これは理論上の最大 FOV です。 ただし、現実的には、人間の目はそこまで見えないだけでなく、モニターや VR ゴーグルなどの表示デバイスを使用すると、視野がさらに狭くなる傾向があります。 人間の目は通常、約 135°(約 2.356 ラジアン)の水平視野と約 180°(π または約 3.142 ラジアン)の垂直視野を持っています。

カメラの FOV を小さくすると、ビューポートに含まれる弧が減少し、ビューにレンダリングされるときにそのコンテンツが拡大します。 これと光学ズーム効果の間には違いがありますが、一般的には仕事を完了するのに十分近い結果です。

次の関数は、指定された視野角と指定されたニアクリッピングプレーンおよびファークリッピングプレーンの距離を統合する透視射影行列を返します。

js
function createPerspectiveMatrix(viewport, fovDegrees, nearClip, farClip) {
  const fovRadians = fovDegrees * (Math.PI / 180.0);
  const aspectRatio = viewport.width / viewport.height;

  const transform = mat4.create();
  mat4.perspective(transform, fovRadians, aspectRatio, nearClip, farClip);
  return transform;
}

FOV 角度 fovDegrees を度数からラジアンに変換し、viewport パラメーターで指定された XRViewport のアスペクト比を計算した後、この関数は glMatrix ライブラリーの mat4.perspective() 関数を使用して、透視行列を計算します。

透視行列は、視野(厳密に言えば、これは垂直方向の視野です)、アスペクト比、およびニアクリッピングプレーンおよびファークリッピングプレーンを 4x4 行列の transform でカプセル化し、呼び出し元に返します。

ニアクリッピングプレーンは、ディスプレイ面に平行なプレーン(平面)からの距離をメートル単位で表したもので、それよりも近くにあるものは何も描画されません。 そのプレーンのカメラ側に横たわる頂点は描画されません。 逆に、ファークリッピングプレーンは、その先には頂点が描画されないプレーンまでのメートル単位の距離です。

拡大縮小係数(scaling factor)またはパーセントを使用してズームするには、1 倍(通常のサイズの 100%)を許可する FOV の最大値にマップし(これにより、ほとんどのコンテンツが表示されます)、最大倍率をサポートしたい FOV の最大値にマップし、その間の対応する値をマップします。

透視行列を計算することによって各フレームのレンダリングパスを開始する場合、フレームの目的のジオメトリーを生成するために適用する必要がある他のすべての変換をその行列にまとめることができます。 例えば、次のようにです。

js
const transform = createPerspectiveMatrix(viewport, 130, 1, 100);
const translateVec = vec3.fromValues(
  -trackDistance,
  -craneDistance,
  pushDistance,
);
mat4.translate(transform, transform, translateVec);

これは、130° の垂直視野を表す透視行列から始まり、次に、トラッククレーン、およびプッシュの動きを含むやり方でカメラを動かす平行移動を適用します。

拡大縮小変換

真の「ズーム」とは異なり、拡大縮小(scaling)では、位置または頂点の xyz 座標値のそれぞれに、その軸の拡大縮小係数を掛けます。 これらは各軸で同一である場合と、必ずしも同一ではない場合もありますが、ズーム効果に最も近い結果は、それぞれに同じ値を使用する必要があります。 これは、シーン内のすべての頂点に(理想的には頂点シェーダーで)適用する必要があります。

2 倍に拡大する場合は、各成分に 2.0 を掛ける必要があります。 同じ量だけ縮小するには、-2.0 を掛けます。 行列の用語では、これは次のように拡大縮小係数された変換行列を使用して実行されます。

js
let scaleTransform = [Sx, 0, 0, 0, 0, Sy, 0, 0, 0, 0, Sz, 0, 0, 0, 0, 1];

この行列は、(Sx, Sy, Sz) で示される係数で拡大または縮小する変換を表します。 Sx は X 軸に沿った拡大縮小係数、Sy は Y 軸に沿った拡大縮小係数、Sz は Z 軸の係数です。 これらの値のいずれかが他の値と異なる場合、結果は一部の次元で他と比較して異なる伸縮になります。

すべての方向に同じ拡大縮小係数を適用する場合は、単純な関数を作成して拡大縮小変換行列を生成できます。

js
function createScalingMatrix(f) {
  return [f, 0, 0, 0, 0, f, 0, 0, 0, 0, f, 0, 0, 0, 0, 1];
}

変換行列を取得したら、変換 scaleTransform をベクトル(または頂点)myVector に適用するだけです。

js
let myVector = [2, 1, -3];
let scaleTransform = [2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 1];
vec4.transformMat4(myVector, myVector, scaleTransform);

または、上記の createScalingMatrix() 関数を使用して、同じ係数ですべての軸に沿った拡大縮小を使用します。

js
let myVector = [2, 1, -3];
vec4.transformMat4(myVector, myVector, createScalingMatrix(2.0));

パン(左または右へのヨー)

パン(pan)またはヨー(yaw)とは、カメラの左から右への回転または右から左への回転であり、それ以外はベースに固定されています。 空間内でのカメラの位置は変化せず、見ている方向のみが変化します。 そして、その方向は水平方向以外に変わりません。 パンは、広大な空間内や広大なオブジェクト上で設定を確立したり、範囲の感覚を提供したりするのに最適です。 あるいは、没入型または VR のシナリオでプレイヤーが頭を回すのをシミュレートするように、左右を見るだけです。

左右にパンするカメラを示す図

これを行うには、Y 軸を中心に回転して、カメラの左右の回転をシミュレートする必要があります。 これまでに使用した glMatrix ライブラリーを使用すると、これは、標準の 4x4 行列を表す mat4 クラスの rotateY() メソッドを使用して実行できます。 行列 viewMatrix で定義された視点 を panAngle ラジアンで回転するには、次のようにします。

js
mat4.rotateY(viewMatrix, viewMatrix, panAngle);

panAngle が正の場合、この変換はカメラを右にパンします。 panAngle の負の値は左にパンします。

ティルト(上または下へのピッチ)

カメラをティルト(tilt)またはピッチ(pitch)すると、カメラの水平部分をまったく変更せずに、カメラの垂直方向の向きを変更しながら、同じ座標で空間に固定したままにします。 単に上下を指す方向を調整するだけです。 ティルトは、森や山などの背の高いオブジェクトやシーンの範囲をキャプチャするのに適していますが、重要なキャラクターやロケールを紹介したり、畏敬の念を起こさせたりする一般的な方法でもあります。 もちろん、プレイヤーが上下を見るサポートを実装するのにも役立ちます。

上下にティルトするカメラを示す図

したがって、カメラをティルトすることは、X 軸を中心にカメラを回転させることで実現できます。 これは、glMatrix の mat4 クラスの rotateX() メソッドなど、行列計算ライブラリーの適切なメソッドを使用して実行できます。

js
mat4.rotateX(viewMatrix, viewMatrix, angle);

angle の正の値はカメラを下に傾け、angle の負の値は上に傾けます。

ドリー(前または後ろへの移動)

ドリー(dolly)ショットは、カメラ全体が前後に移動するショットです。 古典的な映画制作では、これは通常、カメラをトラック(track)上または移動中の車両に取り付けて行われます。 結果のモーションは、特にショットの焦点である人物またはオブジェクトと一緒に移動する場合に、印象的で滑らかな効果を作成できます。

ドリーショットのカメラの動きを示す図

ドリーショットとズームはほぼ同じように見えるはずですが、実際はそうではありません。 ズームがカメラの焦点距離を変更するという事実は、ターゲットがフレーム内で大きくなったり小さくなったりしても、ターゲットとその周囲との間の空間関係が変化しないことを意味します。 一方、ドリーショットは、実際にカメラを動かすことで、物理的な動きの感覚を再現し、シーン内のオブジェクトの関係を、ショットのターゲットに近づいたり遠ざかったりしながらオブジェクトを通り過ぎていく中で、期待通りにシフトさせます。

ドリー操作を実行するには、カメラビューを Z 軸に沿って前後に平行移動します。

js
mat4.translate(viewMatrix, viewMatrix, [0, 0, dollyDistance]);

ここで、[0, 0, dollyDistance] はベクトルで、dollyDistance はカメラをドリーする距離です。 これはカメラの周りの世界全体を動かすことで機能するため、ここで実際に起こるのは、カメラに対して相対的に dollyDistance メートルだけ世界全体が Z 軸に沿って動くということです。 dollyDistance が正の場合、世界はその量だけユーザーに向かって移動し、カメラがシーンに近づきます。 反対に、dollyDistance の負の値は、ユーザーから世界を遠ざけ、カメラがターゲットから後方に動いて見えるようにします。

トラック(左または右への移動)

物理的なカメラを使用したトラック(truck)は、ドリーと同じ種類の索具装置を使用しますが、カメラを前後に移動するのではなく、左から右またはその逆に移動します。 カメラはまったく回転しないため、ショットの焦点が画面からゆっくりと外に出ます。 これは、シーンで感情を確立しようとするときに、集中、時間の経過、または熟考を暗示することができます。 また、カメラがキャラクターと一緒にすべるように動き、シーンを歩いていく、「歩きながら話す」シーンでも頻繁に使用されます。

カメラが左右にトラックする様子を示す図

カメラを左右に移動するには、目的のカメラの動きとは反対の方向に、X 軸に沿ってビュー行列を移動します。

js
mat4.translate(viewMatrix, viewMatrix, [-truckDistance, 0, 0]);

ベクトル [-truckDistance, 0, 0] に注意してください。 これは、トラックの操作がカメラではなく世界を動かすことによって機能するという事実を補います。 全世界を truckDistance によって示される方向とは反対の方向に移動することにより、カメラを予想される方向に移動する効果が得られます。 このように、truckDistance の正の値は、カメラを右に移動し(世界を左に移動することにより)、truckDistance の負の値は、カメラを左に移動します(世界を右に移動することにより)。

ペデスタル(上または下への移動)

ペデスタル(pedestal、台座)ショットは、カメラを床に対して水平に固定したまま、まっすぐ上下に動かしたものです。 背が高くなったり低くなったりする台座(またはポール)の上のカメラで撮影します。 これは、背が高くなったり低くなったり、椅子から立ち上がったり座ったりしている、あるいは単にまっすぐ上下に動いている被写体を追跡するときに役立ちます。

ペデスタルモーションを使用してカメラが上下に移動する様子を示す図

これは、クレーンに取り付けられたカメラを上下に動かすクレーン(crane)ショットに似ています。 ペデスタルまたはクレーンのモーションを実行するには、ビューを Y 軸に沿って、カメラを移動する方向とは反対の方向に移動します。

js
mat4.translate(viewMatrix, viewMatrix, [0, -pedestalDistance, 0]);

pedestalDistance の値を反転することで、実際にはカメラではなく世界を動かしているという事実を補正します。 したがって、pedestalDistance の正の値はカメラを上に移動し、負の値は下に移動します。

カント(左右へのロール)

カント(cant)またはロール(roll)は、ロール軸を中心としたカメラの回転です。 つまり、カメラは空間に固定されたままで、同じ位置に向けられたままですが、カメラの上部が別の方向に向けられるように回転します。

左右にロールするカメラを示す図

これは、手のひらを下にして、手を開いた状態で腕を前に出すことで視覚化できます。 自分の手がカメラで、手の甲がカメラの上部を表しているとします。 「カメラ」が上下逆になるように手を回転させます。 これが、ロール軸の周りに手をカントしたところです。 映画撮影では、カントを使用して、波や乱気流などのさまざまなタイプの非定常モーションをシミュレートできますが、劇的な効果を得るためにも使用できます。

glMatrix を使用して Z 軸を中心にこの回転を実行するには、次のようにします。

js
mat4.rotateZ(viewMatrix, viewMatrix, cantAngle);

動きを組み合わせる

パンしながらズームしたり、同時にティルトしたりカントしたりするなど、複数の動作を同時に実行できます。

複数の軸に沿った平行移動

複数の軸に沿った平行移動は非常に簡単です。 以前は、次のような平行移動を行っていました。

js
mat4.translate(viewMatrix, viewMatrix, [-truckDistance, 0, 0]);
mat4.translate(viewMatrix, viewMatrix, [0, -pedestalDistance, 0]);
mat4.translate(viewMatrix, viewMatrix, [0, 0, dollyDistance]);

ここでの解決策は明白です。 平行移動は、各軸に沿って移動する距離を提供するベクトルとして表現されるため、次のようにそれらを組み合わせることができます。

js
mat4.translate(viewMatrix, viewMatrix, [
  -truckDistance,
  -pedestalDistance,
  dollyDistance,
]);

これにより、行列 viewMatrix の原点が各軸に沿って指定された量だけシフトします。

複数の軸を中心に回転

複数の軸の周りの回転を、回転の共有軸を表すクォータニオンの周りの単一の回転に結合することもできます。 回転を個別に実行するには、オイラー角Euler angles、各軸の周りの別々の角度)を使用して、次のようにピッチ、ヨー、ロールを適用します。

js
mat4.rotateX(viewMatrix, viewMatrix, pitchAngle);
mat4.rotateY(viewMatrix, viewMatrix, yawAngle);
mat4.rotateZ(viewMatrix, viewMatrix, rollAngle);

代わりに、次のようにオイラー角から回転軸を組み合わせたクォータニオンを作成し、乗算を使用して行列を回転させることができます。

js
const axisQuat = quat.create();
const rotateMatrix = mat4.create();
quat.fromEuler(axisQuat, pitchAngle, yawAngle, rollAngle);
mat4.fromQuat(rotateMatrix, axisQuat);
mat4.multiply(viewMatrix, viewMatrix, rotateMatrix);

これにより、ピッチ、ヨー、ロールのオイラー角が 3 つの回転すべてを表すクォータニオンに変換されます。 次に、これは回転変換行列に変換されます。 そして最後に、ビュー行列に回転変換を掛けて、回転を完了します。

WebXR で 3D を表現

WebXR は 3D グラフィックスをさらに一歩進め、ゴーグルやヘッドセットなどの特別なビジュアルハードウェアを使用して 3D グラフィックスを提示し、実際に 3 次元で存在するように見える 3D グラフィックスを作成できるようにします。 これは、現実世界のコンテキスト内にある可能性があります(拡張現実の場合)。

奥行きを知覚するには、シーンに 2 つのパースペクティブが必要です。 2 つのビューを比較することにより、オブジェクトの奥行き、ひいてはビューアーと見ているオブジェクトの間の距離を認識することができます。 これが、わずかに間隔を空けて 2 つの目がある理由です。 一度に片方の目を閉じ、2 つの目を交互に切り替えると、この事実を思い出すことができます。 左目は鼻の左側を見ることができますが、右側は見えませんし、右目は鼻の右側を見ることができますが、左側は見えません。 これは、それぞれの目に見える違いの 1 つにすぎません。

私たちの脳は、視野全体の光レベルと波長に関する 2 つのデータをそれぞれの目から受け取ります。 脳はこのデータを使用して、2 つのパースペクティブ間のわずかな違いを使用して奥行きと距離を把握し、心の中でシーンを構築します。

シーンのレンダリング

XR(仮想現実(VR)と拡張現実(AR)の両方を含む省略表現)のヘッドセットは、2 つの目で得られるビューと同じように、シーンの 2 つのビューを互いに少しずらして描画することで、3D 画像を提示します。 これらのビューは、それぞれの目に別々に送られ、脳が心の中で 3D 画像を構築するために必要とするデータを収集できるようにします。

これを行うために、WebXR はレンダラーに、動画の各フレームに対して 2 回(各目に 1 回)シーンを描画するように要求します。 2 つのビューは、同じフレームバッファーにレンダリングされます。 ビューの 1 つは左側にあり、もう 1 つは右側にあります。 XR デバイスは、画面とレンズを使用して、生成された画像の左半分を左目に提示し、右半分を右目に提示します。

例えば、2560x1440 ピクセルのフレームバッファーを使用するデバイスを考えてみます。 これを 2 つの部分に分割すると(各目に対して半分)、各目のビューが 1280x1440 ピクセルの解像度で描画されます。 概念的には次のようになります。

フレームバッファーが2つの目の視点間でどのように分割されるかを示す図

コードは、XRSession のメソッド requestAnimationFrame() を呼び出して次のアニメーションフレームを提供することを WebXR エンジンに通知し、アニメーションのフレームをレンダリングするコールバック関数を提供します。 ブラウザーがシーンをレンダリングする必要がある場合、ブラウザーはコールバックを呼び出し、入力パラメーターとして現在の時刻と正しいフレームをレンダリングするために必要なデータをカプセル化した XRFrame を提供します。

この情報には、シーン内のビューアーの位置と向きを説明する XRViewerPose と、それぞれがシーンの 1 つのパースペクティブを表す XRView オブジェクトのリストが含まれます。 現在の WebXR 実装では、このリストには 3 つ以上のエントリはありません。 1 つは左目の位置と視野角を示し、もう 1 つは右目に対して同じことを行います。 与えられた XRView がどの目を表すかは、その eye プロパティの値(left または right の文字列)を確認することでわかります(3 番目の可能な値 none は、理論的には別の視点を表すために使用できますが、これは現在の API では完全には利用できません)。

フレームコールバックの例

フレームをレンダリングするためのかなり基本的な(ただし典型的な)コールバックは次のようになります。

js
function myAnimationFrameCallback(time, frame) {
  let adjustedRefSpace = applyPositionOffsets(xrReferenceSpace);
  let pose = frame.getViewerPose(adjustedRefSpace);

  animationFrameRequestID = frame.session.requestAnimationFrame(
    myAnimationFrameCallback,
  );

  if (pose) {
    let glLayer = frame.session.renderState.baseLayer;
    gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
    CheckGLError("Binding the framebuffer");

    gl.clearColor(0, 0, 0, 1.0);
    gl.clearDepth(1.0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    CheckGLError("Clearing the framebuffer");

    const deltaTime = (time - lastFrameTime) * 0.001;
    lastFrameTime = time;

    for (let view of pose.views) {
      let viewport = glLayer.getViewport(view);
      gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
      CheckGLError(`Setting viewport for eye: ${view.eye}`);

      myRenderScene(gl, view, sceneData, deltaTime);
    }
  }
}

コールバックは、カスタム関数 applyPositionOffsets() を呼び出すことから始まります。 この関数は、参照空間を取り、キーボードやマウスのような WebXR によって制御されていないデバイスからのユーザー入力などを考慮するために必要な変更を変換行列に適用します。 この関数によって返された調整済みの XRReferenceSpace は、XRFrame のメソッド getViewerPose() に渡されて、ビューアーの位置と視野角を表す XRViewerPose を取得します。

次に、動画の次のフレームをレンダリングするためのリクエストをキューに入れます。 そのためには requestAnimationFrame() を再度呼び出すだけで、後でそれを行うことを心配する必要はありません。

次に、シーンをレンダリングします。 ポーズの取得に成功した場合、セッションの renderState オブジェクトの baseLayer プロパティから、レンダリングに使用する必要がある XRWebGLLayer を取得します。 これを WebGLRenderingContext のメソッド gl.bindFrameBuffer() を使用して WebGL の gl.FRAMEBUFFER ターゲットにバインドします。

次に、レンダーラーがすべてのピクセルに触れるわけではないため、フレームバッファーをクリアして、既知の状態から開始するようにします。 gl.clearColor() を使用してクリアカラーを不透明な黒に設定し、WebGLRenderingContext のメソッド gl.clearDepth() を呼び出して奥行きバッファーを 1.0 にクリアする値を設定します。 次に、WebGLRenderingContext のメソッド gl.clear() を呼び出します。 これにより、フレームバッファー(マスクパラメーターに gl.COLOR_BUFFER_BIT が含まれるため)と奥行きバッファー(gl.DEPTH_BUFFER_BIT が含まれるため)がクリアされます。

次に、フレームの希望するレンダリング時刻と最後のフレームが描画された時刻を比較して、前のフレームがレンダリングされてからの経過時間を判断します。 この値はマイクロ秒単位なので、0.001 を掛けて(または 1000 で割り算して)秒に変換します。

次に、XRViewerPose 配列の views で見つかったポーズのビューをループします。 ビューごとに、使用する適切なビューポートを XRWebGLLayer に要求し、位置と大きさの情報を gl.viewport() に渡して、一致するように WebGL ビューポートを構成します。 これによりレンダリングが制限され、view.eye で識別される目で見た画像を表すフレームバッファーの部分にのみ描画できるようになります。

制約が確立され、必要なすべての準備が整ったら、カスタム関数 myRenderScene() を呼び出して、実際に計算と WebGL レンダリングを実行してフレームをレンダリングします。 この場合、WebGL コンテキストの glXRViewviewsceneData オブジェクト(頂点シェーダー、フラグメントシェーダー、頂点リスト、テクスチャなどを含む)、および deltaTime を渡しています。 deltaTime は、前のフレームからどれだけの時間が経過したかを示します。 これにより、アニメーションをどこまで進めるかがわかります。

この関数が戻ると、WebXR によって使用されている WebGL フレームバッファーには、シーンの 2 つのコピーがあり、それぞれがフレームの半分を占めています。 1 つは左目用、もう 1 つは右目用です。 これが、XR ソフトウェアとドライバーを介してヘッドセットに到達し、各半分が適切な目に表示されます。

関連情報