2018年11月05日

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

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

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」にOrbitControls.jsなど様々な拡張機能用のライブラリーが入っています。



それでは、制作します。まず、three.min.jsやOrbitControls.jsなど必要なファイルをHTMLで読み込みます。OrbitControls.jsは、視点操作をコントロールするライブラリです。ちなみに、three.min.jsの「min」はファイルの容量を軽くするために最小化したファイルという意味です。

<script src="js/lib/three.min.js"></script>
<script src="js/lib/OrbitControls.js"></script>
<script src="js/script.js"></script>

● script.js

OrbitControlsはカメラのポジションを(0,0,0)にすると動かないので、カメラのポジションションを(0.1,0,0)にしています。とりあえず、これで視点操作がコントロールできる360°パノラマが制作できました。

(function () {

	window.addEventListener("load", function () {
	   init();
	});

	function init(){

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

	    var renderer = new THREE.WebGLRenderer();
	    renderer.setSize(window.innerWidth, window.innerHeight);
	    renderer.render(scene,camera);

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

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

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

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

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

	    scene.add(sphere);

	    //OrbitControlsを初期化
	    var orbitControls = new THREE.OrbitControls(camera);

	    render();

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

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

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

window.addEventListener('resize',onWindowResize,false);

function onWindowResize(){
	camera.aspect = window.innerWidth/window.innerHeight;
	camera.updateProjectionMatrix();
	renderer.setSize(window.innerWidth,window.innerHeight);
}

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

画質を綺麗にしようとすると、テクスチャ画像の容量が重くなってしまうので、ローディング画面をつけます。データの読み込み処理は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...
~ 略 ~

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

window.addEventListener("load", function () {
	//initはここは削除
	//init();

	startLoading();
});

//startLoadingでも使用したいため、ここで宣言
var texture;

function startLoading(){
	TweenMax.to(".loader",0.1,{opacity:1});

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

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

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

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

		TweenMax.to("#loader_wrapper" , 1 , {opacity:0});

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

function init(){

~ 略 ~

	//startLoadingで読み込んでいるので削除
	//var loader = new THREE.TextureLoader();
	//var texture = loader.load('img/pict.jpg');

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

~ 略 ~

}

● WebVR対応

最後にWebVRに対応させます。Three.jsは頻繁にアップデートされていて、現在はr98ですが、r86まではWebVRに対応させるためにvrEffect.jsとVRControls.jsを使用していたようです。現在は「renderer.vr.enabled = true;」とするだけでよい仕様に変わりました。詳細は下記の記事がわかりやすいです。

webvr-polyfill.jsとWebVR.jsを読み込みます。

<script src="js/lib/three_vr/webvr-polyfill.min.js"></script>
<script src="js/lib/three_vr/WebVR.js"></script>

Three.jsの処理をする冒頭で、WebVRPolyfillを実行します。また「renderer.vr.enabled = true;」を実行し、requestAnimationFrameをsetAnimationLoopに変更します。VRのヘッドセットによっては、いきなり全画面のVRにすることを禁止しているものもあるようなので、WebVR.jsを使用して WebVRの開始ボタンを追加します。

~ 略 ~

function init(){

	//WebVRPolyfillを実行
	var polyfill = new WebVRPolyfill();

	~ 略 ~

	var renderer = new THREE.WebGLRenderer();
	    renderer.setSize(window.innerWidth, window.innerHeight);
	    renderer.render(scene,camera);

	    //WebVRに対応
	    renderer.vr.enabled = true;

	var container = document.createElement('div');
	document.body.appendChild(container);
	container.appendChild(renderer.domElement);

	//WebVRの開始ボタンを追加
	container.appendChild(WEBVR.createButton(renderer));

	~ 略 ~

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

基本的にはこれで大丈夫ですが、ひとつ問題があります。「renderer.vr.enabled = true;」を実行すると、PCでOrbitControlsのカメラの処理と競合してうまく動作しません。最適解ではないかもしれませんが、A-FRAMEのdevice.jsを参考にVRのヘッドセットを検出して、PCと処理を分けてみました。WebVR APIは将来的にWebXR APIに移行予定とのことですが、WebVR APIのnavigator.getVRDisplaysでVRヘッドセットを検出できるようです。

~ 略 ~

//カメラのポジションを(0,0,0)に
camera.position.set(0, 0, 0);

~ 略 ~

//OrbitControlsの処理はcheckDeviceに移動
//var orbitControls = new THREE.OrbitControls(camera);

render();
checkDevice();

~ 略 ~

function checkDevice(){
	var ua = window.navigator.userAgent.toLowerCase();
	var _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);

	if (navigator.getVRDisplays) {
		navigator.getVRDisplays().then(function (displays) {
		var vrDisplay = displays.length && displays[0];
		if(vrDisplay == 0){
			if(!_iOS && !_Android && !_Tablet){

				//PCだったらorbitControlsを処理
				orbitControls = new THREE.OrbitControls(camera);
				orbitControls.target.set(

					//カメラのポジションを設定
					camera.position.x + 0.001,
					camera.position.y,
					camera.position.z
					);
				}
			}else{
				//WebVRに対応
				renderer.vr.enabled = true;
			}
		});
	}
}

現時点でVRヘッドセットは、PCと接続するタイプ、スマホを差し込むタイプ、スタンドアロンのタイプと3種類に分かれますが、そのうちスマホを差し込むタイプ以外は、VRディスプレイを検出でき、スマホとタブレットはVRディスプレイに分類すれば、単純にPCのみ処理を分ければ大丈夫なのでは?と思いました。

今後新しいVRヘッドセットが登場することを考えると、あまりよくないかもしれませんが、スタンドアロンのタイプが主流になりそうなのと、PCでも確認できた方が制作が便利なので、現時点ではこの方法がよいかなと思います。

● script.js

完成したscript.jsになります。

(function () {
	window.addEventListener("load", function () {
	   startLoading();
	});

	var texture;

	function startLoading(){
		TweenMax.to(".loader",0.1,{opacity:1});

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

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

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

			TweenMax.to("#loader_wrapper" , 1 , {opacity:0});

			init();
		});

		loadQueue.loadManifest(manifest);
	}

	function init(){
		var polyfill = new WebVRPolyfill();

		var scene = new THREE.Scene();
	    var camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight);
	    camera.position.set(0, 0, 0);
	    scene.add(camera);

	    var renderer = new THREE.WebGLRenderer();
	    renderer.setSize(window.innerWidth, window.innerHeight);
	    renderer.render(scene,camera);

		var container = document.createElement('div');
		document.body.appendChild(container);
	    container.appendChild(renderer.domElement);

		container.appendChild(WEBVR.createButton(renderer));

	    var geometry = new THREE.SphereGeometry(100, 100, 100);
	    geometry.scale(-1, 1, 1);

	    var material = new THREE.MeshBasicMaterial({
	      map: texture
	    });

	    var sphere = new THREE.Mesh(geometry, material);
	    scene.add(sphere);

	    render();
	    checkDevice();

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

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

		function checkDevice(){
			var ua = window.navigator.userAgent.toLowerCase();
			var _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);

			if (navigator.getVRDisplays) {
				navigator.getVRDisplays().then(function (displays) {
			    	var vrDisplay = displays.length && displays[0];
			    	if(vrDisplay == 0){
			    		if(!_iOS && !_Android && !_Tablet){

							orbitControls = new THREE.OrbitControls(camera);
							orbitControls.target.set(
								camera.position.x + 0.001,
								camera.position.y,
								camera.position.z
								);
						}
					}else{
						renderer.vr.enabled = true;
					}
				});
			}
		}
	}
})();

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

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

Firefox Reality

次の記事へ

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