From 33058f2b292b3a581333bdfb21b8f671898c5060 Mon Sep 17 00:00:00 2001 From: Peter Bengtsson Date: Tue, 8 Dec 2020 14:40:17 -0500 Subject: initial commit --- .../ja/web/api/webxr_device_api/cameras/index.html | 440 ++++++++++++++ .../web/api/webxr_device_api/geometry/index.html | 201 +++++++ files/ja/web/api/webxr_device_api/index.html | 209 +++++++ .../web/api/webxr_device_api/lighting/index.html | 258 ++++++++ .../movement_and_motion/index.html | 668 +++++++++++++++++++++ .../web/api/webxr_device_api/rendering/index.html | 363 +++++++++++ .../startup_and_shutdown/index.html | 382 ++++++++++++ 7 files changed, 2521 insertions(+) create mode 100644 files/ja/web/api/webxr_device_api/cameras/index.html create mode 100644 files/ja/web/api/webxr_device_api/geometry/index.html create mode 100644 files/ja/web/api/webxr_device_api/index.html create mode 100644 files/ja/web/api/webxr_device_api/lighting/index.html create mode 100644 files/ja/web/api/webxr_device_api/movement_and_motion/index.html create mode 100644 files/ja/web/api/webxr_device_api/rendering/index.html create mode 100644 files/ja/web/api/webxr_device_api/startup_and_shutdown/index.html (limited to 'files/ja/web/api/webxr_device_api') diff --git a/files/ja/web/api/webxr_device_api/cameras/index.html b/files/ja/web/api/webxr_device_api/cameras/index.html new file mode 100644 index 0000000000..b12ab61b27 --- /dev/null +++ b/files/ja/web/api/webxr_device_api/cameras/index.html @@ -0,0 +1,440 @@ +--- +title: '視点とビューアー: WebXR でのカメラのシミュレーション' +slug: Web/API/WebXR_Device_API/Cameras +tags: + - 3D + - API + - AR + - Advanced + - Cameras + - Coordinates + - Guide + - Location + - Motion + - Position + - VR + - Viewers + - Viewpoints + - WebGL + - WebXR + - WebXR API + - WebXR Device API + - XR + - XRPose + - XRView + - XRViewerPose + - rotation + - transform +translation_of: Web/API/WebXR_Device_API/Cameras +--- +

{{DefaultAPISidebar("WebXR Device API")}}

+ +

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

+ +

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

+ +

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

+ + + +

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

+ +

カメラと相対運動

+ +

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

+ +

仮想カメラ

+ +

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

+ +

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

+ +

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

+ +

視点

+ +

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

+ +

一般化されたカメラ

+ +

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

+ + + +

ゲームにおけるカメラ

+ +

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

+ + + +

カメラの配置

+ +

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 メートル向こうに離れているオブジェクトに向いている場合、結果は次のようになります。

+ +

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

+ +

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

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

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

+ +

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

+ +

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

+ +

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

+ +

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

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

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

+ +

複数の変換の合成

+ +

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

+ +

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

+ +

変換の適用

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

[a1a5a9a13a2a6a10a14a3a7a11a15a4a8a12a16]\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 ]

+ +

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

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

+ +

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

+ +
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 パラメーターで指定された {{domxref("XRViewport")}} のアスペクト比を計算した後、この関数は glMatrix ライブラリーの mat4.perspective() 関数を使用して、透視行列を計算します。

+ +

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

+ +

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

+ +

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

+ +

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

+ +
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 を掛けます。 行列の用語では、これは次のように拡大縮小係数された変換行列を使用して実行されます。

+ +
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 軸の係数です。 これらの値のいずれかが他の値と異なる場合、結果は一部の次元で他と比較して異なる伸縮になります。

+ +

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

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

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

+ +
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() 関数を使用して、同じ係数ですべての軸に沿った拡大縮小を使用します。

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

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

+ +

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

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

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

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

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

+ +

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

+ +

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

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

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

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

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

+ +

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

+ +

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

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

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

+ +

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

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

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

+ +

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

+ +

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

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

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

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

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

+ +

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

+ +

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

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

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

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

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

+ +

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

+ +

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

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

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

+ +

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

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

動きを組み合わせる

+ +

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

+ +

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

+ +

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

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

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

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

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

+ +

複数の軸を中心に回転

+ +

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

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

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

+ +
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つの目の視点間でどのように分割されるかを示す図

+ +

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

+ +

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

+ +

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

+ +

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

+ +
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 によって制御されていないデバイスからのユーザー入力などを考慮するために必要な変更を変換行列に適用します。 この関数によって返された調整済みの {{domxref("XRReferenceSpace")}} は、{{domxref("XRFrame")}} のメソッド {{domxref("XRFrame.getViewerPose", "getViewerPose()")}} に渡されて、ビューアーの位置と視野角を表す {{domxref("XRViewerPose")}} を取得します。

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

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

+ +

関連情報

+ + diff --git a/files/ja/web/api/webxr_device_api/geometry/index.html b/files/ja/web/api/webxr_device_api/geometry/index.html new file mode 100644 index 0000000000..d21adc471f --- /dev/null +++ b/files/ja/web/api/webxr_device_api/geometry/index.html @@ -0,0 +1,201 @@ +--- +title: WebXR の幾何学と参照空間 +slug: Web/API/WebXR_Device_API/Geometry +tags: + - API + - Geometry + - Guide + - Math + - Orientation + - Placement + - Position + - Reference Spaces + - Spaces + - WebXR + - WebXR API + - WebXR Device API +translation_of: Web/API/WebXR_Device_API/Geometry +--- +

{{DefaultAPISidebar("WebXR Device API")}}

+ +

基本的なレベルでは、拡張現実または仮想現実のコンテキストでの WebXR プレゼンテーションのシーンのレンダリングは WebGL を使用して実行されるため、2つの API は同じ設計言語の多くを共有します。 ただし、XR ヘッドセットなどの機器を使用して真の 3D でシーンを提示する機能を提供するために、WebXR には理解する必要がある追加の概念があります。

+ +

この記事では、WebXR が WebGL の幾何学を拡張する方法と、オブジェクトの位置と方向(物理的および仮想的)が空間、特に参照空間を使用して相互にどのように記述されるかを紹介します。

+ +

WebXR での空間追跡の記事は、ユーザーの頭の物理的な位置と向きだけでなく、手などの潜在的に身体の他の部分がデジタル世界にマッピングされ、物理オブジェクトと仮想オブジェクトの両方が動き回るときに追跡されるかをカバーするため、ここで提供される情報に基づいて、シーンを適切にレンダリングして合成できるようにします。

+ +

3D 幾何学の基礎

+ +

ここでは、仮想空間内のオブジェクトの位置、方向、動きを計算するために使用される必要な数学演算に加えて、シーンの人間のビューアーを混合物の中に統合する必要性について説明しますが、幾何学やシーンの 3D 表現を管理するための行列とベクトルは、この記事で達成できることの範囲をはるかに超えています。 ウェブの行列計算で個々の演算について詳しく知ることができます。

+ +

単位

+ +

WebXR で使用される 3D 空間の幾何学の詳細について説明する前に、3D の世界に適用される測定単位を理解しておくと役に立ちます。

+ +

長さと距離

+ +

WebGL はすべての距離と長さをメートル(meters)で測定します。 WebXR はこの標準を継承しています。 また、世界は幅 2 メートル、高さ 2 メートル、奥行き 2 メートルの立方体であるという事実も継承しています。 3つの軸のそれぞれの最小値は -1.0、最大値は 1.0 で、立方体の中心は (0, 0, 0) にあります。

+ +

X、Y、Z 座標軸のそれぞれの最小値が -1、最大値が 1 である WebXR 空間を示す図。

+ +

この 2 立方メートルの空間は、コードのために宇宙全体を囲んでいます。 描画するすべてのものは、コード内で明示的に、または変換を使用してすべての頂点の座標を調整することにより、この空間に収まるように座標をマッピングする必要があります。 もちろん、最も効率的な方法は、WebGL と同じ座標系を使用するようにオブジェクトとコードを設計することです。

+ +

WebGL の座標と長さは、レンダリング時にシーンがレンダリングされているビューポートの大きさに自動的に変換されます。

+ +

角度

+ +

角度は{{interwiki("wikipedia", "ラジアン")}}(radians)を使用して指定します。 度をラジアンに変換するには、度の値に π/180 を掛けるだけです。 次のコードスニペットは、2つの単純な関数、degreesToRadians()radiansToDegrees() を示しています。 これらは、角度を測定するために2つの単位間で相互に変換します。

+ +
const RADIANS_PER_DEGREE = Math.PI / 180.0;
+
+let degreesToRadians = (deg) => deg * RADIANS_PER_DEGREE;
+let radiansToDegrees = (rad) => rad / RADIANS_PER_DEGREE;
+
+ +

時刻と持続時間

+ +
セキュリティ上の理由から、DOMHighResTimeStamp は通常、フィンガープリントやタイミングベースの攻撃で使用されないようにするために、クロックに少しの不正確さを導入します。
+ +

WebXR のすべての時刻と持続時間は、{{domxref("DOMHighResTimeStamp")}} 型を使用して測定します。 これは、開始時刻を基準にミリ秒単位で時刻を指定する倍精度浮動小数点値です。 値は浮動小数点数であるため、プラットフォームとハードウェアによっては、ミリ秒レベルよりも正確である場合があります。

+ +

時刻は主に、シーンの前のアニメーションフレームが描画されてからの経過時間を決定するために使用します。 そのため、時刻は通常、ディスプレイのリフレッシュレートに同調し、パフォーマンスの問題によりフレームレートを制限する必要がある場合は、その一部に同調します。 これは、60 FPS のフレームレートを想定して、時刻は通常 1/60 秒の間隔で進むことを意味します。 計算すると、これは各フレームが 16.6667 ミリ秒間隔で理想的にレンダリングされることを意味することがわかります。

+ +

行列を使用した幾何学操作

+ +

以下に、3D シーンのレンダリング時に実行する必要がある3つの主要な変換に行列を使用する方法など、3D 幾何学に関連する行列数学のガイドを提供します。

+ + + +

変換が点に適用されると言うとき、その延長線上で考えると、点の集まりに適用できることに注意してください。 オブジェクトは空間内のいくつかの点で構成されるいくつかのポリゴンで表されるため、オブジェクトを構成するすべての点に同じ変換を適用すると、オブジェクト全体に同じ変換が適用されます。 ベクトルは座標値を使用して記述され、ベクトルの方向と大きさを定義するため、変換をベクトルに適用することもできます。

+ +

空間の原点について

+ +

完全な XR 拡張シーンは、仮想であれ拡張であれ、1から数十の基準系を合成したものです。 位置と方向のデータを WebXR システムと直接交換する必要があるシーン内の各オブジェクトは、シーン内の他のオブジェクトが理解できるように、必要に応じて理解および適応できる方法でその情報を報告できる必要があります。

+ +

拡張現実(AR)では、これは、仮想オブジェクトを現実の世界に挿入する必要があるためです。 仮想オブジェクトを正しく配置するだけでなく、ユーザーの視点が変化しても、仮想オブジェクトが自分でさまよっているように見えないようにします。 仮想現実(VR)では、不快感やもっと悪いことを引き起こす可能性のある分離や切断を防ぐために、ユーザーの動きが仮想ディスプレイに表示される画像と正確に一致する空間感覚を作り出すことがすべてです。

+ +

したがって、それは空間感覚を作り出すことがすべてです。 XR 開発者の観点から見ると、ステージの設計はユーザーにとって最も重要な部分です。 建築家やセットデザイナーのように、あなたは物理的な環境を通して気分や体験を生み出す力を持っています。 その空間をどのように構築するかは、ユーザーがどのように対話して探索できるかに依存し、影響を与えます。

+ +
空間には通常、前景、中距離、背景の要素があります。 適切なバランスは、ユニークな存在感を生み出し、ユーザーを導くことができます。 前景には、直接対話できるオブジェクトとインターフェイスが含まれています。 中距離には、ある程度対話できるオブジェクト、またはより密接に調査して関与するために近づくことができるオブジェクトが含まれます。 一方、背景は通常、少なくともユーザーが中距離または前景の範囲に移動して近づくことができない限り、ほとんどまたは完全に非対話です。
+ +

WebXR では、(シーンが起こる座標空間など)空間(space)の基本的な概念は、{{domxref("XRSpace")}} のインスタンスによって表されます。 この空間は、ユーザーの環境内のオブジェクトと(光源やカメラなど)他のエンティティの相対位置と動きを決定するために使用します。

+ +

3D 空間内の点は、前述のとおり、3つの成分で構成され、それぞれが3つの軸の1つに沿った点の距離を識別します。 世界内の特定の点の位置は、3つの軸が交わる位置である空間の中心からその点が位置する3つの軸のそれぞれに沿ってどのくらい離れているかを示すことによって記述されます。

+ +

これは空間のネイティブの原点(native origin)であり、ユーザーの環境内の特定の物理的な場所に対応しています。 各空間には、XR デバイスの追跡システムによって追跡される独自のネイティブの原点があります。 これは、空間のローカル座標系の原点である実際の原点(effective origin)とは異なる場合があります。

+ +

座標系の方向性を次の図に示します。

+ +

WebGL および WebXR で使用される座標系を示す図。

+ +

原点オフセット(origin offset)と呼ばれる {{domxref("XRRigidTransform")}} を使用して、点を空間自体の実際の座標系から XR デバイスのネイティブ座標系に変換します。 通常、2つの原点は空間が最初に確立されるときに位置合わせされるため、原点オフセットは最初は単純に恒等変換です。 ただし、位置合わせの変化は時間とともに蓄積されるため、補正するために原点オフセットが変化する場合があります。

+ +

原点に対する空間内の点の位置は、上の図に示す3つの空間軸のそれぞれに沿った点の距離を決定することによって決定されます。 空間の原点は、空間の中心にあり、各軸に沿ってゼロの位置にある点 (0, 0, 0) です。 具体的には、初期の開始条件の下で、空間に対するビューアーのデフォルトの向きを使用します。

+ + + +

すべてのオブジェクトは、最も単純なレベルでは、3D 空間内の点とオフセット変換によって定義される一連のポリゴンであり、オブジェクトを移動および回転して空間内の目的の点に配置する方法を示します。 オフセット変換が単位行列の場合、オブジェクトは原点に配置されます。

+ +

ただし、空間追跡やシーン幾何学に役立つためには、XR デバイスの知覚位置を空間の座標系と相関させる必要があります。 そこで参照スペースの出番です。

+ +

参照空間

+ +

さまざまな XR ハードウェアが利用可能であり、多くの開発者からさまざまなフォームファクターで提供されるため、開発者が使用している追跡技術と直接通信する必要があることを期待することは非現実的であり、拡張性がありません。 代わりに、WebXR デバイス API は、開発者がユーザーエクスペリエンスを計画し、それらのニーズを最もよく表す適切な参照空間を要求するように設計されています。 これは、これらのニーズに一致する {{domxref("XRReferenceSpace")}} を{{Glossary("user agent","ユーザーエージェント")}}に要求することによって行われます。

+ +

XRReferenceSpace オブジェクトは、1つの座標系の基準系を別の座標系の基準系に適合させる手段として機能します。 ヘッドセットを装着した後、あなたの周りの仮想世界が、あなたの位置が (0, 0, 0) である座標系を持っていると考えてください。 つまり、あなたはすべての中心にいます。 それは力強さを感じませんか? ヘッドセットに向かって 真正面が -Z 軸で、+Z が後ろにあります。 X は右が正、左が負です。 Y は下に行くと負、上に行くと正になります。 これは、XRシステムの使用開始時の空間におけるヘッドセットの位置を示し、原点 (0, 0, 0) は基本的には鼻梁に配置されています。 この空間が世界空間(world space)です。

+ +

次に、左手にある XR コントローラーについて考えます。 動きとその向きを報告する機能がありますが、ヘッドセットの位置や、より重要なことに、その座標系については何も知りません。 ただし、コントローラーにはその位置をアプリに報告する方法が必要です。 したがって、独自の座標系があります。 これは、入力イベントが発生したときにアプリに提供される参照空間です。 この参照空間は、コントローラーの座標をヘッドセットの座標にマップする方法を内部的に認識しているため、WebXR は座標を相互に変換できます。

+ +

XRReferenceSpace を作成すると、モーションと方向の追跡を一定のレベルでサポートし、空間がユーザーのヘッドセット、オブザーバーのヘッドセット、仮想カメラなどのビューアーを表す場合に、世界空間に対して空間の位置と向き方向を表す行列を取得できる {{domxref("XRViewerPose")}} を取得するためのメカニズムを提供します。

+ +

これはすべてブラウザーの処理責任であり、基盤となる各参照空間の能力に関係なく、一貫した振る舞いを提供します。 個々の XR デバイスがどれほど強力であったりシンプルであっても、WebXR を使用して記述されたコードは、利用可能なハードウェアの制限内で機能します。

+ +

選択する参照空間のタイプに関係なく、その型は {{domxref("XRReferenceSpace")}} または XRReferenceSpace から派生した型です。 {{domxref("XRReferenceSpaceType")}} 列挙で定義されている現在利用可能な参照空間タイプを以下に示します。

+ +

{{page("/ja/docs/Web/API/XRReferenceSpaceType", "Values")}}

+ +

このガイドの残りの部分では、アプリのニーズに適した参照空間を選択する方法について説明します。

+ +

参照空間を使用した空間関係の定義

+ +

環境に対するオブジェクトの位置と方向を参照したり、環境自体を制約したりするために一般的に使用される方法がいくつかあります。 そのために、WebXR は、参照空間(reference spaces)と呼ばれる標準空間のセットを定義します。 それぞれの標準空間は、ローカル空間の基準系の座標系を、それが存在する空間の座標系に関連付けるさまざまな手法をサポートしています。

+ +

ただし、使用している参照空間のタイプに関係なく、同じ関数を使用して座標を空間から親空間に変換できます。

+ +

参照空間タイプの選択

+ +

まず、使用する参照タイプを決定するプロセスの最も簡単なステップを説明しましょう。 使用する可能性が最も高い参照空間は、locallocal-floorunboundedbounded-floor です。

+ +

床レベルの参照空間

+ +

名前に -floor が含まれている参照空間タイプは、対応する非床空間と同じように機能しますが、ビューアーが地面またはその近く(常に上)の安全な場所に自動的に配置されるようにします。 これは、他に床が確立されていない限り、y 座標が常に 0 である平面です。 これらの空間タイプは、部屋の床が平らでない場合や、地上の高さが変化する床の場合、アバターの垂直位置の変更をサポートしないため、実行できません

+ +

主な参照空間タイプ

+ +

viewer 参照空間は、ビューアーの空間内の位置に対応します。 {{domxref("XRFrame")}} メソッド {{domxref("XRFrame.getViewerPose", "getViewerPose()")}} によって返される {{domxref("XRViewerPose")}} によって使用されます。 それ以外の場合、通常は直接使用されません。 唯一の実際の例外は、ウェブコンテンツ内で XR シーンをインラインで実行するときに viewer 参照空間を使用する可能性が高いことです。

+ +

local 参照空間は、通常、シングルルームなどの比較的小さなエリアを記述するために使用されます。 没入型セッションモード(immersive-vr または immersive-ar)を使用している場合は常に使用できるだけでなく、新しいセッションを要求するときに常にオプション機能に含まれます。 したがって、{{domxref("XRSystem.requestSession", "navigator.xr.requestSession()")}} によって作成されたすべてのセッションは、local 参照空間タイプをサポートします。

+ +

複数の部屋が含まれる可能性のある大きな領域を表すには、ビューアーの動きに制約を指定しない unbounded 参照空間タイプを使用できます。 ユーザーが特定の領域に移動できないようにする場合は、自分で処理する必要があります。

+ +

bounded-floor 参照空間タイプには、対応する床に制限されないタイプはありません。 ユーザーの XR ハードウェアが現実世界の空間内を移動することを許可し、それが可能である場合、bounded-floor 参照空間を使用すると、通過が許可され安全な領域の境界を明確に定義できるため便利です。 制限付き参照空間の使用の詳細については、制限付き参照空間の使用の記事を参照してください。

+ +

参照空間を使用してオブジェクトの位置と方向を記述することにより、WebXR は、基盤となる XR ハードウェアに関係なく、これらの記述に使用するデータの形式を標準化できます。 参照空間の構成は、空間のコンテンツを正しくレンダリングするために必要なビュー行列とオブジェクトポーズを提供します。

+ +

参照空間の確立

+ +

最上位の空間({{domxref("XRSession")}} のメソッド {{domxref("XRSession.requestReferenceSpace", "requestReferenceSpace()")}} を呼び出すことによって取得される空間)は、世界空間全体に使用される座標系を表します。 すべては基本的にこの座標系に関連付けられ、ユーザーの機器の位置と仮想世界の間の関係を表します。

+ +

WebXR を使用して、アノテーションによる世界の拡張から 360°動画再生、科学シミュレーション、仮想現実トレーニングシステムなど、想像できるあらゆるものに対応できますが、3D ビデオゲームを典型的な WebXR アプリケーションの例として取り上げましょう。 ゲーム世界の空間に立っているプレイヤーのアバターのモデルを考えてみましょう。 世界空間を基準にしてアバターを配置するには、世界の参照空間で定義された座標系を使用します。

+ +

プレーヤーを新しい位置に移動するには、すべての座標を書き換えるか、移動するたびに手動で変換を適用しますが、参照空間とそれらを相互に作成できるため、より簡単な方法があります。 プレーヤーのアバターの新しい位置と方向を表す {{domxref("XRRigidTransform")}} オブジェクトを作成してから、{{domxref("XRReferenceSpace")}} メソッド {{domxref("XRReferenceSpace.getOffsetReferenceSpace", "getOffsetReferenceSpace()")}} を使用して、新しい位置にアバターの視点を表す新しい参照空間を作成します。 これは、キーボードやマウスなどの非 XR デバイスを使用してプレーヤーのアバターを世界中に移動するためのサポートを実装する場合に特に便利です。

+ +

{{EmbedYouTube("nVSlQkSSQeQ")}}

+ +

新しく作成された参照空間を使用すると、アバターは同じ座標に留まることができますが、新しい位置に配置され(そして世界をその視点から見ることができるように)世界に現れます。 参照空間を使用してプレーヤーの視点を管理する方法の詳細については、の記事を参照してください。

+ +

私たちのゲームアバターの例の場合、アバター(または他の動いているクリーチャーや機械)が世界中を滑走する単純なブロブになることはまれです。 それらは通常、追加の形だけでなく、動く脚、歩くときに揺れる腕、回転したり素早く上下する頭、動き回る武器などの内部の動きも持っています。 標準の WebGL 手法を使用してこれらを実現し、位置決め行列または {{domxref("XRRigidTransform")}} オブジェクトで実際の原点に対して正しい位置にシフトします。

+ +

参照空間に関するデバイスの制限

+ +

一部の XR デバイスは、API が不足している機能を補うために努力しているにもかかわらず、特定の体験をサポートするように作成できないものもあります。 例えば、GearVR デバイスなどの基本的なヘッドセットを、ユーザーが実際の動きを追跡して環境を歩き回るのをサポートする必要があるアプリで機能させる方法はありません。

+ +

プログレッシブエンハンスメントをサポートし、それによってアプリまたはサイトの可用性を広げるには、必要な機能の量が最も少ない参照空間を選択するか、参照空間の取得の失敗を検出し、あまり強力でない代替手段で再試行するフォールバックメカニズムを提供します。

+ +

発生する互換性の問題は、VR のみのヘッドセットで immersive-ar モード(拡張現実セッション)をサポートできないのと同じくらい根本的なものであるか、XR セッションを作成しようとしたときに満たすことができない1つ以上の必須オプションの要求が含まれる場合があります。

+ +

XR セッションは、{{domxref("XRSystem.requestSession", "navigator.xr.requestSession()")}} メソッドを使用して作成します。 オプションのパラメーターの1つは、{{domxref("XRSessionInit")}} ディクショナリに準拠したオブジェクトであり、これを使用して、セッションがサポートする必要がある(または理想的にはサポートすべき)必須またはオプションの機能を指定できます。 現在サポートされているオプションは、標準参照空間を識別する文字列のみです。 これらを使用すると、コードを実行する前に、必要な、または優先する参照空間タイプをサポートできる WebXR セッションにアクセスできることを保証できます。

+ +
+

: 現在、{{domxref("XRSession")}} を作成するときに使用できるオプションは、使用または優先する参照空間のみです。 将来的には、より多くのオプションが利用可能になる可能性があります。

+
+ +

<<<--- 参照空間要件の表をここに挿入 --->>>

+ +

オブジェクトの配置と方向付け

+ +

アプリと WebXR API の間で交換されるすべての空間(位置、方向、および動き)情報は、フレームのレンダリング時に特定の空間に関連して表現されます。 それ以上の位置と方向の管理はユーザーと WebGL の間で行われますが、オブジェクトを 3D 世界に正しく配置するために、参照空間からの原点オフセットを利用します。

+ +

アニメーションフレームをレンダリングするときは、WebXR セッションの {{domxref("XRSession")}} オブジェクトの {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} メソッドを呼び出したときに指定されたコールバック関数が呼び出されます。 コールバックは、そのパラメーターの1つとして、フレームが発生する時刻を示すタイムスタンプを受け取り、対応するアニメーションフレームのすべてのレンダリングを実行する必要があります。

+ +

時刻値を増やしながらコールバックが繰り返し呼び出されると、コールバックは XR ハードウェアを使用して提示される一連のフレームを生成し、それによって 3D シーンがユーザーに表示されます。

+ +

アニメーションプロセスの詳細については、レンダリングと WebXR フレームアニメーションコールバックの記事を参照してください。

+ +

仮想空間でオブジェクトを配置、方向付け、移動する方法の例と、コードレベルでの詳細な説明については、移動、向き、モーションの記事を参照してください。

+ +

関連情報

+ + diff --git a/files/ja/web/api/webxr_device_api/index.html b/files/ja/web/api/webxr_device_api/index.html new file mode 100644 index 0000000000..c0fcc9a175 --- /dev/null +++ b/files/ja/web/api/webxr_device_api/index.html @@ -0,0 +1,209 @@ +--- +title: WebXR Device API +slug: Web/API/WebXR_Device_API +tags: + - API + - AR + - Augmented Reality + - Graphics + - Overview + - VR + - Virtual Reality + - WebXR + - WebXR API + - WebXR Device API + - XR +translation_of: Web/API/WebXR_Device_API +--- +

{{DefaultAPISidebar("WebXR Device API")}}

+ +

WebXR は、仮想世界を提示するため(仮想現実、virtual reality、または VR)に、またはグラフィック画像を現実世界に追加するため(拡張現実、augmented reality、または AR)に設計されたハードウェアへの 3D シーンのレンダリングをサポートするために一緒に使用される標準のグループです。 WebXR デバイス API は WebXR 機能セットのコアを実装し、出力デバイスの選択を管理し、適切なフレームレートで選択したデバイスに 3D シーンをレンダリングし、入力コントローラーを使用して作成されたモーションベクトルを管理します。

+ +

WebXR 互換デバイスには、モーションと方向の追跡が可能な完全没入型 3D ヘッドセット、フレームを通した現実世界のシーンの上にグラフィックをオーバーレイする眼鏡、カメラで世界を捉えることで現実を拡張し、コンピューターで生成された画像でそのシーンを増強する携帯電話が含まれます。

+ +

これらを実現するために、WebXR デバイス API は次の主要な機能を提供します。

+ + + +

最も基本的なレベルでは、それぞれの目の位置を計算し、その位置からシーンをレンダリングすることにより、ユーザーのそれぞれの目の視点からレンダリングするためにシーンに適用するパースペクティブを計算することにより、ユーザーが現在向いている方向を見ているシーンが 3D で提示されます。 これらの2つの画像はそれぞれ単一のフレームバッファーにレンダリングされ、左目のレンダリングされた画像は左側に、右目の視点はバッファーの右半分にレンダリングされます。 シーンに対する両目の視点がレンダリングされると、結果のフレームバッファーが WebXR デバイスに配信され、ヘッドセットまたは他の適切なディスプレイデバイスを通じてユーザーに提示されます。

+ +

WebXR デバイス API の概念と使用方法

+ +
+
WebXR ハードウェア設定の例
+「ヘッドマウントディスプレイ(Head mounted display)」というラベルの付いたゴーグルを、「位置センサー(Position sensor)」というラベルの付いたウェブカメラを備えたモニターに向けた状態で椅子に座っている人のスケッチ
+ +

古い WebVR API は仮想現実(VR)をサポートするためだけに設計されましたが、WebXR はウェブ上の VR と拡張現実(AR)の両方をサポートします。 AR 機能のサポートは、WebXR 拡張現実モジュールによって追加されます。

+ +

典型的な XR デバイスは 3 または 6 自由度を持つことができ、外部位置センサーがある場合とない場合があります。

+ +

機器はまた、ユーザーが空間を移動したり、頭を回転したりすることなどを感知するために使用する加速度計、気圧計、または他のセンサーを含んでいるかもしれません。

+ +

WebXR API へのアクセス

+ +

特定のウィンドウのコンテキスト内で WebXR API にアクセスするには、{{domxref("navigator.xr")}} プロパティを使用します。 これは、WebXR デバイス API 全体を公開する {{domxref("XRSystem")}} オブジェクトを返します。

+ +
+
{{domxref("navigator.xr")}} {{ReadOnlyInline}}
+
{{domxref("Navigator")}} インターフェイスに追加されたこのプロパティは、WebXR API を公開する {{domxref("XRSystem")}} オブジェクトを返します。 このプロパティがない場合、WebXR は使用できません。
+
+ +

WebXR インターフェイス

+ +
+
{{DOMxRef("XRSystem")}}
+
{{domxref("Navigator.xr", "navigator.xr")}} プロパティは、{{domxref("XRSystem")}} のウィンドウのインスタンスを返します。 これは、コードが WebXR API にアクセスするメカニズムです。 XRSystem インターフェイスを使用して、{{domxref("XRSession")}} を作成し、実際の AR または VR セッション、あるいはその両方を表すことができます。
+
{{DOMxRef("XRFrame")}}
+
XR セッションを提示している間、セッションを構成するすべての追跡対象オブジェクトの状態は XRFrame によって表されます。 XRFrame を取得するには、セッションの {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} メソッドを呼び出し、コールバックを提供します。 コールバックは、XRFrame が利用可能になると呼び出されます。 追跡状態を通信するイベントも XRFrame を使用してその情報を含みます。
+
{{DOMxRef("XRRenderState")}}
+
XRSession による画像出力の合成方法を変更する構成可能なプロパティのセットを提供します。
+
{{DOMxRef("XRSession")}}
+
XR ハードウェアと対話するためのインターフェイスを提供します。 {{domxref("XRSystem.requestSession", "navigator.xr.requestSession()")}} から XRSession が取得されると、セッションを使用して、ビューアーの位置と方向を確認し、デバイスに環境情報を照会し、仮想世界や拡張世界をユーザーに提示できます。
+
{{DOMxRef("XRSpace")}}
+
XRSpace は、すべての仮想座標系インターフェイスが基にする不透明な基本クラスです。 WebXR での位置は、特定の {{domxref("XRFrame")}}  が発生する時点で、常に特定の XRSpace に関連して表現されます。 空間の座標系は、指定された物理的な位置を原点とします。
+
{{DOMxRef("XRReferenceSpace")}}
+
{{domxref("XRSpace")}} のサブクラスであり、ユーザーの物理的環境との関係で空間関係を識別するために使用されます。 XRReferenceSpace 座標系は、{{domxref("XRSession")}} の存続期間を通じて変更されないままであると予想されます。 世界には境界がなく、あらゆる方向に無限に広がります。
+
{{DOMxRef("XRBoundedReferenceSpace")}}
+
XRBoundedReferenceSpace は、{{domxref("XRReferenceSpace")}} 座標系を拡張して、境界が設定された有限世界のサポートをさらに組み込みます。 XRReferenceSpace とは異なり、原点は床にある必要があります(つまり、床では y = 0)。 原点の x 成分と z 成分は、通常、部屋または表面の中心またはその付近にあると推定されます。
+
{{DOMxRef("XRView")}}
+
特定のフレームの XR シーンへの単一のビューを表します。 各 XRView は、シーンをユーザーに提示するために使用されるビデオ表示面に対応しています。 例えば、特定の XR デバイスには、左目用と右目用の2つのビューがあります。 各ビューには、立体画像効果を作成できるようにするために、カメラに対するビューの位置をシフトするために使用されるオフセットがあります。
+
{{DOMxRef("XRViewport")}}
+
ビューポートについて説明します。 ビューポートは、グラフィック面の長方形の部分です。 WebXR では、ビューポートは、特定の {{domxref("XRView")}} に対応する描画面の領域を表します。 例えば、2つの目の視点の1つをシーンにレンダリングするために使用される WebGL フレームバッファーの部分などです。
+
{{DOMxRef("XRRigidTransform")}}
+
{{domxref("XRSpace")}} で記述されている、仮想空間の座標系での位置と方向を使用して定義された変換。
+
{{DOMxRef("XRPose")}}
+
{{domxref("XRSpace")}} を基準にした空間内の位置と方向を記述します。
+
{{DOMxRef("XRViewerPose")}}
+
{{domxref("XRPose")}} に基づいて、XRViewerPose は、XR デバイスによって示される WebXR シーンのビューアーの状態を指定します。 {{domxref("XRView")}} オブジェクトの配列が含まれ、それぞれがシーンの1つの視点を表します。 例えば、人間の視覚で知覚される立体視ビューを作成するには、2つのビューが必要です。 1つは左目用、もう1つは右目用です。 1つのビューはビューアーの位置から少し左にオフセットされ、もう1つのビューは同じ距離だけ右にオフセットされます。 ビューリストは、マルチユーザー環境で、シーンの各観客の視点を表すためにも使用できます。
+
{{DOMxRef("XRInputSource")}}
+
ユーザーがビューアーと同じ仮想空間内でターゲットアクションを実行するために使用できる任意の入力デバイスを表します。 入力ソースには、ハンドコントローラー、光学追跡システム、および XR デバイスに明示的に関連付けられている他のデバイスなどのデバイスが含まれます。 キーボード、マウス、ゲームパッドなどの他の入力デバイスは、XRInputSource インスタンスとして提示されません。
+
{{DOMxRef("XRWebGLLayer")}}
+
シーンのビューがレンダリングされる WebGL フレームバッファーとして機能するレイヤー。 WebGL を使用してシーンをレンダリングすると、グラフィックアクセラレーションによりパフォーマンスが大幅に向上します。
+
+ +

イベントインターフェイス

+ +

以下のインターフェイスは、WebXR API によって使用されるイベントを表すために使用されます。

+ +
+
{{domxref("XRInputSourceEvent")}}
+
{{domxref("XRInputSource")}} の状態が変化すると送信されます。 これは、例えば、デバイスの位置や方向が変わったとき、またはボタンが押されたり離されたときに発生する可能性があります。
+
{{domxref("XRInputSourcesChangeEvent")}}
+
{{domxref("XRSession")}} の使用可能な入力ソースのセットが変更されたことを示すために送信されます。
+
{{domxref("XRReferenceSpaceEvent")}}
+
{{domxref("XRReferenceSpace")}} の状態が変化すると送信されます。
+
{{domxref("XRSessionEvent")}}
+
{{domxref("XRSession")}} の状態が変化したことを示すために送信されます。 例えば、位置や向きがです。
+
+ +

WebGL API の拡張機能

+ +

WebGL API は、WebXR 仕様によって拡張され、WebGL コンテキストを拡張して、WebXR デバイスで表示するビューのレンダリングに使用できるようにします。

+ +
+
{{domxref("WebGLRenderingContextBase.makeXRCompatibile","WebGLRenderingContextBase.makeXRCompatibile()")}}
+
WebGL コンテキストを WebXR と互換性があるように構成します。 {{domxref("WebGLContextAttributes.xrCompatible", "xrCompatible")}} プロパティを true に設定してコンテキストを最初に作成しなかった場合は、WebXR レンダリングに WebGL コンテキストを使用する前に makeXRCompatible() を呼び出す必要があります。 コンテキストが準備されると解決する {{jsxref("Promise")}} を返します。 WebXR で使用するようにコンテキストを構成できない場合は拒否されます。
+
+ +

ガイドとチュートリアル

+ +

次のガイドとチュートリアルは、WebXR とその基礎となる 3D および VR/AR のグラフィックスの概念を理解する方法を学ぶための優れたリソースです。

+ +

基盤と基礎

+ +
+
WebXR の基本
+
WebXR を使用してコンテンツを作成する方法の詳細に入る前に、このテクノロジーの概要を読んでおくと役立つかもしれません。 これには、使い慣れていない用語や、新しい方法で使用されている可能性のある用語の紹介が含まれています。
+
ウェブの行列計算
+
CSS transform と WebGL の両方の目的、および WebXR コンテキストでのオブジェクトの配置と方向の処理を含む、ウェブでの行列の使用方法をカバーするガイド。
+
WebXR アプリケーションのライフサイクル
+
起動から停止までの WebXR アプリケーションの全体的なライフサイクルの概要。 この記事は、コードに詳しく触れずに WebXR エクスペリエンスを作成するための基本的な方法の紹介です。 これは、次のステップに備えるための良い方法です。
+
+ +

複合現実エクスペリエンスの作成

+ +
+
WebXR セッションの起動と停止
+
ヘッドセットやゴーグルなどの XR デバイスを使用して実際にシーンを提示する前に、3D 効果をユーザーに提示できるように、XR デバイスの各ディスプレイに表示するシーンを描画するレンダリングレイヤーにバインドされた WebXR セッションを作成する必要があります。 このガイドでは、WebXR セッションを作成および停止する方法について説明します。
+
WebXR の幾何学と参照空間
+
このガイドでは、3D 幾何学の必要な概念について簡単に説明し、その幾何学が WebXR でどのように表現されるかの基本を詳しく説明します。 参照空間を使用してオブジェクト(およびビューアー)を配置する方法と、使用可能な参照空間のタイプの違い、およびそれらの使用例について説明します。
+
WebXR での空間追跡
+
このガイドでは、オブジェクト(ユーザーの体やそのパーツを含む)が空間に配置される方法と、互いの動きと向きが時間とともに監視および管理される方法について説明します。 この記事では、空間(space)、ポーズ(pose、姿勢)、ビューアー(viewer)、ビュー(view)の関係について説明します。
+
レンダリングと WebXR フレームアニメーションコールバック
+
このガイドでは、フレームのレンダリングをスケジュールする方法から始めて、ビュー内のオブジェクトの配置を決定する方法と、シーンの2つの目のビューのそれぞれに使用される WebGL バッファーにオブジェクトをレンダリングする方法について説明します。
+
視点とビューアー: WebXR でのカメラのシミュレーション
+
WebGL(および WebXR)には、実際にはカメラの概念がありません。 これは、3D グラフィックで視点を表すために使用される従来の概念です。 この記事では、カメラをシミュレートする方法と、ビューアーが実際には動かない世界をビューアーが動くように錯覚させる方法を説明します。
+
WebXR 設定の照明
+
WebXR レンダリングは WebGL に基づいているため、3D アプリケーションで使用されているのと同じ照明技術が WebXR シーンに適用されます。 ただし、照明のコードを記述するときに考慮する必要がある拡張現実および仮想現実の設定の作成に固有の問題があります。 この記事では、これらの問題について説明します。
+
制限付き参照空間の使用
+
この記事では、bounded-floor 参照空間を使用して、XR ハードウェアによって追跡された領域を離れたり、物理的な障害物との衝突なしに、ビューアーが安全に動き回れる境界を定義する方法を検討します。 それをサポートするデバイスでは、bounded-floor はあなたのレパートリーの便利なツールになります。
+
+ +

インタラクティブにする

+ +
+
移動、向き、モーション: WebXR の例
+
この例とチュートリアルでは、WebXR ドキュメント全体で学習した情報を使用して、ユーザーが VR ヘッドセットとキーボードとマウスの両方を使用して動き回れる回転立方体を含むシーンを作成します。
+
入力と入力ソース
+
入力ソースと、WebXR セッションの制御に使用されている入力デバイスを効率的に管理する方法、およびそれらのデバイスからユーザー入力を受信して​​処理する方法のガイド。
+
ターゲティングとヒット検出
+
入力ソースのターゲッティングレイモードとターゲッティングレイ空間を使用してターゲッティングレイを表示し、ターゲットとする面またはオブジェクトを識別し、関連タスクを実行する方法。
+
WebXR 入力プロファイルの使用
+
WebXR 入力プロファイル登録所(WebXR Input Profiles Registry)によって提供される {{Glossary("JSON")}} データを解釈するためのガイド。 これは、ユーザーの使用可能な入力デバイスで使用可能なオプションとコントロールを決定するために使用できます。
+
WebXR アプリケーションでの高度なコントローラーとゲームパッドのサポート
+
WebXR は {{domxref("Gamepad")}} オブジェクトを使用して、複雑な入力デバイス(複数のボタンや軸を持つハンドコントローラーなど)やゲ​​ームパッドのようなデバイスで利用可能なコントロールを記述します。 このガイドでは、これらのデバイスのコントロールを使用する方法を学びます。
+
+ +

パフォーマンスとセキュリティ

+ +
+
WebXR パフォーマンスガイド
+
WebXR アプリケーションのパフォーマンスを最適化するのに役立つ推奨事項とヒント。
+
WebXR の権限とセキュリティ
+
WebXR デバイス API には、機能ポリシーの確立から、ユーザーが複合現実プレゼンテーションをアクティブ化する前に使用する意図を確認することまで、いくつかのセキュリティ領域があります。
+
+ +

他のメディアを含む

+ +
+
3D 環境でのポジショナルオーディオ
+
画面にレンダリングされた 3D シーン、またはヘッドセットを使用して体験した複合現実感のいずれかである 3D 環境では、ソースの方向から来ているように聞こえるようにオーディオを実行することが重要です。 このガイドでは、これを行う方法について説明します。
+
3D 環境でのビデオの再生
+
このガイドでは、ビデオを 3D シーンに再生する方法について説明します。 この手法は、平らなコンピューター画面に表示される標準の WebGL アプリケーション、または WebXR で生成された仮想現実環境や拡張現実環境の両方で使用できます。
+
+ +

仕様

+ + + + + + + + + + + + + + +
仕様状態コメント
{{SpecName("WebXR")}}{{Spec2("WebXR")}}初期定義
+ +

ブラウザーの互換性

+ +

{{Compat("api.Navigator.xr")}}

+ +

関連情報

+ + diff --git a/files/ja/web/api/webxr_device_api/lighting/index.html b/files/ja/web/api/webxr_device_api/lighting/index.html new file mode 100644 index 0000000000..afef0bfb14 --- /dev/null +++ b/files/ja/web/api/webxr_device_api/lighting/index.html @@ -0,0 +1,258 @@ +--- +title: WebXR 設定の照明 +slug: Web/API/WebXR_Device_API/Lighting +tags: + - 3D + - API + - Graphics + - Light + - Shading + - Shadows + - WebGL + - WebXR + - WebXR Device API + - lighting + - rendering +translation_of: Web/API/WebXR_Device_API/Lighting +--- +

{{DefaultAPISidebar("WebXR Device API")}}

+ +

WebXR Device API は、シーンのすべてのレンダリング、テクスチャリング、およびライティング(照明)を実行するために他のテクノロジー(つまり、WebGL とそれに基づくフレームワーク)に依存しているため、WebXR 設定またはシーンには、他の WebGL で生成されたディスプレイと同じ一般的な照明の概念が適用されます。

+ +

ただし、特に拡張現実(AR)アプリケーションの場合、照明のコードを作成する際に留意すべき問題と詳細があります。 このガイドでは、これらのトピックについて説明します。 そして、この記事では、照明が一般的にどのように機能するかについて簡単に説明しますが、照明のチュートリアルや、適切に照明された 3D シーンを作成する方法のガイドではありません。

+ +

フラッシュバック: 3D での照明のシミュレーション

+ +

この記事は 3D シーンを照明するための包括的なガイドではありませんが、照明が一般的にどのように機能するかについて簡単に思い出させるのに役立ちます。 基本的に、仮想シーンでの照明のシミュレーションには、シーン内の各オブジェクトと相互作用して反射した後、各光源からの光が目で受け取る量を計算することが含まれます。

+ +

光の反射

+ +
+
+
+
反射角が入射角にどのように対応するかを示す図。
+ +
反射角が入射角にどのように対応するかを示す図。
+
+
+
+ +

私たちが見るすべてのオブジェクトは、オブジェクトが光を放出または反射する(あるいはその両方)ために見えます。 入ってくる光線(入射光線、incident ray)は、入射角(angle of incidence)と呼ばれる角度で到達します。 入射角 Θi は、入射光線と表面の{{interwiki("wikipedia", "法線ベクトル")}}の間の角度です。

+ +

粗い表面の場合、光はあらゆる方向に均等に反射されます。 ただし、光沢のある鏡のような表面は、法線ベクトルの反対側にあることを除いて、反射角(angle of reflection) Θr が入射角に等しい方向にほとんどの光を反射します。 反射光線(reflected ray)は、法線からその角度で出発します。 これが{{interwiki("wikipedia", "鏡面反射", "反射の法則")}}(law of reflection)です。 これは、シーンのシェーディングに関係する多くの基盤であり、さまざまな種類の光源の振る舞いの観点で役に立ちます。

+ +

もちろん、反射光の光線の色は、光が表面と相互作用するために強度や色相が変化する可能性がありますが、角度は常に同じです。

+ +

光源の構成要素

+ +

光源には3つの主要な構成要素があります。 各構成要素は本質的に一種の光です。

+ +

ビューアーの画面またはヘッドセットに表示されるオブジェクトとそのピクセルの色と明るさに影響を与える可能性のある3種類の光があります。

+ +
+
+
+
環境照明のみの球。 球の奥行きを示すための陰影がまったくないことに注意してください。
+ +
環境照明のみを持つ球。 球の奥行きを示すための陰影がまったくないことに注意してください。
+
+
+
+ +

環境光

+ +

環境光(ambient light)は、定義された光源からではなく、シーン全体に存在する光です。 この光は、シーン内のあらゆる表面にあらゆる方向から同じ強度で到達し、あらゆる方向に均等に反射します。 その結果、環境光によって適用される効果は、シーン全体で普遍的に等しくなります。

+ +

環境光の効果は、光源の強度にピクセルの位置での表面の反射率を単純に乗算することによって計算されます。 シーン内のあらゆるピクセルの色と強度は、シーン内のどこにあるか、または向きに関係なく、まったく同じように影響を受けます。 環境光は通常、シーン全体に影響を与えますが、影の部分が暗くなりすぎるのを防ぐために存在します。 ただし、シーン内の環境光の量は非常に少なくする必要があります。

+ +

光の跳ね返りと散乱はリアルタイムで計算するのにコストがかかる可能性があるため、特に複数の光源が関係している場合は、光散乱の真の効果を実際に計算するのではなく、環境照明を使用してシーン内の他のすべての光源によって引き起こされる散乱光をシミュレートするのが一般的です。 ただし、これを行う場合は、環境光をシーンの照明の実際の効果に一致させるように注意する必要があります。

+ +

環境光を使用して、シーンに色合いを適用することもできます。 例えば、プレーヤーが黄色がかった特別な眼鏡を持っているゲームでは、黄色の環境光を追加できます。

+ +
+
+
+
土星で5番目に大きい月テティスは、左下から日光を浴びています。
+ +
土星で5番目に大きい月テティスは主に太陽に照らされており、土星からの光が反射しています。 これは拡散照明です。
+
+
+
+ +

拡散光

+ +

拡散光(diffuse light)は、表面から均一かつ指向性を持って放射または反射する光です。 これは私たちが通常目にする光の大部分です。 拡散光は特定の位置または方向から来て、影を落とします。 その指向性により、拡散光に面しているオブジェクトの面は、他の面よりも明るくなります。

+ +

拡散光の強度は{{interwiki("wikipedia", "入射角")}}(angle of incidence、光が表面に到達する方向を表すベクトルと表面の法線ベクトルまたは表面に垂直なベクトルとの間の角度)に依存するため、オブジェクトによって反射する光の強度または明るさは、光源に対する表面の向きによって異なります。

+ +
+
+
+
NASA のカッシーニ宇宙船が撮影した写真。 土星の月タイタンの表面にある液体メタンの湖からの光の鏡面反射を示しています。
+ +
NASA のカッシーニ宇宙船が撮影した写真。 土星の月タイタンの表面にある液体メタンの湖からの光の鏡面反射を示しています。
+
+
+
+ +

鏡面反射光

+ +

鏡面反射光(specular light)は、宝石、目、光沢のあるカップや皿などの反射オブジェクトのハイライトを構成する光です。 鏡面反射光は、光源が表面に最も直接当たる点の表面に明るいスポットまたは正方形として現れる傾向があります。

+ +

すべての光源は、環境光、拡散光、および/または鏡面反射光の組み合わせによって表されます。 WebGL シェーダープログラムは、各光源の色、指向性、明るさ、およびその他の要素を取得し、各ピクセルの最終的な色を計算します。

+ +

光源の種類

+ +

光源には4つの基本的な種類があります。 それぞれに、描画されるオブジェクトからの距離と光波の指向性によって光源が特定の特性を帯びる仮想光源が含まれます。 ほとんどの場合、これらの光源タイプの1つまたは複数を使用して、現実世界の光源をシミュレートできます。

+ +

環境光源

+ +

環境光源(ambient light source)は、シーン内の環境光のレベルと色を表す光源です。 シーンにはこれらが複数存在する場合がありますが、それぞれが常にすべてのピクセルに均等に影響するため、これらを1つにまとめることで、パフォーマンスをわずかに向上させることができます。

+ +

環境光源は通常、シーン内のどのオブジェクトにも対応しておらず、現実世界の類似物もありません。

+ +
+
+
+
地球と月は両方とも太陽の指向性照明によって半分照らされています。
+ +
ガリレオ宇宙船が約630万キロメートル離れた場所から撮影した写真で、地球と月の両方が太陽に半分照らされています。
+
+
+
+ +

指向性光源

+ +

指向性光源(directional light source、平行光源)は、特定の方向から来る光源ですが、特定の光源からは来ないため、放出される光線は互いに平行です。 また、光の強さは距離によって変化しません。 これは、指向性光によって投影される影が非常にシャープであり、光と影の間で本質的に瞬時に遷移することを意味します。

+ +

指向性光の最も一般的な例は太陽です。 太陽は実際には単一の(大きな)オブジェクトですが、非常に遠くにあるため、太陽からの光線は基本的に平行です。 太陽光は実際には距離とともに強度が低下しますが、変化率は非常に低く、広大な距離でのみ認識されるため、太陽光の強度変化率は通常、3D シーンのレンダリングには関係ありません。

+ +

点光源

+ +

点光源(point light source)は、特定の場所に配置され、すべての方向に均等に外側に放射する光源です。 電球、ろうそくなどは点光源の例です。 オブジェクトが点光源に近いほど、そのオブジェクトに照射される光は明るくなります。 ポイントライト(point light)の明るさが低下するレートは減衰(attenuation)と呼ばれ、WebGL やその他の照明システムの光源の構成可能な機能です。

+ +

反射の法則と光線の明るさが距離とともに減少するという事実との間で、点光源から放出されて反射する光は、光源に最も近い点で最も明るくなり、光源から離れるほど暗くなる傾向があります。 表面が平らであっても、光源に最も近い点が中心で、法線から離れるように角度が変化するにつれて光線はますます長くなります。

+ +
+
+
+
夜に漆喰の壁を照らすスポットライトの写真。
+ +
夜に漆喰の壁を照らすスポットライトの写真。
+
+
+
+ +

スポット光源

+ +

スポット光源(spot light source)またはスポットライト(spotlight)は、特定の位置に配置され、その方向ベクトルの方向に光の円錐を放出する光源です。 テーパリングレートパラメーターは、光の円錐の端で光の明るさがどれだけ速く落ちるかを定義し、ポイントライトと同様に、減衰パラメーターは、光が距離とともにどのようにフェードするかを制御します。

+ +

光の円錐の端では、光は表面にまったく影響を与えなくなります。

+ +

照明の計算コスト

+ +

シーンを目に見えるようにするには、何らかの照明が含まれている必要があるため、すべてまたはほぼすべてのシーンに少なくとも1つの光源があり、非常に多くの光源を持っているかもしれません。 各光源は、表示される各ピクセルの最終的な色と明るさを決定するために必要な計算量を大幅に増やします。 これらの光源の種類のそれぞれに対してシェーディングを実行することは、その前のものよりも計算量が多くなります。 したがって、環境光を適用するのが最も費用がかからず、次に指向性光源、ポイントライト、最後にスポットライトが続きます。

+ +

さらに、照明がより正確になるように設計されているほど、計算コストが高くなります。 陰影詳細の増加、体積光(つまり、太陽光のビームや空のスポットライトのビームなど、空中で見ることができる照明)、およびその他の照明効果は、シーンにリアリズムと美しさを追加できますが、シーンが GPU を圧倒しないように注意が必要です。

+ +

照らされたピクセルの色の計算

+ +

一部のグラフィックライブラリーには光源オブジェクトのサポートが含まれており、照明効果を自動的に計算して適用しますが、WebGL には含まれていません。 幸い、照明を独自の頂点シェーダーとフラグメントシェーダーで適用することはそれほど難しくありません。

+ +

シーン内のポリゴンごとに、頂点シェーダー(vertex shader)のプログラムが頂点の色を決定し、フラグメントシェーダー(fragment shader)は、割り当てられたテクスチャー、任意の色合いまたは効果、およびその他の視覚データから適切なテクセルを組み合わせて、ポリゴン内の各ピクセルを生成します。 ピクセルがフレームバッファーに格納される前に、シーンの照明が考慮され、ピクセルに適切に適用されるのはこのときです。

+ +

最終的なレンダリングされたシーンの各ピクセルの色は、次のような要素を考慮した複雑な計算を使用して計算されます。

+ + + +

WebGL で照明を実行する方法の詳細については、WebGL でのライティングの記事を参照してください。

+ +

複合現実コンテンツの照明の問題

+ +

シーンの照明中に対処する必要のある通常の問題に加えて、VR または AR のユースケースでは、シェーダーを作成する際の懸念事項が追加されます。 このセクションでは、シーンを構築、レンダリング、および照明するときに考慮すべきいくつかの基本的な複合現実の照明のガイドラインを提供します。 これらのいくつかは他の 3D 設定でも役立ちますが、ほとんどは仮想現実に固有であり、場合によっては拡張現実に固有です。

+ +

シーンは、人物またはそのアバターが存在する可能性のある設定を表すことを目的としているため、光源の配置と提示に関して、ある程度の一貫性とリアリズムを実現するように努める必要があります。 明らかに、このガイドラインには例外があります。 例えば、シーンが異世界または異星人の設定を表している場合や、不安な視覚効果を作成することが目標である場合などです。

+ +

光源配置のリアリズム

+ +

可能であれば、仮想光源を実際に存在するオブジェクトに対応させるようにしてください。 頭上の照明が必要な仮想の部屋がある場合は、光源の場所に天井ランプのモデルを用意します。 例外として、設定にベースライン量の照明を追加するだけの環境照明や、指向性光である太陽(つまり、すべての光線が平行で、空のどこかから来てシーン内のどこかで終わる光源)などがあります。

+ +

作成しようとしている設定とムードに合わせて、現実的な場所に光源を配置してください。 自然に照らされた現実世界の設定のように感じることを目的としたシーンには、スタジオ照明がありません。 太陽光や、シーン内のオブジェクトや水から反射した光などがありますが、シーン内のオブジェクトや人の顔に向けられたランプはありません。

+ +

光とプレイヤーの相互作用のリアリズム

+ +

光源がシーン内にある場合は、ビューアーのアバターが光源と物理的に交差しないようにする必要があります。 結果は…奇妙なものになるかもしれません。

+ +

ビューアーのアバターが物理的な形をとることを意図している場合、ビューアーがそれを決して見ることができない場合でも、光がアバターと正しく相互作用するように、モデルを持っている必要があります。 最低限でも、これはアバターが適切な影を落とす必要があることを意味しますが、アバターが見えるかどうかや、モデルのマテリアル、テクスチャー、その他の属性(特に反射率を含む)などの要因によっては、アバターも光を反射する必要があり、反射した光の着色に影響を与える可能性があります。

+ +

拡張現実におけるリアリズム

+ +

拡張現実は、仮想オブジェクトが独自の光源を持つ物理的な世界内に存在する必要があるため、オブジェクトを照明するのがさらに複雑になります。 そのため、照明を現実世界の光源にできるだけ一致させるようにしてください。 これは、{{anch("Lighting estimation", "照明推定")}}と呼ばれる手法を使用して行われます。

+ +

逆に、それ自体が光源である仮想オブジェクトは、現実世界の設定にその照明を当てることができるコードを作成する準備ができていない限り、使用しないようにする必要があります。 現実世界のオブジェクトに光を当てるのは、基本的に影を付けるのとは逆です。 それは可能ですが、それほど広く実装されていません。

+ +

照明推定

+ +

照明推定(lighting estimation)は、拡張現実プラットフォームで使用される手法であり、シーン内の仮想オブジェクトの照明を、ビューアーを取り巻く現実世界の照明に一致させようとします。 これには、さまざまなセンサー(加速度計とコンパスがある場合はそれを含む)、カメラ、および場合によっては他のセンサーから取得される可能性のあるデータの収集が含まれます。 その他のデータは Geolocation API を使用して収集され、このすべてのデータはアルゴリズムと機械学習エンジンに送られ、推定された照明情報が生成されます。

+ +

現在、WebXR は照明推定のサポートを提供していません。 ただし、仕様は現在 W3C の支援の下で起草されています。 仕様の GitHub リポジトリに含まれている説明文書で、提案された API のすべてと、照明推定の概念についてかなりの量を学ぶことができます。

+ +

本質的に、照明推定は、光源と、シーン内のオブジェクトの形状と方向、およびそれらを構成している材質に関する情報を収集し、現実世界の照明とほぼ一致する仮想光源オブジェクトの作成に使用できるデータを返します。

+ +

特に提案された API のコンテキストで、照明推定がどのように機能するかの詳細は、現時点では範囲外です。 API が安定したら、このドキュメントを詳細で更新します。

+ +

セキュリティとプライバシーの懸念

+ +

現実世界のデータを使用して仮想オブジェクトに照明を生成および適用するために、このすべてのデータを収集することに関連する潜在的なセキュリティ問題がいくつかあります。

+ +

もちろん、多くの AR アプリケーションは、ユーザーがどこにいるかをかなり明確にします。 ユーザーがルーブルのツアーというアプリを実行している場合、ユーザーがフランスのパリにあるルーブル美術館(Musée du Louvre)にいる可能性が非常に高くなります。 ただし、ブラウザーは、ユーザーの同意なしにユーザーの居場所を物理的に特定することを困難にするために、いくつかの手順を実行する必要があります。

+ +

Ambient Light Sensor API

+ +

Ambient Light Sensor API を使用して光データを収集すると、さまざまな潜在的なプライバシー問題が発生します。

+ + + +

ブラウザーがこれらの問題を軽減する方法

+ +

これらのリスクを軽減するために、WebXR Lighting Estimation API 仕様では、ブラウザーは、真の値から多少ずれた照明情報を報告する必要があります。 これを行うには多くの方法があります。

+ +

球面調和関数の精度

+ +

ブラウザーは、{{interwiki("wikipedia", "球面調和関数")}}(spherical harmonics)の精度を下げることにより、フィンガープリント(fingerprinting)のリスクを軽減できます。 仮想現実または拡張現実のアプリケーションの場合のように、リアルタイムレンダリングを実行する場合、球面調和関数照明(spherical harmonic lighting)を使用して、非常にリアルな影とシェーディングを生成するプロセスを簡素化および加速します。 これらの機能の精度を変更することにより、ブラウザーはデータの一貫性を低下させ、重要なことに、同じ設定であっても2台のコンピューターによって生成されるデータを異なるものにします。

+ +

方向の照明からの切り離し

+ +

測位(geolocation)を使用して方向と潜在的な位置情報を決定する AR アプリケーションでは、その情報が照明の状態に直接相関しないようにすることは、ブラウザーがフィンガープリント攻撃からユーザーを保護できるもう1つの方法です。 ユーザーの位置に近い(または近いと主張する)すべてのデバイスでコンパスの方向と光の指向性が同じではないことを確認するだけで、周囲の照明の状態に基づいてユーザーを見つける能力が取り除かれます。

+ +

ブラウザーが非常に明るい指向性光源に関する詳細を提供する場合、その光源はおそらく太陽を表しています。 この明るい光源の指向性を時刻と組み合わせて使用​​すると、Geolocation API を使用せずにユーザーの地理的位置を特定できます。 AR シーンの座標がコンパス座標と一致しないようにし、太陽の光の角度の精度を下げることにより、この手法を使用して位置を正確に推定できなくなります。

+ +

時間的空間的フィルタリング

+ +

建物の自動照明システムを使用して、既知のパターンで光をすばやくオン/オフする攻撃について考えてみます。 適切な予防策がなければ、照明推定データを使用してこのパターンを検出し、ユーザーが特定の場所にいると判断する可能性があります。 これはリモートで実行することも、同じ部屋にいるが他の人も同じ部屋にいるかどうかを判断したい攻撃者が実行することもできます。

+ +

照明推定を使用してユーザーに関する情報を許可なく取得できる別のシナリオとしては、光センサーがユーザーのディスプレイに十分近く、ディスプレイの内容によって引き起こされる照明の変化を検出する場合、アルゴリズムを使用して、ユーザーが特定の動画を視聴しているかどうかを判断したり、ユーザーが見ている動画の数を特定する可能性さえあります。

+ +

Lighting Estimation API 仕様では、すべての{{Glossary("user agent", "ユーザーエージェント")}}が時間的空間的フィルタリング(temporal and spatial filtering)を実行して、ユーザーの位置を特定したり{{interwiki("wikipedia", "サイドチャネル攻撃")}}(side-channel attacks)を実行したりする目的での有用性を低下させる方法でデータをぼやけさせることを義務付けています。

+ +

関連情報

+ + diff --git a/files/ja/web/api/webxr_device_api/movement_and_motion/index.html b/files/ja/web/api/webxr_device_api/movement_and_motion/index.html new file mode 100644 index 0000000000..81cdd2a04b --- /dev/null +++ b/files/ja/web/api/webxr_device_api/movement_and_motion/index.html @@ -0,0 +1,668 @@ +--- +title: '移動、向き、モーション: WebXR の例' +slug: Web/API/WebXR_Device_API/Movement_and_motion +tags: + - 3D + - API + - AR + - Example + - Guide + - Reality + - Tutorial + - VR + - Virtual + - WebXR + - WebXR API + - WebXR Device API + - XR + - augmented + - rendering +translation_of: Web/API/WebXR_Device_API/Movement_and_motion +--- +

{{DefaultAPISidebar("WebXR Device API")}}

+ +

この記事では、WebXR チュートリアルシリーズの以前の記事で紹介した情報を利用して、ユーザーが VR ヘッドセット、キーボード、マウスを使用して自由に移動できる回転立方体をアニメーション化する例を作成します。 これは、3D グラフィックスと VR の幾何学がどのように機能するかについての理解を深めるのに役立つだけでなく、XR レンダリング中に使用される関数とデータがどのように連携するかを確実に理解するのに役立ちます。

+ +
+
この例の実際のスクリーンショット
+ユーザーが動きまわることができるテクスチャー付き立方体を示す例のスクリーンショット
+ +

この例のコアである、回転するテクスチャー付きで照明付きの立方体は、WebGL チュートリアルシリーズから抜粋したものです。 つまり、シリーズの最後から2番目の記事の WebGL でのライティングをカバーします。

+ +

この記事と付随するソースコードを読むときは、3D ヘッドセットのディスプレイが半分に分割された単一の画面であることを覚えておくと役に立ちます。 画面の左半分は左目でのみ表示され、右半分は右目でのみ表示されます。 没入型プレゼンテーションのためにシーンをレンダリングするには、各目の視点から1回ずつ、シーンを複数回レンダリングする必要があります。

+ +

左目をレンダリングする場合、{{domxref("XRWebGLLayer")}} の {{domxref("XRWebGLLayer.viewport", "viewport")}} は、描画を描画面の左半分に制限するように構成されています。 逆に、右目をレンダリングする場合、ビューポートは描画を描画面の右半分に制限するように設定されます。

+ +

この例では、XR デバイスを使用して没入型ディスプレイとしてシーンを提示する場合でも、画面上のキャンバスに表示することでこれを示しています。

+ +

依存関係

+ +

この例では、three.js などの 3D グラフィックフレームワークに依存しませんが、行列演算には過去に他の例で使用している glMatrix ライブラリーを使用します。 この例では、WebXR API の仕様を担当するチームである Immersive Web Working Group によって管理されている WebXR ポリフィルもインポートします。 このポリフィルをインポートすることで、WebXR がまだ実装されていない多くのブラウザーでこの例を機能させることができ、WebXR 仕様のまだ実験的な日々の間に発生する仕様からの一時的な逸脱を滑らかにします。

+ +

オプション

+ +

この例には、ブラウザーにロードする前に定数の値を調整することで構成できるいくつかのオプションがあります。 コードは次のようになります。

+ +
const xRotationDegreesPerSecond = 25;
+const yRotationDegreesPerSecond = 15;
+const zRotationDegreesPerSecond = 35;
+const enableRotation = true;
+const allowMouseRotation = true;
+const allowKeyboardMotion = true;
+const enableForcePolyfill = false;
+//const SESSION_TYPE = "immersive-vr";
+const SESSION_TYPE = "inline";
+const MOUSE_SPEED = 0.003;
+
+ +
+
xRotationDegreesPerSecond
+
X 軸を中心に回転させる1秒あたりの度数。
+
yRotationDegreesPerSecond
+
Y 軸を中心に回転させる1秒あたりの度数。
+
zRotationDegreesPerSecond
+
Z 軸を中心に回転させる1秒あたりの度数。
+
enableRotation
+
立方体の回転を有効にするかどうかを示すブール値。
+
allowMouseRotation
+
true の場合、マウスを使用して視野角をピッチ(上下)したりヨー(左右)したりできます。
+
allowKeyboardMotion
+
true の場合、W、A、S、D キーはビューアーを上、左、下、右に移動し、上下の矢印キーは前後に移動します。 false の場合、ビューへの XR デバイスの変更のみを許可します。
+
enableForcePolyfill
+
このブール値が true の場合、ブラウザーが実際に WebXR をサポートしている場合でも、この例では WebXR ポリフィルの使用を試みます。 false の場合、ブラウザーが {{domxref("navigator.xr")}} を実装していない場合にのみポリフィルを使用します。
+
SESSION_TYPE
+
作成する XR セッションのタイプ: ドキュメントのコンテキストで提示するインラインセッションの inline と、シーンを没入型 VR ヘッドセットに提示する immersive-vr
+
MOUSE_SPEED
+
ピッチとヨーの制御に使用するマウスからの入力をスケーリングするために使用される乗数。
+
MOVE_DISTANCE
+
シーン内でビューアーを移動するために使用するキーのいずれかに応答して移動する距離。
+
+ +
+

: この例では、immersive-vr モードを使用している場合でも、常に画面にレンダリングされる内容が表示されます。 これにより、2つのモード間のレンダリングの違いを比較でき、ヘッドセットがない場合でも没入型モードからの出力を確認できます。

+
+ +

セットアップおよびユーティリティ関数

+ +

次に、WebGL および WebXR 固有の情報を格納するために使用されるものから始めて、アプリケーション全体で使用される変数と定数を宣言します。

+ +
let polyfill = null;
+let xrSession = null;
+let xrInputSources = null;
+let xrReferenceSpace = null;
+let xrButton = null;
+let gl = null;
+let animationFrameRequestID = 0;
+let shaderProgram = null;
+let programInfo = null;
+let buffers = null;
+let texture = null;
+let mouseYaw = 0;
+let mousePitch = 0;
+
+ +

この後に一連の定数が続きます。 これには主に、シーンのレンダリング中に使用されるさまざまなベクトルと行列が含まれます。

+ +
const viewerStartPosition = vec3.fromValues(0, 0, -10);
+const viewerStartOrientation = vec3.fromValues(0, 0, 1.0);
+
+const cubeOrientation = vec3.create();
+const cubeMatrix = mat4.create();
+const mouseMatrix = mat4.create();
+const inverseOrientation = quat.create();
+const RADIANS_PER_DEGREE = Math.PI / 180.0;
+
+ +

最初の2つ(viewerStartPositionviewerStartOrientation)は、空間の中心に対してビューアーが配置される場所と、最初に見る方向を示します。 cubeOrientation は立方体の現在の方向を格納し、cubeMatrixmouseMatrix はシーンのレンダリング中に使用される行列のストレージです。 inverseOrientation は、レンダリングされるフレーム内のオブジェクトの参照空間に適用する回転を表すために使用されるクォータニオンです。

+ +

RADIANS_PER_DEGREEE は、角度をラジアンに変換するために度単位の角度を乗算する値です。

+ +

宣言された最後の4つの変数は、ユーザーに行列を見せるための出力先の {{HTMLElement("div")}} 要素を参照するためのストレージです。

+ +

エラーのロギング

+ +

LogGLError() と呼ばれる関数は、WebGL 関数の実行中に発生したエラーのログ情報を出力するための簡単にカスタマイズされた方法を提供するために実装されています。

+ +
function LogGLError(where) {
+  let err = gl.getError();
+  if (err) {
+    console.error(`WebGL error returned by ${where}: ${err}`);
+  }
+}
+ +

これは、プログラムのどの部分がエラーを生成したかを示すために使用される文字列 where を唯一の入力として受け取ります。 これは、同様のエラーが複数の状況で発生する可能性があるためです。

+ +

頂点シェーダーとフラグメントシェーダー

+ +

頂点シェーダーとフラグメントシェーダーはどちらも、WebGL でのライティングの記事の例で使用されているものとまったく同じです。 ここで使用されている基本的なシェーダーの GLSL ソースコードに興味がある場合は、それを参照してください。

+ +

頂点シェーダーは、各頂点の初期位置と、ビューアーの現在の位置と方向をシミュレートするためにそれらを変換するために適用する必要のある変換を指定して、各頂点の位置を計算するとだけ言っておきましょう。 フラグメントシェーダーは、テクスチャーで見つかった値から必要に応じて補間し、照明効果を適用して各頂点の色を返します。

+ +

WebXR の起動と停止

+ +

スクリプトを最初にロードするときに、{{domxref("Window.load_event", "load")}} イベントのハンドラーをインストールして、初期化を実行できるようにします。

+ +
window.addEventListener("load", onLoad);
+
+function onLoad() {
+  xrButton = document.querySelector("#enter-xr");
+  xrButton.addEventListener("click", onXRButtonClick);
+
+  projectionMatrixOut = document.querySelector("#projection-matrix div");
+  modelMatrixOut = document.querySelector("#model-view-matrix div");
+  cameraMatrixOut = document.querySelector("#camera-matrix div");
+  mouseMatrixOut = document.querySelector("#mouse-matrix div");
+
+  if (!navigator.xr || enableForcePolyfill) {
+    console.log("Using the polyfill");
+    polyfill = new WebXRPolyfill();
+  }
+  setupXRButton();
+}
+ +

load イベントハンドラーは、WebXR のオンとオフを切り替えるボタンへの参照を xrButton に取得し、{{domxref("Element.click_event", "click")}} イベントのハンドラーを追加します。 次に、4つの {{HTMLElement("div")}} ブロックへの参照を取得します。 このブロックには、シーンの実行中に情報提供の目的で、手がかりとなる行列それぞれの現在の内容が出力されます。

+ +

次に、{{domxref("navigator.xr")}} が定義されているかどうかを確認します。 そうでない場合、および/または enableForcePolyfill 構成定数が true に設定されている場合は、WebXRPolyfill クラスをインスタンス化して WebXR ポリフィルをインストールします。

+ +

起動と停止の UI の処理

+ +

次に、setupXRButton() 関数を呼び出します。 この関数は、SESSION_TYPE 定数で指定されたセッションタイプに対する WebXR サポートの有無により、必要に応じて "Enter/Exit WebXR" ボタンを有効または無効にするための構成を処理します。

+ +
function setupXRButton() {
+  if (navigator.xr.isSessionSupported) {
+    navigator.xr.isSessionSupported(SESSION_TYPE)
+    .then((supported) => {
+      xrButton.disabled = !supported;
+    });
+  } else {
+    navigator.xr.supportsSession(SESSION_TYPE)
+    .then(() => {
+      xrButton.disabled = false;
+    })
+    .catch(() => {
+      xrButton.disabled = true;
+    });
+  }
+}
+ +

ボタンのラベルは、実際に WebXR セッションの開始と停止を処理するコードで調整されます。 以下にそれを示します。

+ +

WebXR セッションは、ボタンの {{domxref("Element.click_event", "click")}} イベントのハンドラーによってオンとオフが切り替えられ、ボタンのラベルは、"Enter WebXR" または "Exit WebXR" に適切に設定されます。 これは、onXRButtonClick() イベントハンドラーによって行われます。

+ +
async function onXRButtonClick(event) {
+  if (!xrSession) {
+    navigator.xr.requestSession(SESSION_TYPE)
+    .then(sessionStarted);
+  } else {
+    await xrSession.end();
+
+    if (xrSession) {
+      sessionEnded();
+    }
+  }
+}
+ +

これは、xrSession の値を調べて、進行中の WebXR セッションを表す {{domxref("XRSession")}} オブジェクトがすでにあるかどうかを確認することから始まります。 ない場合は、クリックは WebXR モードを有効にする要求を表しているので、{{domxref("XRSystem.requestSession", "requestSession()")}} を呼び出して、目的の WebXR セッションタイプの WebXR セッションを要求し、次に sessionStarted() を呼び出して、その WebXR セッションでシーンの実行を開始します。

+ +

一方、進行中のセッションがすでにある場合は、その {{domxref("XRSession.end", "end()")}} メソッドを呼び出してセッションを停止します。

+ +

このコードで最後に行うことは、xrSession がまだ非 NULL かどうかを確認することです。 そうである場合は、{{domxref("XRSession.end_event", "end")}} イベントのハンドラーである sessionEnded() を呼び出します。 このコードは必要ないはずですが、少なくとも一部のブラウザーが end イベントを正しく発火しないという問題があるようです。 イベントハンドラーを直接実行することにより、この状況で終了プロセスを手動で完了します。

+ +

WebXR セッションの開始

+ +

sessionStarted() 関数は、イベントハンドラーを設定し、頂点シェーダーとフラグメントシェーダーの GLSL コードをコンパイルしてインストールし、レンダリングループを開始する前に WebGL レイヤーを WebXR セッションにアタッチすることにより、実際のセッションの設定と開始を処理します。 これは、{{domxref("XRSystem.requestSession", "requestSession()")}} によって返される promise のハンドラーとして呼び出されます。

+ +
function sessionStarted(session) {
+  let refSpaceType;
+
+  xrSession = session;
+  xrButton.innerText = "Exit WebXR";
+  xrSession.addEventListener("end", sessionEnded);
+
+  let canvas = document.querySelector("canvas");
+  gl = canvas.getContext("webgl", { xrCompatible: true });
+
+  if (allowMouseRotation) {
+    canvas.addEventListener("pointermove", handlePointerMove);
+    canvas.addEventListener("contextmenu", (event) => { event.preventDefault(); });
+  }
+
+  if (allowKeyboardMotion) {
+    document.addEventListener("keydown", handleKeyDown);
+  }
+
+  shaderProgram = initShaderProgram(gl, vsSource, fsSource);
+
+  programInfo = {
+    program: shaderProgram,
+    attribLocations: {
+      vertexPosition: gl.getAttribLocation(shaderProgram, 'aVertexPosition'),
+      vertexNormal: gl.getAttribLocation(shaderProgram, 'aVertexNormal'),
+      textureCoord: gl.getAttribLocation(shaderProgram, 'aTextureCoord'),
+    },
+    uniformLocations: {
+      projectionMatrix: gl.getUniformLocation(shaderProgram, 'uProjectionMatrix'),
+      modelViewMatrix: gl.getUniformLocation(shaderProgram, 'uModelViewMatrix'),
+      normalMatrix: gl.getUniformLocation(shaderProgram, 'uNormalMatrix'),
+      uSampler: gl.getUniformLocation(shaderProgram, 'uSampler')
+    },
+  };
+
+  buffers = initBuffers(gl);
+  texture = loadTexture(gl, 'https://cdn.glitch.com/a9381af1-18a9-495e-ad01-afddfd15d000%2Ffirefox-logo-solid.png?v=1575659351244');
+
+  xrSession.updateRenderState({
+    baseLayer: new XRWebGLLayer(xrSession, gl)
+  });
+
+  if (SESSION_TYPE == "immersive-vr") {
+    refSpaceType = "local";
+  } else {
+    refSpaceType = "viewer";
+  }
+
+  mat4.fromTranslation(cubeMatrix, viewerStartPosition);
+
+  vec3.copy(cubeOrientation, viewerStartOrientation);
+
+  xrSession.requestReferenceSpace(refSpaceType)
+  .then((refSpace) => {
+    xrReferenceSpace = refSpace.getOffsetReferenceSpace(
+          new XRRigidTransform(viewerStartPosition, cubeOrientation));
+    animationFrameRequestID = xrSession.requestAnimationFrame(drawFrame);
+  });
+
+  return xrSession;
+}
+ +

新しく作成した {{domxref("XRSession")}} オブジェクトを xrSession に保存した後、ボタンのラベルを "Exit WebXR" に設定して、シーンの開始後の新しい機能を示します。 また、{{domxref("XRSession.end_event", "end")}} イベントのハンドラーをインストールしているため、XRSession の終了が通知されます。

+ +

次に、HTML にある {{HTMLElement("canvas")}} への参照とその WebGL レンダリングコンテキストを取得します。 これは、シーンの描画面として使用されます。 xrCompatible プロパティは、要素で {{domxref("HTMLCanvasElement.getContext", "getContext()")}} を呼び出して、キャンバスの WebGL レンダリングコンテキストにアクセスするときに要求されます。 これにより、コンテキストが WebXR レンダリングのソースとして使用できるように構成されます。

+ +

次に、{{domxref("Element.mousemove_event", "mousemove")}} と {{domxref("Element.contextmenu_event","contextmenu")}} のイベントハンドラーを追加しますが、allowMouseRotation 定数が true の場合に限ります。 mousemove ハンドラーは、マウスの動きに基づいてビューのピッチとヨーを処理します。 「マウスルック」機能はマウスの右ボタンを押している間のみ機能し、マウスの右クリックでコンテキストメニューがトリガーされるため、contextmenu イベントのハンドラーをキャンバスに追加して、ユーザーが最初にマウスのドラッグを開始したときにコンテキストメニューが表示されないようにします。

+ +

次に、シェーダープログラムをコンパイルし、その変数への参照を取得し、各位置の配列を格納するバッファーを初期化し、各頂点の位置テーブルへのインデックス、頂点法線、各頂点のテクスチャー座標を格納します。 これはすべて WebGL サンプルコードから直接取得されているため、WebGL でのライティングとその前の記事の WebGL を用いた 3D オブジェクトの作成および WebGL でのテクスチャーの使用を参照してください。 次に、loadTexture() 関数を呼び出して、テクスチャーファイルをロードします。

+ +

レンダリング構造とデータがロードされたので、XRSession を実行する準備を開始します。 baseLayer を新しい {{domxref("XRWebGLLayer")}} に設定して {{domxref("XRSession.updateRenderState()")}} を呼び出すことにより、セッションを WebGL レイヤーに接続して、レンダリング面として何を使用するかを認識します。

+ +

次に、SESSION_TYPE 定数の値を調べて、WebXR コンテキストを没入型にするかインラインにするかを確認します。 没入型セッションは local 参照空間を使用し、インラインセッションは viewer 参照空間を使用します。

+ +

glMatrix ライブラリーの 4x4 行列用の fromTranslation() 関数は、viewerStartPosition 定数で指定されたビューアーの開始位置を変換行列 cubeMatrix に変換するために使用します。 ビューアーの開始方向である viewerStartOrientation 定数は、cubeOrientation にコピーして、時間の経過に伴う立方体の回転を追跡するために使用します。

+ +

sessionStarted() は、セッションの {{domxref("XRSession.requestReferenceSpace", "requestReferenceSpace()")}} メソッドを呼び出して、オブジェクトを作成する空間を記述する参照空間オブジェクトを取得することで仕上げます。 返された promise が {{domxref("XRReferenceSpace")}} オブジェクトに解決されると、その {{domxref("XRReferenceSpace.getOffsetReferenceSpace", "getOffsetReferenceSpace")}} メソッドを呼び出して、オブジェクトの座標系を表す参照空間オブジェクトを取得します。 新しい空間の原点は、viewerStartPosition で指定された世界座標にあり、その方向は cubeOrientation に設定されています。 次に、{{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} メソッドを呼び出して、フレームを描画する準備ができたことをセッションに通知します。 後でリクエストをキャンセルする必要がある場合に備えて、返されたリクエスト ID を記録します。

+ +

最後に、sessionStarted() は、ユーザーの WebXR セッションを表す {{domxref("XRSession")}} を返します。

+ +

セッション終了時

+ +

(ユーザーによる終了か、{{domxref("XRSession.end()")}} の呼び出しで)WebXR セッションが終了すると、{{domxref("XRSession.end_event", "end")}} イベントが送信されます。 これを、sessionEnded() という関数を呼び出すように設定しました。

+ +
function sessionEnded() {
+  xrButton.innerText = "Enter WebXR";
+
+  if (animationFrameRequestID) {
+    xrSession.cancelAnimationFrame(animationFrameRequestID);
+    animationFrameRequestID = 0;
+  }
+  xrSession = null;
+}
+ +

プログラムで WebXR セッションを終了したい場合は、sessionEnded() を直接呼び出すこともできます。 いずれの場合も、ボタンのラベルを更新して、クリックによってセッションが開始されることを示します。 その後、アニメーションフレームに対する保留中のリクエストがある場合は、{{domxref("XRSession.cancelAnimationFrame", "cancelAnimationFrame")}} を呼び出してキャンセルします。

+ +

それが完了すると、xrSession の値を NULL に変更して、セッションが終了したことを示します。

+ +

コントロールの実装

+ +

それでは、キーボードとマウスのイベントを WebXR シナリオでアバターを制御するために使用できるものに変換するコードを見てみましょう。

+ +

キーボードを使用した移動

+ +

空間を移動するための入力を備えた WebXR デバイスがなくても、ユーザーが 3D 世界を移動できるようにするために、{{domxref("Element.keydown_event", "keydown")}} のハンドラーである handleKeyDown() は、押されたキーに基づいてオブジェクトの原点からのオフセットを更新することで応答します。

+ +
function handleKeyDown(event) {
+  switch(event.key) {
+    case "w":
+    case "W":
+      verticalDistance -= MOVE_DISTANCE;
+      break;
+    case "s":
+    case "S":
+      verticalDistance += MOVE_DISTANCE;
+      break;
+    case "a":
+    case "A":
+      transverseDistance += MOVE_DISTANCE;
+      break;
+    case "d":
+    case "D":
+      transverseDistance -= MOVE_DISTANCE;
+      break;
+    case "ArrowUp":
+      axialDistance += MOVE_DISTANCE;
+      break;
+    case "ArrowDown":
+      axialDistance -= MOVE_DISTANCE;
+      break;
+    case "r":
+    case "R":
+      transverseDistance = axialDistance = verticalDistance = 0;
+      mouseYaw = mousePitch = 0;
+      break;
+    default:
+      break;
+  }
+}
+ +

キーとその効果は次のとおりです。

+ + + +

これらのオフセットは、次のフレームの描画からレンダラーによって適用されます。

+ +

マウスによるピッチとヨー

+ +

また、マウスの右ボタンが押されているかどうかを確認する {{domxref("Element.mousemove_event", "mousemove")}} イベントハンドラーがあり、押されている場合は、その次に定義されている rotateViewBy() 関数を呼び出して、新しいピッチ(上下を見る)とヨー(左右を見る)の値を計算して保存します。

+ +
function handlePointerMove(event) {
+  if (event.buttons & 2) {
+    rotateViewBy(event.movementX, event.movementY);
+  }
+}
+ +

新しいピッチとヨーの値の計算は、次の rotateViewBy() 関数で処理します。

+ +
function rotateViewBy(dx, dy) {
+  mouseYaw -= dx * MOUSE_SPEED;
+  mousePitch -= dy * MOUSE_SPEED;
+
+  if (mousePitch < -Math.PI * 0.5) {
+    mousePitch = -Math.PI * 0.5;
+  } else if (mousePitch > Math.PI * 0.5) {
+    mousePitch = Math.PI * 0.5;
+  }
+}
+ +

入力としてマウス移動量の dxdy が与えられると、新しいヨー値は、mouseYaw の現在の値から dxMOUSE_SPEED スケーリング定数の積を引くことで計算します。 そして、MOUSE_SPEED の値を増やすことで、マウスの応答性を制御できます。

+ +

フレームの描画

+ +

{{domxref("XRSession.requestAnimationFrame()")}} のコールバックは、以下に示す drawFrame() 関数に実装されています。 その仕事は、ビューアーの参照空間を取得し、最後のフレームからの経過時間を考慮して、アニメーション化されたオブジェクトに適用する必要のある動きの量を計算し、ビューアーの {{domxref("XRPose")}} によって指定された各ビューをレンダリングすることです。

+ +
let lastFrameTime = 0;
+
+function drawFrame(time, frame) {
+  let session = frame.session;
+  let adjustedRefSpace = xrReferenceSpace;
+  let pose = null;
+
+  animationFrameRequestID = session.requestAnimationFrame(drawFrame);
+  adjustedRefSpace = applyViewerControls(xrReferenceSpace);
+  pose = frame.getViewerPose(adjustedRefSpace);
+
+  if (pose) {
+    let glLayer = session.renderState.baseLayer;
+
+    gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
+    LogGLError("bindFrameBuffer");
+
+    gl.clearColor(0, 0, 0, 1.0);
+    gl.clearDepth(1.0);                 // Clear everything
+    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+    LogGLError("glClear");
+
+    const deltaTime = (time - lastFrameTime) * 0.001;  // Convert to seconds
+    lastFrameTime = time;
+
+    for (let view of pose.views) {
+      let viewport = glLayer.getViewport(view);
+      gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
+      LogGLError(`Setting viewport for eye: ${view.eye}`);
+      gl.canvas.width = viewport.width * pose.views.length;
+      gl.canvas.height = viewport.height;
+      renderScene(gl, view, programInfo, buffers, texture, deltaTime);
+    }
+  }
+}
+ +

最初に行うことは、{{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} を呼び出して、次のフレームをレンダリングするために drawFrame() を再度呼び出すように要求することです。 次に、オブジェクトの参照空間を applyViewerControls() 関数に渡します。 この関数は、キーボードとマウスを使用してユーザーが適用した移動、ピッチ、ヨーを考慮してオブジェクトの位置と方向を変換する、改訂された {{domxref("XRReferenceSpace")}} を返します。 いつものように、ビューアーではなく、世界のオブジェクトを、移動し、方向を変更することを忘れないでください。 返された参照空間により、それを簡単に行うことができます。

+ +

新しい参照空間が手元にあると、ビューアーの両目の視点を表す {{domxref("XRViewerPose")}} が得られます。 それが成功した場合は、セッションで使用されている {{domxref("XRWebGLLayer")}} を取得し、そのフレームバッファーを WebGL フレームバッファーとして使用するようにバインドすることでレンダリングの準備を開始します(WebGL のレンダリングはレイヤーに描画することで、XR デバイスのディスプレイに描画されます)。 XR デバイスにレンダリングするように WebGL が構成されたので、フレームを黒にクリアして、レンダリングを開始する準備が整いました。

+ +

最後のフレームがレンダリングされてからの経過時間(秒単位)は、time パラメーターで指定された現在の時刻から前のフレームのタイムスタンプ lastFrameTime を減算し、0.001 を掛けてミリ秒を秒に変換することで計算します。 次に、現在の時刻を lastFrameTime に保存します。

+ +

drawFrame() 関数は、{{domxref("XRViewerPose")}} で見つかったすべてのビューを反復処理し、ビューのビューポートを設定し、renderScene() を呼び出してフレームをレンダリングすることで終了します。 各ビューのビューポートを設定することにより、各目のビューがそれぞれ WebGL フレームの半分にレンダリングされる典型的なシナリオを処理します。 次に、XR ハードウェアは、各目がその目向けの画像の部分のみを表示するように処理します。

+ +
+

: この例では、XR デバイスと画面の両方にフレームを視覚的に表示しています。 画面上のキャンバスがこれを実行できる適切なサイズであることを確認するために、その幅を個々の {{domxref("XRView")}} の幅にビューの数を掛けたものに等しくなるように設定します。 キャンバスの高さは常にビューポートの高さと同じです。 キャンバスサイズを調整する2行のコードは、通常の WebXR レンダリングループでは必要ありません。

+
+ +

ユーザー入力の適用

+ +

何かをレンダリングし始める前に drawFrame() によって呼び出される applyViewerControls() 関数は、ユーザーがキーを押したり、マウスの右ボタンを押したままマウスをドラッグしたときに handleKeyDown() 関数と handlePointerMove() 関数によって記録された3方向のそれぞれのオフセット、ヨーオフセット、およびピッチオフセットを取得します。 オブジェクトのベース参照空間を入力として受け取り、入力の結果と一致するようにオブジェクトの位置と方向を変更する新しい参照空間を返します。

+ +
function applyViewerControls(refSpace) {
+  if (!mouseYaw && !mousePitch && !axialDistance &&
+      !transverseDistance && !verticalDistance) {
+    return refSpace;
+  }
+
+  quat.identity(inverseOrientation);
+  quat.rotateX(inverseOrientation, inverseOrientation, -mousePitch);
+  quat.rotateY(inverseOrientation, inverseOrientation, -mouseYaw);
+
+  let newTransform = new XRRigidTransform({x: transverseDistance,
+                                           y: verticalDistance,
+                                           z: axialDistance},
+                         {x: inverseOrientation[0], y: inverseOrientation[1],
+                          z: inverseOrientation[2], w: inverseOrientation[3]});
+  mat4.copy(mouseMatrix, newTransform.matrix);
+
+  return refSpace.getOffsetReferenceSpace(newTransform);
+}
+ +

すべての入力オフセットがゼロの場合、元の参照空間を返すだけです。 それ以外の場合は、mousePitchmouseYaw の方向の変更から、その方向の逆を指定するクォータニオンを作成します。 これにより、inverseOrientation を立方体に適用すると、ビューアーの動きが正しく表示されます。

+ +

次に、移動または方向変更されたオブジェクトの新しい {{domxref("XRReferenceSpace")}} を作成するために使用する変換を表す新しい {{domxref("XRRigidTransform")}} オブジェクトを作成します。 位置は、xyz がこれらの各軸に沿って移動したオフセットに対応する新しいベクトルです。 方向は、inverseOrientation クォータニオンです。

+ +

変換の {{domxref("XRRigidTransform.matrix", "matrix")}} を mouseMatrix にコピーします。 これは、後でマウス追跡行列(Mouse tracking matrix)をユーザーに表示するために使用します(したがって、これは通常スキップできる手順です)。 最後に、XRRigidTransform をオブジェクトの現在の {{domxref("XRReferenceSpace")}} に渡して、この変換を統合し、ユーザーの動きを考慮したユーザーに対する立方体の配置を表す参照空間を取得します。 その新しい参照空間を呼び出し元に返します。

+ +

シーンのレンダリング

+ +

renderScene() 関数は、ユーザーがその瞬間に見える世界の部分を実際にレンダリングするために呼び出されます。 XR ギアに必要な 3D 効果を確立するために、それぞれの目でわずかに異なる位置を使用し、それぞれの目に対して1回ずつ呼び出されます。

+ +

このコードのほとんどは、WebGL でのライティングの記事の drawScene() 関数から直接取得した典型的な WebGL レンダリングコードであり、この例の WebGL レンダリング部分の詳細についてはそこを参照してください(GitHubでコードを見る)。 しかし、ここでは、この例に固有のコードから始まっているので、その部分について詳しく見ていきます。

+ +
const normalMatrix = mat4.create();
+const modelViewMatrix = mat4.create();
+
+function renderScene(gl, view, programInfo, buffers, texture, deltaTime) {
+  const xRotationForTime = (xRotationDegreesPerSecond * RADIANS_PER_DEGREE) * deltaTime;
+  const yRotationForTime = (yRotationDegreesPerSecond * RADIANS_PER_DEGREE) * deltaTime;
+  const zRotationForTime = (zRotationDegreesPerSecond * RADIANS_PER_DEGREE) * deltaTime;
+
+  gl.enable(gl.DEPTH_TEST);           // Enable depth testing
+  gl.depthFunc(gl.LEQUAL);            // Near things obscure far things
+
+  if (enableRotation) {
+    mat4.rotate(cubeMatrix,  // destination matrix
+                cubeMatrix,  // matrix to rotate
+                zRotationForTime,     // amount to rotate in radians
+                [0, 0, 1]);       // axis to rotate around (Z)
+    mat4.rotate(cubeMatrix,  // destination matrix
+                cubeMatrix,  // matrix to rotate
+                yRotationForTime, // amount to rotate in radians
+                [0, 1, 0]);       // axis to rotate around (Y)
+    mat4.rotate(cubeMatrix,  // destination matrix
+                cubeMatrix,  // matrix to rotate
+                xRotationForTime, // amount to rotate in radians
+                [1, 0, 0]);       // axis to rotate around (X)
+  }
+
+  mat4.multiply(modelViewMatrix, view.transform.inverse.matrix, cubeMatrix);
+  mat4.invert(normalMatrix, modelViewMatrix);
+  mat4.transpose(normalMatrix, normalMatrix);
+
+  displayMatrix(view.projectionMatrix, 4, projectionMatrixOut);
+  displayMatrix(modelViewMatrix, 4, modelMatrixOut);
+  displayMatrix(view.transform.matrix, 4, cameraMatrixOut);
+  displayMatrix(mouseMatrix, 4, mouseMatrixOut);
+
+  {
+    const numComponents = 3;
+    const type = gl.FLOAT;
+    const normalize = false;
+    const stride = 0;
+    const offset = 0;
+    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.position);
+    gl.vertexAttribPointer(
+        programInfo.attribLocations.vertexPosition,
+        numComponents,
+        type,
+        normalize,
+        stride,
+        offset);
+    gl.enableVertexAttribArray(
+        programInfo.attribLocations.vertexPosition);
+  }
+
+  {
+    const numComponents = 2;
+    const type = gl.FLOAT;
+    const normalize = false;
+    const stride = 0;
+    const offset = 0;
+    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.textureCoord);
+    gl.vertexAttribPointer(
+        programInfo.attribLocations.textureCoord,
+        numComponents,
+        type,
+        normalize,
+        stride,
+        offset);
+    gl.enableVertexAttribArray(
+        programInfo.attribLocations.textureCoord);
+  }
+
+  {
+    const numComponents = 3;
+    const type = gl.FLOAT;
+    const normalize = false;
+    const stride = 0;
+    const offset = 0;
+    gl.bindBuffer(gl.ARRAY_BUFFER, buffers.normal);
+    gl.vertexAttribPointer(
+        programInfo.attribLocations.vertexNormal,
+        numComponents,
+        type,
+        normalize,
+        stride,
+        offset);
+    gl.enableVertexAttribArray(
+        programInfo.attribLocations.vertexNormal);
+  }
+
+  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffers.indices);
+  gl.useProgram(programInfo.program);
+
+  gl.uniformMatrix4fv(
+      programInfo.uniformLocations.projectionMatrix,
+      false,
+      view.projectionMatrix);
+  gl.uniformMatrix4fv(
+      programInfo.uniformLocations.modelViewMatrix,
+      false,
+      modelViewMatrix);
+  gl.uniformMatrix4fv(
+      programInfo.uniformLocations.normalMatrix,
+      false,
+      normalMatrix);
+  gl.activeTexture(gl.TEXTURE0);
+  gl.bindTexture(gl.TEXTURE_2D, texture);
+
+  gl.uniform1i(programInfo.uniformLocations.uSampler, 0);
+
+  {
+    const vertexCount = 36;
+    const type = gl.UNSIGNED_SHORT;
+    const offset = 0;
+    gl.drawElements(gl.TRIANGLES, vertexCount, type, offset);
+  }
+}
+ +

renderScene() は、前のフレームがレンダリングされてから経過した時間内に、3つの軸のそれぞれの周りで発生する回転量を計算することから始まります。 これらの値により、アニメーションする立方体の回転を適切な量に調整して、システムの負荷によって発生する可能性のあるフレームレートの変動に関係なく、その移動速度が一定に保たれるようにすることができます。 これらの値は、経過時間を指定して適用する回転のラジアン数として計算して、定数の xRotationForTimeyRotationForTimezRotationForTime に格納します。

+ +

奥行きテストを有効にして構成した後、enableRotation 定数の値をチェックして、立方体の回転が有効になっているかどうかを確認します。 そうである場合は、glMatrix を使用して、3つの軸を中心に cubeMatrix(世界空間に対する立方体の現在の方向を表す)を回転させます。 立方体のグローバルな方向が確立されたら、それをビューの変換行列の逆行列で乗算して、最終的なモデルビュー行列を取得します。 この行列は、オブジェクトに適用して、アニメーションの目的でそれを回転させるだけでなく、それを移動したり方向を変えたりして、空間を介したビューアーのモーションをシミュレートすることもします。

+ +

次に、モデルビュー行列を取得して反転(逆行列を計算)し、転置(列と行を入れ替える)することで、ビューの正規行列を計算します。

+ +

この例で追加されたコードの最後の数行は、ユーザーによる分析のために行列の内容を表示する関数である displayMatrix() への4つの呼び出しです。 関数の残りの部分は、このコードの派生元である古い WebGL サンプルと同一または本質的に同一です。

+ +

行列の表示

+ +

この例では、説明のために、シーンのレンダリング中に使用している重要な行列の内容を表示しています。 これには、displayMatrix() 関数を使用します。 この関数は MathML を使用して行列をレンダリングし、MathML がユーザーのブラウザーでサポートされていない場合は、より配列に近い形式にフォールバックします。

+ +
function displayMatrix(mat, rowLength, target) {
+  let outHTML = "";
+
+  if (mat && rowLength && rowLength <= mat.length) {
+    let numRows = mat.length / rowLength;
+    outHTML = "<math xmlns='http://www.w3.org/1998/Math/MathML' display='block'>\n<mrow>\n<mo>[</mo>\n<mtable>\n";
+
+    for (let y=0; y<numRows; y++) {
+      outHTML += "<mtr>\n";
+      for (let x=0; x<rowLength; x++) {
+        outHTML += `<mtd><mn>${mat[(x*rowLength) + y].toFixed(2)}</mn></mtd>\n`;
+      }
+      outHTML += "</mtr>\n";
+    }
+
+    outHTML += "</mtable>\n<mo>]</mo>\n</mrow>\n</math>";
+  }
+
+  target.innerHTML = outHTML;
+}
+ +

これにより、target で指定された要素の内容が、4x4 行列を含む新しく作成された {{MathMLElement("math")}} 要素に置き換えられます。 各エントリーは、小数点以下2桁まで表示します。

+ +

ほかのすべて

+ +

残りのコードは、前の例で見つかったものと同じです。

+ +
+
initShaderProgram()
+
GLSL シェーダープログラムを初期化し、loadShader() を呼び出して各シェーダーのプログラムをロードおよびコンパイルしてから、各シェーダーを WebGL コンテキストにアタッチします。 それらがコンパイルされると、プログラムはリンクされ、呼び出し元に返されます。
+
loadShader()
+
シェーダーオブジェクトを作成し、指定されたソースコードをそのオブジェクトにロードしてから、コードをコンパイルし、コンパイラが成功したことを確認してから、新しくコンパイルされたシェーダーを呼び出し元に返します。 エラーが発生した場合は、代わりに NULL を返します。
+
initBuffers()
+
WebGL に渡すデータを含んだバッファーを初期化します。 これらのバッファーには、頂点位置の配列、頂点法線の配列、立方体の各面のテクスチャー座標、および頂点インデックスの配列(頂点リストのどのエントリーが立方体のそれそれの角を表すかを指定)が含まれます。
+
loadTexture()
+
指定された URL で画像をロードし、そこから WebGL テクスチャーを作成します。 画像の寸法が両方とも2の累乗でない場合(isPowerOf2() 関数を参照)、ミップマップを無効とし、ラッピングをエッジに固定します。 これは、ミップマップテクスチャーの最適化されたレンダリングが、WebGL 1 の2の累乗の寸法のテクスチャーに対してのみ機能するためです。 WebGL 2 は、ミップマップに任意の寸法のテクスチャーをサポートしています。
+
isPowerOf2()
+
指定された値が2の累乗の場合、true を返します。 それ以外の場合は false を返します。
+
+ +

すべてをまとめる

+ +

このコードをすべて取得して、上記に含まれていない HTML およびその他の JavaScript コードを追加すると、この例を Glitch で試したときに表示されるものが得られます。 覚えておいてください、歩き回って迷子になったら、R キーを押すだけで最初に戻ることができます。

+ +

ヒント: XR デバイスをお持ちでない場合は、顔を画面に非常に近づけて、キャンバス内の左目と右目の画像の境界に沿って鼻を中央に配置すると、3D 効果を得ることができるかもしれません。 画面を通して画像に注意深く焦点を合わせ、ゆっくりと前後に動くことで、最終的に 3D 画像に焦点を合わせることができるはずです。 これには練習が必要で、視力の鋭さによっては、文字通り鼻が画面に触れているかもしれません。

+ +

この例を出発点として、できることはたくさんあります。 世界にオブジェクトを追加したり、移動コントロールを改善してよりリアルに移動してみてください。 壁、天井、床を追加して、無限に見える宇宙に迷うのではなく、空間に閉じ込めます。 衝突テストやヒットテスト、または立方体の各面のテクスチャーを変更する機能を追加します。

+ +

自分で設定すれば、できることに制限はほとんどありません。

+ +

関連情報

+ + diff --git a/files/ja/web/api/webxr_device_api/rendering/index.html b/files/ja/web/api/webxr_device_api/rendering/index.html new file mode 100644 index 0000000000..43e40e3028 --- /dev/null +++ b/files/ja/web/api/webxr_device_api/rendering/index.html @@ -0,0 +1,363 @@ +--- +title: レンダリングと WebXR フレームアニメーションコールバック +slug: Web/API/WebXR_Device_API/Rendering +tags: + - API + - AR + - Animation + - Drawing + - Frames + - Games + - Guide + - Intermediate + - Reality + - Scene + - VR + - Virtual + - WebXR + - WebXR API + - WebXR Device API + - XR + - augmented + - display + - rendering + - requestAnimationFrame +translation_of: Web/API/WebXR_Device_API/Rendering +--- +

{{DefaultAPISidebar("WebXR Device API")}}

+ +

WebXR 環境をセットアップし、進行中の XR 環境セッションを表す {{domxref("XRSession")}} を作成したら、レンダリングのためにシーンのフレームを XR デバイスに提供する必要があります。 この記事では、{{domxref("XRSession")}} を使用して各フレームを表す {{domxref("XRFrame")}} オブジェクトを取得し、それを使用して、XR デバイスに配信するためのフレームバッファーを準備し、レンダリングループで XR シーンのフレームをデバイスに駆動するプロセスについて説明します。

+ +

仮想環境をレンダリングする前に、navigator.xr.requestSession() メソッドを使用して {{domxref("XRSession")}} を作成することにより、WebXR セッションを確立する必要があります。 また、セッションをフレームバッファーに関連付けて、他のセットアップタスクを実行する必要もあります。 これらのセットアップタスクについては、WebXR セッションの起動と停止の記事で説明されています。

+ +

レンダラーの準備

+ +

XR セッションをセットアップし、WebGL フレームバッファーを接続し、WebGL をシーンをレンダリングするために必要なデータで準備したら、レンダラーをセットアップして実行を開始できます。 これは、描画する参照空間を取得することから始まります。 その原点と方向は、ビューアーの開始位置と視線方向に設定します。 それが手に入ったら、次にシーンをレンダリングするためにフレームバッファーが必要になったときにブラウザーがあなたのレンダリング関数を呼び出すように要求します。 これは、{{domxref("XRSession")}} メソッドの {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} を呼び出すことによって行います。

+ +

したがって、レンダラーの手はじめは次のようになります。

+ +
let worldRefSpace;
+
+async function runXR(xrSession) {
+  worldRefSpace = await xrSession.requestReferenceSpace("immersive-vr");
+
+  if (worldRefSpace) {
+    viewerRefSpace = worldRefSpace.getOffsetReferenceSpace(
+        new XRRigidTransform(viewerStartPosition, viewerStartOrientation));
+    animationFrameRequestID = xrSession.requestAnimationFrame(myDrawFrame);
+  }
+}
+
+ +

没入型世界の参照空間を取得した後、これは、その位置と方向を表す {{domxref("XRRigidTransform")}} を作成し、{{domxref("XRReferenceSpace")}} のメソッド {{domxref("XRReferenceSpace.getOffsetReferenceSpace", "getOffsetReferenceSpace()")}} を呼び出すことにより、ビューアーの位置と方向を表すオフセット参照空間を作成します。

+ +

次に、{{domxref("XRSession")}} のメソッド {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} を呼び出して最初のアニメーションフレームをスケジュールし、フレームをレンダリングするためのコールバック関数 myDrawFrame() を提供します。

+ +

このコードにはループがないことに注意してください! 代わりに、フレームレンダリングコード(この場合は myDrawFrame() という関数)が、もう一度 requestAnimationFrame() を呼び出して別のフレームを描画する時刻をスケジュールします。

+ +

リフレッシュレートとフレームレート

+ +

画面が最後にリフレッシュされてから {{domxref("XRSession")}} のメソッド {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} を呼び出したとすると、ブラウザーは、アプリまたはサイトウィンドウを再描画する準備ができるたびにフレームレンダリングコールバックを呼び出します。 このコンテキストでは、「再描画」とは、画面に表示されるコンテンツが、DOM およびその中の要素が現時点で提示しようとしているものと一致することを保証するプロセスを意味します。

+ +

ハードウェア垂直リフレッシュレート

+ +

ブラウザーは、WebXR コンテンツが表示されている {{HTMLElement("canvas")}} をリフレッシュする準備ができると、フレームレンダリングコールバックを呼び出します。 このコールバックは、指定されたタイムスタンプと、モデルやテクスチャーなどの他の関連データ、およびアプリケーションの状態を使用して、指定された時刻に表示されるように、シーンを WebGL バックバッファーにレンダリングします。 コールバックが戻ると、ブラウザーは最後に画面がリフレッシュされてから変更されたものと共に、そのバックバッファーをディスプレイまたは XR デバイスに転送します。

+ +

歴史的に、ディスプレイは毎秒 60 回リフレッシュされています。 これは、タイミングを合わせるために、米国では 1 秒あたり 60 回(ヨーロッパでは 50 回)循環する AC 配電網の電流フロー波形を使用した初期のディスプレイによるものです。 次のように、このことはいくつかの異なる名前で示されていますが、それらはすべて同等またはほぼ同じです。

+ + + +

他にも同様の用語が使用されていますが、それが何と呼ばれるかに関係なく、適用される測定単位はヘルツ(Hz)です。 1 秒あたり 60 回リフレッシュするディスプレイには、60 Hz のリフレッシュレートがあります。 つまり、1 秒間に表示できるフレームの最大数は 60 です。 それを超える 1 秒あたりのフレーム数に関係なく、1 秒の間に 60 フレームしか画面に表示されません。

+ +

ただし、すべてのディスプレイが 60 Hz で動作するわけではありません。 最近では、より高性能のディスプレイがはるかに高いリフレッシュレートを使用し始めています。 例えば、120 Hz、つまり 120 フレーム/秒のディスプレイは、ますます一般的になっています。 ブラウザーは常にディスプレイと同じレートでリフレッシュを試みます。 つまり、一部のコンピューターでは、コールバックは 1 秒あたり最大 60 回実行されますが、他のコンピューターでは、フレームレートによって異なり 1 秒あたり 90 または 120 回、あるいはそれ以上呼び出される場合があります。

+ +

各フレームのレンダリングに利用できる時間

+ +

これにより、フレーム間で利用可能なほとんどの時間を使用することが重要になります。 ユーザーのデバイスが 60 Hz のディスプレイを使用している場合、コールバックは 1 秒あたり最大 60 回呼び出され、それよりも頻繁に呼び出されることはないので、確実にできることをすることが目標です。 これを実現するには、メインスレッド外で可能な限り実行し、フレームレンダリングのコールバックをできるだけ効率的にします。 以下の図は、時間の 60 Hz ブロックへの分割を示しています。 各ブロックは、少なくとも部分的にシーンのレンダリングに使用されています。

+ +
フレーム期間あたりのレンダラー実行時間
+ +

コンピュータのビジー状態が増すにつれて、コールバックをフレームごとに正確に呼び出すことができなくなり、フレームをスキップしなければならない場合があるため、これは重要です。 これをコマ落ち(dropping frames)と呼びます。 これは、レンダリングが遅延したため、またはレンダリング自体に使用可能な時間よりも長い時間がかかったために、フレームのレンダリングにかかる時間がフレーム間で使用可能な時間を超えると発生します。

+ +
フレーム期間あたりのレンダラー実行時間
+ +

上の図では、フレーム 3 がペイントされる予定になるまでフレーム 2 がレンダリングを完了しなかったため、フレーム 3 はコマ落ちしています。 次に描画されるフレームはフレーム 4 になります。 これは、レンダリングコールバックに渡されるタイムスタンプが役立つもう1つの理由です。 フレーム番号ではなく時間に基づいてシーンを構成することにより、レンダリングされたフレームが遅れることなく、期待したものと一致することを保証できます。

+ +

フレームがコマ落ちすると、影響を受ける表示領域のコンテンツは、フレームループを通過しても変更されません。 そのため、ときどきフレームがコマ落ちすることは通常あまり目立ちませんが、頻繁に発生し始めた場合(特に、非常に短い時間に複数のフレームがコマ落ちした場合)は、不快になり、ディスプレイが使用できなくなる可能性があります。

+ +

幸い、フレーム間で使用できる時間を 1/refreshRate 秒として簡単に計算できます。 つまり、1 をディスプレイのリフレッシュレートで除算します。 結果の値は、フレームがコマ落ちしないようにするために、各フレームをレンダリングするのに使用できる時間です。 例えば、60 Hz のディスプレイでは、1 フレームのレンダリングに 1/60 秒、つまり 0.0166667 秒が使用されます。 また、デバイスのリフレッシュレートが 120 Hz の場合、コマ落ちを避けたい場合、各フレームをレンダリングするのに必要な時間は 0.00883333 秒しかありません。

+ +

ただし、ハードウェアが実際には 120 Hz である場合でも、毎秒 60 回リフレッシュするだけで十分であり、通常はそれをターゲットとすることをお勧めします。 60 FPS はすでに、ほとんどの人がアニメーションが単なる高速の一連の静止画像ではないことを簡単に検出できるポイントを超えています。 つまり、判別がつかないときは、ディスプレイが 60 Hz でリフレッシュされていると想定できます。 コードが適切に記述されている限り、すべてが問題なく動作します。

+ +

レンダラーのパフォーマンスの問題

+ +

明らかに、フレームごとにシーンをレンダリングする時間はほとんどありません。 それだけでなく、レンダラー自体がその時間よりも長く実行されると、フレームがコマ落ちするだけでなく、その時間が完全に無駄になり、他のコードがそのフレームに対してまったく実行されなくなる可能性があります。

+ +

それだけでなく、レンダリングが垂直リフレッシュ境界をまたぐ場合、ティアリング効果(tearing effect)が発生する可能性があります。 ティアリングは、前のフレームがまだ画面に描画されている間にディスプレイハードウェアが次のリフレッシュサイクルを開始すると発生します。 その結果、画面の上部に新しいフレームが表示されますが、フレームの下部には、前のフレームと場合によってはその前のフレームの組み合わせが表示される視覚効果となります。

+ +

したがって、あなたの使命は、利用可能な時間を超過したり、コマ落ちやメインスレッドの過度の悪用を引き起こしたりしないように、コードを十分にタイトかつ軽量に保つことです。

+ +

これらの理由により、レンダラーがかなり小さくて軽量で、ほとんど何もすることがないのでない限り、ブラウザーが他の処理を行う間に次のフレームを計算できるように、できる限りすべてをワーカーにオフロードすることを検討する必要があります。 フレームが実際に呼び出される前に計算とデータを準備するだけで、サイトまたはアプリをより効率的にレンダリングし、メインスレッドのパフォーマンスを向上させ、一般的にユーザーエクスペリエンスを向上させることができます。

+ +

幸い、レンダリングのニーズが特に重い場合は、影響をさらに減らし、パフォーマンスを最適化するために使用できるいくつかのトリックがあります。 WebXR パフォーマンスガイドを参照して、パフォーマンスをできる限り向上させるための推奨事項とヒントを確認してください。

+ +

WebXR フレーム

+ +

フレームレンダリングコールバック関数は、2つのパラメーターを入力として受け取ります。 フレームが対応する時刻と、その時刻のシーンの状態を記述する {{domxref("XRFrame")}} オブジェクトです。

+ +

3D の光学

+ +

私たちが2つの目を持つには理由があります。 2つの目を持つことで、それぞれが本質的にわずかに異なる角度から世界を見ることができます。 それらは既知の固定距離だけ離れているため、私たちの脳は基本的な幾何学と三角法を実行し、その情報から 3D の実在の本質を理解できます。 また、遠近法(perspective)、大きさの違い、さらには通常、3番目の次元の詳細を理解するために物事がどのように見えるかについての理解も利用します。 これらの要因は、とりわけ、私たちの奥行き知覚(depth perception)の源です。

+ +

グラフィックスをレンダリングするときに3次元の幻想を作成するには、これらの要因をできるだけ多くシミュレートする必要があります。 これらをシミュレートするほど、そして正確に行うほど、人間の脳をだまして 3D で画像を知覚させることができます。 XR の利点は、古典的な単眼テクニックを使用して 3D グラフィックス(遠近法、大きさ、シミュレートされた視差)をシミュレートできるだけでなく、アニメーションのフレームごとに、各目につき1回ずつシーンを2回レンダリングすることで、両眼視(つまり、2つの目を使用した視覚)をシミュレートできることです。

+ +

典型的な人間の瞳孔間距離(pupillary distance、瞳孔の中心間の距離)は、54 〜 74 ミリメートル(0.054 〜 0.074 メートル)です。 したがって、ビューアーの頭の中心が [0.0, 2.0, 0.0](水平方向の空間の中心で地上レベルの約 2 メートル)にある場合、まず [-0.032, 2.0, 0.0](中心から左に 32 mm)からシーンをレンダリングし、次に [0.032, 2.0, 0.0](中心から右に 32 mm)で再びレンダリングする必要があります。 このようにして、ビューアーの目の位置を人間の平均瞳孔距離 64 mm に配置します。

+ +

その距離(または XR システムが使用するように構成されている瞳孔間距離)は、網膜歪覚(各網膜の見え方の違い)と視差効果によって脳がオブジェクトまでの距離とオブジェクトの奥行きを計算できるようにするために、私たちの心に十分な違いを見せるのに十分です。 これにより、網膜が 2D 表面にすぎないにもかかわらず、3次元を知覚できるようになります。

+ +

これは下の図に示されています。 下の図では、それぞれの目がビューアーの真正面にあるさいころをどのように認識するかを示しています。 この図では、説明のために一部の点で効果を誇張していますが、概念は同じです。 各目は、境界が目の前の円弧を形成する領域を見ます。 それぞれの目は頭の中心線の片側または反対側にオフセットされ、それぞれの目はほぼ同じ視野を見るので、その結果、それぞれの目は、その前にある世界のわずかに異なる部分を別の角度から見ることができます。

+ +

両眼視のしくみを示す図

+ +

左目はさいころを中央から少し左に見、右目はさいころを中央から少し右に見ます。 その結果、左目はオブジェクトの左側が少しだけ見え、右側が少し見えません。 逆も同様です。 これらの2つの画像は網膜に焦点が合わせられ、結果の信号は視神経を介して後頭葉の後部にある脳の視覚皮質に送信されます。

+ +

脳はこれらの信号を左目と右目から受け取り、ビューアーの脳内に世界の単一の統一された 3D 画像を構成し、その画像を見ます。 また、左目と右目で見られるものの違いにより、脳はオブジェクトの奥行きや大きさなどに関する多くの情報を推測できます。 推測された奥行き情報を、遠近法、影、これらの関係の意味の記憶などの他の手がかりと組み合わせることで、私たちの周りの世界について多くを理解することができます。

+ +

フレーム、ポーズ、ビュー、フレームバッファー

+ +

シーンのある瞬間の状態を表す XRFrame を取得したら、ビューアーを基準にしてシーン内のオブジェクトの位置を決定し、レンダリングできるようにする必要があります。 参照空間に対するビューアーの位置と方向は、{{domxref("XRFrame")}} のメソッド {{domxref("XRFrame.getViewerPose", "getViewerPose()")}} を呼び出して取得した {{domxref("XRViewerPose")}} で表されます。

+ +

XRFrame は、あなたの世界内のオブジェクトの位置または方向を直接追跡しません。 代わりに、位置と方向をシーンの座標系に変換する方法を提供し、ビューアーの位置と方向のデータを XR ハードウェアから収集し、あなたが構成した参照空間に変換して、フレームレンダリングコードにタイムスタンプ付きで配信します。 そのタイムスタンプとあなた独自のデータを使用して、シーンのレンダリング方法を決定します。

+ +

シーンを2回レンダリングした後(フレームバッファーの左半分に1回、フレームバッファーの右半分に1回)、フレームバッファーは XR ハードウェアに送信され、フレームバッファーの各半分が対応する目に表示されます。 これは、多くの場合(常にではありません)、画像を1つの画面に描画し、レンズを使用してその画像の正しい半分を各目に転送します。

+ +

3D が WebXR によってどのように表現されるかについて詳しくは、視点とビューアー: WebXR でのカメラのシミュレーションWebXR による 3D の表現をご覧ください。

+ +

シーンを描く

+ +

ブラウザーがシーンの次のフレームをペイントできるようにフレームバッファーを準備するときが来たら、requestAnimationFrame() に指定した関数が呼び出されます。 それは、描画するフレームの時刻と、レンダリングする必要のあるフレームのシーンの状態に関する詳細を提供する {{domxref("XRFrame")}} オブジェクトを入力として受け取ります。

+ +

理想的には、このコードを 60 FPS のフレームレートに十分か可能な限りそれに近い速さを維持することが必要です。 この1つの関数にはあなたのコードだけではないことを思い出してください。 メインスレッドがフレーム自体の持続時間よりもフレームあたりの時間を長く実行する必要がないことを確認する必要があります。

+ +

基本的なレンダラー

+ +

このバージョンの WebXR レンダリングコールバックでは、比較的単純なプロジェクトに最適な非常に単純なアプローチを使用しています。 この疑似コードは、そのプロセスの概要を示しています。

+ +
for each view in the pose's views list:
+  get the WebXR GL layer's viewport
+  set the WebGL viewport to match
+  for each object in the scene
+    bindProgram()
+    bindVertices()
+    bindMatrices()
+    bindUniforms()
+    bindBuffers()
+    bindTextures()
+    drawMyObject()
+
+ +

簡単に言えば、この形式のレンダラーはビュー優先順(view-first order)を使用しています。 すべてのオブジェクトを1つのビューに描画してから、同じオブジェクトのセットを他のビューにレンダリングして、XR デバイスのディスプレイを構成する2つのビューのそれぞれを続けてレンダリングします。 その結果、オブジェクトを描画するために必要なデータの多くは、フレームごとに2回 GPU に送信されるため、多くの複製された作業があります。 ただし、これは既存の WebGL コードの移植を簡略化し、多くの場合、この作業を行うのに十分なほど優れているため、最初にこの方法を見ていきます。

+ +

そのフレームのシーンを構成する次のオブジェクトに進む前に、各オブジェクトを各目に対して1回ずつ、2回続けてレンダリングする(つまり、オブジェクト優先順(object-first order)でレンダリングする)代替アプローチについては、オブジェクト優先順でレンダリングすることによる最適化を参照してください。

+ +

レンダリングコールバックのサンプル

+ +

この基本的なパターンに従う実際のコードを見てみましょう。 上記の例では、この関数に myDrawFrame() という名前を付けたので、ここでは引き続きこれを使用します。

+ +
let lastFrameTime = 0;
+
+function myDrawFrame(currentFrameTime, frame) {
+  let session = frame.session;
+  let viewerPose;
+
+  // 時間が来たらペイントされる次のフレームをスケジュールします。
+
+  animationFrameRequestID = session.requestAnimationFrame(myDrawFrame);
+
+  // ビューアーの位置と方向を表す XRViewerPose を取得します。
+  // 成功した場合、フレームをレンダリングします。
+
+  viewerPose = frame.getViewerPose(viewerRefSpace);
+  if (viewerPose) {
+    let glLayer = session.renderState.baseLayer;
+    gl.bindFrameBuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
+
+    // まず、色と奥行きのフレームバッファーを消去します。
+
+    gl.clearColor(0, 0, 0, 1.0);
+    gl.clearDepth(1.0);
+    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
+
+    // 最後のフレームがレンダリングされてからの経過時間を計算します。
+    // この値を使用して、アニメーションが意図したとおりの速度で実行されるようにします。
+
+    const deltaTime = currentFrameTime - lastFrameTime;
+    lastFrameTime = currentFrameTime;
+
+    // 次に、セッションのビューごとにシーンレンダリングコードを1回呼び出します。
+
+    for (let view of viewerPose.views) {
+      let viewport = glLayer.getViewport(view);
+      gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
+      myDrawSceneIntoView(view, deltaTime);
+    }
+  }
+}
+
+ +

myDrawFrame() 関数は、frame パラメーターで指定された {{domxref("XRFrame")}} オブジェクトから {{domxref("XRSession")}} を取得し、セッションの {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} メソッドを呼び出して、次のフレームのレンダリングをすぐにスケジュールします。 これにより、すぐにキューに入ることが保証され、myDrawFrame() 関数のこの反復で費やされた残りの時間は、次のフレームを描画するタイミングにカウントされます。

+ +

次に、フレームの {{domxref("XRFrame.getViewerPose", "getViewerPose()")}} メソッドを使用して、ビューアーのポーズ(その位置と方向)を表す {{domxref("XRViewerPose")}} オブジェクトを取得し、WebXR セッションのセットアップ中に以前に取得した viewerRefSpace からビューアーの参照空間を渡します。

+ +

ビューアーのポーズを手にすると、フレームのレンダリングを開始できます。 最初のステップは、WebXR デバイスがフレームを描画したいフレームバッファーへのアクセスを取得することです。 これは、セッションの {{domxref("XRSession.renderState", "renderState")}} オブジェクトの {{domxref("XRRenderState.baseLayer", "baseLayer")}} プロパティからターゲット WebGL レイヤーを取得してから、その {{domxref("XRWebGLLayer")}} オブジェクトから {{domxref("XRWebGLLayer.framebuffer", "framebuffer")}} を取得することによって行われます。 次に、gl.bindFrameBuffer() を呼び出して、今後のすべての描画コマンドのターゲットとしてそのフレームバッファーをバインドします。

+ +

次のステップは、フレームバッファーを消去することです。 レンダリングコードがフレームバッファー内のすべてのピクセルを書き込むことが保証されている場合に限り、理論上はこのステップをスキップできますが、パフォーマンスの全てを最後まで出し切る必要がない限り、とにかくすべてのピクセルに触れていることを確実にするために、描画を開始する前にそれをクリアして、描画を開始するのが一般的に最も安全です。 背景色は、gl.clearColor() を使用して完全に不透明な黒に設定します。 奥行きのクリアは、gl.cleardepth() を呼び出して 1.0 に設定します。 これにより、ピクセルが属するオブジェクトがどれだけ離れているかに関係なく、すべてのピクセルがクリアされます。 最後に、フレームのピクセルバッファーと奥行きバッファーは、COLOR_BUFFER_BITDEPTH_BUFFER_BIT の両方を設定したビットマスクを渡して gl.clear() を呼び出して両方とも消去します。

+ +

WebXR はすべてのビューに単一のフレームバッファーを使用し、ビュー上のビューポートはフレームバッファー内の各目の視点を分離するために使用されるため、各目(または他の視点)を個別にクリアするのではなく、単一のフレームバッファーをクリアするだけで済みます。

+ +

次に、前のフレームがレンダリングされてからの経過時間は、currentFrameTime パラメーターで指定された現在の時刻から、最後のフレームがレンダリングされた保存時刻 lastFrameTime を差し引いて計算されます。 結果は、最後のフレームがレンダリングされてから経過したミリ秒数を示す {{domxref("DOMHighResTimeStamp")}} 値です。 シーンの描画中にこの値を使用して、コールバックが一貫したフレームレートで起動されると想定するのではなく、実際の経過時間を考慮して適切な距離ですべてを移動できるようにします。 この経過時間は変数 deltaTime に保存され、lastFrameTime の値はこのフレームの時刻に置き換えられ、次のフレームの差分を計算する準備が整います。

+ +

それでは、実際にそれぞれの目に対してシーンをレンダリングする時が来ました。 ビューアーのポーズの {{domxref("XRViewerPose.views", "views")}} 配列内のビューを反復処理します。 シーンに対する目のパースペクティブ(perspective)を表すこれらの {{domxref("XRView")}} オブジェクトのそれぞれについて、描画を現在の目の可視画像を表すフレームバッファーの領域に制限することから始める必要があります。

+ +

{{domxref("XRWebGLLayer")}} のメソッド {{domxref("XRWebGLLayer.getViewport", "getViewport()")}} を呼び出して、現在の目の画像用に予約されているフレームバッファー内の領域に描画を制限するビューポートを取得することにより、目のコンテンツをレンダリングする WebGL を準備することから始めます。 次に、ビューポートの X 原点と Y 原点を、幅と高さとともに gl.viewport() に渡して、WebGL ビューポートを一致するように設定します。

+ +

最後に、メソッド myDrawSceneIntoView() を呼び出して、実際に WebGL を使用してシーンをレンダリングします。 これには、描画する目を表す {{domxref("XRView")}}(透視マッピング(perspective mapping)などを実行するため)と deltaTime を渡します。 これにより、シーン描画コードは、時間とともに移動するオブジェクトの位置を決定するときに経過時間を正確に表すことができます。

+ +

ビューを反復するループが終了すると、ビューアーにシーンを提示するために必要なすべての画像がレンダリングされ、戻ると、フレームバッファーは GPU を経由して、最終的には XR デバイスのディスプレイに到達します。 関数の上部で {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} を呼び出してあるので、シーンのアニメーションの次のフレームをレンダリングするときに、コールバックがもう一度呼び出されます。

+ +

このアプローチの欠点

+ +

この関数に費やす時間をできるだけ最小限に抑えることが重要であるため、状態変化の処理に費やす時間が長いほど、実際に描画する時間が短くなります。 このテクニックは少数のオブジェクトに対して非常にうまく機能しますが、各オブジェクトのすべてのデータを2回(左目に対して1回、右目に対して1回)再バインドする必要があるため、状態の調整、バッファーとテクスチャーのアップロードなどに、多くの時間を費やしています。 次のセクションでは、これらの状態の変化を大幅に減らし、特にオブジェクト数が増えるにつれて、はるかに高速なレンダリングアプローチを提供する、変更されたアプローチについて説明します。

+ +

オブジェクト優先でレンダリングすることによる最適化

+ +

単一の WebGL フレームバッファーを使用して、左目と右目の両方のビューを単一のフレームバッファーに含めるという WebXR のアプローチの利点は、処理の順序を再配置することにより、レンダリングパフォーマンスを大幅に向上できることです。 特定のビュー(左目など)のビューポートを設定し、左目で見えるすべてのオブジェクトを1つずつレンダリングし、各オブジェクトに行ったらバッファーを再構成する代わりに、各オブジェクトをそれぞれの目に1回ずつ、2回続けてレンダリングします。 したがって、両方の目に対してバッファー、ユニフォームなどを1回セットアップするだけで済みます。

+ +

結果の疑似コードは次のようになります。

+ +
for each object in the scene
+  bindProgram()
+  bindUniforms()
+  bindBuffers()
+  bindTextures()
+  for each view in the pose's views list
+    get the XRWebGLLayer's viewport
+    set the WebGL viewport to match
+    bindVertices()
+    bindMatrices()
+    drawMyObject()
+
+ +

このように変更することにより、プログラム、ユニフォーム、バッファー、テクスチャー、その他の可能性のあるものだけを、シーン内の各オブジェクトに対して2回ではなく、フレームごとに1回だけバインドします。 これにより、潜在的に非常に大きなマージンでオーバーヘッドが削減されます。

+ +

フレームレートの制限

+ +

他のコードを実行するためにより多くの時間を確保しながら、維持しようとするベースラインのフレームレートを確立するために、意図的にフレームレートを制限する必要がある場合は、フレームを意図的に定期的にスキップすることができます。

+ +

例えば、フレームレートを 50% 下げるには、1 フレームおきにスキップします。

+ +
let tick = 0;
+
+function drawFrame(time, frame) {
+  animationFrameRequestID = frame.session.requestAnimationFrame(drawFrame);
+
+  if (!(tick % 2)) {
+    /* シーンを描く */
+  }
+  tick++;
+}
+ +

このバージョンのレンダリングコールバックは、tick カウンターを維持します。 tick が偶数の値である場合にのみ、フレームがレンダリングされます。 このようにして、ひとつおきのフレームのみをレンダリングします。

+ +

同様に、!(tick % 4) を使用して、4 フレームごとにレンダリングする等々ができます。

+ +

アニメーションを経過時間に合わせる

+ +

レンダリングコールバックは、正当な理由で time パラメータを受け取ります。 この {{domxref("DOMHighResTimeStamp")}} 値は、フレームのレンダリングがスケジュールされた時刻を示す浮動小数点値です。 コールバックの実行は正確に 1/60 秒間隔で発生しないため — そして実際、ユーザーのディスプレイのフレームレートが異なる場合、他のレートで発生する可能性があるため — コードが実行されているという単純な事実に頼って、最後のフレームから 1/60 秒であると想定することはできません。

+ +

そのため、アニメーションが目的の速度で正確にレンダリングされるように、提供されているタイムスタンプを使用する必要があります。 これを行うには、最初に行う必要があるのは、最後のフレームがレンダリングされてから経過した時間を計算することです。

+ +
let lastFrameTime = 0;
+
+function drawFrame(time, frame) {
+  /* ... 次のフレームのスケジュール、バッファーの準備など ... */
+
+  const deltaTime = (time - lastFrameTime) * 0.001;
+  lastFrameTime = time;
+
+  for (let view of pose.views) {
+    /* 各ビューのレンダリング */
+  }
+}
+
+ +

これは、前のフレームのレンダリング時間を含む lastFrameTime と呼ばれるグローバル(またはオブジェクトプロパティ)を維持します。 この場合、時間の値はミリ秒単位で格納されるため、0.001 を掛けて時間を秒に変換します。 場合によっては、これにより後で時間を節約できます。 他の状況では、ミリ秒単位の時間が必要なため、何も変更する必要はありません。

+ +

経過時間を手に入れれば、レンダリングコードは、すべての移動オブジェクトが経過時間内にどれだけ移動したかを計算する手段を持ちます。 例えば、オブジェクトが回転している場合、次のように回転を適用できます。

+ +
const xDeltaRotation = (xRotationDegreesPerSecond * RADIANS_PER_DEGREE) * deltaTime;
+const yDeltaRotation = (yRotationDegreesPerSecond * RADIANS_PER_DEGREE) * deltaTime;
+const zDeltaRotation = (zRotationDegreesPerSecond * RADIANS_PER_DEGREE) * deltaTime;
+
+ +

これは、フレームが最後に描画されてからオブジェクトが3つの軸のそれぞれを中心に回転した量を計算します。 これがないと、経過時間に関係なく、シェイプはフレームごとに指定された量だけ回転します。 これにより、多くの場合、かなりのつっかえが発生します。

+ +

単に回転するのではなく、移動するオブジェクトに適用される同様の概念では、次のようになります。

+ +
const xDistanceMoved = xSpeedPerSecond * deltaTime;
+const yDistanceMoved = ySpeedPerSecond * deltaTime;
+const ZDistanceMoved = zSpeedPerSecond * deltaTime;
+
+ +

xSpeedPerSecondySpeedPerSecondzSpeedPerSecond は、それぞれのオブジェクトの速度の軸の成分を含みます。 つまり、[xDistanceMoved, yDistanceMoved, zDistanceMoved] は、オブジェクトの速度を表すベクトルです。

+ + + +

もちろん、レンダラーを通過するたびに発生する可能性のある他のこともあります。 最も一般的な2つは、ユーザー入力の処理と、シーン内のオブジェクトのユーザー制御状態やアニメーションパスなどの既知の要因に基づいて、オブジェクト(またはビューアー)の位置を更新することです。

+ +

ユーザー制御入力の処理

+ +

WebXR アプリケーションの使用中にユーザーが入力を提供する方法は3つあります。 まず、WebXR は、XR ハードウェア自体に統合されているコントローラーからの入力の直接処理をサポートしています。 これらの入力ソースには、ハンドコントローラー、光学追跡システム、加速度計と磁力計などのデバイス、およびそのような他のデバイスが含まれます。

+ +

2番目のタイプの入力は、XR システムを介して接続されたゲームパッドです。 これは、Gamepad API から継承されたインターフェイスを使用しますが、WebXR を介してそれらを操作します。

+ +

3番目の最後のタイプの入力は、キーボード、マウス、トラックパッド、タッチスクリーン、非 XR ゲームパッドおよびジョイスティックなどの従来の非 XR 入力デバイスです。

+ +

XR ハードウェアから直接収集できる方向と位置の情報は、自動的に適用されます。 したがって、自分で処理する必要があるのは他の種類の入力です。

+ + + +

WebXR を使用してシーンを表示する際にユーザー入力を処理する方法の詳細については、入力と入力ソースの記事を参照してください。

+ +

オブジェクトの位置の更新

+ +

ほとんどの(すべてではありませんが)シーンには、何らかの形のアニメーションが含まれています。 アニメーションでは、物事が適切に動き、互いに反応します。

+ +

例えば、仮想現実や拡張現実のゲームでは、敵の非プレイヤーキャラクター(NPC)がコンピューターに制御され、シーン内を移動する場合があります。 時間の経過とともに世界での位置が変化するだけでなく、各 NPC には相互に関連して移動するボディパーツまたはコンポーネントがある可能性があります。 クリーチャーが歩くと腕と足が揺れ、頭が素早く上下したり回転し、髪が跳ねたり揺れたりし、キャラクターが呼吸すると胴体は拡張収縮します。

+ +

さらに、動いている物体や構造物があるかもしれません。 スポーツゲームでは、空中で弧を描くボールがあり、その動きをシミュレートする必要があります。 レーシングゲームでは、車やその他の乗り物があり、車輪を含めてアニメーションする可動部品があります。 シーンに水がある場合、波紋または波がリアルに見えるようにする必要があります。 (一部のタイプのゲームの場合)ドア、壁、床など、構造の一部が動いている場合があります。

+ +

モーションのもう1つの一般的なソースは、プレイヤー自身です。 コントロールからの入力を解釈した後(XR 所属とそれ以外の両方)、ユーザーの動きをシミュレートするために、それらの変更をシーンに適用する必要があります。 詳細とこれがどのように機能するかの完全な例については、移動、向き、モーションの記事を参照してください。

+ +

次のステップ

+ +

レンダラーを作成したら — または、完成していなくても機能するものがあれば — カメラとそのシーン全体の動きを処理することができます。 これについては、WebXR の視点とビューアーに関する記事で説明しています。

+ +

関連情報

+ + diff --git a/files/ja/web/api/webxr_device_api/startup_and_shutdown/index.html b/files/ja/web/api/webxr_device_api/startup_and_shutdown/index.html new file mode 100644 index 0000000000..d4ec89c57d --- /dev/null +++ b/files/ja/web/api/webxr_device_api/startup_and_shutdown/index.html @@ -0,0 +1,382 @@ +--- +title: WebXR セッションの起動と停止 +slug: Web/API/WebXR_Device_API/Startup_and_shutdown +tags: + - 3D + - API + - AR + - Beginner + - Guide + - Initialization + - Mixed + - Preparation + - Reality + - Setup + - Shutdown + - Startup + - VR + - Virtual + - WebXR + - WebXR API + - WebXR Device API + - XR + - augmented +translation_of: Web/API/WebXR_Device_API/Startup_and_shutdown +--- +

{{DefaultAPISidebar("WebXR Device API")}}{{SecureContext_header}}

+ +

すでに 3D グラフィックス全般、特に WebGL に精通していると想定すると、次の大胆なステップで複合現実を実現できます。 現実の世界に加えて、またはその代わりに人工の風景やオブジェクトを表示するという考え方は、それほど複雑ではありません。 拡張現実または仮想現実のシナリオのレンダリングを開始する前に、WebXR セッションを作成してセットアップする必要があります。 また、適切に停止する方法も知っておく必要があります。 この記事では、これらのことを行う方法を学びます。

+ +

WebXR API へのアクセス

+ +

アプリによる WebXR API へのアクセスは、{{domxref("XRSystem")}} オブジェクトから始まります。 このオブジェクトは、ユーザーの機器で利用可能なハードウェアとドライバーを通じて利用可能な WebXR デバイススイート全体を表します。 {{domxref("Navigator")}} のプロパティ {{domxref("Navigator.xr", "xr")}} を介してドキュメントで使用できるグローバルな XRSystem オブジェクトがあります。 これは、使用可能なハードウェアとドキュメントの環境を考慮して、適切な XR ハードウェアが使用できる場合に XRSystem オブジェクトを返すプロパティです。

+ +

したがって、XRSystem オブジェクトをフェッチする最も単純なコードは次のとおりです。

+ +
const xr = navigator.xr;
+ +

WebXR が利用できない場合、xr の値は null または undefined になります。

+ +

WebXR の可用性

+ +

新しい、まだ開発中の API として、WebXR のサポートは特定のデバイスとブラウザーに限定されています。 そして、それらでさえ、デフォルトで有効にならないかもしれません。 ただし、互換性のあるシステムがない場合でも、WebXR を試すことができる選択肢がある場合があります。

+ +

WebXR ポリフィル

+ +

WebXR 仕様を設計しているチームは、WebXR API をサポートしていないブラウザーで WebXR をシミュレートするために使用できる WebXR ポリフィルを公開しています。 ブラウザーが古い WebVR API をサポートしている場合は、それが使用されます。 それ以外の場合、ポリフィルは、Google の Cardboard VR API を使用する実装にフォールバックします。

+ +

ポリフィルは仕様とともに維持され、仕様に合わせて最新に保たれます。 さらに、WebXR および WebXR に関連するその他のテクノロジーのサポートと、ポリフィルの変更の実装の経時的なサポートとして、ブラウザーとの互換性を維持するために更新されます。

+ +

必ず readme を注意深く読んでください。 ポリフィルには、ターゲットブラウザーに含まれる新しい JavaScript 機能との互換性の程度に応じて、いくつかのバージョンがあります。

+ +

WebXR API エミュレーター拡張機能

+ +

Mozilla WebXR チームは、WebXR API をエミュレートし、HTC Vive、Oculus Go、Oculus Quest、Samsung Gear、Google Cardboard などの互換性のあるさまざまなデバイスをシミュレートする、Firefox と Chrome の両方と互換性のある WebXR API Emulator ブラウザー拡張機能を作成しました。 拡張機能を配置すると、ヘッドセットと任意のハンドコントローラーの位置と向き、およびコントローラーのボタンを制御できる開発者ツールパネルを開くことができます。

+ +
エミュレーターの使用
+ +

実際のヘッドセットを使用するのに比べて少し厄介ですが、これにより、WebXR が通常利用できないデスクトップコンピューターで WebXR のコードを試して開発することができます。 また、コードを実際のデバイスに取り込む前に、いくつかの基本的なテストを実行できます。 ただし、エミュレーターはまだすべての WebXR API を完全にエミュレートしていないため、予期しない問題が発生する可能性があることに注意してください。 ここでも、readme ファイルを注意深く読み、開始する前に制限事項を確認してください。

+ +

重要: 製品をリリースまたは出荷する前に、常に実際の AR や VR のハードウェアでコードをテストする必要があります。 エミュレート、シミュレーション、またはポリフィルされた環境は、物理デバイスでの実際のテストに代わる適切なものではありません。

+ +
拡張機能の取得
+ +

以下のサポートされているブラウザー用の WebXR API エミュレーターをダウンロードしてください。

+ + + +

拡張機能のソースコードは、GitHub で入手できます。

+ +
エミュレーターの問題とメモ
+ +

これは拡張機能に関する完全な記事の場所ではありませんが、言及する価値のある特定の事項がいくつかあります。

+ +

拡張機能のバージョン 0.4.0 は2020年3月26日に発表されました。 安定状態に近づいている WebXR AR モジュールによる拡張現実(AR)のサポートが導入されました。 AR のドキュメントは、近日中に MDN で公開されます。

+ +

その他の改善には、エミュレーターを更新して XR インターフェイスの名前を {{domxref("XRSystem")}} に変更し、スクイーズ(グリップ)入力ソースのサポートを導入し、{{domxref("XRInputSource")}} のプロパティ {{domxref("XRInputSource.profiles", "profiles")}} のサポートを追加します。

+ +

コンテキスト要件

+ +

WebXR 互換環境は、安全にロードされたドキュメントから始まります。 ドキュメントは、ローカルドライブ(http://localhost/... などの URL を使用するなど)からロードするか、ページのロード時に {{Glossary("HTTPS")}} を使用する必要があります。 同様に、JavaScript コードは安全にロードされている必要があります。

+ +

ドキュメントが安全にロードされなかった場合は、それほど遠くまで到達できません。 {{domxref("navigator.xr")}} プロパティは、ドキュメントが安全にロードされていない場合には存在しません。 これは、互換性のある XR ハードウェアが利用できない場合にも当てはまります。 どちらの場合でも、xr プロパティの欠如に備える必要があり、エラーを適切に処理するか、何らかの形式のフォールバックを提供する必要があります。

+ +

WebXR ポリフィルにフォールバック

+ +

フォールバックの選択肢の1つは、WebXR 標準化プロセスを担当する Immersive Web ワーキンググループによって提供される WebXR ポリフィルです。 {{Glossary("polyfill","ポリフィル")}}は、WebXR をネイティブでサポートしていないブラウザーに WebXR のサポートを提供し、サポートしているブラウザーの実装間の不整合を解消するため、WebXR がネイティブで利用できる場合でも役立つ場合があります。

+ +

ここでは、前の {{HTMLElement("script")}} タグを使用してポリフィルが含まれている、またはロードされていると想定して、オプションでポリフィルをインストールした後に {{domxref("XRSystem")}} オブジェクトを返す getXR() 関数を定義します。

+ +
let webxrPolyfill = null;
+
+function getXR(usePolyfill) {
+  let tempXR;
+
+  switch(usePolyfill) {
+    case "if-needed":
+      tempXR = navigator.xr;
+      if (!tempXR) {
+        webxrPolyfill = new WebXRPolyfill();
+        tempXR = webxrPolyfill;
+      }
+      break;
+    case "yes":
+      webxrPolyfill = new WebXRPolyfill();
+      tempXR = webxrPolyfill;
+      break;
+    case "no":
+    default:
+      tempXR = navigator.xr;
+      break;
+  }
+
+  return tempXR;
+}
+
+const xr = getXR("no");  // ネイティブの XRSystem オブジェクトを取得
+const xr = getXR("yes"); // 常にポリフィルから XRSystem を返す
+const xr = getXR("if-needed"); // navigator.xr がない場合にのみポリフィルを使用
+
+ +

返された XRSystem オブジェクトは、MDN で提供されているドキュメントに従って使用できます。 グローバル変数 webxrPolyfill は、ポリフィルへの参照を保持するためにのみ使用され、不要になるまでポリフィルを使用できるようにします。 これを null に設定すると、依存しているオブジェクトがそれを使用しなくなったときに、ポリフィルをガベージコレクションできることを示します。

+ +

もちろん、必要に応じてこれを簡略化できます。 アプリはおそらくポリフィルを使用するかどうかについてあまり行き来しないので、これを必要な特定のケースに単純化できます。

+ +

権限とセキュリティ

+ +

WebXR を中心に展開する多くのセキュリティ対策があります。 まず、ユーザーの世界観を完全に置き換える immersive-vr モードを使用するには、xr-spatial-tracking 機能ポリシーを設定する必要があります。 それに加えて、ドキュメントは安全で現在フォーカスされている必要があります。 最後に、{{domxref("Element.click_event", "click")}} イベントのハンドラーなどのユーザーイベントハンドラーから {{domxref("XRSystem.requestSession", "requestSession()")}} を呼び出す必要があります。

+ +

安全な WebXR の活動と使用方法の詳細については、WebXR の権限とセキュリティの記事を参照してください。

+ +

必要なセッションタイプが利用可能であることの確認

+ +

新しい WebXR セッションを作成する前に、ユーザーのハードウェアとソフトウェアが使用したいプレゼンテーションモードをサポートしているかどうかを最初に確認するのがしばしば賢明です。 これは、たとえば、没入型プレゼンテーションとインラインプレゼンテーションのどちらを使用するかを決定するためにも使用できます。

+ +

特定のモードがサポートされているかどうかを確認するには、{{domxref("XRSystem")}} のメソッド {{domxref("XRSystem.isSessionSupported", "isSessionSupported()")}} を呼び出します。 これは、指定されたタイプのセッションが使用できる場合は true、そうでない場合は false に解決される promise を返します。

+ +
const immersiveOK = await navigator.xr.isSessionSupported("immersive-vr");
+if (immersiveOK) {
+  // 没入型 VR セッションを作成して使用する
+} else {
+  // 代わりにインラインセッションを作成するか、
+  // インラインが必要な場合は非互換性についてユーザーに伝えます
+}
+
+ +

セッションの作成と開始

+ +

WebXR セッションは {{domxref("XRSession")}} オブジェクトによって表されます。 XRSession を取得するには、{{domxref("XRSystem")}} の {{domxref("XRSystem.requestSession", "requestSession()")}} メソッドを呼び出します。 このメソッドは、XRSession を正常に確立できる場合に XRSession で解決する promise を返します。 基本的には、次のようになります。

+ +
xr.requestSession("immersive-vr").then((session) => {
+  xrSession = session;
+  /* セッションのセットアップを続行します */
+});
+
+ +

このコードスニペットの requestSession() に渡されるパラメーター immersive-vr に注意してください。 この文字列は、確立する WebXR セッションのタイプを指定します。 この場合は、完全に没入型の仮想現実体験です。 次の3つの選択肢があります。

+ +
+
immersive-vr
+
ヘッドセットまたは同様のデバイスを使用した、完全に没入型の仮想現実セッション。 ユーザーの周りの世界をあなたが提示する画像で完全に置き換えます。
+
immersive-ar
+
ヘッドセットまたは類似の装置を使用して画像が現実世界に追加される拡張現実セッション。 AR 仕様は流動的であるため、このオプションはまだ広くサポートされていません。
+
inline
+
ドキュメントウィンドウのコンテキスト内での XR 画像の画面表示。
+
+ +

機能ポリシーがその使用を禁止したり、ユーザーがヘッドセットの使用許可を拒否したりするなど、何らかの理由でセッションを作成できなかった場合、promise は拒否されます。 したがって、起動して WebXR セッションを返すより完全な関数は次のようになります。

+ +
async function createImmersiveSession(xr) {
+  try {
+    session = await xr.requestSession("immersive-vr");
+    return session;
+  } catch(error) {
+    throw error;
+  }
+}
+
+ +

この関数は、新しい {{domxref("XRSession")}} を返すか、セッションの作成中にエラーが発生した場合に例外をスローします。

+ +

セッションのカスタマイズ

+ +

表示モードに加えて、{{domxref("XRSystem.requestSession", "requestSession()")}} メソッドは、セッションをカスタマイズするための初期化パラメーターを持つオプションのオブジェクトを取ります。 現在、セッションの構成可能な唯一の側面は、世界の座標系を表すためにどの参照空間を使用する必要があるかです。 必要な参照空間または使用したい参照空間と互換性のあるセッションを取得するために、必須またはオプションの参照空間を指定できます。

+ +

たとえば、無制限(unbounded)の参照空間が必要な場合は、取得するセッションで無制限の空間を使用できるようにするために、それを必須機能として指定できます。

+ +
async function createImmersiveSession(xr) {
+  try {
+    session = await xr.requestSession("immersive-vr", {
+      requiredFeatures: [ "unbounded" ]
+    });
+    return session;
+  } catch(error) {
+    throw error;
+  }
+}
+
+ +

一方、インラインセッションが必要で、ローカル(local)参照空間を使用する場合は、次のようにします。

+ +
async function createInlineSession(xr) {
+  try {
+    session = await xr.requestSession("inline", {
+      optionalFeatures: [ "local" ]
+    });
+    return session;
+  } catch(error) {
+    throw error;
+  }
+}
+
+ +

この createInlineSession() 関数は、ローカル参照空間と互換性のあるインラインセッションを作成しようとします。 参照空間を作成する準備ができたら、ローカル空間を試すことができます。 それが失敗した場合は、すべてのデバイスがサポートする必要があるビューアー(viewer)参照空間にフォールバックします。

+ +

新しいセッションを使用するための準備

+ +

{{domxref("XRSystem.requestSession", "requestSession()")}} メソッドが返した promise が正常に解決されると、使用可能な WebXR セッションが手中にあることがわかります。 次に、セッションを使用できるように準備し、アニメーションを開始できます。

+ +

セッションの構成を完了するために必要な(または必要になる可能性のある)主なことは、次のとおりです。

+ + + +

基本的な形式では、この最終的なセットアップを行うコードは次のようになります。

+ +
async function runSession(session) {
+  let worldData;
+
+  session.addEventListener("end", onSessionEnd);
+
+  let canvas = document.querySelector("canvas");
+  gl = canvas.getContext("webgl", { xrCompatible: true });
+
+  // WebGL データなどを設定する
+
+  worldData = loadGLPrograms(session, "worlddata.xml");
+  if (!worldData) {
+    return NULL;
+  }
+
+  // WebGL の構成を完了する
+
+  worldData.session.updateRenderState({
+    baseLayer: new XRWebGLLayer(worldData.session, gl)
+  });
+
+  // シーンのレンダリングを開始します
+
+  referenceSpace = await worldData.session.requestReferenceSpace("unbounded");
+  worldData.referenceSpace = referenceSpace.getOffsetReferenceSpace(
+        new XRRigidTransform(worldData.playerSpawnPosition, worldData.playerSpawnOrientation));
+  worldData.animationFrameRequestID = worldData.session.requestAnimationFrame(onDrawFrame);
+
+  return worldData;
+}
+
+ +

この例では、worldData という名前のオブジェクトを作成して、その世界とレンダリング環境に関するデータをカプセル化します。 これには、{{domxref("XRSession")}} 自体、WebGL でシーンをレンダリングするために使用されるすべてのデータ、その世界の参照空間、および {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} によって返される ID が含まれます。

+ +

最初に、{{domxref("XRSession.end_event", "end")}} イベントのハンドラーが設定されます。 次に、レンダリングするキャンバスを取得し、その WebGL コンテキストへの参照を取得して、{{domxref("HTMLCanvasElement.getContext", "getContext()")}} を呼び出すときに xrCompatible オプションを指定します。

+ +

次に、WebGL レンダラーに必要なデータとセットアップが実行されてから、WebGL が独自のフレームバッファーとして WebGL コンテキストのフレームバッファーを使用するように構成されます。 これは、{{domxref("XRSession")}} のメソッド {{domxref("XRSession.updateRenderState", "updateRenderState()")}} を使用して行われ、レンダリング状態の {{domxref("XRRenderState.baseLayer", "baseLayer")}} を、WebGL コンテキストをカプセル化する新しく作成された {{domxref("XRWebGLLayer")}} に設定します。

+ +

シーンをレンダリングする準備

+ +

この時点で、XRSession 自体が完全に構成されているため、レンダリングを開始できます。 まず、その世界の座標が記述される参照空間が必要です。 XRSession の {{domxref("XRSession.requestReferenceSpace", "requestReferenceSpace()")}} メソッドを呼び出すことにより、セッションの初期参照空間を取得できます。 requestReferenceSpace() を呼び出すときに、必要な参照空間のタイプの名前を指定します。 この場合、unbounded です。 ニーズに応じて、local または viewer を簡単に指定できます。

+ +
+

ニーズに合った適切な参照空間を選択する方法を理解するには、WebXR の幾何学と参照空間参照空間タイプの選択を参照してください。

+
+ +

requestReferenceSpace() によって返される参照空間は、原点 (0, 0, 0) を空間の中心に配置します。 これは素晴らしいことです — プレイヤーの視点が世界の正確な中心から始まる場合は。 しかし、ほとんどの場合、そうではありません。 その場合は、最初の参照空間で {{domxref("XRReferenceSpace.getOffsetReferenceSpace", "getOffsetReferenceSpace()")}} を呼び出して、(0, 0, 0) がビューアーの位置に配置されるように座標系をオフセットし、同様に顔を望ましい方向にシフトする新しい参照空間を作成します。 getOffsetReferenceSpace() への入力値は、デフォルトの世界座標で指定されたプレーヤーの位置と方向をカプセル化する {{domxref("XRRigidTransform")}} です。

+ +

新しい参照空間が手中にあり、保管するために worldData オブジェクトに格納された状態で、セッションの {{domxref("XRSession.requestAnimationFrame", "requestAnimationFrame()")}} メソッドを呼び出して、WebXR セッションのアニメーションの次のフレームをレンダリングするときにコールバックが実行されるようにスケジュールします。 戻り値は、必要に応じて後でリクエストをキャンセルするために使用できる ID であるため、worldData にも保存します。

+ +

最後に、worldData オブジェクトが呼び出し元に返され、メインコードが後で必要なデータを参照できるようになります。 この時点で、セットアッププロセスが完了し、アプリケーションのレンダリング段階に入りました。 レンダリングの詳細については、レンダリングと WebXR フレームアニメーションコールバックを参照してください。

+ +

運用の詳細について

+ +

明らかに、これはほんの一例です。 すべてを保存するために worldData オブジェクトは必要ありません。 あなたが好きな方法で維持するために必要な情報を保存できます。 別の情報が必要になったり、別の特定の要件が発生したりして、それはあなたが別の方法で、または別の順序で物事を行う原因となります。

+ +

同様に、モデルやその他の情報を読み込んだり、WebGL データ(テクスチャ、頂点バッファー、シェーダーなど)を設定したりするために使用する特定の方法は、ニーズや使用しているフレームワークの状況などによって大きく異なります。

+ +

重要なセッション維持イベント

+ +

WebXR セッションの過程で、セッションの状態の変化を示す、またはセッションを適切に動作させ続けるために必要なことを通知するいくつかのイベントのいずれかを受け取る場合があります。

+ +

セッションの可視状態の変化の検出

+ +

XRSession の可視性の状態が変化すると(セッションが非表示または表示されたとき、またはユーザーが別のコンテキストにフォーカスしたときなど)、セッションは {{domxref("XRSession.visibilitychange_event", "visibilitychange")}} イベントを受け取ります。

+ +
session.onvisibilitychange = (event) => {
+  switch(event.session.visibilityState) {
+    case "hidden":
+      myFrameRate = 10;
+      break;
+    case "blurred-visible":
+      myFrameRate = 30;
+      break;
+    case "visible":
+    default:
+      myFrameRate = 60;
+      break;
+  }
+};
+ +

この例では、可視性の状態に応じて変数 myFrameRate を変更します。 おそらく、レンダラーはこの値を使用して、アニメーションループの進行に応じて新しいフレームをレンダリングする頻度を計算します。 したがって、シーンの「ぼかし(blurred)」が多くなるほどレンダリングの頻度は低くなります。

+ +

参照空間のリセットの検出

+ +

時折、ユーザーの世界での位置を追跡しているときに、ネイティブの原点に不連続またはジャンプが発生することがあります。 これが発生する最も一般的なシナリオは、ユーザーが XR デバイスの再キャリブレーションを要求したとき、または XR ハードウェアから受信した追跡データの流れに一時的な障害が発生したときです。 これらの状況により、ネイティブの原点は、ネイティブの原点をユーザーの位置と向きに合わせるために必要な距離と方向の角度で突然ジャンプします。

+ +

これが発生すると、{{domxref("XRReferenceSpace.reset_event", "reset")}} イベントがセッションの {{domxref("XRReferenceSpace")}} に送信されます。 イベントの {{domxref("XRReferenceSpaceEvent.transform", "transform")}} プロパティは、ネイティブの原点を再調整するために必要な変換を詳述する {{domxref("XRRigidTransform")}} です。

+ +
+

reset イベントは {{domxref("XRSession")}} ではなく {{domxref("XRReferenceSpace")}} で発生することに注意してください。

+
+ +

reset イベントのもう1つの一般的な原因は、制限付き参照空間({{domxref("XRReferenceSpaceType")}} が bounded-floor である参照空間)が、{{domxref("XRBoundedReferenceSpace")}} のプロパティ {{domxref("XRBoundedReferenceSpace.boundsGeometry", "boundsGeometry")}} の変更によって指定されたジオメトリを持っている場合です。

+ +

参照空間のリセットのより一般的な原因と、詳細およびサンプルコードについては、{{domxref("XRReferenceSpace.reset_event", "reset")}} イベントのドキュメントを参照してください。

+ +

WebXR 入力コントロールの使用可能なセットが変更されたときの検出

+ +

WebXR は、WebXR システムに固有の入力コントロールのリストを保持しています。 これらのデバイスには、ハンドヘルドコントローラー、モーションセンサーカメラ、モーションセンシティブグローブ、その他のフィードバックデバイスなどが含まれます。 ユーザーが WebXR コントローラーデバイスを接続または切断すると、{{domxref("XRSession.inputsourceschange_event", "inputsourceschange")}} イベントが XRSession にディスパッチされます。 これは、デバイスの可用性をユーザーに通知する機会であり、デバイスの入力を監視し始め、構成オプションを提供するか、またはそれを使用するために必要なものを提供します。

+ +

WebXR セッションの終了

+ +

ユーザーの VR または AR セッションが終了に近づくと、セッションは終了します。 {{domxref("XRSession")}} の停止は、ユーザーがボタンをクリックしてセッションを終了したために停止する時であるとセッション自体が判断した場合(ユーザーが XR デバイスをオフにした場合など)、またはアプリケーションが然るべきその他の状況に応じて発生します。

+ +

ここでは、WebXR セッションの停止を要求する方法と、要求によるかどうかにかかわらず、セッションが終了したことを検出する方法の両方について説明します。

+ +

セッションの停止

+ +

終了時に WebXR セッションを完全に停止するには、セッションの {{domxref("XRSession.end", "end()")}} メソッドを呼び出す必要があります。 これは、停止がいつ完了するかを知るために使用できる promise を返します。

+ +
async function shutdownXR(session) {
+  if (session) {
+    await session.end();
+
+    /* この時点で、WebXR は完全に停止しています */
+  }
+}
+
+ +

shutdownXR() が呼び出し元に戻ると、WebXR セッションは完全かつ安全に停止しています。

+ +

リソースの解放など、セッションの終了時に実行する必要がある作業がある場合は、メインコード本体ではなく、{{domxref("XRSession.end_event", "end")}} イベントハンドラーでその作業を実行する必要があります。 このようにして、停止が自動的にトリガーされたか手動でトリガーされたかに関係なく、クリーンアップを処理します。

+ +

セッションが終了したときの検出

+ +

以前に確立したように、{{domxref("XRSession.end", "end()")}} メソッドを呼び出したか、ユーザーがヘッドセットをオフにしたか、XR システムで何らかの解決できないエラーが発生したかなど、{{domxref("XRSession")}} に送信される {{domxref("XRSession.end_event", "end")}} イベントを監視することで、WebXR セッションが終了したことを検出できます。

+ +
session.onend = (event) => {
+  /* セッションが停止しました */
+
+  freeResources();
+};
+ +

ここでは、セッションが終了し、end イベントが受信されると、freeResources() 関数が呼び出され、XR の提示を処理するために以前に割り当てたりロードしたりしたリソースが解放されます。 end イベントハンドラーで freeResources() を呼び出すことにより、ユーザーが停止をトリガーするボタンをクリックしたとき(上記の shutdownXR() 関数を呼び出すことなど)と、エラーまたは何らかの理由でセッションが自動的に終了したときの両方で freeResources() を呼び出します。

+ +

関連情報

+ + -- cgit v1.2.3-54-g00ecf