WebXRHitTest.ts 9.2 KB

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