WebXRHitTest.ts 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import { WebXRFeaturesManager, WebXRFeatureName } from '../webXRFeaturesManager';
  2. import { WebXRSessionManager } from '../webXRSessionManager';
  3. import { Observable } from '../../Misc/observable';
  4. import { Vector3, Matrix, Quaternion } from '../../Maths/math.vector';
  5. import { WebXRAbstractFeature } from './WebXRAbstractFeature';
  6. import { IWebXRLegacyHitTestOptions, IWebXRLegacyHitResult, IWebXRHitTestFeature } from './WebXRHitTestLegacy';
  7. import { Tools } from '../../Misc/tools';
  8. import { Nullable } from '../../types';
  9. /**
  10. * Options used for hit testing (version 2)
  11. */
  12. export interface IWebXRHitTestOptions extends IWebXRLegacyHitTestOptions {
  13. /**
  14. * Do not create a permanent hit test. Will usually be used when only
  15. * transient inputs are needed.
  16. */
  17. disablePermanentHitTest?: boolean;
  18. /**
  19. * Enable transient (for example touch-based) hit test inspections
  20. */
  21. enableTransientHitTest?: boolean;
  22. /**
  23. * Offset ray for the permanent hit test
  24. */
  25. offsetRay?: Vector3;
  26. /**
  27. * Offset ray for the transient hit test
  28. */
  29. transientOffsetRay?: Vector3;
  30. /**
  31. * Instead of using viewer space for hit tests, use the reference space defined in the session manager
  32. */
  33. useReferenceSpace?: boolean;
  34. }
  35. /**
  36. * Interface defining the babylon result of hit-test
  37. */
  38. export interface IWebXRHitResult extends IWebXRLegacyHitResult {
  39. /**
  40. * The input source that generated this hit test (if transient)
  41. */
  42. inputSource?: XRInputSource;
  43. /**
  44. * Is this a transient hit test
  45. */
  46. isTransient?: boolean;
  47. /**
  48. * Position of the hit test result
  49. */
  50. position: Vector3;
  51. /**
  52. * Rotation of the hit test result
  53. */
  54. rotationQuaternion: Quaternion;
  55. /**
  56. * The native hit test result
  57. */
  58. xrHitResult: XRHitTestResult;
  59. }
  60. /**
  61. * The currently-working hit-test module.
  62. * Hit test (or Ray-casting) is used to interact with the real world.
  63. * For further information read here - https://github.com/immersive-web/hit-test
  64. *
  65. * Tested on chrome (mobile) 80.
  66. */
  67. export class WebXRHitTest extends WebXRAbstractFeature implements IWebXRHitTestFeature<IWebXRHitResult> {
  68. private _tmpMat: Matrix = new Matrix();
  69. private _tmpPos: Vector3 = new Vector3();
  70. private _tmpQuat: Quaternion = new Quaternion();
  71. private _transientXrHitTestSource: Nullable<XRTransientInputHitTestSource>;
  72. // in XR space z-forward is negative
  73. private _xrHitTestSource: Nullable<XRHitTestSource>;
  74. private initHitTestSource = (referenceSpace: XRReferenceSpace) => {
  75. if (!referenceSpace) {
  76. return;
  77. }
  78. const offsetRay = new XRRay(this.options.offsetRay || {});
  79. const options: XRHitTestOptionsInit = {
  80. space: this.options.useReferenceSpace ? referenceSpace : this._xrSessionManager.viewerReferenceSpace,
  81. offsetRay: offsetRay
  82. };
  83. if (!options.space) {
  84. Tools.Warn('waiting for viewer reference space to initialize');
  85. return;
  86. }
  87. this._xrSessionManager.session.requestHitTestSource(options).then((hitTestSource) => {
  88. if (this._xrHitTestSource) {
  89. this._xrHitTestSource.cancel();
  90. }
  91. this._xrHitTestSource = hitTestSource;
  92. });
  93. }
  94. /**
  95. * The module's name
  96. */
  97. public static readonly Name = WebXRFeatureName.HIT_TEST;
  98. /**
  99. * The (Babylon) version of this module.
  100. * This is an integer representing the implementation version.
  101. * This number does not correspond to the WebXR specs version
  102. */
  103. public static readonly Version = 2;
  104. /**
  105. * When set to true, each hit test will have its own position/rotation objects
  106. * When set to false, position and rotation objects will be reused for each hit test. It is expected that
  107. * the developers will clone them or copy them as they see fit.
  108. */
  109. public autoCloneTransformation: boolean = false;
  110. /**
  111. * Triggered when new babylon (transformed) hit test results are available
  112. */
  113. public onHitTestResultObservable: Observable<IWebXRHitResult[]> = new Observable();
  114. /**
  115. * Use this to temporarily pause hit test checks.
  116. */
  117. public paused: boolean = false;
  118. /**
  119. * Creates a new instance of the hit test feature
  120. * @param _xrSessionManager an instance of WebXRSessionManager
  121. * @param options options to use when constructing this feature
  122. */
  123. constructor(_xrSessionManager: WebXRSessionManager,
  124. /**
  125. * options to use when constructing this feature
  126. */
  127. public readonly options: IWebXRHitTestOptions = {}) {
  128. super(_xrSessionManager);
  129. Tools.Warn('Hit test is an experimental and unstable feature. make sure you enable optionalFeatures when creating the XR session');
  130. }
  131. /**
  132. * attach this feature
  133. * Will usually be called by the features manager
  134. *
  135. * @returns true if successful.
  136. */
  137. public attach(): boolean {
  138. if (!super.attach()) {
  139. return false;
  140. }
  141. if (!this.options.disablePermanentHitTest) {
  142. if (this._xrSessionManager.referenceSpace) {
  143. this.initHitTestSource(this._xrSessionManager.referenceSpace);
  144. }
  145. this._xrSessionManager.onXRReferenceSpaceChanged.add(this.initHitTestSource);
  146. }
  147. if (this.options.enableTransientHitTest) {
  148. const offsetRay = new XRRay(this.options.transientOffsetRay || {});
  149. this._xrSessionManager.session.requestHitTestSourceForTransientInput({
  150. profile : 'generic-touchscreen',
  151. offsetRay
  152. }).then((hitSource) => {
  153. this._transientXrHitTestSource = hitSource;
  154. });
  155. }
  156. return true;
  157. }
  158. /**
  159. * detach this feature.
  160. * Will usually be called by the features manager
  161. *
  162. * @returns true if successful.
  163. */
  164. public detach(): boolean {
  165. if (!super.detach()) {
  166. return false;
  167. }
  168. if (this._xrHitTestSource) {
  169. this._xrHitTestSource.cancel();
  170. this._xrHitTestSource = null;
  171. }
  172. this._xrSessionManager.onXRReferenceSpaceChanged.removeCallback(this.initHitTestSource);
  173. if (this._transientXrHitTestSource) {
  174. this._transientXrHitTestSource.cancel();
  175. this._transientXrHitTestSource = null;
  176. }
  177. return true;
  178. }
  179. /**
  180. * Dispose this feature and all of the resources attached
  181. */
  182. public dispose(): void {
  183. super.dispose();
  184. this.onHitTestResultObservable.clear();
  185. }
  186. protected _onXRFrame(frame: XRFrame) {
  187. // make sure we do nothing if (async) not attached
  188. if (!this.attached || this.paused) {
  189. return;
  190. }
  191. if (this._xrHitTestSource) {
  192. const results = frame.getHitTestResults(this._xrHitTestSource);
  193. if (results.length) {
  194. this._processWebXRHitTestResult(results);
  195. }
  196. }
  197. if (this._transientXrHitTestSource) {
  198. let hitTestResultsPerInputSource = frame.getHitTestResultsForTransientInput(this._transientXrHitTestSource);
  199. hitTestResultsPerInputSource.forEach((resultsPerInputSource) => {
  200. if (resultsPerInputSource.results.length > 0) {
  201. this._processWebXRHitTestResult(resultsPerInputSource.results, resultsPerInputSource.inputSource);
  202. }
  203. });
  204. }
  205. }
  206. private _processWebXRHitTestResult(hitTestResults: XRHitTestResult[], inputSource?: XRInputSource) {
  207. const results : IWebXRHitResult[] = [];
  208. hitTestResults.forEach((hitTestResult) => {
  209. const pose = hitTestResult.getPose(this._xrSessionManager.referenceSpace);
  210. if (!pose) {
  211. return;
  212. }
  213. this._tmpPos.copyFrom(pose.transform.position as unknown as Vector3);
  214. this._tmpQuat.copyFrom(pose.transform.orientation as unknown as Quaternion);
  215. Matrix.FromFloat32ArrayToRefScaled(pose.transform.matrix, 0, 1, this._tmpMat);
  216. if (!this._xrSessionManager.scene.useRightHandedSystem) {
  217. this._tmpPos.z *= -1;
  218. this._tmpQuat.z *= -1;
  219. this._tmpQuat.w *= -1;
  220. this._tmpMat.toggleModelMatrixHandInPlace();
  221. }
  222. const result: IWebXRHitResult = {
  223. position: this.autoCloneTransformation ? this._tmpPos.clone() : this._tmpPos,
  224. rotationQuaternion: this.autoCloneTransformation ? this._tmpQuat.clone() : this._tmpQuat,
  225. transformationMatrix: this.autoCloneTransformation ? this._tmpMat.clone() : this._tmpMat,
  226. inputSource: inputSource,
  227. isTransient: !!inputSource,
  228. xrHitResult: hitTestResult
  229. };
  230. results.push(result);
  231. });
  232. if (results.length) {
  233. this.onHitTestResultObservable.notifyObservers(results);
  234. }
  235. }
  236. }
  237. //register the plugin versions
  238. WebXRFeaturesManager.AddWebXRFeature(WebXRHitTest.Name, (xrSessionManager, options) => {
  239. return () => new WebXRHitTest(xrSessionManager, options);
  240. }, WebXRHitTest.Version, false);