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';
@@ -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();
}
@@ -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));
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';

@@ -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;
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
@@ -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;
@@ -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!');
@@ -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;
}