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); renderer.setClearColor(new THREE.Color(0x000000)); //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); renderer.setClearColor(new THREE.Color(0x000000)); //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); renderer.setClearColor(new THREE.Color(0x000000)); 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); }
完成したデモになります。ヘッドマウントディスプレイで見ると、ルーブル美術館にいる気分が味わえます!