使用 VR 控制器与 WebVR
已弃用:此功能不再推荐。尽管某些浏览器可能仍然支持它,但它可能已被从相关的 Web 标准中删除,可能正在被删除,或者可能仅出于兼容性目的而保留。避免使用它,并在可能的情况下更新现有代码;请参阅此页面底部的兼容性表 来指导您的决定。请注意,此功能可能随时停止工作。
许多 WebVR 硬件设置都配有控制器,这些控制器与头戴式设备一起使用。这些控制器可以通过游戏手柄 API 在 WebVR 应用程序中使用,特别是通过游戏手柄扩展 API 访问控制器姿态、触觉执行器 等。本文介绍了基础知识。
注意:WebVR API 被WebXR API 取代。WebVR 从未被批准为标准,在很少的浏览器中实现了并默认启用,并且支持少量的设备。
WebVR API
WebVR API 是 Web 平台上一个新兴的、非常有趣的功能,它允许开发人员创建基于 Web 的虚拟现实体验。它通过提供对连接到您计算机的 VR 头戴式设备的访问权限来实现这一点,这些头戴式设备被视为VRDisplay
对象,您可以对其进行操作以启动和停止对显示器的呈现,查询运动数据(例如方向和位置),这些数据可以用于在动画循环的每一帧更新显示器等等。
在阅读本文之前,您应该真正熟悉 WebVR API 的基础知识 - 如果您还没有阅读,请先阅读使用 WebVR API,其中还详细说明了浏览器支持和所需的硬件设置。
游戏手柄 API
游戏手柄 API 是一个相当受支持的 API,它允许开发人员访问连接到您计算机的游戏手柄/控制器,并使用它们来控制 Web 应用程序。基本的游戏手柄 API 提供对连接的控制器的访问权限,这些控制器被视为Gamepad
对象,然后可以查询这些对象以找出哪些按钮被按下,哪些摇杆(轴)在任何时间点被移动等等。
您可以在使用游戏手柄 API 和使用游戏手柄 API 实现控制 中找到有关基本游戏手柄 API 用法的更多信息。
但是,在本文中,我们将主要关注游戏手柄扩展 API 提供的一些新功能,这些功能允许访问高级控制器信息,例如位置和方向数据,控制触觉执行器(例如振动硬件)等等。此 API 非常新,目前仅在 Firefox 55+ Beta/Nightly 频道中默认支持和启用。
控制器类型
您在 VR 硬件中会遇到两种类型的控制器
- 6DoF(六自由度)控制器提供对位置和方向数据的访问 - 它们可以通过移动和旋转来操作 VR 场景及其包含的对象。HTC VIVE 控制器就是一个很好的例子。
- 3DoF(三自由度)控制器提供方向数据,但不提供位置数据。Google Daydream 控制器就是一个很好的例子,它可以旋转以指向 3D 空间中的不同事物,就像激光笔一样,但不能在 3D 场景中移动。
基本控制器访问
现在让我们来看一些代码。首先让我们看看如何通过游戏手柄 API 访问 VR 控制器。这里有一些奇怪的细微差别需要注意,因此值得一看。
我们已经写了一个简单的示例来演示 - 请查看我们的vr-controller-basic-info 源代码(也可以在这里查看运行示例)。此演示输出有关连接到您计算机的 VR 显示器和游戏手柄的信息。
获取显示器信息
第一个值得注意的代码如下
let initialRun = true;
if (navigator.getVRDisplays && navigator.getGamepads) {
info.textContent = "WebVR API and Gamepad API supported.";
reportDisplays();
} else {
info.textContent =
"WebVR API and/or Gamepad API not supported by this browser.";
}
这里我们首先使用一个跟踪变量 initialRun
,以表明这是我们第一次加载页面。您稍后将了解有关它的更多信息。接下来,我们检测 WebVR 和游戏手柄 API 是否受支持,方法是检查Navigator.getVRDisplays()
和Navigator.getGamepads()
方法是否存在。如果存在,我们将运行我们的 reportDisplays()
自定义函数来启动该过程。此函数如下所示
function reportDisplays() {
navigator.getVRDisplays().then((displays) => {
console.log(`${displays.length} displays`);
displays.forEach((display, i) => {
const cap = display.capabilities;
// cap is a VRDisplayCapabilities object
const listItem = document.createElement("li");
listItem.innerText = `
VR Display ID: ${display.displayId}
VR Display Name: ${display.displayName}
Display can present content: ${cap.canPresent}
Display is separate from the computer's main display: ${cap.hasExternalDisplay}
Display can return position info: ${cap.hasPosition}
Display can return orientation info: ${cap.hasOrientation}
Display max layers: ${cap.maxLayers}`;
listItem.insertBefore(
document.createElement("strong"),
listItem.firstChild,
).textContent = `Display ${i + 1}`;
list.appendChild(listItem);
});
setTimeout(reportGamepads, 1000);
// For VR, controllers will only be active after their corresponding headset is active
});
}
此函数首先使用基于 Promise 的 Navigator.getVRDisplays()
方法,该方法解析为一个数组,其中包含代表已连接显示器的 VRDisplay
对象。接下来,它会打印出每个显示器的 VRDisplay.displayId
和 VRDisplay.displayName
值,以及显示器关联的 VRDisplayCapabilities
对象中包含的一些有用值。其中最有用的是 hasOrientation
和 hasPosition
,它们允许您检测设备是否可以返回方向和位置数据,并相应地设置您的应用程序。
此函数中包含的最后一行是 setTimeout()
调用,它在 1 秒延迟后运行 reportGamepads()
函数。为什么要这样做?首先,VR 控制器只有在关联的 VR 头戴设备处于活动状态后才会准备就绪,因此我们需要在调用 getVRDisplays()
并返回显示器信息后调用它。其次,Gamepad API 比 WebVR API 旧得多,并且不是基于 Promise 的。如您稍后将看到的,getGamepads()
方法是同步的,它会立即返回 Gamepad
对象 - 它不会等待控制器准备好报告信息。除非您等待一段时间,否则返回的信息可能不准确(至少,这是我们在测试中发现的)。
获取 Gamepad 信息
reportGamepads()
函数如下所示
function reportGamepads() {
const gamepads = navigator.getGamepads();
console.log(`${gamepads.length} controllers`);
for (const gp of gamepads) {
const listItem = document.createElement("li");
listItem.classList = "gamepad";
listItem.innerText = `
Associated with VR Display ID: ${gp.displayId}
Gamepad associated with which hand: ${gp.hand}
Available haptic actuators: ${gp.hapticActuators.length}
Gamepad can return position info: ${gp.pose.hasPosition}
Gamepad can return orientation info: ${gp.pose.hasOrientation}`;
listItem.insertBefore(
document.createElement("strong"),
}),
listItem.firstChild,
).textContent = `Gamepad ${gp.index}`;
list.appendChild(listItem);
}
initialRun = false;
}
它与 reportDisplays()
的工作方式类似 - 我们使用非基于 Promise 的 getGamepads()
方法获取 Gamepad
对象的数组,然后循环遍历每个对象并打印出有关每个对象的信息
属性与控制器关联的头戴设备的Gamepad.displayId
displayId
相同,因此对于将控制器和头戴设备信息关联起来非常有用。
属性是唯一标识每个已连接控制器的数字索引。Gamepad.index
返回控制器预期握持的哪只手。Gamepad.hand
返回控制器中可用的触觉执行器的数组。这里我们返回它的长度,以便我们可以看到每个执行器有多少个可用。Gamepad.hapticActuators
- 最后,我们返回
和GamepadPose.hasPosition
来显示控制器是否可以返回位置和方向数据。它与显示器的工作方式相同,只是在游戏手柄的情况下,这些值在姿势对象上可用,而不是功能对象上。GamepadPose.hasOrientation
请注意,我们还为每个包含控制器信息的列表项提供了 gamepad
的类名。我们将在后面解释这是什么。
这里要做的最后一件事是将 initialRun
变量设置为 false
,因为初始运行现在已经结束。
Gamepad 事件
为了完成本节,我们将研究与游戏手柄相关的事件。我们需要关注的事件有两个 -
和 gamepadconnected
- 它们的功能非常明显。gamepaddisconnected
在我们的示例的末尾,我们首先包含 removeGamepads()
函数
function removeGamepads() {
const gpLi = document.querySelectorAll(".gamepad");
for (let i = 0; i < gpLi.length; i++) {
list.removeChild(gpLi[i]);
}
reportGamepads();
}
此函数获取对所有类名为 gamepad
的列表项的引用,并将它们从 DOM 中删除。然后,它重新运行 reportGamepads()
以使用已连接控制器的更新列表填充列表。
removeGamepads()
将通过以下事件处理程序在每次连接或断开游戏手柄时运行
window.addEventListener("gamepadconnected", (e) => {
info.textContent = `Gamepad ${e.gamepad.index} connected.`;
if (!initialRun) {
setTimeout(removeGamepads, 1000);
}
});
window.addEventListener("gamepaddisconnected", (e) => {
info.textContent = `Gamepad ${e.gamepad.index} disconnected.`;
setTimeout(removeGamepads, 1000);
});
我们在这里有 setTimeout()
调用 - 就像我们在脚本开头的初始化代码中所做的那样 - 以确保游戏手柄在 reportGamepads()
在每种情况下被调用时准备好报告其信息。
但还有一点要注意 - 您会看到在 gamepadconnected
处理程序中,只有在 initialRun
为 false
时才会运行超时。这是因为如果您的游戏手柄在文档首次加载时连接,则会为每个游戏手柄触发一次 gamepadconnected
,因此 removeGamepads()
/reportGamepads()
将运行多次。这会导致结果不准确,因此我们只希望在初始运行后,而不是在初始运行期间,在 gamepadconnected
处理程序内运行 removeGamepads()
。这就是 initialRun
的作用。
介绍一个真实演示
现在让我们看一下在实际的 WebVR 演示中使用的 Gamepad API。您可以在 raw-webgl-controller-example (请在此处查看) 中找到此演示。
与我们的 raw-webgl-example 完全相同(有关详细信息,请参阅 使用 WebVR API),它渲染一个旋转的 3D 立方体,您可以选择在 VR 显示器中呈现它。唯一的区别是,在 VR 呈现模式下,此演示允许您通过移动 VR 控制器来移动立方体(原始演示在您移动 VR 头戴设备时移动立方体)。
我们将在下面探讨此版本的代码差异 - 请参阅 webgl-demo.js。
访问游戏手柄数据
在 drawVRScene()
函数中,您会找到以下代码段
const gamepads = navigator.getGamepads();
const gp = gamepads[0];
if (gp) {
const gpPose = gp.pose;
const curPos = gpPose.position;
const curOrient = gpPose.orientation;
if (poseStatsDisplayed) {
displayPoseStats(gpPose);
}
}
在这里,我们使用
获取已连接的游戏手柄,然后将检测到的第一个游戏手柄存储在 Navigator.getGamepads
gp
变量中。由于我们只需要一个游戏手柄来演示,因此我们将忽略其他游戏手柄。
接下来要做的是获取存储在 gpPose 中的控制器的 GamepadPose
对象(通过查询
),并将当前帧的游戏手柄位置和方向存储在变量中,以便稍后轻松访问它们。我们还使用 Gamepad.pose
displayPoseStats()
函数在 DOM 中显示此帧的姿势统计信息。所有这些操作只有在 gp
实际上有值(如果连接了游戏手柄)时才会进行,这将阻止演示在没有连接游戏手柄的情况下出错。
在代码的稍后位置,您可以找到以下代码块
if (gp && gpPose.hasPosition) {
mvTranslate([
0.0 + curPos[0] * 15 - curOrient[1] * 15,
0.0 + curPos[1] * 15 + curOrient[0] * 15,
-15.0 + curPos[2] * 25,
]);
} else if (gp) {
mvTranslate([0.0 + curOrient[1] * 15, 0.0 + curOrient[0] * 15, -15.0]);
} else {
mvTranslate([0.0, 0.0, -15.0]);
}
在这里,我们根据从已连接控制器接收的
和 position
数据来更改屏幕上立方体的位置。这些值(存储在 orientation
curPos
和 curOrient
中)是包含 X、Y 和 Z 值的
(这里我们只使用 [0],即 X,和 [1],即 Y)。Float32Array
如果 gp
变量中包含 Gamepad
对象,并且它可以返回位置值(gpPose.hasPosition
),表示 6DoF 控制器,我们将使用位置和方向值修改立方体位置。如果只有前者为真,表示 3DoF 控制器,我们将使用方向值来修改立方体位置。如果未连接游戏手柄,我们将完全不修改立方体位置。
显示游戏手柄姿势数据
在 displayPoseStats()
函数中,我们从传递给它的 GamepadPose
对象中获取所有我们想要显示的数据,然后将它们打印到演示中用于显示此类数据的 UI 面板中
function displayPoseStats(pose) {
const pos = pose.position;
const formatCoords = ([x, y, z]) =>
`x ${x.toFixed(3)}, y ${y.toFixed(3)}, z ${z.toFixed(3)}`;
posStats.textContent = pose.hasPosition
? `Position: ${formatCoords(pose.position)}`
: "Position not reported";
orientStats.textContent = pose.hasOrientation
? `Orientation: ${formatCoords(pose.orientation)}`
: "Orientation not reported";
linVelStats.textContent = `Linear velocity: ${formatCoords(
pose.linearVelocity,
)}`;
angVelStats.textContent = `Angular velocity: ${formatCoords(
pose.angularVelocity,
)}`;
linAccStats.textContent = pose.linearAcceleration
? `Linear acceleration: ${formatCoords(pose.linearAcceleration)}`
: "Linear acceleration not reported";
angAccStats.textContent = pose.angularAcceleration
? `Angular acceleration: ${formatCoords(pose.angularAcceleration)}`
: "Angular acceleration not reported";
}
总结
本文为您介绍了如何在 WebVR 应用程序中使用 Gamepad 扩展来使用 VR 控制器。在实际的应用程序中,您可能会有一个更复杂的控制系统,其中控件分配给 VR 控制器上的按钮,并且显示器会同时受到显示器姿势和控制器姿势的影响。但是,在这里,我们只想隔离这些纯 Gamepad 扩展部分。