输入和输入源
完整的 WebXR 体验不仅仅是向用户展示一个完全虚拟的场景或通过添加或改变周围的世界来增强现实。为了创造一种令人满意和引人入胜的体验,用户需要能够与之交互。为此,WebXR 提供了对各种输入设备的支持。
在本指南中,我们将介绍如何使用 WebXR 的输入设备管理功能来确定哪些输入源可用,以及如何监视这些源的输入以处理用户与虚拟或增强环境的交互。
WebXR 中的输入
从根本上说,WebXR 中的输入分为两大类:目标和操作。目标是指用户输入在空间中的一个点。这可能涉及用户点击屏幕上的某个点、跟踪他们的眼睛,或使用操纵杆或运动感应控制器移动光标。
操作包括选择操作(例如单击按钮)和挤压操作(例如拉动触发器或在佩戴触觉手套时收紧握力)。
通过将这两种类型的输入与通过头显或其他机制改变观看位置和/或方向相结合,您可以创建一个交互式的模拟环境。
输入设备类型
WebXR 支持各种不同类型的设备来处理目标和操作输入。这些设备包括但不限于
- 屏幕点击(特别是但不一定仅限于手机和平板电脑)可用于同时执行目标和选择操作。
- 运动感应控制器,使用加速计、磁力计和其他传感器进行运动跟踪和目标定位,并且还可以包含任意数量的按钮、操纵杆、拇指垫、触控板、力传感器等,以提供用于目标和选择的其他输入源。
- 可挤压的触发器或手套握把垫,以提供挤压操作。
- 使用语音识别进行语音命令。
- 空间跟踪的关节手,例如有线手套可以提供目标和挤压操作,以及如果配备按钮或其他选择操作来源的选择操作。
- 单按钮点击设备。
- 注视跟踪(跟踪眼睛的运动以选择目标)。
输入源
每个 WebXR 输入数据源都由一个XRInputSource
对象表示,该对象描述了输入源及其当前状态。每个输入源的信息包括它握在哪个手中(如果适用)、它使用什么目标方法、XRSpace
s 可用于绘制目标射线并找到目标对象或位置以及在用户手中绘制对象,以及指定在用户查看区域中表示控制器的首选方式以及输入如何操作的配置文件字符串。
输入源的基本功能是
- 目标定位
-
监视方向控制(例如运动感应指针或操纵杆或触控板)以瞄准某个方向,可能瞄准某个目标,尽管目标定位需要您自己实现。有关更多信息,请参阅方向和目标。
- 选择
-
使用控制器上的主“选择”按钮或其他输入来选择目标方向(或其指向的对象),或以其他方式启动操作。有关主要操作的详细信息,请参阅主要操作。
- 挤压
-
挤压控制器或控制器上的某个机制以启动辅助操作。部分主要挤压操作更详细地描述了这一点。
WebXR 控制器可能具有的任何其他功能都可以通过输入源的gamepad
对象访问。此对象提供了对控制器所有可能包含的按钮、轴、触控板等内容的访问。请参阅使用游戏手柄对象的高级控制器以了解如何使用这些控制器。
输入源的实例属性
每个单独的XRInputSource
都有一组属性来描述输入的可用轴和按钮、用户握在哪个手中以及如何使用输入源来处理 3D 空间内的目标定位。
手性
手性,由XRInputSource
属性handedness
指示,是一个字符串,指示控制器位于查看者的哪个手中:left
或right
。如果控制器不是手持式,或者不知道控制器位于哪个手中,它也可以设置为none
。
手性可用于各种用途,包括选择适当的网格以在视图中表示控制器,以及如果在显示器上绘制手,则有助于将其呈现到正确的手中。如果您的应用程序使用“主要手”和“辅助手”的概念来确定控制器的功能,它也可能很有用;例如,在游戏中,主要手控制器可能是玩家的武器,而辅助手控制器可能用于控制盾牌的位置。
目标射线模式
目标射线模式是targetRayMode
属性中的一个字符串。它描述了用于确定目标射线以及如何在视觉上呈现时向用户显示它的技术。
当目标射线模式为gaze
时,射线的原点位于查看器处,并指向用户面对的方向。这种注视输入方法相当简单,不需要任何特殊的控件,因为它将基于头显或任何用于确定查看者面部指向哪个方向的设备报告的方向。目标射线应从眼睛之间以垂直于查看者面部方向的方向向外延伸。
更灵活的是tracked-pointer
模式,其中射线的原点位于手持式控制器或手部跟踪系统的原点,并沿控制器指向的方向向外延伸。射线以由正在使用的平台和控制器定义的方向向外延伸(如果已定义);否则,如果当前伸出,射线将以用户用食指指向的相同方向延伸。
第三种也是最后一种目标光线模式最常见于移动设备,如智能手机和平板电脑。screen
模式表示目标光线是根据用户以某种方式与屏幕交互来确定,用户与 WebXR 上下文交互(很可能是查看者点击屏幕或用手指拖动目标光线)。
目标光线空间
用于描述目标光线的位置和方向的XRSpace
位于targetRaySpace
属性中。此空间的原生原点位于发射目标光线的位置(例如,控制器的前面尖端,或者如果控制器被渲染成枪支,则为枪管的末端),并且该空间的方向向量沿着目标光线的路径向外延伸。
您可以使用XRFrame
的getPose()
方法,轻松获取给定帧的绘图处理程序中与targetRaySpace
对应的目标光线。返回的XRPose
的transform
是与目标光线对应的变换。因此,对于输入控制器primaryInput
let targetRayPose = frame.getPose(primaryInput.targetRaySpace, viewerRefSpace);
let targetRayOrigin = targetRayPose.transform.position;
let targetRayVector = targetRayPose.transform.orientation;
有了这些,您现在就有了发射目标光线的位置(targetRayOrigin
)以及它指向的方向(targetRayVector
),这些都在查看者的参考空间(viewerRefSpace
)中给出。这就是您能够绘制目标光线、确定指向什么、进行命中测试等等所需的一切。
抓握空间
输入源的gripSpace
属性是一个XRSpace
,您可以使用它来渲染对象,使它们看起来像是被握在查看者的手中。
图:左手抓握空间的坐标系。 图:右手抓握空间的坐标系。
抓握空间的原生原点位于玩家拳头中心附近,在输入源的局部坐标系中为(0,0,0),而由gripSpace
指定的XRSpace
可随时用于将输入源空间中的坐标或向量转换为世界坐标(反之亦然)。
这意味着,如果您使用 3D 模型来表示您的控制器、玩家角色的手或任何其他代表控制器在空间中的位置的东西,则gripSpace
可以用作变换矩阵,该矩阵可以正确地定位和方向对象的模型以进行渲染。为此,需要使用变换将抓握空间转换为 WebGL 用于渲染目的的世界坐标系。
图:将抓握空间映射到世界坐标系。距离x、y和z共同构成对应于抓握空间G原点的世界坐标(x,y,z)。
在上图中,我们看到了抓握空间,其原点位于G处,位于用户抓握控制器的中点,指向远离用户的方向,平行于z轴。相对于世界空间的原点W,抓握空间的原点位于右侧x个单位、上方y个单位和更远处z个单位。鉴于轴的方向性,抓握空间的坐标可以用世界坐标表示为(x,y,-z);z为负值,因为抓握空间沿z轴更远,因此方向为负。
如果控制器反而位于世界空间原点的左侧且更靠近用户(或者可能位于用户身后,如果用户位于原点,尽管这是一种不舒服的握持控制器的方式),则坐标的x值将为负,但z值为正。除非控制器移到世界空间原点下方,否则y的值仍将为正。
下图显示了这种情况,其中控制器位于世界空间原点的下方和左侧,并且控制器也移动到比原点更靠近我们的位置。结果,x和y的值都为负,而z为正。
图 将抓握空间映射到世界原点,当控制器位于世界原点的下方和左侧,并且比世界原点更靠近我们时。
游戏手柄记录
每个输入源都有一个gamepad
属性,如果它不为NULL
,则它是一个Gamepad
对象,描述了控制器上可用的各种控件和小部件。如果输入设备只有主要的运动传感器、挤压控件和按钮,它可能没有Gamepad
记录。但是,如果gamepad
存在,您可以使用它来识别和轮询控制器上可用的按钮和轴。
虽然Gamepad
记录由Gamepad API规范定义,但它实际上并非由 Gamepad API 管理,并且其功能与 Gamepad API 不完全相同。有关更详细的信息,请参阅使用游戏手柄对象的先进控制器。
配置文件字符串
每个输入源可以具有零个或多个输入配置文件名称字符串,这些字符串位于数组profiles
中,每个字符串都描述了输入源在 3D 世界中的首选视觉表示以及输入源的功能。在下面的输入配置文件中简要介绍了这些配置文件的使用。
瞬态输入源
某些设备可能会创建瞬态输入源,以便与实际上并非来自该设备的操作一起使用,但呈现为来自该设备。例如,如果 XR 设备提供了一种使用鼠标模拟设备上事件的模式,则可能会创建一个新的XRInputSource
对象来表示在处理操作期间模拟的输入源。
这是因为在标准输入设备和 XR 输入源之间保持了分离。在每个瞬态操作期间,使用人工源来表示外部源。
管理输入源
当多个输入源可用时,您需要能够获取有关每个输入源的信息,包括其位置和方向、其目标光线(如果适用于您的需求)以及可以帮助您确定如何以视觉方式呈现输入源(如果有)的详细信息。您还需要能够确定将哪个输入源用于哪些活动;例如,如果用户有两个控制器,哪个控制器将用于操作 UI 元素,或者两个都将用于操作?
因此,要管理输入源,您需要能够枚举输入源、检查每个输入源的配置文件信息,并确定如何使用每个输入控制器。
枚举输入源
由XRSession
对象表示的 WebXR 会话具有一个inputSources
属性,该属性是当前连接到 XR 系统的 WebXR 输入设备的实时列表。
let inputSourceList = xrSession.inputSources;
由于表示列表中每个输入源的XRInputSource
对象的内容是只读的,因此 WebXR 系统通过删除源的记录并添加一个新记录来替换它,从而对这些输入进行更改。每当一个或多个输入源更改时,或者当输入源添加到列表中或从列表中删除时,都会向您的XRSession
发送inputsourceschange
事件。
例如,如果您需要跟踪哪个控制器握在玩家的每一只手中,您可以执行以下操作:
let inputSourceList = NULL;
let leftHandSource = NULL;
let rightHandSource = NULL;
xrSession.addEventListener("inputsourceschange", (event) => {
inputSourceList = event.session.inputSources;
inputSourceList.forEach((source) => {
switch (source) {
case "left":
leftHandSource = source;
break;
case "right":
rightHandSource = source;
break;
}
});
});
inputsourceschange
事件在会话的创建回调首次完成执行时也会触发一次,因此您可以使用它在启动时获取输入源列表,一旦它可用。该事件作为XRInputSourcesChangeEvent
传递,其中包含三个感兴趣的属性
session
-
输入源已更改的
XRSession
。 added
-
零个或多个
XRInputSource
对象的数组,指示已新添加到 XR 系统的输入源。 removed
-
零个或多个
XRInputSource
对象的数组,指示已从 XR 系统中删除的任何输入源。
识别输入的配置文件
每个输入源都有一个profiles
属性,该属性包含应用于输入源的 WebXR 输入配置文件的实时列表,按照从最具体到最不具体的顺序排列。
为了执行任何涉及配置文件扫描的有意义的操作(超出基本功能识别),您可能需要从WebXR 输入配置文件注册表导入 JSON 配置文件数据库。
有关使用输入配置文件的更具体详细信息,请参阅输入配置文件。
选择主控制器
为了避免多个控制器同时无意中操作 UI 而导致的问题,您的应用可能需要一个“主”控制器。此控制器不仅负责点击应用的用户界面,而且还被视为“主要手”,而其他控制器则被视为“辅助手”或其他控制器。
注意:这并不意味着您的应用需要决定一个主控制器。但如果需要,以下策略可能会有所帮助。
您可以通过几种方法来决定哪个控制器为主控制器。我们将介绍三种。
手性
决定哪个控制器为主控制器最直接的方法是让用户定义一个“手性”首选项,用户设置该首选项以指示其哪只手是惯用手。然后,您将查看每个输入源并找到与之匹配的一个(如果可用),如果该手中没有控制器,则回退到另一个控制器。
const primaryInputSource =
xrSession.inputSources.find((src) => src.handedness === user.handedness) ??
xrSession.inputSources[0];
这段代码首先假设第一个输入源是主要的,然后查找其handedness
与user
对象中指定的匹配项。如果匹配,则选择该输入源作为主要输入源。
首次使用
另一种选择是使用用户首先触发选择操作的第一个输入。下面的代码首先假设第一个输入源是主要的,然后为select
事件建立一个处理程序,该处理程序将事件的源记录为主输入源。然后,select
事件处理程序被替换为函数realSelectHandler()
,该函数将用于处理所有将来的select
事件。然后,我们将事件传递给realSelectHandler()
,以允许事件像往常一样被处理。
let primaryInputSource = xrSession.inputSources[0];
xrSession.onselect = (event) => {
primaryInputSource = event.inputSource;
xrSession.onselect = realSelectHandler;
return realSelectHandler(event);
};
其效果是,我们第一次收到select
事件时设置主输入源(无论来自哪个输入源),从那里像往常一样处理事件,并且从那时起像往常一样处理事件,而无需进一步担心哪个输入源是主要的。
用户选择
确定主输入源最复杂的方法非常灵活,但可能需要大量工作来实现。在这种情况下,您将遍历输入源列表及其配置文件,以收集有关每个输入源的信息,然后呈现一个描述每个输入的用户界面,允许用户为每个输入分配用途。做好这件事可能是一项艰巨的任务,但对于可能涉及多个用户输入的复杂应用程序来说,它可能很有用。
您需要实现此功能的大部分信息可以在下面的输入配置文件部分找到。但是,详细信息不在本文的讨论范围之内。
输入配置文件
如上所述,每个输入源都有一个输入配置文件名称列表,这些名称对应于一组描述该输入源及其使用方法的信息。这些名称位于输入源的profiles
属性中,这些配置文件字符串的官方注册表在GitHub上的WebXR Input Profiles Registry中维护。
例如,可以使用generic-trigger-squeeze-touchpad
配置文件名称通过找到值为generic-trigger-squeeze-touchpad
的profileId
字段来定位以下JSON配置文件数据。
{
"profileId": "generic-trigger-squeeze-touchpad",
"fallbackProfileIds": [],
"layouts": {
"left-right-none": {
"selectComponentId": "xr-standard-trigger",
"components": {
"xr-standard-trigger": { "type": "trigger" },
"xr-standard-squeeze": { "type": "squeeze" },
"xr-standard-touchpad": { "type": "touchpad" }
},
"gamepad": {
"mapping": "xr-standard",
"buttons": [
"xr-standard-trigger",
"xr-standard-squeeze",
"xr-standard-touchpad"
],
"axes": [
{ "componentId": "xr-standard-touchpad", "axis": "x-axis" },
{ "componentId": "xr-standard-touchpad", "axis": "y-axis" }
]
}
}
}
}
这是一个控制器,无论它位于哪只手上(即使它当前未与特定手关联),它都有三个组件:一个标准触发器、一个标准挤压输入和一个触控板。根据selectComponentId
属性,xr-standard-trigger
组件用于执行主要操作。
此外,gamepad
对象将这些输入映射到游戏手柄,将触发器、挤压和触控板点击分配给输入源的按钮列表,并将触控板的“轴”分配给轴列表。
profiles
中的列表按反向特异性排序;也就是说,最精确的描述在最前面,最不精确的描述在最后。列表中的第一个条目通常指示控制器的精确型号,或控制器兼容的型号。
例如,Oculus Touch控制器的profiles
中的条目0是oculus-touch
。下一个条目是generic-trigger-squeeze-thumbstick
,表示具有触发器、挤压控制和拇指摇杆的通用设备。虽然Oculus Touch控制器实际上有一个触控板而不是拇指摇杆,但总体描述“足够接近”,因此与名称匹配的配置文件中的详细信息将允许有用地解释控制器。
操作
在WebXR中,**操作**是一种特殊类型的事件,由用户激活控制器上的特殊按钮触发。任何额外的按钮(以及诸如轴控制器(例如操纵杆)之类的东西)都仅通过XRInputSource
属性gamepad
进行管理。有关支持这些额外控件和按钮的更多详细信息,请参阅下面的使用游戏手柄对象的高级控制器。
**主要操作**是当用户激活服务于特殊目的的主要控制元素时触发的操作。目前有两种主要操作类型
- **主要操作**是当用户激活控制器上的主要或“选择”输入时激活的操作。此输入可以是按钮、触发器、触控板点击或点击、语音命令或特殊手势,或者可能是其他某种形式的输入。例如,在具有可点击触控板的手柄控制器、触发器控件以及后退和“菜单”按钮的情况下,点击触控板可能是主要操作。某些控制器可能有一个标有“选择”的按钮。在游戏手柄式控制器上,“A”按钮可能是主要操作。
- **主要挤压操作**是当用户挤压控制器时触发的操作。这种“挤压”可以通过控制器中的压力传感器实际检测到,也可以使用触发器、手势或其他机制模拟。例如,如果输入控制器是触觉手套,则当用户握拳时,它可能会报告主要挤压操作已发生。
虽然给定的输入源只能有一个主要操作和一个主要挤压操作,但输入设备上可能有多个控件配置为触发每个主要操作。例如,用户可能将其控制器设置为触控板的点击和点击都生成主要操作。
下面将详细介绍这些类型的输入操作。
主要操作
每个输入源都应定义一个**主要操作**。主要操作(有时会缩写为“选择操作”)是一个特定于平台的操作,它通过按顺序传递事件selectstart
、select
和selectend
来响应用户的操作。这些事件中的每一个都是XRInputSourceEvent
类型。
注意:如果输入源没有主要操作,则该输入源被认为是**辅助输入源**。
当用户将设备指向您 3D 空间中的目标光线然后触发选择操作时,以下事件将发送到活动XRSession
- 一个
selectstart
事件,指示用户已执行开始主要操作的活动。这可能是手势、按下按钮或类似操作。 - 如果主要操作成功结束(例如,由于用户释放按钮或触发器),而不是由于错误,则发送
select
事件。 - 发送
select
事件后或正在执行操作的控制器断开连接或以其他方式不可用后,将发送selectend
事件。
一般来说,selectstart
和selectend
事件会告诉您何时可能希望向用户显示某些内容,以指示主要操作正在进行。这可能是以新的颜色绘制带有激活按钮的控制器,或者显示目标对象被抓取并四处移动,从selectstart
到达时开始,到收到selectend
时停止。
另一方面,select
事件是告诉您的代码用户已完成他们想要完成的操作的事件。这可能像投掷物体或在游戏中扣动枪的扳机一样简单,也可能像将他们拖动到世界周围的物体重新放置到新位置一样复杂。
如果您的主要操作是一个简单的触发操作,并且您不需要在触发器处于激活状态时进行任何动画,则可以忽略selectstart
和selectend
事件,并对select
事件采取行动。
xrSession.addEventListener("select", (event) => {
let inputSource = event.inputSource;
let frame = event.frame;
/* handle the event */
});
某些操作可能会非常快速地一个接一个地发送这些事件。这些事件之间经过的时间取决于导致操作的硬件设备以及解释硬件操作并将其转换为一系列事件的软件驱动程序。不要假设这些事件会在它们之间以任何特定的时间间隔发生。
例如,如果导致主要操作发生的硬件是按钮,则当用户按下按钮时,您将收到selectstart
,然后当用户释放按钮时,您将收到select
和selectend
。
许多示例展示了如何在整个文档中处理select
事件,例如本文其他地方关于目标和目标光线的部分。
主要挤压操作
**主要挤压操作**是一个特定于平台的操作,它发送XRSession
squeezestart
、squeezeend
和squeeze
事件。这通常由用户挤压控制器、做出模仿抓取东西的手势或使用(挤压)触发器产生。
事件序列与主要操作发送的事件序列相同,除了每个事件的名称。
- 一个
squeezestart
事件发送到XRSession
,指示用户已开始挤压操作。 - 如果主要挤压操作成功结束,则会话将发送
squeeze
事件。 - 然后,发送
squeezeend
事件以指示挤压操作不再进行。无论挤压操作成功与否,都会发送此事件。
主要挤压操作的两种常见用途是在 3D 世界中抓取和/或拾取物体,以及在游戏中或模拟中挤压触发器以发射武器。
示例
此示例代码显示了一组挤压事件处理程序,这些处理程序实现这些事件以管理从场景中拾取和持有物体。代码假设存在一个表示角色的avatar
对象(如本页面上的其他几个示例中所用),以及pickUpObject()
和dropObject()
函数,这些函数处理将物体从世界转移到特定手以及从手中释放物体并将其放回世界。
拾取物体:处理squeezestart事件
xrSession.addEventListener("squeezestart", (event) => {
const targetRaySpace = event.inputSource.targetRaySpace;
const hand = event.inputSource.handedness;
let targetRayPose = event.frame.getPose(targetRaySpace, viewerRefSpace);
if (!targetRayPose) {
return;
}
let targetRayTransform = targetRayPose.transform;
let targetObject = findTargetObject(targetRayTransform);
if (targetObject) {
if (avatar.heldObject[hand]) {
dropObject(hand);
}
pickUpObject(targetObject, hand);
}
});
通过获取这些姿势和变换(如往常一样)以及将输入源的handedness
获取到局部常量hand
中来处理squeezestart
事件。我们将使用它将手映射到该手中持有的物体。
然后代码识别目标对象,如果在瞄准射线上找到了对象,则拾取它。拾取对象首先要查看由avatar.heldObject[hand]
表示的手中是否已经有对象。如果该手中已经握持了对象,则通过调用dropObject()
函数将其放下。
然后调用pickUpObject()
,指定目标对象作为要从场景中移除并放置到指定hand
中的对象。pickUpObject()
还会记录对象的原始位置,以便在取消或中止挤压时将其返回到该位置。
放下对象:挤压事件处理程序
当用户通过松开手势结束挤压操作时,会收到squeeze
事件。在本例中,我们将此解释为释放当前持有的对象,将其放在目标位置的场景中。
此代码假定存在其他函数findTargetPosition()
,它沿着目标射线跟踪,直到与某个物体碰撞,然后返回碰撞发生的坐标,以及putObject()
,它将指定hand
中持有的物体放置在给定位置,并将其从手中移除。
xrSession.addEventListener("squeeze", (event) => {
const targetRaySpace = event.inputSource.targetRaySpace;
const hand = event.inputSource.handedness;
let targetRayPose = event.frame.getPose(targetRaySpace, viewerRefSpace);
if (!targetRayPose) {
return;
}
let targetRayTransform = targetRayPose.transform;
let targetPosition = findTargetPosition(targetRayTransform);
if (targetPosition) {
if (avatar.heldObject[hand]) {
putObject(hand, targetPosition);
avatar.heldObject[hand] = null;
}
}
});
与squeezestart
处理程序一样,这首先收集有关事件所需的信息,包括放下对象的手和目标射线的变换。目标射线变换被传递到假定的findTargetPosition()
函数中,以获取放置放下对象的坐标。
有了位置,我们就可以通过调用putObject()
函数放下对象,该函数以hand
和目标位置作为输入。此函数的作用是从指定的手中移除对象并将其添加回场景,其位置设置为将其放置在findTargetPosition()
返回的坐标之上。
在squeezeend处理程序中取消挤压
即使挤压失败,在挤压完成后也会收到squeezeend
事件。我们通过将当前持有的对象返回到拾取时所在的位置来处理它。
xrSession.addEventListener("squeezeend", (event) => {
const targetRaySpace = event.inputSource.targetRaySpace;
const hand = event.inputSource.handedness;
if (avatar.heldObject[hand]) {
returnObject(hand);
avatar.heldObject[hand] = null;
}
});
在这里,假设returnObject()
函数能够将指定hand
中持有的对象返回到其初始位置,就像squeezestart
事件处理程序中的pickUpObject()
记录的那样。
在这里,假设returnObject()
函数能够将指定hand
中持有的对象返回到其初始位置,就像squeezestart
事件处理程序中的pickUpObject()
记录的那样。
瞬时操作
如果XR设备在inline
模式下使用鼠标模拟控制器,则会发生以下近似顺序的操作
- 用户在呈现WebXR场景的
<canvas>
内部按下鼠标按钮。 - XR设备的驱动程序捕获鼠标事件。
- 设备创建一个新的
XRInputSource
来表示模拟的XR输入源。targetRayMode
设置为screen
,其他信息也按适当方式填写。此新的输入源临时添加到由XRSession
属性inputSources
返回的列表中。 - 浏览器传递与操作相对应的
pointerdown
事件。 - 生成主要操作并以
selectstart
事件的形式发送到应用程序,其源设置为新的XRInputSource
。或者,如果鼠标用作非主手或辅助控制器,则会发送辅助操作。 - 当用户释放鼠标按钮时,
select
事件被发送到XRSession
,然后DOM接收click
事件。然后会话接收selectend
事件,指示操作完成。 - 操作完成后,浏览器会删除瞬态输入源,并发送任何适当的
pointerup
事件。
因此,瞬态输入源确实是瞬态的——它仅在处理输入期间存在,因此不会在输入源列表中列出。
方向和目标
方向是观看者正在观看的方向。这不是使用输入源提供的。而是使用从当前动画帧的XRFrame.getViewerPose()
方法获得的XRPose
来获得。观察者姿势的变换矩阵的旋转分量是观察者的方向。
您可以在文章视点和观察者中了解更多有关如何使用观察者姿势确定方向的信息。
瞄准是用户使用输入源指向特定方向的行为。输入源的targetRaySpace
是一个XRSpace
(实际上可能是一个XRReferenceSpace
),可用于确定目标射线相对于观察者方向的方向。
这可能涉及或不涉及实际指向3D世界中的特定对象;您必须使用命中测试自己确定这一点——也就是说,检查目标射线是否与场景中的任何对象相交。
瞄准和瞄准射线
瞄准射线,其原点位于瞄准射线空间的原点,并指向用户指向控制器设备的方向。瞄准射线使用一个XRSpace
来定义,该空间的原点位于瞄准射线的源(通常是控制器的面向外的端部或其在3D世界中的表示),并且其方向具有-Z从控制器向外延伸,与XRInputSource
的gripSpace
相同的方向。
此空间在输入源的targetRaySpace
属性中找到。它可以用来确定控制器指向的方向,以及确定目标射线的原点和方向。这可以通过执行以下示例来完成,该示例实现了一个需要此信息的select
事件处理程序。像往常一样,此代码假设使用glMatrix执行矩阵和向量数学运算
xrSession.addEventListener("select", (event) => {
const targetRaySpace = event.inputSource.targetRaySpace;
let targetRayPose = event.frame.getPose(targetRaySpace, viewerRefSpace);
if (!targetRayPose) {
return;
}
let targetRayTransform = targetRayPose.transform;
let targetObject = findTargetObject(targetRayTransform);
if (targetObject) {
/* do stuff with the targeted object */
}
});
这在向量targetSourcePoint
中获取瞄准射线的原点,并在四元数targetDirection
中获取射线的方向。使用任一
这首先将目标射线空间提取到局部常量targetRaySpace
中。然后在调用XRFrame
方法getPose()
时使用它来创建一个XRPose
对象,表示目标射线在观察者参考空间viewerRefSpace
中的位置和方向。如果此值为null
,则事件处理程序将返回而不会执行任何其他操作。
目标射线的变换从姿势的transform
属性中获得,并存储在局部targetRayTransform
中。然后使用它(在本例中通过名为findTargetObject()
的函数)来查找射线与之相交的第一个对象。如果瞄准射线确实与场景中的对象相交,我们可以对其执行任何需要的操作。
如果需要剥离目标射线原点的实际位置和射线的方向性,可以这样做
const targetRayOrigin = vec3.create();
const targetRayDirection = quat.create();
mat4.getTranslation(targetRayOrigin, viewerRefSpace);
mat4.getRotation(targetRayDirection, viewerRefSpace);
要确定目标对象,请沿着目标射线跟踪,直到它与对象相交。此过程称为命中测试或碰撞检测。您采取的命中测试方法在很大程度上取决于应用程序的具体需求。第一个问题是:您是在检测与虚拟对象或地形、真实世界对象或地形,还是两者都发生碰撞?
无论哪种情况,要识别目标对象,您都需要确定由XRInputSource
属性targetRaySpace
指定的射线是否与场景中的任何对象相交,无论它们是虚拟的还是真实世界的。
有关所涉及内容的更详细说明,请参阅瞄准和命中检测。
呈现手持物体
输入源的gripSpace
属性标识一个XRSpace
,该空间描述了渲染对象时要使用的原点和方向,以便它看起来像是被握在与输入源相同的手中。此空间旨在用于绘制由对象XRInputSource
表示的手持式WebXR输入控制器的模型,但也可以用于绘制任何对象,例如球、工具或武器。我们在上面介绍了抓握空间,但让我们看看它如何用于绘制代表手或在手中的物体。
由于抓握空间的原点位于手的抓握中心,因此您可以将其用作渲染对象的起点。应用任何需要的偏移变换以将原点移至渲染对象的起点,并在应用任何需要的旋转以使模型正确对齐以与抓握空间的方向对齐时。
使用游戏手柄对象的高级控制器
一个XRInputSource
有一个gamepad
属性,其值如果非null
,则为一个Gamepad
对象,该对象提供对游戏手柄式按钮、轴控制器(如操纵杆或拇指垫)等的访问。这可能包括触发标准XRInputSource
操作的相同按钮,但也可能包含任意数量的其他按钮和控件。
注意:虽然Gamepad
由Gamepad API定义,但它不受Gamepad API管理,因此您不得尝试对它使用任何Gamepad API方法。对象类型被重复使用是为了方便起见。
如果gamepad
的值为null
,则输入源没有使用Gamepad
记录定义任何控件,因为不支持它或它上面没有添加任何控件。
此gamepad
对象不仅用于获取对特殊按钮、触控板等的访问权限,还提供了一种更直接地访问和监视用作主要选择和挤压输入的控件的方法,因为这些控件包含在其buttons
列表中。
由于这种 Gamepad
接口的使用是一种便利而非对 Gamepad API 的真正应用,因此 WebXR 中的使用方式与 Gamepad API 应用中的使用方式存在一些差异。最显著(但并非唯一)的差异是,WebXR 添加了 xr-standard
游戏手柄映射,有关其他差异,请参阅 XRInputSource.gamepad
属性。此游戏手柄映射定义了典型单手手持式 VR 控制器上的控件如何映射到游戏手柄控件。
合并来自非 WebXR 源的输入
有时,您需要一种方法让用户使用 WebXR 外部的控制器提供输入。最常见的是键盘和鼠标输入,但您也可以使用非 XR 游戏手柄设备、网络输入或其他数据源来模拟用户控制。虽然 WebXR 不支持将这些输入设备直接与 XR 场景交互,但您可以自行收集输入数据并应用它。
假设输入用于控制模拟中的化身,这是最常见的用例,WebXR 输入用于通过从非 XR 输入设备收集的数据以以下方式影响化身
- 位置
-
通过对先前已知位置应用 增量,然后用一个新的参考空间替换化身的参考空间(其变换反映了新的位置)来更改化身的位置。
- 方向
-
通过对化身绕三个轴的旋转应用增量,更新其方向向量,然后重新计算其参考空间来更改化身的方向或朝向。
- 动作
-
化身执行一个动作,例如使用物体或武器、跳跃或任何其他与基本移动和旋转无关的活动。
某些输入用于控制应用程序而不是化身。例如,一个按钮可能会打开一个用于配置应用程序的选项菜单。在该菜单打开时,原本用于控制化身的输入可能会改为用于控制菜单的界面。
使用键盘和鼠标事件
捕获键盘和鼠标的输入与在任何 Web 应用程序中一样。设置您需要处理的事件的处理程序,以便获取所需的输入。有趣的是您如何处理这些输入。
想象一个 avatar
对象,我们将使用它来跟踪有关化身及其世界观的信息。我们希望玩家能够使用 W、A、S 和 D 键向前、向左、向后和向右移动。因为我们除了 XR 硬件可能执行的操作外,还管理由键盘和鼠标定义的化身位置,所以我们需要单独维护该信息,并在渲染化身(或从化身视角渲染世界)之前将其应用为变换。
为此,我们在 avatar
对象中包含一个 posDelta
属性,其类型为 DOMPoint
,其中包含要应用于所有三个轴的偏移量,以便调整化身的位置(查看器姿态的参考空间的原点),以包含来自键盘和鼠标的移动和旋转。
键盘输入的相应代码可能如下所示
document.addEventListener("keydown", (event) => {
switch (event.key) {
case "a":
case "A":
avatar.posDelta.x -= ACCEL_X;
break;
case "d":
case "D":
avatar.posDelta.x += ACCEL_X;
break;
case "w":
case "W":
avatar.posDelta.y += ACCEL_Y;
break;
case "s":
case "S":
avatar.posDelta.y -= ACCEL_Y;
break;
default:
break;
}
});
这是一个简单的示例,其中加速度是恒定的,并且不太现实。您可以通过应用一些物理学知识来极大地增强这一点,使加速度根据按键保持的时间和其他因素随时间变化。
将输入应用于场景
现在我们有了需要应用于位置和方向的增量(在我们的示例中,在 avatar
对象的 posDelta
和 orientDelta
属性中),我们可以编写代码来应用这些更改。由于我们已经在按计划渲染场景,因此我们只需将应用这些更改的代码添加到其中,以及准备和绘制场景。
function drawFrame(time, frame) {
applyExternalInputs(avatar);
let pose = frame.getViewerPose(avatar.referenceSpace);
animationFrameRequest = session.requestAnimationFrame(drawFrame);
/* draw the frame here */
}
此处显示的 drawFrame()
函数是在需要绘制帧时调用的回调函数,这是通过调用 XRSession
方法 requestAnimationFrame()
建立的。它调用一个函数 applyExternalInputs()
,我们稍后会定义;它接收 avatar
对象并使用其信息来更新化身的参考帧。
之后,一切照常进行,从更新的参考帧获取查看器的姿态,通过 requestAnimationFrame()
请求下一个帧回调,然后继续设置 WebGL 并绘制场景。绘图和其他相关代码可以在示例 移动、方向和运动 中找到。
applyExternalInputs()
方法接收 avatar
对象,并用一个包含更新后的增量的新的参考空间替换其 referenceSpace
属性。
function applyExternalInputs(avatar) {
if (!avatar.posDelta.x && !avatar.posDelta.y && !avatar.posDelta.z) {
return; // Player hasn't moved with keyboard
}
let newTransform = new XRRigidTransform({
x: avatar.posDelta.x,
y: avatar.posDelta.y,
z: avatar.posDelta.z,
});
avatar.referenceSpace =
avatar.referenceSpace.getOffsetReferenceSpace(newTransform);
}