WebXRManager.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. /**
  2. * @author mrdoob / http://mrdoob.com/
  3. */
  4. import { ArrayCamera } from '../../cameras/ArrayCamera.js';
  5. import { EventDispatcher } from '../../core/EventDispatcher.js';
  6. import { PerspectiveCamera } from '../../cameras/PerspectiveCamera.js';
  7. import { Vector3 } from '../../math/Vector3.js';
  8. import { Vector4 } from '../../math/Vector4.js';
  9. import { WebGLAnimation } from '../webgl/WebGLAnimation.js';
  10. import { WebXRController } from './WebXRController.js';
  11. function WebXRManager( renderer, gl ) {
  12. const scope = this;
  13. let session = null;
  14. let framebufferScaleFactor = 1.0;
  15. let referenceSpace = null;
  16. let referenceSpaceType = 'local-floor';
  17. let pose = null;
  18. const controllers = [];
  19. const inputSourcesMap = new Map();
  20. //
  21. const cameraL = new PerspectiveCamera();
  22. cameraL.layers.enable( 1 );
  23. cameraL.viewport = new Vector4();
  24. const cameraR = new PerspectiveCamera();
  25. cameraR.layers.enable( 2 );
  26. cameraR.viewport = new Vector4();
  27. const cameras = [ cameraL, cameraR ];
  28. const cameraVR = new ArrayCamera();
  29. cameraVR.layers.enable( 1 );
  30. cameraVR.layers.enable( 2 );
  31. let _currentDepthNear = null;
  32. let _currentDepthFar = null;
  33. //
  34. this.enabled = false;
  35. this.isPresenting = false;
  36. this.getController = function ( index ) {
  37. let controller = controllers[ index ];
  38. if ( controller === undefined ) {
  39. controller = new WebXRController();
  40. controllers[ index ] = controller;
  41. }
  42. return controller.getTargetRaySpace();
  43. };
  44. this.getControllerGrip = function ( index ) {
  45. let controller = controllers[ index ];
  46. if ( controller === undefined ) {
  47. controller = new WebXRController();
  48. controllers[ index ] = controller;
  49. }
  50. return controller.getGripSpace();
  51. };
  52. //
  53. function onSessionEvent( event ) {
  54. const controller = inputSourcesMap.get( event.inputSource );
  55. if ( controller ) {
  56. controller.dispatchEvent( { type: event.type } );
  57. }
  58. }
  59. function onSessionEnd() {
  60. inputSourcesMap.forEach( function ( controller, inputSource ) {
  61. controller.disconnect( inputSource );
  62. } );
  63. inputSourcesMap.clear();
  64. //
  65. renderer.setFramebuffer( null );
  66. renderer.setRenderTarget( renderer.getRenderTarget() ); // Hack #15830
  67. animation.stop();
  68. scope.isPresenting = false;
  69. scope.dispatchEvent( { type: 'sessionend' } );
  70. }
  71. function onRequestReferenceSpace( value ) {
  72. referenceSpace = value;
  73. animation.setContext( session );
  74. animation.start();
  75. scope.isPresenting = true;
  76. scope.dispatchEvent( { type: 'sessionstart' } );
  77. }
  78. this.setFramebufferScaleFactor = function ( value ) {
  79. framebufferScaleFactor = value;
  80. if ( scope.isPresenting === true ) {
  81. console.warn( 'THREE.WebXRManager: Cannot change framebuffer scale while presenting.' );
  82. }
  83. };
  84. this.setReferenceSpaceType = function ( value ) {
  85. referenceSpaceType = value;
  86. if ( scope.isPresenting === true ) {
  87. console.warn( 'THREE.WebXRManager: Cannot change reference space type while presenting.' );
  88. }
  89. };
  90. this.getReferenceSpace = function () {
  91. return referenceSpace;
  92. };
  93. this.getSession = function () {
  94. return session;
  95. };
  96. this.setSession = function ( value ) {
  97. session = value;
  98. if ( session !== null ) {
  99. session.addEventListener( 'select', onSessionEvent );
  100. session.addEventListener( 'selectstart', onSessionEvent );
  101. session.addEventListener( 'selectend', onSessionEvent );
  102. session.addEventListener( 'squeeze', onSessionEvent );
  103. session.addEventListener( 'squeezestart', onSessionEvent );
  104. session.addEventListener( 'squeezeend', onSessionEvent );
  105. session.addEventListener( 'end', onSessionEnd );
  106. const attributes = gl.getContextAttributes();
  107. if ( attributes.xrCompatible !== true ) {
  108. gl.makeXRCompatible();
  109. }
  110. const layerInit = {
  111. antialias: attributes.antialias,
  112. alpha: attributes.alpha,
  113. depth: attributes.depth,
  114. stencil: attributes.stencil,
  115. framebufferScaleFactor: framebufferScaleFactor
  116. };
  117. // eslint-disable-next-line no-undef
  118. const baseLayer = new XRWebGLLayer( session, gl, layerInit );
  119. session.updateRenderState( { baseLayer: baseLayer } );
  120. session.requestReferenceSpace( referenceSpaceType ).then( onRequestReferenceSpace );
  121. //
  122. session.addEventListener( 'inputsourceschange', updateInputSources );
  123. }
  124. };
  125. function updateInputSources( event ) {
  126. const inputSources = session.inputSources;
  127. // Assign inputSources to available controllers
  128. for ( let i = 0; i < controllers.length; i ++ ) {
  129. inputSourcesMap.set( inputSources[ i ], controllers[ i ] );
  130. }
  131. // Notify disconnected
  132. for ( let i = 0; i < event.removed.length; i ++ ) {
  133. const inputSource = event.removed[ i ];
  134. const controller = inputSourcesMap.get( inputSource );
  135. if ( controller ) {
  136. controller.dispatchEvent( { type: 'disconnected', data: inputSource } );
  137. inputSourcesMap.delete( inputSource );
  138. }
  139. }
  140. // Notify connected
  141. for ( let i = 0; i < event.added.length; i ++ ) {
  142. const inputSource = event.added[ i ];
  143. const controller = inputSourcesMap.get( inputSource );
  144. if ( controller ) {
  145. controller.dispatchEvent( { type: 'connected', data: inputSource } );
  146. }
  147. }
  148. }
  149. //
  150. const cameraLPos = new Vector3();
  151. const cameraRPos = new Vector3();
  152. /**
  153. * @author jsantell / https://www.jsantell.com/
  154. *
  155. * Assumes 2 cameras that are parallel and share an X-axis, and that
  156. * the cameras' projection and world matrices have already been set.
  157. * And that near and far planes are identical for both cameras.
  158. * Visualization of this technique: https://computergraphics.stackexchange.com/a/4765
  159. */
  160. function setProjectionFromUnion( camera, cameraL, cameraR ) {
  161. cameraLPos.setFromMatrixPosition( cameraL.matrixWorld );
  162. cameraRPos.setFromMatrixPosition( cameraR.matrixWorld );
  163. const ipd = cameraLPos.distanceTo( cameraRPos );
  164. const projL = cameraL.projectionMatrix.elements;
  165. const projR = cameraR.projectionMatrix.elements;
  166. // VR systems will have identical far and near planes, and
  167. // most likely identical top and bottom frustum extents.
  168. // Use the left camera for these values.
  169. const near = projL[ 14 ] / ( projL[ 10 ] - 1 );
  170. const far = projL[ 14 ] / ( projL[ 10 ] + 1 );
  171. const topFov = ( projL[ 9 ] + 1 ) / projL[ 5 ];
  172. const bottomFov = ( projL[ 9 ] - 1 ) / projL[ 5 ];
  173. const leftFov = ( projL[ 8 ] - 1 ) / projL[ 0 ];
  174. const rightFov = ( projR[ 8 ] + 1 ) / projR[ 0 ];
  175. const left = near * leftFov;
  176. const right = near * rightFov;
  177. // Calculate the new camera's position offset from the
  178. // left camera. xOffset should be roughly half `ipd`.
  179. const zOffset = ipd / ( - leftFov + rightFov );
  180. const xOffset = zOffset * - leftFov;
  181. // TODO: Better way to apply this offset?
  182. cameraL.matrixWorld.decompose( camera.position, camera.quaternion, camera.scale );
  183. camera.translateX( xOffset );
  184. camera.translateZ( zOffset );
  185. camera.matrixWorld.compose( camera.position, camera.quaternion, camera.scale );
  186. camera.matrixWorldInverse.getInverse( camera.matrixWorld );
  187. // Find the union of the frustum values of the cameras and scale
  188. // the values so that the near plane's position does not change in world space,
  189. // although must now be relative to the new union camera.
  190. const near2 = near + zOffset;
  191. const far2 = far + zOffset;
  192. const left2 = left - xOffset;
  193. const right2 = right + ( ipd - xOffset );
  194. const top2 = topFov * far / far2 * near2;
  195. const bottom2 = bottomFov * far / far2 * near2;
  196. camera.projectionMatrix.makePerspective( left2, right2, top2, bottom2, near2, far2 );
  197. }
  198. function updateCamera( camera, parent ) {
  199. if ( parent === null ) {
  200. camera.matrixWorld.copy( camera.matrix );
  201. } else {
  202. camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix );
  203. }
  204. camera.matrixWorldInverse.getInverse( camera.matrixWorld );
  205. }
  206. this.getCamera = function ( camera ) {
  207. cameraVR.near = cameraR.near = cameraL.near = camera.near;
  208. cameraVR.far = cameraR.far = cameraL.far = camera.far;
  209. if ( _currentDepthNear !== cameraVR.near || _currentDepthFar !== cameraVR.far ) {
  210. // Note that the new renderState won't apply until the next frame. See #18320
  211. session.updateRenderState( {
  212. depthNear: cameraVR.near,
  213. depthFar: cameraVR.far
  214. } );
  215. _currentDepthNear = cameraVR.near;
  216. _currentDepthFar = cameraVR.far;
  217. }
  218. const parent = camera.parent;
  219. const cameras = cameraVR.cameras;
  220. updateCamera( cameraVR, parent );
  221. for ( let i = 0; i < cameras.length; i ++ ) {
  222. updateCamera( cameras[ i ], parent );
  223. }
  224. // update camera and its children
  225. camera.matrixWorld.copy( cameraVR.matrixWorld );
  226. const children = camera.children;
  227. for ( let i = 0, l = children.length; i < l; i ++ ) {
  228. children[ i ].updateMatrixWorld( true );
  229. }
  230. // update projection matrix for proper view frustum culling
  231. if ( cameras.length === 2 ) {
  232. setProjectionFromUnion( cameraVR, cameraL, cameraR );
  233. } else {
  234. // assume single camera setup (AR)
  235. cameraVR.projectionMatrix.copy( cameraL.projectionMatrix );
  236. }
  237. return cameraVR;
  238. };
  239. // Animation Loop
  240. let onAnimationFrameCallback = null;
  241. function onAnimationFrame( time, frame ) {
  242. pose = frame.getViewerPose( referenceSpace );
  243. if ( pose !== null ) {
  244. const views = pose.views;
  245. const baseLayer = session.renderState.baseLayer;
  246. renderer.setFramebuffer( baseLayer.framebuffer );
  247. let cameraVRNeedsUpdate = false;
  248. // check if it's necessary to rebuild cameraVR's camera list
  249. if ( views.length !== cameraVR.cameras.length ) {
  250. cameraVR.cameras.length = 0;
  251. cameraVRNeedsUpdate = true;
  252. }
  253. for ( let i = 0; i < views.length; i ++ ) {
  254. const view = views[ i ];
  255. const viewport = baseLayer.getViewport( view );
  256. const camera = cameras[ i ];
  257. camera.matrix.fromArray( view.transform.matrix );
  258. camera.projectionMatrix.fromArray( view.projectionMatrix );
  259. camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );
  260. if ( i === 0 ) {
  261. cameraVR.matrix.copy( camera.matrix );
  262. }
  263. if ( cameraVRNeedsUpdate === true ) {
  264. cameraVR.cameras.push( camera );
  265. }
  266. }
  267. }
  268. //
  269. const inputSources = session.inputSources;
  270. for ( let i = 0; i < controllers.length; i ++ ) {
  271. const controller = controllers[ i ];
  272. const inputSource = inputSources[ i ];
  273. controller.update( inputSource, frame, referenceSpace );
  274. }
  275. if ( onAnimationFrameCallback ) onAnimationFrameCallback( time, frame );
  276. }
  277. const animation = new WebGLAnimation();
  278. animation.setAnimationLoop( onAnimationFrame );
  279. this.setAnimationLoop = function ( callback ) {
  280. onAnimationFrameCallback = callback;
  281. };
  282. this.dispose = function () {};
  283. }
  284. Object.assign( WebXRManager.prototype, EventDispatcher.prototype );
  285. export { WebXRManager };