2018年11月05日

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

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

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

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

● 写真の用意

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

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

● 仕組み

360度パノラマ制作に関する情報はいろいろとありますが、「お手軽360°パノラマ制作入門!JSでパノラマビューワーを自作しよう」がわかりやすかったので参考にさせてもらいました。

仕組みはシンプルで、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>

視点操作がコントロールできる360度パノラマコンテンツを制作します。OrbitControlsは、カメラのポジションを(0,0,0)にすると動かないので注意が必要です。
※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';

//===============================================================
// Main
//===============================================================
window.addEventListener('load',function(){
   init();
});

let scene,camera,renderer;

function init(){
    //シーン、カメラ、レンダラーを生成
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight,0.1,1000);
    camera.position.set(0,1.6,3);
    scene.add(camera);
    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);

    //canvasを作成
    const container = document.createElement('div');
    document.body.appendChild(container);
    container.appendChild(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);
    scene.add(sphere);

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

    render();
}

function render(){
    requestAnimationFrame(animate);
}

function animate(){
    renderer.render(scene,camera);
}

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

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

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

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

ローディング画面をつけます。データの読み込み処理はPreloadJSを使用します。ローディング画面は今回のテーマではないので詳細は省略しますが、下記Qiitaの記事がわかりやすいです。また、ローディングアニメーションは、「Single Element CSS Spinners」を使用させてもらいました。

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

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

~ 略 ~

Loading...
~ 略 ~

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

setLoading();

let texture;

function setLoading(){
   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');

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

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

      //ローディング後実行
      threeWorld();
   });
   loadQueue.loadManifest(manifest);
}

function threeWorld(){
   ~ 略 ~

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

   ~ 略 ~
}

● WebVRに対応

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

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

~ 略 ~

function init(){
    ~ 略 ~

    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);
    //VRを許可
    renderer.xr.enabled = true;

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

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

    ~ 略 ~
}

function render() {
    //setAnimationLoopに変更
    //requestAnimationFrame(animate);
    renderer.setAnimationLoop(animate);
}
function animate(){
    renderer.render(scene,camera);
}

● script.js

ソースコードを整理して、完成した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';

//===============================================================
// Main
//===============================================================
window.addEventListener('load',function(){
   init();
});

let scene,camera,renderer;
let texture;
let orbitControls;

function init(){
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight,0.1,1000);
    camera.position.set(0,1.6,3);
    scene.add(camera);
    renderer = new THREE.WebGLRenderer({antialias:true});
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth,window.innerHeight);

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

    document.body.appendChild(VRButton.createButton(renderer));

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

    checkDevice();
    setLoading();
}

function checkDevice(){
    if ('xr' in navigator) {
        navigator.xr.isSessionSupported('immersive-vr').then(function(supported){
            if(supported){
                renderer.xr.enabled = true;
            }else{
                setController();
            }
        });
    } else {
        setController();
    }
}

function setLoading(){
    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');
        texture = new THREE.Texture(image);
        texture.needsUpdate = true;

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

    loadQueue.loadManifest(manifest);
}

function threeWorld(){
    const geometry = new THREE.SphereGeometry(100,100,100);
    geometry.scale(-1,1,1);
    const material = new THREE.MeshBasicMaterial({
        map:texture
    });
    const sphere = new THREE.Mesh(geometry,material);
    scene.add(sphere);
}

function setController(){
    document.addEventListener('touchmove',function(e){e.preventDefault();},{passive:false});
    orbitControls = new OrbitControls(camera,renderer.domElement);
    orbitControls.target.set(0,1.6,0);
    orbitControls.enableDamping = true;
    orbitControls.dampingFactor = 0.5;
    orbitControls.enableZoom = false;
}

function rendering(){
    renderer.setAnimationLoop(animate);
}

function animate(){
    if(orbitControls){
        orbitControls.update();
    }
    renderer.render(scene,camera);
}

完成したデモになります。ヘッドマウントディスプレイで見ると、ルーブル美術館にいる気分が味わえます!

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

関連記事

前の記事へ

Firefox Reality

次の記事へ

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