輸入和輸入源

一個完整的 WebXR 體驗不僅僅是向用戶展示一個完全虛擬的場景,或者透過新增或改變周圍的世界來增強現實。為了創造一個令人滿意和引人入勝的體驗,使用者需要能夠與它互動。為此,WebXR 提供了對各種輸入裝置的支援。

在本指南中,我們將探討如何使用 WebXR 的輸入裝置管理功能來確定有哪些輸入源可用,以及如何監測這些源的輸入,以便處理使用者與您的虛擬或增強環境的互動。

WebXR 中的輸入

從根本上說,WebXR 中的輸入分為兩個基本類別:目標定位和動作。目標定位是使用者輸入對空間中一個點的指定。這可能涉及使用者點選螢幕上的一個點,跟蹤他們的眼睛,或者使用操縱桿或運動感應控制器來移動游標。

動作包括選擇動作(例如點選按鈕)和擠壓動作(例如拉動扳機或戴著觸覺手套時握緊)。

透過將這兩種型別的輸入與透過頭戴裝置或其他機制改變視角位置和/或方向相結合,您可以建立一個互動式的模擬環境。

輸入裝置型別

WebXR 支援各種不同型別的裝置來處理目標定位和動作輸入。這些裝置包括但不限於

  • 螢幕點選(尤其但不僅限於手機或平板電腦)可同時用於執行目標定位和選擇。
  • 運動感應控制器,使用加速度計、磁力計和其他感測器進行運動跟蹤和目標定位,並且可能額外包含任意數量的按鈕、操縱桿、拇指墊、觸控板、力感測器等,以提供用於目標定位和選擇的額外輸入源。
  • 可擠壓的扳機或手套握把墊以提供擠壓動作。
  • 使用語音識別的語音命令。
  • 空間跟蹤的關節手,例如有線手套可以提供目標定位和擠壓動作,如果配備了按鈕或其他選擇動作來源,還可以提供選擇動作。
  • 單按鈕點選裝置。
  • 凝視跟蹤(跟隨眼睛的移動來選擇目標)。

輸入源

每個 WebXR 輸入資料來源都由一個 XRInputSource 物件表示,該物件描述輸入源及其當前狀態。每個輸入源的資訊包括它握在哪隻手(如果適用),它使用的目標定位方法,可用於繪製目標射線和查詢目標物件或位置以及在使用者手中繪製物件的 XRSpace,以及指定在使用者視野中表示控制器以及輸入如何操作的首選方式的配置檔案字串。

輸入源的基本功能是

目標定位

監測方向控制(例如,運動感應指標或操縱桿或軌跡板)以朝某個方向瞄準,可能指向某個目標,儘管目標定位由您自行實現。有關詳細資訊,請參閱朝向和目標定位

選擇

使用主“選擇”按鈕或控制器上的其他輸入來選擇目標方向(或其指向的物件),或以其他方式執行動作。有關主要動作的詳細資訊,請參閱主要動作

擠壓

擠壓控制器或控制器上的機制以執行輔助動作。有關詳細資訊,請參閱主要擠壓動作一節。

WebXR 控制器可能擁有的任何附加功能都透過輸入源的 gamepad 物件訪問。此物件提供對控制器可能包含的所有按鈕、軸、軌跡板等的訪問。請參閱使用遊戲手柄物件的高階控制器以瞭解如何使用這些控制器。

輸入源的例項屬性

每個獨立的 XRInputSource 都有一組屬性,描述了輸入的可用軸和按鈕、使用者握在哪隻手以及輸入源如何用於處理 3D 空間內的目標定位。

慣用手

慣用手,由 XRInputSource 屬性 handedness 表示,是一個字串,指示控制器在檢視者的哪隻手:leftright。如果控制器不是手持的,或者不知道控制器在哪隻手,也可以將其設定為 none

慣用手可以用於各種事物,包括選擇合適的網格來表示檢視中的控制器,如果顯示器上繪製了手,則有助於在正確的手中呈現它。如果您的應用程式使用“主手”和“副手”的概念來確定控制器的功能,它也可能很有用;例如,在遊戲中,主手控制器可能是玩家的武器,而副手控制器可能用於控制盾牌的定位。

目標射線模式

目標射線模式是一個字串,位於 targetRayMode 屬性中。它描述了用於確定目標射線以及如果視覺呈現時應如何向用戶顯示的技術。

當目標射線模式為 gaze 時,射線的原點在觀察者處,並指向使用者所面對的方向。這種凝視輸入方法相當簡單,不需要任何特殊控制,因為它將基於頭戴裝置或用於確定觀察者面部指向哪個方向的任何裝置報告的朝向。目標射線應從眼睛之間向外延伸,方向垂直於觀察者的面部。

更靈活的是 tracked-pointer 模式,其中射線的原點位於手持控制器或手部跟蹤系統的原點,並向控制器指向的方向延伸。如果已定義,射線將沿任何平臺和控制器定義的​​方向向外延伸;否則,如果使用者當前伸出食指,射線將沿使用者指向的方向延伸。

第三種也是最後一種目標射線模式最常見於智慧手機和平板電腦等移動裝置。screen 模式表示目標射線是根據使用者以某種方式與螢幕互動來確定 WebXR 上下文的——最可能是透過觀察者點選螢幕或用手指拖動目標射線。

目標射線空間

用於描述目標射線位置和方向的 XRSpace 位於 targetRaySpace 屬性中。此空間的本機原點位於目標射線發出的點(例如,控制器的前端,或者如果控制器被渲染為槍,則為槍管的末端),並且空間的方向向量沿目標射線的路徑向外延伸。

您可以使用 XRFramegetPose() 方法,在給定幀的繪製處理程式中輕鬆獲取與 targetRaySpace 對應的目標射線。返回的 XRPosetransform 是與目標射線對應的變換。因此,對於輸入控制器 primaryInput

js
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 用於渲染目的的世界座標系。

圖:將握持空間對映到世界座標系。距離 xyz 共同構成了與握持空間 G 的原點對應的世界座標 (x, y, z)。 顯示握持空間和世界空間之間關係的圖表

在上圖中,我們看到握持空間,其原點位於 G,位於使用者握住控制器的中點,控制器直接遠離使用者,平行於 z 軸。相對於世界空間的原點 W,握持空間的原點位於右側 x 單位,上方 y 單位,更遠 z 單位。給定軸的方向性,握持空間的座標可以表示為世界座標 (x, y, -z);z 為負,因為握持空間沿 z 軸更遠,因此在負方向。

如果控制器位於世界空間原點的左側且比世界空間原點更靠近使用者(或者可能位於使用者後面,如果使用者位於原點,儘管這是一種不舒服的握持控制器方式),則座標的 x 值將為負,但 z 值為正。y 的值仍然為正,除非控制器移動到世界空間原點下方。

這在下圖中顯示,其中控制器位於世界空間原點的下方和左側,控制器也比世界原點更靠近我們。因此,xy 的值都為負,而 z 為正。

圖:當控制器位於世界原點下方和左側,且比世界原點更靠近我們時,將握持空間對映到世界原點。 另一個握持空間和世界空間之間的關係

遊戲手柄記錄

每個輸入源都有一個 gamepad 屬性,如果不是 NULL,則是一個 Gamepad 物件,描述控制器上可用的各種控制元件和小部件。如果輸入裝置只有主要的運動感測器、擠壓控制和一個按鈕,它可能沒有 Gamepad 記錄。但是,如果 gamepad 存在,您可以使用它來識別和輪詢控制器上可用的按鈕和軸。

雖然 Gamepad 記錄由 Gamepad API 規範定義,但它實際上不由 Gamepad API 管理,並且功能不完全相同。有關更詳細的資訊,請參閱使用遊戲手柄物件的高階控制器

配置檔案字串

每個輸入源可以有零個或多個輸入配置檔名稱字串,位於陣列 profiles 中,每個字串描述輸入源在 3D 世界中的首選視覺表示以及輸入源的功能。這些配置檔案的使用在下面的輸入配置檔案中簡要描述。

瞬時輸入源

某些裝置可能會建立瞬時輸入源,以便與並非真正來自該裝置的動作一起使用,但卻被呈現為來自該裝置。例如,如果 XR 裝置提供一種模式,其中滑鼠用於模擬裝置上的事件,則可能會建立一個新的 XRInputSource 物件來表示模擬輸入源,持續處理動作。

這是必要的,因為標準輸入裝置和 XR 輸入源之間保持了分離。在每個瞬時動作的持續時間內,使用人工源來表示外部源。

管理輸入源

當有多個輸入源可用時,您需要能夠獲取每個輸入源的資訊,包括其位置和方向,其目標射線(如果適用於您的需求),以及可以幫助您決定如何以視覺方式呈現輸入源(如果需要)的詳細資訊。您還需要能夠確定將哪個輸入源用於哪些活動;例如,如果使用者有兩個控制器,哪個將被跟蹤以操作 UI 元素,或者兩者都將被跟蹤?

因此,要管理輸入源,您需要能夠列舉輸入源,檢查每個輸入源的配置檔案資訊,並決定如何使用每個輸入控制器。

列舉輸入源

XRSession 物件表示的 WebXR 會話具有一個 inputSources 屬性,該屬性是當前連線到 XR 系統的 WebXR 輸入裝置的即時列表。

js
let inputSourceList = xrSession.inputSources;

由於列表中表示每個輸入源的 XRInputSource 物件的內容是隻讀的,因此 WebXR 系統透過刪除源的記錄並新增新記錄來替換它來更改這些輸入。每當一個或多個輸入源發生更改,或者當輸入源被新增到列表或從列表中移除時,都會向您的 XRSession 傳送一個 inputsourceschange 事件。

例如,如果您需要跟蹤玩家每隻手中握著哪個控制器,您可以這樣做

js
let inputSourceList = NULL;
let leftHandSource = NULL;
let rightHandSource = NULL;

xrSession.addEventListener("inputsourceschange", (event) => {
  inputSourceList = event.session.inputSources;

  inputSourceList.forEach((source) => {
    switch (source.handedness) {
      case "left":
        leftHandSource = source;
        break;
      case "right":
        rightHandSource = source;
        break;
    }
  });
});

inputsourceschange 事件在會話建立回撥首次完成執行時也會觸發一次,因此您可以在啟動時立即可用時使用它來獲取輸入源列表。該事件作為 XRInputSourcesChangeEvent 傳遞,其中包括三個感興趣的屬性

會話

輸入源已更改的 XRSession

已新增

一個包含零個或多個 XRInputSource 物件的陣列,指示已新新增到 XR 系統的輸入源。

已移除

一個包含零個或多個 XRInputSource 物件的陣列,指示已從 XR 系統中移除的任何輸入源。

識別輸入的配置檔案

每個輸入源都有一個 profiles 屬性,其中包含適用於輸入源的 WebXR 輸入配置檔案的即時列表,按從最具體到最不具體的順序排列。

為了進行除基本功能識別之外的任何有意義的配置檔案掃描,您可能需要從 WebXR 輸入配置檔案登錄檔匯入 JSON 配置檔案資料庫。

有關使用輸入配置檔案的更具體詳細資訊,請參閱輸入配置檔案

選擇主控制器

為了避免由於多個控制器試圖無意中同時操作 UI 而引入問題,您的應用程式可能需要一個“主”控制器。這個控制器不僅要承擔點選應用程式使用者介面的責任,而且還會被認為是“主手”,而其他控制器則將是副手或附加控制器。

注意:這並不意味著您的應用程式需要決定一個主控制器。但如果確實需要,這些策略可能會有所幫助。

有幾種方法可以決定主控制器。我們將探討三種。

慣用手

決定哪個控制器是主控制器的最直接方法是設定一個使用者可定義的“慣用手”偏好,使用者透過它來指示哪隻手是慣用手。然後,您將檢視每個輸入源並找到與之匹配的一個(如果可用),如果該手中沒有控制器,則回退到另一個控制器。

js
const primaryInputSource =
  xrSession.inputSources.find((src) => src.handedness === user.handedness) ??
  xrSession.inputSources[0];

此程式碼片段首先假定第一個輸入源是主源,然後查詢其 handednessuser 物件中指定的匹配的輸入源。如果匹配,則選擇該輸入源作為主源。

首次使用

另一個選擇是使用使用者觸發選擇動作的第一個輸入。下面的程式碼首先假設第一個輸入源是主源,然後為 select 事件建立一個處理程式,該處理程式將事件的源記錄為主輸入源。然後將 select 事件處理程式替換為函式 realSelectHandler(),該函式將用於處理所有未來的 select 事件。然後我們將事件傳遞給 realSelectHandler(),以允許事件正常處理。

js
let primaryInputSource = xrSession.inputSources[0];

xrSession.onselect = (event) => {
  primaryInputSource = event.inputSource;
  xrSession.onselect = realSelectHandler;
  return realSelectHandler(event);
};

其效果是,無論 select 事件來自哪個輸入源,我們都在第一次收到該事件時設定主輸入源,並從那時起正常處理事件,不再擔心哪個輸入源是主輸入源。

使用者選擇

確定主輸入源最複雜的方法是高度靈活的,但可能需要大量的工作來實現。在這種情況下,您遍歷輸入源列表及其配置檔案以收集每個輸入源的資訊,然後呈現一個描述每個輸入的 UI,允許使用者為它們分配用途。做好這項工作可能是一項艱鉅的任務,但對於可能涉及多個使用者輸入的複雜應用程式可能很有用。

實現此功能所需的大部分資訊可以在下面的輸入配置檔案部分找到。但是,詳細資訊超出了本文的範圍。

輸入配置檔案

如上所述,每個輸入源都有一個輸入配置檔名稱列表,對應於一組描述該輸入源及其如何使用的資訊。這些名稱位於輸入源的 profiles 屬性中,這些配置檔案字串的官方登錄檔維護在 GitHub 上的 WebXR 輸入配置檔案登錄檔中。

例如,generic-trigger-squeeze-touchpad 配置檔名稱可用於透過查詢具有值 generic-trigger-squeeze-touchpadprofileId 欄位來定位以下 JSON 配置檔案資料。

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”按鈕很可能是主要動作。
  • 主要擠壓動作是當用戶擠壓控制器時執行的動作。這種“擠壓”可以透過字面意義上使用控制器中的壓力感測器來檢測,或者可以使用扳機、手勢或其他機制來模擬。例如,如果輸入控制器是觸覺手套,當用戶握緊拳頭時,它可能會報告發生了主要擠壓動作。

雖然給定的輸入源只能有一個主要動作和一個主要擠壓動作,但輸入裝置上可以配置多個控制元件來觸發每個主要動作。例如,使用者可能將控制器設定成輕觸和點選觸控板都會產生一個主要動作。

這些型別的輸入動作將在下面更詳細地描述。

主要動作

每個輸入源都應該定義一個主要動作。主要動作(有時會簡稱為“選擇動作”)是一種特定於平臺的動作,它透過按順序傳遞 selectstartselectselectend 事件來響應使用者的操作。這些事件都屬於 XRInputSourceEvent 型別。

注意:如果輸入源沒有主要動作,則該輸入源被視為輔助輸入源

當用戶在您的 3D 空間中沿著目標射線指向裝置,然後觸發選擇動作時,以下事件將傳送到活動的 XRSession

  1. 一個 selectstart 事件,指示使用者已執行開始主要動作的活動。這可能是一個手勢、按下按鈕等。
  2. 如果主要動作成功結束(例如,由於使用者釋放按鈕或扳機),而不是由於錯誤,則傳送 select 事件。
  3. select 事件傳送之後,或者如果執行動作的控制器斷開連線或變得不可用,則傳送 selectend 事件。

一般來說,selectstartselectend 事件告訴您何時可能需要向用戶顯示一些東西,以指示主要操作正在進行。這可能是用新顏色繪製啟用按鈕的控制器,或者顯示目標物件被抓住並移動,從 selectstart 到達時開始,到 selectend 收到時停止。

另一方面,select 事件是告訴您的程式碼使用者已完成他們想要完成的動作的事件。這可能就像在遊戲中拋擲物體或扣動槍的扳機一樣簡單,也可能像將他們在世界中拖動的物體放置到新位置一樣複雜。

如果您的主要動作是一個簡單的觸發動作,並且您不需要在觸發器接合時進行任何動畫,則可以忽略 selectstartselectend 事件,並對 select 事件採取行動。

js
xrSession.addEventListener("select", (event) => {
  let inputSource = event.inputSource;
  let frame = event.frame;

  /* handle the event */
});

某些動作可能會很快地傳送這些事件,一個接一個。這些事件之間經過的時間取決於導致動作發生的硬體裝置以及解釋硬體動作並將其轉換為一系列事件的軟體驅動程式。不要假定這些事件之間會有任何特定的時間間隔。

例如,如果導致主要動作發生的硬體是一個按鈕,當用戶按下按鈕時,您將收到 selectstart,然後在使用者釋放按鈕時收到 selectselectend

在整個文件中都有許多示例說明如何處理 select 事件,例如本文中目標定位和目標射線一節。

主要擠壓動作

主要擠壓動作是一種平臺特定的動作,它會向 XRSession 傳送 squeezestartsqueezeendsqueeze 事件。這通常是由使用者擠壓控制器、做出模仿抓住東西的手勢或使用(擠壓)扳機產生的。

事件序列與主要動作傳送的事件序列相同,只是每個事件的名稱不同

  1. 一個 squeezestart 事件被髮送到 XRSession,表明使用者已開始擠壓動作。
  2. 如果主要擠壓動作成功結束,則會話會收到一個 squeeze 事件。
  3. 然後,傳送一個 squeezeend 事件,表示擠壓動作不再進行中。無論擠壓動作是否成功,都會發送此事件。

主要擠壓動作的兩個常見用途是在 3D 世界中抓取和/或拾取物體,以及在遊戲或模擬中擠壓扳機以發射武器。

示例

此示例程式碼顯示了一組擠壓事件處理程式,它們實現這些事件以管理從場景中拾取和握持物件。該程式碼假定存在一個代表角色的 avatar 物件(如本頁上的其他幾個示例中所用),以及 pickUpObject()dropObject() 函式,它們分別處理將物件從世界轉移到特定手和從手中釋放物件並將其放回世界。

拾取物件:處理 squeezestart 事件
js
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);
  }
});

處理 squeezestart 事件的方法是照常獲取姿勢和變換,並將輸入源的 handedness 放入區域性常量 hand 中。我們將使用它將手對映到該手中持有的物件。

然後,程式碼識別目標物件,如果沿著目標射線找到物件,則將其拾起。拾起物件涉及首先檢視手(由 avatar.heldObject[hand] 表示)中是否已持有任何物件,如果已持有,則透過呼叫 dropObject() 函式將其放下。

然後呼叫 pickUpObject(),將目標物件指定為要從場景中移除並放入指定 hand 的物件。pickUpObject() 還記錄物件的原始位置,以便在擠壓取消或中止時可以將其返回到該位置。

放下物體:擠壓事件處理程式

當用戶通過鬆開握力來結束擠壓動作時,會收到 squeeze 事件。在此示例中,我們將其解釋為釋放當前持有的物體,將其放置在目標位置的場景中。

此程式碼假設存在附加函式 findTargetPosition(),該函式會沿著目標射線跟蹤直到與某物碰撞,然後返回碰撞發生的座標,以及 putObject(),該函式將指定 hand 中持有的物體放置在給定位置,並將其從手中移除。

js
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 事件。我們透過將當前持有的物件返回到拾取時的位置來處理它。

js
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 模式下使用滑鼠模擬控制器,則大致發生以下序列

  1. 使用者在顯示 WebXR 場景的 <canvas> 內部按下滑鼠按鈕。
  2. 滑鼠事件由 XR 裝置的驅動程式捕獲。
  3. 裝置建立一個新的 XRInputSource 來表示模擬的 XR 輸入源。將 targetRayMode 設定為 screen,並根據需要填寫其他資訊。這個新的輸入源被臨時新增到 XRSession 屬性 inputSources 返回的列表中。
  4. 瀏覽器傳送與動作對應的 pointerdown 事件。
  5. 生成主要動作,並以 selectstart 事件的形式傳送給應用程式,其源設定為新的 XRInputSource。或者,如果滑鼠用作副手或輔助控制器,則傳送輔助動作。
  6. 當用戶釋放滑鼠按鈕時,select 事件將傳送到 XRSession,然後 DOM 收到一個 click 事件。然後會話收到 selectend 事件,指示動作完成。
  7. 當動作完成時,瀏覽器會刪除瞬時輸入源,併發送任何適當的 pointerup 事件。

因此,瞬時輸入源確實是瞬時的——它僅在處理輸入的持續時間記憶體在,因此不會在輸入源列表中列出。

朝向和目標定位

朝向是觀察者正在看的方向。這不是透過輸入源提供的。相反,它是使用從當前動畫幀的 XRFrame.getViewerPose() 方法獲得的 XRPose 獲得的。觀察者姿勢變換矩陣的旋轉分量是觀察者的朝向方向。

您可以在文章視點和觀察者中瞭解更多關於如何使用觀察者姿勢來確定朝向方向。

目標定位是使用者使用輸入源指向特定方向的行為。輸入源的 targetRaySpace 是一個 XRSpace(實際上可能是 XRReferenceSpace),可用於確定目標射線相對於觀察者朝向方向的方向。

這可能涉及也可能不涉及實際指向 3D 世界中的特定物件;您必須使用命中測試自行確定——也就是說,檢查目標射線是否與場景中的任何物件相交。

目標定位和目標射線

目標射線,其原點位於目標射線空間的原點,並指向使用者指向控制器裝置的方向。目標射線使用一個 XRSpace 定義,其原點位於目標射線的來源(通常是控制器向外的一端或其在 3D 世界中的表示),其方向具有 -Z 沿與 XRInputSourcegripSpace 相同的方向從控制器向外延伸。

這個空間可以在輸入源的 targetRaySpace 屬性中找到。它可用於確定控制器指向的方向以及確定目標射線的原點和方向。這可以透過執行以下示例來完成,該示例實現了一個需要此資訊的 select 事件處理程式。像往常一樣,此程式碼假定使用 glMatrix 來執行矩陣和向量數學

js
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() 時使用它,以建立表示目標射線在觀察者參考空間 viewerRefSpace 中的位置和方向的 XRPose 物件。如果此值為 null,則事件處理程式將返回,不再執行任何操作。

目標射線的變換從姿勢的 transform 屬性獲取並存儲在區域性變數 targetRayTransform 中。然後,它(在本例中透過名為 findTargetObject() 的函式)用於查詢射線相交的第一個物件。如果目標射線確實與場景中的物件相交,我們就可以對它做任何需要做的事情。

如果您需要剝離目標射線原點的實際位置和射線的方向性,您可以這樣做

js
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 動作的相同按鈕,但可能包括任意數量的附加按鈕和控制。

注意:雖然 GamepadGamepad 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 輸入裝置收集的資料,以以下方式影響化身

位置

化身的位置透過對先前已知位置應用 delta 來改變,然後用一個新的參考空間替換化身的參考空間,該參考空間的變換反映了新的位置。

方向

透過對其圍繞三個軸的旋轉應用增量來改變化身的方向或朝向方向,更新其方向向量,然後重新計算其參考空間。

動作

化身執行一個動作,例如使用物體或武器、跳躍,或任何其他與基本移動和旋轉無關的活動。

有些輸入用於控制應用程式而不是化身。例如,一個按鈕可能會開啟一個用於配置應用程式的選項選單。當該選單開啟時,原本會控制化身的輸入可能會被用於控制選單的介面。

使用鍵盤和滑鼠事件

從鍵盤和滑鼠捕獲輸入就像在任何 Web 應用程式中一樣。設定您需要處理的事件的處理程式,以獲取您想要的輸入。有趣的是您如何處理這些輸入。

想象一個 avatar 物件,我們將用它來跟蹤化身及其世界觀的資訊。我們希望玩家能夠使用 WASD 鍵向前、向左、向後和向右移動。由於我們除了 XR 硬體可能正在做的事情之外,還在管理由鍵盤和滑鼠定義的化身位置,因此我們需要單獨維護這些資訊,並在渲染化身(或從化身視角渲染世界)之前將其作為變換應用。

為了實現這一點,我們在 avatar 物件中包含一個 posDelta 屬性,型別為 DOMPoint,其中包含要應用於所有三個軸的偏移量,以調整化身位置(觀察者姿勢的參考空間原點),使其包含來自鍵盤和滑鼠的移動和旋轉。

鍵盤輸入的相應程式碼可能如下所示

js
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 物件的 posDeltaorientDelta 屬性中——我們可以編寫程式碼來應用這些更改。由於我們已經按計劃渲染場景,因此我們可以將應用這些更改的程式碼新增到那裡,以及準備和繪製場景。

js
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 屬性替換為一個新的參考空間,該空間包含了更新的增量。

js
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);
}

另見