Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding example VR controls to webXr in viewer #155

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
14 changes: 12 additions & 2 deletions viewer/src/components/context/context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Clock, Mesh, Object3D, Plane, Vector2, Vector3 } from 'three';
import { Clock, Matrix4, Mesh, Object3D, Plane, Vector2, Vector3 } from 'three';
import { VRButton } from 'three/examples/jsm/webxr/VRButton';
import { IfcCamera } from './camera/camera';
import { IfcRaycaster } from './raycaster';
import { IfcRenderer } from './renderer/renderer';
Expand Down Expand Up @@ -178,6 +179,10 @@ export class IfcContext {
return this.ifcCaster.castRayIfc();
}

castVrRay(from: Matrix4, to: Matrix4) {
return this.ifcCaster.castVrRay(from, to);
}

fitToFrame() {
this.ifcCamera.navMode[NavigationModes.Orbit].fitModelToFrame();
}
Expand All @@ -196,21 +201,26 @@ export class IfcContext {
if (this.stats) this.stats.begin();
const isWebXR = this.options.webXR || false;
if (isWebXR) {
document.body.appendChild(VRButton.createButton(this.getRenderer()));
this.getRenderer().xr.enabled = true;
this.renderForWebXR();
} else {
requestAnimationFrame(this.render);
}
this.updateAllComponents();
if (this.stats) this.stats.end();
};

private renderForWebXR = () => {
const newAnimationLoop = () => {
this.webXrMoveTracking();
this.getRenderer().render(this.getScene(), this.getCamera());
};
this.getRenderer().setAnimationLoop(newAnimationLoop);
};

webXrMoveTracking = () => {}; // empty function called on webXR render loop; which is replaced in VRControllers to handle VR movement

private updateAllComponents() {
const delta = this.clock.getDelta();
this.items.components.forEach((component) => component.update(delta));
Expand Down
8 changes: 7 additions & 1 deletion viewer/src/components/context/raycaster.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Intersection, Object3D, Raycaster } from 'three';
import { Intersection, Matrix4, Object3D, Raycaster } from 'three';
import { IfcComponent } from '../../base-types';
import { IfcContext } from './context';

Expand Down Expand Up @@ -29,6 +29,12 @@ export class IfcRaycaster extends IfcComponent {
return filtered.length > 0 ? filtered[0] : null;
}

castVrRay(from: Matrix4, to: Matrix4) {
this.raycaster.ray.origin.setFromMatrixPosition(from);
this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(to);
return this.raycaster.intersectObjects(this.context.items.pickableIfcModels)[0];
}

private filterClippingPlanes(objs: Intersection[]) {
const planes = this.context.getClippingPlanes();
if (objs.length <= 0 || !planes || planes?.length <= 0) return objs;
Expand Down
90 changes: 90 additions & 0 deletions viewer/src/components/context/vrControllers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { Vector3, Line, BufferGeometry, Object3D, Group, Matrix4, Quaternion } from 'three';
import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory';
import { IfcContext } from './context';
import { IfcManager } from '../ifc';

export class IfcVrControllers {
context: IfcContext;
ifcManager: IfcManager;
controller1: Group;
controller2: Group;
controllerGrip1: Group;
controllerGrip2: Group;
cameraDolly = new Object3D();
dummyCam = new Object3D();
tempMatrix = new Matrix4();
letUserMove: Boolean = false;

constructor(context: IfcContext, ifcManager: IfcManager) {
this.context = context;
this.context.webXrMoveTracking = this.handleUserMovement;
this.ifcManager = ifcManager;
this.controller1 = this.context.renderer.renderer.xr.getController(0);
this.controller1.addEventListener('squeezestart', this.allowMovement.bind(this));
this.controller1.addEventListener('squeezeend', this.stopMovement.bind(this));
this.controller2 = this.context.renderer.renderer.xr.getController(1);
this.controller2.addEventListener('selectstart', this.highlight.bind(this));
this.controller2.addEventListener('squeezestart', this.clearHighlight.bind(this));
const controllerModelFactory = new XRControllerModelFactory();
this.controllerGrip1 = this.context.renderer.renderer.xr.getControllerGrip(0);
this.controllerGrip1.add(controllerModelFactory.createControllerModel(this.controllerGrip1));
this.controllerGrip2 = this.context.renderer.renderer.xr.getControllerGrip(1);
this.controllerGrip2.add(controllerModelFactory.createControllerModel(this.controllerGrip2));
this.context.getScene().add(this.controller1);
this.context.getScene().add(this.controller2);
this.context.getScene().add(this.controllerGrip1);
this.context.getScene().add(this.controllerGrip2);
const geometry = new BufferGeometry().setFromPoints([
new Vector3(0, 0, 0),
new Vector3(0, 0, -1)
]);
const line = new Line(geometry);
line.name = 'line';
line.scale.z = 5;
this.controller1.add(line.clone());
this.controller2.add(line.clone());
this.context.getCamera().position.set(0, 0, 0);
this.cameraDolly.add(this.context.getCamera());
this.context.getCamera().add(this.dummyCam);
// Needed to add controllers to dolly?? Not sure without device to test on
// this.cameraDolly.add(this.controller1);
// this.cameraDolly.add(this.controller2);
// this.cameraDolly.add(this.controllerGrip1);
// this.cameraDolly.add(this.controllerGrip2);
}

highlight(event: any) {
const controller = event.target as Group;
const found = this.context.castVrRay(controller.matrixWorld, this.tempMatrix);
if (found) {
this.ifcManager.selector.selection.pick(found);
} else {
this.ifcManager.selector.selection.unpick();
}
}

clearHighlight() {
this.ifcManager.selector.selection.unpick();
}

allowMovement() {
this.letUserMove = true;
}

stopMovement() {
this.letUserMove = false;
}

handleUserMovement = () => {
if (this.letUserMove) {
const speed = 2;
const moveZ = -0.05 * speed;
const saveQuat = this.cameraDolly.quaternion.clone();
const holder = new Quaternion();
this.dummyCam.getWorldQuaternion(holder);
this.cameraDolly.quaternion.copy(holder);
this.cameraDolly.translateZ(moveZ);
this.cameraDolly.quaternion.copy(saveQuat);
}
};
}
3 changes: 3 additions & 0 deletions viewer/src/ifc-viewer-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { PDFWriter } from './components/import-export/pdf';
import { EdgeProjector } from './components/import-export/edges-vectorizer/edge-projection';
import { ClippingEdges } from './components/display/clipping-planes/clipping-edges';
import { SelectionWindow } from './components/selection/selection-window';
import { IfcVrControllers } from './components/context/vrControllers';

export class IfcViewerAPI {
context: IfcContext;
Expand All @@ -37,6 +38,7 @@ export class IfcViewerAPI {
axes: IfcAxes;
dropbox: DropboxAPI;
selectionWindow: SelectionWindow;
vrControllers: IfcVrControllers;

constructor(options: ViewerOptions) {
if (!options.container) throw new Error('Could not get container element!');
Expand All @@ -56,6 +58,7 @@ export class IfcViewerAPI {
this.GLTF = new GLTFManager(this.context, this.IFC);
this.dropbox = new DropboxAPI(this.context, this.IFC);
this.selectionWindow = new SelectionWindow(this.context);
this.vrControllers = new IfcVrControllers(this.context, this.IFC);
ClippingEdges.ifc = this.IFC;
ClippingEdges.context = this.context;
}
Expand Down