Three.jsでXRを作ろう

ここでは,Three.jsというライブラリを使って Web上でXRを作る方法 を紹介します.

はじめに

XRとは?

クロスリアリティのことで,VR,AR,MRといった技術の総称 です.
VRは「仮想現実」といって,現実にはない仮想の(現実っぽい)空間です.没入型の3DゲームはVRです.
ARは「拡張現実」といって,現実にVRを重ねることで,現実を拡張するものです.以前流行ったポケモンGoはAR機能を持っています.
MRは「複合現実」といって,現実とVRを融合させたものです.ARはあくまでも現実が主体でそれにVRを加えたものですが,MRはよりVRと現実が融合したものになっています.

ここでは,VR空間をWeb上に作る方法を紹介します.

Three.jsとは?

Webページ内で3DレンダリングができるJavaScriptライブラリ です.
Webページ内でリアルタイムに3Dを描けるので,ユーザの操作に応じて表示を変更できます.
これをうまく使うことで,WebページでありながらXR体験を提供できるようになります.
Webページなので利用者にアプリをインストールさせる必要がなく,URLを教えるだけで良いのでユーザも開発者も楽ですが,ブラウザ上で動くために処理能力はネイティブアプリと比べて大幅に落ちます.
OculusなどのVRゴーグルにも対応できるので,簡単なXRを作って広めるには丁度よいものです.

準備

まずは開発できる環境を整えましょう.
既に済んでいる場合は飛ばして大丈夫です.

Python3をインストールする

Three.jsを使って快適に開発をするには,Webサーバが必要です.
とはいえサーバを用意するのは(人によっては)大変なので,Python3で簡単に手元のPCをWebサーバにしましょう.

Windowsの場合,Pythonの公式ページからインストーラをダウンロードして,インストールするだけです.
このへんのサイト を参考にすると良いです.

Macの場合,最近のものであれば,はじめからPython3が入っています. Python3が入っているだけでPCがサーバになるわけではないので,怖がらずに入れましょう.

エディタの用意

プログラムを書くためのエディタを用意しましょう.
テキストファイルの読み書きができれば何でも良いです.
Windowsにははじめからメモ帳がありますし,他にもそれっぽいものが良ければ VSCode があります.

ターミナルを利用する練習

もし不慣れであれば,ターミナルを使う練習をしておきましょう.
ここで言うターミナルとは,Windowsであれば「コマンドプロンプト」,Macであれば「ターミナル」アプリを指します.
起動するとホームフォルダにいるはずなので,cdコマンドでフォルダを移動できるようになっておきましょう.
少なくとも開発用のフォルダへ行けないと困ります.

基礎の基礎

開発用フォルダの作成

まず,開発に使うフォルダを適当な場所に適当な名前で作りましょう.
あまり深い場所に作ると大変なので,特にこだわりがないならデスクトップに作ると良いです.
また,フォルダの名前は「半角の英数字」にしましょう.
原則として日本語のファイルやフォルダは開発する上で邪魔になります.

基本となるファイルの作成

こちらのサンプルファイル をダウンロードして,展開しましょう.
中に index.html と program.js の2つのファイルがあります.
これら2つのファイルを,開発用フォルダの中に入れましょう.

ページの表示

サンプルファイルを表示してみましょう.
ターミナルを起動し,cdコマンドで開発用フォルダまで移動してから,
python3 -m http.server
を実行して,簡易サーバを起動します.
ターミナルはそのままおいておいて,Webブラウザで
localhost:8000
にアクセスしましょう.
下のような,青いキューブが回転する様子が見えるでしょうか?

index.html について

これはHTMLファイルです.
head内で,styleタグを使って余計な余白をすべて消しています.
また,必要なThree.jsのファイルをWebから読み込む設定もしています.
ファイルの下部で,program.js というファイルを読み込んでいますが,これがプログラムを書くファイルです.

program.js について

ここにプログラムを書いています.
中にコメントをたくさん書いているので,エディタで開いて見てみましょう.

座標系について

Three.jsにおいて,デフォルトでは上がY方向になっています.
つまり,横方向はX方向とZ方向です.
また,単位は m (メートル) です.
program.js でカメラの位置を (0, 1.7, 5) にしていますが, これはX方向に0m,Z方向に5m,Y(高さ)方向に1.7mの位置,ということです.
1.7mは大体の目線の高さの目安です.

動き回れるようにしよう

このままだと決まった場所にしかいないので,マウスで動き回れるようにしましょう.
window.addEventListener("イベント名", 関数);
とすることで,イベントに対して処理(関数)を割り当てられます.
マウスで動く場合,まず というユーザの動きになるはずです.
この動きそれぞれに対応する関数を作っていきます.

…が,その前に,カメラをコンテナ(何でも入る透明の箱)に入れましょう.
カメラ自体を動かすより,カメラを箱にいれて,その箱を動かした方が楽になります.
また,VRゴーグルで利用する場合,カメラ位置はゴーグルの動きで決まるためこちらで制御できません.
箱に入れて箱を動かすことで,VRゴーグル利用時にも移動が可能になります.
コンテナは Object3D() というものを使います.
以下のようにして,カメラコンテナを作り,カメラを入れて,コンテナの位置を調整し,シーンに追加しましょう.
書く場所はカメラを生み出したあとならどこでも良いですが,animate関数の定義より少し前あたりがわかりやすいでしょう.
const cameraContainer = new THREE.Object3D(); // コンテナを生み出す
cameraContainer.add(camera);// カメラをコンテナに入れる
cameraContainer.position.set(0, 0, 10);// コンテナの場所を移動する
scene.add(cameraContainer);// シーンにコンテナを追加
高さ(Y方向)以外の情報はコンテナに持たせるので,上の方にあるカメラの位置と見る場所を,以下のように変更します.
camera.position.set(0, 1.7, 0);// カメラの場所
camera.lookAt(0, 1.7, -1);// カメラの見る場所
次に,マウスボタン押下,マウスドラッグ,マウスボタンを離したときの3つの関数を作ります.
名前は適当に変えても大丈夫です (があとで出てくるときに読み替えてください).
// 前回のマウスの横の位置と,マウスを押しているかどうかのフラグを入れる変数
var pmouseX, mouseFlag;
// マウスボタンを押したときの処理
function moveStartFunc(e) {
  pmouseX = e.offsetX;// 前回のマウスの横の位置を入れておく
  mouseFlag = true;// マウスを押したフラグをたてる
}

// マウスボタンを離したときの処理
function moveEndFunc() {
  mouseFlag = false;// マウスを押したフラグを消す
}

// マウスドラッグ中の処理
function moveFunc(e) {
  if (mouseFlag) {// マウスが押されているなら
    var mouseX = e.offsetX;// 今のマウスの横方向の位置
    cameraContainer.rotation.y -= 0.001*(mouseX - pmouseX);// 前回との差をもとにY軸回りに回転
    pmouseX = mouseX;// 今の横の位置を,前回の横の位置にする (次回使うため).
  }
}
ここでは視点の回転だけを定義していて,移動そのものは記述していません.
一見,マウスドラッグで移動すれば良いように思いますが,マウスをただ押しているだけのとき(ドラッグ判定ではない)に進むためには,mouseFlag が真かどうかで別途判断する必要があります(あとで書きます).

では,それぞれのイベントに関数を登録しましょう.
マウスを押したときは mousedown ,ドラッグ中は mousemove ,離したときは mouseup です.
window.addEventListener("mousedown", moveStartFunc);
window.addEventListener("mouseup", moveEndFunc);
window.addEventListener("mousemove", moveFunc);
では,進むための処理を書きましょう.
animate関数の中に処理を書きます.
animate関数は Processing でいうところの draw 関数のようなものです.
もし mouseFlag が真であれば,カメラコンテナの向きに合わせて進むように,以下のように処理を追加しましょう.
function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  if (mouseFlag) {
    var r = cameraContainer.rotation.y;
    cameraContainer.position.x -= 0.02*Math.sin(r);
    cameraContainer.position.z -= 0.02*Math.cos(r);
  }
}
編集できたら上書き保存して,ブラウザでページを再読み込みしましょう.
以下のような感じで移動できるようになったでしょうか?

部屋を作ろう

いまいち動いている感がありませんね.
もう少し動いている感じを出すために,部屋を作ってみましょう.

部屋の作成

大きな箱を作って部屋にします.
カメラが z = 10 の場所にいるので,z方向の長さは最低でも21mは欲しいところです.
少し余裕をもって,xとz方向の長さは26mにしてみました.
また,高さは6mとします.
箱の原点は箱の中央なので,そのまま (0, 0, 0) の位置に置くと,床は -3m のところにあることになります.
そこで,y方向には3mだけ上にあげることで,床の高さを y = 0 に合わせます.
const room = new THREE.Mesh(
  new THREE.BoxGeometry(26,5,26),
  new THREE.MeshStandardMaterial({
    color: 0xdddddd,
    roughness: 0.1,
    side: THREE.DoubleSide,
  })
);
room.position.set(0, 3, 0);
scene.add(room);

ライトの調整

ライトが強すぎるので真っ白になってしまうので,環境光を少し減らしましょう.
alightを作成している部分の値を下のように変更します.
const alight = new THREE.AmbientLight(0xffffff, 0.2);
また,点光源の強さも大幅に減らします.
plightを作っている部分を以下のように変更しましょう.
const plight = new THREE.PointLight(0xffffff, 1.0);
また,カメラコンテナの移動が遅いので,少し早くしておきます.
(animate関数の中にあるので値を変更しましょう)
cameraContainer.position.x -= 0.05*Math.sin(r);
cameraContainer.position.z -= 0.05*Math.cos(r);
編集できたら上書き保存して,ブラウザでページを再読み込みしましょう.
以下のような感じで部屋の中を移動できるようになったでしょうか? VRゴーグルに対応しないなら,ここまでで完成です.
色々これに追加することで,オリジナルのVR空間を作ってみましょう.
テクスチャを貼ってみたり,マテリアルを変えてみたりしても面白いと思います.

WebXRに対応しよう

これだけだとVRゴーグルで見ても,ブラウザで見ているのと同じように(平面上に)見えるだけで没入感はありません.
そこでWebXRを使ってVRゴーグルに対応しましょう.

importmapの修正

VRButtonというモジュールを読み込むために,index.htmlにある importmap を修正します.
<script type="importmap">
  {
    "imports": {
      "three": "https://unpkg.com/three@0.146.0/build/three.module.js",
      "vrbutton": "https://unpkg.com/three@0.146.0/examples/jsm/webxr/VRButton.js"
    }
  }
</script>
また,program.js の上部を以下のようにして,VRButtonを読み込みます.
import * as THREE from 'three';
import { VRButton } from 'vrbutton';
そして,program.js の下のほうで
var vrbutton = VRButton.createButton(renderer);
document.body.appendChild(vrbutton);
として,VRボタンを作って配置します. また,レンダラの設定でXRモードを有効にします.
// レンダラの作成とサイズ調整
const renderer = new THREE.WebGLRenderer({
  canvas: document.querySelector("#myCanvas")
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.xr.enabled = true;// 追加
最後に,レンダリングの方法を少し変更します. これまでは requestAnimationFrame を使っていましたが,WebXRではsetAnimationLoopを使います.
// お絵かきするループ(processingでいうところのdraw)
function animate() {
  //requestAnimationFrame(animate); // ここをコメントアウト
  renderer.render(scene, camera);
  cube.rotation.x += 0.01;
  cube.rotation.y += 0.01;
  if (mouseFlag) {
    var r = cameraContainer.rotation.y;
    cameraContainer.position.x -= 0.05*Math.sin(r);
    cameraContainer.position.z -= 0.05*Math.cos(r);
  }
}

//animate(); // ここをコメントアウト
renderer.setAnimationLoop(animate);// ここを追加
これでWebXR対応ができました.
Meta Quest2などのVRゴーグルをかぶり,ゴーグルのブラウザを起動して,
PCのIPアドレス:8000
にアクセスすると,手元のPCで見ているのと同じような画面が見えるはずです.
下部にあるWebXRのボタンが押せるので,押して見ると没入感のある感じで楽しめます. ただし,WebXRのモードの際にはマウスイベントがありません.
かわりにコントローラのイベントを取得する必要がありますが,今はそれを書いていないので, 頭を動かした分しか動けないようになっています.