読者です 読者をやめる 読者になる 読者になる

vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

three.js + WebVR BoilerplateでモバイルVR

Javascript VR

はじめに

前回Spring Bootを使ってHTMLを表示するところまでやりました。

そのままAngularJSで何かサイトを作ろうと思っていたのですが、ちょっと予定を変更して、Webページ上でお手軽?なVRを試してみることにしました。

今回、下記のページを参考にthree.jswebvr-boilerplateを使って試してみました。

なおGitHubに上げてみました。

MobileWebVrDemo - GitHub

準備

まずthree.jswebvr-boilerplatewebvr-polyfill(webvr-boilerplateが依存)から、以下のファイルを取得してSpringBootのプロジェクト/src/main/resources/static/js に追加します。

three.js

  • three.js-master/build/three.min.js
  • three.js-master/examples/js/controls/VRControls.js
  • three.js-master/examples/js/effects/VREffect.js
  • three.js-master/examples/js/loaders/MTLLoader.js
  • three.js-master/examples/js/loaders/OBJLoader.js

webvr-polyfill

  • webvr-polyfill-master/build/webvr-polyfill.js

webvr-boilerplate

  • webvr-boilerplate-master/build/webvr-manager.js

VRに限らず、three.jsはHTMLのcanvas上で動き、HTML自体に記述する処理は画面サイズの指定やJavascriptの読み込みくらいです。

mainpage.html

< !DOCTYPE html >
< html lang="jp" >
    < head >
        < meta charset="UTF-8" / >
        < title>MainPage< /title >
        < meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, shrink-to-fit=no"/ >
        < meta name="apple-mobile-web-app-capable" content="yes" / >
        < meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" / >
        < link rel='stylesheet' type="text/css" href="css/mainpage.css" / >
    < /head >
    < body >
        < script src='js/three.min.js' >< /script >
        < script src='js/VRControls.js' >< /script >
        < script src="js/VREffect.js" >< /script >
        < script src="js/webvr-polyfill.js" >< /script >
        < script src="js/webvr-manager.js" >< /script >
        < script src="js/MTLLoader.js" >< /script >
        < script src="js/OBJLoader.js" >< /script >
        < script src="js/mainpage.js" >< /script >
    < /body >
< /html >

mainpage.css

body {
    width: 100%;
    height: 100%;
    background-color: #000;
    color: #fff;
    margin: 0px;
    padding: 0;
    overflow: hidden;
}

3Dモデルを作る

画面に表示したい3Dモデルを作ります。

私はBlenderを使って作成してみました。Booleanを覚えるだけでも簡単なものならそれっぽく作成できて良いですね:)

f:id:mslGt:20161025013647j:plain

ここで注意が必要なのは、モデルを三角形ポリゴンで構成することで、これを忘れて多角形ポリゴンが含まれた状態で読み込んでしまうと、面の一部が描画されなかったりします。

なおBlenderでの三角形化は Edit Modeで Mesh -> Faces -> Triangulate Faces でできます。
blender上では問題なく表示できるだけに、見逃しやすいポイントかと思います。

また今回は、obj形式でExportしておきます(pumpkin.obj、pumpkin.mtlのようなファイルが作成されます)。

作成したファイルはプロジェクトの /src/main/resources/static/models に置きます。

Sceneを作る

いよいよJavascriptのお話です。

まずはUnityと同じように、3Dモデルを表示するためのSceneを作ります。

mainpage.js

(function () {
    var renderer;
    var camera;
    var controls;
    var effect;
    var scene;
    var manager;
    var objectModel;
    var obj;

    this.initialize = function(){
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);

        document.body.appendChild(renderer.domElement);

        // Sceneを作る
        scene = new THREE.Scene();

        // カメラを生成
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.set(0, 0, 0);

        // VR用コントローラを生成
        controls = new THREE.VRControls(camera);

        // VR用エフェクトを生成(2分割の画面を構築する)
        effect = new THREE.VREffect(renderer);
        effect.setSize(window.innerWidth, window.innerHeight);

        // VRマネージャの生成
        manager = new WebVRManager(renderer, effect);

        // 補助線を2000px表示する
        var axis = new THREE.AxisHelper(2000);
        // 中央に合わせるため 補助線の幅/2 分ずらす 
        axis.position.set(0, -1, 0);
        scene.add(axis);

        ~省略~

    };
    this.animate = function(timestamp) {
        // VRコントローラを更新
        controls.update();
        // VRマネージャからシーンを描画
        manager.render(scene, camera, timestamp);

        // ループして実行
        requestAnimationFrame(animate);
    };
    initialize();
    // アニメーションの開始
    animate(performance ? performance.now() : Date.now());
}).call(this);

上記を実行すると、3Dモデルがないので真っ黒ではありますが、もうCardboardのようなVR環境が整ってしまいます。

3Dモデルの読み込み

次は3Dモデルの読み込みです。

this.initializeに以下を追加します。

// まずMaterialの読み込み
var mtlLoader = new THREE.MTLLoader();
mtlLoader.load("models/pumpkin.mtl",
    function(targetMaterial) {
        targetMaterial.preload();

        // 読み込みが完了したらObjectを読み込む.
        var objLoader = new THREE.OBJLoader();
        // 読み込んだMaterialをセット.
        objLoader.setMaterials(targetMaterial);
        objLoader.load("models/pumpkin.obj",
            function (targetObject) {
                // 読み込んだObjectのスケール値などをセット.
                objectModel = targetObject.clone();
                objectModel.scale.set(1, 1, 1);
                objectModel.rotation.set(0, 3, 0);
                objectModel.position.set(0, 0, 0);
                // 読み込んだObjectをObject3Dに追加.
                obj = new THREE.Object3D();
                obj.add(objectModel);
                // sceneに追加
                scene.add(obj);
        });
    });
  • 情報によってはMaterialとObjectをTHREE.OBJMTLLoaderというクラスを使って一度に読み込んでいるものもありましたが、少なくとも私の環境ではエラーになってしまうため、別々に読み込んでいます。
  • Objectを読み込んでいる最中に処理を行ったり、エラー発生時に何か処理を実行したい場合はObjLoader.loadの第三、第四引数にそれぞれメソッドを指定できます。

ライティング

先程追加した3Dモデルを照らすライトを追加します。こちらもthis.initializeに追加します。

// DirectionalLight - 指向性を持った光.
var light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.set(0, 0, 0);
light.rotation.set(1, 1, 0);
light.castShadow = true;
scene.add(light);

// AmbientLight - 光が当たる・当たらないにかかわらず各3Dモデルの色に影響を与える.
var ambient = new THREE.AmbientLight(0xffffff, 0.5);
ambient.castShadow = true;
ambient.position.set(0,0,0);
scene.add(ambient);

// PointLight - 全方向に向けた豆電球のような?光.
var pointlight = new THREE.PointLight(0xffffff, 0.8);
pointlight.position.set(0.3, 0.4, -3);
pointlight.scale.set(1, 1, 1);
scene.add(pointlight);
  • DirectionalLight、AmbientLightは場所による影響を受けないため、原点においています。
  • PointLightは影を持たないため、castShadowは特に指定していません。

結果

このような結果になります。

f:id:mslGt:20161025013912j:plain

画像はPCで表示しているため右下は最大化ボタンだけが表示されていますが、スマートフォンで表示させるとCardboardのボタンも表示され、押すと画面が二分割されます。

Javascriptのコードをまとめて載せておきます。

mainpage.js

(function () {
    var renderer;
    var camera;
    var controls;
    var effect;
    var scene;
    var manager;
    var objectModel;
    var obj;

    this.initialize = function(){
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);

        document.body.appendChild(renderer.domElement);

        // Sceneを作る
        scene = new THREE.Scene();

        // カメラを生成
        camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
        camera.position.set(0, 0, 0);

        // VR用コントローラを生成
        controls = new THREE.VRControls(camera);

        // VR用エフェクトを生成(2分割の画面を構築する)
        effect = new THREE.VREffect(renderer);
        effect.setSize(window.innerWidth, window.innerHeight);

        // VRマネージャの生成
        manager = new WebVRManager(renderer, effect);

        // 補助線を2000px表示する
        var axis = new THREE.AxisHelper(2000);
        // 中央に合わせるため 補助線の幅/2 分ずらす 
        axis.position.set(0, -1, 0);
        scene.add(axis);

        // まずMaterialの読み込み
    var mtlLoader = new THREE.MTLLoader();
    mtlLoader.load("models/pumpkin.mtl",
        function(targetMaterial) {
            targetMaterial.preload();

            // 読み込みが完了したらObjectを読み込む.
            var objLoader = new THREE.OBJLoader();
            // 読み込んだMaterialをセット.
            objLoader.setMaterials(targetMaterial);
            objLoader.load("models/pumpkin.obj",
                function (targetObject) {
                    // 読み込んだObjectのスケール値などをセット.
                    objectModel = targetObject.clone();
                    objectModel.scale.set(1, 1, 1);
                    objectModel.rotation.set(0, 3, 0);
                    objectModel.position.set(0, 0, 0);
                    // 読み込んだObjectをObject3Dに追加.
                    obj = new THREE.Object3D();
                    obj.add(objectModel);
                    // sceneに追加
                    scene.add(obj);
            });
        });
        // DirectionalLight - 指向性を持った光.
        var light = new THREE.DirectionalLight(0xffffff, 0.5);
        light.position.set(0, 0, 0);
        light.rotation.set(1, 1, 0);
        light.castShadow = true;
        scene.add(light);

        // AmbientLight - 光が当たる・当たらないにかかわらず各3Dモデルの色に影響を与える.
        var ambient = new THREE.AmbientLight(0xffffff, 0.5);
        ambient.castShadow = true;
        ambient.position.set(0,0,0);
        scene.add(ambient);

        // PointLight - 全方向に向けた豆電球のような?光.
        var pointlight = new THREE.PointLight(0xffffff, 0.8);
        pointlight.position.set(0.3, 0.4, -3);
        pointlight.scale.set(1, 1, 1);
        scene.add(pointlight);

    };
    this.animate = function(timestamp) {
        // VRコントローラを更新
        controls.update();
        // VRマネージャからシーンを描画
        manager.render(scene, camera, timestamp);

        // ループして実行
        requestAnimationFrame(animate);
    };
    initialize();
    // アニメーションの開始
    animate(performance ? performance.now() : Date.now());
}).call(this);

おわりに

割と簡単にそれっぽいものが表示できてしまうわけですが、いくつか問題点があります。

  1. ポートレートモードでCardboardボタンを押して、そのあとランドスケープモードに表示切り替えすると、カメラが90度回転してしまう
  2. Rotationの値が角度ではない
  3. 描画速度がモバイルVRの要求に満たない

1,は画面の向きによってカメラを回転させれば対応できそうですし、2.もまぁなんとか。

問題は3.で、モバイルVRでは60FPSを実現させる必要があると言われますが、(計測はしていないものの)現状でもそんな速度はでていないのではないかと思われます。
画面を回転させていると引っかかりがあるのも気になるところです。

まぁブラウザアプリ上で動かしているだけに困難ではあります。

が、Webで実現できる最大のメリットは、ただURLにアクセスするだけで見られる、ということだと思いますので、VIVEやRiftなどの専用端末やアプリだけでなく、Web上で表示するVRの技術も進歩していくと面白そうだな、と思っています。

参考