UNPKG

@shopware-ag/dive

Version:

Shopware Spatial Framework

341 lines (277 loc) 10.4 kB
import { Matrix4, Mesh, Object3D, Quaternion, Vector3, WebXRArrayCamera, } from 'three'; import { DIVERenderer } from '../../../renderer/Renderer'; import { DIVEScene } from '../../../scene/Scene'; import { DIVEWebXRCrosshair } from '../crosshair/WebXRCrosshair'; import { DIVEWebXRRaycaster } from '../raycaster/WebXRRaycaster'; import { DIVEWebXROrigin } from '../origin/WebXROrigin'; import { DIVETouchscreenEvents, DIVEWebXRTouchscreenControls, } from '../touchscreencontrols/WebXRTouchscreenControls'; import { type DIVEMovable } from '../.././../interface/Movable'; import { findInterface } from '../../../helper/findInterface/findInterface'; export class DIVEWebXRController extends Object3D { // general members private _renderer: DIVERenderer; private _scene: DIVEScene; private _session: XRSession; private _frameBuffer: XRFrame | null = null; // raycaster members private _xrRaycaster: DIVEWebXRRaycaster; private _origin: DIVEWebXROrigin; // crosshair private _crosshair: DIVEWebXRCrosshair; // controller members private _touchscreenControls: DIVEWebXRTouchscreenControls; private _handNodeInitialPosition = new Vector3(); private _xrCamera: WebXRArrayCamera; private _placed: boolean = false; // grabbing private _grabbedObject: Object3D | null = null; private _arHitPosition: Vector3 = new Vector3(); private _arHitQuaternion: Quaternion = new Quaternion(); private _arHitScale: Vector3 = new Vector3(1, 1, 1); // grabbing position private _initialObjectPosition: Vector3 | null = null; private _initialRaycastHit: Vector3 | null = null; private _deltaRaycastHit: Vector3 = new Vector3(); // grabbing rotation private _touchQuaterion: Quaternion = new Quaternion(); // grabbing scale private _touchScale: number = 1; private _scaleThreshold: number = 0.1; constructor(session: XRSession, renderer: DIVERenderer, scene: DIVEScene) { super(); this._renderer = renderer; this._scene = scene; this._session = session; this._xrRaycaster = new DIVEWebXRRaycaster(session, renderer, scene); this._origin = new DIVEWebXROrigin(this._session, this._renderer, [ 'plane', ]); this._crosshair = new DIVEWebXRCrosshair(); this._crosshair.visible = false; this._xrCamera = this._renderer.xr.getCamera(); this._scene.XRRoot.XRHandNode.position.set(0, -0.05, -0.25); this._handNodeInitialPosition = this._scene.XRRoot.XRHandNode.position.clone(); this._touchscreenControls = new DIVEWebXRTouchscreenControls( this._session, ); // translating this._touchscreenControls.Subscribe('TOUCH_START', () => this.onTouchStart(), ); this._touchscreenControls.Subscribe('TOUCH_MOVE', () => this.onTouchMove(), ); this._touchscreenControls.Subscribe('TOUCH_END', (p) => this.onTouchEnd(p), ); // rotating this._touchscreenControls.Subscribe('ROTATE_START', () => this.onRotateStart(), ); this._touchscreenControls.Subscribe('ROTATE_MOVE', (p) => this.onRotateMove(p), ); // scaling this._touchscreenControls.Subscribe('PINCH_START', () => this.onPinchStart(), ); this._touchscreenControls.Subscribe('PINCH_MOVE', (p) => this.onPinchMove(p), ); } public async Init(): Promise<this> { this.prepareScene(); await this.initOrigin(); await this.initRaycaster(); return Promise.resolve(this); } public Dispose(): void { this.restoreScene(); this._origin.Dispose(); this._xrRaycaster.Dispose(); // reset placement members this._placed = false; } public Update(frame: XRFrame): void { this._frameBuffer = frame; if (!this._placed) { this.updateHandNode(); if (this._origin) { this._origin.Update(frame); } } } private updateHandNode(): void { this._xrCamera.updateMatrixWorld(); this._scene.XRRoot.XRHandNode.position.copy( this._handNodeInitialPosition .clone() .applyMatrix4(this._xrCamera.matrixWorld), ); this._scene.XRRoot.XRHandNode.quaternion.setFromRotationMatrix( this._xrCamera.matrixWorld, ); } // placement private async initOrigin(): Promise<void> { // initialize origin this._origin = await this._origin.Init(); // set resolve callback: place objects at origin when it is set this._origin.originSet.then(() => { this.placeObjects(this._origin.matrix); }); } private placeObjects(matrix: Matrix4): void { this._scene.XRRoot.XRModelRoot.matrix.copy(matrix); // we are copying children to a new array to keep the original array intact [...this._scene.XRRoot.XRHandNode.children].forEach((child) => { this._scene.XRRoot.XRModelRoot.add(child); }); this._placed = true; } // grabbing private updateObject(): void { if (!this._grabbedObject) return; this._grabbedObject.position.copy(this._arHitPosition); this._grabbedObject.quaternion.copy( this._arHitQuaternion.clone().multiply(this._touchQuaterion), ); this._grabbedObject.scale.copy( new Vector3( this._touchScale, this._touchScale, this._touchScale, ).multiply(this._arHitScale), ); } private onTouchStart(): void { const sceneHits = this._xrRaycaster.GetSceneIntersections(); console.log('sceneHits', sceneHits); if (sceneHits.length === 0) return; if (!sceneHits[0].object) return; const moveable = findInterface<DIVEMovable>( sceneHits[0].object, 'isMovable', ); if (!moveable) return; this._grabbedObject = moveable; } private onTouchMove(): void { // raycast ar if (!this._frameBuffer) return; if (!this._grabbedObject) return; const intersections = this._xrRaycaster.GetARIntersections( this._frameBuffer, ); if (intersections.length === 0) { this._crosshair.visible = false; return; } const hit = intersections[0]; this._crosshair.visible = true; this._crosshair.matrix.copy(hit.matrix); if (!this._grabbedObject) return; // if initial values have been reset by TOUCH_END event then set them again if (!this._initialObjectPosition || !this._initialRaycastHit) { this._initialObjectPosition = this._grabbedObject.position.clone(); this._initialRaycastHit = hit.point.clone(); } // decompose hit matrix to apply hit matrix to object hit.matrix.decompose( this._arHitPosition, this._arHitQuaternion, this._arHitScale, ); // calculate raycast hit delta this._deltaRaycastHit.copy( hit.point.clone().sub(this._initialRaycastHit), ); // apply moved raycast delta to actual object position this._arHitPosition.copy( this._initialObjectPosition.clone().add(this._deltaRaycastHit), ); console.log('arHitPosition', this._arHitPosition); this.updateObject(); } private onTouchEnd(payload: DIVETouchscreenEvents['TOUCH_END']): void { if (payload.touchCount === 0) { this._crosshair.visible = false; // reset grab this._initialObjectPosition = null; this._initialRaycastHit = null; this._grabbedObject = null; } } private _startTouchQuaternion: Quaternion = new Quaternion(); private onRotateStart(): void { this._startTouchQuaternion = this._touchQuaterion.clone(); } private onRotateMove(payload: DIVETouchscreenEvents['ROTATE_MOVE']): void { this._touchQuaterion.setFromAxisAngle( new Vector3(0, -1, 0), payload.delta * 3, ); this._touchQuaterion.multiply(this._startTouchQuaternion); this.updateObject(); } private _startTouchScale: number = 1; private onPinchStart(): void { this._startTouchScale = this._touchScale; } private onPinchMove(payload: DIVETouchscreenEvents['PINCH_MOVE']): void { this._touchScale = this._startTouchScale * payload.current; this.updateObject(); } // prepare & cleanup scene private prepareScene(): void { this._scene.XRRoot.XRModelRoot.matrixAutoUpdate = false; // initialize crosshair this._scene.add(this._crosshair); // hang current scene children to hand node const children: Object3D[] = []; this._scene.Root.children.forEach((child) => { const clone = child.clone(); clone.layers.enableAll(); clone.traverse((obj) => { obj.layers.enableAll(); if (obj instanceof Mesh) { obj.scale.set(0.1, 0.1, 0.1); } }); clone.position.set(0, 0, 0); children.push(clone); }); this._scene.XRRoot.XRHandNode.add(...children); } private restoreScene(): void { this._scene.remove(this._crosshair); // clear hand node and remove attached models this._scene.XRRoot.XRHandNode.clear(); this._scene.XRRoot.XRModelRoot.clear(); this._scene.XRRoot.XRModelRoot.matrixAutoUpdate = true; } // raycast private async initRaycaster(): Promise<void> { // initialize raycaster await this._xrRaycaster.Init(); // check if successful if (!this._xrRaycaster) { console.error( 'Raycaster not initialized successfully. Aborting WebXR...', ); this.Dispose(); return Promise.reject(); } } }