WebXRControllerPhysics.ts 14 KB

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