2018年12月31日

Three.jsで360°パノラマギャラリー制作

Flickr VRの写真を使用させてもらい、パリの360°パノラマギャラリーを制作しました。

360°パノラマギャラリー制作

● PreloadJSで複数画像の読み込み

360°パノラマ画像の表示方法やローディング画面、WebVR対応などの方法は、Three.jsで360°パノラマコンテンツ制作にまとめていますが、画面を切り替えるときにローディングバーを出したくなかったので、最初にパノラマ画像をまとめて読み込んでおきます。

//読み込む外部ファイル情報の準備
var nameArray = ['pict01','pict02','pict03','pict04','pict05','pict06','pict07','pict08','pict09','pict10'];
var manifestArray = [];

//球体の数を保存
var sphereNum = nameArray.length;

for(var i = 0; i < sphereNum; i++){
	var name = nameArray[i];
	var path = 'img/'+name+'.jpg';
	manifestArray.push({id:name,src:path})
}

// LoadQueueクラス
var loadQueue = new createjs.LoadQueue();

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

var imageArray = [];
var textureArray = [];

//読み込みが完了したらテクスチャを配列に格納
loadQueue.on('complete',function(){

	for(var i = 0; i < nameArray.length; i++){
		var tempImage = loadQueue.getResult(nameArray[i]);
		var tempTexture = new THREE.Texture(tempImage);
		tempTexture.needsUpdate = true;

		imageArray.push(tempImage);
		textureArray.push(tempTexture);
	}
});

● 球体の円形配置

Three.jsでキューブ環境マッピングでやったように、ガラス玉のような球体を円形配置してインターフェースにします。

球体を円形配置するには、360度を配置したい個数で割り角度を求め、角度からラジアン、ラジアンから三角関数を使用してx座標、z座標を求めます。

・ラジアン = 角度 × Math.PI/180
・x座標 = Math.cos(ラジアン) × 半径
・z座標 = Math.sin(ラジアン) × 半径


球体にガイド用のテキストも設置したかったので、フォントをダウンロードしてテキストを設置しています。また、ガイド用のテキストは常に内側に向けたかったので、球体とテキストをグループ化して、グループ自体の向きを設定しています。また、TweenMaxは数値をアニメーションさせることができるので、オブジェクトのアニメーションに使用しています。

//フォントの読み込み
var captFont;
var fontLoader = new THREE.FontLoader();
fontLoader.load('./data/gentilis_regular.typeface.json', function(font){
	captFont = font;
});

//円形配置する球体を一つのグループにまとめる
group = new THREE.Group();
scene.add(group);

//キューブ環境マッピング
var pictNameArray = ['posx.jpg','negx.jpg','posy.jpg','negy.jpg','posz.jpg','negz.jpg'];
var textureCubes = [];

for(var i = 0; i < sphereNum; i++){

	var urls = [];
	var num;

	for(var j = 0; j < 6; j++){
		if(i < 9){
			num = '0'+(i+1);
		}else{
			num = (i+1);
		}
		var path = 'img/texture/'+num+'/'+pictNameArray[j];
		urls.push(path);
	}

	var loader = new THREE.CubeTextureLoader();
	var textureCube = loader.load(urls);
	textureCube.mapping = THREE.CubeReflectionMapping;
	textureCubes.push(textureCube);
}

var captNameArray = ['Louvre','Park','Versailles','Hotel','ArtMuseum','TheEiffelTower','Birdview','Station','Restaurant','ArcdeTriomphe'];

//球体の円形配置
//角度
var deg = 360/sphereNum;
//ラジアン
var rad = (deg*Math.PI/180);
//半径
var r = 4;

for(var i = 0; i < sphereNum; i++){

	//球体とテキストをまとめるグループ
	var sphereWrapper = new THREE.Group();

	var geometry = new THREE.SphereBufferGeometry(0.25,64,64);
	var material = new THREE.MeshPhongMaterial({
		envMap:textureCubes[i],
			transparent:true
	});
	//TweenMaxで半透明化
	TweenMax.to(material , 0.5 , {opacity:"0.4",delay:i*0.25});

	//球体
	var sphere = new THREE.Mesh(geometry,material);
	sphere.scale.set(0,0,0);
	TweenMax.to(sphere.scale , 0.5 , {x:"3",y:"3",z:"3",delay:i*0.25});
	//選択する際に取得できるように名前を設定
	sphere.name = 'sphere'+i;
	sphereWrapper.add(sphere);

	//ガイド用のテキスト
	var textGeometry = new THREE.TextBufferGeometry(captNameArray[i], {
		font: captFont,
		size: 0.15,
		height:0.01,
		curveSegments: 3
	});

	var textMaterial = new THREE.MeshBasicMaterial( {
		color: 0xFFFFFF,
		transparent:true,
		opacity:0,
		overdraw: 0.5
	} );

	//TweenMaxで半透明化
	TweenMax.to(textMaterial , 0.5 , {opacity:"0.8",delay:i*0.25});

	var textMesh = new THREE.Mesh(textGeometry, textMaterial);
	textMesh.position.x = -(textGeometry.parameters.shapes.length * 0.0475);
	textMesh.position.z = 0.75;

	sphereWrapper.add(textMesh);

	//円形配置
	sphereWrapper.position.x = Math.cos(rad * i) * r;
	sphereWrapper.position.y = 2;
	sphereWrapper.position.z = Math.sin(rad * i) * r;

	//向きを中心に設定
	sphereWrapper.lookAt(0,1.5,0);

	group.add(sphereWrapper);
}

● script.js

WebVRやローディングに必要なライブラリを読み込みます。

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

完成したscript.jsです。Three.jsでオブジェクトを選択でやったように、球体をコントローラーで選択できるように、パノラマ画像を切り替えることができるようにしました。

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

	var scene,camera,renderer;
	var orbitControls;
	var textureArray;
	var captFont;
	var controller;
	var pointerCircle;
	var raycaster;
	var intersected = [];
	var tempMatrix = new THREE.Matrix4();
	var spaceMaterial;
	var group;
	var sphereNum;

	//画像のローディング処理
	function startLoading(){

		//フォントの読み込み
		var fontLoader = new THREE.FontLoader();
		fontLoader.load('./data/gentilis_regular.typeface.json', function(font){
			captFont = font;
		});

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

		//読み込む外部ファイル情報の準備
		var nameArray = ['pict01','pict02','pict03','pict04','pict05','pict06','pict07','pict08','pict09','pict10'];
		var manifestArray = [];

		//球体の数を保存
		sphereNum = nameArray.length;

		for(var i = 0; i < sphereNum; i++){
			var name = nameArray[i];
			var path = 'img/'+name+'.jpg';
			manifestArray.push({id:name,src:path})
		}

		// LoadQueueクラス
		var loadQueue = new createjs.LoadQueue();

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

		var imageArray = [];
		textureArray = [];

		//読み込みが完了したらテクスチャを配列に格納
		loadQueue.on('complete',function(){

			for(var i = 0; i < nameArray.length; i++){
				var tempImage = loadQueue.getResult(nameArray[i]);
				var tempTexture = new THREE.Texture(tempImage);
				tempTexture.needsUpdate = true;

				imageArray.push(tempImage);
				textureArray.push(tempTexture);
			}

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

			init();
			initObject();
			initLight();
			initController();
		});

		loadQueue.loadManifest(manifestArray);
	}

	//シーン、カメラ、レンダラー生成
	function init(){
		var polyfill = new WebVRPolyfill();

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

		renderer = new THREE.WebGLRenderer({antialias:true});
		renderer.setSize(window.innerWidth, window.innerHeight);
		renderer.render(scene,camera);
		renderer.shadowMap.enabled = true;

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

		render();
		checkDevice();

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

	//オブジェクト生成
	function initObject(){
		var geometry = new THREE.SphereBufferGeometry(100,100,100);
		geometry.scale(-1,1,1);
		spaceMaterial = new THREE.MeshBasicMaterial({
			map:textureArray[Math.floor(Math.random()*10)]
		});
		var spaceSphere = new THREE.Mesh(geometry,spaceMaterial);
		scene.add(spaceSphere);

		//円形配置する球体を一つのグループにまとめる
		group = new THREE.Group();
		scene.add(group);

		//キューブ環境マッピング
		var pictNameArray = ['posx.jpg','negx.jpg','posy.jpg','negy.jpg','posz.jpg','negz.jpg'];
		var textureCubes = [];

		for(var i = 0; i < sphereNum; i++){

			var urls = [];
			var num;

			for(var j = 0; j < 6; j++){
				if(i < 9){
					num = '0'+(i+1);
				}else{
					num = (i+1);
				}
				var path = 'img/texture/'+num+'/'+pictNameArray[j];
				urls.push(path);
			}

			var loader = new THREE.CubeTextureLoader();
			var textureCube = loader.load(urls);
			textureCube.mapping = THREE.CubeReflectionMapping;
			textureCubes.push(textureCube);
		}

		var captNameArray = ['Louvre','Park','Versailles','Hotel','ArtMuseum','TheEiffelTower','Birdview','Station','Restaurant','ArcdeTriomphe'];

		//球体の円形配置
		//角度
		var deg = 360/sphereNum;
		//ラジアン
		var rad = (deg*Math.PI/180);
		//半径
		var r = 4;

		for(var i = 0; i < sphereNum; i++){

			//球体とテキストをまとめるグループ
			var sphereWrapper = new THREE.Group();

			var geometry = new THREE.SphereBufferGeometry(0.25,64,64);
			var material = new THREE.MeshPhongMaterial({
				envMap:textureCubes[i],
				transparent:true
			});

			//TweenMaxで半透明化
			TweenMax.to(material , 0.5 , {opacity:"0.4",delay:i*0.25});

			//球体
			var sphere = new THREE.Mesh(geometry,material);
			sphere.scale.set(0,0,0);
			TweenMax.to(sphere.scale , 0.5 , {x:"3",y:"3",z:"3",delay:i*0.25});
			//選択する際に取得できるように名前を設定
			sphere.name = 'sphere'+i;
			sphereWrapper.add(sphere);

			//ガイド用のテキスト
			var textGeometry = new THREE.TextBufferGeometry(captNameArray[i], {
				font: captFont,
				size: 0.15,
				height:0.01,
				curveSegments: 3
			});

			var textMaterial = new THREE.MeshBasicMaterial( {
				color: 0xFFFFFF,
				transparent:true,
				opacity:0,
				overdraw: 0.5
			} );

			//TweenMaxで半透明化
			TweenMax.to(textMaterial , 0.5 , {opacity:"0.8",delay:i*0.25});

			var textMesh = new THREE.Mesh(textGeometry, textMaterial);
			textMesh.position.x = -(textGeometry.parameters.shapes.length * 0.0475);
			textMesh.position.z = 0.75;

			sphereWrapper.add(textMesh);

			//円形配置
			sphereWrapper.position.x = Math.cos(rad * i) * r;
			sphereWrapper.position.y = 2;
			sphereWrapper.position.z = Math.sin(rad * i) * r;
			//向きを中心に設定
			sphereWrapper.lookAt(0,1.5,0);

			group.add(sphereWrapper);
		}

		var textureLoader = new THREE.TextureLoader();
		var texture = textureLoader.load('img/paris.png');

		geometry = new THREE.PlaneBufferGeometry( 2.5, 2.5, 16 );
		material = new THREE.MeshBasicMaterial( {
			transparent:true,
			map:texture,
			side: THREE.DoubleSide,
			opacity:1
		} );
		var plane = new THREE.Mesh( geometry, material );
		plane.position.set(0,5,-4);
		scene.add( plane );
	}

	//ライト生成
	function initLight(){
		var ambientLight = new THREE.AmbientLight(0xFFFFFF);
		scene.add(ambientLight);

		var directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1, 0);
		directionalLight.position.set(0, 100, 0);
		directionalLight.castShadow = true;
		directionalLight.shadow.camera.top = 20;
		directionalLight.shadow.camera.bottom = -20;
		directionalLight.shadow.camera.right = 20;
		directionalLight.shadow.camera.left = -20;
		directionalLight.shadow.mapSize.set(4096,4096);
		scene.add(directionalLight);
	}

	//コントローラー制御
	function initController(){
		controller = renderer.vr.getController(0);
		controller.addEventListener('selectstart',onSelectStart);
		controller.addEventListener('selectend',onSelectEnd);
		scene.add(controller);

		raycaster = new THREE.Raycaster();

		var geometry = new THREE.BufferGeometry();
		geometry.addAttribute('position',new THREE.Float32BufferAttribute([0,0,0,0,0,-1],3));
		geometry.addAttribute('color',new THREE.Float32BufferAttribute([1.0,1.0,1.0,1.0,1.0,1.0],3));
		geometry.addAttribute('size',new THREE.Float32BufferAttribute([2.0,1.0,1.0],3));

		var material = new THREE.LineBasicMaterial({
			vertexColors:true,
			blending:THREE.AdditiveBlending,
			linewidth:2.0
		});
		controller.add(new THREE.Line(geometry,material));

		pointerCircle = new THREE.Mesh(
			new THREE.CircleBufferGeometry(0.03,32),
			new THREE.MeshBasicMaterial({
				color:0xFFFFFF,
				transparent:true,
				opacity:0.5,
			})
		);

		pointerCircle.position.z = 0;
		controller.add(pointerCircle);

		function onSelectStart(event){
			var controller = event.target;
			var intersections = getIntersections(controller);

			if(intersections.length > 0){

				var intersection = intersections[0];
				var object = intersection.object;
				var num = parseInt(object.name.slice(6));
				spaceMaterial.map = textureArray[num];
				controller.userData.selected = object;
			}
		}

		function onSelectEnd(event){
			var controller = event.target;

			if(controller.userData.selected !== undefined){
				var object = controller.userData.selected;
				controller.userData.selected = undefined;
			}
		}
	}

	function getIntersections(controller){
		tempMatrix.identity().extractRotation(controller.matrixWorld);
		raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld);
		raycaster.ray.direction.set(0,0,-1).applyMatrix4(tempMatrix);

		var sphereArr = [];
		for(var i = 0; i < sphereNum; i++){
			var targetSphere = group.children[i].children[0];
			sphereArr.push(targetSphere);
		}
		return raycaster.intersectObjects(sphereArr);
	}

	function intersectObjects(controller){

		if(controller.userData.selected !== undefined){
			return;
		}

		var intersections = getIntersections(controller);

		if(intersections.length > 0){
			var intersection = intersections[0];
			var object = intersection.object;
			object.material.emissive.r = 0.7;
			object.material.emissive.g = 0.7;
			object.material.emissive.b = 0.7;
			TweenMax.to(object.material , 0.5 , {opacity:"1.0"});
			intersected.push(object);

			pointerCircle.position.z = -intersection.distance+0.1;
			pointerCircle.opacity = 0.1;
		}else{
			pointerCircle.position.z = -50;
			pointerCircle.opacity = 0;
		}
	}

	function clearIntersected(){
		while(intersected.length){
			var object = intersected.pop();
			object.material.emissive.r = 0;
			object.material.emissive.g = 0;
			object.material.emissive.b = 0;
			TweenMax.to(object.material , 0.5 , {opacity:"0.4"});
		}
	}

	//アニメーション
	function render() {
		renderer.setAnimationLoop(render);
		renderer.render(scene, camera);

		if(controller){
			clearIntersected();
			intersectObjects(controller);
		}

		if(group){
			group.rotation.y += 2 * Math.PI / 360 * 0.05;
		}
	}

	//デバイス判定
	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.01,
							camera.position.y,
							camera.position.z
						);
					}
				}else{
					renderer.vr.enabled = true;
				}
			});
		}
	}
})();

完成したscript.jsを調整したデモになります。VRヘッドセットで見ると、パリの360度パノラマギャラリーを見ることができます。

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

Three.jsでキューブ環境マッピング

次の記事へ

Oculus GoでWebVRのデバッグ