2018年12月13日

Three.jsでオブジェクトを選択

WebVRでユーザーインターフェースを制作しようとすると、3D空間上のオブジェクトをコントローラーで選択する必要があります。そこで、Three.jsのサンプルを参考に、コントローラーでオブジェクトを選択する方法を調べつつ、draggingを試してみました。

シャドウマッピング

3DCGにおいて影はクオリティを決める重要な要素です。そこで、オブジェクトの選択方法を試す前にシャドウマッピングを試しました。

Three.jsで影をつけるには、影を落とすオブジェクトに「castShadow = true」を、影を受けるオブジェクトに「receiveShadow = true」を設定する他、レンダラーと光源の設定を有効化します。



● シャドウマッピング

//レンダラーで影を有効化
renderer.shadowMap.enabled = true;

//影を受けるオブジェクト
floor.receiveShadow = true;

//影を落とすオブジェクト
var geometry = new THREE.BoxGeometry(0.5,0.5,0.5);
var material = new THREE.MeshStandardMaterial({
		color:0xCCCCCC,
		roughness:0.7,
		metalness:0.0
});
var box = new THREE.Mesh(geometry,material);
box.position.y = 1;
box.castShadow = true;
scene.add(box);

//光源の影を有効化
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);

Three.jsでオブジェクトを選択

● コントローラの制御

Three.jsでコントローラーを取得でもやりましたが、コントローラーを取得して、コントローラーからラインを出します。ラインはサイズを変更するために、GPU上のシェーダープログラム内で利用する頂点座標などの頂点データを直接操作できるBufferGeometryを使用しています。

//コントローラを取得
var controller = renderer.vr.getController(0);

//コントローライベントを登録
controller.addEventListener('selectstart',onSlectStart);
controller.addEventListener('selectend',onSelectEnd);

scene.add(controller);

//ラインを生成
var geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,0,0),new THREE.Vector3(0,0,-1)]);
var line = new THREE.Line(geometry);
line.name = 'line';
line.scale.z = 5;

//コントローラーにラインを追加
controller.add(line.clone());

//トリガーを押した時のイベント
function onSlectStart(event){
}

//トリガーを離した時のイベント
function onSelectEnd(event){
}

● オブジェクトを選択

3D空間上のオブジェクトを選択するためには、レイキャストを使用して、コントローラーに追加したラインとオブジェクトが交差しているかを調べます。raycaster.intersectObjectsは引数に配列を入れる必要があるため、オブジェクトをGroupに格納します。GroupはObject3Dとほぼ同じ機能を持つオブジェクトを構造的に制するときによく使うものです。

//Groupを生成
var group = new THREE.Group();
scene.add(group);

//ボックスを生成
var geometry = new THREE.BoxGeometry(0.5,0.5,0.5);
var material = new THREE.MeshStandardMaterial({
        color:0xCCCCCC,
        roughness:0.7,
        metalness:0.0
});
var box = new THREE.Mesh(geometry,material);
box.position.y = 1;
box.castShadow = true;

//ボックスをグループに格納
group.add(box);

オブジェクトが選択されたかどうか、レイキャストとMatrix4を使用して調べます。Matrix4は位置と角度を収納できる4×4の行列です。

トリガーを押した時、選択しているオブジェクトを調べて色を変更し、ユーザーデータにオブジェクトを入れます。トリガーを離した時、ユーザーデータにオブジェクトが入っていたら、色を戻して、ユーザーデータを空にすることで選択を解除します。


var tempMatrix = new THREE.Matrix4();
var raycaster = new THREE.Raycaster();

//トリガーを押した時のイベント
function onSlectStart(event){
	var controller = event.target;
	var intersections = getIntersections(controller);

	//交差しているオブジェクトがあったら色を変更
	if(intersections.length > 0){
		var intersection = intersections[0];
		var object = intersection.object;
		object.material.emissive.b = 1;
		controller.userData.selected = object;
	}
}

//トリガーを離した時のイベント
function onSelectEnd(event){
	var controller = event.target;

	if(controller.userData.selected !== undefined){
		var object = controller.userData.selected;
		object.material.emissive.b = 0;
		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);
	return raycaster.intersectObjects(group.children);
}

● script.js

Three.jsで360°パノラマコンテンツ制作でやったように、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です。

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

	var scene,camera,renderer;
	var texture;
	var controller;
	var raycaster;
	var intersected = [];
	var tempMatrix = new THREE.Matrix4();
	var group;

	//画像のローディング処理
	function startLoading(){
		TweenMax.to(".loader",0.1,{opacity:1});

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

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

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

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

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

		loadQueue.loadManifest(manifest);
	}

	//シーン、カメラ、レンダラー生成
	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(){
		texture.repeat.set(50, 50);
		texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
		texture.magFilter = THREE.NearestFilter;
		var geometry = new THREE.PlaneGeometry(250,250);
		var material = new THREE.MeshStandardMaterial({map:texture,roughness:0.0,metalness:0.6});

		//床
		var floor = new THREE.Mesh(geometry,material);
		floor.rotation.x = -Math.PI / 2;
		floor.receiveShadow = true;
		scene.add(floor);

		//グループ
		group = new THREE.Group();
		scene.add(group);

		//いろいろな種類の形状オブジェクト
		var geometries = [
			new THREE.BoxBufferGeometry(0.5,0.5,0.5),
			new THREE.ConeBufferGeometry(0.5,0.5,64),
			new THREE.CylinderBufferGeometry(0.5,0.5,0.5,64),
			new THREE.IcosahedronBufferGeometry(0.5,3),
			new THREE.TorusBufferGeometry(0.5,0.25,64,32)
		];

		for(var i = 0; i < 70; i++){
			var geometry = geometries[Math.floor(Math.random()*geometries.length)];
			var material = new THREE.MeshStandardMaterial({
				color:0xCCCCCC,
				roughness:0.7,
				metalness:0.0
			});
			var object = new THREE.Mesh(geometry,material);
			object.position.x = Math.random() * 20 - 10;
			object.position.y = Math.random() * 10 + 1;
			object.position.z = Math.random() * 20 - 10;
			object.rotation.x = Math.random() * 2 * Math.PI;
			object.rotation.y= Math.random() * 2 * Math.PI;
			object.rotation.z = Math.random() * 2 * Math.PI;
			object.scale.setScalar(Math.random() + 0.5);
			object.castShadow = true;
			object.receiveShadow = true;

			group.add(object);
		}
	}

	//ライト生成
	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',onSlectStart);
		controller.addEventListener('selectend',onSelectEnd);
		scene.add(controller);

		var geometry = new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(0,0,0),new THREE.Vector3(0,0,-1)]);
		var line = new THREE.Line(geometry);
		line.name = 'line';
		line.scale.z = 5;

		controller.add(line.clone());

		raycaster = new THREE.Raycaster();

		//トリガーを押した時のイベント
		function onSlectStart(event){
			var controller = event.target;
			var intersections = getIntersections(controller);

			if(intersections.length > 0){
				var intersection = intersections[0];
				tempMatrix.getInverse(controller.matrixWorld);
				var object = intersection.object;
				object.matrix.premultiply(tempMatrix);
				object.matrix.decompose(object.position,object.quaternion,object.scale);
				object.material.emissive.b = 1;
				controller.add(object);
				controller.userData.selected = object;
			}
		}

		//トリガーを離した時のイベント
		function onSelectEnd(event){
			var controller = event.target;
			if(controller.userData.selected !== undefined){
				var object = controller.userData.selected;
				object.matrix.premultiply(controller.matrixWorld);
				object.matrix.decompose(object.position,object.quaternion,object.scale);
				object.material.emissive.b = 0;
				group.add(object);
				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);
		return raycaster.intersectObjects(group.children);
	}

	//オブジェクトとラインの交差の状態を調査
	function intersectObjects(controller){
		if(controller.userData.selected !== undefined){
			return;
		}

		var line = controller.getObjectByName('line');
		var intersections = getIntersections(controller);

		if(intersections.length > 0){
			var intersection = intersections[0];
			var object = intersection.object;
			object.material.emissive.r = 1;
			intersected.push(object);

			line.scale.z = intersection.distance;
		}else{
			line.scale.z = 5;
		}
	}

	function clearIntersected(){
		while(intersected.length){
			var object = intersected.pop();
			object.material.emissive.r = 0;
		}
	}

	//アニメーション
	function render() {
		if(controller){
			clearIntersected();
			intersectObjects(controller);
		}

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

	//デバイス判定
	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ヘッドセットでアクセスして、コントローラーでドラッグできます!

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

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

次の記事へ

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