Skip to content

Commit

Permalink
Add hover and select interactions, edit readme with how to get started
Browse files Browse the repository at this point in the history
  • Loading branch information
sniok committed May 15, 2020
1 parent 55ce09f commit 531deae
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 92 deletions.
19 changes: 3 additions & 16 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@
"node": true
},
"parser": "babel-eslint",
"extends": [
"eslint:recommended",
"plugin:import/errors",
"plugin:import/warnings",
"prettier"
],
"extends": ["eslint:recommended", "plugin:import/errors", "plugin:import/warnings", "prettier"],
"plugins": ["react-hooks", "react"],
"parserOptions": {
"ecmaFeatures": {
Expand All @@ -24,7 +19,6 @@
"no-dupe-class-members": "error",
"no-empty": "warn",
"no-undef": "error",
"no-unused-expressions": "error",
"no-unused-vars": "error",
"no-var": "warn",
"prefer-const": "warn",
Expand All @@ -45,11 +39,7 @@
"project": "./tsconfig.json",
"sourceType": "module"
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"prettier/@typescript-eslint"
],
"extends": ["plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/eslint-recommended", "prettier/@typescript-eslint"],
"rules": {
"prefer-const": "off",
"import/no-unresolved": "off",
Expand All @@ -60,10 +50,7 @@
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-parameter-properties": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
],
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-inferrable-types": "warn",
"@typescript-eslint/no-empty-function": "off",
Expand Down
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"printWidth": 140,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": true
}
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,59 @@
React components and hooks for creating VR/AR/XR applications with [react-three-fiber](https://github.com/react-spring/react-three-fiber)

**Note: Extremely early in development. Contributors welcome!**

## Installation

```
npm install react-xr
```

## Getting started

Enable VR in the `Canvas` component, add VR button and add `XR` component at the root of your application. This will provide context for XR related state.

```js
import { XR } from 'react-xr'

function App() {
return (
<Canvas
vr
colorManagement
onCreated={({ gl }) => {
document.body.appendChild(VRButton.createButton(gl))
}}>
<XR>{/* All the stuff goes here */}</XR>
</Canvas>
)
}
```

## Adding controllers to the scene

You can access controllers' state (position, orientation, etc.) by using `useXR()` hook

```js
const { controllers } = useXR()
```

To get started with default controller models add `DefaultXRControllers` component. It will fetch appropriate input profile models. You can learn more [here](https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/motion-controllers).

```js
import { XR } from 'react-xr'

function App() {
return (
<Canvas
vr
colorManagement
onCreated={({ gl }) => {
document.body.appendChild(VRButton.createButton(gl))
}}>
<XR>
<DefaultXRControllers />
</XR>
</Canvas>
)
}
```
48 changes: 48 additions & 0 deletions src/Interactions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { useRef, useEffect, useCallback } from 'react'
import { useXR, useXREvent, XRInteractionEvent } from './XR'
import { XRHandedness } from './webxr'

export function Hover({ onChange, children }: any) {
const ref = useRef()
const { addInteraction } = useXR()

useEffect(() => {
addInteraction(ref.current, 'onHover', () => onChange(true))
addInteraction(ref.current, 'onBlur', () => onChange(false))
}, [onChange, addInteraction])

return <group ref={ref}>{children}</group>
}

export function Select({ onSelect, children }: any) {
const ref = useRef()
const { addInteraction } = useXR()

const hovered = useRef(false)
const hoveredWhenStarted = useRef(false)
const hoveredHandedness = useRef<XRHandedness | undefined>(undefined)

const onStart = useCallback((e: XRInteractionEvent) => {
hoveredWhenStarted.current = hovered.current
hoveredHandedness.current = e.controller.inputSource?.handedness
}, [])

const onEnd = useCallback(
(e: XRInteractionEvent) => {
if (hoveredWhenStarted.current && e.controller.inputSource?.handedness === hoveredHandedness.current) {
onSelect()
}
},
[onSelect]
)

useXREvent('selectstart', onStart)
useXREvent('selectend', onEnd)

useEffect(() => {
addInteraction(ref.current, 'onHover', () => (hovered.current = true))
addInteraction(ref.current, 'onBlur', () => (hovered.current = false))
}, [addInteraction])

return <group ref={ref}>{children}</group>
}
160 changes: 160 additions & 0 deletions src/XR.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import * as React from 'react'
import { Object3D, Matrix4, Raycaster, Intersection } from 'three'
import { XRControllerModelFactory } from 'three/examples/jsm/webxr/XRControllerModelFactory'
import { useThree, useFrame } from 'react-three-fiber'
import { XRHandedness } from './webxr'
import { XRController } from './XRController'

const XRContext = React.createContext<{
controllers: XRController[]
addInteraction: any
}>({
controllers: []
} as any)

export interface XRInteractionEvent {
intersection?: Intersection
controller: XRController
}

export type XRInteractionType = 'onHover' | 'onBlur'

export type XRInteractionHandler = (event: XRInteractionEvent) => any

export function XR(props: { children: React.ReactNode }) {
const { gl } = useThree()
const [controllers, setControllers] = React.useState<XRController[]>([])

const state = React.useRef({
interactable: new Set<Object3D>(),
handlers: {
onHover: new WeakMap<Object3D, XRInteractionHandler>(),
onBlur: new WeakMap<Object3D, XRInteractionHandler>()
}
})

const addInteraction = React.useCallback((object: Object3D, eventType: XRInteractionType, handler: any) => {
state.current.interactable.add(object)
state.current.handlers[eventType].set(object, handler)
}, [])

React.useEffect(() => {
const initialControllers = [0, 1].map((id) => XRController.make(id, gl))

setControllers(initialControllers)

// Once they are connected update them with obtained inputSource
const updateController = (index: number) => (event: any) => {
setControllers((existingControllers) => {
const copy = [...existingControllers]
copy[index] = { ...copy[index], inputSource: event.data }
return copy
})
}

initialControllers.forEach(({ controller }, i) => {
controller.addEventListener('connected', updateController(i))
})
}, [gl])

const [raycaster] = React.useState(() => new Raycaster())

useFrame(() => {
const intersect = (controller: Object3D) => {
const objects = Array.from(state.current.interactable)
const tempMatrix = new Matrix4()
tempMatrix.identity().extractRotation(controller.matrixWorld)
raycaster.ray.origin.setFromMatrixPosition(controller.matrixWorld)
raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix)

return raycaster.intersectObjects(objects, true)
}

const { handlers } = state.current

controllers.forEach((it) => {
const { controller, hovering } = it
const hits = new Set()
const intersections = intersect(controller)

intersections.forEach((intersection) => {
let eventObject: Object3D | null = intersection.object
while (eventObject) {
if (!hovering.has(eventObject) && handlers.onHover.has(eventObject)) {
hovering.add(eventObject)
handlers.onHover.get(eventObject)?.({
controller: it,
intersection
})
}
hits.add(eventObject.id)
eventObject = eventObject.parent
}
})

hovering.forEach((object) => {
if (!hits.has(object.id)) {
hovering.delete(object)
if (handlers.onBlur.has(object)) {
handlers.onBlur.get(object)?.({ controller: it })
}
}
})
})
})

return <XRContext.Provider value={{ controllers, addInteraction }}>{props.children}</XRContext.Provider>
}

export const useXR = () => React.useContext(XRContext)

export const useXREvent = (
event: string,
handler: (e: any) => any,
{
handedness
}: {
handedness?: XRHandedness
} = {}
) => {
const { controllers: allControllers } = useXR()

React.useEffect(() => {
const controllers = handedness ? allControllers.filter((it) => it.inputSource?.handedness === handedness) : allControllers

controllers.forEach((it) => it.controller.addEventListener(event, handler))

return () => {
controllers.forEach((it) => it.controller.removeEventListener(event, handler))
}
}, [event, handler, allControllers, handedness])
}

export function DefaultXRControllers() {
const { controllers } = useXR()

const modelFactory = React.useMemo(() => new XRControllerModelFactory(), [])

const [modelMap] = React.useState(() => new WeakMap())

const models = React.useMemo(
() =>
controllers.map(({ controller, grip }) => {
// Model factory listens for 'connect' event so we can only create models on inital render
const model = modelMap.get(controller) ?? modelFactory.createControllerModel(controller)

if (modelMap.get(controller) === undefined) {
modelMap.set(controller, model)
}

return (
<primitive object={grip} dispose={null} key={grip.id}>
<primitive object={model} />
</primitive>
)
}),
[controllers, modelFactory, modelMap]
)

return <group>{models}</group>
}
32 changes: 32 additions & 0 deletions src/XRController.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { WebGLRenderer, Group, Object3D } from 'three'
import { XRInputSource } from './webxr'

export interface XRController {
inputSource?: XRInputSource
/**
* Group with orientation that should be used to render virtual
* objects such that they appear to be held in the user’s hand
*/
grip: Group
/** Group with orientation of the preferred pointing ray */
controller: Group
hovering: Set<Object3D>
selecting: Set<Object3D>
}
export const XRController = {
make: (id: number, gl: WebGLRenderer): XRController => {
const controller = gl.xr.getController(id)
const grip = gl.xr.getControllerGrip(id)
const xrController = {
inputSource: undefined,
grip,
controller,
hovering: new Set<Object3D>(),
selecting: new Set<Object3D>()
}
grip.userData.name = 'grip'
controller.userData.name = 'controller'

return xrController
}
}
Loading

0 comments on commit 531deae

Please sign in to comment.