diff --git a/.changeset/blue-knives-breathe.md b/.changeset/blue-knives-breathe.md new file mode 100644 index 000000000..0077b648a --- /dev/null +++ b/.changeset/blue-knives-breathe.md @@ -0,0 +1,5 @@ +--- +'@antv/g-plugin-device-renderer': patch +--- + +Support left and right eyes in ar session. diff --git a/.changeset/sweet-apples-tease.md b/.changeset/sweet-apples-tease.md new file mode 100644 index 000000000..8b4870f4f --- /dev/null +++ b/.changeset/sweet-apples-tease.md @@ -0,0 +1,5 @@ +--- +'@antv/g-plugin-device-renderer': patch +--- + +Support hit testing in webxr. diff --git a/__tests__/demos/3d/cylinder.ts b/__tests__/demos/3d/cylinder.ts index 09b31f879..000a0ff53 100644 --- a/__tests__/demos/3d/cylinder.ts +++ b/__tests__/demos/3d/cylinder.ts @@ -27,8 +27,7 @@ export async function cylinder(context) { const cylinder = new Mesh({ style: { - x: 300, - y: 250, + transform: `translate3d(300, 250, 0)`, fill: 'white', opacity: 1, geometry: cylinderGeometry, diff --git a/__tests__/demos/3d/force.ts b/__tests__/demos/3d/force.ts index 48ac1e268..a8a8ac275 100644 --- a/__tests__/demos/3d/force.ts +++ b/__tests__/demos/3d/force.ts @@ -1709,18 +1709,18 @@ export async function force(context) { // }); // canvas.appendChild(circle); - // const label = new Text({ - // style: { - // x: node.x + 310, - // y: node.y + 250, - // z: node.z + 1, - // fontFamily: 'sans-serif', - // text: node.id, - // fontSize: 6, - // fill: 'black', - // isBillboard: true, - // }, - // }); + const label = new Text({ + style: { + x: node.x + 310, + y: node.y + 250, + z: node.z + 1, + fontFamily: 'sans-serif', + text: node.id, + fontSize: 6, + fill: 'black', + isBillboard: true, + }, + }); // const rect = new Rect({ // style: { @@ -1735,27 +1735,27 @@ export async function force(context) { // }, // }); // canvas.appendChild(rect); - // canvas.appendChild(label); + canvas.appendChild(label); }); - // dataset.links.forEach((edge) => { - // const { source, target } = edge; - // const line = new Line({ - // style: { - // x1: source.x + 300, - // y1: source.y + 250, - // z1: source.z, - // x2: target.x + 300, - // y2: target.y + 250, - // z2: target.z, - // stroke: 'black', - // lineWidth: 2, - // opacity: 0.5, - // isBillboard: true, // 始终面向屏幕 - // }, - // }); - // canvas.appendChild(line); - // }); + dataset.links.forEach((edge) => { + const { source, target } = edge; + const line = new Line({ + style: { + x1: source.x + 300, + y1: source.y + 250, + z1: source.z, + x2: target.x + 300, + y2: target.y + 250, + z2: target.z, + stroke: 'black', + lineWidth: 2, + opacity: 0.5, + isBillboard: true, // 始终面向屏幕 + }, + }); + canvas.appendChild(line); + }); // add a directional light into scene const light = new DirectionalLight({ @@ -1777,7 +1777,10 @@ export async function force(context) { canvas.getConfig().disableHitTesting = true; - const $button = ARButton.createButton(canvas, renderer, {}); + const $button = ARButton.createButton(canvas, renderer, { + // @see https://github.com/immersive-web/webxr-samples/blob/main/hit-test.html + requiredFeatures: ['local', 'hit-test'], + }); container.appendChild($button); } diff --git a/__tests__/demos/3d/hit-test.ts b/__tests__/demos/3d/hit-test.ts new file mode 100644 index 000000000..e200caa35 --- /dev/null +++ b/__tests__/demos/3d/hit-test.ts @@ -0,0 +1,183 @@ +import { CanvasEvent, Canvas } from '../../../packages/g'; +import { + CubeGeometry, + CylinderGeometry, + MeshPhongMaterial, + MeshBasicMaterial, + DirectionalLight, + Mesh, + Plugin as Plugin3D, +} from '../../../packages/g-plugin-3d'; +import { ARButton, Renderer } from '../../../packages/g-webgl'; + +/** + * @see https://github.com/immersive-web/webxr-samples/blob/main/hit-test.html + */ +export async function hit_test(context: { + canvas: Canvas; + renderer: Renderer; + container: HTMLDivElement; +}) { + const { canvas, renderer, container } = context; + + // wait for canvas' initialization complete + await canvas.ready; + + // use GPU device + const plugin = renderer.getPlugin('device-renderer'); + const device = plugin.getDevice(); + + // create a sphere geometry + const cylinderGeometry = new CylinderGeometry(device, { + radius: 100, + height: 50, + }); + // create a material with Phong lighting model + const material = new MeshPhongMaterial(device, { + shininess: 30, + }); + + // 1. load texture with URL + const map = plugin.loadTexture( + 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*_aqoS73Se3sAAAAAAAAAAAAAARQnAQ', + ); + const cubeGeometry = new CubeGeometry(device, { + width: 200, + height: 200, + depth: 200, + }); + const basicMaterial = new MeshBasicMaterial(device, { + // wireframe: true, + map, + }); + + const reticle = new Mesh({ + style: { + fill: 'red', + opacity: 1, + geometry: cylinderGeometry, + material, + }, + }); + reticle.setPosition(300, 300, 0); + canvas.appendChild(reticle); + + // add a directional light into scene + const light = new DirectionalLight({ + style: { + fill: 'white', + direction: [-1, 0, 1], + }, + }); + canvas.appendChild(light); + + // adjust camera's position + const camera = canvas.getCamera(); + camera.setPerspective(0.1, 1000, 45, 640 / 640); + + let hitTestSource: XRHitTestSource | null = null; + let hitTestSourceRequested = false; + let xrViewerSpace: XRReferenceSpace | null = null; + canvas.addEventListener(CanvasEvent.BEFORE_RENDER, (e) => { + const frame = e.detail as XRFrame; + if (frame) { + const referenceSpace = renderer.xr.getReferenceSpace(); + const session = renderer.xr.getSession(); + let pose = frame.getViewerPose(referenceSpace); + + reticle.style.visibility = 'hidden'; + + if (hitTestSourceRequested === false) { + session.requestReferenceSpace('viewer').then(function (referenceSpace) { + xrViewerSpace = referenceSpace; + session + .requestHitTestSource?.({ space: referenceSpace }) + ?.then(function (source) { + hitTestSource = source; + }); + }); + + session.addEventListener('end', function () { + hitTestSourceRequested = false; + hitTestSource = null; + }); + + hitTestSourceRequested = true; + } + + if (hitTestSource && pose) { + const hitTestResults = frame.getHitTestResults(hitTestSource); + + if (hitTestResults.length) { + const hit = hitTestResults[0]; + reticle.style.visibility = 'visible'; + reticle.setLocalTransform( + hit.getPose(referenceSpace)?.transform.matrix, + ); + + // console.log('position', reticle.getLocalPosition()); + // console.log('rotation', reticle.getRotation()); + // console.log('scale', reticle.getScale()); + + const [x, y, z] = reticle.getLocalPosition(); + + const width = + session.renderState.baseLayer?.framebufferWidth! / + window.devicePixelRatio; + const height = + session.renderState.baseLayer?.framebufferHeight! / + window.devicePixelRatio; + + $domOverlay.innerHTML = `${x}, ${y}, ${z}, ${width}, ${height}`; + + // console.log(`${x}, ${y}, ${z}, ${width}, ${height}`); + + reticle.setLocalPosition( + x * width + width / 2, + height - y * height - height / 2, + z, + ); + } else { + reticle.style.visibility = 'hidden'; + } + } + } + // sphere.rotate(0, 0.1, 0); + }); + + canvas.getConfig().disableHitTesting = true; + + const $domOverlay = document.createElement('div'); + $domOverlay.id = 'overlay'; + document.body.appendChild($domOverlay); + + const $button = ARButton.createButton(canvas, renderer, { + // @see https://github.com/immersive-web/webxr-samples/blob/main/hit-test.html + requiredFeatures: ['local', 'hit-test', 'dom-overlay'], + domOverlay: { + root: document.getElementById('overlay')!, + }, + }); + container.appendChild($button); + + const controller = renderer.xr.getController(0); + controller.addEventListener('select', (e) => { + if (reticle.style.visibility === 'visible') { + const cube = new Mesh({ + style: { + fill: '#1890FF', + opacity: 1, + geometry: cubeGeometry, + material: basicMaterial, + }, + }); + cube.setLocalTransform(reticle.getLocalTransform()); + canvas.appendChild(cube); + } + }); + canvas.appendChild(controller); +} + +hit_test.initRenderer = (renderer) => { + renderer.registerPlugin(new Plugin3D()); +}; diff --git a/__tests__/demos/3d/index.ts b/__tests__/demos/3d/index.ts index 114e25d29..ec39cc888 100644 --- a/__tests__/demos/3d/index.ts +++ b/__tests__/demos/3d/index.ts @@ -4,3 +4,4 @@ export { torus } from './torus'; export { cylinder } from './cylinder'; export { force } from './force'; export { ar } from './webar'; +export { hit_test } from './hit-test'; diff --git a/__tests__/demos/3d/sphere.ts b/__tests__/demos/3d/sphere.ts index 64e20cf26..2648859c5 100644 --- a/__tests__/demos/3d/sphere.ts +++ b/__tests__/demos/3d/sphere.ts @@ -57,9 +57,7 @@ export async function sphere(context) { // create a mesh const sphere = new Mesh({ style: { - x: 320, - y: 320, - z: 0, + transform: `translate3d(320, 320, 0)`, transformOrigin: 'center', fill: '#1890FF', opacity: 1, diff --git a/__tests__/demos/3d/torus.ts b/__tests__/demos/3d/torus.ts index 176d4d236..85de0157f 100644 --- a/__tests__/demos/3d/torus.ts +++ b/__tests__/demos/3d/torus.ts @@ -27,8 +27,7 @@ export async function torus(context) { const torus = new Mesh({ style: { - x: 300, - y: 250, + transform: `translate3d(320, 250, 0)`, fill: 'white', opacity: 1, geometry: torusGeometry, diff --git a/__tests__/demos/bugfix/1636.ts b/__tests__/demos/bugfix/1636.ts index 01d233482..ffab9c534 100644 --- a/__tests__/demos/bugfix/1636.ts +++ b/__tests__/demos/bugfix/1636.ts @@ -27,7 +27,7 @@ export async function image(context) { y: 0, width: 100, height: 100, - img, + src: img, }, }); const image2 = new Image({ @@ -36,7 +36,7 @@ export async function image(context) { y: 0, width: 100, height: 100, - img, + src: img, }, }); group.appendChild(image); diff --git a/__tests__/demos/bugfix/1667.ts b/__tests__/demos/bugfix/1667.ts new file mode 100644 index 000000000..46589065c --- /dev/null +++ b/__tests__/demos/bugfix/1667.ts @@ -0,0 +1,81 @@ +import { Image, Line, Polyline, Rect, Path } from '../../../packages/g'; + +export async function zoom(context) { + const { canvas } = context; + await canvas.ready; + + const rect = new Rect({ + style: { + x: 0, + y: 0, + width: 100, + height: 100, + fill: 'red', + stroke: 'black', + strokeWidth: 2, + }, + }); + canvas.appendChild(rect); + + const image = new Image({ + style: { + x: 150, + y: 0, + width: 100, + height: 100, + src: 'https://gw.alipayobjects.com/mdn/rms_6ae20b/afts/img/A*_aqoS73Se3sAAAAAAAAAAAAAARQnAQ', + }, + }); + canvas.appendChild(image); + + const line = new Line({ + style: { + x1: 250, + y1: 0, + x2: 300, + y2: 200, + stroke: 'black', + lineWidth: 0.1, + }, + }); + canvas.appendChild(line); + + const polyline = new Polyline({ + style: { + points: [ + [0, 100], + [10, 130], + [30, 130], + ], + stroke: 'red', + lineWidth: 0.1, + }, + }); + canvas.appendChild(polyline); + + const path2 = new Path({ + style: { + d: + 'M 100,300' + + 'l 50,-25' + + 'a25,25 -30 0,1 50,-25' + + 'l 50,-25' + + 'a25,50 -30 0,1 50,-25' + + 'l 50,-25' + + 'a25,75 -30 0,1 50,-25' + + 'l 50,-25' + + 'a25,100 -30 0,1 50,-25' + + 'l 50,-25' + + 'l 0, 200,' + + 'z', + lineWidth: 0.1, + lineJoin: 'round', + stroke: '#54BECC', + cursor: 'pointer', + }, + }); + canvas.appendChild(path2); + + const camera = canvas.getCamera(); + camera.setZoom(0.5); +} diff --git a/__tests__/demos/bugfix/index.ts b/__tests__/demos/bugfix/index.ts index 9c67d18f0..7f256967c 100644 --- a/__tests__/demos/bugfix/index.ts +++ b/__tests__/demos/bugfix/index.ts @@ -5,3 +5,4 @@ export { dirty } from './dirty'; export { image } from './1636'; export { shadowroot_offset } from './1677'; export { gradient_text } from './1572'; +export { zoom } from './1667'; diff --git a/packages/g-lite/src/Canvas.ts b/packages/g-lite/src/Canvas.ts index 984394587..57f4d4398 100644 --- a/packages/g-lite/src/Canvas.ts +++ b/packages/g-lite/src/Canvas.ts @@ -482,6 +482,11 @@ export class Canvas extends EventTarget implements ICanvas { } render(frame?: XRFrame) { + if (frame) { + beforeRenderEvent.detail = frame; + afterRenderEvent.detail = frame; + } + this.dispatchEvent(beforeRenderEvent); const renderingService = this.getRenderingService(); diff --git a/packages/g-lite/src/camera/Camera.ts b/packages/g-lite/src/camera/Camera.ts index 6fd153013..430795516 100644 --- a/packages/g-lite/src/camera/Camera.ts +++ b/packages/g-lite/src/camera/Camera.ts @@ -275,6 +275,8 @@ export class Camera implements ICamera { * 计算 MV 矩阵,为相机矩阵的逆矩阵 */ getViewTransform(): mat4 { + // mat4.scale(this.matrix, this.matrix, vec3.fromValues(1, -1, 1)); + return mat4.invert(mat4.create(), this.matrix); } @@ -496,18 +498,12 @@ export class Camera implements ICamera { this.projectionMatrix, left, left + width, - top, top - height, + top, near, this.far, this.clipSpaceNearZ === ClipSpaceNearZ.ZERO, ); - // flipY since the origin of OpenGL/WebGL is bottom-left compared with top-left in Canvas2D - mat4.scale( - this.projectionMatrix, - this.projectionMatrix, - vec3.fromValues(1, -1, 1), - ); mat4.invert(this.projectionMatrixInverse, this.projectionMatrix); @@ -554,18 +550,13 @@ export class Camera implements ICamera { } if (this.clipSpaceNearZ === ClipSpaceNearZ.NEGATIVE_ONE) { - mat4.ortho(this.projectionMatrix, left, right, bottom, top, near, far); + // FlipY with switching bottom & top. + // @see https://stackoverflow.com/a/4886656 + mat4.ortho(this.projectionMatrix, left, right, top, bottom, near, far); } else { - mat4.orthoZO(this.projectionMatrix, left, right, bottom, top, near, far); + mat4.orthoZO(this.projectionMatrix, left, right, top, bottom, near, far); } - // flipY since the origin of OpenGL/WebGL is bottom-left compared with top-left in Canvas2D - mat4.scale( - this.projectionMatrix, - this.projectionMatrix, - vec3.fromValues(1, -1, 1), - ); - mat4.invert(this.projectionMatrixInverse, this.projectionMatrix); this._getOrthoMatrix(); diff --git a/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts b/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts index 4b8790ee0..6e3df8c8c 100644 --- a/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts +++ b/packages/g-plugin-device-renderer/src/RenderGraphPlugin.ts @@ -76,7 +76,11 @@ export class RenderGraphPlugin implements RenderingPlugin { /** * used in main forward rendering pass */ - world: new RenderInstList(), + leftEye: new RenderInstList(), + /** + * right eye in VR session. + */ + rightEye: new RenderInstList(), /** * used in picking pass, should disable blending */ @@ -322,7 +326,6 @@ export class RenderGraphPlugin implements RenderingPlugin { // @ts-ignore ...view.projectionMatrix, ); - // mat4.scale(projectionMatrix, projectionMatrix, [1, -1, 1]); // flipY const viewMatrix = mat4.invert(mat4.create(), cameraMatrix); mat4.scale(viewMatrix, viewMatrix, vec3.fromValues(1, -1, 1)); @@ -426,7 +429,7 @@ export class RenderGraphPlugin implements RenderingPlugin { ); pass.exec((passRenderer, scope) => { - this.cameras.forEach(({ viewport }) => { + this.cameras.forEach(({ viewport }, i) => { const { x, y, width, height } = viewport; const { viewportW, viewportH } = scope['currentPass']; @@ -439,17 +442,10 @@ export class RenderGraphPlugin implements RenderingPlugin { height * viewportH, ); - // console.log( - // x * viewportW, - // y * viewportH, - // width * viewportW, - // height * viewportH, - // ); - - this.renderLists.world.drawOnPassRenderer( - renderInstManager.renderCache, - passRenderer, - ); + (i === 0 + ? this.renderLists.leftEye + : this.renderLists.rightEye + ).drawOnPassRenderer(renderInstManager.renderCache, passRenderer); }); }); }); @@ -479,13 +475,10 @@ export class RenderGraphPlugin implements RenderingPlugin { const renderInstManager = this.renderHelper.renderInstManager; const { width, height } = this.context.config; this.cameras.forEach( - ({ - viewport, - cameraPosition, - viewMatrix, - projectionMatrix, - isOrtho, - }) => { + ( + { viewport, cameraPosition, viewMatrix, projectionMatrix, isOrtho }, + i, + ) => { const { width: normalizedW, height: normalizedH } = viewport; // Push our outer template, which contains the dynamic UBO bindings... @@ -540,7 +533,9 @@ export class RenderGraphPlugin implements RenderingPlugin { }, ]); - this.batchManager.render(this.renderLists.world); + this.batchManager.render( + i === 0 ? this.renderLists.leftEye : this.renderLists.rightEye, + ); renderInstManager.popTemplateRenderInst(); }, diff --git a/packages/g-webgl/src/WebXRController.ts b/packages/g-webgl/src/WebXRController.ts new file mode 100644 index 000000000..8be97615d --- /dev/null +++ b/packages/g-webgl/src/WebXRController.ts @@ -0,0 +1,218 @@ +import { Group, CustomEvent } from '@antv/g-lite'; +import { vec3 } from 'gl-matrix'; + +const DEFAULT_EVENT = new CustomEvent(''); + +/** + * @see https://github.com/mrdoob/three.js/blob/master/src/renderers/webxr/WebXRController.js + */ +export class WebXRController { + private targetRay: Group = null; + + connect(inputSource: XRInputSource) { + // if (inputSource && inputSource.hand) { + // const hand = this._hand; + + // if (hand) { + // for (const inputjoint of inputSource.hand.values()) { + // // Initialize hand with joints when connected + // this._getHandJoint(hand, inputjoint); + // } + // } + // } + + this.dispatchEvent({ type: 'connected', data: inputSource }); + + return this; + } + + disconnect(inputSource: XRInputSource) { + this.dispatchEvent({ type: 'disconnected', data: inputSource }); + + if (this.targetRay !== null) { + this.targetRay.style.visible = false; + } + + // if (this._grip !== null) { + // this._grip.visible = false; + // } + + // if (this._hand !== null) { + // this._hand.visible = false; + // } + + return this; + } + + update( + inputSource: XRInputSource, + frame: XRFrame, + referenceSpace: XRReferenceSpace, + ) { + let inputPose: XRPose & { + linearVelocity?: Float32Array; + angularVelocity?: Float32Array; + } = null; + // let gripPose = null; + // let handPose = null; + + const targetRay = this.targetRay; + // const grip = this._grip; + // const hand = this._hand; + + if (inputSource && frame.session.visibilityState !== 'visible-blurred') { + // if (hand && inputSource.hand) { + // handPose = true; + + // for (const inputjoint of inputSource.hand.values()) { + // // Update the joints groups with the XRJoint poses + // const jointPose = frame.getJointPose(inputjoint, referenceSpace); + + // // The transform of this joint will be updated with the joint pose on each frame + // const joint = this._getHandJoint(hand, inputjoint); + + // if (jointPose !== null) { + // joint.matrix.fromArray(jointPose.transform.matrix); + // joint.matrix.decompose(joint.position, joint.rotation, joint.scale); + // joint.matrixWorldNeedsUpdate = true; + // joint.jointRadius = jointPose.radius; + // } + + // joint.visible = jointPose !== null; + // } + + // // Custom events + + // // Check pinchz + // const indexTip = hand.joints['index-finger-tip']; + // const thumbTip = hand.joints['thumb-tip']; + // const distance = indexTip.position.distanceTo(thumbTip.position); + + // const distanceToPinch = 0.02; + // const threshold = 0.005; + + // if ( + // hand.inputState.pinching && + // distance > distanceToPinch + threshold + // ) { + // hand.inputState.pinching = false; + // this.dispatchEvent({ + // type: 'pinchend', + // handedness: inputSource.handedness, + // target: this, + // }); + // } else if ( + // !hand.inputState.pinching && + // distance <= distanceToPinch - threshold + // ) { + // hand.inputState.pinching = true; + // this.dispatchEvent({ + // type: 'pinchstart', + // handedness: inputSource.handedness, + // target: this, + // }); + // } + // } else { + // if (grip !== null && inputSource.gripSpace) { + // gripPose = frame.getPose(inputSource.gripSpace, referenceSpace); + + // if (gripPose !== null) { + // grip.matrix.fromArray(gripPose.transform.matrix); + // grip.matrix.decompose(grip.position, grip.rotation, grip.scale); + // grip.matrixWorldNeedsUpdate = true; + + // if (gripPose.linearVelocity) { + // grip.hasLinearVelocity = true; + // grip.linearVelocity.copy(gripPose.linearVelocity); + // } else { + // grip.hasLinearVelocity = false; + // } + + // if (gripPose.angularVelocity) { + // grip.hasAngularVelocity = true; + // grip.angularVelocity.copy(gripPose.angularVelocity); + // } else { + // grip.hasAngularVelocity = false; + // } + // } + // } + // } + + if (targetRay !== null) { + inputPose = frame.getPose(inputSource.targetRaySpace, referenceSpace); + + // Some runtimes (namely Vive Cosmos with Vive OpenXR Runtime) have only grip space and ray space is equal to it + // if (inputPose === null && gripPose !== null) { + // inputPose = gripPose; + // } + + if (inputPose !== null) { + targetRay.setLocalTransform(inputPose.transform.matrix); + + if (inputPose.linearVelocity) { + targetRay.style.hasLinearVelocity = true; + vec3.copy(targetRay.style.linearVelocity, inputPose.linearVelocity); + } else { + targetRay.style.hasLinearVelocity = false; + } + + if (inputPose.angularVelocity) { + targetRay.style.hasAngularVelocity = true; + targetRay.style.angularVelocity.copy(inputPose.angularVelocity); + } else { + targetRay.style.hasAngularVelocity = false; + } + + this.dispatchEvent({ type: 'move' }); + } + } + } + + if (targetRay !== null) { + targetRay.style.visible = inputPose !== null; + } + + // if (grip !== null) { + // grip.visible = gripPose !== null; + // } + + // if (hand !== null) { + // hand.visible = handPose !== null; + // } + + return this; + } + + getTargetRaySpace() { + if (this.targetRay === null) { + this.targetRay = new Group(); + this.targetRay.style.visible = false; + this.targetRay.style.hasLinearVelocity = false; + this.targetRay.style.linearVelocity = vec3.create(); + this.targetRay.style.hasAngularVelocity = false; + this.targetRay.style.angularVelocity = vec3.create(); + } + + return this.targetRay; + } + + dispatchEvent(event: { type: string; data?: any }) { + const { type, data } = event; + DEFAULT_EVENT.type = type; + DEFAULT_EVENT.detail = data; + + if (this.targetRay !== null) { + this.targetRay.dispatchEvent(DEFAULT_EVENT); + } + + // if (this._grip !== null) { + // this._grip.dispatchEvent(event); + // } + + // if (this._hand !== null) { + // this._hand.dispatchEvent(event); + // } + + return this; + } +} diff --git a/packages/g-webgl/src/WebXRManager.ts b/packages/g-webgl/src/WebXRManager.ts index a50ce577d..5d26b9965 100644 --- a/packages/g-webgl/src/WebXRManager.ts +++ b/packages/g-webgl/src/WebXRManager.ts @@ -1,11 +1,14 @@ import { Canvas } from '@antv/g-lite'; import { DeviceRenderer } from '.'; +import { WebXRController } from './WebXRController'; export class WebXRManager { private session: XRSession; private referenceSpaceType: XRReferenceSpaceType; private referenceSpace: XRReferenceSpace; private glBaseLayer: XRWebGLLayer; + private controllers: WebXRController[] = []; + private controllerInputSources: XRInputSource[] = []; constructor(private plugin: DeviceRenderer.Plugin) {} @@ -21,8 +24,14 @@ export class WebXRManager { await gl.makeXRCompatible(); } - // session.addEventListener('select', this.onSessionEvent); + session.addEventListener('select', this.onSessionEvent); + session.addEventListener('selectstart', this.onSessionEvent); + session.addEventListener('selectend', this.onSessionEvent); + session.addEventListener('squeeze', this.onSessionEvent); + session.addEventListener('squeezestart', this.onSessionEvent); + session.addEventListener('squeezeend', this.onSessionEvent); session.addEventListener('end', this.onSessionEnd); + session.addEventListener('inputsourceschange', this.onInputSourcesChange); if (session.renderState.layers === undefined) { const layerInit = { @@ -46,45 +55,6 @@ export class WebXRManager { canvas.requestAnimationFrame = session.requestAnimationFrame.bind(session); - - // const onXRFrame: XRFrameRequestCallback = (time, frame) => { - // // Assumed to be a XRWebGLLayer for now. - // let layer = session.renderState.baseLayer; - // if (!layer) { - // layer = session.renderState.layers![0] as XRWebGLLayer; - // } else { - // // Bind the graphics framebuffer to the baseLayer's framebuffer. - // // Only baseLayer has framebuffer and we need to bind it, even if it is null (for inline sessions). - // // gl.bindFramebuffer(gl.FRAMEBUFFER, layer.framebuffer); - // } - - // swapChain.configureSwapChain( - // layer.framebufferWidth, - // layer.framebufferHeight, - // layer.framebuffer, - // ); - - // // Retrieve the pose of the device. - // // XRFrame.getViewerPose can return null while the session attempts to establish tracking. - // const pose = frame.getViewerPose(this.referenceSpace); - // if (pose) { - // const p = pose.transform.position; - - // // In mobile AR, we only have one view. - // const view = pose.views[0]; - // const viewport = session.renderState.baseLayer!.getViewport(view)!; - - // // Use the view's transform matrix and projection matrix - // // const viewMatrix = mat4.invert(mat4.create(), view.transform.matrix); - // const viewMatrix = view.transform.inverse.matrix; - // const projectionMatrix = view.projectionMatrix; - // } - - // // Queue up the next draw request. - // session.requestAnimationFrame(onXRFrame); - // }; - - // session.requestAnimationFrame(onXRFrame); } } @@ -92,7 +62,121 @@ export class WebXRManager { this.referenceSpaceType = referenceSpaceType; } + getSession() { + return this.session; + } + + getReferenceSpace() { + return this.referenceSpace; + } + + private getOrCreateController(index: number) { + let controller = this.controllers[index]; + if (controller === undefined) { + controller = new WebXRController(); + this.controllers[index] = controller; + } + return controller; + } + + getController(index: number) { + return this.getOrCreateController(index).getTargetRaySpace(); + } + + // getControllerGrip(index: number) { + // return this.getOrCreateController(index).getGripSpace(); + // } + + // getHand(index: number) { + // return this.getOrCreateController(index).getHandSpace(); + // } + private onSessionEnd = () => { + this.session.removeEventListener('select', this.onSessionEvent); + this.session.removeEventListener('selectstart', this.onSessionEvent); + this.session.removeEventListener('selectend', this.onSessionEvent); + this.session.removeEventListener('squeeze', this.onSessionEvent); + this.session.removeEventListener('squeezestart', this.onSessionEvent); + this.session.removeEventListener('squeezeend', this.onSessionEvent); this.session.removeEventListener('end', this.onSessionEnd); + this.session.removeEventListener( + 'inputsourceschange', + this.onInputSourcesChange, + ); + + for (let i = 0; i < this.controllers.length; i++) { + const inputSource = this.controllerInputSources[i]; + if (inputSource === null) continue; + this.controllerInputSources[i] = null; + this.controllers[i].disconnect(inputSource); + } + }; + + private onSessionEvent = (event: XRInputSourceEvent) => { + const controllerIndex = this.controllerInputSources.indexOf( + event.inputSource, + ); + + if (controllerIndex === -1) { + return; + } + + const controller = this.controllers[controllerIndex]; + + if (controller !== undefined) { + controller.update( + event.inputSource, + event.frame, + // @ts-ignore + this.session.referenceSpace, + ); + controller.dispatchEvent({ type: event.type, data: event.inputSource }); + } + }; + + private onInputSourcesChange = (event: XRInputSourceChangeEvent) => { + // Notify disconnected + for (let i = 0; i < event.removed.length; i++) { + const inputSource = event.removed[i]; + const index = this.controllerInputSources.indexOf(inputSource); + + if (index >= 0) { + this.controllerInputSources[index] = null; + this.controllers[index].disconnect(inputSource); + } + } + + // Notify connected + for (let i = 0; i < event.added.length; i++) { + const inputSource = event.added[i]; + + let controllerIndex = this.controllerInputSources.indexOf(inputSource); + + if (controllerIndex === -1) { + // Assign input source a controller that currently has no input source + + for (let i = 0; i < this.controllers.length; i++) { + if (i >= this.controllerInputSources.length) { + this.controllerInputSources.push(inputSource); + controllerIndex = i; + break; + } else if (this.controllerInputSources[i] === null) { + this.controllerInputSources[i] = inputSource; + controllerIndex = i; + break; + } + } + + // If all controllers do currently receive input we ignore new ones + + if (controllerIndex === -1) break; + } + + const controller = this.controllers[controllerIndex]; + + if (controller) { + controller.connect(inputSource); + } + } }; }