├── .env ├── .eslintcache ├── .gitignore ├── README.md ├── package.json ├── public ├── index.html ├── manifest.json └── robots.txt ├── readmeAssets ├── withOffsetting2.gif └── withoutOffsetting2.gif ├── src ├── Box.js ├── Reticle.jsx ├── index.js └── styles.css └── yarn.lock /.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /.eslintcache: -------------------------------------------------------------------------------- 1 | [{"/Users/jacobjaffe/Projects/pointer-lock-controls-offset-example/src/index.js":"1","/Users/jacobjaffe/Projects/pointer-lock-controls-offset-example/src/Reticle.jsx":"2","/Users/jacobjaffe/Projects/pointer-lock-controls-offset-example/src/Box.js":"3"},{"size":2823,"mtime":1607021329721,"results":"4","hashOfConfig":"5"},{"size":480,"mtime":1607020648574,"results":"6","hashOfConfig":"5"},{"size":960,"mtime":1607018069306,"results":"7","hashOfConfig":"5"},{"filePath":"8","messages":"9","errorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},"1okbr6x",{"filePath":"10","messages":"11","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"12","messages":"13","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/jacobjaffe/Projects/pointer-lock-controls-offset-example/src/index.js",["14","15","16"],"/Users/jacobjaffe/Projects/pointer-lock-controls-offset-example/src/Reticle.jsx",[],"/Users/jacobjaffe/Projects/pointer-lock-controls-offset-example/src/Box.js",[],{"ruleId":"17","severity":1,"message":"18","line":1,"column":36,"nodeType":"19","messageId":"20","endLine":1,"endColumn":44},{"ruleId":"17","severity":1,"message":"21","line":59,"column":9,"nodeType":"19","messageId":"20","endLine":59,"endColumn":21},{"ruleId":"17","severity":1,"message":"22","line":60,"column":19,"nodeType":"19","messageId":"20","endLine":60,"endColumn":24},"no-unused-vars","'useState' is defined but never used.","Identifier","unusedVar","'mouseReticle' is assigned a value but never used.","'mouse' is assigned a value but never used."] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pointer Lock Controls Offset Example 2 | 3 | This repo provides an example of how to use mouse offsets with pointer lock controls with Three Fiber. 4 | 5 | Normally, when raycasting for pointer events, the raycast occurs from the offset of the mouse and camera. 6 | 7 | However, when using a PointerLockControl, the offset should be disregarded, as the mouse position does _not_ change. This can cause issues unless the mouse is set to the middle of the canvas before entering the pointer lock. 8 | 9 | By setting the offset to be the middle of the canvas, the offset is ignored, and the raycasts operate as expected. 10 | 11 | This is demo'd here by having the raycasted mouse location tracked with a `red circle`, and the expected raycast location tracked with a `black reticle`. 12 | 13 | ## Without Offsetting 14 | 15 | ![Without Offsetting](readmeAssets/withoutOffsetting2.gif) 16 | 17 | ## With Offsetting 18 | 19 | ![With Offsetting](readmeAssets/withOffsetting2.gif) 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pointer-lock-controls-offset-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-three/drei": "^2.2.7", 7 | "react": "^17.0.1", 8 | "react-dom": "^17.0.1", 9 | "react-scripts": "4.0.1", 10 | "react-three-fiber": "^5.3.6", 11 | "three": "^0.123.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "build": "react-scripts build" 16 | }, 17 | "eslintConfig": { 18 | "extends": [ 19 | "react-app", 20 | "react-app/jest" 21 | ] 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [], 5 | "start_url": ".", 6 | "display": "standalone", 7 | "theme_color": "#000000", 8 | "background_color": "#ffffff" 9 | } 10 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /readmeAssets/withOffsetting2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobJaffe/pointer-lock-controls-offset-example/1a2d4b6881dca11879f7bd11f2a6d87d4f8a8231/readmeAssets/withOffsetting2.gif -------------------------------------------------------------------------------- /readmeAssets/withoutOffsetting2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JacobJaffe/pointer-lock-controls-offset-example/1a2d4b6881dca11879f7bd11f2a6d87d4f8a8231/readmeAssets/withoutOffsetting2.gif -------------------------------------------------------------------------------- /src/Box.js: -------------------------------------------------------------------------------- 1 | // https://codesandbox.io/s/rrppl0y8l4?file=/src/App.js:1095-1166 2 | 3 | import React, { useRef, useState } from "react"; 4 | import { useFrame } from "react-three-fiber"; 5 | 6 | function Box(props) { 7 | // This reference will give us direct access to the mesh 8 | const mesh = useRef() 9 | // Set up state for the hovered and active state 10 | const [hovered, setHover] = useState(false) 11 | const [active, setActive] = useState(false) 12 | // Rotate mesh every frame, this is outside of React without overhead 13 | useFrame(() => { 14 | mesh.current.rotation.x = mesh.current.rotation.y += 0.01 15 | }) 16 | return ( 17 | setActive(!active)} 22 | onPointerOver={(e) => setHover(true)} 23 | onPointerOut={(e) => setHover(false)}> 24 | 25 | 26 | 27 | ) 28 | } 29 | 30 | export default Box; -------------------------------------------------------------------------------- /src/Reticle.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import * as THREE from "three"; 3 | 4 | const SIZE = 0.01; 5 | 6 | const Reticle = () => { 7 | const vertices = useMemo( 8 | () => 9 | [ 10 | [0, SIZE, 0], 11 | [0, -SIZE, 0], 12 | [0, 0, 0], 13 | [SIZE, 0, 0], 14 | [-SIZE, 0, 0], 15 | ].map((v) => new THREE.Vector3(...v)), 16 | [] 17 | ); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default Reticle; 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import ReactDOM from "react-dom"; 3 | import { Canvas, useThree, useFrame } from "react-three-fiber"; 4 | import { PointerLockControls, PerspectiveCamera } from "@react-three/drei"; 5 | import Reticle from "./Reticle"; 6 | import "./styles.css"; 7 | import Box from "./Box"; 8 | import { Vector3 } from "three"; 9 | 10 | function App() { 11 | const controlsRef = useRef(); 12 | const isLocked = useRef(false); 13 | 14 | return ( 15 | { 19 | if (isLocked.current) { 20 | return ({ 21 | offsetX: width / 2, 22 | offsetY: height / 2 23 | }) 24 | } else { 25 | return null; 26 | } 27 | } 28 | }} 29 | > 30 | 31 | 32 | { 34 | if (controlsRef.current) { 35 | controlsRef.current.addEventListener('lock', () => { 36 | console.log('lock'); 37 | isLocked.current = true 38 | }); 39 | controlsRef.current.addEventListener('unlock', () => { 40 | console.log('unlock') 41 | isLocked.current = false; 42 | }); 43 | } 44 | }} 45 | ref={controlsRef} 46 | /> 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | 58 | function Camera() { 59 | const mouseReticle = useRef(); 60 | const { camera, mouse } = useThree(); 61 | // initialize camera to look at origin. 62 | useEffect(() => { 63 | camera.lookAt(0, 0, 0); 64 | }, [camera]); 65 | 66 | return ( 67 | 68 | {/* This reticle lives where the camera is pointing. 69 | This would be used in a first person environment */} 70 | 71 | 72 | 73 | 74 | ); 75 | } 76 | 77 | // Project a reticle of the mouse position onto the near plane. 78 | // A little janky because the rotation is off, so it gets cut. 79 | function MouseReticle() { 80 | const { camera, mouse } = useThree(); 81 | const mouseReticle = useRef(); 82 | 83 | useFrame(() => { 84 | if (mouseReticle.current) { 85 | const vector = new Vector3(mouse.x, mouse.y, -0.8).unproject(camera); 86 | mouseReticle.current.position.set(...vector.toArray()); 87 | } 88 | }) 89 | 90 | return ( 91 | 92 | 93 | 94 | 95 | ) 96 | } 97 | 98 | const rootElement = document.getElementById("root"); 99 | ReactDOM.render(, rootElement); -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body, 7 | #root { 8 | width: 100%; 9 | height: 100%; 10 | margin: 0; 11 | padding: 0; 12 | } --------------------------------------------------------------------------------