かつて、
controllerModel.motionController.data
にアクセスすることで強引に取得していた、トリガー、A、B、X、Yボタンの状態ですが、VRボタンを、Three.jsのVRButton.jsからImmersive Webのwebxr-button.jsへ切り替えたところ、スマートに取得できるようになったので、その方法をまとめます。
Immersive Webのwebxr-button.jsの設置方法は、こちらの記事を参考にしてみてください。
一言で言えば、renderer.xr.setSessionにsessionを渡すだけです。
コントローラーの状態の取得
ざっくりした手順としては、
❶ sessionのinputsourceschangeイベントにコールバック関数を設定
function onSessionStarted(session) { renderer.xr.setSession(session); session.addEventListener('inputsourceschange', handleInputSourceChange); }
❷ コールバック関数内でgamepadを取得
function handleInputSourceChange({ added, session }) { session.requestAnimationFrame(tick); function tick() { const sources = added.map((source) => { const buttons = source.gamepad.buttons.map(({ pressed, touched, value }) => { return { pressed, touched, value }; }); return { gamepad: { axes: source.gamepad.axes, buttons }, handedness: source.handedness }; }); console.log(sources); // => コントローラーの状態がログに表示される session.requestAnimationFrame(tick); } }
という流れです。
実際はログに表示するだけでなく、コントローラーの状態をどうにかして外部に伝える必要があります。
ソースコード
WebXrButtonWithGamepadEvent.jsx
import styled from 'styled-components'; import { useEffect, useRef, useState } from 'react'; import { WebXRButton } from '~/scripts/immersive/webxr-button'; const Wrapper = styled.span` position: relative; &[data-renderer='false'] { .webvr-ui-title { display: none !important; } .webvr-ui-svg { display: none; } } &[data-renderer='true'] { .webvr-ui-svg-error { display: none; } } button { display: flex; flex-direction: row-reverse; align-items: center; justify-content: center; width: 176px; height: 56px; background: #FAFAFA; cursor: pointer; } .webvr-ui-title { margin-left: 8px; } `; export default function WebXrButtonWithGamepadEvent({ renderer, onChangeControllerState = (evt) => {} }) { const wrapperRef = useRef(null); const [ xrButton, setXrButton ] = useState(null); const [ supportedXr, setSupportedXr ] = useState(false); const [ controllerState, setControllerState ] = useState('{ "sources": [] }'); useEffect(() => { if (!!renderer) { setXrButton(new WebXRButton({ color: '#212121', injectCSS: false, onRequestSession: onRequestSession, onEndSession: onEndSession })); if (navigator.xr) { (async () => { const supported = await navigator.xr.isSessionSupported('immersive-vr'); setSupportedXr(supported); })(); } } }, [renderer]); useEffect(() => { if (!xrButton) { return; } xrButton.enabled = supportedXr; if (xrButton.enabled && wrapperRef.current) { wrapperRef.current.innerHTML = ''; wrapperRef.current.appendChild(xrButton.domElement); } }, [xrButton, supportedXr, wrapperRef]); useEffect(() => { const { sources } = JSON.parse(controllerState); onChangeControllerState(sources); }, [controllerState]); function onRequestSession() { if (!renderer) { return; } const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking' ] }; navigator.xr.requestSession('immersive-vr', sessionInit).then(onSessionStarted); } function onSessionStarted(session) { renderer.xr.setSession(session); session.addEventListener('inputsourceschange', handleInputSourceChange); } function onEndSession(session) { session.end(); } function handleInputSourceChange({ added, session }) { session.requestAnimationFrame(tick); function tick() { const sources = added.map((source) => { const buttons = source.gamepad.buttons.map(({ pressed, touched, value }) => { return { pressed, touched, value }; }); return { gamepad: { axes: source.gamepad.axes, buttons }, handedness: source.handedness }; }); setControllerState(JSON.stringify({ sources })); session.requestAnimationFrame(tick); } } return ( <Wrapper ref={ wrapperRef } data-renderer={ !!renderer } className="web-xr-button" ></Wrapper> ); }
親から授かったonChangeControllerStateをコントローラーの状態を引数にして実装します。
useStateを使っているので、実行されるのはコントローラーの状態に変更があった時のみです。
使い方
<WebXrButtonWithGamepadEvent renderer={ renderer } onChangeControllerState={ handleChangeControllerState } />
という感じで、Three.jsのrendererとコントローラーの状態に変更があった際のコールバックを渡して使う想定です。
DEMO
r127
r136
VRモードに切り替えて、コントローラを操作すると、状態が更新されます。
右手のコントローラーの状態は右に、左手のコントローラーの状態は左に表示されます。
ほぼほぼ生のデータを使っているので、不要な情報も取得してしまっていますが、一旦これで満足してます。