Skip to content

Adding example VR controls to webXr in viewer #155

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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