From 70f4230dcfcca2527d0a01b5385c4e2584e64e3b Mon Sep 17 00:00:00 2001 From: Oleksandr Dubenko Date: Sun, 30 Aug 2020 17:25:33 +0200 Subject: [PATCH] Add Hands component, rework how controllers are loaded, add useController hook, add Hands example, add examples folder --- README.md | 28 +- examples/.env | 1 + examples/.gitignore | 23 + examples/README.md | 12 + examples/config-overrides.js | 25 + examples/package.json | 76 + examples/public/index.html | 20 + examples/src/index.js | 148 + examples/yarn.lock | 13102 +++++++++++++++++++++++++++++++++ package.json | 6 +- src/DefaultXRControllers.tsx | 84 + src/Hands.tsx | 24 + src/Interactions.tsx | 6 +- src/XR.tsx | 167 +- src/XRController.tsx | 24 +- src/index.tsx | 2 + src/webxr.tsx | 18 +- yarn.lock | 16 +- 18 files changed, 13638 insertions(+), 144 deletions(-) create mode 100644 examples/.env create mode 100644 examples/.gitignore create mode 100644 examples/README.md create mode 100644 examples/config-overrides.js create mode 100644 examples/package.json create mode 100644 examples/public/index.html create mode 100644 examples/src/index.js create mode 100644 examples/yarn.lock create mode 100644 src/DefaultXRControllers.tsx create mode 100644 src/Hands.tsx diff --git a/README.md b/README.md index 119bbaf..57d201e 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ React components and hooks for creating VR/AR applications with [react-three-fib +

These demos are real, you can click them! They contain the full code, too. @@ -89,7 +90,7 @@ Controllers is an array of `XRController` objects interface XRController { grip: Group controller: Group - inputSource?: XRInputSource + inputSource: XRInputSource // ... // more in XRController.ts } @@ -119,6 +120,23 @@ const onSqueeze = useCallback(() => console.log('Left controller squeeze'), []) useXREvent('squeeze', onSqueeze, { handedness: 'left' }) ``` +### useControllers + +Use this hook to get n instance of the controller + +```jsx +const leftController = useController('left') +``` + +### `` + +Add hands model for hand-tracking. Currently only works on Oculus Quest with #webxr-hands experimental flag enabled + +```jsx + + +``` + ### Interactions `react-xr` comes with built-in high level interaction components. @@ -145,7 +163,7 @@ useXREvent('squeeze', onSqueeze, { handedness: 'left' }) ## Getting the VR Camera (HMD) Location -To get the position of the VR camera, use three's WebXRManager instance. +To get the position of the VR camera, use three's WebXRManager instance. ```jsx const { camera } = useThree() @@ -159,7 +177,7 @@ If you want to attach the user to an object so it can be moved around, just pare ```jsx const mesh = useRef() const { gl, camera } = useThree() - + useEffect(() => { const cam = gl.xr.isPresenting ? gl.xr.getCamera(camera) : camera mesh.current.add(cam) @@ -169,7 +187,7 @@ useEffect(() => { // bundle add the controllers to the same object as the camera so it all stays together. const { controllers } = useXR() useEffect(() => { - if (controllers.length > 0) controllers.forEach(c => mesh.current.add(c.grip)) - return () => controllers.forEach(c => mesh.current.remove(c.grip)) + if (controllers.length > 0) controllers.forEach((c) => mesh.current.add(c.grip)) + return () => controllers.forEach((c) => mesh.current.remove(c.grip)) }, [controllers, mesh]) ``` diff --git a/examples/.env b/examples/.env new file mode 100644 index 0000000..7d910f1 --- /dev/null +++ b/examples/.env @@ -0,0 +1 @@ +SKIP_PREFLIGHT_CHECK=true \ No newline at end of file diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..4d29575 --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..6fc4c6b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,12 @@ +# Explore Examples + +You can explore additional examples locally: + +```shell +git clone https://github.com/react-spring/react-xr +cd react-xr +yarn +cd examples +yarn +yarn start +``` diff --git a/examples/config-overrides.js b/examples/config-overrides.js new file mode 100644 index 0000000..96ffad7 --- /dev/null +++ b/examples/config-overrides.js @@ -0,0 +1,25 @@ +const { addWebpackAlias, removeModuleScopePlugin, babelInclude, override } = require('customize-cra') +const { addReactRefresh } = require('customize-cra-react-refresh') +const path = require('path') + +module.exports = (config, env) => { + config.resolve.extensions = [...config.resolve.extensions, '.ts', '.tsx'] + return override( + addReactRefresh(), + removeModuleScopePlugin(), + babelInclude([path.resolve('src'), path.resolve('../src')]), + addWebpackAlias({ + 'react-three-fiber': path.resolve('node_modules/react-three-fiber'), + 'react-xr': path.resolve('../src/'), + react: path.resolve('node_modules/react'), + 'react-dom': path.resolve('node_modules/react-dom'), + scheduler: path.resolve('node_modules/scheduler'), + 'react-scheduler': path.resolve('node_modules/react-scheduler'), + 'prop-types': path.resolve('node_modules/prop-types'), + three: path.resolve('node_modules/three'), + // three$: path.res olve('node_modules/three/src/Three'), + //three$: path.resolve('./resources/three.js'), + // '../../../build/three.module.js': path.resolve('./resources/three.js'), + }) + )(config, env) +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..91031db --- /dev/null +++ b/examples/package.json @@ -0,0 +1,76 @@ +{ + "name": "examples-cra", + "version": "0.1.0", + "private": true, + "dependencies": { + "@testing-library/jest-dom": "^5.3.0", + "@testing-library/react": "^10.0.2", + "@testing-library/user-event": "^10.0.1", + "cannon": "^0.6.2", + "drei": "^0.0.64", + "pseudo-worker": "^1.3.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-merge-refs": "^1.0.0", + "react-router-dom": "^5.1.2", + "react-scripts": "3.4.1", + "react-spring": "^8.0.27", + "react-three-fiber": "^4.2.17", + "react-three-gui": "^0.1.5", + "react-use-gesture": "^7.0.9", + "styled-components": "^5.0.1", + "three": "^0.119.1", + "threejs-meshline": "^2.0.10", + "use-cannon": "https://github.com/react-spring/use-cannon.git", + "zustand": "^2.2.3" + }, + "scripts": { + "start": "HTTPS=true react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", + "eject": "react-app-rewired eject" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "prettier": { + "semi": false, + "trailingComma": "es5", + "singleQuote": true, + "jsxBracketSameLine": true, + "tabWidth": 2, + "printWidth": 120 + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "prettier --write", + "git add" + ] + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/preset-typescript": "^7.9.0", + "customize-cra": "^0.9.1", + "customize-cra-react-refresh": "^1.0.1", + "husky": "^4.2.3", + "lint-staged": "^10.1.2", + "prettier": "^2.0.2", + "react-app-rewired": "^2.1.5" + } +} diff --git a/examples/public/index.html b/examples/public/index.html new file mode 100644 index 0000000..45ad037 --- /dev/null +++ b/examples/public/index.html @@ -0,0 +1,20 @@ + + + + + + react-xr examples + + + +

+ + diff --git a/examples/src/index.js b/examples/src/index.js new file mode 100644 index 0000000..72ee6fb --- /dev/null +++ b/examples/src/index.js @@ -0,0 +1,148 @@ +import ReactDOM from 'react-dom' +import React, { useState, useEffect, useRef, Suspense, useMemo, useCallback } from 'react' +import { VRCanvas, useXREvent, DefaultXRControllers, Hands, Select, Hover, useXR } from 'react-xr' +import { useFrame, useThree } from 'react-three-fiber' +import { OrbitControls, Sky, Text, Plane, Box } from 'drei' +import { Color, Box3, BufferGeometry, Vector3, TextureLoader, ImageLoader } from 'three/build/three.module' + +function Key({ name, pos = [0, 0], onClick, width = 1, ...rest }) { + const meshRef = useRef() + + const { gl } = useThree() + + const leftHand = gl.xr.getHand(0) + + const focused = useRef(false) + useFrame(() => { + if (!meshRef.current) { + return + } + const leftTip = leftHand.joints[9] + if (leftTip === undefined) { + return + } + + const box = new Box3().setFromObject(meshRef.current) + + if (box.containsPoint(leftTip.position)) { + if (!focused.current) { + onClick() + focused.current = true + meshRef.current.material.color = new Color(0x444444) + } + } else { + if (focused.current) { + meshRef.current.material.color = new Color(0xffffff) + focused.current = false + } + } + }) + + const keySize = 0.018 + const keyWidth = width * keySize + const keyGap = 0.004 + const size = keySize + keyGap + + const xpi = (pos[0] / 10) * Math.PI + const offset = 0.01 - Math.sin(xpi) / 20 + + const position = [size * pos[0], -size * pos[1], offset] + + return ( + + + + + + + {name} + + + ) +} + +function Keyboard() { + const [state, setState] = useState({ + text: '', + focused: ' ', + }) + + const { gl } = useThree() + + useEffect(() => { + const rightHand = gl.xr.getHand(1) + + const onPinch = () => { + setState((it) => { + const text = it.focused === 'backspace' ? it.text.substring(0, it.text.length - 1) : it.text + it.focused + return { + text, + focused: it.focused, + } + }) + } + rightHand.addEventListener('pinchstart', onPinch) + + return () => { + rightHand.removeEventListener('pinchstart', onPinch) + } + }, [gl, setState]) + + const onClick = (key) => () => setState((it) => ({ ...it, focused: key })) + + return ( + + + {state.text + '|'} + + {['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'].map((key, i) => ( + + ))} + {['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'].map((key, i) => ( + + ))} + {['z', 'x', 'c', 'v', 'b', 'n', 'm'].map((key, i) => ( + + ))} + + + + ) +} + +function Button(props) { + const [hover, setHover] = useState(false) + const [color, setColor] = useState(0x123456) + + const onSelect = useCallback(() => { + setColor((Math.random() * 0xffffff) | 0) + }, [setColor]) + + return ( + + ) +} + +function App() { + return ( + + + + + + +