WebXRControllerPhysics.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import { WebXRAbstractFeature } from "./WebXRAbstractFeature";
  2. import { Vector3, Quaternion } from "../../../Maths/math.vector";
  3. import { WebXRController } from "../webXRController";
  4. import { PhysicsImpostor } from "../../../Physics/physicsImpostor";
  5. import { WebXRInput } from "../webXRInput";
  6. import { WebXRSessionManager } from "../webXRSessionManager";
  7. import { AbstractMesh } from "../../../Meshes/abstractMesh";
  8. import { SphereBuilder } from "../../../Meshes/Builders/sphereBuilder";
  9. import { WebXRFeatureName, WebXRFeaturesManager } from "../webXRFeaturesManager";
  10. import { Logger } from '../../../Misc/logger';
  11. /**
  12. * Options for the controller physics feature
  13. */
  14. export class IWebXRControllerPhysicsOptions {
  15. /**
  16. * the xr input to use with this pointer selection
  17. */
  18. xrInput: WebXRInput;
  19. /**
  20. * The physics properties of the future impostors
  21. */
  22. physicsProperties?: {
  23. /**
  24. * If set to true, a mesh impostor will be created when the controller mesh was loaded
  25. * Note that this requires a physics engine that supports mesh impostors!
  26. */
  27. useControllerMesh?: boolean;
  28. /**
  29. * The type of impostor to create. Default is sphere
  30. */
  31. impostorType?: number;
  32. /**
  33. * the size of the impostor
  34. */
  35. impostorSize?: number | { width: number, height: number, depth: number };
  36. /**
  37. * Friction definitions
  38. */
  39. friction?: number;
  40. /**
  41. * Restitution
  42. */
  43. restitution?: number;
  44. };
  45. }
  46. /**
  47. * Add physics impostor to your webxr controllers,
  48. * including naive calculation of their linear and angular velocity
  49. */
  50. export class WebXRControllerPhysics extends WebXRAbstractFeature {
  51. /**
  52. * The module's name
  53. */
  54. public static readonly Name = WebXRFeatureName.PHYSICS_CONTROLLERS;
  55. /**
  56. * The (Babylon) version of this module.
  57. * This is an integer representing the implementation version.
  58. * This number does not correspond to the webxr specs version
  59. */
  60. public static readonly Version = 1;
  61. private _lastTimestamp: number = 0;
  62. private _delta: number = 0;
  63. private _controllers: {
  64. [id: string]: {
  65. xrController: WebXRController;
  66. impostorMesh?: AbstractMesh,
  67. impostor: PhysicsImpostor
  68. oldPos?: Vector3;
  69. oldRotation?: Quaternion;
  70. }
  71. } = {};
  72. private _tmpVector: Vector3 = new Vector3();
  73. private _tmpQuaternion: Quaternion = new Quaternion();
  74. /**
  75. * Construct a new Controller Physics Feature
  76. * @param _xrSessionManager the corresponding xr session manager
  77. * @param _options options to create this feature with
  78. */
  79. constructor(_xrSessionManager: WebXRSessionManager, private readonly _options: IWebXRControllerPhysicsOptions) {
  80. super(_xrSessionManager);
  81. if (!this._options.physicsProperties) {
  82. this._options.physicsProperties = {
  83. };
  84. }
  85. }
  86. /**
  87. * Update the physics properties provided in the constructor
  88. * @param newProperties the new properties object
  89. */
  90. public setPhysicsProperties(newProperties: {
  91. impostorType?: number,
  92. impostorSize?: number | { width: number, height: number, depth: number },
  93. friction?: number,
  94. restitution?: number
  95. }) {
  96. this._options.physicsProperties = {
  97. ...this._options.physicsProperties,
  98. ...newProperties
  99. };
  100. }
  101. /**
  102. * attach this feature
  103. * Will usually be called by the features manager
  104. *
  105. * @returns true if successful.
  106. */
  107. attach(): boolean {
  108. if (!super.attach()) {
  109. return false;
  110. }
  111. if (!this._options.xrInput) {
  112. return true;
  113. }
  114. this._options.xrInput.controllers.forEach(this._attachController);
  115. this._addNewAttachObserver(this._options.xrInput.onControllerAddedObservable, this._attachController);
  116. this._addNewAttachObserver(this._options.xrInput.onControllerRemovedObservable, (controller) => {
  117. // REMOVE the controller
  118. this._detachController(controller.uniqueId);
  119. });
  120. return true;
  121. }
  122. /**
  123. * detach this feature.
  124. * Will usually be called by the features manager
  125. *
  126. * @returns true if successful.
  127. */
  128. detach(): boolean {
  129. if (!super.detach()) {
  130. return false;
  131. }
  132. Object.keys(this._controllers).forEach((controllerId) => {
  133. this._detachController(controllerId);
  134. });
  135. return true;
  136. }
  137. /**
  138. * Manually add a controller (if no xrInput was provided or physics engine was not enabled)
  139. * @param xrController the controller to add
  140. */
  141. public addController(xrController: WebXRController) {
  142. this._attachController(xrController);
  143. }
  144. private _debugMode = false;
  145. /**
  146. * @hidden
  147. * enable debugging - will show console outputs and the impostor mesh
  148. */
  149. public _enablePhysicsDebug() {
  150. this._debugMode = true;
  151. Object.keys(this._controllers).forEach((controllerId) => {
  152. const controllerData = this._controllers[controllerId];
  153. if (controllerData.impostorMesh) {
  154. controllerData.impostorMesh.isVisible = true;
  155. }
  156. });
  157. }
  158. private _attachController = (xrController: WebXRController
  159. ) => {
  160. if (this._controllers[xrController.uniqueId]) {
  161. // already attached
  162. return;
  163. }
  164. if (!this._xrSessionManager.scene.isPhysicsEnabled()) {
  165. Logger.Warn("physics engine not enabled, skipped. Please add this controller manually.");
  166. }
  167. if (this._options.physicsProperties!.useControllerMesh) {
  168. xrController.onMotionControllerProfileLoaded.addOnce((motionController) => {
  169. motionController.onModelLoadedObservable.addOnce(() => {
  170. const impostor = new PhysicsImpostor(motionController.rootMesh!, PhysicsImpostor.MeshImpostor, {
  171. mass: 0,
  172. ...this._options.physicsProperties
  173. });
  174. const controllerMesh = xrController.grip || xrController.pointer;
  175. this._controllers[xrController.uniqueId] = {
  176. xrController,
  177. impostor,
  178. oldPos: controllerMesh.position.clone(),
  179. oldRotation: controllerMesh.rotationQuaternion!.clone()
  180. };
  181. });
  182. });
  183. } else {
  184. const impostorType: number = this._options.physicsProperties!.impostorType || PhysicsImpostor.SphereImpostor;
  185. const impostorSize: number | { width: number, height: number, depth: number } = this._options.physicsProperties!.impostorSize || 0.08;
  186. const impostorMesh = SphereBuilder.CreateSphere('impostor-mesh-' + xrController.uniqueId, {
  187. diameterX: typeof impostorSize === 'number' ? impostorSize : impostorSize.width,
  188. diameterY: typeof impostorSize === 'number' ? impostorSize : impostorSize.height,
  189. diameterZ: typeof impostorSize === 'number' ? impostorSize : impostorSize.depth
  190. });
  191. impostorMesh.isVisible = this._debugMode;
  192. impostorMesh.isPickable = false;
  193. impostorMesh.rotationQuaternion = new Quaternion();
  194. const controllerMesh = xrController.grip || xrController.pointer;
  195. impostorMesh.position.copyFrom(controllerMesh.position);
  196. impostorMesh.rotationQuaternion!.copyFrom(controllerMesh.rotationQuaternion!);
  197. const impostor = new PhysicsImpostor(impostorMesh, impostorType, {
  198. mass: 0,
  199. ...this._options.physicsProperties
  200. });
  201. this._controllers[xrController.uniqueId] = {
  202. xrController,
  203. impostor,
  204. impostorMesh
  205. };
  206. }
  207. }
  208. private _detachController(xrControllerUniqueId: string) {
  209. const controllerData = this._controllers[xrControllerUniqueId];
  210. if (!controllerData) { return; }
  211. if (controllerData.impostorMesh) {
  212. controllerData.impostorMesh.dispose();
  213. }
  214. // remove from the map
  215. delete this._controllers[xrControllerUniqueId];
  216. }
  217. protected _onXRFrame(_xrFrame: any): void {
  218. this._delta = (this._xrSessionManager.currentTimestamp - this._lastTimestamp);
  219. this._lastTimestamp = this._xrSessionManager.currentTimestamp;
  220. Object.keys(this._controllers).forEach((controllerId) => {
  221. const controllerData = this._controllers[controllerId];
  222. const controllerMesh = controllerData.xrController.grip || controllerData.xrController.pointer;
  223. const comparedPosition = controllerData.oldPos || controllerData.impostorMesh!.position;
  224. const comparedQuaternion = controllerData.oldRotation || controllerData.impostorMesh!.rotationQuaternion!;
  225. if (!controllerMesh.position.equalsWithEpsilon(comparedPosition)) {
  226. controllerMesh.position.subtractToRef(comparedPosition, this._tmpVector);
  227. this._tmpVector.scaleInPlace(this._delta);
  228. controllerData.impostor.setLinearVelocity(this._tmpVector);
  229. if (this._debugMode) {
  230. console.log(this._tmpVector, 'linear');
  231. }
  232. }
  233. if (!comparedQuaternion.equalsWithEpsilon(controllerMesh.rotationQuaternion!)) {
  234. // roughly based on this - https://www.gamedev.net/forums/topic/347752-quaternion-and-angular-velocity/
  235. comparedQuaternion.conjugateInPlace().multiplyToRef(controllerMesh.rotationQuaternion!, this._tmpQuaternion);
  236. const len = Math.sqrt(this._tmpQuaternion.x * this._tmpQuaternion.x + this._tmpQuaternion.y * this._tmpQuaternion.y + this._tmpQuaternion.z * this._tmpQuaternion.z);
  237. this._tmpVector.set(this._tmpQuaternion.x, this._tmpQuaternion.y, this._tmpQuaternion.z);
  238. // define a better epsilon
  239. if (len < 0.001) {
  240. this._tmpVector.scaleInPlace(2);
  241. } else {
  242. const angle = 2 * Math.atan2(len, this._tmpQuaternion.w);
  243. this._tmpVector.scaleInPlace((angle / (len * this._delta)));
  244. }
  245. controllerData.impostor.setAngularVelocity(this._tmpVector);
  246. if (this._debugMode) {
  247. console.log(this._tmpVector, this._tmpQuaternion, 'angular');
  248. }
  249. }
  250. comparedPosition.copyFrom(controllerMesh.position);
  251. comparedQuaternion.copyFrom(controllerMesh.rotationQuaternion!);
  252. });
  253. }
  254. }
  255. //register the plugin
  256. WebXRFeaturesManager.AddWebXRFeature(WebXRControllerPhysics.Name, (xrSessionManager, options) => {
  257. return () => new WebXRControllerPhysics(xrSessionManager, options);
  258. }, WebXRControllerPhysics.Version, true);