Skip to content

Commit

Permalink
Use new hand models, fix default controllers, remove three-stdlib dep…
Browse files Browse the repository at this point in the history
…endency for now
  • Loading branch information
sniok committed May 6, 2021
1 parent f39f04c commit 7f0064b
Show file tree
Hide file tree
Showing 13 changed files with 949 additions and 123 deletions.
3 changes: 2 additions & 1 deletion examples/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
RayGrab,
useHitTest,
ARCanvas,
DefaultXRControllers,
} from '@react-three/xr'
// import { OrbitControls, Sky, Text, Plane, Box } from '@react-three/drei'
import { Box, Sky, Text } from '@react-three/drei'
Expand Down Expand Up @@ -64,7 +65,7 @@ function App() {

<Hands />
<Button position={[0, 0.8, -1]} />
{/* <DefaultXRControllers /> */}
<DefaultXRControllers />
{/* <HitTestExample /> */}
</VRCanvas>
)
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@
"react-dom": ">=17.0"
},
"dependencies": {
"react-merge-refs": "^1.1.0",
"three-stdlib": ">=2.0"
"react-merge-refs": "^1.1.0"
}
}
4 changes: 2 additions & 2 deletions src/DefaultXRControllers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useXR } from './XR'
import React, { useEffect } from 'react'
import { Color, Mesh, MeshBasicMaterial, BoxBufferGeometry, MeshBasicMaterialParameters, Group, Object3D, Intersection } from 'three'
import { useFrame, useThree } from '@react-three/fiber'
import { XRControllerModelFactory } from 'three-stdlib/webxr/XRControllerModelFactory'
import { XRControllerModelFactory } from './webxr/XRControllerModelFactory'

const modelFactory = new XRControllerModelFactory()
const modelCache = new WeakMap<Group, any>()
Expand Down Expand Up @@ -43,7 +43,7 @@ export function DefaultXRControllers({ rayMaterial = {} }: { rayMaterial?: MeshB
if (modelCache.has(controller)) {
model = modelCache.get(controller)
} else {
model = modelFactory.createControllerModel(controller)
model = modelFactory.createControllerModel(controller) as any
controller.dispatchEvent({ type: 'connected', data: inputSource, fake: true })
modelCache.set(controller, model)
}
Expand Down
18 changes: 4 additions & 14 deletions src/Hands.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
import { useThree } from '@react-three/fiber'
import { OculusHandModel } from './webxr/OculusHandModel.js'
import { useEffect } from 'react'
import { XRHandModelFactory } from 'three-stdlib/webxr/XRHandModelFactory'
import { XRHandOculusMeshModelOptions } from 'three-stdlib/webxr/XRHandOculusMeshModel'

interface HandsProps {
profile?: 'spheres' | 'boxes' | 'oculus' | 'oculus_lowpoly'
}

export function Hands({ profile = 'oculus' }: HandsProps) {
export function Hands() {
const { scene, gl } = useThree()

useEffect(() => {
const handFactory = new XRHandModelFactory().setPath('https://threejs.org/examples/models/fbx/')

const options = profile === 'oculus_lowpoly' ? ({ model: 'lowpoly' } as XRHandOculusMeshModelOptions) : undefined
const threeProfile = profile === 'oculus_lowpoly' ? 'oculus' : profile

// @ts-ignore
const hand1 = gl.xr.getHand(0)
hand1.add(new OculusHandModel(hand1))
scene.add(hand1)
hand1.add(handFactory.createHandModel(hand1, threeProfile, options))

// @ts-ignore
const hand2 = gl.xr.getHand(1)
hand1.add(new OculusHandModel(hand2))
scene.add(hand2)
hand2.add(handFactory.createHandModel(hand2, threeProfile, options))
}, [scene, gl])

return null
Expand Down
8 changes: 5 additions & 3 deletions src/XR.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */

import * as React from 'react'
import { Canvas, useFrame, useThree } from '@react-three/fiber'
import { ARButton } from 'three-stdlib/webxr/ARButton'
import { VRButton } from 'three-stdlib/webxr/VRButton'
import { ARButton } from './webxr/ARButton'
import { VRButton } from './webxr/VRButton'
import { XRController } from './XRController'
import { Props as ContainerProps } from '@react-three/fiber/dist/declarations/src/web/Canvas'
import { InteractionManager, InteractionsContext } from './Interactions'
Expand Down Expand Up @@ -188,7 +190,7 @@ export const useXRFrame = (callback: (time: DOMHighResTimeStamp, xrFrame: XRFram
gl.xr.getSession()!.cancelAnimationFrame(requestRef.current)
}
}
}, [gl.xr.isPresenting, loop])
}, [gl.xr.isPresenting, loop, gl.xr])
}

export const useController = (handedness: XRHandedness) => {
Expand Down
159 changes: 159 additions & 0 deletions src/webxr/ARButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
class ARButton {
static createButton(renderer, sessionInit = {}) {
const button = document.createElement('button')

function showStartAR(/*device*/) {
if (sessionInit.domOverlay === undefined) {
const overlay = document.createElement('div')
overlay.style.display = 'none'
document.body.appendChild(overlay)

const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('width', 38)
svg.setAttribute('height', 38)
svg.style.position = 'absolute'
svg.style.right = '20px'
svg.style.top = '20px'
svg.addEventListener('click', function () {
currentSession.end()
})
overlay.appendChild(svg)

const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
path.setAttribute('d', 'M 12,12 L 28,28 M 28,12 12,28')
path.setAttribute('stroke', '#fff')
path.setAttribute('stroke-width', 2)
svg.appendChild(path)

if (sessionInit.optionalFeatures === undefined) {
sessionInit.optionalFeatures = []
}

sessionInit.optionalFeatures.push('dom-overlay')
sessionInit.domOverlay = { root: overlay }
}

//

let currentSession = null

async function onSessionStarted(session) {
session.addEventListener('end', onSessionEnded)

renderer.xr.setReferenceSpaceType('local')

await renderer.xr.setSession(session)

button.textContent = 'STOP AR'
sessionInit.domOverlay.root.style.display = ''

currentSession = session
}

function onSessionEnded(/*event*/) {
currentSession.removeEventListener('end', onSessionEnded)

button.textContent = 'START AR'
sessionInit.domOverlay.root.style.display = 'none'

currentSession = null
}

//

button.style.display = ''

button.style.cursor = 'pointer'
button.style.left = 'calc(50% - 50px)'
button.style.width = '100px'

button.textContent = 'START AR'

button.onmouseenter = function () {
button.style.opacity = '1.0'
}

button.onmouseleave = function () {
button.style.opacity = '0.5'
}

button.onclick = function () {
if (currentSession === null) {
navigator.xr.requestSession('immersive-ar', sessionInit).then(onSessionStarted)
} else {
currentSession.end()
}
}
}

function disableButton() {
button.style.display = ''

button.style.cursor = 'auto'
button.style.left = 'calc(50% - 75px)'
button.style.width = '150px'

button.onmouseenter = null
button.onmouseleave = null

button.onclick = null
}

function showARNotSupported() {
disableButton()

button.textContent = 'AR NOT SUPPORTED'
}

function stylizeElement(element) {
element.style.position = 'absolute'
element.style.bottom = '20px'
element.style.padding = '12px 6px'
element.style.border = '1px solid #fff'
element.style.borderRadius = '4px'
element.style.background = 'rgba(0,0,0,0.1)'
element.style.color = '#fff'
element.style.font = 'normal 13px sans-serif'
element.style.textAlign = 'center'
element.style.opacity = '0.5'
element.style.outline = 'none'
element.style.zIndex = '999'
}

if ('xr' in navigator) {
button.id = 'ARButton'
button.style.display = 'none'

stylizeElement(button)

navigator.xr
.isSessionSupported('immersive-ar')
.then(function (supported) {
supported ? showStartAR() : showARNotSupported()
})
.catch(showARNotSupported)

return button
} else {
const message = document.createElement('a')

if (window.isSecureContext === false) {
message.href = document.location.href.replace(/^http:/, 'https:')
message.innerHTML = 'WEBXR NEEDS HTTPS' // TODO Improve message
} else {
message.href = 'https://immersiveweb.dev/'
message.innerHTML = 'WEBXR NOT AVAILABLE'
}

message.style.left = 'calc(50% - 90px)'
message.style.width = '180px'
message.style.textDecoration = 'none'

stylizeElement(message)

return message
}
}
}

export { ARButton }
74 changes: 74 additions & 0 deletions src/webxr/OculusHandModel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Object3D, Sphere, Box3 } from 'three'
import { XRHandMeshModel } from './XRHandMeshModel.js'

const TOUCH_RADIUS = 0.01
const POINTING_JOINT = 'index-finger-tip'

class OculusHandModel extends Object3D {
constructor(controller) {
super()

this.controller = controller
this.motionController = null
this.envMap = null

this.mesh = null

controller.addEventListener('connected', (event) => {
const xrInputSource = event.data

if (xrInputSource.hand && !this.motionController) {
this.xrInputSource = xrInputSource

this.motionController = new XRHandMeshModel(this, controller, this.path, xrInputSource.handedness)
}
})

controller.addEventListener('disconnected', () => {
this.clear()
this.motionController = null
})
}

updateMatrixWorld(force) {
super.updateMatrixWorld(force)

if (this.motionController) {
this.motionController.updateMesh()
}
}

getPointerPosition() {
const indexFingerTip = this.controller.joints[POINTING_JOINT]
if (indexFingerTip) {
return indexFingerTip.position
} else {
return null
}
}

intersectBoxObject(boxObject) {
const pointerPosition = this.getPointerPosition()
if (pointerPosition) {
const indexSphere = new Sphere(pointerPosition, TOUCH_RADIUS)
const box = new Box3().setFromObject(boxObject)
return indexSphere.intersectsBox(box)
} else {
return false
}
}

checkButton(button) {
if (this.intersectBoxObject(button)) {
button.onPress()
} else {
button.onClear()
}

if (button.isPressed()) {
button.whilePressed()
}
}
}

export { OculusHandModel }
Loading

0 comments on commit 7f0064b

Please sign in to comment.