WebXRControllerPhysics.ts 14 KB

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