2018年11月13日 - WebVR・Three.js
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.SphereGeometry(radius,32,32);
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);
renderer.setClearColor(new THREE.Color(0x000000));
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.SphereGeometry(radius,32,32);
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);
}
完成したデモになります。ヘッドマウントディスプレイで確認すると、ボタンを押すとボールが発射されます!

