Skip to content

Commit

Permalink
feat(troika-xr): add basic TeleportControls
Browse files Browse the repository at this point in the history
  • Loading branch information
lojjic committed Feb 10, 2021
1 parent fc4322a commit 319ed29
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 0 deletions.
48 changes: 48 additions & 0 deletions packages/troika-xr/src/facade/teleport/GroundTarget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ExtrudeBufferGeometry, MeshLambertMaterial, Path, Shape } from 'three'
import { MeshFacade } from 'troika-3d'

const degreeToRad = Math.PI / 180

let getMarkerGeometry = function () {
const radius = 0.15
const innerRadius = 0.1
const depth = 0.05
const shape = new Shape()
shape.moveTo(radius, -radius)
.lineTo(radius, 0)
.absellipse(0, 0, radius, radius, 0, 270 * degreeToRad, false, 0)
.lineTo(radius, -radius)
shape.holes = [
new Path().moveTo(innerRadius, -innerRadius)
.lineTo(0, -innerRadius)
.absellipse(0, 0, innerRadius, innerRadius, 270 * degreeToRad, 0, true, 0)
.lineTo(innerRadius, -innerRadius)
]

const geom = new ExtrudeBufferGeometry(shape, {
curveSegments: 64,
depth,
bevelEnabled: false
// bevelSize: 0.01,
// bevelThickness: 0.01,
// bevelSegments: 1
})
.rotateX(Math.PI / 2)
.rotateY(Math.PI / 4)
.translate(0, depth, 0)

getMarkerGeometry = () => geom
return geom
}

export class GroundTarget extends MeshFacade {
constructor (parent) {
super(parent)
this.geometry = getMarkerGeometry()
this.material = new MeshLambertMaterial({
transparent: true,
opacity: 0.8
})
this.autoDisposeGeometry = true
}
}
117 changes: 117 additions & 0 deletions packages/troika-xr/src/facade/teleport/TeleportControls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Object3DFacade } from 'troika-3d'
import { Group, Plane, Quaternion, Sphere, Vector3 } from 'three'
import { GroundTarget } from './GroundTarget.js'

const raycastPlane = new Plane().setComponents(0, 1, 0, 0)
const tempVec3 = new Vector3()
const tempQuat = new Quaternion()
const infiniteSphere = new Sphere(undefined, Infinity)

/**
* Basic teleportation. Add an instance of this facade anywhere in the scene.
* The user can point-drag an XR pointer ray at the ground plane and be
* teleported to that location when releasing. They can use a thumbstick while
* targeting a ground location to set the resulting orientation direction. Also,
* using the thumbstick while not targeting the ground will snap-rotate the view
* by 45 degrees.
*
* It must be given a `onTeleport` callback function, which will be called with
* an object holding `{position: {xPos, zPos}, rotation: yRot}`. These can then
* be applied to the scene's `camera` config as the new camera reference origin.
*
* Currently this implementation only supports teleporting along the x-z plane
* at y=0.
*/
export class TeleportControls extends Object3DFacade {
constructor (parent) {
super(parent, new Group())

this.maxDistance = 10
this.targeting = false
this.onTeleport = null

let markerConfig = this.markerConfig = {
key: 'marker',
facade: GroundTarget,
'material.color': 0x003399,
visible: false
}
this.children = [
markerConfig
]

let lastAxisAngle = 0
this.addEventListener('dragstart', e => {
lastAxisAngle = 0
this.targeting = true
this.afterUpdate()
})
this.addEventListener('drag', e => {
if (this.targeting) {
let point = e.ray.intersectPlane(raycastPlane, tempVec3)
if (point && point.distanceTo(this.getCameraPosition()) < this.maxDistance) {
this.targeting = true
markerConfig.x = tempVec3.x
markerConfig.z = tempVec3.z
this.notifyWorld('needsRender')
// For rotation, start with the current direction of the camera. Then rotate
// relative to that by the last controller stick/axis position.
tempQuat.setFromRotationMatrix(this.getCameraFacade().threeObject.matrixWorld)
tempVec3.set(0, 0, -1).applyQuaternion(tempQuat)
markerConfig.rotateY = Math.atan2(-tempVec3.x, -tempVec3.z) + lastAxisAngle
} else {
this.targeting = false
}
this.afterUpdate()
}
})
this.addEventListener('dragend', e => {
if (this.targeting) {
this.targeting = false
this.afterUpdate()
this.onTeleport({
position: { x: markerConfig.x, z: markerConfig.z },
rotation: markerConfig.rotateY
})
}
})

const rotateDebounce = 500
const rotateBy = Math.PI / -4
let lastRotateTime = 0
this.addEventListener('wheel', e => {
if (this.targeting) {
lastAxisAngle = Math.atan2(-e.deltaX, -e.deltaY)
} else {
let now = Date.now()
if (now - lastRotateTime > rotateDebounce && Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
lastRotateTime = now //keep from rotating again until axis is reset
this.onTeleport({
rotation: Math.sign(e.deltaX) * rotateBy + this.getCameraFacade().rotateY
})
}
}
})
}

afterUpdate () {
this.markerConfig.visible = this.targeting
super.afterUpdate()
}

getBoundingSphere () {
return infiniteSphere
}

// Raycast for dragging events will hit anywhere on ground plane if no other object
// is hit first.
raycast (raycaster) {
const intersection = raycaster.ray.intersectPlane(raycastPlane, tempVec3)
return intersection
? [{
distance: raycaster.ray.origin.distanceTo(intersection),
point: intersection.clone()
}]
: null
}
}
3 changes: 3 additions & 0 deletions packages/troika-xr/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,8 @@ export {default as OculusTouchGrip} from './facade/grip-models/OculusTouchGrip.j
// Wrist Mounted UI Container
export {WristMountedUI} from './facade/wrist-mounted-ui/WristMountedUI.js'

// Teleport Controls
export {TeleportControls} from './facade/teleport/TeleportControls.js'

// Misc
export * from './XRUtils.js'

0 comments on commit 319ed29

Please sign in to comment.