@shopware-ag/dive
Version:
Shopware Spatial Framework
341 lines (277 loc) • 10.4 kB
text/typescript
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();
}
}
}