WebXRHandTracking.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. import { WebXRAbstractFeature } from "./WebXRAbstractFeature";
  2. import { WebXRSessionManager } from "../webXRSessionManager";
  3. import { WebXRFeatureName } from "../webXRFeaturesManager";
  4. import { AbstractMesh } from "../../Meshes/abstractMesh";
  5. import { Mesh } from "../../Meshes/mesh";
  6. import { SphereBuilder } from "../../Meshes/Builders/sphereBuilder";
  7. import { WebXRInput } from "../webXRInput";
  8. import { WebXRInputSource } from "../webXRInputSource";
  9. import { Quaternion } from "../../Maths/math.vector";
  10. import { Nullable } from "../../types";
  11. import { PhysicsImpostor } from "../../Physics/physicsImpostor";
  12. import { WebXRFeaturesManager } from "../webXRFeaturesManager";
  13. import { IDisposable } from "../../scene";
  14. import { Observable } from "../../Misc/observable";
  15. import { InstancedMesh } from "../../Meshes/instancedMesh";
  16. declare const XRHand: XRHand;
  17. /**
  18. * Configuration interface for the hand tracking feature
  19. */
  20. export interface IWebXRHandTrackingOptions {
  21. /**
  22. * The xrInput that will be used as source for new hands
  23. */
  24. xrInput: WebXRInput;
  25. /**
  26. * Configuration object for the joint meshes
  27. */
  28. jointMeshes?: {
  29. /**
  30. * Should the meshes created be invisible (defaults to false)
  31. */
  32. invisible?: boolean;
  33. /**
  34. * A source mesh to be used to create instances. Defaults to a sphere.
  35. * This mesh will be the source for all other (25) meshes.
  36. * It should have the general size of a single unit, as the instances will be scaled according to the provided radius
  37. */
  38. sourceMesh?: Mesh;
  39. /**
  40. * This function will be called after a mesh was created for a specific joint.
  41. * Using this function you can either manipulate the instance or return a new mesh.
  42. * When returning a new mesh the instance created before will be disposed
  43. */
  44. onHandJointMeshGenerated?: (meshInstance: InstancedMesh, jointId: number, controllerId: string) => Mesh | undefined;
  45. /**
  46. * Should the source mesh stay visible. Defaults to false
  47. */
  48. keepOriginalVisible?: boolean;
  49. /**
  50. * Scale factor for all instances (defaults to 2)
  51. */
  52. scaleFactor?: number;
  53. /**
  54. * Should each instance have its own physics impostor
  55. */
  56. enablePhysics?: boolean;
  57. /**
  58. * If enabled, override default physics properties
  59. */
  60. physicsProps?: { friction?: number; restitution?: number; impostorType?: number };
  61. /**
  62. * For future use - a single hand-mesh that will be updated according to the XRHand data provided
  63. */
  64. handMesh?: AbstractMesh;
  65. };
  66. }
  67. /**
  68. * Parts of the hands divided to writs and finger names
  69. */
  70. export const enum HandPart {
  71. /**
  72. * HandPart - Wrist
  73. */
  74. WRIST = "wrist",
  75. /**
  76. * HandPart - The THumb
  77. */
  78. THUMB = "thumb",
  79. /**
  80. * HandPart - Index finger
  81. */
  82. INDEX = "index",
  83. /**
  84. * HandPart - Middle finger
  85. */
  86. MIDDLE = "middle",
  87. /**
  88. * HandPart - Ring finger
  89. */
  90. RING = "ring",
  91. /**
  92. * HandPart - Little finger
  93. */
  94. LITTLE = "little",
  95. }
  96. /**
  97. * Representing a single hand (with its corresponding native XRHand object)
  98. */
  99. export class WebXRHand implements IDisposable {
  100. /**
  101. * Hand-parts definition (key is HandPart)
  102. */
  103. public handPartsDefinition: { [key: string]: number[] };
  104. /**
  105. * Populate the HandPartsDefinition object.
  106. * This is called as a side effect since certain browsers don't have XRHand defined.
  107. */
  108. private generateHandPartsDefinition(hand: XRHand) {
  109. return {
  110. [HandPart.WRIST]: [hand.WRIST],
  111. [HandPart.THUMB]: [hand.THUMB_METACARPAL, hand.THUMB_PHALANX_PROXIMAL, hand.THUMB_PHALANX_DISTAL, hand.THUMB_PHALANX_TIP],
  112. [HandPart.INDEX]: [hand.INDEX_METACARPAL, hand.INDEX_PHALANX_PROXIMAL, hand.INDEX_PHALANX_INTERMEDIATE, hand.INDEX_PHALANX_DISTAL, hand.INDEX_PHALANX_TIP],
  113. [HandPart.MIDDLE]: [hand.MIDDLE_METACARPAL, hand.MIDDLE_PHALANX_PROXIMAL, hand.MIDDLE_PHALANX_INTERMEDIATE, hand.MIDDLE_PHALANX_DISTAL, hand.MIDDLE_PHALANX_TIP],
  114. [HandPart.RING]: [hand.RING_METACARPAL, hand.RING_PHALANX_PROXIMAL, hand.RING_PHALANX_INTERMEDIATE, hand.RING_PHALANX_DISTAL, hand.RING_PHALANX_TIP],
  115. [HandPart.LITTLE]: [hand.LITTLE_METACARPAL, hand.LITTLE_PHALANX_PROXIMAL, hand.LITTLE_PHALANX_INTERMEDIATE, hand.LITTLE_PHALANX_DISTAL, hand.LITTLE_PHALANX_TIP],
  116. };
  117. }
  118. /**
  119. * Construct a new hand object
  120. * @param xrController the controller to which the hand correlates
  121. * @param trackedMeshes the meshes to be used to track the hand joints
  122. */
  123. constructor(
  124. /** the controller to which the hand correlates */
  125. public readonly xrController: WebXRInputSource,
  126. /** the meshes to be used to track the hand joints */
  127. public readonly trackedMeshes: AbstractMesh[]
  128. ) {
  129. this.handPartsDefinition = this.generateHandPartsDefinition(xrController.inputSource.hand!);
  130. }
  131. /**
  132. * Update this hand from the latest xr frame
  133. * @param xrFrame xrFrame to update from
  134. * @param referenceSpace The current viewer reference space
  135. * @param scaleFactor optional scale factor for the meshes
  136. */
  137. public updateFromXRFrame(xrFrame: XRFrame, referenceSpace: XRReferenceSpace, scaleFactor: number = 2) {
  138. const hand = this.xrController.inputSource.hand;
  139. if (!hand) {
  140. return;
  141. }
  142. this.trackedMeshes.forEach((mesh, idx) => {
  143. const xrJoint = hand[idx];
  144. if (xrJoint) {
  145. let pose = xrFrame.getJointPose!(xrJoint, referenceSpace);
  146. if (!pose || !pose.transform) {
  147. return;
  148. }
  149. // get the transformation. can be done with matrix decomposition as well
  150. const pos = pose.transform.position;
  151. const orientation = pose.transform.orientation;
  152. mesh.position.set(pos.x, pos.y, pos.z);
  153. mesh.rotationQuaternion!.set(orientation.x, orientation.y, orientation.z, orientation.w);
  154. // left handed system conversion
  155. if (!mesh.getScene().useRightHandedSystem) {
  156. mesh.position.z *= -1;
  157. mesh.rotationQuaternion!.z *= -1;
  158. mesh.rotationQuaternion!.w *= -1;
  159. }
  160. // get the radius of the joint. In general it is static, but just in case it does change we update it on each frame.
  161. const radius = (pose.radius || 0.008) * scaleFactor;
  162. mesh.scaling.set(radius, radius, radius);
  163. }
  164. });
  165. }
  166. /**
  167. * Get meshes of part of the hand
  168. * @param part the part of hand to get
  169. * @returns An array of meshes that correlate to the hand part requested
  170. */
  171. public getHandPartMeshes(part: HandPart): AbstractMesh[] {
  172. return this.handPartsDefinition[part].map((idx) => this.trackedMeshes[idx]);
  173. }
  174. /**
  175. * Dispose this Hand object
  176. */
  177. public dispose() {
  178. this.trackedMeshes.forEach((mesh) => mesh.dispose());
  179. }
  180. }
  181. /**
  182. * WebXR Hand Joint tracking feature, available for selected browsers and devices
  183. */
  184. export class WebXRHandTracking extends WebXRAbstractFeature {
  185. private static _idCounter = 0;
  186. /**
  187. * The module's name
  188. */
  189. public static readonly Name = WebXRFeatureName.HAND_TRACKING;
  190. /**
  191. * The (Babylon) version of this module.
  192. * This is an integer representing the implementation version.
  193. * This number does not correspond to the WebXR specs version
  194. */
  195. public static readonly Version = 1;
  196. /**
  197. * This observable will notify registered observers when a new hand object was added and initialized
  198. */
  199. public onHandAddedObservable: Observable<WebXRHand> = new Observable();
  200. /**
  201. * This observable will notify its observers right before the hand object is disposed
  202. */
  203. public onHandRemovedObservable: Observable<WebXRHand> = new Observable();
  204. private _hands: {
  205. [controllerId: string]: {
  206. id: number;
  207. handObject: WebXRHand;
  208. };
  209. } = {};
  210. /**
  211. * Creates a new instance of the hit test feature
  212. * @param _xrSessionManager an instance of WebXRSessionManager
  213. * @param options options to use when constructing this feature
  214. */
  215. constructor(
  216. _xrSessionManager: WebXRSessionManager,
  217. /**
  218. * options to use when constructing this feature
  219. */
  220. public readonly options: IWebXRHandTrackingOptions
  221. ) {
  222. super(_xrSessionManager);
  223. this.xrNativeFeatureName = "hand-tracking";
  224. }
  225. /**
  226. * Check if the needed objects are defined.
  227. * This does not mean that the feature is enabled, but that the objects needed are well defined.
  228. */
  229. public isCompatible(): boolean {
  230. return typeof XRHand !== "undefined";
  231. }
  232. /**
  233. * attach this feature
  234. * Will usually be called by the features manager
  235. *
  236. * @returns true if successful.
  237. */
  238. public attach(): boolean {
  239. if (!super.attach()) {
  240. return false;
  241. }
  242. this.options.xrInput.controllers.forEach(this._attachHand);
  243. this._addNewAttachObserver(this.options.xrInput.onControllerAddedObservable, this._attachHand);
  244. this._addNewAttachObserver(this.options.xrInput.onControllerRemovedObservable, (controller) => {
  245. // REMOVE the controller
  246. this._detachHand(controller.uniqueId);
  247. });
  248. return true;
  249. }
  250. /**
  251. * detach this feature.
  252. * Will usually be called by the features manager
  253. *
  254. * @returns true if successful.
  255. */
  256. public detach(): boolean {
  257. if (!super.detach()) {
  258. return false;
  259. }
  260. Object.keys(this._hands).forEach((controllerId) => {
  261. this._detachHand(controllerId);
  262. });
  263. return true;
  264. }
  265. /**
  266. * Dispose this feature and all of the resources attached
  267. */
  268. public dispose(): void {
  269. super.dispose();
  270. this.onHandAddedObservable.clear();
  271. }
  272. /**
  273. * Get the hand object according to the controller id
  274. * @param controllerId the controller id to which we want to get the hand
  275. * @returns null if not found or the WebXRHand object if found
  276. */
  277. public getHandByControllerId(controllerId: string): Nullable<WebXRHand> {
  278. return this._hands[controllerId]?.handObject || null;
  279. }
  280. /**
  281. * Get a hand object according to the requested handedness
  282. * @param handedness the handedness to request
  283. * @returns null if not found or the WebXRHand object if found
  284. */
  285. public getHandByHandedness(handedness: XRHandedness): Nullable<WebXRHand> {
  286. const handednesses = Object.keys(this._hands).map((key) => this._hands[key].handObject.xrController.inputSource.handedness);
  287. const found = handednesses.indexOf(handedness);
  288. if (found !== -1) {
  289. return this._hands[found].handObject;
  290. }
  291. return null;
  292. }
  293. protected _onXRFrame(_xrFrame: XRFrame): void {
  294. // iterate over the hands object
  295. Object.keys(this._hands).forEach((id) => {
  296. this._hands[id].handObject.updateFromXRFrame(_xrFrame, this._xrSessionManager.referenceSpace, this.options.jointMeshes?.scaleFactor);
  297. });
  298. }
  299. private _attachHand = (xrController: WebXRInputSource) => {
  300. if (!xrController.inputSource.hand || this._hands[xrController.uniqueId]) {
  301. // already attached
  302. return;
  303. }
  304. const hand = xrController.inputSource.hand;
  305. const trackedMeshes: AbstractMesh[] = [];
  306. const originalMesh = this.options.jointMeshes?.sourceMesh || SphereBuilder.CreateSphere("jointParent", { diameter: 1 });
  307. originalMesh.isVisible = !!this.options.jointMeshes?.keepOriginalVisible;
  308. for (let i = 0; i < hand.length; ++i) {
  309. let newInstance: AbstractMesh = originalMesh.createInstance(`${xrController.uniqueId}-handJoint-${i}`);
  310. if (this.options.jointMeshes?.onHandJointMeshGenerated) {
  311. const returnedMesh = this.options.jointMeshes.onHandJointMeshGenerated(newInstance as InstancedMesh, i, xrController.uniqueId);
  312. if (returnedMesh) {
  313. if (returnedMesh !== newInstance) {
  314. newInstance.dispose();
  315. newInstance = returnedMesh;
  316. }
  317. }
  318. }
  319. newInstance.isPickable = false;
  320. if (this.options.jointMeshes?.enablePhysics) {
  321. const props = this.options.jointMeshes.physicsProps || {};
  322. const type = props.impostorType !== undefined ? props.impostorType : PhysicsImpostor.SphereImpostor;
  323. newInstance.physicsImpostor = new PhysicsImpostor(newInstance, type, { mass: 0, ...props });
  324. }
  325. newInstance.rotationQuaternion = new Quaternion();
  326. trackedMeshes.push(newInstance);
  327. }
  328. const webxrHand = new WebXRHand(xrController, trackedMeshes);
  329. // get two new meshes
  330. this._hands[xrController.uniqueId] = {
  331. handObject: webxrHand,
  332. id: WebXRHandTracking._idCounter++,
  333. };
  334. this.onHandAddedObservable.notifyObservers(webxrHand);
  335. };
  336. private _detachHand(controllerId: string) {
  337. if (this._hands[controllerId]) {
  338. this.onHandRemovedObservable.notifyObservers(this._hands[controllerId].handObject);
  339. this._hands[controllerId].handObject.dispose();
  340. }
  341. }
  342. }
  343. //register the plugin
  344. WebXRFeaturesManager.AddWebXRFeature(
  345. WebXRHandTracking.Name,
  346. (xrSessionManager, options) => {
  347. return () => new WebXRHandTracking(xrSessionManager, options);
  348. },
  349. WebXRHandTracking.Version,
  350. false
  351. );