2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【PlayCanvas】Meta Questのブラウザで動くWebXRのシーンを作成してみる(準備〜実機で確認まで)

Last updated at Posted at 2022-02-22

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)

準備

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のシーンに入るための手順です。

  1. VR用のカメラ及びシーンを準備(PlayCanvasのヒエラルキーで作成)
  2. ユーザーがボタンを押すことで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を選択

ヒエラルキー上からVR Cameraを選択します。

6.controllerEntityを選択

次にコントローラーとして利用するためのテンプレートを追加していきます。
スクリプト属性のcontorllerEntityを選択します。

7. Controllerを選択

このテンプレートの値はASSETにあるものを追加します。
ハンズオン資料内のControllerテンプレートを追加してください。

これでPlayCanvas側のシーンの準備は完了です。

vr-starter-kitの中身について

① WebXRに対応している端末かどうかの判定

今回のプロジェクトでは、pc.app.xr.supportedpc.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エンティティを選択

ヒエラルキーから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.jsenterVr実行されます。

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を作りましたので、もしよろしければご参加ください!

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?