Skip to content

Commit 8a71a29

Browse files
committed
feat: xr physical movement
Previously, XR movement was best-effort and had some restrictions and odd behavior. This change improves XR so that the player physics capsule moves with you when you physically move in vr, and when snap turning it pivots correctly at the players position. Now, if you physically walk of a high ledge the player will actually fall, and if you try to walk through a wall, physics will push back.
1 parent 22c5e27 commit 8a71a29

File tree

3 files changed

+138
-63
lines changed

3 files changed

+138
-63
lines changed

src/core/entities/PlayerLocal.js

Lines changed: 135 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Entity } from './Entity'
22
import { clamp } from '../utils'
33
import * as THREE from '../extras/three'
4+
import { XRControllerModelFactory } from 'three/addons'
45
import { Layers } from '../extras/Layers'
56
import { DEG2RAD, RAD2DEG } from '../extras/general'
67
import { createNode } from '../extras/createNode'
@@ -105,6 +106,12 @@ export class PlayerLocal extends Entity {
105106
prevTransform: new THREE.Matrix4(),
106107
}
107108

109+
this.xrControllerModelFactory = null
110+
this.xrControllerModel1 = null
111+
this.xrControllerModel2 = null
112+
this.xrRig = new THREE.Object3D()
113+
this.xrRig.rotation.reorder('YXZ')
114+
108115
this.mode = Modes.IDLE
109116
this.axis = new THREE.Vector3()
110117
this.gaze = new THREE.Vector3()
@@ -117,6 +124,9 @@ export class PlayerLocal extends Entity {
117124
this.base.position.fromArray(this.data.position)
118125
this.base.quaternion.fromArray(this.data.quaternion)
119126

127+
this.hmdDelta = new THREE.Vector3()
128+
this.hmdLast = new THREE.Vector3()
129+
120130
this.aura = createNode('group')
121131

122132
this.nametag = createNode('nametag', { label: '', health: this.data.health, active: false })
@@ -175,6 +185,7 @@ export class PlayerLocal extends Entity {
175185
this.initControl()
176186

177187
this.world.setHot(this, true)
188+
this.world.on('xrSession', this.onXRSession)
178189
this.world.emit('ready', true)
179190
}
180191

@@ -310,6 +321,62 @@ export class PlayerLocal extends Entity {
310321
// this.control.setActions([{ type: 'escape', label: 'Menu' }])
311322
}
312323

324+
onXRSession = session => {
325+
if (session) {
326+
if (!this.xrControllerModel1) {
327+
this.xrControllerModelFactory = new XRControllerModelFactory()
328+
this.xrControllerModel1 = this.world.graphics.renderer.xr.getControllerGrip(0)
329+
this.xrControllerModel1.add(this.xrControllerModelFactory.createControllerModel(this.xrControllerModel1))
330+
this.xrRig.add(this.xrControllerModel1)
331+
this.xrControllerModel2 = this.world.graphics.renderer.xr.getControllerGrip(1)
332+
this.xrControllerModel2.add(this.xrControllerModelFactory.createControllerModel(this.xrControllerModel2))
333+
this.xrRig.add(this.xrControllerModel2)
334+
}
335+
this.world.stage.scene.add(this.xrRig)
336+
this.xrRig.add(this.world.camera)
337+
this.cam.zoom = 0
338+
this.control.camera.write = false
339+
this.isXR = true
340+
} else {
341+
this.world.stage.scene.remove(this.xrRig)
342+
this.world.rig.add(this.world.camera)
343+
this.world.camera.position.set(0, 0, 0)
344+
this.world.camera.rotation.set(0, 0, 0)
345+
this.cam.zoom = 1
346+
this.control.camera.write = true
347+
this.isXR = false
348+
}
349+
}
350+
351+
setXRPlayerPosition(position) {
352+
const parent = this.xrRig
353+
const child = this.world.camera
354+
const feetWorldPos = child.getWorldPosition(v2)
355+
feetWorldPos.y -= child.position.y
356+
const offset = v1.subVectors(position, feetWorldPos)
357+
parent.position.add(offset)
358+
359+
// const offset = v1.copy(position)
360+
// const offset = v1.copy(position)
361+
// offset.x -= parent.position.x + child.position.x
362+
// offset.y -= parent.position.y
363+
// offset.z -= parent.position.z + child.position.z
364+
// parent.position.add(offset)
365+
}
366+
367+
turnXRRigAtPlayer(degrees) {
368+
const parent = this.xrRig
369+
const child = this.world.camera
370+
// console.log(child.getWorldPosition(new THREE.Vector3()))
371+
const pivotWorld = new THREE.Vector3()
372+
child.getWorldPosition(pivotWorld)
373+
parent.rotateOnAxis(UP, degrees * THREE.MathUtils.DEG2RAD)
374+
const offset = child.position.clone()
375+
offset.applyQuaternion(parent.quaternion)
376+
parent.position.copy(pivotWorld).sub(offset)
377+
// console.log(child.getWorldPosition(new THREE.Vector3()))
378+
}
379+
313380
toggleFlying(value) {
314381
value = isBoolean(value) ? value : !this.flying
315382
if (this.flying === value) return
@@ -353,6 +420,7 @@ export class PlayerLocal extends Entity {
353420
}
354421

355422
fixedUpdate(delta) {
423+
const xr = this.isXR
356424
const freeze = this.data.effect?.freeze
357425
const anchor = this.getAnchorMatrix()
358426
const snare = this.data.effect?.snare || 0
@@ -709,25 +777,55 @@ export class PlayerLocal extends Entity {
709777
}
710778

711779
update(delta) {
712-
const isXR = this.world.xr?.session
780+
const xr = this.isXR
713781
const freeze = this.data.effect?.freeze
714782
const anchor = this.getAnchorMatrix()
715783

784+
// if (xr) return
785+
// console.log('update')
786+
787+
if (xr) {
788+
// move the rig so that the ground underneath the camera aligns with the base player
789+
this.setXRPlayerPosition(this.base.position)
790+
// fetch any physical movement delta
791+
this.world.camera.getWorldPosition(v1)
792+
v1.y = 0
793+
v2.copy(this.xrRig.position)
794+
v2.y = 0
795+
v3.copy(v1).sub(v2)
796+
this.hmdDelta.copy(v3).sub(this.hmdLast)
797+
this.hmdLast.copy(v3)
798+
// apply physical movement delta to capsule so physics stays with us if we wander
799+
const pose = this.capsule.getGlobalPose()
800+
v2.copy(pose.p).add(this.hmdDelta)
801+
v2.toPxVec3(pose.p)
802+
this.capsule.setGlobalPose(pose)
803+
}
804+
716805
// update cam look direction
717-
if (isXR) {
806+
if (xr) {
718807
// in xr clear camera rotation (handled internally)
719808
// in xr we only track turn here, which is added to the xr camera later on
720-
this.cam.rotation.x = 0
721-
this.cam.rotation.z = 0
809+
// this.cam.rotation.x = 0
810+
// this.cam.rotation.z = 0
722811
if (this.control.xrRightStick.value.x === 0 && this.didSnapTurn) {
723812
this.didSnapTurn = false
724813
} else if (this.control.xrRightStick.value.x > 0 && !this.didSnapTurn) {
725-
this.cam.rotation.y -= 45 * DEG2RAD
814+
this.turnXRRigAtPlayer(-45)
726815
this.didSnapTurn = true
727816
} else if (this.control.xrRightStick.value.x < 0 && !this.didSnapTurn) {
728-
this.cam.rotation.y += 45 * DEG2RAD
817+
this.turnXRRigAtPlayer(45)
729818
this.didSnapTurn = true
730819
}
820+
// if we did snap turn, we need to refresh the hmd position to cancel it out
821+
if (this.didSnapTurn) {
822+
this.world.camera.getWorldPosition(v1)
823+
v1.y = 0
824+
v2.copy(this.xrRig.position)
825+
v2.y = 0
826+
v3.copy(v1).sub(v2)
827+
this.hmdLast.copy(v3)
828+
}
731829
} else if (this.control.pointer.locked) {
732830
// or pointer lock, rotate camera with pointer movement
733831
this.cam.rotation.x += -this.control.pointer.delta.y * POINTER_LOOK_SPEED * delta
@@ -741,25 +839,16 @@ export class PlayerLocal extends Entity {
741839
}
742840

743841
// ensure we can't look too far up/down
744-
if (!isXR) {
842+
if (!xr) {
745843
this.cam.rotation.x = clamp(this.cam.rotation.x, -89 * DEG2RAD, 89 * DEG2RAD)
746844
}
747845

748846
// zoom camera if scrolling wheel
749-
if (!isXR) {
847+
if (!xr) {
750848
this.cam.zoom += -this.control.scrollDelta.value * ZOOM_SPEED * delta
751849
this.cam.zoom = clamp(this.cam.zoom, MIN_ZOOM, MAX_ZOOM)
752850
}
753851

754-
// force zoom in xr to trigger first person (below)
755-
if (isXR && !this.xrActive) {
756-
this.cam.zoom = 0
757-
this.xrActive = true
758-
} else if (!isXR && this.xrActive) {
759-
this.cam.zoom = 1
760-
this.xrActive = false
761-
}
762-
763852
// transition in and out of first person
764853
if (this.cam.zoom < 1 && !this.firstPerson) {
765854
this.cam.zoom = 0
@@ -777,14 +866,14 @@ export class PlayerLocal extends Entity {
777866
}
778867

779868
// watch jump presses to either fly or air-jump
780-
this.jumpDown = isXR ? this.control.xrRightBtn1.down : this.control.space.down || this.control.touchA.down
781-
if (isXR ? this.control.xrRightBtn1.pressed : this.control.space.pressed || this.control.touchA.pressed) {
869+
this.jumpDown = xr ? this.control.xrRightBtn1.down : this.control.space.down || this.control.touchA.down
870+
if (xr ? this.control.xrRightBtn1.pressed : this.control.space.pressed || this.control.touchA.pressed) {
782871
this.jumpPressed = true
783872
}
784873

785874
// get our movement direction
786875
this.moveDir.set(0, 0, 0)
787-
if (isXR) {
876+
if (xr) {
788877
// in xr use controller input
789878
this.moveDir.x = this.control.xrLeftStick.value.x
790879
this.moveDir.z = this.control.xrLeftStick.value.z
@@ -830,7 +919,7 @@ export class PlayerLocal extends Entity {
830919
}
831920

832921
// determine if we're "running"
833-
if (this.stick?.active || isXR) {
922+
if (this.stick?.active || xr) {
834923
// touch/xr joysticks at full extent
835924
this.running = this.moving && this.moveDir.length() > 0.9
836925
} else {
@@ -842,9 +931,10 @@ export class PlayerLocal extends Entity {
842931
this.moveDir.normalize()
843932

844933
// flying direction
845-
if (isXR) {
934+
if (xr) {
846935
this.flyDir.copy(this.moveDir)
847-
this.flyDir.applyQuaternion(this.world.xr.camera.quaternion)
936+
this.world.camera.getWorldQuaternion(q1)
937+
this.flyDir.applyQuaternion(q1)
848938
} else {
849939
this.flyDir.copy(this.moveDir)
850940
this.flyDir.applyQuaternion(this.cam.quaternion)
@@ -868,9 +958,11 @@ export class PlayerLocal extends Entity {
868958
if (moveDeg < 0) moveDeg += 360
869959

870960
// rotate direction to face camera Y direction
871-
if (isXR) {
872-
e1.copy(this.world.xr.camera.rotation).reorder('YXZ')
873-
e1.y += this.cam.rotation.y
961+
if (xr) {
962+
this.world.camera.getWorldQuaternion(q1)
963+
e1.setFromQuaternion(q1).reorder('YXZ')
964+
// e1.y += this.xrRig.rotation.y
965+
// e1.y += this.cam.rotation.y // why not world.camera now?
874966
const yQuaternion = q1.setFromAxisAngle(UP, e1.y)
875967
this.moveDir.applyQuaternion(yQuaternion)
876968
} else {
@@ -881,9 +973,11 @@ export class PlayerLocal extends Entity {
881973
// get initial facing angle matching camera
882974
let rotY = 0
883975
let applyRotY
884-
if (isXR) {
885-
e1.copy(this.world.xr.camera.rotation).reorder('YXZ')
886-
rotY = e1.y + this.cam.rotation.y
976+
if (xr) {
977+
this.world.camera.getWorldQuaternion(q1)
978+
e1.setFromQuaternion(q1).reorder('YXZ')
979+
rotY = e1.y
980+
// rotY = e1.y + this.cam.rotation.y
887981
} else {
888982
rotY = this.cam.rotation.y
889983
}
@@ -932,8 +1026,9 @@ export class PlayerLocal extends Entity {
9321026
this.mode = mode
9331027

9341028
// set gaze direction
935-
if (isXR) {
936-
this.gaze.copy(FORWARD).applyQuaternion(this.world.xr.camera.quaternion)
1029+
if (xr) {
1030+
this.world.camera.getWorldQuaternion(q1)
1031+
this.gaze.copy(FORWARD).applyQuaternion(q1)
9371032
} else {
9381033
this.gaze.copy(FORWARD).applyQuaternion(this.cam.quaternion)
9391034
if (!this.firstPerson) {
@@ -1010,8 +1105,12 @@ export class PlayerLocal extends Entity {
10101105
}
10111106

10121107
lateUpdate(delta) {
1013-
const isXR = this.world.xr?.session
1108+
const xr = this.isXR
10141109
const anchor = this.getAnchorMatrix()
1110+
1111+
// if (xr) return
1112+
// console.log('lateUpdate')
1113+
10151114
// if we're anchored, force into that pose
10161115
if (anchor) {
10171116
this.base.position.setFromMatrixPosition(anchor)
@@ -1022,7 +1121,7 @@ export class PlayerLocal extends Entity {
10221121
}
10231122
// make camera follow our position horizontally
10241123
this.cam.position.copy(this.base.position)
1025-
if (isXR) {
1124+
if (xr) {
10261125
// ...
10271126
} else {
10281127
// and vertically at our vrm model height
@@ -1034,10 +1133,10 @@ export class PlayerLocal extends Entity {
10341133
this.cam.position.add(right.multiplyScalar(0.3))
10351134
}
10361135
}
1037-
if (this.world.xr?.session) {
1136+
if (xr) {
10381137
// in vr snap camera
1039-
this.control.camera.position.copy(this.cam.position)
1040-
this.control.camera.quaternion.copy(this.cam.quaternion)
1138+
// this.control.camera.position.copy(this.cam.position)
1139+
// this.control.camera.quaternion.copy(this.cam.quaternion)
10411140
} else {
10421141
// otherwise interpolate camera towards target
10431142
simpleCamLerp(this.world, this.control.camera, this.cam, delta)

src/core/systems/ClientGraphics.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,6 @@ export class ClientGraphics extends System {
6363
this.renderer.toneMappingExposure = 1
6464
this.renderer.outputColorSpace = THREE.SRGBColorSpace
6565
this.renderer.xr.enabled = true
66-
this.renderer.xr.setReferenceSpaceType('local-floor')
67-
this.renderer.xr.setFoveation(1)
6866
this.maxAnisotropy = this.renderer.capabilities.getMaxAnisotropy()
6967
THREE.Texture.DEFAULT_ANISOTROPY = this.maxAnisotropy
7068
this.usePostprocessing = this.world.prefs.postprocessing

src/core/systems/XR.js

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
import { System } from './System'
22
import * as THREE from '../extras/three'
3-
import { XRControllerModelFactory } from 'three/addons'
4-
5-
const UP = new THREE.Vector3(0, 1, 0)
6-
7-
const v1 = new THREE.Vector3()
8-
const e1 = new THREE.Euler(0, 0, 0, 'YXZ')
93

104
/**
115
* XR System
@@ -19,11 +13,8 @@ export class XR extends System {
1913
super(world)
2014
this.session = null
2115
this.camera = null
22-
this.controller1Model = null
23-
this.controller2Model = null
2416
this.supportsVR = false
2517
this.supportsAR = false
26-
this.controllerModelFactory = new XRControllerModelFactory()
2718
}
2819

2920
async init() {
@@ -32,6 +23,8 @@ export class XR extends System {
3223
}
3324

3425
async enter() {
26+
this.world.graphics.renderer.xr.setReferenceSpaceType('local-floor')
27+
this.world.graphics.renderer.xr.setFoveation(1)
3528
const session = await navigator.xr?.requestSession('immersive-vr', {
3629
requiredFeatures: ['local-floor'],
3730
})
@@ -43,28 +36,13 @@ export class XR extends System {
4336
}
4437
this.world.graphics.renderer.xr.setSession(session)
4538
session.addEventListener('end', this.onSessionEnd)
46-
this.session = session
4739
this.camera = this.world.graphics.renderer.xr.getCamera()
40+
this.session = session
4841
this.world.emit('xrSession', session)
49-
50-
this.controller1Model = this.world.graphics.renderer.xr.getControllerGrip(0)
51-
this.controller1Model.add(this.controllerModelFactory.createControllerModel(this.controller1Model))
52-
this.world.rig.add(this.controller1Model)
53-
54-
this.controller2Model = this.world.graphics.renderer.xr.getControllerGrip(1)
55-
this.controller2Model.add(this.controllerModelFactory.createControllerModel(this.controller2Model))
56-
this.world.rig.add(this.controller2Model)
5742
}
5843

5944
onSessionEnd = () => {
60-
this.world.camera.position.set(0, 0, 0)
61-
this.world.camera.rotation.set(0, 0, 0)
62-
this.world.rig.remove(this.controller1Model)
63-
this.world.rig.remove(this.controller2Model)
6445
this.session = null
65-
this.camera = null
66-
this.controller1Model = null
67-
this.controller2Model = null
6846
this.world.emit('xrSession', null)
6947
}
7048
}

0 commit comments

Comments
 (0)