2018年11月13日

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

WebVRのコンテンツでは、コントローラーが重要なユーザーインターフェースになります。そこで、Three.jsのサンプルを参考に、コントローラーの取得方法を調べつつballshooterを試してみました。

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

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

コントローラーは、「renderer.vr.getController(0)」で取得できます。両手用でコントローラーが2つある場合は、「renderer.vr.getController(1)」で2つ目のコントローラーを取得できます。

また、コントローラーから光線を出します。

● コントローラーの取得

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

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

scene.add(controller);

//コントローラーから出る光線を生成
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position',new THREE.Float32BufferAttribute([0,0,0,0,0,-1],3));
geometry.setAttribute('color',new THREE.Float32BufferAttribute([1.0,1.0,1.0,0,0,0],3));

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

controller.userData.isSeleting = false;

//コントローライベントを設定
function onSelectStart(){
	this.userData.isSeleting = true;
}
function onSelectEnd(){
	this.userData.isSeleting = false;
}

● コントローラーからボールを発射

ただ光線を出すだけではおもしろくないので、Three.jsのサンプルを参考に、コントローラーからボールを発射します。Three.jsのオブジェクトには独自の変数を格納できる「userData」があり、そこに速度を保存しています。また、ボールの物理シミュレーションは、Y軸のみシミュレーションするようにしました。

//透明なボックスを生成
let geometry = new THREE.BoxGeometry(40,40,40,10,10,10);
let material = new THREE.MeshPhongMaterial({color:0xFFFFFF,transparent:true,opacity:0});
room = new THREE.Mesh(geometry,material);
scene.add(room);

//ボールを生成して、ボックスに追加
geometry = new THREE.IcosahedronBufferGeometry(radius,2);

for(let i = 0; i < 400; i++){
	material = new THREE.MeshPhongMaterial({color:0xb6c4c6});
	const object = new THREE.Mesh(geometry,material);

	//ボールの初期位置を設定
	object.position.x = Math.random()*40-20;
	object.position.y = Math.random()*40;
	object.position.z = Math.random()*40-20;

	//速度を設定
	object.userData.velocity = new THREE.Vector3();
	object.userData.velocity.x = Math.random() * 0.01 - 0.005;
	object.userData.velocity.y = Math.random() * 0.01 - 0.005;
	object.userData.velocity.z = Math.random() * 0.01 - 0.005;

	//ボックスに追加
	room.add(object);
}

let count = 0;

//コントローラーのボタンを押したら、ボールを発射
function handleCotroller(controller){

	if(controller && controller.userData.isSeleting){
		let object = room.children[count++];
		object.position.copy(controller.position);
		object.userData.velocity.x = (Math.random()-0.5)*3;
		object.userData.velocity.y = (Math.random()-0.5)*3;
		object.userData.velocity.z = (Math.random()-9);
		object.userData.velocity.applyQuaternion(controller.quaternion);

		if(count === room.children.length){
			 count = 0;
		}
	}
}

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

const radius = 0.1;
let clock = new THREE.Clock();

//ボールの物理シミュレーション
function animate() {
	const delta = clock.getDelta() * 0.8;
	const range = 20 - radius;

	if(room){
		for(let i=0; i < room.children.length; i++){
			let object = room.children[i];

			//ボールをシミュレーション
			object.position.x += object.userData.velocity.x * delta;
			object.position.y += object.userData.velocity.y * delta;
			object.position.z += object.userData.velocity.z * delta;

			//Y軸の物理シミュレーション
			if(object.position.y < radius || object.position.y > 20*2){
				object.position.y = Math.max( object.position.y,radius );
				object.userData.velocity.x *= 0.98;
				object.userData.velocity.y = -object.userData.velocity.y * 0.8;
				object.userData.velocity.z *= 0.98;
			}

			//重力の設定
			object.userData.velocity.y -= 9.8 * delta;
		}
	}

	//取得したcontrollerを引数に設定し、handleCotrollerを実行
	handleCotroller(controller);

	renderer.render(scene,camera);
}

● script.js

まず、「Three.jsで360度パノラマコンテンツ制作」でやったように、必要なライブラリを読み込みます。

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

Three.js関連のライブラリはscript.jsからインポートするので、script.jsはtype="module"をつけて読み込みます。

<script src="js/script.js" type="module"></script>

完成した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 orbitControls;
let room,controller;
let texture;
const radius = 0.1;
let count = 0;
let clock = new THREE.Clock();

function init(){
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(50,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;
                setVrController();
            }else{
                setController();
            }
        });
    } else {
        setController();
    }
}

function setLoading(){
    TweenMax.to('.loader',0.1,{opacity:1});

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

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

    loadQueue.on('complete',function(){
        const image = loadQueue.getResult('ground');
        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();
        setLight();
        rendering();
    });

    loadQueue.loadManifest(manifest);
}

function threeWorld(){
    texture.repeat.set(50,50);
    texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
    texture.magFilter = THREE.NearestFilter;
    let geometry = new THREE.PlaneGeometry(250,250);
    let material = new THREE.MeshStandardMaterial({map:texture,roughness:0,metalness:0.5});

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

    geometry = new THREE.BoxGeometry(40,40,40,10,10,10);
    material = new THREE.MeshPhongMaterial({color:0xFFFFFF,transparent:true,opacity:0});
    room = new THREE.Mesh(geometry,material);
    room.geometry.translate(0,20,0);
    scene.add(room);

    geometry = new THREE.IcosahedronBufferGeometry(radius,2);

    for(let i = 0; i < 400; i++){
        material = new THREE.MeshPhongMaterial({color:0xb6c4c6});
        const object = new THREE.Mesh(geometry,material);
        object.position.x = Math.random()*40-20;
        object.position.y = Math.random()*40;
        object.position.z = Math.random()*40-20;
        object.userData.velocity = new THREE.Vector3();
        object.userData.velocity.x = Math.random() * 0.01 - 0.005;
        object.userData.velocity.y = Math.random() * 0.01 - 0.005;
        object.userData.velocity.z = Math.random() * 0.01 - 0.005;
        room.add(object);
    }
}

function setLight(){
    const ambientLight = new THREE.AmbientLight(0xFFFFFF);
    scene.add(ambientLight);

    const directionalLight = new THREE.DirectionalLight(0xFFFFFF,1,0);
    directionalLight.position.set(0,80,0);
    scene.add(directionalLight);
}

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 setVrController(){
    controller = renderer.xr.getController(0);
    controller.addEventListener('selectstart',onSelectStart);
    controller.addEventListener('selectend',onSelectEnd);
    scene.add(controller);

    const geometry = new THREE.BufferGeometry();
    geometry.setAttribute('position',new THREE.Float32BufferAttribute([0,0,0,0,0,-1],3));
    geometry.setAttribute('color',new THREE.Float32BufferAttribute([1.0,1.0,1.0,0,0,0],3));

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

    controller.userData.isSeleting = false;

    function onSelectStart(){
        this.userData.isSeleting = true;
    }
    function onSelectEnd(){
        this.userData.isSeleting = false;
    }
}

function handleCotroller(controller){
    if(controller && controller.userData.isSeleting){
        let object = room.children[count++];
        object.position.copy(controller.position);
        object.userData.velocity.x = (Math.random()-0.5)*3;
        object.userData.velocity.y = (Math.random()-0.5)*3;
        object.userData.velocity.z = (Math.random()-9);
        object.userData.velocity.applyQuaternion(controller.quaternion);

        if(count === room.children.length){
            count = 0;
        }
    }
}

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

function animate(){
    if(orbitControls){
        orbitControls.update();
    }

    const delta = clock.getDelta()*0.8;
    const range = 20-radius;

    if(room){
        for(let i=0; i < room.children.length; i++){
            let object = room.children[i];
            object.position.x += object.userData.velocity.x * delta;
            object.position.y += object.userData.velocity.y * delta;
            object.position.z += object.userData.velocity.z * delta;

            if(object.position.y < radius || object.position.y > 20*2){
                object.position.y = Math.max(object.position.y,radius);
                object.userData.velocity.x *= 0.98;
                object.userData.velocity.y =- object.userData.velocity.y * 0.8;
                object.userData.velocity.z *= 0.98;
            }

            object.userData.velocity.y -= 9.8*delta;
        }
    }

    handleCotroller(controller);
    renderer.render(scene,camera);
}

完成したデモになります。ヘッドマウントディスプレイで確認すると、ボタンを押すとボールが発射されます!

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

関連記事

前の記事へ

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

次の記事へ

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