-
-
Notifications
You must be signed in to change notification settings - Fork 166
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add hover and select interactions, edit readme with how to get started
- Loading branch information
Showing
8 changed files
with
313 additions
and
92 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.