2018年11月13日

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

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

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

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

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

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

● コントローラーの取得

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

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

this.scene.add(this.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});
this.controller.add(new THREE.Line(geometry,material));

this.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});
this.room = new THREE.Mesh(geometry,material);
this.scene.add(this.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;

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

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

	if(this.room){
		for(let i=0; i < this.room.children.length; i++){
			let object = this.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 < this.radius || object.position.y > 20*2){
				object.position.y = Math.max( object.position.y, this.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を実行
	this.handleCotroller(this.controller);

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

let count = 0;

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

	if(this.controller && this.controller.userData.isSeleting){
		let object = this.room.children[this.count++];
		object.position.copy(this.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(this.controller.quaternion);

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

● script.js

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

<script src="js/lib/preloadjs.min.js"></script>
<script src="js/lib/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';

//===============================================================
// BasicView
//===============================================================
class BasicView{
    constructor(){
        this.init();
    }

    init(){
        //シーン、カメラ、レンダラーを生成
        this.scene = new THREE.Scene();
        this.camera = new THREE.PerspectiveCamera(90,window.innerWidth/window.innerHeight,0.1,1000);
        this.camera.position.set(0,0,0);
        this.scene.add(this.camera);
        this.renderer = new THREE.WebGLRenderer({antialias:true});
        this.renderer.setPixelRatio(window.devicePixelRatio);
        this.renderer.setSize(window.innerWidth,window.innerHeight);

        const container = document.querySelector('#canvas_wrapper');
        container.appendChild(this.renderer.domElement);

        document.body.appendChild(VRButton.createButton(this.renderer));

        const _this = this;
        window.addEventListener('resize',function(){
            _this.camera.aspect = window.innerWidth/window.innerHeight;
            _this.camera.updateProjectionMatrix();
            _this.renderer.setSize(window.innerWidth,window.innerHeight);
        },false);
    }
    startRendering(){
        this.renderer.setAnimationLoop(this.render.bind(this));
        this.render();
        this.onTick();
    }
    render(){
        this.renderer.render(this.scene,this.camera);
    }
    onTick(){
    }
}
//===============================================================
// ThreeWorld extend BasicView
//===============================================================
class ThreeWorld extends BasicView{
    constructor(){
        super();
        this.initThreeWorld();
    }
    initThreeWorld(){
        this.radius = 0.1;
        this.count = 0;
        this.clock = new THREE.Clock();

        this.checkDevice();
        this.setLoading();
        this.startRendering();
    }

    //開発用にPCで動作確認できるように設定
    checkDevice(){
        const ua = window.navigator.userAgent.toLowerCase();
        let _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);
        _Pc = /windows|mac/.test(ua);

        if(_Pc || _iOS || _Tablet){
            //OrbitControlsを初期化
            const orbitControls = new OrbitControls(this.camera,this.renderer.domElement);
            orbitControls.target.set(
                this.camera.position.x + 0.01,
                this.camera.position.y + 1,
                this.camera.position.z
            );
        }else{
            //VRを許可
            this.renderer.vr.enabled = true;
        }
        document.addEventListener('touchmove', function(e) {e.preventDefault();}, {passive: false});
    }

    //ローディング画面
    setLoading(){
        const _this = this;
        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');
            _this.texture = new THREE.Texture(image);
            _this.texture.needsUpdate = true;

            //ローディング画面を消去
            TweenMax.to("#loader_wrapper" , 1 , {
                opacity:0,
                onComplete: function(){
                    document.getElementById("loader_wrapper").style.display ="none";
                }
            });

            //ローディング後、実行
            _this.initObject();
            _this.initLight();
            _this.initController();
        });

        loadQueue.loadManifest(manifest);
    }

    initObject(){
        //床を生成
        this.texture.repeat.set(50, 50);
        this.texture.wrapS = this.texture.wrapT = THREE.RepeatWrapping;
        this.texture.magFilter = THREE.NearestFilter;
        let geometry = new THREE.PlaneGeometry(250,250);
        let material = new THREE.MeshStandardMaterial({map:this.texture,roughness:0.0,metalness:0.6});

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

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

        //ボールを生成
        geometry = new THREE.IcosahedronBufferGeometry(this.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;
            this.room.add(object);
        }
    }

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

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

    //コントローラの設定
    initController(){
        this.controller = this.renderer.vr.getController(0);
        this.controller.addEventListener('selectstart',onSelectStart);
        this.controller.addEventListener('selectend',onSelectEnd);
        this.scene.add(this.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});
        this.controller.add(new THREE.Line(geometry,material));

        this.controller.userData.isSeleting = false;

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

    //コントローラーの制御
    handleCotroller(controller){
        if(this.controller && this.controller.userData.isSeleting){
            let object = this.room.children[this.count++];
            object.position.copy(this.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(this.controller.quaternion);

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

    //アニメーション
    onTick(){
        const delta = this.clock.getDelta() * 0.8;
        const range = 20 - this.radius;

        if(this.room){
            for(let i=0; i < this.room.children.length; i++){
                let object = this.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 < this.radius || object.position.y > 20*2){
                    object.position.y = Math.max( object.position.y, this.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;
            }
        }
        this.handleCotroller(this.controller);
    }
}
//===============================================================
// Window load
//===============================================================
window.addEventListener("load", function () {
   const threeWorld = new ThreeWorld();
});

完成したデモになります。VRヘッドセットでアクセスして、トリガーを押すとボールが発射されます!

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

関連記事

前の記事へ

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

次の記事へ

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