From 64065b73840a14f38e11d41ae1f10911a17d4e91 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Wed, 3 Dec 2025 01:03:19 +0100 Subject: [PATCH 1/7] docs: Initial project for customizing things --- examples/customizing-models/app.tsx | 28 +++++++ examples/customizing-models/index.html | 12 +++ examples/customizing-models/index.tsx | 9 +++ examples/customizing-models/package.json | 15 ++++ examples/customizing-models/styles.css | 32 ++++++++ examples/customizing-models/vite.config.ts | 12 +++ pnpm-lock.yaml | 91 +++++++++++++++++++++- 7 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 examples/customizing-models/app.tsx create mode 100644 examples/customizing-models/index.html create mode 100644 examples/customizing-models/index.tsx create mode 100644 examples/customizing-models/package.json create mode 100644 examples/customizing-models/styles.css create mode 100644 examples/customizing-models/vite.config.ts diff --git a/examples/customizing-models/app.tsx b/examples/customizing-models/app.tsx new file mode 100644 index 00000000..517a93ca --- /dev/null +++ b/examples/customizing-models/app.tsx @@ -0,0 +1,28 @@ +import { OrbitControls, Plane } from '@react-three/drei' +import { Canvas } from '@react-three/fiber' +import { createXRStore, XR } from '@react-three/xr' +import * as THREE from 'three' +import './styles.css' + +const store = createXRStore() + +const axisColor = new THREE.Color('#9d3d4a') +const gridColor = new THREE.Color('#4f4f4f') + +export function App() { + return ( +
+ + + + + + + + + + + +
+ ) +} diff --git a/examples/customizing-models/index.html b/examples/customizing-models/index.html new file mode 100644 index 00000000..eee04f27 --- /dev/null +++ b/examples/customizing-models/index.html @@ -0,0 +1,12 @@ + + + + + + Customizing Models + + + +
+ + diff --git a/examples/customizing-models/index.tsx b/examples/customizing-models/index.tsx new file mode 100644 index 00000000..94a7343b --- /dev/null +++ b/examples/customizing-models/index.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './app.js' + +createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/examples/customizing-models/package.json b/examples/customizing-models/package.json new file mode 100644 index 00000000..7eb0f3c9 --- /dev/null +++ b/examples/customizing-models/package.json @@ -0,0 +1,15 @@ +{ + "dependencies": { + "@pmndrs/pointer-events": "workspace:~", + "@preact/signals": "^2.0.1", + "@react-three/handle": "workspace:~", + "@react-three/uikit": "^1.0.4", + "@react-three/xr": "workspace:~" + }, + "scripts": { + "dev": "vite --host", + "build": "vite build", + "check:eslint": "eslint \"*.{ts,tsx}\"", + "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" + } +} diff --git a/examples/customizing-models/styles.css b/examples/customizing-models/styles.css new file mode 100644 index 00000000..aa5258fc --- /dev/null +++ b/examples/customizing-models/styles.css @@ -0,0 +1,32 @@ +html { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; +} + +.App { + font-family: sans-serif; + text-align: center; + width: 100vw; + height: 100vh; +} + +button { + position: absolute; + background: black; + border-radius: 0.5rem; + border: none; + font-weight: bold; + color: white; + padding: 1rem 2rem; + cursor: pointer; + font-size: 1.5rem; + bottom: 1rem; + left: 50%; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 1); + transform: translate(-50%, 0); +} \ No newline at end of file diff --git a/examples/customizing-models/vite.config.ts b/examples/customizing-models/vite.config.ts new file mode 100644 index 00000000..0995d4c4 --- /dev/null +++ b/examples/customizing-models/vite.config.ts @@ -0,0 +1,12 @@ +import basicSsl from '@vitejs/plugin-basic-ssl' +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vitejs.dev/config/ +export default defineConfig({ + base: '/xr/examples/heads-up-display/', + plugins: [react(), basicSsl()], + resolve: { + dedupe: ['@react-three/fiber', 'three'], + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eeb3ade2..f8302c0e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,6 +106,24 @@ importers: specifier: ^5.2.11 version: 5.4.14(@types/node@22.13.0) + examples/customizing-models: + dependencies: + '@pmndrs/pointer-events': + specifier: workspace:~ + version: link:../../packages/pointer-events + '@preact/signals': + specifier: ^2.0.1 + version: 2.0.1(preact@10.25.4) + '@react-three/handle': + specifier: workspace:~ + version: link:../../packages/react/handle + '@react-three/uikit': + specifier: ^1.0.4 + version: 1.0.60(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(use-sync-external-store@1.4.0(react@19.0.0)) + '@react-three/xr': + specifier: workspace:~ + version: link:../../packages/react/xr + examples/demo-controller: dependencies: '@pmndrs/xr': @@ -850,12 +868,18 @@ packages: '@pmndrs/msdfonts@0.8.7': resolution: {integrity: sha512-O+2xzQQeIV249SfuSJVsAIun2/9SnA9V2P90NRDxzRYVzI8smXk6na1iA7E/RsCvfpWFhyX3g9zaKdiTotavoA==} + '@pmndrs/msdfonts@1.0.60': + resolution: {integrity: sha512-ZSYH/Y5XRgsnXCRpYkM6KmlM47m06CD1K1VjdBmoRsaXki7tqurP83QemPrV/VfQXfCqC49lQIYCabOCCL3HwQ==} + '@pmndrs/pointer-events@6.6.1': resolution: {integrity: sha512-O7CMUtoCyEqVT5X0syjsRKazX+7Q7//zBKj5mjIfIP+nlcwvBYCvdRTgHf6cGqB7jRgXLCd/3LSEgdoAXys/3Q==} '@pmndrs/uikit-lucide@0.4.4': resolution: {integrity: sha512-us9lzwtG7gByDmYfqZ9KXeSnbMRWTuHXBdLWsCtmuu1mYk91UZlqsBdvLo0GsC8SDHz3S7Y49Hlgtsw4OcRZ/Q==} + '@pmndrs/uikit-pub-sub@1.0.60': + resolution: {integrity: sha512-rciKs4iRsAYfGCuWS2b3uYGd2hoJXFso0rOcP20iHxu2Lk1XnYamPgtCmJZyhnkNiqJm+6Ms2MzGad+jcZ0yKw==} + '@pmndrs/uikit@0.4.4': resolution: {integrity: sha512-AT4B0zp4KP96lqh6oam44VO/H49cOZjEm09+0XiVXIgzA721heUHXgI4FhWGb1qlcI4T/2FHMBaw5sXh1o1npw==} peerDependencies: @@ -876,6 +900,11 @@ packages: peerDependencies: three: '>=0.160' + '@pmndrs/uikit@1.0.60': + resolution: {integrity: sha512-yKBxMIjT3JEHz6hcQxieqklCjBrAEq3oVvBcccCSvmkXmkBYJ+QLlBWDQTxcIQWxOQVBoiehgUB2G6mTNIuceQ==} + peerDependencies: + three: '>=0.162' + '@preact/signals-core@1.8.0': resolution: {integrity: sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA==} @@ -974,6 +1003,12 @@ packages: '@react-three/fiber': '>=8' react: '>=18' + '@react-three/uikit@1.0.60': + resolution: {integrity: sha512-SppRlp3JQ2kOCRN9gnJ+aEGQccKpU7SwLNzsRyT3m2FdvoUR+me8HINEB9W0fd5dp6O0jV7vzngnL1g0Cxs6oA==} + peerDependencies: + '@react-three/fiber': '>=8' + react: '>=18' + '@rollup/rollup-android-arm-eabi@4.34.0': resolution: {integrity: sha512-Eeao7ewDq79jVEsrtWIj5RNqB8p2knlm9fhR6uJ2gqP7UfbLrTrxevudVrEPDM7Wkpn/HpRC2QfazH7MXLz3vQ==} cpu: [arm] @@ -3365,6 +3400,24 @@ packages: use-sync-external-store: optional: true + zustand@5.0.9: + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -3726,6 +3779,8 @@ snapshots: '@pmndrs/msdfonts@0.8.7': {} + '@pmndrs/msdfonts@1.0.60': {} + '@pmndrs/pointer-events@6.6.1': {} '@pmndrs/uikit-lucide@0.4.4(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': @@ -3735,6 +3790,10 @@ snapshots: - three - ts-node + '@pmndrs/uikit-pub-sub@1.0.60': + dependencies: + '@preact/signals-core': 1.8.0 + '@pmndrs/uikit@0.4.4(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: '@preact/signals-core': 1.8.0 @@ -3781,6 +3840,14 @@ snapshots: transitivePeerDependencies: - ts-node + '@pmndrs/uikit@1.0.60(three@0.173.0)': + dependencies: + '@pmndrs/msdfonts': 1.0.60 + '@pmndrs/uikit-pub-sub': 1.0.60 + '@preact/signals-core': 1.8.0 + three: 0.173.0 + yoga-layout: 3.2.1 + '@preact/signals-core@1.8.0': {} '@preact/signals@2.0.1(preact@10.25.4)': @@ -3872,7 +3939,7 @@ snapshots: '@react-three/uikit-default@0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: - '@react-three/uikit': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + '@react-three/uikit': 0.8.21(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) '@react-three/uikit-lucide': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) tunnel-rat: 0.1.2(@types/react@19.0.8)(react@19.0.0) transitivePeerDependencies: @@ -3896,7 +3963,7 @@ snapshots: '@react-three/uikit-lucide@0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3))': dependencies: - '@react-three/uikit': 0.8.7(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) + '@react-three/uikit': 0.8.21(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(ts-node@10.9.2(@types/node@22.13.0)(typescript@5.7.3)) transitivePeerDependencies: - '@react-three/fiber' - '@types/react' @@ -3982,6 +4049,20 @@ snapshots: - three - ts-node + '@react-three/uikit@1.0.60(@react-three/fiber@9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0))(@types/react@19.0.8)(react@19.0.0)(three@0.173.0)(use-sync-external-store@1.4.0(react@19.0.0))': + dependencies: + '@pmndrs/uikit': 1.0.60(three@0.173.0) + '@preact/signals-core': 1.8.0 + '@react-three/fiber': 9.0.0-rc.5(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(three@0.173.0) + react: 19.0.0 + suspend-react: 0.1.3(react@19.0.0) + zustand: 5.0.9(@types/react@19.0.8)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)) + transitivePeerDependencies: + - '@types/react' + - immer + - three + - use-sync-external-store + '@rollup/rollup-android-arm-eabi@4.34.0': optional: true @@ -6620,3 +6701,9 @@ snapshots: '@types/react': 19.0.8 react: 19.0.0 use-sync-external-store: 1.4.0(react@19.0.0) + + zustand@5.0.9(@types/react@19.0.8)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)): + optionalDependencies: + '@types/react': 19.0.8 + react: 19.0.0 + use-sync-external-store: 1.4.0(react@19.0.0) From b9c880edf502160c4341aad15491ad6def8602fd Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Tue, 30 Dec 2025 20:12:24 +0100 Subject: [PATCH 2/7] docs: Started work on the upgraded tutorial --- docs/tutorials/custom-inputs.md | 112 ++++++++++++++++++++- examples/customizing-models/CustomHand.tsx | 26 +++++ 2 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 examples/customizing-models/CustomHand.tsx diff --git a/docs/tutorials/custom-inputs.md b/docs/tutorials/custom-inputs.md index 989be9fc..067c78cb 100644 --- a/docs/tutorials/custom-inputs.md +++ b/docs/tutorials/custom-inputs.md @@ -4,18 +4,124 @@ description: Customize interactions and style of inputs such as hands, controlle nav: 16 --- -@react-three/xr provides a set of default hands, controllers, transient pointers, gazes, and screen input that can be configured and completely exchanged with your own implementation. The following example shows how to configure the ray color of the ray pointer in the users hand. +@react-three/xr provides a set of default hands, controllers, transient pointers, gazes, and a screen input that can be configured and completely exchanged with your own implementations. The following tutorial will teach you how to swap out each piece with your own implementations. +### Setup +Like many of the other tutorials, we'll start with this basic scene. Create your new React project and add the following to your `App.tsx` file and your `styles.css` files respectively: + +**App.tsx:** +```tsx +import { Box } from '@react-three/drei' +import { Canvas } from '@react-three/fiber' +import { createXRStore, XR } from '@react-three/xr' +import * as THREE from 'three' +import './styles.css' + +const store = createXRStore() + +const axisColor = new THREE.Color('#9d3d4a') +const gridColor = new THREE.Color('#4f4f4f') + +export function App() { + return ( +
+ + + + + + + + + + +
+ ) +} +``` + +**styles.css:** +```css +html { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; +} + +.App { + font-family: sans-serif; + text-align: center; + width: 100vw; + height: 100vh; +} + +button { + position: absolute; + background: black; + border-radius: 0.5rem; + border: none; + font-weight: bold; + color: white; + padding: 1rem 2rem; + cursor: pointer; + font-size: 1.5rem; + bottom: 1rem; + left: 50%; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 1); + transform: translate(-50%, 0); +} +``` + +### Modifying Default Implementations +The purpose of this section is to demonstrate how to modify the default implementation of components already included with @react-three/xr. Often the default implementations are sufficient for most applications, but just need some minor tweaking to get them to work how you want them to. We're going to start nice and easy here by chaniging the color of the ray pointer on the default hand implementation. This can be done by passing in options to the `createXRStore` function like so: + +**App.tsx:** ```tsx const store = createXRStore({ hand: { rayPointer: { rayModel: { color: 'red' } } } }) ``` -In some cases, the default hand/controller/... implementations are not enough. The following code sample shows how to provide your own custom hand implementation though the xr store options. +Running your application should now show a red Ray Pointer instead of the default white one. Let's go a step further and modify the behavior of the default hand implementation by adding a touch interaction to the pinky finger. To do this, we need to create a new `CustomHand` component which we will pass into the `createXRStore` function. Create a new file called `CustomHand.tsx` and add the following code: +**CustomHand.tsx:** ```tsx -const store = createXRStore({ hand: CustomHand }) +import { defaultTouchPointerOpacity, PointerCursorModel, useTouchPointer, useXRInputSourceStateContext, XRHandModel, XRSpace } from '@react-three/xr' +import { Suspense, useRef } from 'react' +import { Object3D } from 'three' + +export function CustomHand() { + const state = useXRInputSourceStateContext('hand') + const middleFingerRef = useRef(null) + const pointer = useTouchPointer(middleFingerRef, state) + + return ( + <> + + + + + + + ) +} ``` +There's a lot to unpack here, so let's take a second to go through it. + +// TODO: Continue from here + + + + + + + + ## Custom Hand Let's build our own hand implementation which renders the normal hand model but only has a touch interaction which works using the middle finger. diff --git a/examples/customizing-models/CustomHand.tsx b/examples/customizing-models/CustomHand.tsx new file mode 100644 index 00000000..94d6854d --- /dev/null +++ b/examples/customizing-models/CustomHand.tsx @@ -0,0 +1,26 @@ +import { + defaultTouchPointerOpacity, + PointerCursorModel, + useTouchPointer, + useXRInputSourceStateContext, + XRHandModel, + XRSpace, +} from '@react-three/xr' +import { Suspense, useRef } from 'react' +import { Object3D } from 'three' + +export function CustomHand() { + const state = useXRInputSourceStateContext('hand') + const middleFingerRef = useRef(null) + const pointer = useTouchPointer(middleFingerRef, state) + + return ( + <> + + + + + + + ) +} From 6462cc28b930ef664d7ed9d9afc4af25745bdb8c Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Fri, 2 Jan 2026 21:43:01 +0100 Subject: [PATCH 3/7] docs: More work on the customizing tutorial. Mentioning meshes --- docs/tutorials/custom-inputs.md | 81 +++++++++++++++++-- examples/customizing-models/CustomHand.tsx | 6 +- .../customizing-models/CustomHandModel.tsx | 11 +++ examples/customizing-models/app.tsx | 15 +++- 4 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 examples/customizing-models/CustomHandModel.tsx diff --git a/docs/tutorials/custom-inputs.md b/docs/tutorials/custom-inputs.md index 067c78cb..775f4b21 100644 --- a/docs/tutorials/custom-inputs.md +++ b/docs/tutorials/custom-inputs.md @@ -23,6 +23,12 @@ const axisColor = new THREE.Color('#9d3d4a') const gridColor = new THREE.Color('#4f4f4f') export function App() { + const [boxColor, setBoxColor] = useState('orange') + + const onBoxClick = () => { + setBoxColor(boxColor === 'orange' ? 'green' : 'orange') + } + return (
@@ -32,11 +38,13 @@ export function App() { + + + + - +
) } @@ -86,7 +94,7 @@ The purpose of this section is to demonstrate how to modify the default implemen const store = createXRStore({ hand: { rayPointer: { rayModel: { color: 'red' } } } }) ``` -Running your application should now show a red Ray Pointer instead of the default white one. Let's go a step further and modify the behavior of the default hand implementation by adding a touch interaction to the pinky finger. To do this, we need to create a new `CustomHand` component which we will pass into the `createXRStore` function. Create a new file called `CustomHand.tsx` and add the following code: +Running your application should now show a red `rayPointer` instead of the default white one. Let's go a step further and modify the behavior of the default hand implementation by adding a touch interaction to the pinky finger. To do this, we need to create a new `CustomHand` component which we will pass into the `createXRStore` function. Create a new file called `CustomHand.tsx` and add the following code: **CustomHand.tsx:** ```tsx @@ -96,12 +104,12 @@ import { Object3D } from 'three' export function CustomHand() { const state = useXRInputSourceStateContext('hand') - const middleFingerRef = useRef(null) - const pointer = useTouchPointer(middleFingerRef, state) + const pinkyFingerRef = useRef(null) + const pointer = useTouchPointer(pinkyFingerRef, state) return ( <> - + @@ -112,8 +120,65 @@ export function CustomHand() { ``` There's a lot to unpack here, so let's take a second to go through it. + - `useXRInputSourceStateContext('hand')`: This is a hook that gives access to the state of the hand input source. + - `useTouchPointer()`: This hook allows us to create a touch pointer, and attach it to a specific reference space. + - `XRHandModel`: This component is the default hand model provided by @react-three/xr. By including it here, we get the default hand with our custom touch interaction. On top of it. + +With our `CustomHand` component created, we can now pass it into the `createXRStore` function like so: + +**App.tsx:** +```tsx +//Previous Code... +import { CustomHand } from './CustomHand' +//Previous Code... + +const store = createXRStore({ hand: CustomHand }) +//Previous Code... +``` + +With our custom hand component passed into the store, we can now use our pinky finger touch pointer to change the color of the box in our scene. + + +### Custom Models +So far we've seen how to tweak the default implementation, and how to add interactions onto the default models, but what if we want to add our own custom models for the hands? Adding a custom model is tricky, but not impossible. Technically any gltf should be fine, however, if you want it to animate, then it will need to have the necessary armature, and the bones will need to be named correctly. If you are interested in creating your own animated hand model, the easiest way to do so would be to start with the [default hand model gltf found here](https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0.20/dist/profiles/generic-hand/). This model has the correct armature and bone names to work with WebXR, so you can use it as a starting point, or as a template for your own model. When you have your model ready, you can use the starting code for the default hand as a base, and load in your model instead of the default one. The default implementation is found [here](https://github.com/pmndrs/xr/blob/main/packages/react/xr/src/hand.tsx). For this tutorial, we'll keep things simple and just use a sphere to represent our hand. Create a new file called `CustomHandModel.tsx` and add the following code: + +**CustomHandModel.tsx:** + +```tsx +import { Sphere } from '@react-three/drei' + +export function CustomHandModel() { + return ( + + + + + + ) +} +``` + +once we have our custom hand model, swap it out for our `CustomHand` in the `app.tsx`. + +**app.tsx** + +```tsx +//Previous Code ... +import { CustomHandModel } from './CustomHandModel' +//Previous Code ... + +const store = createXRStore({ hand: CustomHandModel }) +//Previous Code ... +``` + +With that done, we should now have blue spheres representing our hands in our scene. + + +### Customizing Controllers + + +// TODO: Continue here -// TODO: Continue from here diff --git a/examples/customizing-models/CustomHand.tsx b/examples/customizing-models/CustomHand.tsx index 94d6854d..bcfba9f5 100644 --- a/examples/customizing-models/CustomHand.tsx +++ b/examples/customizing-models/CustomHand.tsx @@ -11,12 +11,12 @@ import { Object3D } from 'three' export function CustomHand() { const state = useXRInputSourceStateContext('hand') - const middleFingerRef = useRef(null) - const pointer = useTouchPointer(middleFingerRef, state) + const pinkyFingerRef = useRef(null) + const pointer = useTouchPointer(pinkyFingerRef, state) return ( <> - + diff --git a/examples/customizing-models/CustomHandModel.tsx b/examples/customizing-models/CustomHandModel.tsx new file mode 100644 index 00000000..487c5e73 --- /dev/null +++ b/examples/customizing-models/CustomHandModel.tsx @@ -0,0 +1,11 @@ +import { Sphere } from '@react-three/drei' + +export function CustomHandModel() { + return ( + + + + + + ) +} diff --git a/examples/customizing-models/app.tsx b/examples/customizing-models/app.tsx index 517a93ca..76ce2f58 100644 --- a/examples/customizing-models/app.tsx +++ b/examples/customizing-models/app.tsx @@ -1,15 +1,23 @@ -import { OrbitControls, Plane } from '@react-three/drei' +import { Box, OrbitControls, Plane } from '@react-three/drei' import { Canvas } from '@react-three/fiber' import { createXRStore, XR } from '@react-three/xr' +import { useState } from 'react' import * as THREE from 'three' +import { CustomHandModel } from './CustomHandModel.js' import './styles.css' -const store = createXRStore() +const store = createXRStore({ hand: CustomHandModel }) const axisColor = new THREE.Color('#9d3d4a') const gridColor = new THREE.Color('#4f4f4f') export function App() { + const [boxColor, setBoxColor] = useState('orange') + + const onBoxClick = () => { + setBoxColor(boxColor === 'orange' ? 'green' : 'orange') + } + return (
@@ -19,6 +27,9 @@ export function App() { + + + From 2b71a27f02ff5a6f026d0175710e8d9f0dbf36d9 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Sat, 3 Jan 2026 15:18:28 +0100 Subject: [PATCH 4/7] docs: Finished the tutorial --- docs/tutorials/custom-inputs.md | 82 ++++--------------- .../customizing-models/CustomHandModel.tsx | 2 +- examples/customizing-models/app.tsx | 10 +-- examples/customizing-models/vite.config.ts | 2 +- 4 files changed, 22 insertions(+), 74 deletions(-) diff --git a/docs/tutorials/custom-inputs.md b/docs/tutorials/custom-inputs.md index 775f4b21..93b97d10 100644 --- a/docs/tutorials/custom-inputs.md +++ b/docs/tutorials/custom-inputs.md @@ -1,19 +1,20 @@ --- -title: Custom Hands/Controllers/... +title: Customizing Components description: Customize interactions and style of inputs such as hands, controllers, and more nav: 16 --- -@react-three/xr provides a set of default hands, controllers, transient pointers, gazes, and a screen input that can be configured and completely exchanged with your own implementations. The following tutorial will teach you how to swap out each piece with your own implementations. +@react-three/xr provides a set of default hands, controllers, transient pointers, gazes, and a screen input. All of these defaults can be configured or completely exchanged with your own implementations. The following tutorial will teach you the basics of customizing these components in react-three/xr. ### Setup Like many of the other tutorials, we'll start with this basic scene. Create your new React project and add the following to your `App.tsx` file and your `styles.css` files respectively: **App.tsx:** ```tsx -import { Box } from '@react-three/drei' +import { Box, OrbitControls, Plane } from '@react-three/drei' import { Canvas } from '@react-three/fiber' import { createXRStore, XR } from '@react-three/xr' +import { useState } from 'react' import * as THREE from 'three' import './styles.css' @@ -26,7 +27,7 @@ export function App() { const [boxColor, setBoxColor] = useState('orange') const onBoxClick = () => { - setBoxColor(boxColor === 'orange' ? 'green' : 'orange') + setBoxColor(boxColor === 'orange' ? 'purple' : 'orange') } return ( @@ -38,8 +39,8 @@ export function App() { - - + + @@ -87,11 +88,13 @@ button { ``` ### Modifying Default Implementations -The purpose of this section is to demonstrate how to modify the default implementation of components already included with @react-three/xr. Often the default implementations are sufficient for most applications, but just need some minor tweaking to get them to work how you want them to. We're going to start nice and easy here by chaniging the color of the ray pointer on the default hand implementation. This can be done by passing in options to the `createXRStore` function like so: +The purpose of this section is to demonstrate how to modify the default implementation of components already included with @react-three/xr. Often the default implementations are sufficient for most applications, but just need some minor tweaking to get them to work how you want them to. We're going to start nice and easy here by changing the color of the ray pointer for our hand. This can be done by passing in options to the `createXRStore` function. In your `App.tsx` file replace the existing `createXRStore` call with the following: **App.tsx:** ```tsx +//Previous Code ... const store = createXRStore({ hand: { rayPointer: { rayModel: { color: 'red' } } } }) +//Previous Code ... ``` Running your application should now show a red `rayPointer` instead of the default white one. Let's go a step further and modify the behavior of the default hand implementation by adding a touch interaction to the pinky finger. To do this, we need to create a new `CustomHand` component which we will pass into the `createXRStore` function. Create a new file called `CustomHand.tsx` and add the following code: @@ -122,7 +125,7 @@ export function CustomHand() { There's a lot to unpack here, so let's take a second to go through it. - `useXRInputSourceStateContext('hand')`: This is a hook that gives access to the state of the hand input source. - `useTouchPointer()`: This hook allows us to create a touch pointer, and attach it to a specific reference space. - - `XRHandModel`: This component is the default hand model provided by @react-three/xr. By including it here, we get the default hand with our custom touch interaction. On top of it. + - `XRHandModel`: This component is the default hand model provided by @react-three/xr. By including it here, we get the default hand with our custom touch interaction included with it. With our `CustomHand` component created, we can now pass it into the `createXRStore` function like so: @@ -151,7 +154,7 @@ export function CustomHandModel() { return ( - + ) @@ -160,7 +163,7 @@ export function CustomHandModel() { once we have our custom hand model, swap it out for our `CustomHand` in the `app.tsx`. -**app.tsx** +**App.tsx:** ```tsx //Previous Code ... @@ -174,61 +177,6 @@ const store = createXRStore({ hand: CustomHandModel }) With that done, we should now have blue spheres representing our hands in our scene. -### Customizing Controllers - - -// TODO: Continue here - - - - - - - - - -## Custom Hand - -Let's build our own hand implementation which renders the normal hand model but only has a touch interaction which works using the middle finger. - -First we're getting the state of the hand, creating a reference to the position of the middle finger, and creating a touch pointer. - -```tsx -const state = useXRInputSourceStateContext('hand') -const middleFingerRef = useRef(null) -const pointer = useTouchPointer(middleFingerRef, state) -``` - -Next, we use the `state` to place an `XRSpace` for setting up the `middleFingerRef` and add an `XRHandModel` and `PointerCursorModel` to render the hand and a cursor visualization. - -```tsx - - - - - -``` - -
- Full Code - -```tsx -export function CustomHand() { - const state = useXRInputSourceStateContext('hand') - const middleFingerRef = useRef(null) - const pointer = useTouchPointer(middleFingerRef, state) - return ( - <> - - - - - - - ) -} -``` - -
+### Where to go from here? -This tutorial also applies to building custom controllers, transient pointers, gaze, and screen input implementations. +There are many more ways to customize the functionality of `@react-three/xr`. Most of the concepts shown in this tutorial can be applied to building custom controllers, transient pointers, gaze, and screen input implementations as needed for your own applications. All you have to do is find the default implementation, then tweak or replace it as necessary. Good luck, and as always, happy coding! diff --git a/examples/customizing-models/CustomHandModel.tsx b/examples/customizing-models/CustomHandModel.tsx index 487c5e73..48efecac 100644 --- a/examples/customizing-models/CustomHandModel.tsx +++ b/examples/customizing-models/CustomHandModel.tsx @@ -4,7 +4,7 @@ export function CustomHandModel() { return ( - + ) diff --git a/examples/customizing-models/app.tsx b/examples/customizing-models/app.tsx index 76ce2f58..1b5eae2e 100644 --- a/examples/customizing-models/app.tsx +++ b/examples/customizing-models/app.tsx @@ -3,10 +3,10 @@ import { Canvas } from '@react-three/fiber' import { createXRStore, XR } from '@react-three/xr' import { useState } from 'react' import * as THREE from 'three' -import { CustomHandModel } from './CustomHandModel.js' +import { CustomHand } from './CustomHand.js' import './styles.css' -const store = createXRStore({ hand: CustomHandModel }) +const store = createXRStore({ hand: CustomHand }) const axisColor = new THREE.Color('#9d3d4a') const gridColor = new THREE.Color('#4f4f4f') @@ -15,7 +15,7 @@ export function App() { const [boxColor, setBoxColor] = useState('orange') const onBoxClick = () => { - setBoxColor(boxColor === 'orange' ? 'green' : 'orange') + setBoxColor(boxColor === 'orange' ? 'purple' : 'orange') } return ( @@ -27,8 +27,8 @@ export function App() { - - + + diff --git a/examples/customizing-models/vite.config.ts b/examples/customizing-models/vite.config.ts index 0995d4c4..0b5c3e0a 100644 --- a/examples/customizing-models/vite.config.ts +++ b/examples/customizing-models/vite.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vite' // https://vitejs.dev/config/ export default defineConfig({ - base: '/xr/examples/heads-up-display/', + base: '/xr/examples/customizing-models/', plugins: [react(), basicSsl()], resolve: { dedupe: ['@react-three/fiber', 'three'], From 9ad454c2d5528c5f181b955cb30f1d7da8d79863 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Sat, 3 Jan 2026 15:31:11 +0100 Subject: [PATCH 5/7] docs: Added some explanation text to the example --- examples/customizing-models/app.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/customizing-models/app.tsx b/examples/customizing-models/app.tsx index 1b5eae2e..dde57b29 100644 --- a/examples/customizing-models/app.tsx +++ b/examples/customizing-models/app.tsx @@ -1,5 +1,6 @@ import { Box, OrbitControls, Plane } from '@react-three/drei' import { Canvas } from '@react-three/fiber' +import { Container, Text } from '@react-three/uikit' import { createXRStore, XR } from '@react-three/xr' import { useState } from 'react' import * as THREE from 'three' @@ -30,6 +31,18 @@ export function App() { + + + {'Using hand tracking, tap the box with your pinky finger to change its color'} + + From daa8cc0b253be62d1d3d10fd928f8c9638b344b2 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Sat, 3 Jan 2026 15:34:10 +0100 Subject: [PATCH 6/7] docs: Added customization tutorial to the pipeline --- .github/workflows/static.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 2d62eff2..e3a9b391 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -132,6 +132,7 @@ jobs: mkdir -p public/examples/hit-testing mkdir -p public/examples/uikit mkdir -p public/examples/portal + mkdir -p public/examples/customizing-models cp -r ./examples/minecraft/dist/* ./public/examples/minecraft cp -r ./examples/pingpong/dist/* ./public/examples/pingpong cp -r ./examples/rag-doll/dist/* ./public/examples/rag-doll @@ -144,6 +145,7 @@ jobs: cp -r ./examples/hit-testing/dist/* ./public/examples/hit-testing cp -r ./examples/uikit/dist/* ./public/examples/uikit cp -r ./examples/portal/dist/* ./public/examples/portal + cp -r ./examples/customizing-models/dist/* ./public/examples/customizing-models - name: Upload Artifact uses: actions/upload-artifact@v4 From 049d84f65cb54dd83465ebbee9ba78e57f91ad18 Mon Sep 17 00:00:00 2001 From: taeuscherpferd Date: Sat, 3 Jan 2026 15:36:57 +0100 Subject: [PATCH 7/7] docs: Added Links --- docs/tutorials/custom-inputs.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorials/custom-inputs.md b/docs/tutorials/custom-inputs.md index 93b97d10..7620d2ed 100644 --- a/docs/tutorials/custom-inputs.md +++ b/docs/tutorials/custom-inputs.md @@ -180,3 +180,6 @@ With that done, we should now have blue spheres representing our hands in our sc ### Where to go from here? There are many more ways to customize the functionality of `@react-three/xr`. Most of the concepts shown in this tutorial can be applied to building custom controllers, transient pointers, gaze, and screen input implementations as needed for your own applications. All you have to do is find the default implementation, then tweak or replace it as necessary. Good luck, and as always, happy coding! + +- *The example project can be found here: [Customization Example](https://pmndrs.github.io/xr/examples/customizing-models/)* +- *Full source code for this tutorial can be found here: [Customization Example Source](https://github.com/pmndrs/xr/tree/main/examples/customizing-models)* \ No newline at end of file