WebXRAnchorSystem.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. import { WebXRFeatureName, WebXRFeaturesManager } from "../webXRFeaturesManager";
  2. import { WebXRSessionManager } from "../webXRSessionManager";
  3. import { Observable } from "../../Misc/observable";
  4. import { Matrix, Vector3, Quaternion } from "../../Maths/math.vector";
  5. import { TransformNode } from "../../Meshes/transformNode";
  6. import { WebXRAbstractFeature } from "./WebXRAbstractFeature";
  7. import { IWebXRHitResult } from "./WebXRHitTest";
  8. import { Tools } from "../../Misc/tools";
  9. /**
  10. * Configuration options of the anchor system
  11. */
  12. export interface IWebXRAnchorSystemOptions {
  13. /**
  14. * a node that will be used to convert local to world coordinates
  15. */
  16. worldParentNode?: TransformNode;
  17. /**
  18. * If set to true a reference of the created anchors will be kept until the next session starts
  19. * If not defined, anchors will be removed from the array when the feature is detached or the session ended.
  20. */
  21. doNotRemoveAnchorsOnSessionEnded?: boolean;
  22. }
  23. /**
  24. * A babylon container for an XR Anchor
  25. */
  26. export interface IWebXRAnchor {
  27. /**
  28. * A babylon-assigned ID for this anchor
  29. */
  30. id: number;
  31. /**
  32. * Transformation matrix to apply to an object attached to this anchor
  33. */
  34. transformationMatrix: Matrix;
  35. /**
  36. * The native anchor object
  37. */
  38. xrAnchor: XRAnchor;
  39. /**
  40. * if defined, this object will be constantly updated by the anchor's position and rotation
  41. */
  42. attachedNode?: TransformNode;
  43. /**
  44. * Remove this anchor from the scene
  45. */
  46. remove(): void;
  47. }
  48. /**
  49. * An internal interface for a future (promise based) anchor
  50. */
  51. interface IWebXRFutureAnchor {
  52. /**
  53. * The native anchor
  54. */
  55. nativeAnchor?: XRAnchor;
  56. /**
  57. * Was this request submitted to the xr frame?
  58. */
  59. submitted: boolean;
  60. /**
  61. * Was this promise resolved already?
  62. */
  63. resolved: boolean;
  64. /**
  65. * A resolve function
  66. */
  67. resolve: (xrAnchor: IWebXRAnchor) => void;
  68. /**
  69. * A reject function
  70. */
  71. reject: (msg?: string) => void;
  72. /**
  73. * The XR Transformation of the future anchor
  74. */
  75. xrTransformation: XRRigidTransform;
  76. }
  77. let anchorIdProvider = 0;
  78. /**
  79. * An implementation of the anchor system for WebXR.
  80. * For further information see https://github.com/immersive-web/anchors/
  81. */
  82. export class WebXRAnchorSystem extends WebXRAbstractFeature {
  83. private _lastFrameDetected: XRAnchorSet = new Set();
  84. private _trackedAnchors: Array<IWebXRAnchor> = [];
  85. private _referenceSpaceForFrameAnchors: XRReferenceSpace;
  86. private _futureAnchors: IWebXRFutureAnchor[] = [];
  87. /**
  88. * The module's name
  89. */
  90. public static readonly Name = WebXRFeatureName.ANCHOR_SYSTEM;
  91. /**
  92. * The (Babylon) version of this module.
  93. * This is an integer representing the implementation version.
  94. * This number does not correspond to the WebXR specs version
  95. */
  96. public static readonly Version = 1;
  97. /**
  98. * Observers registered here will be executed when a new anchor was added to the session
  99. */
  100. public onAnchorAddedObservable: Observable<IWebXRAnchor> = new Observable();
  101. /**
  102. * Observers registered here will be executed when an anchor was removed from the session
  103. */
  104. public onAnchorRemovedObservable: Observable<IWebXRAnchor> = new Observable();
  105. /**
  106. * Observers registered here will be executed when an existing anchor updates
  107. * This can execute N times every frame
  108. */
  109. public onAnchorUpdatedObservable: Observable<IWebXRAnchor> = new Observable();
  110. /**
  111. * Set the reference space to use for anchor creation, when not using a hit test.
  112. * Will default to the session's reference space if not defined
  113. */
  114. public set referenceSpaceForFrameAnchors(referenceSpace: XRReferenceSpace) {
  115. this._referenceSpaceForFrameAnchors = referenceSpace;
  116. }
  117. /**
  118. * constructs a new anchor system
  119. * @param _xrSessionManager an instance of WebXRSessionManager
  120. * @param _options configuration object for this feature
  121. */
  122. constructor(_xrSessionManager: WebXRSessionManager, private _options: IWebXRAnchorSystemOptions = {}) {
  123. super(_xrSessionManager);
  124. this.xrNativeFeatureName = "anchors";
  125. }
  126. private _tmpVector = new Vector3();
  127. private _tmpQuaternion = new Quaternion();
  128. private _populateTmpTransformation(position: Vector3, rotationQuaternion: Quaternion) {
  129. this._tmpVector.copyFrom(position);
  130. this._tmpQuaternion.copyFrom(rotationQuaternion);
  131. if (!this._xrSessionManager.scene.useRightHandedSystem) {
  132. this._tmpVector.z *= -1;
  133. this._tmpQuaternion.z *= -1;
  134. this._tmpQuaternion.w *= -1;
  135. }
  136. return {
  137. position: this._tmpVector,
  138. rotationQuaternion: this._tmpQuaternion,
  139. };
  140. }
  141. /**
  142. * Create a new anchor point using a hit test result at a specific point in the scene
  143. * An anchor is tracked only after it is added to the trackerAnchors in xrFrame. The promise returned here does not yet guaranty that.
  144. * Use onAnchorAddedObservable to get newly added anchors if you require tracking guaranty.
  145. *
  146. * @param hitTestResult The hit test result to use for this anchor creation
  147. * @param position an optional position offset for this anchor
  148. * @param rotationQuaternion an optional rotation offset for this anchor
  149. * @returns A promise that fulfills when babylon has created the corresponding WebXRAnchor object and tracking has begun
  150. */
  151. public async addAnchorPointUsingHitTestResultAsync(hitTestResult: IWebXRHitResult, position: Vector3 = new Vector3(), rotationQuaternion: Quaternion = new Quaternion()): Promise<IWebXRAnchor> {
  152. // convert to XR space (right handed) if needed
  153. this._populateTmpTransformation(position, rotationQuaternion);
  154. // the matrix that we'll use
  155. const m = new XRRigidTransform({ x: this._tmpVector.x, y: this._tmpVector.y, z: this._tmpVector.z }, { x: this._tmpQuaternion.x, y: this._tmpQuaternion.y, z: this._tmpQuaternion.z, w: this._tmpQuaternion.w });
  156. if (!hitTestResult.xrHitResult.createAnchor) {
  157. this.detach();
  158. throw new Error("Anchors not enabled in this environment/browser");
  159. } else {
  160. try {
  161. const nativeAnchor = await hitTestResult.xrHitResult.createAnchor(m);
  162. return new Promise<IWebXRAnchor>((resolve, reject) => {
  163. this._futureAnchors.push({
  164. nativeAnchor,
  165. resolved: false,
  166. submitted: true,
  167. xrTransformation: m,
  168. resolve,
  169. reject,
  170. });
  171. });
  172. } catch (error) {
  173. throw new Error(error);
  174. }
  175. }
  176. }
  177. /**
  178. * Add a new anchor at a specific position and rotation
  179. * This function will add a new anchor per default in the next available frame. Unless forced, the createAnchor function
  180. * will be called in the next xrFrame loop to make sure that the anchor can be created correctly.
  181. * An anchor is tracked only after it is added to the trackerAnchors in xrFrame. The promise returned here does not yet guaranty that.
  182. * Use onAnchorAddedObservable to get newly added anchors if you require tracking guaranty.
  183. *
  184. * @param position the position in which to add an anchor
  185. * @param rotationQuaternion an optional rotation for the anchor transformation
  186. * @param forceCreateInCurrentFrame force the creation of this anchor in the current frame. Must be called inside xrFrame loop!
  187. * @returns A promise that fulfills when babylon has created the corresponding WebXRAnchor object and tracking has begun
  188. */
  189. public async addAnchorAtPositionAndRotationAsync(position: Vector3, rotationQuaternion: Quaternion = new Quaternion(), forceCreateInCurrentFrame = false): Promise<IWebXRAnchor> {
  190. // convert to XR space (right handed) if needed
  191. this._populateTmpTransformation(position, rotationQuaternion);
  192. // the matrix that we'll use
  193. const xrTransformation = new XRRigidTransform({ x: this._tmpVector.x, y: this._tmpVector.y, z: this._tmpVector.z }, { x: this._tmpQuaternion.x, y: this._tmpQuaternion.y, z: this._tmpQuaternion.z, w: this._tmpQuaternion.w });
  194. const xrAnchor = forceCreateInCurrentFrame && this.attached && this._xrSessionManager.currentFrame ? await this._createAnchorAtTransformation(xrTransformation, this._xrSessionManager.currentFrame) : undefined;
  195. // add the transformation to the future anchors list
  196. return new Promise<IWebXRAnchor>((resolve, reject) => {
  197. this._futureAnchors.push({
  198. nativeAnchor: xrAnchor,
  199. resolved: false,
  200. submitted: false,
  201. xrTransformation,
  202. resolve,
  203. reject,
  204. });
  205. });
  206. }
  207. /**
  208. * Get the list of anchors currently being tracked by the system
  209. */
  210. public get anchors(): IWebXRAnchor[] {
  211. return this._trackedAnchors;
  212. }
  213. /**
  214. * detach this feature.
  215. * Will usually be called by the features manager
  216. *
  217. * @returns true if successful.
  218. */
  219. public detach(): boolean {
  220. if (!super.detach()) {
  221. return false;
  222. }
  223. if (!this._options.doNotRemoveAnchorsOnSessionEnded) {
  224. while (this._trackedAnchors.length) {
  225. const toRemove = this._trackedAnchors.pop();
  226. if (toRemove) {
  227. try {
  228. // try to natively remove it as well
  229. toRemove.remove();
  230. } catch (e) {
  231. // no-op
  232. }
  233. // as the xr frame loop is removed, we need to notify manually
  234. this.onAnchorRemovedObservable.notifyObservers(toRemove);
  235. }
  236. }
  237. }
  238. return true;
  239. }
  240. /**
  241. * Dispose this feature and all of the resources attached
  242. */
  243. public dispose(): void {
  244. this._futureAnchors.length = 0;
  245. super.dispose();
  246. this.onAnchorAddedObservable.clear();
  247. this.onAnchorRemovedObservable.clear();
  248. this.onAnchorUpdatedObservable.clear();
  249. }
  250. protected _onXRFrame(frame: XRFrame) {
  251. if (!this.attached || !frame) {
  252. return;
  253. }
  254. const trackedAnchors = frame.trackedAnchors;
  255. if (trackedAnchors) {
  256. const toRemove = this._trackedAnchors
  257. .filter((anchor) => !trackedAnchors.has(anchor.xrAnchor))
  258. .map((anchor) => {
  259. const index = this._trackedAnchors.indexOf(anchor);
  260. return index;
  261. });
  262. let idxTracker = 0;
  263. toRemove.forEach((index) => {
  264. const anchor = this._trackedAnchors.splice(index - idxTracker, 1)[0];
  265. this.onAnchorRemovedObservable.notifyObservers(anchor);
  266. idxTracker++;
  267. });
  268. // now check for new ones
  269. trackedAnchors.forEach((xrAnchor) => {
  270. if (!this._lastFrameDetected.has(xrAnchor)) {
  271. const newAnchor: Partial<IWebXRAnchor> = {
  272. id: anchorIdProvider++,
  273. xrAnchor: xrAnchor,
  274. remove: xrAnchor.delete,
  275. };
  276. const anchor = this._updateAnchorWithXRFrame(xrAnchor, newAnchor, frame);
  277. this._trackedAnchors.push(anchor);
  278. this.onAnchorAddedObservable.notifyObservers(anchor);
  279. // search for the future anchor promise that matches this
  280. const results = this._futureAnchors.filter((futureAnchor) => futureAnchor.nativeAnchor === xrAnchor);
  281. const result = results[0];
  282. if (result) {
  283. result.resolve(anchor);
  284. result.resolved = true;
  285. }
  286. } else {
  287. let index = this._findIndexInAnchorArray(xrAnchor);
  288. const anchor = this._trackedAnchors[index];
  289. try {
  290. // anchors update every frame
  291. this._updateAnchorWithXRFrame(xrAnchor, anchor, frame);
  292. if (anchor.attachedNode) {
  293. anchor.attachedNode.rotationQuaternion = anchor.attachedNode.rotationQuaternion || new Quaternion();
  294. anchor.transformationMatrix.decompose(anchor.attachedNode.scaling, anchor.attachedNode.rotationQuaternion, anchor.attachedNode.position);
  295. }
  296. this.onAnchorUpdatedObservable.notifyObservers(anchor);
  297. } catch (e) {
  298. Tools.Warn(`Anchor could not be updated`);
  299. }
  300. }
  301. });
  302. this._lastFrameDetected = trackedAnchors;
  303. }
  304. // process future anchors
  305. this._futureAnchors.forEach((futureAnchor) => {
  306. if (!futureAnchor.resolved && !futureAnchor.submitted) {
  307. this._createAnchorAtTransformation(futureAnchor.xrTransformation, frame).then(
  308. (nativeAnchor) => {
  309. futureAnchor.nativeAnchor = nativeAnchor;
  310. },
  311. (error) => {
  312. futureAnchor.resolved = true;
  313. futureAnchor.reject(error);
  314. }
  315. );
  316. futureAnchor.submitted = true;
  317. }
  318. });
  319. }
  320. /**
  321. * avoiding using Array.find for global support.
  322. * @param xrAnchor the plane to find in the array
  323. */
  324. private _findIndexInAnchorArray(xrAnchor: XRAnchor) {
  325. for (let i = 0; i < this._trackedAnchors.length; ++i) {
  326. if (this._trackedAnchors[i].xrAnchor === xrAnchor) {
  327. return i;
  328. }
  329. }
  330. return -1;
  331. }
  332. private _updateAnchorWithXRFrame(xrAnchor: XRAnchor, anchor: Partial<IWebXRAnchor>, xrFrame: XRFrame): IWebXRAnchor {
  333. // matrix
  334. const pose = xrFrame.getPose(xrAnchor.anchorSpace, this._xrSessionManager.referenceSpace);
  335. if (pose) {
  336. const mat = anchor.transformationMatrix || new Matrix();
  337. Matrix.FromArrayToRef(pose.transform.matrix, 0, mat);
  338. if (!this._xrSessionManager.scene.useRightHandedSystem) {
  339. mat.toggleModelMatrixHandInPlace();
  340. }
  341. anchor.transformationMatrix = mat;
  342. if (!this._options.worldParentNode) {
  343. // Logger.Warn("Please provide a world parent node to apply world transformation");
  344. } else {
  345. mat.multiplyToRef(this._options.worldParentNode.getWorldMatrix(), mat);
  346. }
  347. }
  348. return <IWebXRAnchor>anchor;
  349. }
  350. private async _createAnchorAtTransformation(xrTransformation: XRRigidTransform, xrFrame: XRFrame) {
  351. if (xrFrame.createAnchor) {
  352. try {
  353. return xrFrame.createAnchor(xrTransformation, this._referenceSpaceForFrameAnchors ?? this._xrSessionManager.referenceSpace);
  354. } catch (error) {
  355. throw new Error(error);
  356. }
  357. } else {
  358. this.detach();
  359. throw new Error("Anchors are not enabled in your browser");
  360. }
  361. }
  362. }
  363. // register the plugin
  364. WebXRFeaturesManager.AddWebXRFeature(
  365. WebXRAnchorSystem.Name,
  366. (xrSessionManager, options) => {
  367. return () => new WebXRAnchorSystem(xrSessionManager, options);
  368. },
  369. WebXRAnchorSystem.Version
  370. );