PlayCanvasでMeta Questのブラウザで動くVRのシーンを作ってみよう
WebGLベースのゲームエンジンPlayCanvasでWebXR Device APIを利用したVRのシーンを作る方法を紹介します。今回作るプロジェクトが動作する端末は2022年2月22日現在はAndroidのスマートフォンでの3DoFおよびQuestブラウザ / Steam VR上での6DoFの動作を確認しています。
完成シーン
この記事で作成プロジェクトはこちらからアクセスできます。
※WebXR Device APIに対応していない場合は利用できません。
https://playcanv.as/p/jDg3BAic/
※PCで確認する場合にはChromeにヘッドマウントディスプレイのエミュレーターの拡張機能を入れてご確認ください。
体験中のイメージ
Androidスマートフォン(3DoF)
Questブラウザ(6DoF)
準備
- PlayCanvasのアカウント(https://playcanvas.jp/)
- Oculus Quest(Android端末)
Oculus Quest / Androidで利用できるWebXRを利用したシーンの構築をします。
PlayCanvasを使ってブラウザで開発を進めていくためパソコンにインストールするソフトウェアはありません。準備としてPlayCanvasのアカウントの作成をお願いします。
PlayCanvasについて
PlayCanvasはJavaScriptで記述されたWebGLのエンジンで、最近GitHubのスター数が7000を超えました。SaaS型のエディターを兼ね備えているゲームエンジンです。engineについては主にSnapのメンバーWill Eastcottを中心にOSSとして公開及び継続的に開発が進められています。Engine単体として利用する場合には、Three.js, Babylon.jsなどのWebGLライブラリと似た使い方を使用することができます。またビジュアルエディターとコードエディターが存在しており。エディターはクラウド上でプロジェクトの作成から公開までが出来ます。FBXやOBJといった3Dモデルの素材については、ブラウザへドラッグアンドドロップをすることでGUIから操作・配置ができるエディターがあります。
素材について
キャラクターや移動のスクリプトについては事前に用意されている素材を使用します。スターターキットは、キャラクターの3Dモデル、キャラクターの移動が事前に入っているPlayCanvasのプロジェクトです、PlayCanvasの公開プロジェクトをコピー(Fork)する機能を使用して作成していきます。
プロジェクトの準備 - スターターキットを使用する
他のプロジェクトをForkという形で複製することができます。
この機能を使用して、プロジェクトを元にプロジェクトを作成していきます。
1. フォーク(プロジェクトの複製)
https://playcanvas.com/project/883637/overview/
PlayCanvasのアカウントが作成できましたらこちらからこちらのリンクをクリックして
上記リンクからプロジェクトにアクセスして「Forkボタン」をクリックします。
2. プロジェクトのフォーク
プロジェクト名を入力して(好きな名前で大丈夫です)FORKをしてください。
3. エディタを起動
プロジェクトページの中にある、EditorボタンをクリックしPlayCanvas Editorを起動します。
4. シーンの選択
シーンの選択画面がでてきますので、Mainというシーンを選択してください。
5. シーンが開かれます
これでシーンが開かれました、次は、スターターキットの紹介になります。
プロジェクトの中身について
フォークした後のシーンはこのような構成となっております。
左側のメニューのヒエラルキー
には、今回ステージとして利用する(Mapエンティティ
, Playerエンティティ
)とDirectional Light
が設定されています。下側メニューのASSETには今回使用する素材が配置されています。
プロジェクトを起動
右上のLaunchボタンからプロジェクトを起動することができます。
起動されたプロジェクト
起動されたプロジェクトは現在このような状態となっております。
ここからカメラをWebXR用のカメラとして利用していきます。
PlayCanvasでWebXRシーンの構築
PlayCanvasでシーンを構築するためには、最小で構成する場合には「シーン」「VR用カメラ」の2つのエンティティが必要となります。、またVRのシーンへ入るためにはユーザーからの入力(クリックやタッチなど)の動作が必要です。
PlayCanvasでWebVRのシーンに入るための手順です。
- VR用のカメラ及びシーンを準備(PlayCanvasのヒエラルキーで作成)
- ユーザーがボタンを押すことでVRシーンへ入る
VR用のカメラ/シーンの準備 & 設定
VRのカメラの設定・コントローラーの設定・VR空間での移動のスクリプトを含んでいるスクリプトとなります。こちらを利用するための準備をします。Playerエンティティにスクリプトを追加して設定をするところまでを行います。
1. ADD COMPONENTをクリック
ヒエラルキー上からPlayer
を選択してください、選択したあと右側のインスペクター
から「ADD COMPONENT」をクリックします。
2. SCRIPTコンポネントをクリック
3. ADD SCRIPT -> VR Starter Kitを選択
「ADD SCRIPT」 -> 「vr-starter-kit.js
」を選択して。
スクリプトコンポーネントに追懐するためのスクリプトを追加します。
これでスクリプトを追加することが出来ました。このあとスクリプトに対しての初期設定を行っていきます。初期設定として、カメラのコントローラーの設定をします。設定には、PlayCanvasのスクリプト属性という機能を利用します。
追加されたvr-starter-kit.jsについて
const EVENT = {
ADD_CONTOLLER: "add",
SELECT: pc.EVENT_SELECT,
VR_END: "end",
VR_ENTER: "vr:enter",
};
class VRStarterKit extends pc.ScriptType {
initialize() {
this.controllers = []; // コントローラー用の配列
this.app.isSupported = this.app.xr.supported && pc.XRTYPE_VR; // 起動時XRのサポートをしているか確認
// WebXR(VR)のAPIに対応してる場合
if (this.app.isSupported) {
this.app.on(EVENT.VR_ENTER, this.enterVr, this); // EVENT.VR_ENTERのイベントが発火された場合 enterVr関数を実行
this.app.xr.input.on(EVENT.ADD_CONTOLLER, this.createController, this); // コントローラーが追加された場合に発火
this.app.keyboard.on(pc.EVENT_KEYDOWN, this.exitVr, this); // キーボードが押された場合に発火
// this.app.xr.input.on(EVENT.SELECT, () => { }, this);
// this.app.xr.on(EVENT.VR_END, async () => { });
}
this.movementSpeed = 2.5; // 移動速度
this.rotateSpeed = 2; // 回転速度
this.rotateThreshold = 0.5; // 回転のしきい値
this.rotateResetThreshold = 0.25; // しきい値
this.lineColor = new pc.Color(1, 1, 1); // コントローラーから出る線の色を指定(白)
this.tmpVec = new pc.Vec3(); // {x: 0, y: 0, z: 0}
}
exitVr(e) {
const { key } = e;
// Escキーが押された場合
if (key === pc.KEY_ESCAPE) {
this.vrCamera.camera.endXr(() => { }); // Xrのモードを抜ける
}
}
async enterVr() {
// WebXR(VR)のAPIに対応してる場合
if (this.app.isSupported) {
const result = this.vrCamera.camera.startXr(
pc.XRTYPE_VR,
this.XRReferenceSpaceType,
{
callback: (error) => {
if (error !== null) alert(error);
},
}
); // カメラコンポーネントのstartXr実行しVRシーンへ切り替える
console.log(result);
} else {
alert("未対応のブラウザです。");
}
}
createController(inputSource) {
const entity = this.controllerEntity.resource.instantiate(); // コントローラーをテンプレートからインスタンス化
this.entity.addChild(entity); // Player配下に配置
entity.inputSource = inputSource; // コントローラーの情報を登録
this.controllers.push(entity); // エンティティをシーン上に配置
inputSource.on("remove", () => {
// コントローラー削除された際の処理を追加
this.controllers.splice(this.controllers.indexOf(entity), 1);
entity.destroy();
});
}
xrMovement(dt) {
let lastRotateValue = 0;
const tmpVec2A = new pc.Vec2();
const tmpVec2B = new pc.Vec2();
const tmpVec3A = new pc.Vec3();
for (let i = 0; i < this.controllers.length; i++) {
const inputSource = this.controllers[i].inputSource;
if (inputSource.handedness === pc.XRHAND_LEFT) {
tmpVec2A.set(inputSource.gamepad.axes[2], inputSource.gamepad.axes[3]);
if (tmpVec2A.length()) {
tmpVec2A.normalize();
tmpVec2B.x = this.vrCamera.forward.x;
tmpVec2B.y = this.vrCamera.forward.z;
tmpVec2B.normalize();
const rad = Math.atan2(tmpVec2B.x, tmpVec2B.y) - Math.PI / 2;
const t = tmpVec2A.x * Math.sin(rad) - tmpVec2A.y * Math.cos(rad);
tmpVec2A.y = tmpVec2A.y * Math.sin(rad) + tmpVec2A.x * Math.cos(rad);
tmpVec2A.x = t;
tmpVec2A.scale(this.movementSpeed * dt);
this.entity.translate(tmpVec2A.x, 0, tmpVec2A.y);
}
} else if (inputSource.handedness === pc.XRHAND_RIGHT) {
const rotate = -inputSource.gamepad.axes[2];
if (lastRotateValue > 0 && rotate < this.rotateResetThreshold) {
lastRotateValue = 0;
} else if (lastRotateValue < 0 && rotate > -this.rotateResetThreshold) {
lastRotateValue = 0;
}
if (lastRotateValue === 0 && Math.abs(rotate) > this.rotateThreshold) {
lastRotateValue = Math.sign(rotate);
tmpVec3A.copy(this.vrCamera.getLocalPosition());
this.entity.translateLocal(tmpVec3A);
this.entity.rotateLocal(0, Math.sign(rotate) * this.rotateSpeed, 0);
this.entity.translateLocal(tmpVec3A.scale(-1));
}
}
}
}
xrContoller() {
for (let i = 0; i < this.controllers.length; i++) {
const inputSource = this.controllers[i].inputSource;
if (inputSource.grip) {
this.controllers[i].setLocalPosition(inputSource.getLocalPosition()); // コントローラーの角度をコントローラーの角度に合わせる
this.tmpVec.copy(inputSource.getDirection());
this.tmpVec.scale(100).add(inputSource.getOrigin());
this.app.drawLine(inputSource.getOrigin(), this.tmpVec, this.lineColor); // 線を引く
}
}
}
update(dt) {
this.xrContoller(); // コントローラーの位置を同期
this.xrMovement(dt); // キャラクターの位置を動かす
}
}
pc.registerScript(VRStarterKit);
VRStarterKit.attributes.add("vrCamera", { type: "entity" });
VRStarterKit.attributes.add("controllerEntity", {
type: "asset",
assetType: "template",
});
VRStarterKit.attributes.add("XRReferenceSpaceType", {
type: "string",
enum: [
{ "bounded-floor": "bounded-floor" },
{ local: "local" },
{ "local-floor": "local-floor" },
{ unbounded: "unbounded" },
{ viewer: "viewer" },
],
default: "local-floor",
});
スクリプト属性
スクリプトのアトリビュート機能は、スクリプト内で使用する変数をPlayCanvasエディタ内で編集することができるようにする便利な機能です。この機能を使うことで、一度コードを書いた後にエンティティごと作られるインスタンスにそれぞれ違うパラメータを設定する調整ができるようになります。これにより、アーティスト、デザイナーやその他のプログラマーではないチームメンバーがコードを書かずに値を変更できるにプロパティを露出させることができます。
https://developer.playcanvas.com/ja/user-manual/scripting/script-attributes/
今回のスクリプトではvrCamera
, controllerEntity
, XRReferenceSpaceType
の3つの値を設定できるようになっています。
それぞれの値を設定します。
4. SELECT ENTITYを選択
SELECT ENTITYをクリックして、カメラとして利用するエンティティを追加していきます。
5. VR Cameraを選択
6.controllerEntityを選択
次にコントローラーとして利用するためのテンプレートを追加していきます。
スクリプト属性のcontorllerEntity
を選択します。
7. Controllerを選択
このテンプレートの値はASSET
にあるものを追加します。
ハンズオン資料内のController
テンプレートを追加してください。
これでPlayCanvas側のシーンの準備は完了です。
vr-starter-kitの中身について
① WebXRに対応している端末かどうかの判定
今回のプロジェクトでは、pc.app.xr.supported
とpc.XRTYPE_VR
に対応しているかどうかを確認しています。this.app.isSupported
の場合にその他のVR関係の処理を追加していきます。
this.app.isSupported = this.app.xr.supported && pc.XRTYPE_VR; // 起動時XRのサポートをしているか確認
// WebXR(VR)のAPIに対応してる場合
if (this.app.isSupported) {
}
② camera.startXrを実行してVRに入る
カメラエンティティ内のstartXr
を利用してVRシーンに入ります。このメソッドを実行するためには、事前にユーザーから入力が必要です。そのため次にUIを作成して、ユーザーがボタンをクリックしたら発火されるvr:enter
イベントでメソッドを実行するようにします。
this.app.on(EVENT.VR_ENTER, this.enterVr, this); // EVENT.VR_ENTERのイベントが発火された場合 enterVr関数を実行
async enterVr() {
// ...
this.vrCamera.camera.startXr(pc.XRTYPE_VR, this.XRReferenceSpaceType)
// ...
}
③ escキーが押されたらVRを抜ける
キーボードが押された際にthis.enterVr
を実行します。入力されたキーがesc
キーだった場合にはendXr
を実行し終了します。
this.app.keyboard.on(pc.EVENT_KEYDOWN, this.exitVr, this); // キーボードが押された場合に発火
exitVr(e) {
const { key } = e;
// Escキーが押された場合
if (key === pc.KEY_ESCAPE) {
this.vrCamera.camera.endXr(() => { });
}
}
④ VRのコントローラーについて
createController
は実行された端末にコントローラーが存在する場合に実行します。
Questなどのコントローラーを追加した際に実行されます。Androidなどのスマートフォンの場合には実行されません。そのタイミングでテンプレートとして設定したエンティティをシーン上に追加します。
this.controllers = []; // コントローラー管理用の配列
this.app.xr.input.on(EVENT.ADD_CONTOLLER, this.createController, this); // コントローラーが追加された場合に発火
createController(inputSource) {
const entity = this.controllerEntity.resource.instantiate(); // コントローラーをテンプレートからインスタンス化
this.entity.addChild(entity); // Player配下に配置
entity.inputSource = inputSource;
this.controllers.push(entity); // コントローラーの情報を登録
inputSource.on("remove", () => {
// コントローラー削除された際の処理を追加
this.controllers.splice(this.controllers.indexOf(entity), 1);
entity.destroy();
});
}
⑤ コントローラーの左右の判定について
if (inputSource.handedness === pc.XRHAND_LEFT) {
} else if (inputSource.handedness === pc.XRHAND_RIGHT) {
}
UIの作成
こちらのようなVRシーンへ入るためのUIを実装していきます。
PlayCanvasでcanvas外にUIを実装する場合については、HTML / CSSを記述する必要がありますが、今回は、pcuiを利用して作成していきます。
pcuiについて
PlayCanvasでUIを作成するためには、PlayCanvasチームの作成しているUIライブラリです。
このライブラリを使用するとPlayCanvasでのUIの実装が比較的簡単に行うことが出来ます。
詳細については、ボタンやスライダーなどの実装がStorybook上で公開されています。
アセット(スクリプト)の読み込み順について
こちらのライブラリについては事前にインポートしています。このようにインポートしているライブラリについては、PlayCanvasのロード時に読み込まれるようになっているため、エディタでスクリプトを記述する際には、グローバルに展開されているpcuiのメソッドを利用します。
また、事前に読み込まれているスクリプトの一覧については、エディタ左メニュー下部の「SETTINGS
」 -> 「SCRIPT LOADING ORDER
」から確認できます。
1. Rootエンティティを選択
2. Add Scriptをクリック
SCRIPTコンポーネントが追加されているので、「ADD SCRIPT」をクリックします。
3. ui.jsを追加
スクリプトの一覧が表示されますのでui.jsを選択してください。
class Ui extends pc.ScriptType {
initialize() {
this.setupMainContainer();
this.setupMainPane();
this.addInfoBox(
"概要",
`このハンズオンはPlayCanvasのチュートリアルとして公開されているプロジェクトを利用してWebXR Device APIで動作するVRを体験するハンズオンです。`
);
this.addEnterVrButton();
}
addEnterVrButton() {
const { Button } = pcui;
const button = new Button({
text: "VRシーンに入る",
});
button.on("click", () => {
this.app.fire("vr:enter");
});
this.pane.appendChild(button.dom);
}
addInfoBox(title, text) {
const { InfoBox } = pcui;
const infobox = new InfoBox({
title,
text,
});
this.pane.appendChild(infobox.dom);
}
setupMainContainer() {
const { Container } = pcui;
const container = new Container({
grid: true,
});
container.style.position = "absolute";
container.style.height = "100%";
container.style.width = "100%";
container.style.zIndex = 10;
document.body.appendChild(container.dom);
this.ui = container.dom;
}
setupMainPane() {
const { Panel } = pcui;
const panel = new Panel({
flex: true,
collapsible: true,
collapsed: false,
collapseHorizontally: true,
removable: false,
headerText: "PlayCanvasハンズオンについて",
});
const content = panel.content;
panel.style.width = "285px";
this.ui.appendChild(panel.dom);
this.pane = content.dom;
}
}
pc.registerScript(Ui, "ui");
ui.jsについて
① ボタンを追加し、ボタンが押されたらイベントを発火
pcuiを利用してボタンを追加します。このボタンはクリックされた場合にvr:enter
イベントを発火します。これにより、VRカメラのenterVr
を実行します。
addEnterVrButton() {
const { Button } = pcui;
const button = new Button({
text: "VRシーンに入る",
});
button.on("click", () => {
this.app.fire("vr:enter");
});
this.pane.appendChild(button.dom);
}
ボタンクリックでvr-starter-kit.js
のenterVr
実行されます。
this.app.on(EVENT.VR_ENTER, this.enterVr, this); // EVENT.VR_ENTERのイベントが発火された場合 enterVr関数を実行
ui.jsのaddEnterVrButton
でボタンが押された時に、PlayCanvasでvr:enter
イベントを発火するボタンを追加しています。UI上からクリックを取得することが出来たので次はこのUIのイベントが発火されたときの処理を記述していきます。
4. 起動する
ui.jsを追加するとこのようにPlayCanvasのシーンを起動した後にUIが表示されるようになります。今回のプロジェクトについては設定はこれで完了です。ここまで出来たら実機で確認をしてみます。現在のlaunch
から始まるUIについては、ログインしているユーザーのみ利用可能なためこのプロジェクトを共有する場合にはPublish
をする必要があります。
作ったプロジェクトを公開する。
プロジェクトを作成する事ができました。作ったプロジェクトをウェブ上で共有します。PlayCanvasでは作成したゲームをウェブ上ですぐに公開できます。
1. PUBLISH/DOWNLOADをクリック
2. PUBLISH TO PLAYCANVASをクリック
3. PUBLISH NOWを選択
4. URLを共有しよう!
PUBLISHが成功するとBUILDSに共有できるURLが生成されます。 こちらを共有することで他の方への共有などができます。
完成シーン
Androidスマートフォン
Questブラウザ
このような形でUIと組み合わせてWebXRのシーンを構築することが出来ました。
WebXRのシーン上ではPlayCanvasの2D Screen / 3D Screenのコンポーネント等を利用することが出来るのでVR上でのUIを作る事ができます。そちらにつきましては、別記事または追記予定です。
今回のプロジェクトで質問や意見がありましたら。@mxcn3まで連絡をお願いします。
その他関連
PlayCanvasのユーザー会のSlackを作りました!
少しでも興味がありましたら、ユーザー同士で解決・PlayCanvasを推進するためのSlackを作りましたので、もしよろしければご参加ください!