Skip to content

Commit 567ac1c

Browse files
authored
fix(hand tracking): fix issue when hand lose tracking and stays visible
Fixes #273 Me a @saitonakamura checked how the events were sent and found some issues with code not cleaning up, not disposing used resources and not hiding hands when they are not tracking. We decided to add OculusHandModel and XRHandMeshModel to this library so the fix is ready for usage as soon as possible.
1 parent b48c6e3 commit 567ac1c

File tree

7 files changed

+258
-17
lines changed

7 files changed

+258
-17
lines changed

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
## Run locally
1414

15-
* Run `yarn dev` to be able to to run examples
15+
* Run `yarn dev` to be able to run examples
1616

1717
## PR checklist
1818

@@ -27,4 +27,4 @@
2727
### Test examples locally
2828

2929
Using real device is recommended, otherwise you can use https://github.com/meta-quest/immersive-web-emulator/.
30-
See [Run locally](#run-locally)
30+
See [Run locally](#run-locally)

examples/src/demos/Teleport.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Canvas } from '@react-three/fiber'
2-
import { Hands, XR, VRButton, TeleportationPlane, Controllers } from '@react-three/xr'
2+
import { XR, VRButton, TeleportationPlane, Controllers } from '@react-three/xr'
33

44
export default function () {
55
return (

src/Controllers.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,16 @@ class ControllerModel extends THREE.Group {
6464
}
6565

6666
private _onConnected(event: XRControllerEvent) {
67+
if (event.data?.hand) {
68+
return
69+
}
6770
modelFactory.initializeControllerModel(this.xrControllerModel, event)
6871
}
6972

70-
private _onDisconnected(_event: XRControllerEvent) {
73+
private _onDisconnected(event: XRControllerEvent) {
74+
if (event.data?.hand) {
75+
return
76+
}
7177
this.xrControllerModel.disconnect()
7278
}
7379

src/Hands.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react'
22
import { Object3DNode, extend, createPortal } from '@react-three/fiber'
3-
import { OculusHandModel } from 'three-stdlib'
3+
import { OculusHandModel } from './OculusHandModel'
44
import { useXR } from './XR'
55
import { useIsomorphicLayoutEffect } from './utils'
66

src/OculusHandModel.ts

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Object3D, Sphere, Box3, Mesh, Texture, Vector3, EventListener, Event } from 'three'
2+
import { XRHandMeshModel } from './XRHandMeshModel'
3+
const TOUCH_RADIUS = 0.01
4+
const POINTING_JOINT = 'index-finger-tip'
5+
6+
export interface XRButton extends Object3D {
7+
onPress(): void
8+
onClear(): void
9+
isPressed(): boolean
10+
whilePressed(): void
11+
}
12+
13+
class OculusHandModel extends Object3D {
14+
controller: Object3D
15+
motionController: XRHandMeshModel | null
16+
envMap: Texture | null
17+
mesh: Mesh | null
18+
xrInputSource: XRInputSource | null
19+
20+
leftModelPath?: string
21+
rightModelPath?: string
22+
23+
constructor(controller: Object3D, leftModelPath?: string, rightModelPath?: string) {
24+
super()
25+
26+
this.controller = controller
27+
this.motionController = null
28+
this.envMap = null
29+
this.leftModelPath = leftModelPath
30+
this.rightModelPath = rightModelPath
31+
32+
this.mesh = null
33+
this.xrInputSource = null
34+
35+
controller.addEventListener('connected', this._onConnected)
36+
controller.addEventListener('disconnected', this._onDisconnected)
37+
}
38+
39+
private _onConnected: EventListener<Event, 'connected', Object3D<Event>> = (event) => {
40+
const xrInputSource = event.data
41+
42+
if (xrInputSource.hand && !this.motionController) {
43+
this.xrInputSource = xrInputSource
44+
45+
this.motionController = new XRHandMeshModel(
46+
this,
47+
this.controller,
48+
undefined,
49+
xrInputSource.handedness,
50+
xrInputSource.handedness === 'left' ? this.leftModelPath : this.rightModelPath
51+
)
52+
}
53+
}
54+
55+
private _onDisconnected: EventListener<Event, 'disconnected', Object3D<Event>> = () => {
56+
if (!this.xrInputSource?.hand) {
57+
return;
58+
}
59+
this.motionControllerCleanup()
60+
}
61+
62+
private motionControllerCleanup(): void {
63+
this.clear()
64+
this.motionController?.dispose()
65+
this.motionController = null
66+
}
67+
68+
updateMatrixWorld(force?: boolean): void {
69+
super.updateMatrixWorld(force)
70+
71+
if (this.motionController) {
72+
this.motionController.updateMesh()
73+
}
74+
}
75+
76+
getPointerPosition(): Vector3 | null {
77+
// @ts-ignore XRController needs to extend Group
78+
const indexFingerTip = this.controller.joints[POINTING_JOINT]
79+
if (indexFingerTip) {
80+
return indexFingerTip.position
81+
} else {
82+
return null
83+
}
84+
}
85+
86+
intersectBoxObject(boxObject: Object3D): boolean {
87+
const pointerPosition = this.getPointerPosition()
88+
if (pointerPosition) {
89+
const indexSphere = new Sphere(pointerPosition, TOUCH_RADIUS)
90+
const box = new Box3().setFromObject(boxObject)
91+
return indexSphere.intersectsBox(box)
92+
} else {
93+
return false
94+
}
95+
}
96+
97+
checkButton(button: XRButton): void {
98+
if (this.intersectBoxObject(button)) {
99+
button.onPress()
100+
} else {
101+
button.onClear()
102+
}
103+
104+
if (button.isPressed()) {
105+
button.whilePressed()
106+
}
107+
}
108+
109+
dispose(): void {
110+
this.motionControllerCleanup()
111+
112+
this.controller.removeEventListener('connected', this._onConnected)
113+
this.controller.removeEventListener('disconnected', this._onDisconnected)
114+
}
115+
}
116+
117+
export { OculusHandModel }

src/XRHandMeshModel.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { Object3D } from 'three'
2+
import { GLTFLoader } from 'three-stdlib'
3+
4+
const DEFAULT_HAND_PROFILE_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/[email protected]/dist/profiles/generic-hand/'
5+
6+
class XRHandMeshModel {
7+
controller: Object3D
8+
handModel: Object3D
9+
bones: Object3D[]
10+
scene?: Object3D
11+
12+
constructor(
13+
handModel: Object3D,
14+
controller: Object3D,
15+
path: string = DEFAULT_HAND_PROFILE_PATH,
16+
handedness: string,
17+
customModelPath?: string
18+
) {
19+
this.controller = controller
20+
this.handModel = handModel
21+
22+
this.bones = []
23+
24+
const loader = new GLTFLoader()
25+
if (!customModelPath) loader.setPath(path)
26+
loader.load(customModelPath ?? `${handedness}.glb`, (gltf: { scene: Object3D }) => {
27+
const object = gltf.scene.children[0]
28+
this.handModel.add(object)
29+
this.scene = object
30+
31+
const mesh = object.getObjectByProperty('type', 'SkinnedMesh')!
32+
mesh.frustumCulled = false
33+
mesh.castShadow = true
34+
mesh.receiveShadow = true
35+
36+
const joints = [
37+
'wrist',
38+
'thumb-metacarpal',
39+
'thumb-phalanx-proximal',
40+
'thumb-phalanx-distal',
41+
'thumb-tip',
42+
'index-finger-metacarpal',
43+
'index-finger-phalanx-proximal',
44+
'index-finger-phalanx-intermediate',
45+
'index-finger-phalanx-distal',
46+
'index-finger-tip',
47+
'middle-finger-metacarpal',
48+
'middle-finger-phalanx-proximal',
49+
'middle-finger-phalanx-intermediate',
50+
'middle-finger-phalanx-distal',
51+
'middle-finger-tip',
52+
'ring-finger-metacarpal',
53+
'ring-finger-phalanx-proximal',
54+
'ring-finger-phalanx-intermediate',
55+
'ring-finger-phalanx-distal',
56+
'ring-finger-tip',
57+
'pinky-finger-metacarpal',
58+
'pinky-finger-phalanx-proximal',
59+
'pinky-finger-phalanx-intermediate',
60+
'pinky-finger-phalanx-distal',
61+
'pinky-finger-tip'
62+
]
63+
64+
joints.forEach((jointName) => {
65+
const bone = object.getObjectByName(jointName) as any
66+
67+
if (bone !== undefined) {
68+
bone.jointName = jointName
69+
} else {
70+
console.warn(`Couldn't find ${jointName} in ${handedness} hand mesh`)
71+
}
72+
73+
this.bones.push(bone)
74+
})
75+
})
76+
}
77+
78+
updateMesh(): void {
79+
// XR Joints
80+
const XRJoints = (this.controller as any).joints
81+
let allInvisible = true
82+
83+
for (let i = 0; i < this.bones.length; i++) {
84+
const bone = this.bones[i]
85+
86+
if (bone) {
87+
const XRJoint = XRJoints[(bone as any).jointName]
88+
89+
if (XRJoint.visible) {
90+
const position = XRJoint.position
91+
bone.position.copy(position)
92+
bone.quaternion.copy(XRJoint.quaternion)
93+
allInvisible = false
94+
}
95+
}
96+
}
97+
98+
// Hide hand mesh if all joints are invisible in case hand loses tracking
99+
if (allInvisible && this.scene) {
100+
this.scene.visible = false
101+
} else if (this.scene) {
102+
this.scene.visible = true
103+
}
104+
}
105+
106+
dispose(): void {
107+
if (this.scene) {
108+
this.handModel.remove(this.scene)
109+
}
110+
}
111+
}
112+
113+
export { XRHandMeshModel }

yarn.lock

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"@jridgewell/gen-mapping" "^0.1.0"
1111
"@jridgewell/trace-mapping" "^0.3.9"
1212

13-
1413
"@babel/code-frame@^7.18.6":
1514
version "7.18.6"
1615
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz"
@@ -755,6 +754,11 @@
755754
resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz"
756755
integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==
757756

757+
"@types/draco3d@^1.4.0":
758+
version "1.4.2"
759+
resolved "https://registry.yarnpkg.com/@types/draco3d/-/draco3d-1.4.2.tgz#7faccb809db2a5e19b9efb97c5f2eb9d64d527ea"
760+
integrity sha512-goh23EGr6CLV6aKPwN1p8kBD/7tT5V/bLpToSbarKrwVejqNrspVrv8DhliteYkkhZYrlq/fwKZRRUzH4XN88w==
761+
758762
"@types/json-schema@^7.0.9":
759763
version "7.0.11"
760764
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz"
@@ -827,6 +831,11 @@
827831
resolved "https://registry.npmjs.org/@types/webxr/-/webxr-0.4.0.tgz"
828832
integrity sha512-LQvrACV3Pj17GpkwHwXuTd733gfY+D7b9mKdrTmLdO7vo7P/o6209Qqtk63y/FCv/lspdmi0pWz6Qe/ull9kQg==
829833

834+
"@types/webxr@^0.5.2":
835+
version "0.5.2"
836+
resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.2.tgz#5d9627b0ffe223aa3b166de7112ac8a9460dc54f"
837+
integrity sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw==
838+
830839
"@typescript-eslint/eslint-plugin@^5.11.0":
831840
version "5.28.0"
832841
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.28.0.tgz"
@@ -2337,11 +2346,6 @@ jsonc-parser@^3.2.0:
23372346
array-includes "^3.1.4"
23382347
object.assign "^4.1.2"
23392348

2340-
ktx-parse@^0.2.1:
2341-
version "0.2.2"
2342-
resolved "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.2.2.tgz"
2343-
integrity sha512-cFBc1jnGG2WlUf52NbDUXK2obJ+Mo9WUkBRvr6tP6CKxRMvZwDDFNV3JAS4cewETp5KyexByfWm9sm+O8AffiQ==
2344-
23452349
ktx-parse@^0.4.5:
23462350
version "0.4.5"
23472351
resolved "https://registry.npmjs.org/ktx-parse/-/ktx-parse-0.4.5.tgz"
@@ -3187,16 +3191,17 @@ three-mesh-bvh@^0.5.10:
31873191
integrity sha512-IMNHrAnsLCIxcFmAGkA4Wibw1QEpFQlkR72XUxZFOatNSpfMRUhJXQwQ5jPxbrX0W+OR838t/IR3laMOvQnT/g==
31883192

31893193
three-stdlib@^2.10.2:
3190-
version "2.12.1"
3191-
resolved "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.12.1.tgz"
3192-
integrity sha512-G3SSsCOBiWa0sjjPt+K28ikQ84Plm/ZVUozMfWagK59kZqBWcaPVXpOThkAgvdBpm2zCWLW3edAoW/4XIbljVQ==
3194+
version "2.23.10"
3195+
resolved "https://registry.yarnpkg.com/three-stdlib/-/three-stdlib-2.23.10.tgz#94907a558a00da327bd74308c92078fea72f77fc"
3196+
integrity sha512-y0DlxaN5HZXI9hKjEtqO2xlCEt7XyDCOMvD2M3JJFBmYjwbU+PbJ1n3Z+7Hr/6BeVGE6KZYcqPMnfKrTK5WTJg==
31933197
dependencies:
3194-
"@babel/runtime" "^7.16.7"
3195-
"@webgpu/glslang" "^0.0.15"
3198+
"@types/draco3d" "^1.4.0"
3199+
"@types/offscreencanvas" "^2019.6.4"
3200+
"@types/webxr" "^0.5.2"
31963201
chevrotain "^10.1.2"
31973202
draco3d "^1.4.1"
31983203
fflate "^0.6.9"
3199-
ktx-parse "^0.2.1"
3204+
ktx-parse "^0.4.5"
32003205
mmd-parser "^1.0.4"
32013206
opentype.js "^1.3.3"
32023207
potpack "^1.0.1"

0 commit comments

Comments
 (0)