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); }
完成したデモになります。ヘッドマウントディスプレイで確認すると、ボタンを押すとボールが発射されます!