2018年11月05日

Three.jsで360度パノラマコンテンツ制作

VRを体験してみると、やはり多いのが360度パノラマ画像・動画を使用したコンテンツです。そこで360度パノラマコンテンツを制作したいと思います。A-FRAMEを使用するともっと簡単にできると思いますが、いろいろ試していきたいので、Three.jsを使用して制作します。

※ChromeがWebXR Device APIに対応し、Three.jsの仕様が変更になったため内容を修正しました。(2019年12月12日)

360度パノラマコンテンツ制作

● 写真の用意

まずは写真を用意する必要があります。RICOH THETAなどの360度カメラを使用すれば撮影できますが、今回はFlickr VRでダウンロードさせてもらいました。

トップページのExplore VR photosから好きな写真を選んで、写真のページへ移動し、右下のアイコンから画像をダウンロードします。Oculus Goで動作確認をして、読み込む画像のサイズは、4096px×2048pxくらいがちょうどうよかったので、フォトショップで解像度を調整しました。

● 仕組み

360度パノラマ制作に関する情報はいろいろとありますが、「ICS MEDIAさんの記事」が一番わかりやすかったので参考にさせてもらいました。

仕組みはシンプルで、3D空間上に球体を配置し、裏側に360度パノラマ写真のテクスチャをはります。あとは3D空間の中心にカメラを設置し視点操作をするだけです。

Three.jsの基本は、いろいろなサイトに情報が載っているので省略しますが、「Three.js」の中の「examples > js」と「examples > jsm」にOrbitControls.jsなど様々な拡張機能用のライブラリーが入っています。※「jsm」はES2015(ES6)用です。

● script.js

Three.js関連のライブラリはscript.jsからインポートするので、script.jsはtype="module"をつけて読み込みます。

<script src="js/script.js" type="module"></script>

OrbitControlsはカメラのポジションを(0,0,0)にすると動かないので、カメラのポジションションを(0.1,0,0)にしています。とりあえず、これで視点操作がコントロールできる360度パノラマが制作できます。
※OrbitControls.js内のthree.module.jsのパスを変更する必要があります。
※拡張性を考慮してクラスを使用しています。

//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';
import { VRButton } from './lib/three_jsm/VRButton.js';

//===============================================================
// ThreeWorld
//===============================================================
class ThreeWorld{

    constructor(){
        //シーン、カメラ、レンダラーを生成
        this.scene = new THREE.Scene();
        this.camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight);
        this.camera.position.set(0.1, 0, 0);
        this.scene.add(this.camera);

        this.renderer = new THREE.WebGLRenderer();
        this.renderer.setSize(window.innerWidth,window.innerHeight);

        //canvasを作成
        const container = document.createElement('div');
        document.body.appendChild(container);
        container.appendChild(this.renderer.domElement);

        //球体の形状を生成
        const geometry = new THREE.SphereGeometry(100, 100, 100);
        geometry.scale(-1, 1, 1);

        //テクスチャ画像を読み込み
        const loader = new THREE.TextureLoader();
        const texture = loader.load('img/pict.jpg');

        //球体のマテリアルを生成
        const material = new THREE.MeshBasicMaterial({
          map: texture
        });

        //球体を生成
        const sphere = new THREE.Mesh(geometry, material);

        this.scene.add(sphere);

        //OrbitControlsを初期化
        const orbitControls = new OrbitControls(this.camera,this.renderer.domElement);

        this.render();
    }

    render() {
        requestAnimationFrame(this.render.bind(this));
        this.renderer.render(this.scene, this.camera);
    }
}
//===============================================================
// Window load
//===============================================================
window.addEventListener("load", function () {
   const threeWorld = new ThreeWorld();
});

● ウィンドウのリサイズに対応

ウィンドウのリサイズに対応します。

const _this = this;

window.addEventListener('resize',function(){
   _this.camera.aspect = window.innerWidth/window.innerHeight;
   _this.camera.updateProjectionMatrix();
   _this.renderer.setSize(window.innerWidth,window.innerHeight);
},false);

● ローディング画面を設置

画質を綺麗にしようとすると、テクスチャ画像の容量が重くなってしまうので、ローディング画面をつけます。データの読み込み処理はPreloadJSを使用します。ローディング画面は今回のテーマではないので詳細は省略しますが、下記Qiitaの記事がわかりやすいです。また、ローディングアニメーションは、「Single Element CSS Spinners」を使用させてもらいました。

preloadjs.min.jsを読み込みます。TweenMax.min.jsはローディング画面のアニメーションに使用しています。

<script src="js/lib/preloadjs.min.js"></script>
<script src="js/lib/TweenMax.min.js"></script>

~ 略 ~

Loading...
~ 略 ~

これで、ローディングアニメーション後にパノラマ画像が表示されるようになります。

constructor(){
   this.setLoading();
}

setLoading(){
   const _this = this;
   TweenMax.to(".loader",0.1,{opacity:1});

   const manifest = [
      {id:'pict01',src:'img/pict.jpg'}
   ];
   const loadQueue = new createjs.LoadQueue();

   loadQueue.on('progress',function(e){
      const progress = e.progress;
   });

   loadQueue.on('complete',function(){
      const image = loadQueue.getResult('pict01');

      //テクスチャは画像を読み込んだ後に設定
      _this.texture = new THREE.Texture(image);
      _this.texture.needsUpdate = true;

         TweenMax.to("#loader_wrapper" , 1 , {
            opacity:0,
            onComplete: function(){
               document.getElementById("loader_wrapper").style.display ="none";
            }
         });

         //ローディング後、実行
         _this.init();
      });

      loadQueue.loadManifest(manifest);
}

init(){
   ~ 略 ~

   //球体のマテリアルを生成
   const material = new THREE.MeshBasicMaterial({
      map: this.texture
   });

   ~ 略 ~
}

● WebVRに対応

最後にWebVRに対応します。Three.jsは頻繁にアップデートされていて、2019年12月時点ではr111ですが、WebVR.jsが非推奨になり、代わりにVRButton.jsを使用するように仕様が変更されました。詳細は下記の記事を参考にしてください。

「this.renderer.vr.enabled」でVRを許可し、VRButton.jsを使用して VRの開始ボタンを追加します。VRButton.jsはWebXR Device APIを使用していて、WebXR Device APIに対応していないブラウザは、「WEBXR NOT SUPPORTED」と表示されます。最後にrequestAnimationFrameをsetAnimationLoopに変更します。

~ 略 ~

init(){
    ~ 略 ~

    this.renderer = new THREE.WebGLRenderer();
    this.renderer.setSize(window.innerWidth,window.innerHeight);
    this.renderer.setPixelRatio(window.devicePixelRatio);
    //VRを許可
    this.renderer.vr.enabled = true;

    const container = document.querySelector('#canvas_wrapper');
    container.appendChild(this.renderer.domElement);

    //VRボタンを設置
    document.body.appendChild(VRButton.createButton(this.renderer));

    ~ 略 ~
}

render() {
    //setAnimationLoopに変更
    //requestAnimationFrame(this.render.bind(this));
    this.renderer.setAnimationLoop(this.render.bind(this));

    this.renderer.render(this.scene, this.camera);
}

● script.js

ソースコードを整理して、開発用にPCで動作確認できるようにしました。完成したscript.jsになります。

//===============================================================
// Import Library
//===============================================================
import * as THREE from './lib/three_jsm/three.module.js';
import { OrbitControls } from './lib/three_jsm/OrbitControls.js';
import { VRButton } from './lib/three_jsm/VRButton.js';

//===============================================================
// BasicView
//===============================================================
class BasicView{
    constructor(){
        this.init();
    }

    init(){
        //シーン、カメラ、レンダラーを生成
        this.scene = new THREE.Scene();
        this.camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight);
        this.cameraContainer = new THREE.Object3D();
        this.cameraContainer.add(this.camera);
        this.scene.add(this.cameraContainer);
        this.renderer = new THREE.WebGLRenderer({antialias:true});
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.setSize(window.innerWidth,window.innerHeight);

        //canvasを作成
        const container = document.querySelector('#canvas_wrapper');
        container.appendChild(this.renderer.domElement);

        //VRボタンを設置
        document.body.appendChild(VRButton.createButton(this.renderer));

        //ウィンドウのリサイズに対応
        const _this = this;
        window.addEventListener('resize',function(){
            _this.camera.aspect = window.innerWidth/window.innerHeight;
            _this.camera.updateProjectionMatrix();
            _this.renderer.setSize(window.innerWidth,window.innerHeight);
        },false);
    }
    startRendering(){
        this.renderer.setAnimationLoop(this.startRendering.bind(this));
        this.render();
        this.onTick();
    }
    render(){
        this.renderer.render(this.scene,this.camera);
    }
    onTick(){
    }
}

//===============================================================
// ThreeWorld extend BasicView
//===============================================================
class ThreeWorld extends BasicView{

    constructor(){
        super();
        this.initThreeWorld();
    }

    initThreeWorld(){
        this.checkDevice();
        this.setLoading();
        this.startRendering();
    }

    //開発用にPCで動作確認できるように設定
    checkDevice(){
        const ua = window.navigator.userAgent.toLowerCase();
        let _iOS,_Android,_Tablet,_Pc,_vrDisplay;

        _iOS = /ipad|iphone|ipod/.test(ua);
        _Android = /android/.test(ua);
        _Tablet = /ipad|nexus (7|9)|xoom|sch-i800|playbook|tablet|kindle/i.test(ua);
        _Pc = /windows|mac/.test(ua);

        if(_Pc || _iOS || _Tablet){

            //OrbitControlsを初期化
            const orbitControls = new OrbitControls(this.camera,this.renderer.domElement);
            orbitControls.target.set(
                this.camera.position.x + 0.001,
                this.camera.position.y,
                this.camera.position.z
            );
            orbitControls.rotateSpeed = 0.5;
        }else{
            this.cameraContainer.position.y = -5;
            //VRを許可
            this.renderer.vr.enabled = true;
        }
        document.addEventListener('touchmove', function(e) {e.preventDefault();}, {passive: false});
    }

    //ローディング画面
    setLoading(){
        const _this = this;
        TweenMax.to(".loader",0.1,{opacity:1});

        const manifest = [
            {id:'pict01',src:'img/pict.jpg'}
        ];
        const loadQueue = new createjs.LoadQueue();

        loadQueue.on('progress',function(e){
            const progress = e.progress;
        });

        loadQueue.on('complete',function(){
            const image = loadQueue.getResult('pict01');
            _this.texture = new THREE.Texture(image);
            _this.texture.needsUpdate = true;

            //ローディング画面を消去
            TweenMax.to("#loader_wrapper" , 1 , {
                opacity:0,
                onComplete: function(){
                    document.getElementById("loader_wrapper").style.display ="none";
                }
            });

            //ローディング後、実行
            _this.initObject();
        });

        loadQueue.loadManifest(manifest);
    }

    initObject(){
        const _this = this;

        //球体の形状を生成
        const geometry = new THREE.SphereGeometry(100,100,100);
        geometry.scale(-1,1,1);

        //球体のマテリアルを生成
        const material = new THREE.MeshBasicMaterial({
            map:_this.texture
        });

        //球体を生成
        const sphere = new THREE.Mesh(geometry,material);
        this.scene.add(sphere);
    }
}

//===============================================================
// Window load
//===============================================================
window.addEventListener("load", function () {
   const threeWorld = new ThreeWorld();
});

完成したデモになります。VRヘッドセットで見ると、ルーブル美術館にいる気分が味わえます!

  • このエントリーをはてなブックマークに追加

関連記事

前の記事へ

Firefox Reality

次の記事へ

Three.jsでコントローラーを取得