├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── alpha.yml │ ├── packages.yml │ └── static.yml ├── .gitignore ├── .mocharc.json ├── .prettierrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── GitVersion.yml ├── LICENSE ├── README.md ├── docs ├── advanced │ └── performance.md ├── getting-started │ ├── basic-example.gif │ ├── convert-to-xr.md │ ├── development-setup.md │ ├── editor.gif │ ├── emulator.gif │ ├── examples.md │ ├── faq.md │ ├── hit-testing.gif │ ├── introduction.html │ ├── introduction.jpeg │ ├── introduction.md │ ├── introduction.png │ ├── layers.gif │ ├── logo.svg │ ├── minecraft-demo.gif │ ├── pingpong-demo.gif │ ├── portal.gif │ ├── ragdoll-demo.gif │ ├── room-demo.gif │ ├── secondary-input-sources.gif │ ├── showcases.md │ ├── showcases │ │ └── volu-dev.gif │ ├── stage-demo.gif │ ├── uikit.gif │ └── watch-demo.gif ├── handles │ ├── annotated-door.jpg │ ├── door-handle.gif │ ├── editor.gif │ ├── handle-component.md │ ├── introduction.md │ ├── pivot.gif │ ├── prebuild-handles.gif │ ├── prebuild-handles.md │ ├── react-three-handle-gif-1.gif │ ├── screen-handle-components.md │ └── transform.gif ├── migration │ ├── from-natuerlich.md │ └── from-react-three-xr-5.md └── tutorials │ ├── anchors.md │ ├── custom-inputs.md │ ├── dom-overlay.md │ ├── gamepad.md │ ├── guards.md │ ├── hit-test.md │ ├── interactions.md │ ├── layers.md │ ├── object-detection.md │ ├── origin.md │ ├── secondary-input-sources.md │ ├── store.md │ ├── teleport-example.gif │ └── teleport.md ├── examples ├── demo-controller │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ └── vite.config.ts ├── editor │ ├── .gitignore │ ├── README.md │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── camera.glb │ │ ├── end.mp3 │ │ ├── rotate.glb │ │ ├── start.mp3 │ │ └── sun.glb │ └── vite.config.ts ├── handheld-ar │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ └── vite.config.ts ├── handle-vanilla │ ├── .gitignore │ ├── index.html │ ├── index.ts │ ├── package.json │ └── vite.config.ts ├── handle │ ├── .gitignore │ ├── README.md │ ├── app.tsx │ ├── door.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── axe.gltf │ │ ├── door.glb │ │ ├── ship.glb │ │ └── spaceship.glb │ ├── ship.tsx │ ├── smoke.tsx │ ├── spaceship.tsx │ └── vite.config.ts ├── hit-testing │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ └── duck.gltf │ ├── src │ │ ├── app.tsx │ │ ├── duck.tsx │ │ ├── ducks.tsx │ │ ├── hit-test-handheld.tsx │ │ ├── hit-test-headset.tsx │ │ ├── hit-test.tsx │ │ └── reticle.tsx │ ├── style.css │ └── vite.config.ts ├── layers │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── test.jpg │ │ └── test.mp4 │ └── vite.config.ts ├── minecraft │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── axe.glb │ │ ├── dirt.jpg │ │ ├── favicon.ico │ │ ├── grass.jpg │ │ ├── index.html │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ └── robots.txt │ ├── src │ │ ├── App.tsx │ │ ├── Axe.tsx │ │ ├── Cube.tsx │ │ ├── Ground.tsx │ │ ├── Player.tsx │ │ └── VRPlayerControl.tsx │ └── vite.config.ts ├── miniature │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ └── model.glb │ └── vite.config.ts ├── pingpong │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── crossp.jpg │ │ ├── index.html │ │ ├── ping.mp3 │ │ └── pingpong.glb │ ├── src │ │ ├── App.jsx │ │ ├── Hand.jsx │ │ └── state.js │ └── vite.config.ts ├── pointer-events │ ├── .gitignore │ ├── index.html │ ├── index.ts │ ├── package.json │ └── vite.config.ts ├── portal │ ├── .gitignore │ ├── README.md │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── lever.tsx │ ├── package.json │ ├── public │ │ ├── img.jpg │ │ ├── lever-sound.mp3 │ │ ├── lever.glb │ │ ├── normal.jpg │ │ └── open-door-sound.mp3 │ └── vite.config.ts ├── rag-doll │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── cup.glb │ │ └── index.html │ ├── src │ │ ├── App.jsx │ │ ├── components │ │ │ ├── Furniture.jsx │ │ │ └── Guy.jsx │ │ └── helpers │ │ │ ├── Block.jsx │ │ │ ├── Drag.js │ │ │ └── createRagdoll.js │ └── vite.config.ts ├── react-three-xr │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── smoke.tsx │ └── vite.config.ts ├── rollercoaster │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ └── rollercoaster.glb │ └── vite.config.ts ├── room-with-shadows │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── index.html │ │ └── room-transformed.glb │ ├── src │ │ ├── App.jsx │ │ └── Room.jsx │ └── vite.config.ts ├── secondary-input-sources │ ├── .gitignore │ ├── app.tsx │ ├── hand.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── pistol.glb │ │ └── watch-v1.glb │ └── vite.config.ts ├── stage │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ ├── cobra-transformed.glb │ │ ├── datsun-transformed.glb │ │ └── index.html │ ├── src │ │ ├── App.jsx │ │ ├── Cobra.jsx │ │ └── Datsun.jsx │ └── vite.config.ts ├── uikit │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ │ └── picture.jpg │ └── vite.config.ts ├── use-gesture │ ├── .gitignore │ ├── app.tsx │ ├── index.html │ ├── index.tsx │ ├── package.json │ └── vite.config.ts ├── vanilla │ ├── .gitignore │ ├── index.html │ ├── index.ts │ ├── package.json │ ├── public │ │ └── test.jpg │ └── vite.config.ts └── watch │ ├── .gitignore │ ├── index.html │ ├── index.tsx │ ├── package.json │ ├── public │ ├── index.html │ └── watch-v1.glb │ ├── src │ ├── App.tsx │ └── Hand.tsx │ └── vite.config.ts ├── package.json ├── packages ├── handle │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── computations │ │ │ ├── index.ts │ │ │ ├── one-pointer.ts │ │ │ ├── translate-as.ts │ │ │ ├── two-pointer.ts │ │ │ └── utils.ts │ │ ├── handles │ │ │ ├── axis.ts │ │ │ ├── context.ts │ │ │ ├── drag.ts │ │ │ ├── index.ts │ │ │ ├── material.ts │ │ │ ├── pivot │ │ │ │ ├── index.ts │ │ │ │ ├── rotate.ts │ │ │ │ └── scale.ts │ │ │ ├── registered.ts │ │ │ ├── rotate │ │ │ │ ├── axis.ts │ │ │ │ ├── free.ts │ │ │ │ ├── index.ts │ │ │ │ └── screen.ts │ │ │ ├── scale │ │ │ │ ├── axis.ts │ │ │ │ ├── free.ts │ │ │ │ ├── index.ts │ │ │ │ ├── plane.ts │ │ │ │ └── uniform.ts │ │ │ ├── transform.ts │ │ │ ├── translate │ │ │ │ ├── axis.ts │ │ │ │ ├── delta.ts │ │ │ │ ├── free.ts │ │ │ │ ├── index.ts │ │ │ │ └── plane.ts │ │ │ └── utils.ts │ │ ├── index.ts │ │ ├── screen │ │ │ ├── camera.ts │ │ │ ├── index.ts │ │ │ ├── map.ts │ │ │ ├── orbit.ts │ │ │ ├── pan.ts │ │ │ ├── rotate.ts │ │ │ ├── store.ts │ │ │ ├── utils.ts │ │ │ └── zoom.ts │ │ ├── state.ts │ │ ├── store.ts │ │ └── utils.ts │ └── tsconfig.json ├── pointer-events │ ├── LICENSE │ ├── README.md │ ├── bench.json │ ├── bench │ │ └── pointer.bench.ts │ ├── build.tsconfig.json │ ├── package.json │ ├── src │ │ ├── combine.ts │ │ ├── event.ts │ │ ├── forward.ts │ │ ├── html-event.ts │ │ ├── index.ts │ │ ├── intersections │ │ │ ├── index.ts │ │ │ ├── intersector.ts │ │ │ ├── lines.ts │ │ │ ├── ray.ts │ │ │ ├── sphere.ts │ │ │ └── utils.ts │ │ ├── pointer.ts │ │ ├── pointer │ │ │ ├── grab.ts │ │ │ ├── index.ts │ │ │ ├── lines.ts │ │ │ ├── ray.ts │ │ │ └── touch.ts │ │ └── utils.ts │ ├── tests │ │ ├── browser │ │ │ ├── index.html │ │ │ └── index.ts │ │ ├── index.spec.ts │ │ └── utils.ts │ └── tsconfig.json ├── react │ ├── handle │ │ ├── LICENSE │ │ ├── README.md │ │ ├── handle.gif │ │ ├── package.json │ │ ├── src │ │ │ ├── component.tsx │ │ │ ├── handles │ │ │ │ ├── index.ts │ │ │ │ ├── pivot.tsx │ │ │ │ ├── screen.tsx │ │ │ │ ├── transform.tsx │ │ │ │ └── utils.ts │ │ │ ├── hook.ts │ │ │ └── index.ts │ │ └── tsconfig.json │ └── xr │ │ ├── LICENSE │ │ ├── package.json │ │ ├── plugins │ │ └── pretty-md.mjs │ │ ├── src │ │ ├── anchor.tsx │ │ ├── contexts.tsx │ │ ├── controller-locomotion.ts │ │ ├── controller.tsx │ │ ├── default.tsx │ │ ├── deprecated │ │ │ ├── button.tsx │ │ │ ├── hooks.tsx │ │ │ ├── index.ts │ │ │ ├── interactive.tsx │ │ │ └── ray-grab.tsx │ │ ├── dom-overlay.tsx │ │ ├── elements.tsx │ │ ├── events.tsx │ │ ├── guard │ │ │ ├── facing-camera.tsx │ │ │ ├── focus.tsx │ │ │ ├── index.tsx │ │ │ ├── session-mode.tsx │ │ │ └── session-supported.tsx │ │ ├── hand.tsx │ │ ├── hit-test.tsx │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── input.tsx │ │ ├── layer.tsx │ │ ├── mesh.tsx │ │ ├── origin.tsx │ │ ├── plane.tsx │ │ ├── pointer.tsx │ │ ├── space.tsx │ │ ├── teleport.tsx │ │ ├── utils.ts │ │ └── xr.tsx │ │ ├── tsconfig.json │ │ └── typedoc.json └── xr │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ ├── anchor.ts │ ├── controller-locomotion.ts │ ├── controller │ │ ├── gamepad.ts │ │ ├── index.ts │ │ ├── layout.ts │ │ ├── model.ts │ │ ├── state.ts │ │ ├── utils.ts │ │ └── visual.ts │ ├── default.ts │ ├── emulate.ts │ ├── hand │ │ ├── index.ts │ │ ├── model.ts │ │ ├── pose.ts │ │ ├── state.ts │ │ └── visual.ts │ ├── hit-test.ts │ ├── index.ts │ ├── init.ts │ ├── input.ts │ ├── internals.ts │ ├── layer.ts │ ├── mesh.ts │ ├── misc.ts │ ├── plane.ts │ ├── pointer │ │ ├── cursor.ts │ │ ├── default.ts │ │ ├── event.ts │ │ ├── index.ts │ │ └── ray.ts │ ├── space.ts │ ├── store.ts │ ├── teleport.ts │ ├── utils.ts │ ├── vanilla │ │ ├── controller.ts │ │ ├── default.ts │ │ ├── elements.ts │ │ ├── hand.ts │ │ ├── index.ts │ │ ├── layer.ts │ │ ├── mesh.ts │ │ ├── origin.ts │ │ ├── plane.ts │ │ ├── pointer.ts │ │ ├── space.ts │ │ ├── types.ts │ │ ├── utils.ts │ │ └── xr.ts │ └── visible.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es6": true 8 | }, 9 | "settings": { 10 | "react": { 11 | "version": "detect" 12 | } 13 | }, 14 | "extends": [ 15 | "prettier", 16 | "plugin:prettier/recommended", 17 | "plugin:react-hooks/recommended", 18 | "plugin:import/recommended", 19 | "plugin:@react-three/recommended" 20 | ], 21 | "plugins": [ "@typescript-eslint", "react", "react-hooks", "import", "unused-imports", "prettier", "@react-three" ], 22 | "rules": { 23 | "import/no-unresolved": "off", 24 | "import/named": "off", 25 | "import/namespace": "off", 26 | "import/no-named-as-default-member": "off", 27 | "import/extensions": [ "error", "ignorePackages" ], 28 | 29 | "import/order": ["error", { 30 | "groups": [ 31 | "builtin", 32 | "external", 33 | "internal", 34 | ["parent", "sibling", "index"], 35 | "object", 36 | "type" 37 | ], 38 | "newlines-between": "never", 39 | "alphabetize": { 40 | "order": "asc", 41 | "caseInsensitive": true 42 | } 43 | }], 44 | 45 | // "simple-import-sort/imports": "error", 46 | "unused-imports/no-unused-imports": "error" 47 | } 48 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | 3 | *.png binary 4 | *.jpg binary 5 | *.jpeg binary 6 | *.gif binary 7 | *.ico binary 8 | *.mov binary 9 | *.mp4 binary 10 | *.mp3 binary 11 | *.flv binary 12 | *.fla binary 13 | *.wav binary 14 | *.swf binary 15 | *.gz binary 16 | *.zip binary 17 | *.7z binary 18 | *.ttf binary 19 | *.eot binary 20 | *.woff binary 21 | *.pyc binary 22 | *.pdf binary 23 | *.glb binary 24 | *.gltf binary -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: bbohlender 2 | polar: bbohlender 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | package-lock.json 4 | .DS_Store 5 | .idea 6 | docs/API/* -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "node-option": ["experimental-specifier-resolution=node", "loader=ts-node/esm/transpile-only"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "printWidth": 120, 7 | "endOfLine": "auto" 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // enable ESLint only for this workspace 3 | "eslint.enable": true, 4 | 5 | // Run the equivalent of `eslint --fix` each time you save 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | }, 9 | 10 | // Disable the built‑in “Organize Imports” so it doesn’t fight ESLint 11 | "editor.formatOnSave": false, 12 | "typescript.preferences.organizeImports": false, 13 | } 14 | 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | This project uses [semantic commits](https://conventionalcommits.org) and [semver](https://semver.org). 4 | 5 | To get started, make sure you have [Node](https://nodejs.org) and [PNPM](https://pnpm.io/) installed. Install dependencies, generate files, and build the libraries with: 6 | 7 | ```bash 8 | pnpm i 9 | pnpm -r copy 10 | pnpm -r build 11 | ``` 12 | 13 | ### Development 14 | 15 | Locally run examples with: 16 | 17 | ```bash 18 | cd examples/react-three-xr 19 | pnpm dev 20 | ``` 21 | 22 | **Important** 23 | 24 | When making a change to the packages, the `vite` cache of the examples needs to be cleared (delete `node_modules/.vite` inside the running example). Rebuilding the libraries is not necessary unless types have changed. 25 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | commit-message-incrementing: Disabled 2 | branches: 3 | main: 4 | increment: None 5 | develop: 6 | increment: None -------------------------------------------------------------------------------- /docs/advanced/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Performance 3 | description: Important considerations for building performant immersive web applications with react-three/xr 4 | nav: 22 5 | --- 6 | 7 | All performance optimizations for non-immersive 3D web applications are also applicable for immersive XR web applications. Relevant guides on the topic of performance for 3D web applications are the [R3F performance guide](https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance), [R3F performance pitfalls](https://docs.pmnd.rs/react-three-fiber/advanced/pitfalls), and the [Threejs tips and tricks](https://discoverthreejs.com/tips-and-tricks/#performance). In general, it is good to check if your web application's performance is GPU- or CPU-bound to select the correct optimization techniques. 8 | 9 | When it comes to immersive XR web applications, there are a few other options to improve GPU performance. We also recommend taking a look at this [WebXR Performance Optimization guide](https://developer.oculus.com/documentation/web/webxr-perf/). 10 | 11 | ## Frame Buffer Scaling 12 | 13 | The frame buffer scaling factor allows to control the size of the frame buffer your web application draws to. Decreasing this number can improve performance but reduces the resolution. A frame buffer scaling factor of 1.0 sets the frame buffer resolution to the native display resolution. 14 | 15 | ## Foveation 16 | 17 | Foveation allows rendering with a lower resolution at the edges of the eye's viewport to improve GPU performance. If your application is optimized and the performance is still GPU bound, consider increasing the foveation to improve performance. -------------------------------------------------------------------------------- /docs/getting-started/basic-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/basic-example.gif -------------------------------------------------------------------------------- /docs/getting-started/editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/editor.gif -------------------------------------------------------------------------------- /docs/getting-started/emulator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/emulator.gif -------------------------------------------------------------------------------- /docs/getting-started/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | description: Examples made with @react-three/xr 4 | nav: 3 5 | --- 6 | 7 | 8 |
  • 9 | [![Screenshot from Room demo](./room-demo.gif)](https://pmndrs.github.io/xr/examples/room-with-shadows/) 10 |
  • 11 |
  • 12 | [![Screenshot from Stage demo](./stage-demo.gif)](https://pmndrs.github.io/xr/examples/stage/) 13 |
  • 14 |
  • 15 | [![Screenshot from Ragdoll demo](./ragdoll-demo.gif)](https://pmndrs.github.io/xr/examples/rag-doll/) 16 |
  • 17 |
  • 18 | [![Screenshot from Watch demo](./watch-demo.gif)](https://pmndrs.github.io/xr/examples/watch/) 19 |
  • 20 |
  • 21 | [![Screenshot from Minecraft demo](./minecraft-demo.gif)](https://pmndrs.github.io/xr/examples/minecraft/) 22 |
  • 23 |
  • 24 | [![Screenshot from Pingpong demo](./pingpong-demo.gif)](https://pmndrs.github.io/xr/examples/pingpong/) 25 |
  • 26 |
  • 27 | [![Screenshot from Layers demo](./layers.gif)](https://pmndrs.github.io/xr/examples/layers/) 28 |
  • 29 |
  • 30 | [![Screenshot from Secondary Input Sources demo](./secondary-input-sources.gif)](https://pmndrs.github.io/xr/examples/secondary-input-sources/) 31 |
  • 32 |
  • 33 | [![Screenshot from the React-three-handle Editor demo](./editor.gif)](https://pmndrs.github.io/xr/examples/editor/) 34 |
  • 35 |
  • 36 | [![Screenshot from the hit testing demo](./hit-testing.gif)](https://pmndrs.github.io/xr/examples/hit-testing/) 37 | by [Sung Powley](https://bsky.app/profile/sung-powley.bsky.social) 38 |
  • 39 |
  • 40 | [![Screenshot from the uikit + handle demo](./uikit.gif)](https://pmndrs.github.io/xr/examples/uikit/) 41 |
  • 42 |
  • 43 | [![Screenshot from the portal demo](./portal.gif)](https://pmndrs.github.io/xr/examples/portal/) 44 |
  • 45 |
    46 | -------------------------------------------------------------------------------- /docs/getting-started/hit-testing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/hit-testing.gif -------------------------------------------------------------------------------- /docs/getting-started/introduction.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/introduction.jpeg -------------------------------------------------------------------------------- /docs/getting-started/introduction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/introduction.png -------------------------------------------------------------------------------- /docs/getting-started/layers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/layers.gif -------------------------------------------------------------------------------- /docs/getting-started/minecraft-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/minecraft-demo.gif -------------------------------------------------------------------------------- /docs/getting-started/pingpong-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/pingpong-demo.gif -------------------------------------------------------------------------------- /docs/getting-started/portal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/portal.gif -------------------------------------------------------------------------------- /docs/getting-started/ragdoll-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/ragdoll-demo.gif -------------------------------------------------------------------------------- /docs/getting-started/room-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/room-demo.gif -------------------------------------------------------------------------------- /docs/getting-started/secondary-input-sources.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/secondary-input-sources.gif -------------------------------------------------------------------------------- /docs/getting-started/showcases.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Showcases 3 | description: Public products build with @react-three/xr 4 | nav: 4 5 | --- 6 | 7 | 8 | 9 |
  • 10 | ![volu.dev](./showcases/volu-dev.gif) 11 | [Spatial development hub](https://volu.dev) 12 |
  • 13 |
    14 | 15 | Are we missing your public product? Please message us via [Twitter](https://x.com/BelaBohlender) or [Discord](https://discord.gg/poimandres). -------------------------------------------------------------------------------- /docs/getting-started/showcases/volu-dev.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/showcases/volu-dev.gif -------------------------------------------------------------------------------- /docs/getting-started/stage-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/stage-demo.gif -------------------------------------------------------------------------------- /docs/getting-started/uikit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/uikit.gif -------------------------------------------------------------------------------- /docs/getting-started/watch-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/getting-started/watch-demo.gif -------------------------------------------------------------------------------- /docs/handles/annotated-door.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/handles/annotated-door.jpg -------------------------------------------------------------------------------- /docs/handles/door-handle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/handles/door-handle.gif -------------------------------------------------------------------------------- /docs/handles/editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/handles/editor.gif -------------------------------------------------------------------------------- /docs/handles/pivot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/handles/pivot.gif -------------------------------------------------------------------------------- /docs/handles/prebuild-handles.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/handles/prebuild-handles.gif -------------------------------------------------------------------------------- /docs/handles/react-three-handle-gif-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/handles/react-three-handle-gif-1.gif -------------------------------------------------------------------------------- /docs/handles/transform.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/handles/transform.gif -------------------------------------------------------------------------------- /docs/migration/from-natuerlich.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: from Natuerlich 3 | description: Migrate your application from natuerlich 4 | nav: 24 5 | --- 6 | 7 | @react-three/xr is inspired by natuerlich, and therefore, many things are similar, especially the way interactions are handled. However, a few things have been changed and renamed. 8 | 9 | - use `XROrigin` instead of `ImmersiveSessionOrigin` 10 | - use `...` instead of `XRCanvas` 11 | - configure settings such as `foveation` through `createXRStore` 12 | - use `store.enterXR` instead of `useEnterXR` 13 | - use `DragControls` **TBD** instead of `Grabbale` 14 | - don't add hands and controllers yourself, and configure them through the `createXRStore` options. Click [here](../tutorials/custom-inputs.md) for more info regarding controller/hand/... customization. 15 | - use teleport as described [here](../tutorials/teleport.md) 16 | -------------------------------------------------------------------------------- /docs/migration/from-react-three-xr-5.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: from @react-three/xr v5 3 | description: Migrate your application from @react-three/xr v5 4 | nav: 23 5 | --- 6 | 7 | The goal of @react-three/xr v6 is to align this library closer to the react-three ecosystem. We, therefore, focussed on supporting the react-three/fiber event handlers. Another focus of v6 is to reduce boilerplate and provide more defaults while also giving developers more access to the lower-level WebXR primitives. In combination, these changes allow developers to build XR experiences that interoperate with the whole react-three ecosystem using only a few lines of code. 8 | 9 | For everybody that is transitioning from v5 to v6, we have created a small compatibility layer that includes `XRButton`, `ARButton`, `VRButton`, `useInteraction`, `useXREvent`, `Interactive`, and `RayGrab`. However, we recommend transitioning away from the compatibility layer as the new recommended way of building with @react-three/xr is more aligned with the whole react-three ecosystem. 10 | 11 | For the `Controllers` and `Hands` components there are not correspondances in @react-three/xr v6 since input methods such as controllers, hands, but also transient-pointers are added by default. Users can configure the default implementation of those input methods as described [here](../tutorials/custom-inputs.md). The teleportation feature of @react-three/xr v5 has also slightly changed. The new API is explained [here](../tutorials/teleport.md). -------------------------------------------------------------------------------- /docs/tutorials/anchors.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Anchors 3 | description: How to create and manage anchors in your AR experience? 4 | nav: 17 5 | --- 6 | 7 | Anchors allow to anchor virtual objects into the physical world in AR experiences. `react-three/xr` offers a multitude of ways to create and manage anchors. A simple solution is `useXRAnchor`, which works similarly to `useState` as it returns the current anchor and a function to request a new anchor as a tuple. 8 | 9 | ```tsx 10 | const [anchor, requestAnchor] = useXRAnchor() 11 | ``` 12 | 13 | With the `requestAnchor` function, we can request an anchor relative to the `"world"`, a `"space"`, or a `"hitTestResult"` 14 | 15 | ```tsx 16 | requestAnchor({ relativeTo: "space", space: ... }) 17 | ``` 18 | 19 | Once the anchor is created, the `useXRAnchor` hook exposes it as `anchor`. We can now use this `anchor` to put content into it using the `` component. 20 | 21 | ```tsx 22 | ...your content 23 | ``` 24 | 25 | The following example shows a `Anchor` component that uses the `useXRAnchor` hook and the `XRSpace` component to anchor a Box to the position of the right hand or controller when the respective hand or controller is selected (pinch/trigger). 26 | 27 | ```tsx 28 | export function Anchor() { 29 | const [anchor, requestAnchor] = useXRAnchor() 30 | const controllerState = useXRInputSourceState('controller', 'right') 31 | const handState = useXRInputSourceState('hand', 'right') 32 | const inputSource = controllerState?.inputSource ?? handState?.inputSource 33 | useXRInputSourceEvent( 34 | inputSource, 35 | 'select', 36 | async () => { 37 | if (inputSource == null) { 38 | return 39 | } 40 | requestAnchor({ relativeTo: 'space', space: inputSource.targetRaySpace }) 41 | }, 42 | [requestAnchor, inputSource], 43 | ) 44 | if (anchor == null) { 45 | return null 46 | } 47 | return ( 48 | 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/tutorials/dom-overlay.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dom Overlay 3 | description: How to add HTML elements for hand-held AR experiences with Dom overlay? 4 | nav: 18 5 | --- 6 | 7 | For hand-held AR experiences, such as those using a Smartphone, WebXR offers the dom overlay capability, allowing developers to use HTML code overlayed over the experience. In case scene 3D overlays or overlays in non-handheld AR/VR experiences are needed, check out [pmndrs/uikit](https://github.com/pmndrs/uikit). 8 | 9 | We can add dom overlay content to an experience using the `XRDomOverlay` component, which allows to write html code inside it. This HTML code will be overlayed over the hand-held AR experience. 10 | 11 | ```tsx 12 | 15 |
    Hello World
    16 |
    17 | ``` 18 | 19 | The following shows the complete code for a simple AR experience with a `Hello World` button that can toggle its color when clicked on. 20 | 21 | ```tsx 22 | const store = createXRStore() 23 | 24 | export function App() { 25 | const [bool, setBool] = useState(false) 26 | return ( 27 | <> 28 | 29 | 30 | 31 | 32 | 35 |
    setBool((b) => !b)} 38 | > 39 | Hello World 40 |
    41 |
    42 |
    43 |
    44 | 45 | ) 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/tutorials/guards.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guards 3 | description: Render and show parts of your application conditionally using guards 4 | nav: 20 5 | --- 6 | 7 | Guards allow to conditionally display or include content. For instance, the `IfInSessionMode` guard allows only displaying a background when the session is not an AR session. The `IfInSessionMode` can receive either a list of `allow` session modes or a list of `deny` session modes. 8 | 9 | ```tsx 10 | import { Canvas } from '@react-three/fiber' 11 | import { IfInSessionMode, XR, createXRStore } from '@react-three/xr' 12 | 13 | const store = createXRStore() 14 | 15 | export function App() { 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/tutorials/hit-test.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hit Test 3 | description: How to add hit testing capabilities to your AR experiences? 4 | nav: 19 5 | --- 6 | 7 | Hit testing allows to check intersections with real-world geometry in AR experiences. `@react-three/xr` provides various hooks and components for setting up hit testing. 8 | The following example shows how to set up a hit test inside the right hand using the `XRHitTest` component, how to get the first hit test result using the `onResults` callback, and how to get the world position of that result into a vector. 9 | 10 | ```tsx 11 | const matrixHelper = new Matrix4() 12 | const hitTestPosition = new Vector3() 13 | 14 | const store = createXRStore({ 15 | hand: { 16 | right: () => { 17 | const state = useXRHandState() 18 | return ( 19 | <> 20 | 21 | { 24 | if (results.length === 0) { 25 | return 26 | } 27 | getWorldMatrix(matrixHelper, results[0]) 28 | hitTestPosition.setFromMatrixPosition(matrixHelper) 29 | }} 30 | /> 31 | 32 | ) 33 | }, 34 | }, 35 | }) 36 | ``` 37 | 38 | With the `hitTestPosition` containing the world position of the last hit test, we can use it to create a 3d object and sync it to the object's position on every frame. 39 | 40 | ```tsx 41 | function Point() { 42 | const ref = useRef(null) 43 | useFrame(() => ref.current?.position.copy(hitTestPosition)) 44 | return ( 45 | 46 | 47 | 48 | 49 | ) 50 | } 51 | ``` 52 | 53 | Alternatively, for devices that provide mesh detection, we can also add normal pointer events listeners to the XR Mesh to achieve the same behavior. Check out [this tutorial](./object-detection.md) for more information about mesh detection. 54 | -------------------------------------------------------------------------------- /docs/tutorials/layers.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Layers 3 | description: How to use display images, videos, and custom renders at high quality on quad, cylinder, and equirectangular shapes? 4 | nav: 15 5 | --- 6 | 7 | Layers allow to render videos, images, and complete scenes with higher performance and higher quality while preserving battery life and latency for quad, cylinder, and equirectangular shapes using the WebXR Layer API. Layers are perfect for use cases that display flat, high-quality content, such as videos, images, and user interfaces. The following example illustrates how to create a layer that renders a video. 8 | 9 | First, we create a layer at `0, 1.5, -0.5` with a scale of `0.5` that displays a video assigned to `src` and starts that video when clicked. 10 | 11 | ```tsx 12 | video.play()} scale={0.5} src={video} /> 13 | ``` 14 | 15 | The assigned video is an HTML video element that is loaded from `test.mp4`. 16 | 17 | ```tsx 18 | const video = useMemo(() => { 19 | const result = document.createElement('video') 20 | result.src = 'test.mp4' 21 | return result 22 | }, []) 23 | ``` 24 | 25 | Combined, the final app looks like this 26 | 27 | ```tsx 28 | export function App() { 29 | const video = useMemo(() => { 30 | const result = document.createElement('video') 31 | result.src = 'test.mp4' 32 | return result 33 | }, []) 34 | return ( 35 | 36 | 37 | video.play()} scale={0.5} src={video} /> 38 | 39 | 40 | ) 41 | } 42 | ``` 43 | 44 | Instead of images and videos, Layers can also be used to display dynamically rendered content. The following example illustrates how to render a red cube onto the layer. This scene will be re-rendered every frame, allowing for fully dynamic content. 45 | 46 | ```tsx 47 | 48 | 49 | 50 | 51 | 52 | 53 | ``` 54 | -------------------------------------------------------------------------------- /docs/tutorials/object-detection.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Object Detection 3 | description: Use detected objects such as meshes and planes for rendering, scene understanding, physics, and more 4 | nav: 10 5 | --- 6 | 7 | @react-three/xr allows to use the devices mesh and plane detection functionality to detect the meshes and planes in the environment to modify the rendering, allow physics interactions with the environment, and more. 8 | 9 | ## Detected Planes 10 | 11 | The detected planes are accessible through the `useXRPlanes` hook or directly from `useXR(xr => xr.detectPlanes)` and manually go through the returned array. To render the planes in the correct place, the planes' space must provided to the `XRSpace` component. The following example shows how to render the red planes for all detected walls. 12 | 13 | ```tsx 14 | function RedWalls() { 15 | const wallPlanes = useXRPlanes('wall') 16 | return ( 17 | <> 18 | {wallPlanes.map((plane) => ( 19 | 20 | 21 | 22 | 23 | 24 | ))} 25 | 26 | ) 27 | } 28 | ``` 29 | 30 | ## Detected Meshes 31 | 32 | Mesh detection provides access to the geometry of the environment. Similarly to xr planes, @react-three/fiber allows to retrieve detected meshes using `useXRMeshes` and offers the `XRMeshModel` to render the individual meshes. 33 | -------------------------------------------------------------------------------- /docs/tutorials/secondary-input-sources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Secondary Input Sources 3 | description: How to use primary and secondary input sources (multiple controllers and hands) simultaneously? 4 | nav: 14 5 | --- 6 | 7 | Most standalone XR headsets support hand and controller tracking. While typical XR experiences often support both input methods, they only use the primary inputs, which refers to one input per hand and limits the inputs to `2`. However, the headset often also tracks the secondary input sources. By enabling the `secondaryInputSources` flag when creating an xr store, we can access the secondary input sources and use them to track real-world objects, for example. 8 | 9 | ```ts 10 | createXRStore({ secondaryInputSources: true }) 11 | ``` 12 | 13 | Secondary input sources are exposed to the developer just like primary input sources, with the exception of the `isPrimary` flat, which is false. The following example illustrates how to show the primary input controllers using the default controller components while rendering the secondary input controllers as simple cubes. 14 | 15 | ```tsx 16 | createXRStore({ 17 | secondaryInputSources: true, 18 | controller: () => { 19 | const { isPrimary } = useXRInputSourceStateContext('controller') 20 | if (isPrimary) { 21 | return 22 | } 23 | return ( 24 | 25 | 26 | 27 | ) 28 | }, 29 | }) 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/tutorials/teleport-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/docs/tutorials/teleport-example.gif -------------------------------------------------------------------------------- /examples/demo-controller/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/demo-controller/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/demo-controller/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/demo-controller/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/xr": "workspace:~", 4 | "@react-three/uikit": "^0.5.3", 5 | "@react-three/xr": "workspace:~" 6 | }, 7 | "scripts": { 8 | "dev": "vite --host", 9 | "check:eslint": "eslint \"*.{ts,tsx}\"", 10 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/demo-controller/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['@react-three/fiber', 'three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/editor/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/editor/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/editor/README.md -------------------------------------------------------------------------------- /examples/editor/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/editor/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/handle": "workspace:~", 4 | "@pmndrs/pointer-events": "workspace:~", 5 | "@react-three/handle": "workspace:~", 6 | "@react-three/xr": "workspace:~", 7 | "meshline": "^3.3.1", 8 | "postprocessing": "^6.36.6", 9 | "zustand": "^4.5.2" 10 | }, 11 | "scripts": { 12 | "dev": "vite --host", 13 | "build": "vite build", 14 | "check:eslint": "eslint \"*.{ts,tsx}\"", 15 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/editor/public/camera.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/editor/public/camera.glb -------------------------------------------------------------------------------- /examples/editor/public/end.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/editor/public/end.mp3 -------------------------------------------------------------------------------- /examples/editor/public/rotate.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/editor/public/rotate.glb -------------------------------------------------------------------------------- /examples/editor/public/start.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/editor/public/start.mp3 -------------------------------------------------------------------------------- /examples/editor/public/sun.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/editor/public/sun.glb -------------------------------------------------------------------------------- /examples/editor/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/editor/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/handheld-ar/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/handheld-ar/app.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { createXRStore, XR, XRDomOverlay, XROrigin } from '@react-three/xr' 3 | import { useState } from 'react' 4 | import {} from '@react-three/drei' 5 | 6 | const store = createXRStore() 7 | 8 | export function App() { 9 | const [bool, setBool] = useState(false) 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 20 |
    setBool((b) => !b)} 23 | > 24 | Hello World 25 |
    26 |
    27 |
    28 |
    29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /examples/handheld-ar/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/handheld-ar/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/handheld-ar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/xr": "workspace:~" 4 | }, 5 | "scripts": { 6 | "dev": "vite --host", 7 | "check:eslint": "eslint \"*.{ts,tsx}\"", 8 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/handheld-ar/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['@react-three/fiber', 'three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/handle-vanilla/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/handle-vanilla/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 |
    12 | 13 |
    14 |
    15 | 16 | -------------------------------------------------------------------------------- /examples/handle-vanilla/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/pointer-events": "workspace:~", 4 | "@pmndrs/handle": "workspace:~", 5 | "@pmndrs/xr": "workspace:~" 6 | }, 7 | "scripts": { 8 | "dev": "vite --host", 9 | "check:eslint": "eslint \"*.{ts,tsx}\"", 10 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/handle-vanilla/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [basicSsl()], 7 | build: { 8 | outDir: './dist', 9 | }, 10 | base: '/handle-vanilla/', 11 | resolve: { 12 | dedupe: ['three'], 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /examples/handle/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/handle/README.md: -------------------------------------------------------------------------------- 1 | door model by [witnessk](https://sketchfab.com/witnessk) 2 | space ship by [Comrade1280](https://sketchfab.com/comrade1280) -------------------------------------------------------------------------------- /examples/handle/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/handle/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/handle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@dimforge/rapier3d-compat": "^0.14.0", 4 | "@pmndrs/handle": "workspace:~", 5 | "@pmndrs/pointer-events": "workspace:~", 6 | "@react-three/handle": "workspace:~", 7 | "@react-three/rapier": "^1.5.0", 8 | "@react-three/xr": "workspace:~" 9 | }, 10 | "scripts": { 11 | "dev": "vite --host", 12 | "check:eslint": "eslint \"*.{ts,tsx}\"", 13 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/handle/public/door.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/handle/public/door.glb -------------------------------------------------------------------------------- /examples/handle/public/ship.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/handle/public/ship.glb -------------------------------------------------------------------------------- /examples/handle/public/spaceship.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/handle/public/spaceship.glb -------------------------------------------------------------------------------- /examples/handle/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['@react-three/fiber', 'three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/hit-testing/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/hit-testing/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/hit-testing/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './src/app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/hit-testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hit-testing", 3 | "dependencies": { 4 | "@react-three/xr": "workspace:~" 5 | }, 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build", 9 | "check:eslint": "eslint \"src/**/*.{ts,tsx}\"", 10 | "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/hit-testing/src/duck.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * "Duck" 3 | * 4 | * https://market.pmnd.rs/model/duck 5 | * 6 | * created by: Kay Lousberg 7 | * license: CC0 8 | */ 9 | import { useGLTF } from '@react-three/drei' 10 | 11 | // const MODEL = 'https://vazxmixjsiawhamofees.supabase.co/storage/v1/object/public/models/duck/model.gltf' 12 | const MODEL = 'duck.gltf' 13 | 14 | useGLTF.preload(MODEL) 15 | 16 | export const Duck = (props: any) => { 17 | const { nodes, materials } = useGLTF(MODEL) as any 18 | 19 | return ( 20 | 21 | 26 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /examples/hit-testing/src/ducks.tsx: -------------------------------------------------------------------------------- 1 | import { useXRInputSourceEvent } from '@react-three/xr' 2 | import { useState } from 'react' 3 | import { Quaternion, Vector3 } from 'three' 4 | import { hitTestMatrices } from './app.js' 5 | import { Duck } from './duck.js' 6 | 7 | const vectorHelper = new Vector3() 8 | 9 | export const Ducks = () => { 10 | const [ducks, setDucks] = useState>([]) 11 | 12 | useXRInputSourceEvent( 13 | 'all', 14 | 'select', 15 | (e) => { 16 | const matrix = hitTestMatrices[e.inputSource.handedness] 17 | if (matrix) { 18 | const position = new Vector3() 19 | const quaternion = new Quaternion() 20 | 21 | matrix.decompose(position, quaternion, vectorHelper) 22 | setDucks((ducks) => [...ducks, { position, quaternion }]) 23 | } 24 | }, 25 | 26 | [], 27 | ) 28 | 29 | return ducks.map((item, index) => ( 30 | 31 | )) 32 | } 33 | -------------------------------------------------------------------------------- /examples/hit-testing/src/hit-test-handheld.tsx: -------------------------------------------------------------------------------- 1 | import { useXRHitTest } from '@react-three/xr' 2 | import { onResults } from './app.js' 3 | import { Reticle } from './reticle.js' 4 | 5 | const HitTestHandheld = () => { 6 | useXRHitTest(onResults.bind(null, 'none'), 'viewer') 7 | 8 | return 9 | } 10 | 11 | export { HitTestHandheld } 12 | -------------------------------------------------------------------------------- /examples/hit-testing/src/hit-test-headset.tsx: -------------------------------------------------------------------------------- 1 | import { Reticle } from './reticle.js' 2 | 3 | export const HitTestHeadset = () => { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /examples/hit-testing/src/hit-test.tsx: -------------------------------------------------------------------------------- 1 | import { useXR } from '@react-three/xr' 2 | import { HitTestHandheld } from './hit-test-handheld.js' 3 | import { HitTestHeadset } from './hit-test-headset.js' 4 | 5 | export const HitTest = () => { 6 | const isHandheld = useXR((xr) => xr.session?.interactionMode === 'screen-space') 7 | return isHandheld ? : 8 | } 9 | -------------------------------------------------------------------------------- /examples/hit-testing/src/reticle.tsx: -------------------------------------------------------------------------------- 1 | import { ThreeElements, useFrame } from '@react-three/fiber' 2 | import { forwardRef, memo, useRef } from 'react' 3 | import * as THREE from 'three' 4 | import { Mesh } from 'three' 5 | import { BufferGeometryUtils } from 'three/examples/jsm/Addons.js' 6 | import { hitTestMatrices } from './app.js' 7 | 8 | const ReticleMesh = forwardRef((props, ref) => { 9 | const geometry_merged = BufferGeometryUtils.mergeGeometries([ 10 | new THREE.RingGeometry(0.05, 0.06, 30), 11 | new THREE.CircleGeometry(0.007, 12), 12 | ]).rotateX(-Math.PI * 0.5) 13 | 14 | return ( 15 | 16 | 17 | 18 | ) 19 | }) 20 | 21 | export const Reticle = memo(({ handedness }: { handedness: XRHandedness }) => { 22 | const ref = useRef(undefined) 23 | 24 | useFrame(() => { 25 | if (ref.current == null) { 26 | return 27 | } 28 | const matrix = hitTestMatrices[handedness] 29 | if (matrix != null) { 30 | ref.current.visible = true 31 | ref.current.position.setFromMatrixPosition(matrix) 32 | ref.current.quaternion.setFromRotationMatrix(matrix) 33 | } else { 34 | ref.current.visible = false 35 | } 36 | }) 37 | 38 | return 39 | }) 40 | -------------------------------------------------------------------------------- /examples/hit-testing/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | touch-action: none; 3 | margin: 0; 4 | position: relative; 5 | width: 100dvw; 6 | height: 100dvh; 7 | overflow: hidden; 8 | } 9 | 10 | #root { 11 | position: absolute; 12 | inset: 0; 13 | display: flex; 14 | flex-direction: column; 15 | } 16 | 17 | #root canvas { 18 | width: 100%; 19 | flex-grow: 1; 20 | z-index: 0; 21 | } 22 | 23 | button { 24 | position: absolute; 25 | z-index: 100; 26 | background: black; 27 | border-radius: 0.5rem; 28 | border: none; 29 | font-weight: bold; 30 | color: white; 31 | padding: 1rem 2rem; 32 | cursor: pointer; 33 | font-size: 1.5rem; 34 | bottom: 1rem; 35 | left: 50%; 36 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 1); 37 | transform: translate(-50%, 0); 38 | } -------------------------------------------------------------------------------- /examples/hit-testing/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import basicSsl from '@vitejs/plugin-basic-ssl' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/hit-testing/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/layers/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/layers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/layers/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/layers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/pointer-events": "workspace:~", 4 | "@react-three/xr": "workspace:~" 5 | }, 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build", 9 | "check:eslint": "eslint \"*.{ts,tsx}\"", 10 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/layers/public/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/layers/public/test.jpg -------------------------------------------------------------------------------- /examples/layers/public/test.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/layers/public/test.mp4 -------------------------------------------------------------------------------- /examples/layers/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/layers/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/minecraft/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/minecraft/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/minecraft/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | //@ts-ignore 4 | import { App } from './src/App.js' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/minecraft/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/rapier": "^1.4.0", 4 | "@react-three/xr": "workspace:~", 5 | "zustand": "^4.5.4" 6 | }, 7 | "type": "module", 8 | "scripts": { 9 | "dev": "vite --host", 10 | "build": "vite build", 11 | "check:eslint": "eslint \"src/**/*.{ts,tsx}\"", 12 | "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/minecraft/public/axe.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/minecraft/public/axe.glb -------------------------------------------------------------------------------- /examples/minecraft/public/dirt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/minecraft/public/dirt.jpg -------------------------------------------------------------------------------- /examples/minecraft/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/minecraft/public/favicon.ico -------------------------------------------------------------------------------- /examples/minecraft/public/grass.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/minecraft/public/grass.jpg -------------------------------------------------------------------------------- /examples/minecraft/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/minecraft/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/minecraft/public/logo192.png -------------------------------------------------------------------------------- /examples/minecraft/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/minecraft/public/logo512.png -------------------------------------------------------------------------------- /examples/minecraft/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /examples/minecraft/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /examples/minecraft/src/Axe.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Auto-generated by: https://github.com/pmndrs/gltfjsx 3 | author: Blender3D (https://sketchfab.com/Blender3D) 4 | license: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/) 5 | source: https://sketchfab.com/models/0d62f4d3676545c88ec8523213c055dd 6 | title: Minecraft Diamond Axe 7 | */ 8 | 9 | import { useGLTF } from '@react-three/drei' 10 | import { ThreeElements } from '@react-three/fiber' 11 | import { Mesh } from 'three' 12 | 13 | export function Axe(props: ThreeElements['group']) { 14 | const { nodes, materials } = useGLTF('axe.glb') 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | 25 | useGLTF.preload('axe.glb') 26 | -------------------------------------------------------------------------------- /examples/minecraft/src/Ground.tsx: -------------------------------------------------------------------------------- 1 | import { useTexture } from '@react-three/drei' 2 | import { CuboidCollider, RigidBody, RigidBodyProps } from '@react-three/rapier' 3 | import * as THREE from 'three' 4 | 5 | export function Ground(props: RigidBodyProps) { 6 | const texture = useTexture('grass.jpg') 7 | texture.wrapS = texture.wrapT = THREE.RepeatWrapping 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /examples/minecraft/src/VRPlayerControl.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame } from '@react-three/fiber' 2 | import { Vector3Object } from '@react-three/rapier' 3 | import { useXRControllerLocomotion, useXRInputSourceState, XROrigin } from '@react-three/xr' 4 | import * as THREE from 'three' 5 | 6 | export function VRPlayerControl({ 7 | playerJump, 8 | playerMove, 9 | }: { 10 | playerJump?: () => void 11 | playerMove: (params: { 12 | forward: boolean 13 | backward: boolean 14 | left: boolean 15 | right: boolean 16 | rotationYVelocity: number 17 | velocity?: Vector3Object 18 | newVelocity?: THREE.Vector3 19 | }) => void 20 | }) { 21 | const controllerRight = useXRInputSourceState('controller', 'right') 22 | 23 | const physicsMove = (velocity: THREE.Vector3, rotationYVelocity: number) => { 24 | playerMove({ 25 | forward: false, 26 | backward: false, 27 | left: false, 28 | right: false, 29 | rotationYVelocity, 30 | velocity: undefined, 31 | newVelocity: velocity, 32 | }) 33 | } 34 | 35 | useXRControllerLocomotion(physicsMove, { speed: 5 }) 36 | 37 | useFrame(() => { 38 | if (controllerRight?.gamepad?.['a-button']?.state === 'pressed') { 39 | playerJump?.() 40 | } 41 | }) 42 | 43 | return 44 | } 45 | -------------------------------------------------------------------------------- /examples/minecraft/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import basicSsl from '@vitejs/plugin-basic-ssl' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/minecraft/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/miniature/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/miniature/app.tsx: -------------------------------------------------------------------------------- 1 | import { Gltf, OrbitControls } from '@react-three/drei' 2 | import { Canvas } from '@react-three/fiber' 3 | import { createXRStore, XR, XROrigin } from '@react-three/xr' 4 | import { useEffect, useState } from 'react' 5 | import { useStore } from 'zustand' 6 | 7 | const store = createXRStore({ offerSession: 'immersive-vr' }) 8 | 9 | export function App() { 10 | const [miniature, setMinitature] = useState(false) 11 | const session = useStore(store, (xr) => xr.session) 12 | useEffect(() => { 13 | if (session == null) { 14 | return 15 | } 16 | const listener = () => setMinitature((miniature) => !miniature) 17 | session.addEventListener('select', listener) 18 | return () => session.removeEventListener('select', listener) 19 | }, [session]) 20 | return ( 21 | <> 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /examples/miniature/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/miniature/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/miniature/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/xr": "workspace:~", 4 | "zustand": "^4.5.2" 5 | }, 6 | "scripts": { 7 | "dev": "vite --host", 8 | "check:eslint": "eslint \"*.{ts,tsx}\"", 9 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/miniature/public/model.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/miniature/public/model.glb -------------------------------------------------------------------------------- /examples/miniature/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['@react-three/fiber', 'three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/pingpong/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/pingpong/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/pingpong/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | //@ts-ignore 4 | import { App } from './src/App.jsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/pingpong/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/rapier": "^1.4.0", 4 | "@react-three/xr": "workspace:~", 5 | "valtio": "^1.13.2" 6 | }, 7 | "type": "module", 8 | "scripts": { 9 | "dev": "vite --host", 10 | "build": "vite build", 11 | "check:eslint": "eslint \"*.{ts,tsx}\"", 12 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 13 | 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/pingpong/public/crossp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/pingpong/public/crossp.jpg -------------------------------------------------------------------------------- /examples/pingpong/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 28 |
    29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/pingpong/public/ping.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/pingpong/public/ping.mp3 -------------------------------------------------------------------------------- /examples/pingpong/public/pingpong.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/pingpong/public/pingpong.glb -------------------------------------------------------------------------------- /examples/pingpong/src/state.js: -------------------------------------------------------------------------------- 1 | import { proxy } from 'valtio' 2 | 3 | const ping = new Audio('ping.mp3') 4 | 5 | export const state = proxy({ 6 | count: 0, 7 | api: { 8 | pong(velocity) { 9 | console.log(velocity) 10 | ping.currentTime = 0 11 | ping.volume = Math.min(Math.max(0, velocity / 20, 0), 1) 12 | ping.play() 13 | if (velocity > 10) ++state.count 14 | }, 15 | reset: () => (state.count = 0), 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /examples/pingpong/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/pingpong/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/pointer-events/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/pointer-events/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 |
    12 | 13 |
    14 |
    15 | 16 | -------------------------------------------------------------------------------- /examples/pointer-events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/pointer-events": "workspace:~" 4 | }, 5 | "scripts": { 6 | "dev": "vite", 7 | "check:eslint": "eslint \"*.{ts,tsx}\"", 8 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/pointer-events/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | build: { 6 | outDir: './dist', 7 | }, 8 | resolve: { 9 | dedupe: ['three'], 10 | }, 11 | base: '/pointer-events/', 12 | }) 13 | -------------------------------------------------------------------------------- /examples/portal/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/portal/README.md: -------------------------------------------------------------------------------- 1 | ## Attributions 2 | 3 | Music by Rockot from Pixabay 4 | 5 | Lever 3D Model by [Amine.Elouneg](https://sketchfab.com/Amine.Elouneg) from Sketchfab 6 | 7 | Lever Sound Effect by freesound_community from Pixabay 8 | 9 | Sound Effect by freesound_community from Pixabay -------------------------------------------------------------------------------- /examples/portal/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/portal/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/portal/lever.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Auto-generated by: https://github.com/pmndrs/gltfjsx 3 | */ 4 | 5 | import { useGLTF } from '@react-three/drei' 6 | import { defaultApply, Handle } from '@react-three/handle' 7 | import React, { MutableRefObject } from 'react' 8 | import { JSX } from 'react/jsx-runtime' 9 | import * as THREE from 'three' 10 | import { GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js' 11 | import { degToRad } from 'three/src/math/MathUtils.js' 12 | 13 | type GLTFResult = GLTF & { 14 | nodes: { 15 | body: THREE.Mesh 16 | lever: THREE.Mesh 17 | } 18 | materials: { 19 | lver: THREE.MeshStandardMaterial 20 | } 21 | } 22 | 23 | export function Lever({ openRef, ...props }: JSX.IntrinsicElements['group'] & { openRef: MutableRefObject }) { 24 | const { nodes, materials } = useGLTF('lever.glb') as any as GLTFResult 25 | return ( 26 | 27 | 33 | 34 | { 37 | if (state.current.rotation.z > degToRad(-20)) { 38 | openRef.current = false 39 | } 40 | if (state.current.rotation.z < degToRad(-115)) { 41 | openRef.current = true 42 | } 43 | defaultApply(state, target) 44 | }} 45 | scale={false} 46 | multitouch={false} 47 | rotate={{ x: false, y: false, z: [degToRad(-135), 0] }} 48 | > 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | 56 | useGLTF.preload('lever.glb') 57 | -------------------------------------------------------------------------------- /examples/portal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/xr": "workspace:~", 4 | "@react-three/handle": "workspace:~" 5 | }, 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build", 9 | "check:eslint": "eslint \"*.{ts,tsx}\"", 10 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/portal/public/img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/portal/public/img.jpg -------------------------------------------------------------------------------- /examples/portal/public/lever-sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/portal/public/lever-sound.mp3 -------------------------------------------------------------------------------- /examples/portal/public/lever.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/portal/public/lever.glb -------------------------------------------------------------------------------- /examples/portal/public/normal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/portal/public/normal.jpg -------------------------------------------------------------------------------- /examples/portal/public/open-door-sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/portal/public/open-door-sound.mp3 -------------------------------------------------------------------------------- /examples/portal/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/portal/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/rag-doll/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/rag-doll/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/rag-doll/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | //@ts-ignore 4 | import { App } from './src/App.jsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/rag-doll/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/cannon": "^6.6.0", 4 | "@react-three/xr": "workspace:~", 5 | "@pmndrs/pointer-events": "workspace:~" 6 | }, 7 | "type": "module", 8 | "scripts": { 9 | "dev": "vite --host", 10 | "build": "vite build", 11 | "check:eslint": "eslint \"*.{ts,tsx}\"", 12 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/rag-doll/public/cup.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/rag-doll/public/cup.glb -------------------------------------------------------------------------------- /examples/rag-doll/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
    31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/rag-doll/src/helpers/Block.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef } from 'react' 2 | import { RoundedBox } from '@react-three/drei' 3 | 4 | export const Block = forwardRef( 5 | ({ children, transparent = false, opacity = 1, color = 'white', args = [1, 1, 1], ...props }, ref) => { 6 | return ( 7 | 8 | 9 | {children} 10 | 11 | ) 12 | }, 13 | ) 14 | -------------------------------------------------------------------------------- /examples/rag-doll/src/helpers/Drag.js: -------------------------------------------------------------------------------- 1 | import { createRef, useCallback, useEffect } from 'react' 2 | import { useFrame } from '@react-three/fiber' 3 | import { usePointToPointConstraint, useSphere } from '@react-three/cannon' 4 | import { useRef } from 'react' 5 | import { Vector3 } from 'three' 6 | 7 | export const cursor = createRef() 8 | 9 | let grabbingPointerId = undefined 10 | const grabbedPosition = new Vector3() 11 | 12 | export function useDragConstraint(child) { 13 | const [, , api] = usePointToPointConstraint(cursor, child, { pivotA: [0, 0, 0], pivotB: [0, 0, 0] }) 14 | useEffect(() => void api.disable(), []) 15 | const onPointerUp = useCallback((e) => { 16 | if (grabbingPointerId == null) { 17 | return 18 | } 19 | grabbingPointerId = undefined 20 | document.body.style.cursor = 'grab' 21 | e.target.releasePointerCapture(e.pointerId) 22 | api.disable() 23 | }, []) 24 | const onPointerDown = useCallback((e) => { 25 | if (grabbingPointerId != null) { 26 | return 27 | } 28 | grabbingPointerId = e.pointerId 29 | grabbedPosition.copy(e.point) 30 | document.body.style.cursor = 'grabbing' 31 | e.stopPropagation() 32 | e.target.setPointerCapture(e.pointerId) 33 | api.enable() 34 | }, []) 35 | const onPointerMove = useCallback((e) => { 36 | if (grabbingPointerId != e.pointerId) { 37 | return 38 | } 39 | grabbedPosition.copy(e.point) 40 | }) 41 | return { onPointerUp, onPointerMove, onPointerDown } 42 | } 43 | 44 | export function Cursor() { 45 | const [, api] = useSphere(() => ({ collisionFilterMask: 0, type: 'Kinematic', mass: 0, args: [0.5] }), cursor) 46 | useFrame(() => { 47 | if (grabbingPointerId == null) { 48 | return 49 | } 50 | api.position.set(grabbedPosition.x, grabbedPosition.y, grabbedPosition.z) 51 | }) 52 | return null 53 | } 54 | -------------------------------------------------------------------------------- /examples/rag-doll/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/rag-doll/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/react-three-xr/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/react-three-xr/app.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { useHover, createXRStore, XR, XROrigin, TeleportTarget } from '@react-three/xr' 3 | import { useRef, useState } from 'react' 4 | import { Mesh, Vector3 } from 'three' 5 | import { Smoke } from './smoke.js' 6 | 7 | const store = createXRStore({ 8 | hand: { teleportPointer: true }, 9 | controller: { teleportPointer: true }, 10 | }) 11 | 12 | export function App() { 13 | const [position, setPosition] = useState(new Vector3()) 14 | return ( 15 | <> 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ) 34 | } 35 | 36 | function Cube() { 37 | const ref = useRef(null) 38 | const hover = useHover(ref) 39 | const [toggle, setToggle] = useState(false) 40 | return ( 41 | setToggle((x) => !x)} 43 | position={[0, 1, -1]} 44 | scale={0.1} 45 | pointerEventsType={{ deny: 'grab' }} 46 | ref={ref} 47 | > 48 | 49 | 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /examples/react-three-xr/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/react-three-xr/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/react-three-xr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/xr": "workspace:~" 4 | }, 5 | "scripts": { 6 | "dev": "vite --host", 7 | "check:eslint": "eslint \"*.{ts,tsx}\"", 8 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/react-three-xr/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['@react-three/fiber', 'three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/rollercoaster/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/rollercoaster/app.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls, useGLTF } from '@react-three/drei' 2 | import { Canvas, createPortal, useFrame } from '@react-three/fiber' 3 | import { createXRStore, XR, XROrigin } from '@react-three/xr' 4 | import { useEffect, useMemo } from 'react' 5 | import { AnimationMixer } from 'three' 6 | 7 | const store = createXRStore({ 8 | controller: false, 9 | offerSession: 'immersive-vr', 10 | }) 11 | 12 | export function App() { 13 | return ( 14 | <> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | function RollerCoaster() { 29 | const gltf = useGLTF('rollercoaster.glb') 30 | 31 | const mixer = useMemo(() => new AnimationMixer(gltf.scene), []) 32 | useEffect(() => { 33 | for (const animation of gltf.animations) { 34 | console.log(animation) 35 | mixer.clipAction(animation).play() 36 | } 37 | }, [gltf, mixer]) 38 | useFrame((state, delta) => mixer.update(delta)) 39 | return ( 40 | <> 41 | 42 | {createPortal( 43 | 44 | 45 | , 46 | gltf.scene.getObjectByName('Sessel')!, 47 | )} 48 | 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /examples/rollercoaster/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/rollercoaster/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/rollercoaster/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/xr": "workspace:~" 4 | }, 5 | "scripts": { 6 | "dev": "vite --host", 7 | "check:eslint": "eslint \"*.{ts,tsx}\"", 8 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/rollercoaster/public/rollercoaster.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/rollercoaster/public/rollercoaster.glb -------------------------------------------------------------------------------- /examples/rollercoaster/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['@react-three/fiber', 'three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/room-with-shadows/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/room-with-shadows/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/room-with-shadows/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | //@ts-ignore 4 | import App from './src/App.jsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/room-with-shadows/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/xr": "workspace:~", 4 | "maath": "^0.10.8" 5 | }, 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite --host", 9 | "build": "vite build", 10 | "check:eslint": "eslint \"*.{ts,tsx}\"", 11 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/room-with-shadows/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 | 29 |
    30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/room-with-shadows/public/room-transformed.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/room-with-shadows/public/room-transformed.glb -------------------------------------------------------------------------------- /examples/room-with-shadows/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/room-with-shadows/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/secondary-input-sources/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/secondary-input-sources/app.tsx: -------------------------------------------------------------------------------- 1 | import { Environment, Gltf } from '@react-three/drei' 2 | import { Canvas } from '@react-three/fiber' 3 | import { createXRStore, XR, useXR, XRControllerModel, XRSpace } from '@react-three/xr' 4 | import { Suspense } from 'react' 5 | import { HandWithWatch } from './hand.js' 6 | 7 | const store = createXRStore({ 8 | secondaryInputSources: true, 9 | foveation: 0, 10 | hand: { right: HandWithWatch, left: false }, 11 | controller: () => { 12 | // eslint-disable-next-line react-hooks/rules-of-hooks 13 | const hasHands = useXR((xr) => xr.inputSourceStates.find((state) => state.type === 'hand') != null) 14 | return ( 15 | <> 16 | 17 | 18 | 25 | 26 | 27 | {!hasHands && } 28 | 29 | ) 30 | }, 31 | }) 32 | 33 | export function App() { 34 | return ( 35 | <> 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /examples/secondary-input-sources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/secondary-input-sources/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/secondary-input-sources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/uikit": "^0.4.1", 4 | "@react-three/uikit-lucide": "^0.4.1", 5 | "@react-three/xr": "workspace:~" 6 | }, 7 | "scripts": { 8 | "dev": "vite --host", 9 | "build": "vite build", 10 | "check:eslint": "eslint \"*.{ts,tsx}\"", 11 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/secondary-input-sources/public/pistol.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/secondary-input-sources/public/pistol.glb -------------------------------------------------------------------------------- /examples/secondary-input-sources/public/watch-v1.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/secondary-input-sources/public/watch-v1.glb -------------------------------------------------------------------------------- /examples/secondary-input-sources/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/secondary-input-sources/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/stage/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/stage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/stage/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | //@ts-ignore 4 | import App from './src/App.jsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/stage/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@react-three/xr": "workspace:~" 4 | }, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --host", 8 | "build": "vite build", 9 | "check:eslint": "eslint \"*.{ts,tsx}\"", 10 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/stage/public/cobra-transformed.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/stage/public/cobra-transformed.glb -------------------------------------------------------------------------------- /examples/stage/public/datsun-transformed.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/stage/public/datsun-transformed.glb -------------------------------------------------------------------------------- /examples/stage/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
    31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/stage/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { Center, AccumulativeShadows, RandomizedLight, Environment, OrbitControls } from '@react-three/drei' 3 | import { Model } from './Datsun.jsx' 4 | import { XROrigin, XR, createXRStore } from '@react-three/xr' 5 | import { Suspense } from 'react' 6 | 7 | const store = createXRStore({ depthSensing: true, hand: false }) 8 | 9 | export default function App() { 10 | return ( 11 | <> 12 | 33 | 34 | 35 | 36 | 37 |
    38 | 39 |
    40 |
    41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
    51 | 52 | 53 | 54 | 55 |
    56 |
    57 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /examples/stage/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/stage/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/uikit/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/uikit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/uikit/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/uikit/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/pointer-events": "workspace:~", 4 | "@preact/signals": "^2.0.1", 5 | "@react-three/handle": "workspace:~", 6 | "@react-three/uikit": "^0.8.3", 7 | "@react-three/uikit-default": "^0.8.3", 8 | "@react-three/uikit-lucide": "^0.8.3", 9 | "@react-three/xr": "workspace:~" 10 | }, 11 | "scripts": { 12 | "dev": "vite --host", 13 | "build": "vite build", 14 | "check:eslint": "eslint \"*.{ts,tsx}\"", 15 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/uikit/public/picture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/uikit/public/picture.jpg -------------------------------------------------------------------------------- /examples/uikit/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | base: '/xr/examples/uikit/', 8 | plugins: [react(), basicSsl()], 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /examples/use-gesture/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/use-gesture/app.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from '@react-three/fiber' 2 | import { noEvents, PointerEvents } from '@react-three/xr' 3 | import { useDrag } from '@use-gesture/react' 4 | import { useRef } from 'react' 5 | 6 | export function App() { 7 | return ( 8 | 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | function DragCube() { 16 | const ref = useRef(null) 17 | const bind = useDrag(({ movement, xy, delta }) => console.log(...xy, 'movement', ...movement, 'delta', ...delta)) 18 | return ( 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /examples/use-gesture/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/use-gesture/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import { App } from './app.js' 4 | 5 | createRoot(document.getElementById('root')!).render( 6 | 7 | 8 | , 9 | ) 10 | -------------------------------------------------------------------------------- /examples/use-gesture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/pointer-events": "workspace:~", 4 | "@react-three/xr": "workspace:~", 5 | "@use-gesture/react": "^10.3.1" 6 | }, 7 | "scripts": { 8 | "dev": "vite --host", 9 | "check:eslint": "eslint \"*.{ts,tsx}\"", 10 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/use-gesture/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['@react-three/fiber', 'three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/vanilla/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/vanilla/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 |
    13 | 14 | 15 | Link 16 |
    17 | 18 | -------------------------------------------------------------------------------- /examples/vanilla/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@pmndrs/uikit": "^0.4.4", 4 | "@pmndrs/uikit-lucide": "^0.4.4", 5 | "@pmndrs/xr": "workspace:~", 6 | "@pmndrs/pointer-events": "workspace:~" 7 | }, 8 | "scripts": { 9 | "dev": "vite --host", 10 | "build": "vite build", 11 | "check:eslint": "eslint \"*.{ts,tsx}\"", 12 | "fix:eslint": "eslint \"*.{ts,tsx}\" --fix" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/vanilla/public/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/vanilla/public/test.jpg -------------------------------------------------------------------------------- /examples/vanilla/vite.config.ts: -------------------------------------------------------------------------------- 1 | import basicSsl from '@vitejs/plugin-basic-ssl' 2 | import react from '@vitejs/plugin-react' 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | resolve: { 9 | dedupe: ['three'], 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /examples/watch/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /examples/watch/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /examples/watch/index.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | //@ts-ignore 4 | import App from './src/App.jsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /examples/watch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@preact/signals-core": "^1.6.1", 4 | "@react-three/uikit": "^0.4.1", 5 | "@react-three/uikit-lucide": "^0.4.1", 6 | "@react-three/xr": "workspace:~" 7 | }, 8 | "type": "module", 9 | "scripts": { 10 | "dev": "vite --host", 11 | "build": "vite build", 12 | "check:eslint": "eslint \"src/**/*.{ts,tsx}\"", 13 | "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/watch/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
    31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/watch/public/watch-v1.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/examples/watch/public/watch-v1.glb -------------------------------------------------------------------------------- /examples/watch/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { OrbitControls } from '@react-three/drei' 2 | import { Canvas } from '@react-three/fiber' 3 | import { IfInSessionMode, XR, createXRStore } from '@react-three/xr' 4 | import React from 'react' 5 | import { HandWithWatch, Watch } from './Hand.jsx' 6 | 7 | const store = createXRStore({ 8 | hand: { 9 | right: HandWithWatch, 10 | left: { model: { colorWrite: false, renderOrder: -1 }, grabPointer: false, rayPointer: false }, 11 | }, 12 | foveation: 0, 13 | bounded: false, 14 | }) 15 | 16 | export default function App() { 17 | return ( 18 | <> 19 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /examples/watch/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import basicSsl from '@vitejs/plugin-basic-ssl' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), basicSsl()], 8 | base: '/xr/examples/watch/', 9 | resolve: { 10 | dedupe: ['@react-three/fiber', 'three'], 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Bela Bohlender", 3 | "license": "MIT", 4 | "type": "module", 5 | "devDependencies": { 6 | "@react-three/drei": "rc", 7 | "@react-three/eslint-plugin": "^0.1.1", 8 | "@react-three/fiber": "rc", 9 | "@types/chai": "^5.0.1", 10 | "@types/eslint": "^8.56.10", 11 | "@types/mocha": "^10.0.10", 12 | "@types/react": "^19", 13 | "@types/react-dom": "^19", 14 | "@types/three": "latest", 15 | "@types/webxr": "^0.5.21", 16 | "@typescript-eslint/eslint-plugin": "^7.12.0", 17 | "@typescript-eslint/parser": "^7.12.0", 18 | "@vitejs/plugin-basic-ssl": "^1.1.0", 19 | "@vitejs/plugin-react": "^4.3.0", 20 | "chai": "^5.1.2", 21 | "eslint": "^8", 22 | "eslint-config-prettier": "^9.1.0", 23 | "eslint-plugin-import": "^2.29.1", 24 | "eslint-plugin-prettier": "^5.1.3", 25 | "eslint-plugin-react": "^7.34.2", 26 | "eslint-plugin-react-hooks": "^4.6.2", 27 | "eslint-plugin-unused-imports": "^4.1.4", 28 | "json": "^11.0.0", 29 | "mocha": "^10.8.2", 30 | "playwright": "^1.45.1", 31 | "react": "^19", 32 | "react-dom": "^19", 33 | "three": "latest", 34 | "typescript": "^5.5.4", 35 | "vite": "^5.2.11" 36 | }, 37 | "packageManager": "pnpm@9.4.0+sha512.f549b8a52c9d2b8536762f99c0722205efc5af913e77835dbccc3b0b0b2ca9e7dc8022b78062c17291c48e88749c70ce88eb5a74f1fa8c4bf5e18bb46c8bd83a", 38 | "dependencies": { 39 | "ts-node": "^10.9.2" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/handle/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Bela Bohlender 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/handle/README.md: -------------------------------------------------------------------------------- 1 | # @pmndrs/handle 2 | 3 | framework agnostic handle implementation for threejs 4 | 5 | ## How to use 6 | 7 | ```ts 8 | //lets create a handle to translate the object on the x axis (the target and the handle are both the object) 9 | const store = new HandleStore(object, () => ({ scale: false, rotate: false, translate: "x" })) 10 | store.bind(object) 11 | ``` -------------------------------------------------------------------------------- /packages/handle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pmndrs/handle", 3 | "description": "framework agnostic expandable handle implementation for threejs", 4 | "author": "Bela Bohlender", 5 | "type": "module", 6 | "homepage": "https://github.com/pmndrs/xr", 7 | "version": "0.0.0", 8 | "scripts": { 9 | "build": "tsc", 10 | "test": "mocha ./tests/*.spec.ts", 11 | "check:prettier": "prettier --check src", 12 | "check:eslint": "eslint \"src/**/*.{ts,tsx}\"", 13 | "fix:prettier": "prettier --write src", 14 | "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix" 15 | }, 16 | "keywords": [ 17 | "r3f", 18 | "xr", 19 | "ar", 20 | "vr", 21 | "three.js", 22 | "react", 23 | "typescript" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git@github.com:pmndrs/xr.git" 28 | }, 29 | "publishConfig": { 30 | "main": "dist/index.js" 31 | }, 32 | "main": "src/index.ts", 33 | "license": "SEE LICENSE IN LICENSE", 34 | "dependencies": { 35 | "@pmndrs/pointer-events": "workspace:~", 36 | "zustand": "^4.5.2" 37 | }, 38 | "files": [ 39 | "dist" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /packages/handle/src/computations/index.ts: -------------------------------------------------------------------------------- 1 | export * from './one-pointer.js' 2 | export * from './two-pointer.js' 3 | export * from './translate-as.js' 4 | -------------------------------------------------------------------------------- /packages/handle/src/handles/drag.ts: -------------------------------------------------------------------------------- 1 | import { HandleOptions, HandleStore } from '../store.js' 2 | import type { PointerEventsMap } from '@pmndrs/pointer-events' 3 | import type { Object3D, Object3DEventMap } from 'three' 4 | 5 | export class DragHandle { 6 | private connections: Array<{ store: HandleStore; unbind: () => void }> 7 | 8 | constructor(objects: Array, getOptions?: () => HandleOptions) { 9 | this.connections = objects.map((object) => { 10 | const store = new HandleStore(object, () => ({ 11 | multitouch: false, 12 | scale: false, 13 | ...getOptions?.(), 14 | })) 15 | return { 16 | store, 17 | unbind: store.bind(object as Object3D), 18 | } 19 | }) 20 | } 21 | 22 | update(time: number): void { 23 | for (const { store } of this.connections) { 24 | store.update(time) 25 | } 26 | } 27 | 28 | dispose(): void { 29 | for (const { unbind } of this.connections) { 30 | unbind() 31 | } 32 | this.connections.length = 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/handle/src/handles/index.ts: -------------------------------------------------------------------------------- 1 | import type { Axis } from '../state.js' 2 | import type { Vector2Tuple } from 'three' 3 | 4 | export type HandlesProperties = 5 | | boolean 6 | | { 7 | x?: boolean | Vector2Tuple | 'disabled' 8 | y?: boolean | Vector2Tuple | 'disabled' 9 | z?: boolean | Vector2Tuple | 'disabled' 10 | e?: boolean | Vector2Tuple | 'disabled' 11 | } 12 | | 'disabled' 13 | | Axis 14 | | 'e' 15 | 16 | export type TransformHandlesSpace = 'local' | 'world' 17 | 18 | export * from './context.js' 19 | export * from './material.js' 20 | export * from './axis.js' 21 | export * from './registered.js' 22 | export * from './drag.js' 23 | export * from './pivot/index.js' 24 | export * from './translate/index.js' 25 | export * from './scale/index.js' 26 | export * from './rotate/index.js' 27 | export * from './transform.js' 28 | -------------------------------------------------------------------------------- /packages/handle/src/handles/material.ts: -------------------------------------------------------------------------------- 1 | import { Color, ColorRepresentation, LineBasicMaterial, MeshBasicMaterial } from 'three' 2 | import { HandlesContext } from './context.js' 3 | 4 | export const handleXRayMaterialProperties = { 5 | depthTest: false, 6 | depthWrite: false, 7 | fog: false, 8 | toneMapped: false, 9 | transparent: true, 10 | } 11 | 12 | export function setupHandlesContextHoverMaterial( 13 | context: HandlesContext, 14 | material: MeshBasicMaterial | LineBasicMaterial, 15 | tag: string, 16 | { 17 | color, 18 | hoverColor, 19 | hoverOpacity, 20 | opacity, 21 | disabled = false, 22 | }: { 23 | color: ColorRepresentation 24 | disabled?: boolean 25 | opacity?: number 26 | hoverColor?: ColorRepresentation 27 | hoverOpacity?: number 28 | }, 29 | ) { 30 | if ((hoverColor == null && hoverOpacity == null) || disabled) { 31 | material.color.set(color) 32 | material.opacity = opacity ?? 1 33 | if (disabled) { 34 | material.opacity *= 0.5 35 | material.color.lerp(new Color(1, 1, 1), 0.5) 36 | } 37 | return 38 | } 39 | hoverColor ??= color 40 | return context.subscribeHover((tags) => { 41 | const isHovered = tags.some((activeTag) => activeTag.includes(tag)) 42 | material.color.set(isHovered ? hoverColor : color) 43 | material.opacity = (isHovered ? hoverOpacity : opacity) ?? 1 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /packages/handle/src/handles/pivot/scale.ts: -------------------------------------------------------------------------------- 1 | import { ColorRepresentation, Mesh, MeshBasicMaterial, SphereGeometry } from 'three' 2 | import { Axis } from '../../state.js' 3 | import { HandlesContext } from '../context.js' 4 | import { HandlesProperties } from '../index.js' 5 | import { handleXRayMaterialProperties, setupHandlesContextHoverMaterial } from '../material.js' 6 | import { RegisteredHandle } from '../registered.js' 7 | import { extractHandleTransformOptions } from '../utils.js' 8 | 9 | export class PivotAxisScaleHandle extends RegisteredHandle { 10 | constructor(context: HandlesContext, axis: Axis, tagPrefix: string) { 11 | super(context, axis, tagPrefix, () => ({ 12 | scale: this.options, 13 | rotate: false, 14 | translate: 'as-scale', 15 | multitouch: false, 16 | })) 17 | } 18 | 19 | bind(defaultColor: ColorRepresentation, config?: HandlesProperties) { 20 | const { options, disabled } = extractHandleTransformOptions(this.axis, config) 21 | if (options === false) { 22 | return undefined 23 | } 24 | this.options = options 25 | const material = new MeshBasicMaterial(handleXRayMaterialProperties) 26 | const cleanupHover = setupHandlesContextHoverMaterial(this.context, material, this.tag, { 27 | color: defaultColor, 28 | hoverColor: 0xffff40, 29 | disabled, 30 | }) 31 | 32 | const mesh = new Mesh(new SphereGeometry(0.04), material) 33 | mesh.renderOrder = Infinity 34 | mesh.pointerEventsOrder = Infinity 35 | mesh.position.x = 0.68 36 | 37 | const unregister = disabled ? undefined : this.context.registerHandle(this.store, mesh, this.tag) 38 | 39 | this.add(mesh) 40 | 41 | return () => { 42 | material.dispose() 43 | mesh.geometry.dispose() 44 | unregister?.() 45 | cleanupHover?.() 46 | this.remove(mesh) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/handle/src/handles/registered.ts: -------------------------------------------------------------------------------- 1 | import { Group } from 'three' 2 | import { HandleStore, HandleOptions } from '../store.js' 3 | import { HandlesContext } from './context.js' 4 | import { extractHandleTransformOptions } from './utils.js' 5 | 6 | export class RegisteredHandle extends Group { 7 | public readonly store: HandleStore 8 | protected options: Exclude['options'], false> | undefined 9 | protected readonly tag: string 10 | 11 | constructor( 12 | protected readonly context: HandlesContext, 13 | protected readonly axis: 'x' | 'y' | 'z' | 'xy' | 'yz' | 'xz' | 'xyz' | 'e', 14 | tagPrefix: string, 15 | getOptions: () => HandleOptions, 16 | ) { 17 | super() 18 | this.tag = (tagPrefix ?? '') + axis 19 | this.store = new HandleStore(context.target, () => context.getHandleOptions(this.tag, getOptions)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/handle/src/handles/scale/plane.ts: -------------------------------------------------------------------------------- 1 | import { ColorRepresentation, MeshBasicMaterial, Mesh, BoxGeometry } from 'three' 2 | import { HandlesContext } from '../context.js' 3 | import { HandlesProperties } from '../index.js' 4 | import { handleXRayMaterialProperties, setupHandlesContextHoverMaterial } from '../material.js' 5 | import { RegisteredHandle } from '../registered.js' 6 | import { extractHandleTransformOptions } from '../utils.js' 7 | 8 | export class PlaneScaleHandle extends RegisteredHandle { 9 | constructor(context: HandlesContext, tag: 'xy' | 'yz' | 'xz', tagPrefix: string = '') { 10 | super(context, tag, tagPrefix, () => ({ 11 | translate: 'as-scale', 12 | scale: this.options, 13 | rotate: false, 14 | multitouch: false, 15 | })) 16 | } 17 | 18 | bind(defaultColor: ColorRepresentation, defaultHoverColor: ColorRepresentation, config?: HandlesProperties) { 19 | const { options, disabled } = extractHandleTransformOptions(this.axis, config) 20 | if (options === false) { 21 | return undefined 22 | } 23 | this.options = options 24 | const material = new MeshBasicMaterial(handleXRayMaterialProperties) 25 | const cleanupHover = setupHandlesContextHoverMaterial(this.context, material, this.tag, { 26 | opacity: 0.5, 27 | hoverOpacity: 1, 28 | color: defaultColor, 29 | hoverColor: defaultHoverColor, 30 | disabled, 31 | }) 32 | 33 | const mesh = new Mesh(new BoxGeometry(0.2, 0.2, 0.01), material) 34 | mesh.renderOrder = Infinity 35 | mesh.pointerEventsOrder = Infinity 36 | mesh.position.set(0.15, 0.15, 0) 37 | 38 | const unregister = disabled ? undefined : this.context.registerHandle(this.store, mesh, this.tag) 39 | 40 | this.add(mesh) 41 | 42 | return () => { 43 | material.dispose() 44 | mesh.geometry.dispose() 45 | unregister?.() 46 | cleanupHover?.() 47 | this.remove(mesh) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/handle/src/handles/translate/plane.ts: -------------------------------------------------------------------------------- 1 | import { ColorRepresentation, MeshBasicMaterial, Mesh, BoxGeometry, Vector3 } from 'three' 2 | import { HandlesContext } from '../context.js' 3 | import { HandlesProperties } from '../index.js' 4 | import { handleXRayMaterialProperties, setupHandlesContextHoverMaterial } from '../material.js' 5 | import { RegisteredHandle } from '../registered.js' 6 | import { extractHandleTransformOptions } from '../utils.js' 7 | 8 | export class PlaneTranslateHandle extends RegisteredHandle { 9 | constructor( 10 | context: HandlesContext, 11 | tag: 'xy' | 'yz' | 'xz', 12 | tagPrefix: string = '', 13 | axisVectors?: [Vector3, Vector3], 14 | ) { 15 | super(context, tag, tagPrefix, () => ({ 16 | translate: axisVectors ?? this.options, 17 | scale: false, 18 | rotate: false, 19 | multitouch: false, 20 | })) 21 | } 22 | 23 | bind(defaultColor: ColorRepresentation, defaultHoverColor: ColorRepresentation, config?: HandlesProperties) { 24 | const { options, disabled } = extractHandleTransformOptions(this.axis, config) 25 | if (options === false) { 26 | return undefined 27 | } 28 | this.options = options 29 | const material = new MeshBasicMaterial(handleXRayMaterialProperties) 30 | 31 | const cleanupHover = setupHandlesContextHoverMaterial(this.context, material, this.tag, { 32 | opacity: 0.5, 33 | hoverOpacity: 1, 34 | color: defaultColor, 35 | hoverColor: defaultHoverColor, 36 | disabled, 37 | }) 38 | 39 | const mesh = new Mesh(new BoxGeometry(0.2, 0.2, 0.01), material) 40 | mesh.renderOrder = Infinity 41 | mesh.pointerEventsOrder = Infinity 42 | mesh.position.set(0.15, 0.15, 0) 43 | 44 | const unregister = disabled ? undefined : this.context.registerHandle(this.store, mesh, this.tag) 45 | 46 | this.add(mesh) 47 | 48 | return () => { 49 | material.dispose() 50 | mesh.geometry.dispose() 51 | unregister?.() 52 | cleanupHover?.() 53 | this.remove(mesh) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/handle/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './store.js' 2 | export * from './handles/index.js' 3 | export type { Axis, HandleState, HandleTransformState } from './state.js' 4 | export * from './screen/index.js' 5 | -------------------------------------------------------------------------------- /packages/handle/src/screen/index.ts: -------------------------------------------------------------------------------- 1 | import { ScreenHandleStore } from './store.js' 2 | 3 | export function filterForOnePointerRightClickOrTwoPointer(map: ScreenHandleStore['map']): boolean { 4 | if (map.size != 1) { 5 | return map.size === 2 6 | } 7 | const [p] = map.values() 8 | return p.initialEvent.button === 2 9 | } 10 | 11 | export function filterForOnePointerLeftClick(map: ScreenHandleStore['map']): boolean { 12 | if (map.size != 1) { 13 | return false 14 | } 15 | const [p] = map.values() 16 | return p.initialEvent.button === 0 17 | } 18 | 19 | export * from './camera.js' 20 | export * from './store.js' 21 | export * from './pan.js' 22 | export * from './zoom.js' 23 | export * from './rotate.js' 24 | export * from './pan.js' 25 | export * from './orbit.js' 26 | export * from './map.js' 27 | -------------------------------------------------------------------------------- /packages/handle/src/screen/rotate.ts: -------------------------------------------------------------------------------- 1 | import { OrthographicCamera, PerspectiveCamera, Vector2, Vector2Tuple } from 'three' 2 | import { StoreApi } from 'zustand/vanilla' 3 | import { defaultScreenCameraApply, ScreenCameraStateAndFunctions } from './camera.js' 4 | import { ScreenHandleStore } from './store.js' 5 | import { average } from './utils.js' 6 | 7 | const vector2Helper = new Vector2() 8 | const initialHelper = new Vector2() 9 | 10 | export class RotateScreenHandleStore extends ScreenHandleStore> { 11 | constructor( 12 | store: StoreApi, 13 | getCamera: () => PerspectiveCamera | OrthographicCamera, 14 | public filter: (map: ScreenHandleStore['map']) => boolean, 15 | public customApply?: typeof defaultScreenCameraApply, 16 | public speed?: number, 17 | ) { 18 | super( 19 | ([initialPitch, initialYaw], map) => { 20 | if (!this.filter(map)) { 21 | return 22 | } 23 | 24 | average(vector2Helper, map, 'currentScreenPosition') 25 | average(initialHelper, map, 'initialScreenPosition') 26 | 27 | vector2Helper.sub(initialHelper).multiplyScalar(-Math.PI * (this.speed ?? 1)) 28 | 29 | const camera = getCamera() 30 | const aspect = 31 | camera instanceof PerspectiveCamera 32 | ? camera.aspect 33 | : (camera.right - camera.left) / (camera.top - camera.bottom) 34 | ;(this.customApply ?? defaultScreenCameraApply)( 35 | { 36 | pitch: initialPitch - vector2Helper.y, 37 | yaw: initialYaw + vector2Helper.x * aspect, 38 | }, 39 | store, 40 | ) 41 | }, 42 | () => [store.getState().pitch, store.getState().yaw], 43 | ) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/handle/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { PointerEvent } from '@pmndrs/pointer-events' 2 | import { Vector3 } from 'three' 3 | 4 | export function getWorldDirection(event: PointerEvent, target: Vector3): boolean { 5 | if (event.details.type === 'sphere') { 6 | return false 7 | } 8 | if (event.details.type === 'lines') { 9 | const { line } = event.details 10 | target.copy(line.end).sub(line.start).normalize() 11 | return true 12 | } 13 | if (event.details.type === 'screen-ray') { 14 | target.copy(event.details.direction) 15 | return true 16 | } 17 | target.set(0, 0, -1).applyQuaternion(event.pointerQuaternion) 18 | return true 19 | } 20 | -------------------------------------------------------------------------------- /packages/handle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declaration": true 6 | }, 7 | "include": ["src"] 8 | } -------------------------------------------------------------------------------- /packages/pointer-events/build.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src"] 4 | } -------------------------------------------------------------------------------- /packages/pointer-events/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pmndrs/pointer-events", 3 | "description": "framework agnostic pointer-events implementation for threejs", 4 | "license": "SEE LICENSE IN LICENSE", 5 | "version": "0.0.0", 6 | "homepage": "https://github.com/pmndrs/xr", 7 | "author": "Bela Bohlender", 8 | "type": "module", 9 | "keywords": [ 10 | "r3f", 11 | "events", 12 | "pointer", 13 | "three.js", 14 | "userinterface", 15 | "typescript" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:pmndrs/xr.git" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "publishConfig": { 25 | "main": "dist/index.js" 26 | }, 27 | "main": "src/index.ts", 28 | "scripts": { 29 | "build": "tsc -p build.tsconfig.json", 30 | "test": "vitest run --printConsoleTrace", 31 | "bench-write": "vitest bench --output-json bench.json --run", 32 | "bench-compare": "vitest bench --compare bench.json --run", 33 | "example": "vite example --host", 34 | "example:build": "vite build example", 35 | "check:prettier": "prettier --check src", 36 | "check:eslint": "eslint \"src/**/*.{ts,tsx}\"", 37 | "fix:prettier": "prettier --write src", 38 | "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix" 39 | }, 40 | "dependencies": {}, 41 | "devDependencies": { 42 | "@types/node": "^20.12.11", 43 | "vite": "^5.2.11", 44 | "vitest": "^2.0.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/pointer-events/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { AllowedPointerEvents, AllowedPointerEventsType } from './pointer.js' 2 | import type { createStore } from '@react-three/fiber/dist/declarations/src/core/store.js' 3 | 4 | declare module 'three' { 5 | interface Object3D { 6 | __r3f?: { 7 | eventCount: number 8 | handlers: Record void) | undefined> 9 | root: ReturnType 10 | } 11 | /** 12 | * undefined and true means the transformation is ready 13 | * false means transformation is not ready 14 | */ 15 | transformReady?: boolean 16 | 17 | /** 18 | * @default parent.pointerEvents ?? this.defaultPointerEvents 19 | */ 20 | pointerEvents?: AllowedPointerEvents 21 | /** 22 | * @default "listener" 23 | */ 24 | defaultPointerEvents?: AllowedPointerEvents 25 | /** 26 | * @default "all" 27 | */ 28 | pointerEventsType?: AllowedPointerEventsType 29 | /** 30 | * @default 0 31 | * sorted by highest number first 32 | * (just like a higher renderOrder number will result in rendering over the previous - if depthTest is false) 33 | */ 34 | pointerEventsOrder?: number 35 | isVoidObject?: boolean 36 | } 37 | } 38 | 39 | export * from './pointer.js' 40 | export * from './event.js' 41 | export * from './intersections/index.js' 42 | export * from './forward.js' 43 | export * from './pointer/index.js' 44 | export * from './combine.js' 45 | -------------------------------------------------------------------------------- /packages/pointer-events/src/intersections/index.ts: -------------------------------------------------------------------------------- 1 | import { Intersection as ThreeIntersection, Quaternion, Vector3, Line3, Vector2 } from 'three' 2 | 3 | export type Intersection = ThreeIntersection & { 4 | pointerPosition: Vector3 5 | pointerQuaternion: Quaternion 6 | /** 7 | * equal to "point" while the pointer is not captured 8 | * while the pointer is captured, "pointOnFace" represents the point transformed with the pointer along the face even if the pointer does not point on the face 9 | */ 10 | pointOnFace: Vector3 11 | localPoint: Vector3 12 | details: 13 | | { 14 | type: 'lines' 15 | distanceOnLine: number 16 | lineIndex: number 17 | line: Line3 18 | } 19 | | { 20 | type: 'screen-ray' 21 | /** 22 | * distance to the near plane of the camera of the screen 23 | */ 24 | distanceViewPlane: number 25 | direction: Vector3 26 | /** 27 | * point on the screen for x and y from -1 to 1 28 | */ 29 | screenPoint: Vector2 30 | } 31 | | { 32 | type: 'ray' 33 | } 34 | | { 35 | type: 'sphere' 36 | /** 37 | * set when the event is captured because the "distance" property is only the distance to a "expected intersection" 38 | */ 39 | distanceToFace?: number 40 | } 41 | } 42 | 43 | export type IntersectionOptions = { 44 | /** 45 | * @returns a negative number if i1 should be sorted before i2 46 | * for sorting by distance use i1.distance - i2.distance 47 | * => if i1 has a smaller distance the value is negative and i1 is returned as intersection 48 | */ 49 | customSort?: ( 50 | i1: ThreeIntersection, 51 | pointerEventsOrder1: number | undefined, 52 | i2: ThreeIntersection, 53 | pointerEventsOrder2: number | undefined, 54 | ) => number 55 | } 56 | 57 | export * from './intersector.js' 58 | export * from './lines.js' 59 | export * from './ray.js' 60 | export * from './sphere.js' 61 | -------------------------------------------------------------------------------- /packages/pointer-events/src/intersections/intersector.ts: -------------------------------------------------------------------------------- 1 | import { Mesh, Object3D, SphereGeometry } from 'three' 2 | import { Intersection } from '../index.js' 3 | import { PointerCapture } from '../pointer.js' 4 | 5 | const VoidObjectRadius = 10000000000 6 | const VoidObjectGeometry = new SphereGeometry(VoidObjectRadius) 7 | 8 | const sceneVoidObjectMap = new Map() 9 | 10 | export function getVoidObject(scene: Object3D): Object3D { 11 | let entry = sceneVoidObjectMap.get(scene) 12 | if (entry == null) { 13 | entry = new Mesh(VoidObjectGeometry) 14 | entry.isVoidObject = true 15 | entry.parent = scene 16 | //makes sure all other intersections are always prioritized 17 | entry.pointerEventsOrder = -Infinity 18 | sceneVoidObjectMap.set(scene, entry) 19 | } 20 | return entry 21 | } 22 | 23 | export interface Intersector { 24 | intersectPointerCapture(pointerCapture: PointerCapture, nativeEvent: unknown): Intersection 25 | isReady(): boolean 26 | startIntersection(nativeEvent: unknown): void 27 | executeIntersection(scene: Object3D, objectPointerEventsOrder: number): void 28 | finalizeIntersection(scene: Object3D): Intersection 29 | } 30 | -------------------------------------------------------------------------------- /packages/pointer-events/src/pointer/grab.ts: -------------------------------------------------------------------------------- 1 | import { Object3D } from 'three' 2 | import { generateUniquePointerId } from './index.js' 3 | import { IntersectionOptions } from '../intersections/index.js' 4 | import { SphereIntersector } from '../intersections/sphere.js' 5 | import { GetCamera, Pointer, PointerOptions } from '../pointer.js' 6 | 7 | export type GrabPointerOptions = { 8 | /** 9 | * @default 0.07 10 | */ 11 | radius?: number 12 | } & PointerOptions & 13 | IntersectionOptions 14 | 15 | export function createGrabPointer( 16 | getCamera: GetCamera, 17 | space: { current?: Object3D | null }, 18 | pointerState: any, 19 | options: GrabPointerOptions = {}, 20 | pointerType: string = 'grab', 21 | ) { 22 | return new Pointer( 23 | generateUniquePointerId(), 24 | pointerType, 25 | pointerState, 26 | new SphereIntersector(space, () => options.radius ?? 0.07, options), 27 | getCamera, 28 | undefined, 29 | undefined, 30 | undefined, 31 | options, 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /packages/pointer-events/src/pointer/index.ts: -------------------------------------------------------------------------------- 1 | let pointerIdCounter = 23412 2 | 3 | export function generateUniquePointerId() { 4 | return pointerIdCounter++ 5 | } 6 | 7 | export * from './grab.js' 8 | export * from './ray.js' 9 | export * from './lines.js' 10 | export * from './touch.js' 11 | -------------------------------------------------------------------------------- /packages/pointer-events/src/pointer/lines.ts: -------------------------------------------------------------------------------- 1 | import { Object3D, Vector3 } from 'three' 2 | import { IntersectionOptions, LinesIntersector } from '../intersections/index.js' 3 | import { GetCamera, Pointer, PointerOptions } from '../pointer.js' 4 | import { generateUniquePointerId } from './index.js' 5 | 6 | export type LinesPointerOptions = { 7 | /** 8 | * @default 0 9 | * distance to intersection in local space 10 | */ 11 | minDistance?: number 12 | /** 13 | * points for that compose the lines 14 | * @default [new Vector3(0,0,0), new Vector3(0,0,1)] 15 | */ 16 | linePoints?: Array 17 | } & PointerOptions & 18 | IntersectionOptions 19 | 20 | export function createLinesPointer( 21 | getCamera: GetCamera, 22 | space: { current?: Object3D | null }, 23 | pointerState: any, 24 | options: LinesPointerOptions = {}, 25 | pointerType: string = 'lines', 26 | ) { 27 | return new Pointer( 28 | generateUniquePointerId(), 29 | pointerType, 30 | pointerState, 31 | new LinesIntersector(space, options), 32 | getCamera, 33 | undefined, 34 | undefined, 35 | undefined, 36 | options, 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /packages/pointer-events/src/pointer/ray.ts: -------------------------------------------------------------------------------- 1 | import { Object3D, Vector3 } from 'three' 2 | import { IntersectionOptions, RayIntersector } from '../intersections/index.js' 3 | import { GetCamera, Pointer, PointerOptions } from '../pointer.js' 4 | import { generateUniquePointerId } from './index.js' 5 | 6 | export type RayPointerOptions = { 7 | /** 8 | * @default 0 9 | * distance to intersection in local space 10 | */ 11 | minDistance?: number 12 | /** 13 | * @default NegZAxis 14 | */ 15 | direction?: Vector3 16 | } & PointerOptions & 17 | IntersectionOptions 18 | 19 | export function createRayPointer( 20 | getCamera: GetCamera, 21 | space: { current?: Object3D | null }, 22 | pointerState: any, 23 | options: RayPointerOptions = {}, 24 | pointerType: string = 'ray', 25 | ) { 26 | return new Pointer( 27 | generateUniquePointerId(), 28 | pointerType, 29 | pointerState, 30 | new RayIntersector(space, options), 31 | getCamera, 32 | undefined, 33 | undefined, 34 | undefined, 35 | options, 36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /packages/pointer-events/src/pointer/touch.ts: -------------------------------------------------------------------------------- 1 | import { Object3D } from 'three' 2 | import { Intersection, IntersectionOptions, SphereIntersector } from '../intersections/index.js' 3 | import { GetCamera, Pointer, PointerOptions } from '../pointer.js' 4 | import { generateUniquePointerId } from './index.js' 5 | 6 | export type TouchPointerOptions = { 7 | /** 8 | * @default 0.1 9 | */ 10 | hoverRadius?: number 11 | /** 12 | * @default 0.03 13 | */ 14 | downRadius?: number 15 | /** 16 | * @default 0 17 | */ 18 | button?: number 19 | } & PointerOptions & 20 | IntersectionOptions 21 | 22 | export function createTouchPointer( 23 | getCamera: GetCamera, 24 | space: { current?: Object3D | null }, 25 | pointerState: any, 26 | options: TouchPointerOptions = {}, 27 | pointerType: string = 'touch', 28 | ) { 29 | return new Pointer( 30 | generateUniquePointerId(), 31 | pointerType, 32 | pointerState, 33 | new SphereIntersector(space, () => options.hoverRadius ?? 0.1, options), 34 | getCamera, 35 | createUpdateTouchPointer(options), 36 | undefined, 37 | undefined, 38 | options, 39 | ) 40 | } 41 | 42 | function createUpdateTouchPointer(options: TouchPointerOptions) { 43 | let wasPointerDown = false 44 | return (pointer: Pointer) => { 45 | if (!pointer.getEnabled()) { 46 | return 47 | } 48 | const intersection = pointer.getIntersection() 49 | const isPointerDown = computeIsPointerDown(intersection, options.downRadius ?? 0.03) 50 | if (isPointerDown === wasPointerDown) { 51 | return 52 | } 53 | const nativeEvent = { timeStamp: performance.now(), button: options.button ?? 0 } 54 | if (isPointerDown) { 55 | pointer.down(nativeEvent) 56 | } else { 57 | pointer.up(nativeEvent) 58 | } 59 | wasPointerDown = isPointerDown 60 | } 61 | } 62 | 63 | function computeIsPointerDown(intersection: Intersection | undefined, downRadius: number): boolean { 64 | if (intersection == null) { 65 | return false 66 | } 67 | return intersection.distance <= downRadius 68 | } 69 | -------------------------------------------------------------------------------- /packages/pointer-events/tests/browser/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /packages/pointer-events/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declaration": true, 6 | "skipLibCheck": true 7 | }, 8 | "include": ["src", "tests"] 9 | } -------------------------------------------------------------------------------- /packages/react/handle/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Bela Bohlender 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/react/handle/handle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pmndrs/xr/82c1b0320e6e3bf06e464fdf2ae4a62fb50ad251/packages/react/handle/handle.gif -------------------------------------------------------------------------------- /packages/react/handle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-three/handle", 3 | "description": "agnostic expandable handles for react-three-fiber", 4 | "author": "Bela Bohlender", 5 | "license": "SEE LICENSE IN LICENSE", 6 | "homepage": "https://github.com/pmndrs/xr", 7 | "type": "module", 8 | "keywords": [ 9 | "r3f", 10 | "handle", 11 | "controls", 12 | "three.js", 13 | "react", 14 | "typescript" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:pmndrs/xr.git" 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "publishConfig": { 24 | "main": "dist/index.js" 25 | }, 26 | "main": "src/index.ts", 27 | "peerDependencies": { 28 | "@react-three/fiber": ">=8", 29 | "react": ">=18", 30 | "three": "*" 31 | }, 32 | "dependencies": { 33 | "@pmndrs/handle": "workspace:~", 34 | "@pmndrs/pointer-events": "workspace:~", 35 | "zustand": "^4.5.2" 36 | }, 37 | "devDependencies": { 38 | "@react-three/fiber": "rc" 39 | }, 40 | "scripts": { 41 | "build": "tsc", 42 | "check:prettier": "prettier --check src", 43 | "check:eslint": "eslint \"src/**/*.{ts,tsx}\"", 44 | "fix:prettier": "prettier --write src", 45 | "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/react/handle/src/handles/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pivot.js' 2 | export * from './transform.js' 3 | export * from './screen.js' 4 | -------------------------------------------------------------------------------- /packages/react/handle/src/handles/utils.ts: -------------------------------------------------------------------------------- 1 | import { defaultApply, HandleOptions, HandlesProperties, HandleState } from '@pmndrs/handle' 2 | import { useThree } from '@react-three/fiber' 3 | import { useCallback } from 'react' 4 | import { Object3D, Vector2Tuple } from 'three' 5 | 6 | type ControlsProto = { 7 | enabled: boolean 8 | } 9 | 10 | export function useApplyThatDisablesDefaultControls(apply: HandleOptions['apply']): HandleOptions['apply'] { 11 | const controls = useThree((s) => s.controls as unknown as ControlsProto | undefined) 12 | return useCallback( 13 | (state: HandleState, target: Object3D) => { 14 | if (controls != null && state.first) { 15 | controls.enabled = false 16 | } 17 | if (controls != null && state.last) { 18 | controls.enabled = true 19 | } 20 | return (apply ?? defaultApply)(state, target) 21 | }, 22 | [apply, controls], 23 | ) 24 | } 25 | 26 | export function disableProperties(properties: HandlesProperties | undefined): HandlesProperties | undefined { 27 | if (properties === false) { 28 | return false 29 | } 30 | if (properties === true || properties === undefined || properties === 'disabled') { 31 | return 'disabled' 32 | } 33 | if (typeof properties === 'string') { 34 | const result: HandlesProperties = { 35 | x: false, 36 | y: false, 37 | z: false, 38 | e: false, 39 | } 40 | result[properties] = 'disabled' 41 | return result 42 | } 43 | return { 44 | x: disabledOrRemoved(properties.x), 45 | y: disabledOrRemoved(properties.y), 46 | z: disabledOrRemoved(properties.z), 47 | e: disabledOrRemoved(properties.e), 48 | } 49 | } 50 | 51 | function disabledOrRemoved(value: boolean | Vector2Tuple | 'disabled' | undefined) { 52 | if (value === false) { 53 | return false 54 | } 55 | return 'disabled' as const 56 | } 57 | -------------------------------------------------------------------------------- /packages/react/handle/src/hook.ts: -------------------------------------------------------------------------------- 1 | import { HandleOptions as BaseHandleOptions, HandleStore } from '@pmndrs/handle' 2 | import { useFrame } from '@react-three/fiber' 3 | import { RefObject, useEffect, useMemo, useRef } from 'react' 4 | import { Object3D } from 'three' 5 | 6 | export type HandleOptions = { 7 | /** 8 | * @default target 9 | */ 10 | handle?: RefObject 11 | /** 12 | * set to `false` to bind the store yourself using `store.bind(handle)` or capture objects yourself using `store.capture(pointerId, handle)` 13 | * @default true 14 | */ 15 | bind?: boolean 16 | } & BaseHandleOptions 17 | 18 | /** 19 | * // TODO: Add a valid description for this function. 20 | * 21 | * @template T - The type of the handle state. 22 | * @param {RefObject} target - The target object to attach the handle to. 23 | * @param {HandleOptions} [options={}] - Options for configuring the handle. 24 | * @param {() => HandleOptions} [getHandleOptions] - A function to dynamically provide handle options. 25 | * @returns {HandleStore} - The handle store instance. 26 | */ 27 | export function useHandle( 28 | target: RefObject, 29 | options: HandleOptions = {}, 30 | getHandleOptions?: () => HandleOptions, 31 | ): HandleStore { 32 | const optionsRef = useRef(options) 33 | optionsRef.current = options 34 | const getHandleOptionsRef = useRef(getHandleOptions) 35 | getHandleOptionsRef.current = getHandleOptions 36 | const store = useMemo( 37 | () => new HandleStore(target, () => ({ ...getHandleOptionsRef.current?.(), ...optionsRef.current })), 38 | [target], 39 | ) 40 | useFrame((state) => store.update(state.clock.getElapsedTime()), -1) 41 | const handleRef = options.handle ?? target 42 | useEffect(() => { 43 | if (options.bind === false) { 44 | return 45 | } 46 | const handle = handleRef.current 47 | if (handle == null) { 48 | return 49 | } 50 | return store.bind(handle as Object3D) 51 | }, [store, handleRef, options.bind]) 52 | return store 53 | } 54 | -------------------------------------------------------------------------------- /packages/react/handle/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hook.js' 2 | export * from './component.js' 3 | export * from './handles/index.js' 4 | export { 5 | HandleStore, 6 | ScreenHandleStore, 7 | type HandleState, 8 | type HandleTransformState, 9 | filterForOnePointerLeftClick, 10 | filterForOnePointerRightClickOrTwoPointer, 11 | defaultApply, 12 | defaultMapHandlesScreenCameraApply, 13 | defaultOrbitHandlesScreenCameraApply, 14 | defaultScreenCameraApply, 15 | } from '@pmndrs/handle' 16 | -------------------------------------------------------------------------------- /packages/react/handle/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "declaration": true 6 | } 7 | } -------------------------------------------------------------------------------- /packages/react/xr/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Bela Bohlender 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /packages/react/xr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@react-three/xr", 3 | "description": "VR/AR for react-three-fiber", 4 | "author": "Bela Bohlender", 5 | "license": "SEE LICENSE IN LICENSE", 6 | "homepage": "https://github.com/pmndrs/xr", 7 | "type": "module", 8 | "keywords": [ 9 | "r3f", 10 | "xr", 11 | "ar", 12 | "vr", 13 | "three.js", 14 | "react", 15 | "typescript" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:pmndrs/xr.git" 20 | }, 21 | "files": [ 22 | "dist" 23 | ], 24 | "publishConfig": { 25 | "main": "dist/index.js" 26 | }, 27 | "main": "src/index.ts", 28 | "peerDependencies": { 29 | "@react-three/fiber": ">=8", 30 | "react": ">=18", 31 | "react-dom": ">=18", 32 | "three": "*" 33 | }, 34 | "devDependencies": { 35 | "@react-three/fiber": "rc", 36 | "typedoc": "^0.28.3", 37 | "typedoc-plugin-frontmatter": "^1.3.0", 38 | "typedoc-plugin-markdown": "^4.6.3" 39 | }, 40 | "dependencies": { 41 | "@pmndrs/pointer-events": "workspace:~", 42 | "@pmndrs/xr": "workspace:~", 43 | "suspend-react": "^0.1.3", 44 | "tunnel-rat": "^0.1.2", 45 | "zustand": "^4.5.2" 46 | }, 47 | "scripts": { 48 | "build": "tsc", 49 | "generate_docs": "typedoc", 50 | "check:prettier": "prettier --check src", 51 | "check:eslint": "eslint \"src/**/*.{ts,tsx}\"", 52 | "fix:prettier": "prettier --write src", 53 | "fix:eslint": "eslint \"src/**/*.{ts,tsx}\" --fix" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/react/xr/src/contexts.tsx: -------------------------------------------------------------------------------- 1 | import { CombinedPointer } from '@pmndrs/pointer-events' 2 | import { XRInputSourceState } from '@pmndrs/xr/internals' 3 | import { createContext } from 'react' 4 | import { XRStore } from './xr.js' 5 | 6 | export const xrContext = createContext(undefined) 7 | export const xrInputSourceStateContext = createContext(undefined) 8 | export const xrSpaceContext = createContext(undefined) 9 | export const combinedPointerContext = createContext(undefined) 10 | -------------------------------------------------------------------------------- /packages/react/xr/src/deprecated/button.tsx: -------------------------------------------------------------------------------- 1 | import { ButtonHTMLAttributes, ComponentPropsWithoutRef, forwardRef } from 'react' 2 | import { useStore } from 'zustand' 3 | import { useXRSessionModeSupported } from '../hooks.js' 4 | import { XRStore } from '../xr.js' 5 | 6 | /** 7 | * @deprecated use ` 26 | ) 27 | }) 28 | 29 | /** 30 | * @deprecated use `