├── LICENSE ├── README.md ├── _config.yml ├── build └── three.module.js ├── examples ├── jsm │ ├── controls │ │ ├── DeviceOrientationControls.js │ │ ├── DragControls.js │ │ ├── FirstPersonControls.js │ │ ├── FlyControls.js │ │ ├── OrbitControls.js │ │ ├── PointerLockControls.js │ │ ├── TrackballControls.js │ │ ├── TransformControls.js │ │ └── experimental │ │ │ └── CameraControls.js │ ├── geometries │ │ ├── BoxLineGeometry.js │ │ ├── ConvexGeometry.js │ │ ├── DecalGeometry.js │ │ ├── LightningStrike.js │ │ ├── ParametricGeometries.js │ │ ├── RoundedBoxGeometry.js │ │ └── TeapotGeometry.js │ ├── libs │ │ └── motion-controllers.module.js │ ├── loaders │ │ └── GLTFLoader.js │ └── webxr │ │ ├── OculusHandModel.js │ │ ├── OculusHandPointerModel.js │ │ ├── Text2D.js │ │ ├── VRButton.js │ │ ├── XRControllerModelFactory.js │ │ ├── XREstimatedLight.js │ │ ├── XRHandMeshModel.js │ │ ├── XRHandModelFactory.js │ │ └── XRHandPrimitiveModel.js ├── main.css ├── threejs_vr_hand_input.html └── threejs_vr_hand_input2.html └── images ├── 1.gif └── 2.gif /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Hartwell Fong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Threejs-VR-Hand-Input 2 | Explore Threejs VR hand input 3 | 4 | ## System Requirements 5 | 6 | Oculus Quest (tested Quest 1, no controllers)
7 | 8 | Oculus Browser >15.4 (Quest update 29.0)
9 | 10 | Not sure if Oculus Browser needs to be configured for WebXR like in the early days. If examples do not work, type "chrome://flags" in Oculus Browser and search for "webxr". "WebXR experiences with hand and joints tracking" and "WebXR Layers" are enabled.
11 | 12 | Threejs-VR-Hand-Input uses a subset of three.js r129 to start VR with hand and joints tracking.
13 | 14 | Codes for WebXR hand tracking may stop working after an Oculus Browser or threejs update.
15 | 16 | ## 1. Minimal Threejs VR Hand Input
17 | 18 | 19 | 20 | Open Oculus Browser to link and "Enter VR" with index finger-thumb click. No controllers as the codes are hand tracking only.
21 | 22 | [https://physicslibrary.github.io/Threejs-VR-Hand-Input/examples/threejs_vr_hand-input.html](https://physicslibrary.github.io/Threejs-VR-Hand-Input/examples/threejs_vr_hand_input.html) 23 | 24 | ## 2. Threejs VR Hand Input Palm-Up Gesture
25 | 26 | 27 | 28 | An example of left palm-up to make a box visible (open a menus or change variables). Distance between red cubes' y-positions decides when the palm is facing up.
29 | 30 | [https://physicslibrary.github.io/Threejs-VR-Hand-Input/examples/threejs_vr_hand-input2.html](https://physicslibrary.github.io/Threejs-VR-Hand-Input/examples/threejs_vr_hand_input2.html) 31 | 32 | ## Credits 33 | 34 | https://threejs.org/ 35 | 36 |
Copyright (c) 2021 Hartwell Fong 37 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /examples/jsm/controls/DeviceOrientationControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | Euler, 3 | EventDispatcher, 4 | MathUtils, 5 | Quaternion, 6 | Vector3 7 | } from '../../../build/three.module.js'; 8 | 9 | const _zee = new Vector3( 0, 0, 1 ); 10 | const _euler = new Euler(); 11 | const _q0 = new Quaternion(); 12 | const _q1 = new Quaternion( - Math.sqrt( 0.5 ), 0, 0, Math.sqrt( 0.5 ) ); // - PI/2 around the x-axis 13 | 14 | const _changeEvent = { type: 'change' }; 15 | 16 | class DeviceOrientationControls extends EventDispatcher { 17 | 18 | constructor( object ) { 19 | 20 | super(); 21 | 22 | if ( window.isSecureContext === false ) { 23 | 24 | console.error( 'THREE.DeviceOrientationControls: DeviceOrientationEvent is only available in secure contexts (https)' ); 25 | 26 | } 27 | 28 | const scope = this; 29 | 30 | const EPS = 0.000001; 31 | const lastQuaternion = new Quaternion(); 32 | 33 | this.object = object; 34 | this.object.rotation.reorder( 'YXZ' ); 35 | 36 | this.enabled = true; 37 | 38 | this.deviceOrientation = {}; 39 | this.screenOrientation = 0; 40 | 41 | this.alphaOffset = 0; // radians 42 | 43 | const onDeviceOrientationChangeEvent = function ( event ) { 44 | 45 | scope.deviceOrientation = event; 46 | 47 | }; 48 | 49 | const onScreenOrientationChangeEvent = function () { 50 | 51 | scope.screenOrientation = window.orientation || 0; 52 | 53 | }; 54 | 55 | // The angles alpha, beta and gamma form a set of intrinsic Tait-Bryan angles of type Z-X'-Y'' 56 | 57 | const setObjectQuaternion = function ( quaternion, alpha, beta, gamma, orient ) { 58 | 59 | _euler.set( beta, alpha, - gamma, 'YXZ' ); // 'ZXY' for the device, but 'YXZ' for us 60 | 61 | quaternion.setFromEuler( _euler ); // orient the device 62 | 63 | quaternion.multiply( _q1 ); // camera looks out the back of the device, not the top 64 | 65 | quaternion.multiply( _q0.setFromAxisAngle( _zee, - orient ) ); // adjust for screen orientation 66 | 67 | }; 68 | 69 | this.connect = function () { 70 | 71 | onScreenOrientationChangeEvent(); // run once on load 72 | 73 | // iOS 13+ 74 | 75 | if ( window.DeviceOrientationEvent !== undefined && typeof window.DeviceOrientationEvent.requestPermission === 'function' ) { 76 | 77 | window.DeviceOrientationEvent.requestPermission().then( function ( response ) { 78 | 79 | if ( response == 'granted' ) { 80 | 81 | window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent ); 82 | window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); 83 | 84 | } 85 | 86 | } ).catch( function ( error ) { 87 | 88 | console.error( 'THREE.DeviceOrientationControls: Unable to use DeviceOrientation API:', error ); 89 | 90 | } ); 91 | 92 | } else { 93 | 94 | window.addEventListener( 'orientationchange', onScreenOrientationChangeEvent ); 95 | window.addEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); 96 | 97 | } 98 | 99 | scope.enabled = true; 100 | 101 | }; 102 | 103 | this.disconnect = function () { 104 | 105 | window.removeEventListener( 'orientationchange', onScreenOrientationChangeEvent ); 106 | window.removeEventListener( 'deviceorientation', onDeviceOrientationChangeEvent ); 107 | 108 | scope.enabled = false; 109 | 110 | }; 111 | 112 | this.update = function () { 113 | 114 | if ( scope.enabled === false ) return; 115 | 116 | const device = scope.deviceOrientation; 117 | 118 | if ( device ) { 119 | 120 | const alpha = device.alpha ? MathUtils.degToRad( device.alpha ) + scope.alphaOffset : 0; // Z 121 | 122 | const beta = device.beta ? MathUtils.degToRad( device.beta ) : 0; // X' 123 | 124 | const gamma = device.gamma ? MathUtils.degToRad( device.gamma ) : 0; // Y'' 125 | 126 | const orient = scope.screenOrientation ? MathUtils.degToRad( scope.screenOrientation ) : 0; // O 127 | 128 | setObjectQuaternion( scope.object.quaternion, alpha, beta, gamma, orient ); 129 | 130 | if ( 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 131 | 132 | lastQuaternion.copy( scope.object.quaternion ); 133 | scope.dispatchEvent( _changeEvent ); 134 | 135 | } 136 | 137 | } 138 | 139 | }; 140 | 141 | this.dispose = function () { 142 | 143 | scope.disconnect(); 144 | 145 | }; 146 | 147 | this.connect(); 148 | 149 | } 150 | 151 | } 152 | 153 | export { DeviceOrientationControls }; 154 | -------------------------------------------------------------------------------- /examples/jsm/controls/DragControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | Matrix4, 4 | Plane, 5 | Raycaster, 6 | Vector2, 7 | Vector3 8 | } from '../../../build/three.module.js'; 9 | 10 | const _plane = new Plane(); 11 | const _raycaster = new Raycaster(); 12 | 13 | const _mouse = new Vector2(); 14 | const _offset = new Vector3(); 15 | const _intersection = new Vector3(); 16 | const _worldPosition = new Vector3(); 17 | const _inverseMatrix = new Matrix4(); 18 | 19 | class DragControls extends EventDispatcher { 20 | 21 | constructor( _objects, _camera, _domElement ) { 22 | 23 | super(); 24 | 25 | let _selected = null, _hovered = null; 26 | 27 | const _intersections = []; 28 | 29 | // 30 | 31 | const scope = this; 32 | 33 | function activate() { 34 | 35 | _domElement.addEventListener( 'pointermove', onPointerMove ); 36 | _domElement.addEventListener( 'pointerdown', onPointerDown ); 37 | _domElement.addEventListener( 'pointerup', onPointerCancel ); 38 | _domElement.addEventListener( 'pointerleave', onPointerCancel ); 39 | _domElement.addEventListener( 'touchmove', onTouchMove, { passive: false } ); 40 | _domElement.addEventListener( 'touchstart', onTouchStart, { passive: false } ); 41 | _domElement.addEventListener( 'touchend', onTouchEnd ); 42 | 43 | } 44 | 45 | function deactivate() { 46 | 47 | _domElement.removeEventListener( 'pointermove', onPointerMove ); 48 | _domElement.removeEventListener( 'pointerdown', onPointerDown ); 49 | _domElement.removeEventListener( 'pointerup', onPointerCancel ); 50 | _domElement.removeEventListener( 'pointerleave', onPointerCancel ); 51 | _domElement.removeEventListener( 'touchmove', onTouchMove ); 52 | _domElement.removeEventListener( 'touchstart', onTouchStart ); 53 | _domElement.removeEventListener( 'touchend', onTouchEnd ); 54 | 55 | _domElement.style.cursor = ''; 56 | 57 | } 58 | 59 | function dispose() { 60 | 61 | deactivate(); 62 | 63 | } 64 | 65 | function getObjects() { 66 | 67 | return _objects; 68 | 69 | } 70 | 71 | function onPointerMove( event ) { 72 | 73 | event.preventDefault(); 74 | 75 | switch ( event.pointerType ) { 76 | 77 | case 'mouse': 78 | case 'pen': 79 | onMouseMove( event ); 80 | break; 81 | 82 | // TODO touch 83 | 84 | } 85 | 86 | } 87 | 88 | function onMouseMove( event ) { 89 | 90 | const rect = _domElement.getBoundingClientRect(); 91 | 92 | _mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1; 93 | _mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1; 94 | 95 | _raycaster.setFromCamera( _mouse, _camera ); 96 | 97 | if ( _selected && scope.enabled ) { 98 | 99 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 100 | 101 | _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) ); 102 | 103 | } 104 | 105 | scope.dispatchEvent( { type: 'drag', object: _selected } ); 106 | 107 | return; 108 | 109 | } 110 | 111 | _intersections.length = 0; 112 | 113 | _raycaster.setFromCamera( _mouse, _camera ); 114 | _raycaster.intersectObjects( _objects, true, _intersections ); 115 | 116 | if ( _intersections.length > 0 ) { 117 | 118 | const object = _intersections[ 0 ].object; 119 | 120 | _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( object.matrixWorld ) ); 121 | 122 | if ( _hovered !== object && _hovered !== null ) { 123 | 124 | scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); 125 | 126 | _domElement.style.cursor = 'auto'; 127 | _hovered = null; 128 | 129 | } 130 | 131 | if ( _hovered !== object ) { 132 | 133 | scope.dispatchEvent( { type: 'hoveron', object: object } ); 134 | 135 | _domElement.style.cursor = 'pointer'; 136 | _hovered = object; 137 | 138 | } 139 | 140 | } else { 141 | 142 | if ( _hovered !== null ) { 143 | 144 | scope.dispatchEvent( { type: 'hoveroff', object: _hovered } ); 145 | 146 | _domElement.style.cursor = 'auto'; 147 | _hovered = null; 148 | 149 | } 150 | 151 | } 152 | 153 | } 154 | 155 | function onPointerDown( event ) { 156 | 157 | event.preventDefault(); 158 | 159 | switch ( event.pointerType ) { 160 | 161 | case 'mouse': 162 | case 'pen': 163 | onMouseDown( event ); 164 | break; 165 | 166 | // TODO touch 167 | 168 | } 169 | 170 | } 171 | 172 | function onMouseDown( event ) { 173 | 174 | event.preventDefault(); 175 | 176 | _intersections.length = 0; 177 | 178 | _raycaster.setFromCamera( _mouse, _camera ); 179 | _raycaster.intersectObjects( _objects, true, _intersections ); 180 | 181 | if ( _intersections.length > 0 ) { 182 | 183 | _selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object; 184 | 185 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 186 | 187 | _inverseMatrix.copy( _selected.parent.matrixWorld ).invert(); 188 | _offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) ); 189 | 190 | } 191 | 192 | _domElement.style.cursor = 'move'; 193 | 194 | scope.dispatchEvent( { type: 'dragstart', object: _selected } ); 195 | 196 | } 197 | 198 | 199 | } 200 | 201 | function onPointerCancel( event ) { 202 | 203 | event.preventDefault(); 204 | 205 | switch ( event.pointerType ) { 206 | 207 | case 'mouse': 208 | case 'pen': 209 | onMouseCancel( event ); 210 | break; 211 | 212 | // TODO touch 213 | 214 | } 215 | 216 | } 217 | 218 | function onMouseCancel( event ) { 219 | 220 | event.preventDefault(); 221 | 222 | if ( _selected ) { 223 | 224 | scope.dispatchEvent( { type: 'dragend', object: _selected } ); 225 | 226 | _selected = null; 227 | 228 | } 229 | 230 | _domElement.style.cursor = _hovered ? 'pointer' : 'auto'; 231 | 232 | } 233 | 234 | function onTouchMove( event ) { 235 | 236 | event.preventDefault(); 237 | event = event.changedTouches[ 0 ]; 238 | 239 | const rect = _domElement.getBoundingClientRect(); 240 | 241 | _mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1; 242 | _mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1; 243 | 244 | _raycaster.setFromCamera( _mouse, _camera ); 245 | 246 | if ( _selected && scope.enabled ) { 247 | 248 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 249 | 250 | _selected.position.copy( _intersection.sub( _offset ).applyMatrix4( _inverseMatrix ) ); 251 | 252 | } 253 | 254 | scope.dispatchEvent( { type: 'drag', object: _selected } ); 255 | 256 | return; 257 | 258 | } 259 | 260 | } 261 | 262 | function onTouchStart( event ) { 263 | 264 | event.preventDefault(); 265 | event = event.changedTouches[ 0 ]; 266 | 267 | const rect = _domElement.getBoundingClientRect(); 268 | 269 | _mouse.x = ( ( event.clientX - rect.left ) / rect.width ) * 2 - 1; 270 | _mouse.y = - ( ( event.clientY - rect.top ) / rect.height ) * 2 + 1; 271 | 272 | _intersections.length = 0; 273 | 274 | _raycaster.setFromCamera( _mouse, _camera ); 275 | _raycaster.intersectObjects( _objects, true, _intersections ); 276 | 277 | if ( _intersections.length > 0 ) { 278 | 279 | _selected = ( scope.transformGroup === true ) ? _objects[ 0 ] : _intersections[ 0 ].object; 280 | 281 | _plane.setFromNormalAndCoplanarPoint( _camera.getWorldDirection( _plane.normal ), _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) ); 282 | 283 | if ( _raycaster.ray.intersectPlane( _plane, _intersection ) ) { 284 | 285 | _inverseMatrix.copy( _selected.parent.matrixWorld ).invert(); 286 | _offset.copy( _intersection ).sub( _worldPosition.setFromMatrixPosition( _selected.matrixWorld ) ); 287 | 288 | } 289 | 290 | _domElement.style.cursor = 'move'; 291 | 292 | scope.dispatchEvent( { type: 'dragstart', object: _selected } ); 293 | 294 | } 295 | 296 | 297 | } 298 | 299 | function onTouchEnd( event ) { 300 | 301 | event.preventDefault(); 302 | 303 | if ( _selected ) { 304 | 305 | scope.dispatchEvent( { type: 'dragend', object: _selected } ); 306 | 307 | _selected = null; 308 | 309 | } 310 | 311 | _domElement.style.cursor = 'auto'; 312 | 313 | } 314 | 315 | activate(); 316 | 317 | // API 318 | 319 | this.enabled = true; 320 | this.transformGroup = false; 321 | 322 | this.activate = activate; 323 | this.deactivate = deactivate; 324 | this.dispose = dispose; 325 | this.getObjects = getObjects; 326 | 327 | } 328 | 329 | } 330 | 331 | export { DragControls }; 332 | -------------------------------------------------------------------------------- /examples/jsm/controls/FirstPersonControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | MathUtils, 3 | Spherical, 4 | Vector3 5 | } from '../../../build/three.module.js'; 6 | 7 | const _lookDirection = new Vector3(); 8 | const _spherical = new Spherical(); 9 | const _target = new Vector3(); 10 | 11 | class FirstPersonControls { 12 | 13 | constructor( object, domElement ) { 14 | 15 | if ( domElement === undefined ) { 16 | 17 | console.warn( 'THREE.FirstPersonControls: The second parameter "domElement" is now mandatory.' ); 18 | domElement = document; 19 | 20 | } 21 | 22 | this.object = object; 23 | this.domElement = domElement; 24 | 25 | // API 26 | 27 | this.enabled = true; 28 | 29 | this.movementSpeed = 1.0; 30 | this.lookSpeed = 0.005; 31 | 32 | this.lookVertical = true; 33 | this.autoForward = false; 34 | 35 | this.activeLook = true; 36 | 37 | this.heightSpeed = false; 38 | this.heightCoef = 1.0; 39 | this.heightMin = 0.0; 40 | this.heightMax = 1.0; 41 | 42 | this.constrainVertical = false; 43 | this.verticalMin = 0; 44 | this.verticalMax = Math.PI; 45 | 46 | this.mouseDragOn = false; 47 | 48 | // internals 49 | 50 | this.autoSpeedFactor = 0.0; 51 | 52 | this.mouseX = 0; 53 | this.mouseY = 0; 54 | 55 | this.moveForward = false; 56 | this.moveBackward = false; 57 | this.moveLeft = false; 58 | this.moveRight = false; 59 | 60 | this.viewHalfX = 0; 61 | this.viewHalfY = 0; 62 | 63 | // private variables 64 | 65 | let lat = 0; 66 | let lon = 0; 67 | 68 | // 69 | 70 | this.handleResize = function () { 71 | 72 | if ( this.domElement === document ) { 73 | 74 | this.viewHalfX = window.innerWidth / 2; 75 | this.viewHalfY = window.innerHeight / 2; 76 | 77 | } else { 78 | 79 | this.viewHalfX = this.domElement.offsetWidth / 2; 80 | this.viewHalfY = this.domElement.offsetHeight / 2; 81 | 82 | } 83 | 84 | }; 85 | 86 | this.onMouseDown = function ( event ) { 87 | 88 | if ( this.domElement !== document ) { 89 | 90 | this.domElement.focus(); 91 | 92 | } 93 | 94 | event.preventDefault(); 95 | 96 | if ( this.activeLook ) { 97 | 98 | switch ( event.button ) { 99 | 100 | case 0: this.moveForward = true; break; 101 | case 2: this.moveBackward = true; break; 102 | 103 | } 104 | 105 | } 106 | 107 | this.mouseDragOn = true; 108 | 109 | }; 110 | 111 | this.onMouseUp = function ( event ) { 112 | 113 | event.preventDefault(); 114 | 115 | if ( this.activeLook ) { 116 | 117 | switch ( event.button ) { 118 | 119 | case 0: this.moveForward = false; break; 120 | case 2: this.moveBackward = false; break; 121 | 122 | } 123 | 124 | } 125 | 126 | this.mouseDragOn = false; 127 | 128 | }; 129 | 130 | this.onMouseMove = function ( event ) { 131 | 132 | if ( this.domElement === document ) { 133 | 134 | this.mouseX = event.pageX - this.viewHalfX; 135 | this.mouseY = event.pageY - this.viewHalfY; 136 | 137 | } else { 138 | 139 | this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX; 140 | this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY; 141 | 142 | } 143 | 144 | }; 145 | 146 | this.onKeyDown = function ( event ) { 147 | 148 | //event.preventDefault(); 149 | 150 | switch ( event.code ) { 151 | 152 | case 'ArrowUp': 153 | case 'KeyW': this.moveForward = true; break; 154 | 155 | case 'ArrowLeft': 156 | case 'KeyA': this.moveLeft = true; break; 157 | 158 | case 'ArrowDown': 159 | case 'KeyS': this.moveBackward = true; break; 160 | 161 | case 'ArrowRight': 162 | case 'KeyD': this.moveRight = true; break; 163 | 164 | case 'KeyR': this.moveUp = true; break; 165 | case 'KeyF': this.moveDown = true; break; 166 | 167 | } 168 | 169 | }; 170 | 171 | this.onKeyUp = function ( event ) { 172 | 173 | switch ( event.code ) { 174 | 175 | case 'ArrowUp': 176 | case 'KeyW': this.moveForward = false; break; 177 | 178 | case 'ArrowLeft': 179 | case 'KeyA': this.moveLeft = false; break; 180 | 181 | case 'ArrowDown': 182 | case 'KeyS': this.moveBackward = false; break; 183 | 184 | case 'ArrowRight': 185 | case 'KeyD': this.moveRight = false; break; 186 | 187 | case 'KeyR': this.moveUp = false; break; 188 | case 'KeyF': this.moveDown = false; break; 189 | 190 | } 191 | 192 | }; 193 | 194 | this.lookAt = function ( x, y, z ) { 195 | 196 | if ( x.isVector3 ) { 197 | 198 | _target.copy( x ); 199 | 200 | } else { 201 | 202 | _target.set( x, y, z ); 203 | 204 | } 205 | 206 | this.object.lookAt( _target ); 207 | 208 | setOrientation( this ); 209 | 210 | return this; 211 | 212 | }; 213 | 214 | this.update = function () { 215 | 216 | const targetPosition = new Vector3(); 217 | 218 | return function update( delta ) { 219 | 220 | if ( this.enabled === false ) return; 221 | 222 | if ( this.heightSpeed ) { 223 | 224 | const y = MathUtils.clamp( this.object.position.y, this.heightMin, this.heightMax ); 225 | const heightDelta = y - this.heightMin; 226 | 227 | this.autoSpeedFactor = delta * ( heightDelta * this.heightCoef ); 228 | 229 | } else { 230 | 231 | this.autoSpeedFactor = 0.0; 232 | 233 | } 234 | 235 | const actualMoveSpeed = delta * this.movementSpeed; 236 | 237 | if ( this.moveForward || ( this.autoForward && ! this.moveBackward ) ) this.object.translateZ( - ( actualMoveSpeed + this.autoSpeedFactor ) ); 238 | if ( this.moveBackward ) this.object.translateZ( actualMoveSpeed ); 239 | 240 | if ( this.moveLeft ) this.object.translateX( - actualMoveSpeed ); 241 | if ( this.moveRight ) this.object.translateX( actualMoveSpeed ); 242 | 243 | if ( this.moveUp ) this.object.translateY( actualMoveSpeed ); 244 | if ( this.moveDown ) this.object.translateY( - actualMoveSpeed ); 245 | 246 | let actualLookSpeed = delta * this.lookSpeed; 247 | 248 | if ( ! this.activeLook ) { 249 | 250 | actualLookSpeed = 0; 251 | 252 | } 253 | 254 | let verticalLookRatio = 1; 255 | 256 | if ( this.constrainVertical ) { 257 | 258 | verticalLookRatio = Math.PI / ( this.verticalMax - this.verticalMin ); 259 | 260 | } 261 | 262 | lon -= this.mouseX * actualLookSpeed; 263 | if ( this.lookVertical ) lat -= this.mouseY * actualLookSpeed * verticalLookRatio; 264 | 265 | lat = Math.max( - 85, Math.min( 85, lat ) ); 266 | 267 | let phi = MathUtils.degToRad( 90 - lat ); 268 | const theta = MathUtils.degToRad( lon ); 269 | 270 | if ( this.constrainVertical ) { 271 | 272 | phi = MathUtils.mapLinear( phi, 0, Math.PI, this.verticalMin, this.verticalMax ); 273 | 274 | } 275 | 276 | const position = this.object.position; 277 | 278 | targetPosition.setFromSphericalCoords( 1, phi, theta ).add( position ); 279 | 280 | this.object.lookAt( targetPosition ); 281 | 282 | }; 283 | 284 | }(); 285 | 286 | this.dispose = function () { 287 | 288 | this.domElement.removeEventListener( 'contextmenu', contextmenu ); 289 | this.domElement.removeEventListener( 'mousedown', _onMouseDown ); 290 | this.domElement.removeEventListener( 'mousemove', _onMouseMove ); 291 | this.domElement.removeEventListener( 'mouseup', _onMouseUp ); 292 | 293 | window.removeEventListener( 'keydown', _onKeyDown ); 294 | window.removeEventListener( 'keyup', _onKeyUp ); 295 | 296 | }; 297 | 298 | const _onMouseMove = this.onMouseMove.bind( this ); 299 | const _onMouseDown = this.onMouseDown.bind( this ); 300 | const _onMouseUp = this.onMouseUp.bind( this ); 301 | const _onKeyDown = this.onKeyDown.bind( this ); 302 | const _onKeyUp = this.onKeyUp.bind( this ); 303 | 304 | this.domElement.addEventListener( 'contextmenu', contextmenu ); 305 | this.domElement.addEventListener( 'mousemove', _onMouseMove ); 306 | this.domElement.addEventListener( 'mousedown', _onMouseDown ); 307 | this.domElement.addEventListener( 'mouseup', _onMouseUp ); 308 | 309 | window.addEventListener( 'keydown', _onKeyDown ); 310 | window.addEventListener( 'keyup', _onKeyUp ); 311 | 312 | function setOrientation( controls ) { 313 | 314 | const quaternion = controls.object.quaternion; 315 | 316 | _lookDirection.set( 0, 0, - 1 ).applyQuaternion( quaternion ); 317 | _spherical.setFromVector3( _lookDirection ); 318 | 319 | lat = 90 - MathUtils.radToDeg( _spherical.phi ); 320 | lon = MathUtils.radToDeg( _spherical.theta ); 321 | 322 | } 323 | 324 | this.handleResize(); 325 | 326 | setOrientation( this ); 327 | 328 | } 329 | 330 | } 331 | 332 | function contextmenu( event ) { 333 | 334 | event.preventDefault(); 335 | 336 | } 337 | 338 | export { FirstPersonControls }; 339 | -------------------------------------------------------------------------------- /examples/jsm/controls/FlyControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | Quaternion, 4 | Vector3 5 | } from '../../../build/three.module.js'; 6 | 7 | const _changeEvent = { type: 'change' }; 8 | 9 | class FlyControls extends EventDispatcher { 10 | 11 | constructor( object, domElement ) { 12 | 13 | super(); 14 | 15 | if ( domElement === undefined ) { 16 | 17 | console.warn( 'THREE.FlyControls: The second parameter "domElement" is now mandatory.' ); 18 | domElement = document; 19 | 20 | } 21 | 22 | this.object = object; 23 | this.domElement = domElement; 24 | 25 | // API 26 | 27 | this.movementSpeed = 1.0; 28 | this.rollSpeed = 0.005; 29 | 30 | this.dragToLook = false; 31 | this.autoForward = false; 32 | 33 | // disable default target object behavior 34 | 35 | // internals 36 | 37 | const scope = this; 38 | 39 | const EPS = 0.000001; 40 | 41 | const lastQuaternion = new Quaternion(); 42 | const lastPosition = new Vector3(); 43 | 44 | this.tmpQuaternion = new Quaternion(); 45 | 46 | this.mouseStatus = 0; 47 | 48 | this.moveState = { up: 0, down: 0, left: 0, right: 0, forward: 0, back: 0, pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0 }; 49 | this.moveVector = new Vector3( 0, 0, 0 ); 50 | this.rotationVector = new Vector3( 0, 0, 0 ); 51 | 52 | this.keydown = function ( event ) { 53 | 54 | if ( event.altKey ) { 55 | 56 | return; 57 | 58 | } 59 | 60 | //event.preventDefault(); 61 | 62 | switch ( event.code ) { 63 | 64 | case 'ShiftLeft': 65 | case 'ShiftRight': this.movementSpeedMultiplier = .1; break; 66 | 67 | case 'KeyW': this.moveState.forward = 1; break; 68 | case 'KeyS': this.moveState.back = 1; break; 69 | 70 | case 'KeyA': this.moveState.left = 1; break; 71 | case 'KeyD': this.moveState.right = 1; break; 72 | 73 | case 'KeyR': this.moveState.up = 1; break; 74 | case 'KeyF': this.moveState.down = 1; break; 75 | 76 | case 'ArrowUp': this.moveState.pitchUp = 1; break; 77 | case 'ArrowDown': this.moveState.pitchDown = 1; break; 78 | 79 | case 'ArrowLeft': this.moveState.yawLeft = 1; break; 80 | case 'ArrowRight': this.moveState.yawRight = 1; break; 81 | 82 | case 'KeyQ': this.moveState.rollLeft = 1; break; 83 | case 'KeyE': this.moveState.rollRight = 1; break; 84 | 85 | } 86 | 87 | this.updateMovementVector(); 88 | this.updateRotationVector(); 89 | 90 | }; 91 | 92 | this.keyup = function ( event ) { 93 | 94 | switch ( event.code ) { 95 | 96 | case 'ShiftLeft': 97 | case 'ShiftRight': this.movementSpeedMultiplier = 1; break; 98 | 99 | case 'KeyW': this.moveState.forward = 0; break; 100 | case 'KeyS': this.moveState.back = 0; break; 101 | 102 | case 'KeyA': this.moveState.left = 0; break; 103 | case 'KeyD': this.moveState.right = 0; break; 104 | 105 | case 'KeyR': this.moveState.up = 0; break; 106 | case 'KeyF': this.moveState.down = 0; break; 107 | 108 | case 'ArrowUp': this.moveState.pitchUp = 0; break; 109 | case 'ArrowDown': this.moveState.pitchDown = 0; break; 110 | 111 | case 'ArrowLeft': this.moveState.yawLeft = 0; break; 112 | case 'ArrowRight': this.moveState.yawRight = 0; break; 113 | 114 | case 'KeyQ': this.moveState.rollLeft = 0; break; 115 | case 'KeyE': this.moveState.rollRight = 0; break; 116 | 117 | } 118 | 119 | this.updateMovementVector(); 120 | this.updateRotationVector(); 121 | 122 | }; 123 | 124 | this.mousedown = function ( event ) { 125 | 126 | if ( this.domElement !== document ) { 127 | 128 | this.domElement.focus(); 129 | 130 | } 131 | 132 | event.preventDefault(); 133 | 134 | if ( this.dragToLook ) { 135 | 136 | this.mouseStatus ++; 137 | 138 | } else { 139 | 140 | switch ( event.button ) { 141 | 142 | case 0: this.moveState.forward = 1; break; 143 | case 2: this.moveState.back = 1; break; 144 | 145 | } 146 | 147 | this.updateMovementVector(); 148 | 149 | } 150 | 151 | }; 152 | 153 | this.mousemove = function ( event ) { 154 | 155 | if ( ! this.dragToLook || this.mouseStatus > 0 ) { 156 | 157 | const container = this.getContainerDimensions(); 158 | const halfWidth = container.size[ 0 ] / 2; 159 | const halfHeight = container.size[ 1 ] / 2; 160 | 161 | this.moveState.yawLeft = - ( ( event.pageX - container.offset[ 0 ] ) - halfWidth ) / halfWidth; 162 | this.moveState.pitchDown = ( ( event.pageY - container.offset[ 1 ] ) - halfHeight ) / halfHeight; 163 | 164 | this.updateRotationVector(); 165 | 166 | } 167 | 168 | }; 169 | 170 | this.mouseup = function ( event ) { 171 | 172 | event.preventDefault(); 173 | 174 | if ( this.dragToLook ) { 175 | 176 | this.mouseStatus --; 177 | 178 | this.moveState.yawLeft = this.moveState.pitchDown = 0; 179 | 180 | } else { 181 | 182 | switch ( event.button ) { 183 | 184 | case 0: this.moveState.forward = 0; break; 185 | case 2: this.moveState.back = 0; break; 186 | 187 | } 188 | 189 | this.updateMovementVector(); 190 | 191 | } 192 | 193 | this.updateRotationVector(); 194 | 195 | }; 196 | 197 | this.update = function ( delta ) { 198 | 199 | const moveMult = delta * scope.movementSpeed; 200 | const rotMult = delta * scope.rollSpeed; 201 | 202 | scope.object.translateX( scope.moveVector.x * moveMult ); 203 | scope.object.translateY( scope.moveVector.y * moveMult ); 204 | scope.object.translateZ( scope.moveVector.z * moveMult ); 205 | 206 | scope.tmpQuaternion.set( scope.rotationVector.x * rotMult, scope.rotationVector.y * rotMult, scope.rotationVector.z * rotMult, 1 ).normalize(); 207 | scope.object.quaternion.multiply( scope.tmpQuaternion ); 208 | 209 | if ( 210 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 211 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS 212 | ) { 213 | 214 | scope.dispatchEvent( _changeEvent ); 215 | lastQuaternion.copy( scope.object.quaternion ); 216 | lastPosition.copy( scope.object.position ); 217 | 218 | } 219 | 220 | }; 221 | 222 | this.updateMovementVector = function () { 223 | 224 | const forward = ( this.moveState.forward || ( this.autoForward && ! this.moveState.back ) ) ? 1 : 0; 225 | 226 | this.moveVector.x = ( - this.moveState.left + this.moveState.right ); 227 | this.moveVector.y = ( - this.moveState.down + this.moveState.up ); 228 | this.moveVector.z = ( - forward + this.moveState.back ); 229 | 230 | //console.log( 'move:', [ this.moveVector.x, this.moveVector.y, this.moveVector.z ] ); 231 | 232 | }; 233 | 234 | this.updateRotationVector = function () { 235 | 236 | this.rotationVector.x = ( - this.moveState.pitchDown + this.moveState.pitchUp ); 237 | this.rotationVector.y = ( - this.moveState.yawRight + this.moveState.yawLeft ); 238 | this.rotationVector.z = ( - this.moveState.rollRight + this.moveState.rollLeft ); 239 | 240 | //console.log( 'rotate:', [ this.rotationVector.x, this.rotationVector.y, this.rotationVector.z ] ); 241 | 242 | }; 243 | 244 | this.getContainerDimensions = function () { 245 | 246 | if ( this.domElement != document ) { 247 | 248 | return { 249 | size: [ this.domElement.offsetWidth, this.domElement.offsetHeight ], 250 | offset: [ this.domElement.offsetLeft, this.domElement.offsetTop ] 251 | }; 252 | 253 | } else { 254 | 255 | return { 256 | size: [ window.innerWidth, window.innerHeight ], 257 | offset: [ 0, 0 ] 258 | }; 259 | 260 | } 261 | 262 | }; 263 | 264 | this.dispose = function () { 265 | 266 | this.domElement.removeEventListener( 'contextmenu', contextmenu ); 267 | this.domElement.removeEventListener( 'mousedown', _mousedown ); 268 | this.domElement.removeEventListener( 'mousemove', _mousemove ); 269 | this.domElement.removeEventListener( 'mouseup', _mouseup ); 270 | 271 | window.removeEventListener( 'keydown', _keydown ); 272 | window.removeEventListener( 'keyup', _keyup ); 273 | 274 | }; 275 | 276 | const _mousemove = this.mousemove.bind( this ); 277 | const _mousedown = this.mousedown.bind( this ); 278 | const _mouseup = this.mouseup.bind( this ); 279 | const _keydown = this.keydown.bind( this ); 280 | const _keyup = this.keyup.bind( this ); 281 | 282 | this.domElement.addEventListener( 'contextmenu', contextmenu ); 283 | 284 | this.domElement.addEventListener( 'mousemove', _mousemove ); 285 | this.domElement.addEventListener( 'mousedown', _mousedown ); 286 | this.domElement.addEventListener( 'mouseup', _mouseup ); 287 | 288 | window.addEventListener( 'keydown', _keydown ); 289 | window.addEventListener( 'keyup', _keyup ); 290 | 291 | this.updateMovementVector(); 292 | this.updateRotationVector(); 293 | 294 | } 295 | 296 | } 297 | 298 | function contextmenu( event ) { 299 | 300 | event.preventDefault(); 301 | 302 | } 303 | 304 | export { FlyControls }; 305 | -------------------------------------------------------------------------------- /examples/jsm/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3 9 | } from '../../../build/three.module.js'; 10 | 11 | // This set of controls performs orbiting, dollying (zooming), and panning. 12 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 13 | // 14 | // Orbit - left mouse / touch: one-finger move 15 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 16 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 17 | 18 | const _changeEvent = { type: 'change' }; 19 | const _startEvent = { type: 'start' }; 20 | const _endEvent = { type: 'end' }; 21 | 22 | class OrbitControls extends EventDispatcher { 23 | 24 | constructor( object, domElement ) { 25 | 26 | super(); 27 | 28 | if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' ); 29 | if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 30 | 31 | this.object = object; 32 | this.domElement = domElement; 33 | 34 | // Set to false to disable this control 35 | this.enabled = true; 36 | 37 | // "target" sets the location of focus, where the object orbits around 38 | this.target = new Vector3(); 39 | 40 | // How far you can dolly in and out ( PerspectiveCamera only ) 41 | this.minDistance = 0; 42 | this.maxDistance = Infinity; 43 | 44 | // How far you can zoom in and out ( OrthographicCamera only ) 45 | this.minZoom = 0; 46 | this.maxZoom = Infinity; 47 | 48 | // How far you can orbit vertically, upper and lower limits. 49 | // Range is 0 to Math.PI radians. 50 | this.minPolarAngle = 0; // radians 51 | this.maxPolarAngle = Math.PI; // radians 52 | 53 | // How far you can orbit horizontally, upper and lower limits. 54 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 55 | this.minAzimuthAngle = - Infinity; // radians 56 | this.maxAzimuthAngle = Infinity; // radians 57 | 58 | // Set to true to enable damping (inertia) 59 | // If damping is enabled, you must call controls.update() in your animation loop 60 | this.enableDamping = false; 61 | this.dampingFactor = 0.05; 62 | 63 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 64 | // Set to false to disable zooming 65 | this.enableZoom = true; 66 | this.zoomSpeed = 1.0; 67 | 68 | // Set to false to disable rotating 69 | this.enableRotate = true; 70 | this.rotateSpeed = 1.0; 71 | 72 | // Set to false to disable panning 73 | this.enablePan = true; 74 | this.panSpeed = 1.0; 75 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 76 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 77 | 78 | // Set to true to automatically rotate around the target 79 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 80 | this.autoRotate = false; 81 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 82 | 83 | // The four arrow keys 84 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 85 | 86 | // Mouse buttons 87 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 88 | 89 | // Touch fingers 90 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 91 | 92 | // for reset 93 | this.target0 = this.target.clone(); 94 | this.position0 = this.object.position.clone(); 95 | this.zoom0 = this.object.zoom; 96 | 97 | // the target DOM element for key events 98 | this._domElementKeyEvents = null; 99 | 100 | // 101 | // public methods 102 | // 103 | 104 | this.getPolarAngle = function () { 105 | 106 | return spherical.phi; 107 | 108 | }; 109 | 110 | this.getAzimuthalAngle = function () { 111 | 112 | return spherical.theta; 113 | 114 | }; 115 | 116 | this.listenToKeyEvents = function ( domElement ) { 117 | 118 | domElement.addEventListener( 'keydown', onKeyDown ); 119 | this._domElementKeyEvents = domElement; 120 | 121 | }; 122 | 123 | this.saveState = function () { 124 | 125 | scope.target0.copy( scope.target ); 126 | scope.position0.copy( scope.object.position ); 127 | scope.zoom0 = scope.object.zoom; 128 | 129 | }; 130 | 131 | this.reset = function () { 132 | 133 | scope.target.copy( scope.target0 ); 134 | scope.object.position.copy( scope.position0 ); 135 | scope.object.zoom = scope.zoom0; 136 | 137 | scope.object.updateProjectionMatrix(); 138 | scope.dispatchEvent( _changeEvent ); 139 | 140 | scope.update(); 141 | 142 | state = STATE.NONE; 143 | 144 | }; 145 | 146 | // this method is exposed, but perhaps it would be better if we can make it private... 147 | this.update = function () { 148 | 149 | const offset = new Vector3(); 150 | 151 | // so camera.up is the orbit axis 152 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 153 | const quatInverse = quat.clone().invert(); 154 | 155 | const lastPosition = new Vector3(); 156 | const lastQuaternion = new Quaternion(); 157 | 158 | const twoPI = 2 * Math.PI; 159 | 160 | return function update() { 161 | 162 | const position = scope.object.position; 163 | 164 | offset.copy( position ).sub( scope.target ); 165 | 166 | // rotate offset to "y-axis-is-up" space 167 | offset.applyQuaternion( quat ); 168 | 169 | // angle from z-axis around y-axis 170 | spherical.setFromVector3( offset ); 171 | 172 | if ( scope.autoRotate && state === STATE.NONE ) { 173 | 174 | rotateLeft( getAutoRotationAngle() ); 175 | 176 | } 177 | 178 | if ( scope.enableDamping ) { 179 | 180 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 181 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 182 | 183 | } else { 184 | 185 | spherical.theta += sphericalDelta.theta; 186 | spherical.phi += sphericalDelta.phi; 187 | 188 | } 189 | 190 | // restrict theta to be between desired limits 191 | 192 | let min = scope.minAzimuthAngle; 193 | let max = scope.maxAzimuthAngle; 194 | 195 | if ( isFinite( min ) && isFinite( max ) ) { 196 | 197 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 198 | 199 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 200 | 201 | if ( min <= max ) { 202 | 203 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 204 | 205 | } else { 206 | 207 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 208 | Math.max( min, spherical.theta ) : 209 | Math.min( max, spherical.theta ); 210 | 211 | } 212 | 213 | } 214 | 215 | // restrict phi to be between desired limits 216 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 217 | 218 | spherical.makeSafe(); 219 | 220 | 221 | spherical.radius *= scale; 222 | 223 | // restrict radius to be between desired limits 224 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 225 | 226 | // move target to panned location 227 | 228 | if ( scope.enableDamping === true ) { 229 | 230 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 231 | 232 | } else { 233 | 234 | scope.target.add( panOffset ); 235 | 236 | } 237 | 238 | offset.setFromSpherical( spherical ); 239 | 240 | // rotate offset back to "camera-up-vector-is-up" space 241 | offset.applyQuaternion( quatInverse ); 242 | 243 | position.copy( scope.target ).add( offset ); 244 | 245 | scope.object.lookAt( scope.target ); 246 | 247 | if ( scope.enableDamping === true ) { 248 | 249 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 250 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 251 | 252 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 253 | 254 | } else { 255 | 256 | sphericalDelta.set( 0, 0, 0 ); 257 | 258 | panOffset.set( 0, 0, 0 ); 259 | 260 | } 261 | 262 | scale = 1; 263 | 264 | // update condition is: 265 | // min(camera displacement, camera rotation in radians)^2 > EPS 266 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 267 | 268 | if ( zoomChanged || 269 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 270 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 271 | 272 | scope.dispatchEvent( _changeEvent ); 273 | 274 | lastPosition.copy( scope.object.position ); 275 | lastQuaternion.copy( scope.object.quaternion ); 276 | zoomChanged = false; 277 | 278 | return true; 279 | 280 | } 281 | 282 | return false; 283 | 284 | }; 285 | 286 | }(); 287 | 288 | this.dispose = function () { 289 | 290 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 291 | 292 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 293 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 294 | 295 | scope.domElement.removeEventListener( 'touchstart', onTouchStart ); 296 | scope.domElement.removeEventListener( 'touchend', onTouchEnd ); 297 | scope.domElement.removeEventListener( 'touchmove', onTouchMove ); 298 | 299 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 300 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 301 | 302 | 303 | if ( scope._domElementKeyEvents !== null ) { 304 | 305 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 306 | 307 | } 308 | 309 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 310 | 311 | }; 312 | 313 | // 314 | // internals 315 | // 316 | 317 | const scope = this; 318 | 319 | const STATE = { 320 | NONE: - 1, 321 | ROTATE: 0, 322 | DOLLY: 1, 323 | PAN: 2, 324 | TOUCH_ROTATE: 3, 325 | TOUCH_PAN: 4, 326 | TOUCH_DOLLY_PAN: 5, 327 | TOUCH_DOLLY_ROTATE: 6 328 | }; 329 | 330 | let state = STATE.NONE; 331 | 332 | const EPS = 0.000001; 333 | 334 | // current position in spherical coordinates 335 | const spherical = new Spherical(); 336 | const sphericalDelta = new Spherical(); 337 | 338 | let scale = 1; 339 | const panOffset = new Vector3(); 340 | let zoomChanged = false; 341 | 342 | const rotateStart = new Vector2(); 343 | const rotateEnd = new Vector2(); 344 | const rotateDelta = new Vector2(); 345 | 346 | const panStart = new Vector2(); 347 | const panEnd = new Vector2(); 348 | const panDelta = new Vector2(); 349 | 350 | const dollyStart = new Vector2(); 351 | const dollyEnd = new Vector2(); 352 | const dollyDelta = new Vector2(); 353 | 354 | function getAutoRotationAngle() { 355 | 356 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 357 | 358 | } 359 | 360 | function getZoomScale() { 361 | 362 | return Math.pow( 0.95, scope.zoomSpeed ); 363 | 364 | } 365 | 366 | function rotateLeft( angle ) { 367 | 368 | sphericalDelta.theta -= angle; 369 | 370 | } 371 | 372 | function rotateUp( angle ) { 373 | 374 | sphericalDelta.phi -= angle; 375 | 376 | } 377 | 378 | const panLeft = function () { 379 | 380 | const v = new Vector3(); 381 | 382 | return function panLeft( distance, objectMatrix ) { 383 | 384 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 385 | v.multiplyScalar( - distance ); 386 | 387 | panOffset.add( v ); 388 | 389 | }; 390 | 391 | }(); 392 | 393 | const panUp = function () { 394 | 395 | const v = new Vector3(); 396 | 397 | return function panUp( distance, objectMatrix ) { 398 | 399 | if ( scope.screenSpacePanning === true ) { 400 | 401 | v.setFromMatrixColumn( objectMatrix, 1 ); 402 | 403 | } else { 404 | 405 | v.setFromMatrixColumn( objectMatrix, 0 ); 406 | v.crossVectors( scope.object.up, v ); 407 | 408 | } 409 | 410 | v.multiplyScalar( distance ); 411 | 412 | panOffset.add( v ); 413 | 414 | }; 415 | 416 | }(); 417 | 418 | // deltaX and deltaY are in pixels; right and down are positive 419 | const pan = function () { 420 | 421 | const offset = new Vector3(); 422 | 423 | return function pan( deltaX, deltaY ) { 424 | 425 | const element = scope.domElement; 426 | 427 | if ( scope.object.isPerspectiveCamera ) { 428 | 429 | // perspective 430 | const position = scope.object.position; 431 | offset.copy( position ).sub( scope.target ); 432 | let targetDistance = offset.length(); 433 | 434 | // half of the fov is center to top of screen 435 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 436 | 437 | // we use only clientHeight here so aspect ratio does not distort speed 438 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 439 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 440 | 441 | } else if ( scope.object.isOrthographicCamera ) { 442 | 443 | // orthographic 444 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 445 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 446 | 447 | } else { 448 | 449 | // camera neither orthographic nor perspective 450 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 451 | scope.enablePan = false; 452 | 453 | } 454 | 455 | }; 456 | 457 | }(); 458 | 459 | function dollyOut( dollyScale ) { 460 | 461 | if ( scope.object.isPerspectiveCamera ) { 462 | 463 | scale /= dollyScale; 464 | 465 | } else if ( scope.object.isOrthographicCamera ) { 466 | 467 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 468 | scope.object.updateProjectionMatrix(); 469 | zoomChanged = true; 470 | 471 | } else { 472 | 473 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 474 | scope.enableZoom = false; 475 | 476 | } 477 | 478 | } 479 | 480 | function dollyIn( dollyScale ) { 481 | 482 | if ( scope.object.isPerspectiveCamera ) { 483 | 484 | scale *= dollyScale; 485 | 486 | } else if ( scope.object.isOrthographicCamera ) { 487 | 488 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 489 | scope.object.updateProjectionMatrix(); 490 | zoomChanged = true; 491 | 492 | } else { 493 | 494 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 495 | scope.enableZoom = false; 496 | 497 | } 498 | 499 | } 500 | 501 | // 502 | // event callbacks - update the object state 503 | // 504 | 505 | function handleMouseDownRotate( event ) { 506 | 507 | rotateStart.set( event.clientX, event.clientY ); 508 | 509 | } 510 | 511 | function handleMouseDownDolly( event ) { 512 | 513 | dollyStart.set( event.clientX, event.clientY ); 514 | 515 | } 516 | 517 | function handleMouseDownPan( event ) { 518 | 519 | panStart.set( event.clientX, event.clientY ); 520 | 521 | } 522 | 523 | function handleMouseMoveRotate( event ) { 524 | 525 | rotateEnd.set( event.clientX, event.clientY ); 526 | 527 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 528 | 529 | const element = scope.domElement; 530 | 531 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 532 | 533 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 534 | 535 | rotateStart.copy( rotateEnd ); 536 | 537 | scope.update(); 538 | 539 | } 540 | 541 | function handleMouseMoveDolly( event ) { 542 | 543 | dollyEnd.set( event.clientX, event.clientY ); 544 | 545 | dollyDelta.subVectors( dollyEnd, dollyStart ); 546 | 547 | if ( dollyDelta.y > 0 ) { 548 | 549 | dollyOut( getZoomScale() ); 550 | 551 | } else if ( dollyDelta.y < 0 ) { 552 | 553 | dollyIn( getZoomScale() ); 554 | 555 | } 556 | 557 | dollyStart.copy( dollyEnd ); 558 | 559 | scope.update(); 560 | 561 | } 562 | 563 | function handleMouseMovePan( event ) { 564 | 565 | panEnd.set( event.clientX, event.clientY ); 566 | 567 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 568 | 569 | pan( panDelta.x, panDelta.y ); 570 | 571 | panStart.copy( panEnd ); 572 | 573 | scope.update(); 574 | 575 | } 576 | 577 | function handleMouseUp( /*event*/ ) { 578 | 579 | // no-op 580 | 581 | } 582 | 583 | function handleMouseWheel( event ) { 584 | 585 | if ( event.deltaY < 0 ) { 586 | 587 | dollyIn( getZoomScale() ); 588 | 589 | } else if ( event.deltaY > 0 ) { 590 | 591 | dollyOut( getZoomScale() ); 592 | 593 | } 594 | 595 | scope.update(); 596 | 597 | } 598 | 599 | function handleKeyDown( event ) { 600 | 601 | let needsUpdate = false; 602 | 603 | switch ( event.code ) { 604 | 605 | case scope.keys.UP: 606 | pan( 0, scope.keyPanSpeed ); 607 | needsUpdate = true; 608 | break; 609 | 610 | case scope.keys.BOTTOM: 611 | pan( 0, - scope.keyPanSpeed ); 612 | needsUpdate = true; 613 | break; 614 | 615 | case scope.keys.LEFT: 616 | pan( scope.keyPanSpeed, 0 ); 617 | needsUpdate = true; 618 | break; 619 | 620 | case scope.keys.RIGHT: 621 | pan( - scope.keyPanSpeed, 0 ); 622 | needsUpdate = true; 623 | break; 624 | 625 | } 626 | 627 | if ( needsUpdate ) { 628 | 629 | // prevent the browser from scrolling on cursor keys 630 | event.preventDefault(); 631 | 632 | scope.update(); 633 | 634 | } 635 | 636 | 637 | } 638 | 639 | function handleTouchStartRotate( event ) { 640 | 641 | if ( event.touches.length == 1 ) { 642 | 643 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 644 | 645 | } else { 646 | 647 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 648 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 649 | 650 | rotateStart.set( x, y ); 651 | 652 | } 653 | 654 | } 655 | 656 | function handleTouchStartPan( event ) { 657 | 658 | if ( event.touches.length == 1 ) { 659 | 660 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 661 | 662 | } else { 663 | 664 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 665 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 666 | 667 | panStart.set( x, y ); 668 | 669 | } 670 | 671 | } 672 | 673 | function handleTouchStartDolly( event ) { 674 | 675 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 676 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 677 | 678 | const distance = Math.sqrt( dx * dx + dy * dy ); 679 | 680 | dollyStart.set( 0, distance ); 681 | 682 | } 683 | 684 | function handleTouchStartDollyPan( event ) { 685 | 686 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 687 | 688 | if ( scope.enablePan ) handleTouchStartPan( event ); 689 | 690 | } 691 | 692 | function handleTouchStartDollyRotate( event ) { 693 | 694 | if ( scope.enableZoom ) handleTouchStartDolly( event ); 695 | 696 | if ( scope.enableRotate ) handleTouchStartRotate( event ); 697 | 698 | } 699 | 700 | function handleTouchMoveRotate( event ) { 701 | 702 | if ( event.touches.length == 1 ) { 703 | 704 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 705 | 706 | } else { 707 | 708 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 709 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 710 | 711 | rotateEnd.set( x, y ); 712 | 713 | } 714 | 715 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 716 | 717 | const element = scope.domElement; 718 | 719 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 720 | 721 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 722 | 723 | rotateStart.copy( rotateEnd ); 724 | 725 | } 726 | 727 | function handleTouchMovePan( event ) { 728 | 729 | if ( event.touches.length == 1 ) { 730 | 731 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 732 | 733 | } else { 734 | 735 | const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 736 | const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 737 | 738 | panEnd.set( x, y ); 739 | 740 | } 741 | 742 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 743 | 744 | pan( panDelta.x, panDelta.y ); 745 | 746 | panStart.copy( panEnd ); 747 | 748 | } 749 | 750 | function handleTouchMoveDolly( event ) { 751 | 752 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 753 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 754 | 755 | const distance = Math.sqrt( dx * dx + dy * dy ); 756 | 757 | dollyEnd.set( 0, distance ); 758 | 759 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 760 | 761 | dollyOut( dollyDelta.y ); 762 | 763 | dollyStart.copy( dollyEnd ); 764 | 765 | } 766 | 767 | function handleTouchMoveDollyPan( event ) { 768 | 769 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 770 | 771 | if ( scope.enablePan ) handleTouchMovePan( event ); 772 | 773 | } 774 | 775 | function handleTouchMoveDollyRotate( event ) { 776 | 777 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 778 | 779 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 780 | 781 | } 782 | 783 | function handleTouchEnd( /*event*/ ) { 784 | 785 | // no-op 786 | 787 | } 788 | 789 | // 790 | // event handlers - FSM: listen for events and reset state 791 | // 792 | 793 | function onPointerDown( event ) { 794 | 795 | if ( scope.enabled === false ) return; 796 | 797 | switch ( event.pointerType ) { 798 | 799 | case 'mouse': 800 | case 'pen': 801 | onMouseDown( event ); 802 | break; 803 | 804 | // TODO touch 805 | 806 | } 807 | 808 | } 809 | 810 | function onPointerMove( event ) { 811 | 812 | if ( scope.enabled === false ) return; 813 | 814 | switch ( event.pointerType ) { 815 | 816 | case 'mouse': 817 | case 'pen': 818 | onMouseMove( event ); 819 | break; 820 | 821 | // TODO touch 822 | 823 | } 824 | 825 | } 826 | 827 | function onPointerUp( event ) { 828 | 829 | switch ( event.pointerType ) { 830 | 831 | case 'mouse': 832 | case 'pen': 833 | onMouseUp( event ); 834 | break; 835 | 836 | // TODO touch 837 | 838 | } 839 | 840 | } 841 | 842 | function onMouseDown( event ) { 843 | 844 | // Prevent the browser from scrolling. 845 | event.preventDefault(); 846 | 847 | // Manually set the focus since calling preventDefault above 848 | // prevents the browser from setting it automatically. 849 | 850 | scope.domElement.focus ? scope.domElement.focus() : window.focus(); 851 | 852 | let mouseAction; 853 | 854 | switch ( event.button ) { 855 | 856 | case 0: 857 | 858 | mouseAction = scope.mouseButtons.LEFT; 859 | break; 860 | 861 | case 1: 862 | 863 | mouseAction = scope.mouseButtons.MIDDLE; 864 | break; 865 | 866 | case 2: 867 | 868 | mouseAction = scope.mouseButtons.RIGHT; 869 | break; 870 | 871 | default: 872 | 873 | mouseAction = - 1; 874 | 875 | } 876 | 877 | switch ( mouseAction ) { 878 | 879 | case MOUSE.DOLLY: 880 | 881 | if ( scope.enableZoom === false ) return; 882 | 883 | handleMouseDownDolly( event ); 884 | 885 | state = STATE.DOLLY; 886 | 887 | break; 888 | 889 | case MOUSE.ROTATE: 890 | 891 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 892 | 893 | if ( scope.enablePan === false ) return; 894 | 895 | handleMouseDownPan( event ); 896 | 897 | state = STATE.PAN; 898 | 899 | } else { 900 | 901 | if ( scope.enableRotate === false ) return; 902 | 903 | handleMouseDownRotate( event ); 904 | 905 | state = STATE.ROTATE; 906 | 907 | } 908 | 909 | break; 910 | 911 | case MOUSE.PAN: 912 | 913 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 914 | 915 | if ( scope.enableRotate === false ) return; 916 | 917 | handleMouseDownRotate( event ); 918 | 919 | state = STATE.ROTATE; 920 | 921 | } else { 922 | 923 | if ( scope.enablePan === false ) return; 924 | 925 | handleMouseDownPan( event ); 926 | 927 | state = STATE.PAN; 928 | 929 | } 930 | 931 | break; 932 | 933 | default: 934 | 935 | state = STATE.NONE; 936 | 937 | } 938 | 939 | if ( state !== STATE.NONE ) { 940 | 941 | scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); 942 | scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); 943 | 944 | scope.dispatchEvent( _startEvent ); 945 | 946 | } 947 | 948 | } 949 | 950 | function onMouseMove( event ) { 951 | 952 | if ( scope.enabled === false ) return; 953 | 954 | event.preventDefault(); 955 | 956 | switch ( state ) { 957 | 958 | case STATE.ROTATE: 959 | 960 | if ( scope.enableRotate === false ) return; 961 | 962 | handleMouseMoveRotate( event ); 963 | 964 | break; 965 | 966 | case STATE.DOLLY: 967 | 968 | if ( scope.enableZoom === false ) return; 969 | 970 | handleMouseMoveDolly( event ); 971 | 972 | break; 973 | 974 | case STATE.PAN: 975 | 976 | if ( scope.enablePan === false ) return; 977 | 978 | handleMouseMovePan( event ); 979 | 980 | break; 981 | 982 | } 983 | 984 | } 985 | 986 | function onMouseUp( event ) { 987 | 988 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 989 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 990 | 991 | if ( scope.enabled === false ) return; 992 | 993 | handleMouseUp( event ); 994 | 995 | scope.dispatchEvent( _endEvent ); 996 | 997 | state = STATE.NONE; 998 | 999 | } 1000 | 1001 | function onMouseWheel( event ) { 1002 | 1003 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 1004 | 1005 | event.preventDefault(); 1006 | 1007 | scope.dispatchEvent( _startEvent ); 1008 | 1009 | handleMouseWheel( event ); 1010 | 1011 | scope.dispatchEvent( _endEvent ); 1012 | 1013 | } 1014 | 1015 | function onKeyDown( event ) { 1016 | 1017 | if ( scope.enabled === false || scope.enablePan === false ) return; 1018 | 1019 | handleKeyDown( event ); 1020 | 1021 | } 1022 | 1023 | function onTouchStart( event ) { 1024 | 1025 | if ( scope.enabled === false ) return; 1026 | 1027 | event.preventDefault(); // prevent scrolling 1028 | 1029 | switch ( event.touches.length ) { 1030 | 1031 | case 1: 1032 | 1033 | switch ( scope.touches.ONE ) { 1034 | 1035 | case TOUCH.ROTATE: 1036 | 1037 | if ( scope.enableRotate === false ) return; 1038 | 1039 | handleTouchStartRotate( event ); 1040 | 1041 | state = STATE.TOUCH_ROTATE; 1042 | 1043 | break; 1044 | 1045 | case TOUCH.PAN: 1046 | 1047 | if ( scope.enablePan === false ) return; 1048 | 1049 | handleTouchStartPan( event ); 1050 | 1051 | state = STATE.TOUCH_PAN; 1052 | 1053 | break; 1054 | 1055 | default: 1056 | 1057 | state = STATE.NONE; 1058 | 1059 | } 1060 | 1061 | break; 1062 | 1063 | case 2: 1064 | 1065 | switch ( scope.touches.TWO ) { 1066 | 1067 | case TOUCH.DOLLY_PAN: 1068 | 1069 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1070 | 1071 | handleTouchStartDollyPan( event ); 1072 | 1073 | state = STATE.TOUCH_DOLLY_PAN; 1074 | 1075 | break; 1076 | 1077 | case TOUCH.DOLLY_ROTATE: 1078 | 1079 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1080 | 1081 | handleTouchStartDollyRotate( event ); 1082 | 1083 | state = STATE.TOUCH_DOLLY_ROTATE; 1084 | 1085 | break; 1086 | 1087 | default: 1088 | 1089 | state = STATE.NONE; 1090 | 1091 | } 1092 | 1093 | break; 1094 | 1095 | default: 1096 | 1097 | state = STATE.NONE; 1098 | 1099 | } 1100 | 1101 | if ( state !== STATE.NONE ) { 1102 | 1103 | scope.dispatchEvent( _startEvent ); 1104 | 1105 | } 1106 | 1107 | } 1108 | 1109 | function onTouchMove( event ) { 1110 | 1111 | if ( scope.enabled === false ) return; 1112 | 1113 | event.preventDefault(); // prevent scrolling 1114 | 1115 | switch ( state ) { 1116 | 1117 | case STATE.TOUCH_ROTATE: 1118 | 1119 | if ( scope.enableRotate === false ) return; 1120 | 1121 | handleTouchMoveRotate( event ); 1122 | 1123 | scope.update(); 1124 | 1125 | break; 1126 | 1127 | case STATE.TOUCH_PAN: 1128 | 1129 | if ( scope.enablePan === false ) return; 1130 | 1131 | handleTouchMovePan( event ); 1132 | 1133 | scope.update(); 1134 | 1135 | break; 1136 | 1137 | case STATE.TOUCH_DOLLY_PAN: 1138 | 1139 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1140 | 1141 | handleTouchMoveDollyPan( event ); 1142 | 1143 | scope.update(); 1144 | 1145 | break; 1146 | 1147 | case STATE.TOUCH_DOLLY_ROTATE: 1148 | 1149 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1150 | 1151 | handleTouchMoveDollyRotate( event ); 1152 | 1153 | scope.update(); 1154 | 1155 | break; 1156 | 1157 | default: 1158 | 1159 | state = STATE.NONE; 1160 | 1161 | } 1162 | 1163 | } 1164 | 1165 | function onTouchEnd( event ) { 1166 | 1167 | if ( scope.enabled === false ) return; 1168 | 1169 | handleTouchEnd( event ); 1170 | 1171 | scope.dispatchEvent( _endEvent ); 1172 | 1173 | state = STATE.NONE; 1174 | 1175 | } 1176 | 1177 | function onContextMenu( event ) { 1178 | 1179 | if ( scope.enabled === false ) return; 1180 | 1181 | event.preventDefault(); 1182 | 1183 | } 1184 | 1185 | // 1186 | 1187 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1188 | 1189 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1190 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1191 | 1192 | scope.domElement.addEventListener( 'touchstart', onTouchStart, { passive: false } ); 1193 | scope.domElement.addEventListener( 'touchend', onTouchEnd ); 1194 | scope.domElement.addEventListener( 'touchmove', onTouchMove, { passive: false } ); 1195 | 1196 | // force an update at start 1197 | 1198 | this.update(); 1199 | 1200 | } 1201 | 1202 | } 1203 | 1204 | 1205 | // This set of controls performs orbiting, dollying (zooming), and panning. 1206 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 1207 | // This is very similar to OrbitControls, another set of touch behavior 1208 | // 1209 | // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate 1210 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 1211 | // Pan - left mouse, or arrow keys / touch: one-finger move 1212 | 1213 | class MapControls extends OrbitControls { 1214 | 1215 | constructor( object, domElement ) { 1216 | 1217 | super( object, domElement ); 1218 | 1219 | this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up 1220 | 1221 | this.mouseButtons.LEFT = MOUSE.PAN; 1222 | this.mouseButtons.RIGHT = MOUSE.ROTATE; 1223 | 1224 | this.touches.ONE = TOUCH.PAN; 1225 | this.touches.TWO = TOUCH.DOLLY_ROTATE; 1226 | 1227 | } 1228 | 1229 | } 1230 | 1231 | export { OrbitControls, MapControls }; 1232 | -------------------------------------------------------------------------------- /examples/jsm/controls/PointerLockControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | Euler, 3 | EventDispatcher, 4 | Vector3 5 | } from '../../../build/three.module.js'; 6 | 7 | const _euler = new Euler( 0, 0, 0, 'YXZ' ); 8 | const _vector = new Vector3(); 9 | 10 | const _changeEvent = { type: 'change' }; 11 | const _lockEvent = { type: 'lock' }; 12 | const _unlockEvent = { type: 'unlock' }; 13 | 14 | const _PI_2 = Math.PI / 2; 15 | 16 | class PointerLockControls extends EventDispatcher { 17 | 18 | constructor( camera, domElement ) { 19 | 20 | super(); 21 | 22 | if ( domElement === undefined ) { 23 | 24 | console.warn( 'THREE.PointerLockControls: The second parameter "domElement" is now mandatory.' ); 25 | domElement = document.body; 26 | 27 | } 28 | 29 | this.domElement = domElement; 30 | this.isLocked = false; 31 | 32 | // Set to constrain the pitch of the camera 33 | // Range is 0 to Math.PI radians 34 | this.minPolarAngle = 0; // radians 35 | this.maxPolarAngle = Math.PI; // radians 36 | 37 | const scope = this; 38 | 39 | function onMouseMove( event ) { 40 | 41 | if ( scope.isLocked === false ) return; 42 | 43 | const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0; 44 | const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0; 45 | 46 | _euler.setFromQuaternion( camera.quaternion ); 47 | 48 | _euler.y -= movementX * 0.002; 49 | _euler.x -= movementY * 0.002; 50 | 51 | _euler.x = Math.max( _PI_2 - scope.maxPolarAngle, Math.min( _PI_2 - scope.minPolarAngle, _euler.x ) ); 52 | 53 | camera.quaternion.setFromEuler( _euler ); 54 | 55 | scope.dispatchEvent( _changeEvent ); 56 | 57 | } 58 | 59 | function onPointerlockChange() { 60 | 61 | if ( scope.domElement.ownerDocument.pointerLockElement === scope.domElement ) { 62 | 63 | scope.dispatchEvent( _lockEvent ); 64 | 65 | scope.isLocked = true; 66 | 67 | } else { 68 | 69 | scope.dispatchEvent( _unlockEvent ); 70 | 71 | scope.isLocked = false; 72 | 73 | } 74 | 75 | } 76 | 77 | function onPointerlockError() { 78 | 79 | console.error( 'THREE.PointerLockControls: Unable to use Pointer Lock API' ); 80 | 81 | } 82 | 83 | this.connect = function () { 84 | 85 | scope.domElement.ownerDocument.addEventListener( 'mousemove', onMouseMove ); 86 | scope.domElement.ownerDocument.addEventListener( 'pointerlockchange', onPointerlockChange ); 87 | scope.domElement.ownerDocument.addEventListener( 'pointerlockerror', onPointerlockError ); 88 | 89 | }; 90 | 91 | this.disconnect = function () { 92 | 93 | scope.domElement.ownerDocument.removeEventListener( 'mousemove', onMouseMove ); 94 | scope.domElement.ownerDocument.removeEventListener( 'pointerlockchange', onPointerlockChange ); 95 | scope.domElement.ownerDocument.removeEventListener( 'pointerlockerror', onPointerlockError ); 96 | 97 | }; 98 | 99 | this.dispose = function () { 100 | 101 | this.disconnect(); 102 | 103 | }; 104 | 105 | this.getObject = function () { // retaining this method for backward compatibility 106 | 107 | return camera; 108 | 109 | }; 110 | 111 | this.getDirection = function () { 112 | 113 | const direction = new Vector3( 0, 0, - 1 ); 114 | 115 | return function ( v ) { 116 | 117 | return v.copy( direction ).applyQuaternion( camera.quaternion ); 118 | 119 | }; 120 | 121 | }(); 122 | 123 | this.moveForward = function ( distance ) { 124 | 125 | // move forward parallel to the xz-plane 126 | // assumes camera.up is y-up 127 | 128 | _vector.setFromMatrixColumn( camera.matrix, 0 ); 129 | 130 | _vector.crossVectors( camera.up, _vector ); 131 | 132 | camera.position.addScaledVector( _vector, distance ); 133 | 134 | }; 135 | 136 | this.moveRight = function ( distance ) { 137 | 138 | _vector.setFromMatrixColumn( camera.matrix, 0 ); 139 | 140 | camera.position.addScaledVector( _vector, distance ); 141 | 142 | }; 143 | 144 | this.lock = function () { 145 | 146 | this.domElement.requestPointerLock(); 147 | 148 | }; 149 | 150 | this.unlock = function () { 151 | 152 | scope.domElement.ownerDocument.exitPointerLock(); 153 | 154 | }; 155 | 156 | this.connect(); 157 | 158 | } 159 | 160 | } 161 | 162 | export { PointerLockControls }; 163 | -------------------------------------------------------------------------------- /examples/jsm/controls/TrackballControls.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Vector2, 6 | Vector3 7 | } from '../../../build/three.module.js'; 8 | 9 | const _changeEvent = { type: 'change' }; 10 | const _startEvent = { type: 'start' }; 11 | const _endEvent = { type: 'end' }; 12 | 13 | class TrackballControls extends EventDispatcher { 14 | 15 | constructor( object, domElement ) { 16 | 17 | super(); 18 | 19 | if ( domElement === undefined ) console.warn( 'THREE.TrackballControls: The second parameter "domElement" is now mandatory.' ); 20 | if ( domElement === document ) console.error( 'THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' ); 21 | 22 | const scope = this; 23 | const STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 }; 24 | 25 | this.object = object; 26 | this.domElement = domElement; 27 | 28 | // API 29 | 30 | this.enabled = true; 31 | 32 | this.screen = { left: 0, top: 0, width: 0, height: 0 }; 33 | 34 | this.rotateSpeed = 1.0; 35 | this.zoomSpeed = 1.2; 36 | this.panSpeed = 0.3; 37 | 38 | this.noRotate = false; 39 | this.noZoom = false; 40 | this.noPan = false; 41 | 42 | this.staticMoving = false; 43 | this.dynamicDampingFactor = 0.2; 44 | 45 | this.minDistance = 0; 46 | this.maxDistance = Infinity; 47 | 48 | this.keys = [ 'KeyA' /*A*/, 'KeyS' /*S*/, 'KeyD' /*D*/ ]; 49 | 50 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 51 | 52 | // internals 53 | 54 | this.target = new Vector3(); 55 | 56 | const EPS = 0.000001; 57 | 58 | const lastPosition = new Vector3(); 59 | let lastZoom = 1; 60 | 61 | let _state = STATE.NONE, 62 | _keyState = STATE.NONE, 63 | 64 | _touchZoomDistanceStart = 0, 65 | _touchZoomDistanceEnd = 0, 66 | 67 | _lastAngle = 0; 68 | 69 | const _eye = new Vector3(), 70 | 71 | _movePrev = new Vector2(), 72 | _moveCurr = new Vector2(), 73 | 74 | _lastAxis = new Vector3(), 75 | 76 | _zoomStart = new Vector2(), 77 | _zoomEnd = new Vector2(), 78 | 79 | _panStart = new Vector2(), 80 | _panEnd = new Vector2(); 81 | 82 | // for reset 83 | 84 | this.target0 = this.target.clone(); 85 | this.position0 = this.object.position.clone(); 86 | this.up0 = this.object.up.clone(); 87 | this.zoom0 = this.object.zoom; 88 | 89 | // methods 90 | 91 | this.handleResize = function () { 92 | 93 | const box = scope.domElement.getBoundingClientRect(); 94 | // adjustments come from similar code in the jquery offset() function 95 | const d = scope.domElement.ownerDocument.documentElement; 96 | scope.screen.left = box.left + window.pageXOffset - d.clientLeft; 97 | scope.screen.top = box.top + window.pageYOffset - d.clientTop; 98 | scope.screen.width = box.width; 99 | scope.screen.height = box.height; 100 | 101 | }; 102 | 103 | const getMouseOnScreen = ( function () { 104 | 105 | const vector = new Vector2(); 106 | 107 | return function getMouseOnScreen( pageX, pageY ) { 108 | 109 | vector.set( 110 | ( pageX - scope.screen.left ) / scope.screen.width, 111 | ( pageY - scope.screen.top ) / scope.screen.height 112 | ); 113 | 114 | return vector; 115 | 116 | }; 117 | 118 | }() ); 119 | 120 | const getMouseOnCircle = ( function () { 121 | 122 | const vector = new Vector2(); 123 | 124 | return function getMouseOnCircle( pageX, pageY ) { 125 | 126 | vector.set( 127 | ( ( pageX - scope.screen.width * 0.5 - scope.screen.left ) / ( scope.screen.width * 0.5 ) ), 128 | ( ( scope.screen.height + 2 * ( scope.screen.top - pageY ) ) / scope.screen.width ) // screen.width intentional 129 | ); 130 | 131 | return vector; 132 | 133 | }; 134 | 135 | }() ); 136 | 137 | this.rotateCamera = ( function () { 138 | 139 | const axis = new Vector3(), 140 | quaternion = new Quaternion(), 141 | eyeDirection = new Vector3(), 142 | objectUpDirection = new Vector3(), 143 | objectSidewaysDirection = new Vector3(), 144 | moveDirection = new Vector3(); 145 | 146 | return function rotateCamera() { 147 | 148 | moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 ); 149 | let angle = moveDirection.length(); 150 | 151 | if ( angle ) { 152 | 153 | _eye.copy( scope.object.position ).sub( scope.target ); 154 | 155 | eyeDirection.copy( _eye ).normalize(); 156 | objectUpDirection.copy( scope.object.up ).normalize(); 157 | objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize(); 158 | 159 | objectUpDirection.setLength( _moveCurr.y - _movePrev.y ); 160 | objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x ); 161 | 162 | moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) ); 163 | 164 | axis.crossVectors( moveDirection, _eye ).normalize(); 165 | 166 | angle *= scope.rotateSpeed; 167 | quaternion.setFromAxisAngle( axis, angle ); 168 | 169 | _eye.applyQuaternion( quaternion ); 170 | scope.object.up.applyQuaternion( quaternion ); 171 | 172 | _lastAxis.copy( axis ); 173 | _lastAngle = angle; 174 | 175 | } else if ( ! scope.staticMoving && _lastAngle ) { 176 | 177 | _lastAngle *= Math.sqrt( 1.0 - scope.dynamicDampingFactor ); 178 | _eye.copy( scope.object.position ).sub( scope.target ); 179 | quaternion.setFromAxisAngle( _lastAxis, _lastAngle ); 180 | _eye.applyQuaternion( quaternion ); 181 | scope.object.up.applyQuaternion( quaternion ); 182 | 183 | } 184 | 185 | _movePrev.copy( _moveCurr ); 186 | 187 | }; 188 | 189 | }() ); 190 | 191 | 192 | this.zoomCamera = function () { 193 | 194 | let factor; 195 | 196 | if ( _state === STATE.TOUCH_ZOOM_PAN ) { 197 | 198 | factor = _touchZoomDistanceStart / _touchZoomDistanceEnd; 199 | _touchZoomDistanceStart = _touchZoomDistanceEnd; 200 | 201 | if ( scope.object.isPerspectiveCamera ) { 202 | 203 | _eye.multiplyScalar( factor ); 204 | 205 | } else if ( scope.object.isOrthographicCamera ) { 206 | 207 | scope.object.zoom *= factor; 208 | scope.object.updateProjectionMatrix(); 209 | 210 | } else { 211 | 212 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 213 | 214 | } 215 | 216 | } else { 217 | 218 | factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * scope.zoomSpeed; 219 | 220 | if ( factor !== 1.0 && factor > 0.0 ) { 221 | 222 | if ( scope.object.isPerspectiveCamera ) { 223 | 224 | _eye.multiplyScalar( factor ); 225 | 226 | } else if ( scope.object.isOrthographicCamera ) { 227 | 228 | scope.object.zoom /= factor; 229 | scope.object.updateProjectionMatrix(); 230 | 231 | } else { 232 | 233 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 234 | 235 | } 236 | 237 | } 238 | 239 | if ( scope.staticMoving ) { 240 | 241 | _zoomStart.copy( _zoomEnd ); 242 | 243 | } else { 244 | 245 | _zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor; 246 | 247 | } 248 | 249 | } 250 | 251 | }; 252 | 253 | this.panCamera = ( function () { 254 | 255 | const mouseChange = new Vector2(), 256 | objectUp = new Vector3(), 257 | pan = new Vector3(); 258 | 259 | return function panCamera() { 260 | 261 | mouseChange.copy( _panEnd ).sub( _panStart ); 262 | 263 | if ( mouseChange.lengthSq() ) { 264 | 265 | if ( scope.object.isOrthographicCamera ) { 266 | 267 | const scale_x = ( scope.object.right - scope.object.left ) / scope.object.zoom / scope.domElement.clientWidth; 268 | const scale_y = ( scope.object.top - scope.object.bottom ) / scope.object.zoom / scope.domElement.clientWidth; 269 | 270 | mouseChange.x *= scale_x; 271 | mouseChange.y *= scale_y; 272 | 273 | } 274 | 275 | mouseChange.multiplyScalar( _eye.length() * scope.panSpeed ); 276 | 277 | pan.copy( _eye ).cross( scope.object.up ).setLength( mouseChange.x ); 278 | pan.add( objectUp.copy( scope.object.up ).setLength( mouseChange.y ) ); 279 | 280 | scope.object.position.add( pan ); 281 | scope.target.add( pan ); 282 | 283 | if ( scope.staticMoving ) { 284 | 285 | _panStart.copy( _panEnd ); 286 | 287 | } else { 288 | 289 | _panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( scope.dynamicDampingFactor ) ); 290 | 291 | } 292 | 293 | } 294 | 295 | }; 296 | 297 | }() ); 298 | 299 | this.checkDistances = function () { 300 | 301 | if ( ! scope.noZoom || ! scope.noPan ) { 302 | 303 | if ( _eye.lengthSq() > scope.maxDistance * scope.maxDistance ) { 304 | 305 | scope.object.position.addVectors( scope.target, _eye.setLength( scope.maxDistance ) ); 306 | _zoomStart.copy( _zoomEnd ); 307 | 308 | } 309 | 310 | if ( _eye.lengthSq() < scope.minDistance * scope.minDistance ) { 311 | 312 | scope.object.position.addVectors( scope.target, _eye.setLength( scope.minDistance ) ); 313 | _zoomStart.copy( _zoomEnd ); 314 | 315 | } 316 | 317 | } 318 | 319 | }; 320 | 321 | this.update = function () { 322 | 323 | _eye.subVectors( scope.object.position, scope.target ); 324 | 325 | if ( ! scope.noRotate ) { 326 | 327 | scope.rotateCamera(); 328 | 329 | } 330 | 331 | if ( ! scope.noZoom ) { 332 | 333 | scope.zoomCamera(); 334 | 335 | } 336 | 337 | if ( ! scope.noPan ) { 338 | 339 | scope.panCamera(); 340 | 341 | } 342 | 343 | scope.object.position.addVectors( scope.target, _eye ); 344 | 345 | if ( scope.object.isPerspectiveCamera ) { 346 | 347 | scope.checkDistances(); 348 | 349 | scope.object.lookAt( scope.target ); 350 | 351 | if ( lastPosition.distanceToSquared( scope.object.position ) > EPS ) { 352 | 353 | scope.dispatchEvent( _changeEvent ); 354 | 355 | lastPosition.copy( scope.object.position ); 356 | 357 | } 358 | 359 | } else if ( scope.object.isOrthographicCamera ) { 360 | 361 | scope.object.lookAt( scope.target ); 362 | 363 | if ( lastPosition.distanceToSquared( scope.object.position ) > EPS || lastZoom !== scope.object.zoom ) { 364 | 365 | scope.dispatchEvent( _changeEvent ); 366 | 367 | lastPosition.copy( scope.object.position ); 368 | lastZoom = scope.object.zoom; 369 | 370 | } 371 | 372 | } else { 373 | 374 | console.warn( 'THREE.TrackballControls: Unsupported camera type' ); 375 | 376 | } 377 | 378 | }; 379 | 380 | this.reset = function () { 381 | 382 | _state = STATE.NONE; 383 | _keyState = STATE.NONE; 384 | 385 | scope.target.copy( scope.target0 ); 386 | scope.object.position.copy( scope.position0 ); 387 | scope.object.up.copy( scope.up0 ); 388 | scope.object.zoom = scope.zoom0; 389 | 390 | scope.object.updateProjectionMatrix(); 391 | 392 | _eye.subVectors( scope.object.position, scope.target ); 393 | 394 | scope.object.lookAt( scope.target ); 395 | 396 | scope.dispatchEvent( _changeEvent ); 397 | 398 | lastPosition.copy( scope.object.position ); 399 | lastZoom = scope.object.zoom; 400 | 401 | }; 402 | 403 | // listeners 404 | 405 | function onPointerDown( event ) { 406 | 407 | if ( scope.enabled === false ) return; 408 | 409 | switch ( event.pointerType ) { 410 | 411 | case 'mouse': 412 | case 'pen': 413 | onMouseDown( event ); 414 | break; 415 | 416 | // TODO touch 417 | 418 | } 419 | 420 | } 421 | 422 | function onPointerMove( event ) { 423 | 424 | if ( scope.enabled === false ) return; 425 | 426 | switch ( event.pointerType ) { 427 | 428 | case 'mouse': 429 | case 'pen': 430 | onMouseMove( event ); 431 | break; 432 | 433 | // TODO touch 434 | 435 | } 436 | 437 | } 438 | 439 | function onPointerUp( event ) { 440 | 441 | if ( scope.enabled === false ) return; 442 | 443 | switch ( event.pointerType ) { 444 | 445 | case 'mouse': 446 | case 'pen': 447 | onMouseUp( event ); 448 | break; 449 | 450 | // TODO touch 451 | 452 | } 453 | 454 | } 455 | 456 | function keydown( event ) { 457 | 458 | if ( scope.enabled === false ) return; 459 | 460 | window.removeEventListener( 'keydown', keydown ); 461 | 462 | if ( _keyState !== STATE.NONE ) { 463 | 464 | return; 465 | 466 | } else if ( event.code === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) { 467 | 468 | _keyState = STATE.ROTATE; 469 | 470 | } else if ( event.code === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) { 471 | 472 | _keyState = STATE.ZOOM; 473 | 474 | } else if ( event.code === scope.keys[ STATE.PAN ] && ! scope.noPan ) { 475 | 476 | _keyState = STATE.PAN; 477 | 478 | } 479 | 480 | } 481 | 482 | function keyup() { 483 | 484 | if ( scope.enabled === false ) return; 485 | 486 | _keyState = STATE.NONE; 487 | 488 | window.addEventListener( 'keydown', keydown ); 489 | 490 | } 491 | 492 | function onMouseDown( event ) { 493 | 494 | event.preventDefault(); 495 | 496 | if ( _state === STATE.NONE ) { 497 | 498 | switch ( event.button ) { 499 | 500 | case scope.mouseButtons.LEFT: 501 | _state = STATE.ROTATE; 502 | break; 503 | 504 | case scope.mouseButtons.MIDDLE: 505 | _state = STATE.ZOOM; 506 | break; 507 | 508 | case scope.mouseButtons.RIGHT: 509 | _state = STATE.PAN; 510 | break; 511 | 512 | default: 513 | _state = STATE.NONE; 514 | 515 | } 516 | 517 | } 518 | 519 | const state = ( _keyState !== STATE.NONE ) ? _keyState : _state; 520 | 521 | if ( state === STATE.ROTATE && ! scope.noRotate ) { 522 | 523 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 524 | _movePrev.copy( _moveCurr ); 525 | 526 | } else if ( state === STATE.ZOOM && ! scope.noZoom ) { 527 | 528 | _zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 529 | _zoomEnd.copy( _zoomStart ); 530 | 531 | } else if ( state === STATE.PAN && ! scope.noPan ) { 532 | 533 | _panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 534 | _panEnd.copy( _panStart ); 535 | 536 | } 537 | 538 | scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); 539 | scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); 540 | 541 | scope.dispatchEvent( _startEvent ); 542 | 543 | } 544 | 545 | function onMouseMove( event ) { 546 | 547 | if ( scope.enabled === false ) return; 548 | 549 | event.preventDefault(); 550 | 551 | const state = ( _keyState !== STATE.NONE ) ? _keyState : _state; 552 | 553 | if ( state === STATE.ROTATE && ! scope.noRotate ) { 554 | 555 | _movePrev.copy( _moveCurr ); 556 | _moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) ); 557 | 558 | } else if ( state === STATE.ZOOM && ! scope.noZoom ) { 559 | 560 | _zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 561 | 562 | } else if ( state === STATE.PAN && ! scope.noPan ) { 563 | 564 | _panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) ); 565 | 566 | } 567 | 568 | } 569 | 570 | function onMouseUp( event ) { 571 | 572 | if ( scope.enabled === false ) return; 573 | 574 | event.preventDefault(); 575 | 576 | _state = STATE.NONE; 577 | 578 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 579 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 580 | 581 | scope.dispatchEvent( _endEvent ); 582 | 583 | } 584 | 585 | function mousewheel( event ) { 586 | 587 | if ( scope.enabled === false ) return; 588 | 589 | if ( scope.noZoom === true ) return; 590 | 591 | event.preventDefault(); 592 | 593 | switch ( event.deltaMode ) { 594 | 595 | case 2: 596 | // Zoom in pages 597 | _zoomStart.y -= event.deltaY * 0.025; 598 | break; 599 | 600 | case 1: 601 | // Zoom in lines 602 | _zoomStart.y -= event.deltaY * 0.01; 603 | break; 604 | 605 | default: 606 | // undefined, 0, assume pixels 607 | _zoomStart.y -= event.deltaY * 0.00025; 608 | break; 609 | 610 | } 611 | 612 | scope.dispatchEvent( _startEvent ); 613 | scope.dispatchEvent( _endEvent ); 614 | 615 | } 616 | 617 | function touchstart( event ) { 618 | 619 | if ( scope.enabled === false ) return; 620 | 621 | event.preventDefault(); 622 | 623 | switch ( event.touches.length ) { 624 | 625 | case 1: 626 | _state = STATE.TOUCH_ROTATE; 627 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 628 | _movePrev.copy( _moveCurr ); 629 | break; 630 | 631 | default: // 2 or more 632 | _state = STATE.TOUCH_ZOOM_PAN; 633 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 634 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 635 | _touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy ); 636 | 637 | const x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 638 | const y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 639 | _panStart.copy( getMouseOnScreen( x, y ) ); 640 | _panEnd.copy( _panStart ); 641 | break; 642 | 643 | } 644 | 645 | scope.dispatchEvent( _startEvent ); 646 | 647 | } 648 | 649 | function touchmove( event ) { 650 | 651 | if ( scope.enabled === false ) return; 652 | 653 | event.preventDefault(); 654 | 655 | switch ( event.touches.length ) { 656 | 657 | case 1: 658 | _movePrev.copy( _moveCurr ); 659 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 660 | break; 661 | 662 | default: // 2 or more 663 | const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 664 | const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 665 | _touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy ); 666 | 667 | const x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2; 668 | const y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2; 669 | _panEnd.copy( getMouseOnScreen( x, y ) ); 670 | break; 671 | 672 | } 673 | 674 | } 675 | 676 | function touchend( event ) { 677 | 678 | if ( scope.enabled === false ) return; 679 | 680 | switch ( event.touches.length ) { 681 | 682 | case 0: 683 | _state = STATE.NONE; 684 | break; 685 | 686 | case 1: 687 | _state = STATE.TOUCH_ROTATE; 688 | _moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) ); 689 | _movePrev.copy( _moveCurr ); 690 | break; 691 | 692 | } 693 | 694 | scope.dispatchEvent( _endEvent ); 695 | 696 | } 697 | 698 | function contextmenu( event ) { 699 | 700 | if ( scope.enabled === false ) return; 701 | 702 | event.preventDefault(); 703 | 704 | } 705 | 706 | this.dispose = function () { 707 | 708 | scope.domElement.removeEventListener( 'contextmenu', contextmenu ); 709 | 710 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 711 | scope.domElement.removeEventListener( 'wheel', mousewheel ); 712 | 713 | scope.domElement.removeEventListener( 'touchstart', touchstart ); 714 | scope.domElement.removeEventListener( 'touchend', touchend ); 715 | scope.domElement.removeEventListener( 'touchmove', touchmove ); 716 | 717 | scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove ); 718 | scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp ); 719 | 720 | window.removeEventListener( 'keydown', keydown ); 721 | window.removeEventListener( 'keyup', keyup ); 722 | 723 | }; 724 | 725 | this.domElement.addEventListener( 'contextmenu', contextmenu ); 726 | 727 | this.domElement.addEventListener( 'pointerdown', onPointerDown ); 728 | this.domElement.addEventListener( 'wheel', mousewheel, { passive: false } ); 729 | 730 | this.domElement.addEventListener( 'touchstart', touchstart, { passive: false } ); 731 | this.domElement.addEventListener( 'touchend', touchend ); 732 | this.domElement.addEventListener( 'touchmove', touchmove, { passive: false } ); 733 | 734 | this.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove ); 735 | this.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp ); 736 | 737 | window.addEventListener( 'keydown', keydown ); 738 | window.addEventListener( 'keyup', keyup ); 739 | 740 | this.handleResize(); 741 | 742 | // force an update at start 743 | this.update(); 744 | 745 | } 746 | 747 | } 748 | 749 | export { TrackballControls }; 750 | -------------------------------------------------------------------------------- /examples/jsm/geometries/BoxLineGeometry.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, 3 | Float32BufferAttribute 4 | } from '../../../build/three.module.js'; 5 | 6 | class BoxLineGeometry extends BufferGeometry { 7 | 8 | constructor( width = 1, height = 1, depth = 1, widthSegments = 1, heightSegments = 1, depthSegments = 1 ) { 9 | 10 | super(); 11 | 12 | widthSegments = Math.floor( widthSegments ); 13 | heightSegments = Math.floor( heightSegments ); 14 | depthSegments = Math.floor( depthSegments ); 15 | 16 | const widthHalf = width / 2; 17 | const heightHalf = height / 2; 18 | const depthHalf = depth / 2; 19 | 20 | const segmentWidth = width / widthSegments; 21 | const segmentHeight = height / heightSegments; 22 | const segmentDepth = depth / depthSegments; 23 | 24 | const vertices = []; 25 | 26 | let x = - widthHalf; 27 | let y = - heightHalf; 28 | let z = - depthHalf; 29 | 30 | for ( let i = 0; i <= widthSegments; i ++ ) { 31 | 32 | vertices.push( x, - heightHalf, - depthHalf, x, heightHalf, - depthHalf ); 33 | vertices.push( x, heightHalf, - depthHalf, x, heightHalf, depthHalf ); 34 | vertices.push( x, heightHalf, depthHalf, x, - heightHalf, depthHalf ); 35 | vertices.push( x, - heightHalf, depthHalf, x, - heightHalf, - depthHalf ); 36 | 37 | x += segmentWidth; 38 | 39 | } 40 | 41 | for ( let i = 0; i <= heightSegments; i ++ ) { 42 | 43 | vertices.push( - widthHalf, y, - depthHalf, widthHalf, y, - depthHalf ); 44 | vertices.push( widthHalf, y, - depthHalf, widthHalf, y, depthHalf ); 45 | vertices.push( widthHalf, y, depthHalf, - widthHalf, y, depthHalf ); 46 | vertices.push( - widthHalf, y, depthHalf, - widthHalf, y, - depthHalf ); 47 | 48 | y += segmentHeight; 49 | 50 | } 51 | 52 | for ( let i = 0; i <= depthSegments; i ++ ) { 53 | 54 | vertices.push( - widthHalf, - heightHalf, z, - widthHalf, heightHalf, z ); 55 | vertices.push( - widthHalf, heightHalf, z, widthHalf, heightHalf, z ); 56 | vertices.push( widthHalf, heightHalf, z, widthHalf, - heightHalf, z ); 57 | vertices.push( widthHalf, - heightHalf, z, - widthHalf, - heightHalf, z ); 58 | 59 | z += segmentDepth; 60 | 61 | } 62 | 63 | this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); 64 | 65 | } 66 | 67 | } 68 | 69 | export { BoxLineGeometry }; 70 | -------------------------------------------------------------------------------- /examples/jsm/geometries/ConvexGeometry.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, 3 | Float32BufferAttribute 4 | } from '../../../build/three.module.js'; 5 | import { ConvexHull } from '../math/ConvexHull.js'; 6 | 7 | class ConvexGeometry extends BufferGeometry { 8 | 9 | constructor( points ) { 10 | 11 | super(); 12 | 13 | // buffers 14 | 15 | const vertices = []; 16 | const normals = []; 17 | 18 | if ( ConvexHull === undefined ) { 19 | 20 | console.error( 'THREE.ConvexBufferGeometry: ConvexBufferGeometry relies on ConvexHull' ); 21 | 22 | } 23 | 24 | const convexHull = new ConvexHull().setFromPoints( points ); 25 | 26 | // generate vertices and normals 27 | 28 | const faces = convexHull.faces; 29 | 30 | for ( let i = 0; i < faces.length; i ++ ) { 31 | 32 | const face = faces[ i ]; 33 | let edge = face.edge; 34 | 35 | // we move along a doubly-connected edge list to access all face points (see HalfEdge docs) 36 | 37 | do { 38 | 39 | const point = edge.head().point; 40 | 41 | vertices.push( point.x, point.y, point.z ); 42 | normals.push( face.normal.x, face.normal.y, face.normal.z ); 43 | 44 | edge = edge.next; 45 | 46 | } while ( edge !== face.edge ); 47 | 48 | } 49 | 50 | // build geometry 51 | 52 | this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); 53 | this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); 54 | 55 | } 56 | 57 | } 58 | 59 | export { ConvexGeometry }; 60 | -------------------------------------------------------------------------------- /examples/jsm/geometries/DecalGeometry.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry, 3 | Float32BufferAttribute, 4 | Matrix4, 5 | Vector3 6 | } from '../../../build/three.module.js'; 7 | 8 | /** 9 | * You can use this geometry to create a decal mesh, that serves different kinds of purposes. 10 | * e.g. adding unique details to models, performing dynamic visual environmental changes or covering seams. 11 | * 12 | * Constructor parameter: 13 | * 14 | * mesh — Any mesh object 15 | * position — Position of the decal projector 16 | * orientation — Orientation of the decal projector 17 | * size — Size of the decal projector 18 | * 19 | * reference: http://blog.wolfire.com/2009/06/how-to-project-decals/ 20 | * 21 | */ 22 | 23 | class DecalGeometry extends BufferGeometry { 24 | 25 | constructor( mesh, position, orientation, size ) { 26 | 27 | super(); 28 | 29 | // buffers 30 | 31 | const vertices = []; 32 | const normals = []; 33 | const uvs = []; 34 | 35 | // helpers 36 | 37 | const plane = new Vector3(); 38 | 39 | // this matrix represents the transformation of the decal projector 40 | 41 | const projectorMatrix = new Matrix4(); 42 | projectorMatrix.makeRotationFromEuler( orientation ); 43 | projectorMatrix.setPosition( position ); 44 | 45 | const projectorMatrixInverse = new Matrix4(); 46 | projectorMatrixInverse.copy( projectorMatrix ).invert(); 47 | 48 | // generate buffers 49 | 50 | generate(); 51 | 52 | // build geometry 53 | 54 | this.setAttribute( 'position', new Float32BufferAttribute( vertices, 3 ) ); 55 | this.setAttribute( 'normal', new Float32BufferAttribute( normals, 3 ) ); 56 | this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) ); 57 | 58 | function generate() { 59 | 60 | let decalVertices = []; 61 | 62 | const vertex = new Vector3(); 63 | const normal = new Vector3(); 64 | 65 | // handle different geometry types 66 | 67 | if ( mesh.geometry.isGeometry === true ) { 68 | 69 | console.error( 'THREE.DecalGeometry no longer supports THREE.Geometry. Use BufferGeometry instead.' ); 70 | return; 71 | 72 | } 73 | 74 | const geometry = mesh.geometry; 75 | 76 | const positionAttribute = geometry.attributes.position; 77 | const normalAttribute = geometry.attributes.normal; 78 | 79 | // first, create an array of 'DecalVertex' objects 80 | // three consecutive 'DecalVertex' objects represent a single face 81 | // 82 | // this data structure will be later used to perform the clipping 83 | 84 | if ( geometry.index !== null ) { 85 | 86 | // indexed BufferGeometry 87 | 88 | const index = geometry.index; 89 | 90 | for ( let i = 0; i < index.count; i ++ ) { 91 | 92 | vertex.fromBufferAttribute( positionAttribute, index.getX( i ) ); 93 | normal.fromBufferAttribute( normalAttribute, index.getX( i ) ); 94 | 95 | pushDecalVertex( decalVertices, vertex, normal ); 96 | 97 | } 98 | 99 | } else { 100 | 101 | // non-indexed BufferGeometry 102 | 103 | for ( let i = 0; i < positionAttribute.count; i ++ ) { 104 | 105 | vertex.fromBufferAttribute( positionAttribute, i ); 106 | normal.fromBufferAttribute( normalAttribute, i ); 107 | 108 | pushDecalVertex( decalVertices, vertex, normal ); 109 | 110 | } 111 | 112 | } 113 | 114 | // second, clip the geometry so that it doesn't extend out from the projector 115 | 116 | decalVertices = clipGeometry( decalVertices, plane.set( 1, 0, 0 ) ); 117 | decalVertices = clipGeometry( decalVertices, plane.set( - 1, 0, 0 ) ); 118 | decalVertices = clipGeometry( decalVertices, plane.set( 0, 1, 0 ) ); 119 | decalVertices = clipGeometry( decalVertices, plane.set( 0, - 1, 0 ) ); 120 | decalVertices = clipGeometry( decalVertices, plane.set( 0, 0, 1 ) ); 121 | decalVertices = clipGeometry( decalVertices, plane.set( 0, 0, - 1 ) ); 122 | 123 | // third, generate final vertices, normals and uvs 124 | 125 | for ( let i = 0; i < decalVertices.length; i ++ ) { 126 | 127 | const decalVertex = decalVertices[ i ]; 128 | 129 | // create texture coordinates (we are still in projector space) 130 | 131 | uvs.push( 132 | 0.5 + ( decalVertex.position.x / size.x ), 133 | 0.5 + ( decalVertex.position.y / size.y ) 134 | ); 135 | 136 | // transform the vertex back to world space 137 | 138 | decalVertex.position.applyMatrix4( projectorMatrix ); 139 | 140 | // now create vertex and normal buffer data 141 | 142 | vertices.push( decalVertex.position.x, decalVertex.position.y, decalVertex.position.z ); 143 | normals.push( decalVertex.normal.x, decalVertex.normal.y, decalVertex.normal.z ); 144 | 145 | } 146 | 147 | } 148 | 149 | function pushDecalVertex( decalVertices, vertex, normal ) { 150 | 151 | // transform the vertex to world space, then to projector space 152 | 153 | vertex.applyMatrix4( mesh.matrixWorld ); 154 | vertex.applyMatrix4( projectorMatrixInverse ); 155 | 156 | normal.transformDirection( mesh.matrixWorld ); 157 | 158 | decalVertices.push( new DecalVertex( vertex.clone(), normal.clone() ) ); 159 | 160 | } 161 | 162 | function clipGeometry( inVertices, plane ) { 163 | 164 | const outVertices = []; 165 | 166 | const s = 0.5 * Math.abs( size.dot( plane ) ); 167 | 168 | // a single iteration clips one face, 169 | // which consists of three consecutive 'DecalVertex' objects 170 | 171 | for ( let i = 0; i < inVertices.length; i += 3 ) { 172 | 173 | let total = 0; 174 | let nV1; 175 | let nV2; 176 | let nV3; 177 | let nV4; 178 | 179 | const d1 = inVertices[ i + 0 ].position.dot( plane ) - s; 180 | const d2 = inVertices[ i + 1 ].position.dot( plane ) - s; 181 | const d3 = inVertices[ i + 2 ].position.dot( plane ) - s; 182 | 183 | const v1Out = d1 > 0; 184 | const v2Out = d2 > 0; 185 | const v3Out = d3 > 0; 186 | 187 | // calculate, how many vertices of the face lie outside of the clipping plane 188 | 189 | total = ( v1Out ? 1 : 0 ) + ( v2Out ? 1 : 0 ) + ( v3Out ? 1 : 0 ); 190 | 191 | switch ( total ) { 192 | 193 | case 0: { 194 | 195 | // the entire face lies inside of the plane, no clipping needed 196 | 197 | outVertices.push( inVertices[ i ] ); 198 | outVertices.push( inVertices[ i + 1 ] ); 199 | outVertices.push( inVertices[ i + 2 ] ); 200 | break; 201 | 202 | } 203 | 204 | case 1: { 205 | 206 | // one vertex lies outside of the plane, perform clipping 207 | 208 | if ( v1Out ) { 209 | 210 | nV1 = inVertices[ i + 1 ]; 211 | nV2 = inVertices[ i + 2 ]; 212 | nV3 = clip( inVertices[ i ], nV1, plane, s ); 213 | nV4 = clip( inVertices[ i ], nV2, plane, s ); 214 | 215 | } 216 | 217 | if ( v2Out ) { 218 | 219 | nV1 = inVertices[ i ]; 220 | nV2 = inVertices[ i + 2 ]; 221 | nV3 = clip( inVertices[ i + 1 ], nV1, plane, s ); 222 | nV4 = clip( inVertices[ i + 1 ], nV2, plane, s ); 223 | 224 | outVertices.push( nV3 ); 225 | outVertices.push( nV2.clone() ); 226 | outVertices.push( nV1.clone() ); 227 | 228 | outVertices.push( nV2.clone() ); 229 | outVertices.push( nV3.clone() ); 230 | outVertices.push( nV4 ); 231 | break; 232 | 233 | } 234 | 235 | if ( v3Out ) { 236 | 237 | nV1 = inVertices[ i ]; 238 | nV2 = inVertices[ i + 1 ]; 239 | nV3 = clip( inVertices[ i + 2 ], nV1, plane, s ); 240 | nV4 = clip( inVertices[ i + 2 ], nV2, plane, s ); 241 | 242 | } 243 | 244 | outVertices.push( nV1.clone() ); 245 | outVertices.push( nV2.clone() ); 246 | outVertices.push( nV3 ); 247 | 248 | outVertices.push( nV4 ); 249 | outVertices.push( nV3.clone() ); 250 | outVertices.push( nV2.clone() ); 251 | 252 | break; 253 | 254 | } 255 | 256 | case 2: { 257 | 258 | // two vertices lies outside of the plane, perform clipping 259 | 260 | if ( ! v1Out ) { 261 | 262 | nV1 = inVertices[ i ].clone(); 263 | nV2 = clip( nV1, inVertices[ i + 1 ], plane, s ); 264 | nV3 = clip( nV1, inVertices[ i + 2 ], plane, s ); 265 | outVertices.push( nV1 ); 266 | outVertices.push( nV2 ); 267 | outVertices.push( nV3 ); 268 | 269 | } 270 | 271 | if ( ! v2Out ) { 272 | 273 | nV1 = inVertices[ i + 1 ].clone(); 274 | nV2 = clip( nV1, inVertices[ i + 2 ], plane, s ); 275 | nV3 = clip( nV1, inVertices[ i ], plane, s ); 276 | outVertices.push( nV1 ); 277 | outVertices.push( nV2 ); 278 | outVertices.push( nV3 ); 279 | 280 | } 281 | 282 | if ( ! v3Out ) { 283 | 284 | nV1 = inVertices[ i + 2 ].clone(); 285 | nV2 = clip( nV1, inVertices[ i ], plane, s ); 286 | nV3 = clip( nV1, inVertices[ i + 1 ], plane, s ); 287 | outVertices.push( nV1 ); 288 | outVertices.push( nV2 ); 289 | outVertices.push( nV3 ); 290 | 291 | } 292 | 293 | break; 294 | 295 | } 296 | 297 | case 3: { 298 | 299 | // the entire face lies outside of the plane, so let's discard the corresponding vertices 300 | 301 | break; 302 | 303 | } 304 | 305 | } 306 | 307 | } 308 | 309 | return outVertices; 310 | 311 | } 312 | 313 | function clip( v0, v1, p, s ) { 314 | 315 | const d0 = v0.position.dot( p ) - s; 316 | const d1 = v1.position.dot( p ) - s; 317 | 318 | const s0 = d0 / ( d0 - d1 ); 319 | 320 | const v = new DecalVertex( 321 | new Vector3( 322 | v0.position.x + s0 * ( v1.position.x - v0.position.x ), 323 | v0.position.y + s0 * ( v1.position.y - v0.position.y ), 324 | v0.position.z + s0 * ( v1.position.z - v0.position.z ) 325 | ), 326 | new Vector3( 327 | v0.normal.x + s0 * ( v1.normal.x - v0.normal.x ), 328 | v0.normal.y + s0 * ( v1.normal.y - v0.normal.y ), 329 | v0.normal.z + s0 * ( v1.normal.z - v0.normal.z ) 330 | ) 331 | ); 332 | 333 | // need to clip more values (texture coordinates)? do it this way: 334 | // intersectpoint.value = a.value + s * ( b.value - a.value ); 335 | 336 | return v; 337 | 338 | } 339 | 340 | } 341 | 342 | } 343 | 344 | // helper 345 | 346 | class DecalVertex { 347 | 348 | constructor( position, normal ) { 349 | 350 | this.position = position; 351 | this.normal = normal; 352 | 353 | } 354 | 355 | clone() { 356 | 357 | return new this.constructor( this.position.clone(), this.normal.clone() ); 358 | 359 | } 360 | 361 | } 362 | 363 | export { DecalGeometry, DecalVertex }; 364 | -------------------------------------------------------------------------------- /examples/jsm/geometries/ParametricGeometries.js: -------------------------------------------------------------------------------- 1 | import { 2 | Curve, 3 | ParametricGeometry, 4 | Vector3 5 | } from '../../../build/three.module.js'; 6 | 7 | /** 8 | * Experimenting of primitive geometry creation using Surface Parametric equations 9 | */ 10 | 11 | const ParametricGeometries = { 12 | 13 | klein: function ( v, u, target ) { 14 | 15 | u *= Math.PI; 16 | v *= 2 * Math.PI; 17 | 18 | u = u * 2; 19 | let x, z; 20 | if ( u < Math.PI ) { 21 | 22 | x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( u ) * Math.cos( v ); 23 | z = - 8 * Math.sin( u ) - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( u ) * Math.cos( v ); 24 | 25 | } else { 26 | 27 | x = 3 * Math.cos( u ) * ( 1 + Math.sin( u ) ) + ( 2 * ( 1 - Math.cos( u ) / 2 ) ) * Math.cos( v + Math.PI ); 28 | z = - 8 * Math.sin( u ); 29 | 30 | } 31 | 32 | const y = - 2 * ( 1 - Math.cos( u ) / 2 ) * Math.sin( v ); 33 | 34 | target.set( x, y, z ); 35 | 36 | }, 37 | 38 | plane: function ( width, height ) { 39 | 40 | return function ( u, v, target ) { 41 | 42 | const x = u * width; 43 | const y = 0; 44 | const z = v * height; 45 | 46 | target.set( x, y, z ); 47 | 48 | }; 49 | 50 | }, 51 | 52 | mobius: function ( u, t, target ) { 53 | 54 | // flat mobius strip 55 | // http://www.wolframalpha.com/input/?i=M%C3%B6bius+strip+parametric+equations&lk=1&a=ClashPrefs_*Surface.MoebiusStrip.SurfaceProperty.ParametricEquations- 56 | u = u - 0.5; 57 | const v = 2 * Math.PI * t; 58 | 59 | const a = 2; 60 | 61 | const x = Math.cos( v ) * ( a + u * Math.cos( v / 2 ) ); 62 | const y = Math.sin( v ) * ( a + u * Math.cos( v / 2 ) ); 63 | const z = u * Math.sin( v / 2 ); 64 | 65 | target.set( x, y, z ); 66 | 67 | }, 68 | 69 | mobius3d: function ( u, t, target ) { 70 | 71 | // volumetric mobius strip 72 | 73 | u *= Math.PI; 74 | t *= 2 * Math.PI; 75 | 76 | u = u * 2; 77 | const phi = u / 2; 78 | const major = 2.25, a = 0.125, b = 0.65; 79 | 80 | let x = a * Math.cos( t ) * Math.cos( phi ) - b * Math.sin( t ) * Math.sin( phi ); 81 | const z = a * Math.cos( t ) * Math.sin( phi ) + b * Math.sin( t ) * Math.cos( phi ); 82 | const y = ( major + x ) * Math.sin( u ); 83 | x = ( major + x ) * Math.cos( u ); 84 | 85 | target.set( x, y, z ); 86 | 87 | } 88 | 89 | }; 90 | 91 | 92 | /********************************************* 93 | * 94 | * Parametric Replacement for TubeGeometry 95 | * 96 | *********************************************/ 97 | 98 | ParametricGeometries.TubeGeometry = class TubeGeometry extends ParametricGeometry { 99 | 100 | constructor( path, segments = 64, radius = 1, segmentsRadius = 8, closed = false ) { 101 | 102 | const numpoints = segments + 1; 103 | 104 | const frames = path.computeFrenetFrames( segments, closed ), 105 | tangents = frames.tangents, 106 | normals = frames.normals, 107 | binormals = frames.binormals; 108 | 109 | const position = new Vector3(); 110 | 111 | function ParametricTube( u, v, target ) { 112 | 113 | v *= 2 * Math.PI; 114 | 115 | const i = Math.floor( u * ( numpoints - 1 ) ); 116 | 117 | path.getPointAt( u, position ); 118 | 119 | const normal = normals[ i ]; 120 | const binormal = binormals[ i ]; 121 | 122 | const cx = - radius * Math.cos( v ); // TODO: Hack: Negating it so it faces outside. 123 | const cy = radius * Math.sin( v ); 124 | 125 | position.x += cx * normal.x + cy * binormal.x; 126 | position.y += cx * normal.y + cy * binormal.y; 127 | position.z += cx * normal.z + cy * binormal.z; 128 | 129 | target.copy( position ); 130 | 131 | } 132 | 133 | super( ParametricTube, segments, segmentsRadius ); 134 | 135 | // proxy internals 136 | 137 | this.tangents = tangents; 138 | this.normals = normals; 139 | this.binormals = binormals; 140 | 141 | this.path = path; 142 | this.segments = segments; 143 | this.radius = radius; 144 | this.segmentsRadius = segmentsRadius; 145 | this.closed = closed; 146 | 147 | } 148 | 149 | }; 150 | 151 | 152 | /********************************************* 153 | * 154 | * Parametric Replacement for TorusKnotGeometry 155 | * 156 | *********************************************/ 157 | ParametricGeometries.TorusKnotGeometry = class TorusKnotGeometry extends ParametricGeometries.TubeGeometry { 158 | 159 | constructor( radius = 200, tube = 40, segmentsT = 64, segmentsR = 8, p = 2, q = 3 ) { 160 | 161 | class TorusKnotCurve extends Curve { 162 | 163 | getPoint( t, optionalTarget = new Vector3() ) { 164 | 165 | const point = optionalTarget; 166 | 167 | t *= Math.PI * 2; 168 | 169 | const r = 0.5; 170 | 171 | const x = ( 1 + r * Math.cos( q * t ) ) * Math.cos( p * t ); 172 | const y = ( 1 + r * Math.cos( q * t ) ) * Math.sin( p * t ); 173 | const z = r * Math.sin( q * t ); 174 | 175 | return point.set( x, y, z ).multiplyScalar( radius ); 176 | 177 | } 178 | 179 | } 180 | 181 | const segments = segmentsT; 182 | const radiusSegments = segmentsR; 183 | const extrudePath = new TorusKnotCurve(); 184 | 185 | super( extrudePath, segments, tube, radiusSegments, true, false ); 186 | 187 | this.radius = radius; 188 | this.tube = tube; 189 | this.segmentsT = segmentsT; 190 | this.segmentsR = segmentsR; 191 | this.p = p; 192 | this.q = q; 193 | 194 | } 195 | 196 | }; 197 | 198 | /********************************************* 199 | * 200 | * Parametric Replacement for SphereGeometry 201 | * 202 | *********************************************/ 203 | ParametricGeometries.SphereGeometry = class SphereGeometry extends ParametricGeometry { 204 | 205 | constructor( size, u, v ) { 206 | 207 | function sphere( u, v, target ) { 208 | 209 | u *= Math.PI; 210 | v *= 2 * Math.PI; 211 | 212 | var x = size * Math.sin( u ) * Math.cos( v ); 213 | var y = size * Math.sin( u ) * Math.sin( v ); 214 | var z = size * Math.cos( u ); 215 | 216 | target.set( x, y, z ); 217 | 218 | } 219 | 220 | super( sphere, u, v ); 221 | 222 | } 223 | 224 | }; 225 | 226 | 227 | /********************************************* 228 | * 229 | * Parametric Replacement for PlaneGeometry 230 | * 231 | *********************************************/ 232 | 233 | ParametricGeometries.PlaneGeometry = class PlaneGeometry extends ParametricGeometry { 234 | 235 | constructor( width, depth, segmentsWidth, segmentsDepth ) { 236 | 237 | function plane( u, v, target ) { 238 | 239 | const x = u * width; 240 | const y = 0; 241 | const z = v * depth; 242 | 243 | target.set( x, y, z ); 244 | 245 | } 246 | 247 | super( plane, segmentsWidth, segmentsDepth ); 248 | 249 | } 250 | 251 | }; 252 | 253 | export { ParametricGeometries }; 254 | -------------------------------------------------------------------------------- /examples/jsm/geometries/RoundedBoxGeometry.js: -------------------------------------------------------------------------------- 1 | import { 2 | BoxGeometry, 3 | Vector3 4 | } from '../../../build/three.module.js'; 5 | 6 | const _tempNormal = new Vector3(); 7 | 8 | function getUv( faceDirVector, normal, uvAxis, projectionAxis, radius, sideLength ) { 9 | 10 | const totArcLength = 2 * Math.PI * radius / 4; 11 | 12 | // length of the planes between the arcs on each axis 13 | const centerLength = Math.max( sideLength - 2 * radius, 0 ); 14 | const halfArc = Math.PI / 4; 15 | 16 | // Get the vector projected onto the Y plane 17 | _tempNormal.copy( normal ); 18 | _tempNormal[ projectionAxis ] = 0; 19 | _tempNormal.normalize(); 20 | 21 | // total amount of UV space alloted to a single arc 22 | const arcUvRatio = 0.5 * totArcLength / ( totArcLength + centerLength ); 23 | 24 | // the distance along one arc the point is at 25 | const arcAngleRatio = 1.0 - ( _tempNormal.angleTo( faceDirVector ) / halfArc ); 26 | 27 | if ( Math.sign( _tempNormal[ uvAxis ] ) === 1 ) { 28 | 29 | return arcAngleRatio * arcUvRatio; 30 | 31 | } else { 32 | 33 | // total amount of UV space alloted to the plane between the arcs 34 | const lenUv = centerLength / ( totArcLength + centerLength ); 35 | return lenUv + arcUvRatio + arcUvRatio * ( 1.0 - arcAngleRatio ); 36 | 37 | } 38 | 39 | } 40 | 41 | class RoundedBoxGeometry extends BoxGeometry { 42 | 43 | constructor( width = 1, height = 1, depth = 1, segments = 2, radius = 0.1 ) { 44 | 45 | // ensure segments is odd so we have a plane connecting the rounded corners 46 | segments = segments * 2 + 1; 47 | 48 | // ensure radius isn't bigger than shortest side 49 | radius = Math.min( width / 2, height / 2, depth / 2, radius ); 50 | 51 | super( 1, 1, 1, segments, segments, segments ); 52 | 53 | // if we just have one segment we're the same as a regular box 54 | if ( segments === 1 ) return; 55 | 56 | const geometry2 = this.toNonIndexed(); 57 | 58 | this.index = null; 59 | this.attributes.position = geometry2.attributes.position; 60 | this.attributes.normal = geometry2.attributes.normal; 61 | this.attributes.uv = geometry2.attributes.uv; 62 | 63 | // 64 | 65 | const position = new Vector3(); 66 | const normal = new Vector3(); 67 | 68 | const box = new Vector3( width, height, depth ).divideScalar( 2 ).subScalar( radius ); 69 | 70 | const positions = this.attributes.position.array; 71 | const normals = this.attributes.normal.array; 72 | const uvs = this.attributes.uv.array; 73 | 74 | const faceTris = positions.length / 6; 75 | const faceDirVector = new Vector3(); 76 | const halfSegmentSize = 0.5 / segments; 77 | 78 | for ( let i = 0, j = 0; i < positions.length; i += 3, j += 2 ) { 79 | 80 | position.fromArray( positions, i ); 81 | normal.copy( position ); 82 | normal.x -= Math.sign( normal.x ) * halfSegmentSize; 83 | normal.y -= Math.sign( normal.y ) * halfSegmentSize; 84 | normal.z -= Math.sign( normal.z ) * halfSegmentSize; 85 | normal.normalize(); 86 | 87 | positions[ i + 0 ] = box.x * Math.sign( position.x ) + normal.x * radius; 88 | positions[ i + 1 ] = box.y * Math.sign( position.y ) + normal.y * radius; 89 | positions[ i + 2 ] = box.z * Math.sign( position.z ) + normal.z * radius; 90 | 91 | normals[ i + 0 ] = normal.x; 92 | normals[ i + 1 ] = normal.y; 93 | normals[ i + 2 ] = normal.z; 94 | 95 | const side = Math.floor( i / faceTris ); 96 | 97 | switch ( side ) { 98 | 99 | case 0: // right 100 | 101 | // generate UVs along Z then Y 102 | faceDirVector.set( 1, 0, 0 ); 103 | uvs[ j + 0 ] = getUv( faceDirVector, normal, 'z', 'y', radius, depth ); 104 | uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'z', radius, height ); 105 | break; 106 | 107 | case 1: // left 108 | 109 | // generate UVs along Z then Y 110 | faceDirVector.set( - 1, 0, 0 ); 111 | uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'z', 'y', radius, depth ); 112 | uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'z', radius, height ); 113 | break; 114 | 115 | case 2: // top 116 | 117 | // generate UVs along X then Z 118 | faceDirVector.set( 0, 1, 0 ); 119 | uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'z', radius, width ); 120 | uvs[ j + 1 ] = getUv( faceDirVector, normal, 'z', 'x', radius, depth ); 121 | break; 122 | 123 | case 3: // bottom 124 | 125 | // generate UVs along X then Z 126 | faceDirVector.set( 0, - 1, 0 ); 127 | uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'z', radius, width ); 128 | uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'z', 'x', radius, depth ); 129 | break; 130 | 131 | case 4: // front 132 | 133 | // generate UVs along X then Y 134 | faceDirVector.set( 0, 0, 1 ); 135 | uvs[ j + 0 ] = 1.0 - getUv( faceDirVector, normal, 'x', 'y', radius, width ); 136 | uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'x', radius, height ); 137 | break; 138 | 139 | case 5: // back 140 | 141 | // generate UVs along X then Y 142 | faceDirVector.set( 0, 0, - 1 ); 143 | uvs[ j + 0 ] = getUv( faceDirVector, normal, 'x', 'y', radius, width ); 144 | uvs[ j + 1 ] = 1.0 - getUv( faceDirVector, normal, 'y', 'x', radius, height ); 145 | break; 146 | 147 | } 148 | 149 | } 150 | 151 | } 152 | 153 | } 154 | 155 | export { RoundedBoxGeometry }; 156 | -------------------------------------------------------------------------------- /examples/jsm/geometries/TeapotGeometry.js: -------------------------------------------------------------------------------- 1 | import { 2 | BufferAttribute, 3 | BufferGeometry, 4 | Matrix4, 5 | Vector3, 6 | Vector4 7 | } from '../../../build/three.module.js'; 8 | 9 | /** 10 | * Tessellates the famous Utah teapot database by Martin Newell into triangles. 11 | * 12 | * Parameters: size = 50, segments = 10, bottom = true, lid = true, body = true, 13 | * fitLid = false, blinn = true 14 | * 15 | * size is a relative scale: I've scaled the teapot to fit vertically between -1 and 1. 16 | * Think of it as a "radius". 17 | * segments - number of line segments to subdivide each patch edge; 18 | * 1 is possible but gives degenerates, so two is the real minimum. 19 | * bottom - boolean, if true (default) then the bottom patches are added. Some consider 20 | * adding the bottom heresy, so set this to "false" to adhere to the One True Way. 21 | * lid - to remove the lid and look inside, set to true. 22 | * body - to remove the body and leave the lid, set this and "bottom" to false. 23 | * fitLid - the lid is a tad small in the original. This stretches it a bit so you can't 24 | * see the teapot's insides through the gap. 25 | * blinn - Jim Blinn scaled the original data vertically by dividing by about 1.3 to look 26 | * nicer. If you want to see the original teapot, similar to the real-world model, set 27 | * this to false. True by default. 28 | * See http://en.wikipedia.org/wiki/File:Original_Utah_Teapot.jpg for the original 29 | * real-world teapot (from http://en.wikipedia.org/wiki/Utah_teapot). 30 | * 31 | * Note that the bottom (the last four patches) is not flat - blame Frank Crow, not me. 32 | * 33 | * The teapot should normally be rendered as a double sided object, since for some 34 | * patches both sides can be seen, e.g., the gap around the lid and inside the spout. 35 | * 36 | * Segments 'n' determines the number of triangles output. 37 | * Total triangles = 32*2*n*n - 8*n [degenerates at the top and bottom cusps are deleted] 38 | * 39 | * size_factor # triangles 40 | * 1 56 41 | * 2 240 42 | * 3 552 43 | * 4 992 44 | * 45 | * 10 6320 46 | * 20 25440 47 | * 30 57360 48 | * 49 | * Code converted from my ancient SPD software, http://tog.acm.org/resources/SPD/ 50 | * Created for the Udacity course "Interactive Rendering", http://bit.ly/ericity 51 | * Lesson: https://www.udacity.com/course/viewer#!/c-cs291/l-68866048/m-106482448 52 | * YouTube video on teapot history: https://www.youtube.com/watch?v=DxMfblPzFNc 53 | * 54 | * See https://en.wikipedia.org/wiki/Utah_teapot for the history of the teapot 55 | * 56 | */ 57 | 58 | class TeapotGeometry extends BufferGeometry { 59 | 60 | constructor( size = 50, segments = 10, bottom = true, lid = true, body = true, fitLid = true, blinn = true ) { 61 | 62 | // 32 * 4 * 4 Bezier spline patches 63 | const teapotPatches = [ 64 | /*rim*/ 65 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 66 | 3, 16, 17, 18, 7, 19, 20, 21, 11, 22, 23, 24, 15, 25, 26, 27, 67 | 18, 28, 29, 30, 21, 31, 32, 33, 24, 34, 35, 36, 27, 37, 38, 39, 68 | 30, 40, 41, 0, 33, 42, 43, 4, 36, 44, 45, 8, 39, 46, 47, 12, 69 | /*body*/ 70 | 12, 13, 14, 15, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 71 | 15, 25, 26, 27, 51, 60, 61, 62, 55, 63, 64, 65, 59, 66, 67, 68, 72 | 27, 37, 38, 39, 62, 69, 70, 71, 65, 72, 73, 74, 68, 75, 76, 77, 73 | 39, 46, 47, 12, 71, 78, 79, 48, 74, 80, 81, 52, 77, 82, 83, 56, 74 | 56, 57, 58, 59, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 75 | 59, 66, 67, 68, 87, 96, 97, 98, 91, 99, 100, 101, 95, 102, 103, 104, 76 | 68, 75, 76, 77, 98, 105, 106, 107, 101, 108, 109, 110, 104, 111, 112, 113, 77 | 77, 82, 83, 56, 107, 114, 115, 84, 110, 116, 117, 88, 113, 118, 119, 92, 78 | /*handle*/ 79 | 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 80 | 123, 136, 137, 120, 127, 138, 139, 124, 131, 140, 141, 128, 135, 142, 143, 132, 81 | 132, 133, 134, 135, 144, 145, 146, 147, 148, 149, 150, 151, 68, 152, 153, 154, 82 | 135, 142, 143, 132, 147, 155, 156, 144, 151, 157, 158, 148, 154, 159, 160, 68, 83 | /*spout*/ 84 | 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 85 | 164, 177, 178, 161, 168, 179, 180, 165, 172, 181, 182, 169, 176, 183, 184, 173, 86 | 173, 174, 175, 176, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 87 | 176, 183, 184, 173, 188, 197, 198, 185, 192, 199, 200, 189, 196, 201, 202, 193, 88 | /*lid*/ 89 | 203, 203, 203, 203, 204, 205, 206, 207, 208, 208, 208, 208, 209, 210, 211, 212, 90 | 203, 203, 203, 203, 207, 213, 214, 215, 208, 208, 208, 208, 212, 216, 217, 218, 91 | 203, 203, 203, 203, 215, 219, 220, 221, 208, 208, 208, 208, 218, 222, 223, 224, 92 | 203, 203, 203, 203, 221, 225, 226, 204, 208, 208, 208, 208, 224, 227, 228, 209, 93 | 209, 210, 211, 212, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 94 | 212, 216, 217, 218, 232, 241, 242, 243, 236, 244, 245, 246, 240, 247, 248, 249, 95 | 218, 222, 223, 224, 243, 250, 251, 252, 246, 253, 254, 255, 249, 256, 257, 258, 96 | 224, 227, 228, 209, 252, 259, 260, 229, 255, 261, 262, 233, 258, 263, 264, 237, 97 | /*bottom*/ 98 | 265, 265, 265, 265, 266, 267, 268, 269, 270, 271, 272, 273, 92, 119, 118, 113, 99 | 265, 265, 265, 265, 269, 274, 275, 276, 273, 277, 278, 279, 113, 112, 111, 104, 100 | 265, 265, 265, 265, 276, 280, 281, 282, 279, 283, 284, 285, 104, 103, 102, 95, 101 | 265, 265, 265, 265, 282, 286, 287, 266, 285, 288, 289, 270, 95, 94, 93, 92 102 | ]; 103 | 104 | const teapotVertices = [ 105 | 1.4, 0, 2.4, 106 | 1.4, - 0.784, 2.4, 107 | 0.784, - 1.4, 2.4, 108 | 0, - 1.4, 2.4, 109 | 1.3375, 0, 2.53125, 110 | 1.3375, - 0.749, 2.53125, 111 | 0.749, - 1.3375, 2.53125, 112 | 0, - 1.3375, 2.53125, 113 | 1.4375, 0, 2.53125, 114 | 1.4375, - 0.805, 2.53125, 115 | 0.805, - 1.4375, 2.53125, 116 | 0, - 1.4375, 2.53125, 117 | 1.5, 0, 2.4, 118 | 1.5, - 0.84, 2.4, 119 | 0.84, - 1.5, 2.4, 120 | 0, - 1.5, 2.4, 121 | - 0.784, - 1.4, 2.4, 122 | - 1.4, - 0.784, 2.4, 123 | - 1.4, 0, 2.4, 124 | - 0.749, - 1.3375, 2.53125, 125 | - 1.3375, - 0.749, 2.53125, 126 | - 1.3375, 0, 2.53125, 127 | - 0.805, - 1.4375, 2.53125, 128 | - 1.4375, - 0.805, 2.53125, 129 | - 1.4375, 0, 2.53125, 130 | - 0.84, - 1.5, 2.4, 131 | - 1.5, - 0.84, 2.4, 132 | - 1.5, 0, 2.4, 133 | - 1.4, 0.784, 2.4, 134 | - 0.784, 1.4, 2.4, 135 | 0, 1.4, 2.4, 136 | - 1.3375, 0.749, 2.53125, 137 | - 0.749, 1.3375, 2.53125, 138 | 0, 1.3375, 2.53125, 139 | - 1.4375, 0.805, 2.53125, 140 | - 0.805, 1.4375, 2.53125, 141 | 0, 1.4375, 2.53125, 142 | - 1.5, 0.84, 2.4, 143 | - 0.84, 1.5, 2.4, 144 | 0, 1.5, 2.4, 145 | 0.784, 1.4, 2.4, 146 | 1.4, 0.784, 2.4, 147 | 0.749, 1.3375, 2.53125, 148 | 1.3375, 0.749, 2.53125, 149 | 0.805, 1.4375, 2.53125, 150 | 1.4375, 0.805, 2.53125, 151 | 0.84, 1.5, 2.4, 152 | 1.5, 0.84, 2.4, 153 | 1.75, 0, 1.875, 154 | 1.75, - 0.98, 1.875, 155 | 0.98, - 1.75, 1.875, 156 | 0, - 1.75, 1.875, 157 | 2, 0, 1.35, 158 | 2, - 1.12, 1.35, 159 | 1.12, - 2, 1.35, 160 | 0, - 2, 1.35, 161 | 2, 0, 0.9, 162 | 2, - 1.12, 0.9, 163 | 1.12, - 2, 0.9, 164 | 0, - 2, 0.9, 165 | - 0.98, - 1.75, 1.875, 166 | - 1.75, - 0.98, 1.875, 167 | - 1.75, 0, 1.875, 168 | - 1.12, - 2, 1.35, 169 | - 2, - 1.12, 1.35, 170 | - 2, 0, 1.35, 171 | - 1.12, - 2, 0.9, 172 | - 2, - 1.12, 0.9, 173 | - 2, 0, 0.9, 174 | - 1.75, 0.98, 1.875, 175 | - 0.98, 1.75, 1.875, 176 | 0, 1.75, 1.875, 177 | - 2, 1.12, 1.35, 178 | - 1.12, 2, 1.35, 179 | 0, 2, 1.35, 180 | - 2, 1.12, 0.9, 181 | - 1.12, 2, 0.9, 182 | 0, 2, 0.9, 183 | 0.98, 1.75, 1.875, 184 | 1.75, 0.98, 1.875, 185 | 1.12, 2, 1.35, 186 | 2, 1.12, 1.35, 187 | 1.12, 2, 0.9, 188 | 2, 1.12, 0.9, 189 | 2, 0, 0.45, 190 | 2, - 1.12, 0.45, 191 | 1.12, - 2, 0.45, 192 | 0, - 2, 0.45, 193 | 1.5, 0, 0.225, 194 | 1.5, - 0.84, 0.225, 195 | 0.84, - 1.5, 0.225, 196 | 0, - 1.5, 0.225, 197 | 1.5, 0, 0.15, 198 | 1.5, - 0.84, 0.15, 199 | 0.84, - 1.5, 0.15, 200 | 0, - 1.5, 0.15, 201 | - 1.12, - 2, 0.45, 202 | - 2, - 1.12, 0.45, 203 | - 2, 0, 0.45, 204 | - 0.84, - 1.5, 0.225, 205 | - 1.5, - 0.84, 0.225, 206 | - 1.5, 0, 0.225, 207 | - 0.84, - 1.5, 0.15, 208 | - 1.5, - 0.84, 0.15, 209 | - 1.5, 0, 0.15, 210 | - 2, 1.12, 0.45, 211 | - 1.12, 2, 0.45, 212 | 0, 2, 0.45, 213 | - 1.5, 0.84, 0.225, 214 | - 0.84, 1.5, 0.225, 215 | 0, 1.5, 0.225, 216 | - 1.5, 0.84, 0.15, 217 | - 0.84, 1.5, 0.15, 218 | 0, 1.5, 0.15, 219 | 1.12, 2, 0.45, 220 | 2, 1.12, 0.45, 221 | 0.84, 1.5, 0.225, 222 | 1.5, 0.84, 0.225, 223 | 0.84, 1.5, 0.15, 224 | 1.5, 0.84, 0.15, 225 | - 1.6, 0, 2.025, 226 | - 1.6, - 0.3, 2.025, 227 | - 1.5, - 0.3, 2.25, 228 | - 1.5, 0, 2.25, 229 | - 2.3, 0, 2.025, 230 | - 2.3, - 0.3, 2.025, 231 | - 2.5, - 0.3, 2.25, 232 | - 2.5, 0, 2.25, 233 | - 2.7, 0, 2.025, 234 | - 2.7, - 0.3, 2.025, 235 | - 3, - 0.3, 2.25, 236 | - 3, 0, 2.25, 237 | - 2.7, 0, 1.8, 238 | - 2.7, - 0.3, 1.8, 239 | - 3, - 0.3, 1.8, 240 | - 3, 0, 1.8, 241 | - 1.5, 0.3, 2.25, 242 | - 1.6, 0.3, 2.025, 243 | - 2.5, 0.3, 2.25, 244 | - 2.3, 0.3, 2.025, 245 | - 3, 0.3, 2.25, 246 | - 2.7, 0.3, 2.025, 247 | - 3, 0.3, 1.8, 248 | - 2.7, 0.3, 1.8, 249 | - 2.7, 0, 1.575, 250 | - 2.7, - 0.3, 1.575, 251 | - 3, - 0.3, 1.35, 252 | - 3, 0, 1.35, 253 | - 2.5, 0, 1.125, 254 | - 2.5, - 0.3, 1.125, 255 | - 2.65, - 0.3, 0.9375, 256 | - 2.65, 0, 0.9375, 257 | - 2, - 0.3, 0.9, 258 | - 1.9, - 0.3, 0.6, 259 | - 1.9, 0, 0.6, 260 | - 3, 0.3, 1.35, 261 | - 2.7, 0.3, 1.575, 262 | - 2.65, 0.3, 0.9375, 263 | - 2.5, 0.3, 1.125, 264 | - 1.9, 0.3, 0.6, 265 | - 2, 0.3, 0.9, 266 | 1.7, 0, 1.425, 267 | 1.7, - 0.66, 1.425, 268 | 1.7, - 0.66, 0.6, 269 | 1.7, 0, 0.6, 270 | 2.6, 0, 1.425, 271 | 2.6, - 0.66, 1.425, 272 | 3.1, - 0.66, 0.825, 273 | 3.1, 0, 0.825, 274 | 2.3, 0, 2.1, 275 | 2.3, - 0.25, 2.1, 276 | 2.4, - 0.25, 2.025, 277 | 2.4, 0, 2.025, 278 | 2.7, 0, 2.4, 279 | 2.7, - 0.25, 2.4, 280 | 3.3, - 0.25, 2.4, 281 | 3.3, 0, 2.4, 282 | 1.7, 0.66, 0.6, 283 | 1.7, 0.66, 1.425, 284 | 3.1, 0.66, 0.825, 285 | 2.6, 0.66, 1.425, 286 | 2.4, 0.25, 2.025, 287 | 2.3, 0.25, 2.1, 288 | 3.3, 0.25, 2.4, 289 | 2.7, 0.25, 2.4, 290 | 2.8, 0, 2.475, 291 | 2.8, - 0.25, 2.475, 292 | 3.525, - 0.25, 2.49375, 293 | 3.525, 0, 2.49375, 294 | 2.9, 0, 2.475, 295 | 2.9, - 0.15, 2.475, 296 | 3.45, - 0.15, 2.5125, 297 | 3.45, 0, 2.5125, 298 | 2.8, 0, 2.4, 299 | 2.8, - 0.15, 2.4, 300 | 3.2, - 0.15, 2.4, 301 | 3.2, 0, 2.4, 302 | 3.525, 0.25, 2.49375, 303 | 2.8, 0.25, 2.475, 304 | 3.45, 0.15, 2.5125, 305 | 2.9, 0.15, 2.475, 306 | 3.2, 0.15, 2.4, 307 | 2.8, 0.15, 2.4, 308 | 0, 0, 3.15, 309 | 0.8, 0, 3.15, 310 | 0.8, - 0.45, 3.15, 311 | 0.45, - 0.8, 3.15, 312 | 0, - 0.8, 3.15, 313 | 0, 0, 2.85, 314 | 0.2, 0, 2.7, 315 | 0.2, - 0.112, 2.7, 316 | 0.112, - 0.2, 2.7, 317 | 0, - 0.2, 2.7, 318 | - 0.45, - 0.8, 3.15, 319 | - 0.8, - 0.45, 3.15, 320 | - 0.8, 0, 3.15, 321 | - 0.112, - 0.2, 2.7, 322 | - 0.2, - 0.112, 2.7, 323 | - 0.2, 0, 2.7, 324 | - 0.8, 0.45, 3.15, 325 | - 0.45, 0.8, 3.15, 326 | 0, 0.8, 3.15, 327 | - 0.2, 0.112, 2.7, 328 | - 0.112, 0.2, 2.7, 329 | 0, 0.2, 2.7, 330 | 0.45, 0.8, 3.15, 331 | 0.8, 0.45, 3.15, 332 | 0.112, 0.2, 2.7, 333 | 0.2, 0.112, 2.7, 334 | 0.4, 0, 2.55, 335 | 0.4, - 0.224, 2.55, 336 | 0.224, - 0.4, 2.55, 337 | 0, - 0.4, 2.55, 338 | 1.3, 0, 2.55, 339 | 1.3, - 0.728, 2.55, 340 | 0.728, - 1.3, 2.55, 341 | 0, - 1.3, 2.55, 342 | 1.3, 0, 2.4, 343 | 1.3, - 0.728, 2.4, 344 | 0.728, - 1.3, 2.4, 345 | 0, - 1.3, 2.4, 346 | - 0.224, - 0.4, 2.55, 347 | - 0.4, - 0.224, 2.55, 348 | - 0.4, 0, 2.55, 349 | - 0.728, - 1.3, 2.55, 350 | - 1.3, - 0.728, 2.55, 351 | - 1.3, 0, 2.55, 352 | - 0.728, - 1.3, 2.4, 353 | - 1.3, - 0.728, 2.4, 354 | - 1.3, 0, 2.4, 355 | - 0.4, 0.224, 2.55, 356 | - 0.224, 0.4, 2.55, 357 | 0, 0.4, 2.55, 358 | - 1.3, 0.728, 2.55, 359 | - 0.728, 1.3, 2.55, 360 | 0, 1.3, 2.55, 361 | - 1.3, 0.728, 2.4, 362 | - 0.728, 1.3, 2.4, 363 | 0, 1.3, 2.4, 364 | 0.224, 0.4, 2.55, 365 | 0.4, 0.224, 2.55, 366 | 0.728, 1.3, 2.55, 367 | 1.3, 0.728, 2.55, 368 | 0.728, 1.3, 2.4, 369 | 1.3, 0.728, 2.4, 370 | 0, 0, 0, 371 | 1.425, 0, 0, 372 | 1.425, 0.798, 0, 373 | 0.798, 1.425, 0, 374 | 0, 1.425, 0, 375 | 1.5, 0, 0.075, 376 | 1.5, 0.84, 0.075, 377 | 0.84, 1.5, 0.075, 378 | 0, 1.5, 0.075, 379 | - 0.798, 1.425, 0, 380 | - 1.425, 0.798, 0, 381 | - 1.425, 0, 0, 382 | - 0.84, 1.5, 0.075, 383 | - 1.5, 0.84, 0.075, 384 | - 1.5, 0, 0.075, 385 | - 1.425, - 0.798, 0, 386 | - 0.798, - 1.425, 0, 387 | 0, - 1.425, 0, 388 | - 1.5, - 0.84, 0.075, 389 | - 0.84, - 1.5, 0.075, 390 | 0, - 1.5, 0.075, 391 | 0.798, - 1.425, 0, 392 | 1.425, - 0.798, 0, 393 | 0.84, - 1.5, 0.075, 394 | 1.5, - 0.84, 0.075 395 | ]; 396 | 397 | super(); 398 | 399 | // number of segments per patch 400 | segments = Math.max( 2, Math.floor( segments ) ); 401 | 402 | // Jim Blinn scaled the teapot down in size by about 1.3 for 403 | // some rendering tests. He liked the new proportions that he kept 404 | // the data in this form. The model was distributed with these new 405 | // proportions and became the norm. Trivia: comparing images of the 406 | // real teapot and the computer model, the ratio for the bowl of the 407 | // real teapot is more like 1.25, but since 1.3 is the traditional 408 | // value given, we use it here. 409 | const blinnScale = 1.3; 410 | 411 | // scale the size to be the real scaling factor 412 | const maxHeight = 3.15 * ( blinn ? 1 : blinnScale ); 413 | 414 | const maxHeight2 = maxHeight / 2; 415 | const trueSize = size / maxHeight2; 416 | 417 | // Number of elements depends on what is needed. Subtract degenerate 418 | // triangles at tip of bottom and lid out in advance. 419 | let numTriangles = bottom ? ( 8 * segments - 4 ) * segments : 0; 420 | numTriangles += lid ? ( 16 * segments - 4 ) * segments : 0; 421 | numTriangles += body ? 40 * segments * segments : 0; 422 | 423 | const indices = new Uint32Array( numTriangles * 3 ); 424 | 425 | let numVertices = bottom ? 4 : 0; 426 | numVertices += lid ? 8 : 0; 427 | numVertices += body ? 20 : 0; 428 | numVertices *= ( segments + 1 ) * ( segments + 1 ); 429 | 430 | const vertices = new Float32Array( numVertices * 3 ); 431 | const normals = new Float32Array( numVertices * 3 ); 432 | const uvs = new Float32Array( numVertices * 2 ); 433 | 434 | // Bezier form 435 | const ms = new Matrix4(); 436 | ms.set( 437 | - 1.0, 3.0, - 3.0, 1.0, 438 | 3.0, - 6.0, 3.0, 0.0, 439 | - 3.0, 3.0, 0.0, 0.0, 440 | 1.0, 0.0, 0.0, 0.0 ); 441 | 442 | const g = []; 443 | 444 | const sp = []; 445 | const tp = []; 446 | const dsp = []; 447 | const dtp = []; 448 | 449 | // M * G * M matrix, sort of see 450 | // http://www.cs.helsinki.fi/group/goa/mallinnus/curves/surfaces.html 451 | const mgm = []; 452 | 453 | const vert = []; 454 | const sdir = []; 455 | const tdir = []; 456 | 457 | const norm = new Vector3(); 458 | 459 | let tcoord; 460 | 461 | let sval; 462 | let tval; 463 | let p; 464 | let dsval = 0; 465 | let dtval = 0; 466 | 467 | const normOut = new Vector3(); 468 | 469 | const gmx = new Matrix4(); 470 | const tmtx = new Matrix4(); 471 | 472 | const vsp = new Vector4(); 473 | const vtp = new Vector4(); 474 | const vdsp = new Vector4(); 475 | const vdtp = new Vector4(); 476 | 477 | const vsdir = new Vector3(); 478 | const vtdir = new Vector3(); 479 | 480 | const mst = ms.clone(); 481 | mst.transpose(); 482 | 483 | // internal function: test if triangle has any matching vertices; 484 | // if so, don't save triangle, since it won't display anything. 485 | const notDegenerate = ( vtx1, vtx2, vtx3 ) => // if any vertex matches, return false 486 | ! ( ( ( vertices[ vtx1 * 3 ] === vertices[ vtx2 * 3 ] ) && 487 | ( vertices[ vtx1 * 3 + 1 ] === vertices[ vtx2 * 3 + 1 ] ) && 488 | ( vertices[ vtx1 * 3 + 2 ] === vertices[ vtx2 * 3 + 2 ] ) ) || 489 | ( ( vertices[ vtx1 * 3 ] === vertices[ vtx3 * 3 ] ) && 490 | ( vertices[ vtx1 * 3 + 1 ] === vertices[ vtx3 * 3 + 1 ] ) && 491 | ( vertices[ vtx1 * 3 + 2 ] === vertices[ vtx3 * 3 + 2 ] ) ) || ( vertices[ vtx2 * 3 ] === vertices[ vtx3 * 3 ] ) && 492 | ( vertices[ vtx2 * 3 + 1 ] === vertices[ vtx3 * 3 + 1 ] ) && 493 | ( vertices[ vtx2 * 3 + 2 ] === vertices[ vtx3 * 3 + 2 ] ) ); 494 | 495 | 496 | for ( let i = 0; i < 3; i ++ ) { 497 | 498 | mgm[ i ] = new Matrix4(); 499 | 500 | } 501 | 502 | const minPatches = body ? 0 : 20; 503 | const maxPatches = bottom ? 32 : 28; 504 | 505 | const vertPerRow = segments + 1; 506 | 507 | let surfCount = 0; 508 | 509 | let vertCount = 0; 510 | let normCount = 0; 511 | let uvCount = 0; 512 | 513 | let indexCount = 0; 514 | 515 | for ( let surf = minPatches; surf < maxPatches; surf ++ ) { 516 | 517 | // lid is in the middle of the data, patches 20-27, 518 | // so ignore it for this part of the loop if the lid is not desired 519 | if ( lid || ( surf < 20 || surf >= 28 ) ) { 520 | 521 | // get M * G * M matrix for x,y,z 522 | for ( let i = 0; i < 3; i ++ ) { 523 | 524 | // get control patches 525 | for ( let r = 0; r < 4; r ++ ) { 526 | 527 | for ( let c = 0; c < 4; c ++ ) { 528 | 529 | // transposed 530 | g[ c * 4 + r ] = teapotVertices[ teapotPatches[ surf * 16 + r * 4 + c ] * 3 + i ]; 531 | 532 | // is the lid to be made larger, and is this a point on the lid 533 | // that is X or Y? 534 | if ( fitLid && ( surf >= 20 && surf < 28 ) && ( i !== 2 ) ) { 535 | 536 | // increase XY size by 7.7%, found empirically. I don't 537 | // increase Z so that the teapot will continue to fit in the 538 | // space -1 to 1 for Y (Y is up for the final model). 539 | g[ c * 4 + r ] *= 1.077; 540 | 541 | } 542 | 543 | // Blinn "fixed" the teapot by dividing Z by blinnScale, and that's the 544 | // data we now use. The original teapot is taller. Fix it: 545 | if ( ! blinn && ( i === 2 ) ) { 546 | 547 | g[ c * 4 + r ] *= blinnScale; 548 | 549 | } 550 | 551 | } 552 | 553 | } 554 | 555 | gmx.set( g[ 0 ], g[ 1 ], g[ 2 ], g[ 3 ], g[ 4 ], g[ 5 ], g[ 6 ], g[ 7 ], g[ 8 ], g[ 9 ], g[ 10 ], g[ 11 ], g[ 12 ], g[ 13 ], g[ 14 ], g[ 15 ] ); 556 | 557 | tmtx.multiplyMatrices( gmx, ms ); 558 | mgm[ i ].multiplyMatrices( mst, tmtx ); 559 | 560 | } 561 | 562 | // step along, get points, and output 563 | for ( let sstep = 0; sstep <= segments; sstep ++ ) { 564 | 565 | const s = sstep / segments; 566 | 567 | for ( let tstep = 0; tstep <= segments; tstep ++ ) { 568 | 569 | const t = tstep / segments; 570 | 571 | // point from basis 572 | // get power vectors and their derivatives 573 | for ( p = 4, sval = tval = 1.0; p --; ) { 574 | 575 | sp[ p ] = sval; 576 | tp[ p ] = tval; 577 | sval *= s; 578 | tval *= t; 579 | 580 | if ( p === 3 ) { 581 | 582 | dsp[ p ] = dtp[ p ] = 0.0; 583 | dsval = dtval = 1.0; 584 | 585 | } else { 586 | 587 | dsp[ p ] = dsval * ( 3 - p ); 588 | dtp[ p ] = dtval * ( 3 - p ); 589 | dsval *= s; 590 | dtval *= t; 591 | 592 | } 593 | 594 | } 595 | 596 | vsp.fromArray( sp ); 597 | vtp.fromArray( tp ); 598 | vdsp.fromArray( dsp ); 599 | vdtp.fromArray( dtp ); 600 | 601 | // do for x,y,z 602 | for ( let i = 0; i < 3; i ++ ) { 603 | 604 | // multiply power vectors times matrix to get value 605 | tcoord = vsp.clone(); 606 | tcoord.applyMatrix4( mgm[ i ] ); 607 | vert[ i ] = tcoord.dot( vtp ); 608 | 609 | // get s and t tangent vectors 610 | tcoord = vdsp.clone(); 611 | tcoord.applyMatrix4( mgm[ i ] ); 612 | sdir[ i ] = tcoord.dot( vtp ); 613 | 614 | tcoord = vsp.clone(); 615 | tcoord.applyMatrix4( mgm[ i ] ); 616 | tdir[ i ] = tcoord.dot( vdtp ); 617 | 618 | } 619 | 620 | // find normal 621 | vsdir.fromArray( sdir ); 622 | vtdir.fromArray( tdir ); 623 | norm.crossVectors( vtdir, vsdir ); 624 | norm.normalize(); 625 | 626 | // if X and Z length is 0, at the cusp, so point the normal up or down, depending on patch number 627 | if ( vert[ 0 ] === 0 && vert[ 1 ] === 0 ) { 628 | 629 | // if above the middle of the teapot, normal points up, else down 630 | normOut.set( 0, vert[ 2 ] > maxHeight2 ? 1 : - 1, 0 ); 631 | 632 | } else { 633 | 634 | // standard output: rotate on X axis 635 | normOut.set( norm.x, norm.z, - norm.y ); 636 | 637 | } 638 | 639 | // store it all 640 | vertices[ vertCount ++ ] = trueSize * vert[ 0 ]; 641 | vertices[ vertCount ++ ] = trueSize * ( vert[ 2 ] - maxHeight2 ); 642 | vertices[ vertCount ++ ] = - trueSize * vert[ 1 ]; 643 | 644 | normals[ normCount ++ ] = normOut.x; 645 | normals[ normCount ++ ] = normOut.y; 646 | normals[ normCount ++ ] = normOut.z; 647 | 648 | uvs[ uvCount ++ ] = 1 - t; 649 | uvs[ uvCount ++ ] = 1 - s; 650 | 651 | } 652 | 653 | } 654 | 655 | // save the faces 656 | for ( let sstep = 0; sstep < segments; sstep ++ ) { 657 | 658 | for ( let tstep = 0; tstep < segments; tstep ++ ) { 659 | 660 | const v1 = surfCount * vertPerRow * vertPerRow + sstep * vertPerRow + tstep; 661 | const v2 = v1 + 1; 662 | const v3 = v2 + vertPerRow; 663 | const v4 = v1 + vertPerRow; 664 | 665 | // Normals and UVs cannot be shared. Without clone(), you can see the consequences 666 | // of sharing if you call geometry.applyMatrix4( matrix ). 667 | if ( notDegenerate( v1, v2, v3 ) ) { 668 | 669 | indices[ indexCount ++ ] = v1; 670 | indices[ indexCount ++ ] = v2; 671 | indices[ indexCount ++ ] = v3; 672 | 673 | } 674 | 675 | if ( notDegenerate( v1, v3, v4 ) ) { 676 | 677 | indices[ indexCount ++ ] = v1; 678 | indices[ indexCount ++ ] = v3; 679 | indices[ indexCount ++ ] = v4; 680 | 681 | } 682 | 683 | } 684 | 685 | } 686 | 687 | // increment only if a surface was used 688 | surfCount ++; 689 | 690 | } 691 | 692 | } 693 | 694 | this.setIndex( new BufferAttribute( indices, 1 ) ); 695 | this.setAttribute( 'position', new BufferAttribute( vertices, 3 ) ); 696 | this.setAttribute( 'normal', new BufferAttribute( normals, 3 ) ); 697 | this.setAttribute( 'uv', new BufferAttribute( uvs, 2 ) ); 698 | 699 | this.computeBoundingSphere(); 700 | 701 | } 702 | 703 | } 704 | 705 | export { TeapotGeometry }; 706 | -------------------------------------------------------------------------------- /examples/jsm/libs/motion-controllers.module.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @webxr-input-profiles/motion-controllers 1.0.0 https://github.com/immersive-web/webxr-input-profiles 3 | */ 4 | 5 | const Constants = { 6 | Handedness: Object.freeze({ 7 | NONE: 'none', 8 | LEFT: 'left', 9 | RIGHT: 'right' 10 | }), 11 | 12 | ComponentState: Object.freeze({ 13 | DEFAULT: 'default', 14 | TOUCHED: 'touched', 15 | PRESSED: 'pressed' 16 | }), 17 | 18 | ComponentProperty: Object.freeze({ 19 | BUTTON: 'button', 20 | X_AXIS: 'xAxis', 21 | Y_AXIS: 'yAxis', 22 | STATE: 'state' 23 | }), 24 | 25 | ComponentType: Object.freeze({ 26 | TRIGGER: 'trigger', 27 | SQUEEZE: 'squeeze', 28 | TOUCHPAD: 'touchpad', 29 | THUMBSTICK: 'thumbstick', 30 | BUTTON: 'button' 31 | }), 32 | 33 | ButtonTouchThreshold: 0.05, 34 | 35 | AxisTouchThreshold: 0.1, 36 | 37 | VisualResponseProperty: Object.freeze({ 38 | TRANSFORM: 'transform', 39 | VISIBILITY: 'visibility' 40 | }) 41 | }; 42 | 43 | /** 44 | * @description Static helper function to fetch a JSON file and turn it into a JS object 45 | * @param {string} path - Path to JSON file to be fetched 46 | */ 47 | async function fetchJsonFile(path) { 48 | const response = await fetch(path); 49 | if (!response.ok) { 50 | throw new Error(response.statusText); 51 | } else { 52 | return response.json(); 53 | } 54 | } 55 | 56 | async function fetchProfilesList(basePath) { 57 | if (!basePath) { 58 | throw new Error('No basePath supplied'); 59 | } 60 | 61 | const profileListFileName = 'profilesList.json'; 62 | const profilesList = await fetchJsonFile(`${basePath}/${profileListFileName}`); 63 | return profilesList; 64 | } 65 | 66 | async function fetchProfile(xrInputSource, basePath, defaultProfile = null, getAssetPath = true) { 67 | if (!xrInputSource) { 68 | throw new Error('No xrInputSource supplied'); 69 | } 70 | 71 | if (!basePath) { 72 | throw new Error('No basePath supplied'); 73 | } 74 | 75 | // Get the list of profiles 76 | const supportedProfilesList = await fetchProfilesList(basePath); 77 | 78 | // Find the relative path to the first requested profile that is recognized 79 | let match; 80 | xrInputSource.profiles.some((profileId) => { 81 | const supportedProfile = supportedProfilesList[profileId]; 82 | if (supportedProfile) { 83 | match = { 84 | profileId, 85 | profilePath: `${basePath}/${supportedProfile.path}`, 86 | deprecated: !!supportedProfile.deprecated 87 | }; 88 | } 89 | return !!match; 90 | }); 91 | 92 | if (!match) { 93 | if (!defaultProfile) { 94 | throw new Error('No matching profile name found'); 95 | } 96 | 97 | const supportedProfile = supportedProfilesList[defaultProfile]; 98 | if (!supportedProfile) { 99 | throw new Error(`No matching profile name found and default profile "${defaultProfile}" missing.`); 100 | } 101 | 102 | match = { 103 | profileId: defaultProfile, 104 | profilePath: `${basePath}/${supportedProfile.path}`, 105 | deprecated: !!supportedProfile.deprecated 106 | }; 107 | } 108 | 109 | const profile = await fetchJsonFile(match.profilePath); 110 | 111 | let assetPath; 112 | if (getAssetPath) { 113 | let layout; 114 | if (xrInputSource.handedness === 'any') { 115 | layout = profile.layouts[Object.keys(profile.layouts)[0]]; 116 | } else { 117 | layout = profile.layouts[xrInputSource.handedness]; 118 | } 119 | if (!layout) { 120 | throw new Error( 121 | `No matching handedness, ${xrInputSource.handedness}, in profile ${match.profileId}` 122 | ); 123 | } 124 | 125 | if (layout.assetPath) { 126 | assetPath = match.profilePath.replace('profile.json', layout.assetPath); 127 | } 128 | } 129 | 130 | return { profile, assetPath }; 131 | } 132 | 133 | /** @constant {Object} */ 134 | const defaultComponentValues = { 135 | xAxis: 0, 136 | yAxis: 0, 137 | button: 0, 138 | state: Constants.ComponentState.DEFAULT 139 | }; 140 | 141 | /** 142 | * @description Converts an X, Y coordinate from the range -1 to 1 (as reported by the Gamepad 143 | * API) to the range 0 to 1 (for interpolation). Also caps the X, Y values to be bounded within 144 | * a circle. This ensures that thumbsticks are not animated outside the bounds of their physical 145 | * range of motion and touchpads do not report touch locations off their physical bounds. 146 | * @param {number} x The original x coordinate in the range -1 to 1 147 | * @param {number} y The original y coordinate in the range -1 to 1 148 | */ 149 | function normalizeAxes(x = 0, y = 0) { 150 | let xAxis = x; 151 | let yAxis = y; 152 | 153 | // Determine if the point is outside the bounds of the circle 154 | // and, if so, place it on the edge of the circle 155 | const hypotenuse = Math.sqrt((x * x) + (y * y)); 156 | if (hypotenuse > 1) { 157 | const theta = Math.atan2(y, x); 158 | xAxis = Math.cos(theta); 159 | yAxis = Math.sin(theta); 160 | } 161 | 162 | // Scale and move the circle so values are in the interpolation range. The circle's origin moves 163 | // from (0, 0) to (0.5, 0.5). The circle's radius scales from 1 to be 0.5. 164 | const result = { 165 | normalizedXAxis: (xAxis * 0.5) + 0.5, 166 | normalizedYAxis: (yAxis * 0.5) + 0.5 167 | }; 168 | return result; 169 | } 170 | 171 | /** 172 | * Contains the description of how the 3D model should visually respond to a specific user input. 173 | * This is accomplished by initializing the object with the name of a node in the 3D model and 174 | * property that need to be modified in response to user input, the name of the nodes representing 175 | * the allowable range of motion, and the name of the input which triggers the change. In response 176 | * to the named input changing, this object computes the appropriate weighting to use for 177 | * interpolating between the range of motion nodes. 178 | */ 179 | class VisualResponse { 180 | constructor(visualResponseDescription) { 181 | this.componentProperty = visualResponseDescription.componentProperty; 182 | this.states = visualResponseDescription.states; 183 | this.valueNodeName = visualResponseDescription.valueNodeName; 184 | this.valueNodeProperty = visualResponseDescription.valueNodeProperty; 185 | 186 | if (this.valueNodeProperty === Constants.VisualResponseProperty.TRANSFORM) { 187 | this.minNodeName = visualResponseDescription.minNodeName; 188 | this.maxNodeName = visualResponseDescription.maxNodeName; 189 | } 190 | 191 | // Initializes the response's current value based on default data 192 | this.value = 0; 193 | this.updateFromComponent(defaultComponentValues); 194 | } 195 | 196 | /** 197 | * Computes the visual response's interpolation weight based on component state 198 | * @param {Object} componentValues - The component from which to update 199 | * @param {number} xAxis - The reported X axis value of the component 200 | * @param {number} yAxis - The reported Y axis value of the component 201 | * @param {number} button - The reported value of the component's button 202 | * @param {string} state - The component's active state 203 | */ 204 | updateFromComponent({ 205 | xAxis, yAxis, button, state 206 | }) { 207 | const { normalizedXAxis, normalizedYAxis } = normalizeAxes(xAxis, yAxis); 208 | switch (this.componentProperty) { 209 | case Constants.ComponentProperty.X_AXIS: 210 | this.value = (this.states.includes(state)) ? normalizedXAxis : 0.5; 211 | break; 212 | case Constants.ComponentProperty.Y_AXIS: 213 | this.value = (this.states.includes(state)) ? normalizedYAxis : 0.5; 214 | break; 215 | case Constants.ComponentProperty.BUTTON: 216 | this.value = (this.states.includes(state)) ? button : 0; 217 | break; 218 | case Constants.ComponentProperty.STATE: 219 | if (this.valueNodeProperty === Constants.VisualResponseProperty.VISIBILITY) { 220 | this.value = (this.states.includes(state)); 221 | } else { 222 | this.value = this.states.includes(state) ? 1.0 : 0.0; 223 | } 224 | break; 225 | default: 226 | throw new Error(`Unexpected visualResponse componentProperty ${this.componentProperty}`); 227 | } 228 | } 229 | } 230 | 231 | class Component { 232 | /** 233 | * @param {Object} componentId - Id of the component 234 | * @param {Object} componentDescription - Description of the component to be created 235 | */ 236 | constructor(componentId, componentDescription) { 237 | if (!componentId 238 | || !componentDescription 239 | || !componentDescription.visualResponses 240 | || !componentDescription.gamepadIndices 241 | || Object.keys(componentDescription.gamepadIndices).length === 0) { 242 | throw new Error('Invalid arguments supplied'); 243 | } 244 | 245 | this.id = componentId; 246 | this.type = componentDescription.type; 247 | this.rootNodeName = componentDescription.rootNodeName; 248 | this.touchPointNodeName = componentDescription.touchPointNodeName; 249 | 250 | // Build all the visual responses for this component 251 | this.visualResponses = {}; 252 | Object.keys(componentDescription.visualResponses).forEach((responseName) => { 253 | const visualResponse = new VisualResponse(componentDescription.visualResponses[responseName]); 254 | this.visualResponses[responseName] = visualResponse; 255 | }); 256 | 257 | // Set default values 258 | this.gamepadIndices = Object.assign({}, componentDescription.gamepadIndices); 259 | 260 | this.values = { 261 | state: Constants.ComponentState.DEFAULT, 262 | button: (this.gamepadIndices.button !== undefined) ? 0 : undefined, 263 | xAxis: (this.gamepadIndices.xAxis !== undefined) ? 0 : undefined, 264 | yAxis: (this.gamepadIndices.yAxis !== undefined) ? 0 : undefined 265 | }; 266 | } 267 | 268 | get data() { 269 | const data = { id: this.id, ...this.values }; 270 | return data; 271 | } 272 | 273 | /** 274 | * @description Poll for updated data based on current gamepad state 275 | * @param {Object} gamepad - The gamepad object from which the component data should be polled 276 | */ 277 | updateFromGamepad(gamepad) { 278 | // Set the state to default before processing other data sources 279 | this.values.state = Constants.ComponentState.DEFAULT; 280 | 281 | // Get and normalize button 282 | if (this.gamepadIndices.button !== undefined 283 | && gamepad.buttons.length > this.gamepadIndices.button) { 284 | const gamepadButton = gamepad.buttons[this.gamepadIndices.button]; 285 | this.values.button = gamepadButton.value; 286 | this.values.button = (this.values.button < 0) ? 0 : this.values.button; 287 | this.values.button = (this.values.button > 1) ? 1 : this.values.button; 288 | 289 | // Set the state based on the button 290 | if (gamepadButton.pressed || this.values.button === 1) { 291 | this.values.state = Constants.ComponentState.PRESSED; 292 | } else if (gamepadButton.touched || this.values.button > Constants.ButtonTouchThreshold) { 293 | this.values.state = Constants.ComponentState.TOUCHED; 294 | } 295 | } 296 | 297 | // Get and normalize x axis value 298 | if (this.gamepadIndices.xAxis !== undefined 299 | && gamepad.axes.length > this.gamepadIndices.xAxis) { 300 | this.values.xAxis = gamepad.axes[this.gamepadIndices.xAxis]; 301 | this.values.xAxis = (this.values.xAxis < -1) ? -1 : this.values.xAxis; 302 | this.values.xAxis = (this.values.xAxis > 1) ? 1 : this.values.xAxis; 303 | 304 | // If the state is still default, check if the xAxis makes it touched 305 | if (this.values.state === Constants.ComponentState.DEFAULT 306 | && Math.abs(this.values.xAxis) > Constants.AxisTouchThreshold) { 307 | this.values.state = Constants.ComponentState.TOUCHED; 308 | } 309 | } 310 | 311 | // Get and normalize Y axis value 312 | if (this.gamepadIndices.yAxis !== undefined 313 | && gamepad.axes.length > this.gamepadIndices.yAxis) { 314 | this.values.yAxis = gamepad.axes[this.gamepadIndices.yAxis]; 315 | this.values.yAxis = (this.values.yAxis < -1) ? -1 : this.values.yAxis; 316 | this.values.yAxis = (this.values.yAxis > 1) ? 1 : this.values.yAxis; 317 | 318 | // If the state is still default, check if the yAxis makes it touched 319 | if (this.values.state === Constants.ComponentState.DEFAULT 320 | && Math.abs(this.values.yAxis) > Constants.AxisTouchThreshold) { 321 | this.values.state = Constants.ComponentState.TOUCHED; 322 | } 323 | } 324 | 325 | // Update the visual response weights based on the current component data 326 | Object.values(this.visualResponses).forEach((visualResponse) => { 327 | visualResponse.updateFromComponent(this.values); 328 | }); 329 | } 330 | } 331 | 332 | /** 333 | * @description Builds a motion controller with components and visual responses based on the 334 | * supplied profile description. Data is polled from the xrInputSource's gamepad. 335 | * @author Nell Waliczek / https://github.com/NellWaliczek 336 | */ 337 | class MotionController { 338 | /** 339 | * @param {Object} xrInputSource - The XRInputSource to build the MotionController around 340 | * @param {Object} profile - The best matched profile description for the supplied xrInputSource 341 | * @param {Object} assetUrl 342 | */ 343 | constructor(xrInputSource, profile, assetUrl) { 344 | if (!xrInputSource) { 345 | throw new Error('No xrInputSource supplied'); 346 | } 347 | 348 | if (!profile) { 349 | throw new Error('No profile supplied'); 350 | } 351 | 352 | this.xrInputSource = xrInputSource; 353 | this.assetUrl = assetUrl; 354 | this.id = profile.profileId; 355 | 356 | // Build child components as described in the profile description 357 | this.layoutDescription = profile.layouts[xrInputSource.handedness]; 358 | this.components = {}; 359 | Object.keys(this.layoutDescription.components).forEach((componentId) => { 360 | const componentDescription = this.layoutDescription.components[componentId]; 361 | this.components[componentId] = new Component(componentId, componentDescription); 362 | }); 363 | 364 | // Initialize components based on current gamepad state 365 | this.updateFromGamepad(); 366 | } 367 | 368 | get gripSpace() { 369 | return this.xrInputSource.gripSpace; 370 | } 371 | 372 | get targetRaySpace() { 373 | return this.xrInputSource.targetRaySpace; 374 | } 375 | 376 | /** 377 | * @description Returns a subset of component data for simplified debugging 378 | */ 379 | get data() { 380 | const data = []; 381 | Object.values(this.components).forEach((component) => { 382 | data.push(component.data); 383 | }); 384 | return data; 385 | } 386 | 387 | /** 388 | * @description Poll for updated data based on current gamepad state 389 | */ 390 | updateFromGamepad() { 391 | Object.values(this.components).forEach((component) => { 392 | component.updateFromGamepad(this.xrInputSource.gamepad); 393 | }); 394 | } 395 | } 396 | 397 | export { Constants, MotionController, fetchProfile, fetchProfilesList }; 398 | -------------------------------------------------------------------------------- /examples/jsm/webxr/OculusHandModel.js: -------------------------------------------------------------------------------- 1 | import { Object3D, Sphere, Box3 } from '../../../build/three.module.js'; 2 | import { XRHandMeshModel } from './XRHandMeshModel.js'; 3 | 4 | const TOUCH_RADIUS = 0.01; 5 | const POINTING_JOINT = 'index-finger-tip'; 6 | 7 | class OculusHandModel extends Object3D { 8 | 9 | constructor( controller ) { 10 | 11 | super(); 12 | 13 | this.controller = controller; 14 | this.motionController = null; 15 | this.envMap = null; 16 | 17 | this.mesh = null; 18 | 19 | controller.addEventListener( 'connected', ( event ) => { 20 | 21 | const xrInputSource = event.data; 22 | 23 | if ( xrInputSource.hand && ! this.motionController ) { 24 | 25 | this.xrInputSource = xrInputSource; 26 | 27 | this.motionController = new XRHandMeshModel( this, controller, this.path, xrInputSource.handedness ); 28 | 29 | } 30 | 31 | } ); 32 | 33 | controller.addEventListener( 'disconnected', () => { 34 | 35 | this.clear(); 36 | this.motionController = null; 37 | 38 | } ); 39 | 40 | } 41 | 42 | updateMatrixWorld( force ) { 43 | 44 | super.updateMatrixWorld( force ); 45 | 46 | if ( this.motionController ) { 47 | 48 | this.motionController.updateMesh(); 49 | 50 | } 51 | 52 | } 53 | 54 | getPointerPosition() { 55 | 56 | const indexFingerTip = this.controller.joints[ POINTING_JOINT ]; 57 | if ( indexFingerTip ) { 58 | 59 | return indexFingerTip.position; 60 | 61 | } else { 62 | 63 | return null; 64 | 65 | } 66 | 67 | } 68 | 69 | intersectBoxObject( boxObject ) { 70 | 71 | const pointerPosition = this.getPointerPosition(); 72 | if ( pointerPosition ) { 73 | 74 | const indexSphere = new Sphere( pointerPosition, TOUCH_RADIUS ); 75 | const box = new Box3().setFromObject( boxObject ); 76 | return indexSphere.intersectsBox( box ); 77 | 78 | } else { 79 | 80 | return false; 81 | 82 | } 83 | 84 | } 85 | 86 | checkButton( button ) { 87 | 88 | if ( this.intersectBoxObject( button ) ) { 89 | 90 | button.onPress(); 91 | 92 | } else { 93 | 94 | button.onClear(); 95 | 96 | } 97 | 98 | if ( button.isPressed() ) { 99 | 100 | button.whilePressed(); 101 | 102 | } 103 | 104 | } 105 | 106 | } 107 | 108 | export { OculusHandModel }; 109 | -------------------------------------------------------------------------------- /examples/jsm/webxr/OculusHandPointerModel.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../../build/three.module.js'; 2 | 3 | const PINCH_MAX = 0.05; 4 | const PINCH_THRESHOLD = 0.02; 5 | const PINCH_MIN = 0.01; 6 | const POINTER_ADVANCE_MAX = 0.02; 7 | const POINTER_OPACITY_MAX = 1; 8 | const POINTER_OPACITY_MIN = 0.4; 9 | const POINTER_FRONT_RADIUS = 0.002; 10 | const POINTER_REAR_RADIUS = 0.01; 11 | const POINTER_REAR_RADIUS_MIN = 0.003; 12 | const POINTER_LENGTH = 0.035; 13 | const POINTER_SEGMENTS = 16; 14 | const POINTER_RINGS = 12; 15 | const POINTER_HEMISPHERE_ANGLE = 110; 16 | const YAXIS = new THREE.Vector3( 0, 1, 0 ); 17 | const ZAXIS = new THREE.Vector3( 0, 0, 1 ); 18 | 19 | const CURSOR_RADIUS = 0.02; 20 | const CURSOR_MAX_DISTANCE = 1.5; 21 | 22 | class OculusHandPointerModel extends THREE.Object3D { 23 | 24 | constructor( hand, controller ) { 25 | 26 | super(); 27 | 28 | this.hand = hand; 29 | this.controller = controller; 30 | this.motionController = null; 31 | this.envMap = null; 32 | 33 | this.mesh = null; 34 | 35 | this.pointerGeometry = null; 36 | this.pointerMesh = null; 37 | this.pointerObject = null; 38 | 39 | this.pinched = false; 40 | this.attached = false; 41 | 42 | this.cursorObject = null; 43 | 44 | this.raycaster = null; 45 | 46 | hand.addEventListener( 'connected', ( event ) => { 47 | 48 | const xrInputSource = event.data; 49 | if ( xrInputSource.hand ) { 50 | 51 | this.visible = true; 52 | this.xrInputSource = xrInputSource; 53 | 54 | this.createPointer(); 55 | 56 | } 57 | 58 | } ); 59 | 60 | } 61 | 62 | _drawVerticesRing( vertices, baseVector, ringIndex ) { 63 | 64 | const segmentVector = baseVector.clone(); 65 | for ( var i = 0; i < POINTER_SEGMENTS; i ++ ) { 66 | 67 | segmentVector.applyAxisAngle( ZAXIS, ( Math.PI * 2 ) / POINTER_SEGMENTS ); 68 | const vid = ringIndex * POINTER_SEGMENTS + i; 69 | vertices[ 3 * vid ] = segmentVector.x; 70 | vertices[ 3 * vid + 1 ] = segmentVector.y; 71 | vertices[ 3 * vid + 2 ] = segmentVector.z; 72 | 73 | } 74 | 75 | } 76 | 77 | _updatePointerVertices( rearRadius ) { 78 | 79 | const vertices = this.pointerGeometry.attributes.position.array; 80 | // first ring for front face 81 | const frontFaceBase = new THREE.Vector3( 82 | POINTER_FRONT_RADIUS, 83 | 0, 84 | - 1 * ( POINTER_LENGTH - rearRadius ) 85 | ); 86 | this._drawVerticesRing( vertices, frontFaceBase, 0 ); 87 | 88 | // rings for rear hemisphere 89 | const rearBase = new THREE.Vector3( 90 | Math.sin( ( Math.PI * POINTER_HEMISPHERE_ANGLE ) / 180 ) * rearRadius, 91 | Math.cos( ( Math.PI * POINTER_HEMISPHERE_ANGLE ) / 180 ) * rearRadius, 92 | 0 93 | ); 94 | for ( var i = 0; i < POINTER_RINGS; i ++ ) { 95 | 96 | this._drawVerticesRing( vertices, rearBase, i + 1 ); 97 | rearBase.applyAxisAngle( 98 | YAXIS, 99 | ( Math.PI * POINTER_HEMISPHERE_ANGLE ) / 180 / ( POINTER_RINGS * - 2 ) 100 | ); 101 | 102 | } 103 | 104 | // front and rear face center vertices 105 | const frontCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS ); 106 | const rearCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS ) + 1; 107 | const frontCenter = new THREE.Vector3( 108 | 0, 109 | 0, 110 | - 1 * ( POINTER_LENGTH - rearRadius ) 111 | ); 112 | vertices[ frontCenterIndex * 3 ] = frontCenter.x; 113 | vertices[ frontCenterIndex * 3 + 1 ] = frontCenter.y; 114 | vertices[ frontCenterIndex * 3 + 2 ] = frontCenter.z; 115 | const rearCenter = new THREE.Vector3( 0, 0, rearRadius ); 116 | vertices[ rearCenterIndex * 3 ] = rearCenter.x; 117 | vertices[ rearCenterIndex * 3 + 1 ] = rearCenter.y; 118 | vertices[ rearCenterIndex * 3 + 2 ] = rearCenter.z; 119 | 120 | this.pointerGeometry.setAttribute( 121 | 'position', 122 | new THREE.Float32BufferAttribute( vertices, 3 ) 123 | ); 124 | // verticesNeedUpdate = true; 125 | 126 | } 127 | 128 | createPointer() { 129 | 130 | var i, j; 131 | const vertices = new Array( 132 | ( ( POINTER_RINGS + 1 ) * POINTER_SEGMENTS + 2 ) * 3 133 | ).fill( 0 ); 134 | // const vertices = []; 135 | const indices = []; 136 | this.pointerGeometry = new THREE.BufferGeometry(); 137 | 138 | this.pointerGeometry.setAttribute( 139 | 'position', 140 | new THREE.Float32BufferAttribute( vertices, 3 ) 141 | ); 142 | 143 | this._updatePointerVertices( POINTER_REAR_RADIUS ); 144 | 145 | // construct faces to connect rings 146 | for ( i = 0; i < POINTER_RINGS; i ++ ) { 147 | 148 | for ( j = 0; j < POINTER_SEGMENTS - 1; j ++ ) { 149 | 150 | indices.push( 151 | i * POINTER_SEGMENTS + j, 152 | i * POINTER_SEGMENTS + j + 1, 153 | ( i + 1 ) * POINTER_SEGMENTS + j 154 | ); 155 | indices.push( 156 | i * POINTER_SEGMENTS + j + 1, 157 | ( i + 1 ) * POINTER_SEGMENTS + j + 1, 158 | ( i + 1 ) * POINTER_SEGMENTS + j 159 | ); 160 | 161 | } 162 | 163 | indices.push( 164 | ( i + 1 ) * POINTER_SEGMENTS - 1, 165 | i * POINTER_SEGMENTS, 166 | ( i + 2 ) * POINTER_SEGMENTS - 1 167 | ); 168 | indices.push( 169 | i * POINTER_SEGMENTS, 170 | ( i + 1 ) * POINTER_SEGMENTS, 171 | ( i + 2 ) * POINTER_SEGMENTS - 1 172 | ); 173 | 174 | } 175 | 176 | // construct front and rear face 177 | const frontCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS ); 178 | const rearCenterIndex = POINTER_SEGMENTS * ( 1 + POINTER_RINGS ) + 1; 179 | 180 | for ( i = 0; i < POINTER_SEGMENTS - 1; i ++ ) { 181 | 182 | indices.push( frontCenterIndex, i + 1, i ); 183 | indices.push( 184 | rearCenterIndex, 185 | i + POINTER_SEGMENTS * POINTER_RINGS, 186 | i + POINTER_SEGMENTS * POINTER_RINGS + 1 187 | ); 188 | 189 | } 190 | 191 | indices.push( frontCenterIndex, 0, POINTER_SEGMENTS - 1 ); 192 | indices.push( 193 | rearCenterIndex, 194 | POINTER_SEGMENTS * ( POINTER_RINGS + 1 ) - 1, 195 | POINTER_SEGMENTS * POINTER_RINGS 196 | ); 197 | 198 | const material = new THREE.MeshBasicMaterial(); 199 | material.transparent = true; 200 | material.opacity = POINTER_OPACITY_MIN; 201 | 202 | this.pointerGeometry.setIndex( indices ); 203 | 204 | this.pointerMesh = new THREE.Mesh( this.pointerGeometry, material ); 205 | 206 | this.pointerMesh.position.set( 0, 0, - 1 * POINTER_REAR_RADIUS ); 207 | this.pointerObject = new THREE.Object3D(); 208 | this.pointerObject.add( this.pointerMesh ); 209 | 210 | this.raycaster = new THREE.Raycaster(); 211 | 212 | // create cursor 213 | const cursorGeometry = new THREE.SphereGeometry( CURSOR_RADIUS, 10, 10 ); 214 | const cursorMaterial = new THREE.MeshBasicMaterial(); 215 | cursorMaterial.transparent = true; 216 | cursorMaterial.opacity = POINTER_OPACITY_MIN; 217 | 218 | this.cursorObject = new THREE.Mesh( cursorGeometry, cursorMaterial ); 219 | this.pointerObject.add( this.cursorObject ); 220 | 221 | this.add( this.pointerObject ); 222 | 223 | } 224 | 225 | _updateRaycaster() { 226 | 227 | if ( this.raycaster ) { 228 | 229 | const pointerMatrix = this.pointerObject.matrixWorld; 230 | const tempMatrix = new THREE.Matrix4(); 231 | tempMatrix.identity().extractRotation( pointerMatrix ); 232 | this.raycaster.ray.origin.setFromMatrixPosition( pointerMatrix ); 233 | this.raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix ); 234 | 235 | } 236 | 237 | } 238 | 239 | _updatePointer() { 240 | 241 | this.pointerObject.visible = this.controller.visible; 242 | const indexTip = this.hand.joints[ 'index-finger-tip' ]; 243 | const thumbTip = this.hand.joints[ 'thumb-tip' ]; 244 | const distance = indexTip.position.distanceTo( thumbTip.position ); 245 | const position = indexTip.position 246 | .clone() 247 | .add( thumbTip.position ) 248 | .multiplyScalar( 0.5 ); 249 | this.pointerObject.position.copy( position ); 250 | this.pointerObject.quaternion.copy( this.controller.quaternion ); 251 | 252 | this.pinched = distance <= PINCH_THRESHOLD; 253 | 254 | const pinchScale = ( distance - PINCH_MIN ) / ( PINCH_MAX - PINCH_MIN ); 255 | const focusScale = ( distance - PINCH_MIN ) / ( PINCH_THRESHOLD - PINCH_MIN ); 256 | if ( pinchScale > 1 ) { 257 | 258 | this._updatePointerVertices( POINTER_REAR_RADIUS ); 259 | this.pointerMesh.position.set( 0, 0, - 1 * POINTER_REAR_RADIUS ); 260 | this.pointerMesh.material.opacity = POINTER_OPACITY_MIN; 261 | 262 | } else if ( pinchScale > 0 ) { 263 | 264 | const rearRadius = 265 | ( POINTER_REAR_RADIUS - POINTER_REAR_RADIUS_MIN ) * pinchScale + 266 | POINTER_REAR_RADIUS_MIN; 267 | this._updatePointerVertices( rearRadius ); 268 | if ( focusScale < 1 ) { 269 | 270 | this.pointerMesh.position.set( 271 | 0, 272 | 0, 273 | - 1 * rearRadius - ( 1 - focusScale ) * POINTER_ADVANCE_MAX 274 | ); 275 | this.pointerMesh.material.opacity = 276 | POINTER_OPACITY_MIN + 277 | ( 1 - focusScale ) * ( POINTER_OPACITY_MAX - POINTER_OPACITY_MIN ); 278 | 279 | } else { 280 | 281 | this.pointerMesh.position.set( 0, 0, - 1 * rearRadius ); 282 | this.pointerMesh.material.opacity = POINTER_OPACITY_MIN; 283 | 284 | } 285 | 286 | } else { 287 | 288 | this._updatePointerVertices( POINTER_REAR_RADIUS_MIN ); 289 | this.pointerMesh.position.set( 290 | 0, 291 | 0, 292 | - 1 * POINTER_REAR_RADIUS_MIN - POINTER_ADVANCE_MAX 293 | ); 294 | this.pointerMesh.material.opacity = POINTER_OPACITY_MAX; 295 | 296 | } 297 | 298 | this.cursorObject.material.opacity = this.pointerMesh.material.opacity; 299 | 300 | } 301 | 302 | updateMatrixWorld( force ) { 303 | 304 | super.updateMatrixWorld( force ); 305 | if ( this.pointerGeometry ) { 306 | 307 | this._updatePointer(); 308 | this._updateRaycaster(); 309 | 310 | } 311 | 312 | } 313 | 314 | isPinched() { 315 | 316 | return this.pinched; 317 | 318 | } 319 | 320 | setAttached( attached ) { 321 | 322 | this.attached = attached; 323 | 324 | } 325 | 326 | isAttached() { 327 | 328 | return this.attached; 329 | 330 | } 331 | 332 | intersectObject( object ) { 333 | 334 | if ( this.raycaster ) { 335 | 336 | return this.raycaster.intersectObject( object ); 337 | 338 | } 339 | 340 | } 341 | 342 | intersectObjects( objects ) { 343 | 344 | if ( this.raycaster ) { 345 | 346 | return this.raycaster.intersectObjects( objects ); 347 | 348 | } 349 | 350 | } 351 | 352 | checkIntersections( objects ) { 353 | 354 | if ( this.raycaster && ! this.attached ) { 355 | 356 | const intersections = this.raycaster.intersectObjects( objects ); 357 | const direction = new THREE.Vector3( 0, 0, - 1 ); 358 | if ( intersections.length > 0 ) { 359 | 360 | const intersection = intersections[ 0 ]; 361 | const distance = intersection.distance; 362 | this.cursorObject.position.copy( direction.multiplyScalar( distance ) ); 363 | 364 | } else { 365 | 366 | this.cursorObject.position.copy( direction.multiplyScalar( CURSOR_MAX_DISTANCE ) ); 367 | 368 | } 369 | 370 | } 371 | 372 | } 373 | 374 | setCursor( distance ) { 375 | 376 | const direction = new THREE.Vector3( 0, 0, - 1 ); 377 | if ( this.raycaster && ! this.attached ) { 378 | 379 | this.cursorObject.position.copy( direction.multiplyScalar( distance ) ); 380 | 381 | } 382 | 383 | } 384 | 385 | } 386 | 387 | export { OculusHandPointerModel }; 388 | -------------------------------------------------------------------------------- /examples/jsm/webxr/Text2D.js: -------------------------------------------------------------------------------- 1 | import * as THREE from '../../../build/three.module.js'; 2 | 3 | function createText( message, height ) { 4 | 5 | const canvas = document.createElement( 'canvas' ); 6 | const context = canvas.getContext( '2d' ); 7 | let metrics = null; 8 | const textHeight = 100; 9 | context.font = 'normal ' + textHeight + 'px Arial'; 10 | metrics = context.measureText( message ); 11 | const textWidth = metrics.width; 12 | canvas.width = textWidth; 13 | canvas.height = textHeight; 14 | context.font = 'normal ' + textHeight + 'px Arial'; 15 | context.textAlign = 'center'; 16 | context.textBaseline = 'middle'; 17 | context.fillStyle = '#ffffff'; 18 | context.fillText( message, textWidth / 2, textHeight / 2 ); 19 | 20 | const texture = new THREE.Texture( canvas ); 21 | texture.needsUpdate = true; 22 | //var spriteAlignment = new THREE.Vector2(0,0) ; 23 | const material = new THREE.MeshBasicMaterial( { 24 | color: 0xffffff, 25 | side: THREE.DoubleSide, 26 | map: texture, 27 | transparent: true, 28 | } ); 29 | const geometry = new THREE.PlaneGeometry( 30 | ( height * textWidth ) / textHeight, 31 | height 32 | ); 33 | const plane = new THREE.Mesh( geometry, material ); 34 | return plane; 35 | 36 | } 37 | 38 | export { createText }; 39 | -------------------------------------------------------------------------------- /examples/jsm/webxr/VRButton.js: -------------------------------------------------------------------------------- 1 | class VRButton { 2 | 3 | static createButton( renderer, options ) { 4 | 5 | if ( options ) { 6 | 7 | console.error( 'THREE.VRButton: The "options" parameter has been removed. Please set the reference space type via renderer.xr.setReferenceSpaceType() instead.' ); 8 | 9 | } 10 | 11 | const button = document.createElement( 'button' ); 12 | 13 | function showEnterVR( /*device*/ ) { 14 | 15 | let currentSession = null; 16 | 17 | async function onSessionStarted( session ) { 18 | 19 | session.addEventListener( 'end', onSessionEnded ); 20 | 21 | await renderer.xr.setSession( session ); 22 | button.textContent = 'EXIT VR'; 23 | 24 | currentSession = session; 25 | 26 | } 27 | 28 | function onSessionEnded( /*event*/ ) { 29 | 30 | currentSession.removeEventListener( 'end', onSessionEnded ); 31 | 32 | button.textContent = 'ENTER VR'; 33 | 34 | currentSession = null; 35 | 36 | } 37 | 38 | // 39 | 40 | button.style.display = ''; 41 | 42 | button.style.cursor = 'pointer'; 43 | button.style.left = 'calc(50% - 50px)'; 44 | button.style.width = '100px'; 45 | 46 | button.textContent = 'ENTER VR'; 47 | 48 | button.onmouseenter = function () { 49 | 50 | button.style.opacity = '1.0'; 51 | 52 | }; 53 | 54 | button.onmouseleave = function () { 55 | 56 | button.style.opacity = '0.5'; 57 | 58 | }; 59 | 60 | button.onclick = function () { 61 | 62 | if ( currentSession === null ) { 63 | 64 | // WebXR's requestReferenceSpace only works if the corresponding feature 65 | // was requested at session creation time. For simplicity, just ask for 66 | // the interesting ones as optional features, but be aware that the 67 | // requestReferenceSpace call will fail if it turns out to be unavailable. 68 | // ('local' is always available for immersive sessions and doesn't need to 69 | // be requested separately.) 70 | 71 | const sessionInit = { optionalFeatures: [ 'local-floor', 'bounded-floor', 'hand-tracking' ] }; 72 | navigator.xr.requestSession( 'immersive-vr', sessionInit ).then( onSessionStarted ); 73 | 74 | } else { 75 | 76 | currentSession.end(); 77 | 78 | } 79 | 80 | }; 81 | 82 | } 83 | 84 | function disableButton() { 85 | 86 | button.style.display = ''; 87 | 88 | button.style.cursor = 'auto'; 89 | button.style.left = 'calc(50% - 75px)'; 90 | button.style.width = '150px'; 91 | 92 | button.onmouseenter = null; 93 | button.onmouseleave = null; 94 | 95 | button.onclick = null; 96 | 97 | } 98 | 99 | function showWebXRNotFound() { 100 | 101 | disableButton(); 102 | 103 | button.textContent = 'VR NOT SUPPORTED'; 104 | 105 | } 106 | 107 | function stylizeElement( element ) { 108 | 109 | element.style.position = 'absolute'; 110 | element.style.bottom = '20px'; 111 | element.style.padding = '12px 6px'; 112 | element.style.border = '1px solid #fff'; 113 | element.style.borderRadius = '4px'; 114 | element.style.background = 'rgba(0,0,0,0.1)'; 115 | element.style.color = '#fff'; 116 | element.style.font = 'normal 13px sans-serif'; 117 | element.style.textAlign = 'center'; 118 | element.style.opacity = '0.5'; 119 | element.style.outline = 'none'; 120 | element.style.zIndex = '999'; 121 | 122 | } 123 | 124 | if ( 'xr' in navigator ) { 125 | 126 | button.id = 'VRButton'; 127 | button.style.display = 'none'; 128 | 129 | stylizeElement( button ); 130 | 131 | navigator.xr.isSessionSupported( 'immersive-vr' ).then( function ( supported ) { 132 | 133 | supported ? showEnterVR() : showWebXRNotFound(); 134 | 135 | } ); 136 | 137 | return button; 138 | 139 | } else { 140 | 141 | const message = document.createElement( 'a' ); 142 | 143 | if ( window.isSecureContext === false ) { 144 | 145 | message.href = document.location.href.replace( /^http:/, 'https:' ); 146 | message.innerHTML = 'WEBXR NEEDS HTTPS'; // TODO Improve message 147 | 148 | } else { 149 | 150 | message.href = 'https://immersiveweb.dev/'; 151 | message.innerHTML = 'WEBXR NOT AVAILABLE'; 152 | 153 | } 154 | 155 | message.style.left = 'calc(50% - 90px)'; 156 | message.style.width = '180px'; 157 | message.style.textDecoration = 'none'; 158 | 159 | stylizeElement( message ); 160 | 161 | return message; 162 | 163 | } 164 | 165 | } 166 | 167 | } 168 | 169 | export { VRButton }; 170 | -------------------------------------------------------------------------------- /examples/jsm/webxr/XRControllerModelFactory.js: -------------------------------------------------------------------------------- 1 | import { 2 | Mesh, 3 | MeshBasicMaterial, 4 | Object3D, 5 | SphereGeometry, 6 | } from '../../../build/three.module.js'; 7 | 8 | import { GLTFLoader } from '../loaders/GLTFLoader.js'; 9 | 10 | import { 11 | Constants as MotionControllerConstants, 12 | fetchProfile, 13 | MotionController 14 | } from '../libs/motion-controllers.module.js'; 15 | 16 | const DEFAULT_PROFILES_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles'; 17 | const DEFAULT_PROFILE = 'generic-trigger'; 18 | 19 | class XRControllerModel extends Object3D { 20 | 21 | constructor() { 22 | 23 | super(); 24 | 25 | this.motionController = null; 26 | this.envMap = null; 27 | 28 | } 29 | 30 | setEnvironmentMap( envMap ) { 31 | 32 | if ( this.envMap == envMap ) { 33 | 34 | return this; 35 | 36 | } 37 | 38 | this.envMap = envMap; 39 | this.traverse( ( child ) => { 40 | 41 | if ( child.isMesh ) { 42 | 43 | child.material.envMap = this.envMap; 44 | child.material.needsUpdate = true; 45 | 46 | } 47 | 48 | } ); 49 | 50 | return this; 51 | 52 | } 53 | 54 | /** 55 | * Polls data from the XRInputSource and updates the model's components to match 56 | * the real world data 57 | */ 58 | updateMatrixWorld( force ) { 59 | 60 | super.updateMatrixWorld( force ); 61 | 62 | if ( ! this.motionController ) return; 63 | 64 | // Cause the MotionController to poll the Gamepad for data 65 | this.motionController.updateFromGamepad(); 66 | 67 | // Update the 3D model to reflect the button, thumbstick, and touchpad state 68 | Object.values( this.motionController.components ).forEach( ( component ) => { 69 | 70 | // Update node data based on the visual responses' current states 71 | Object.values( component.visualResponses ).forEach( ( visualResponse ) => { 72 | 73 | const { valueNode, minNode, maxNode, value, valueNodeProperty } = visualResponse; 74 | 75 | // Skip if the visual response node is not found. No error is needed, 76 | // because it will have been reported at load time. 77 | if ( ! valueNode ) return; 78 | 79 | // Calculate the new properties based on the weight supplied 80 | if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.VISIBILITY ) { 81 | 82 | valueNode.visible = value; 83 | 84 | } else if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) { 85 | 86 | valueNode.quaternion.slerpQuaternions( 87 | minNode.quaternion, 88 | maxNode.quaternion, 89 | value 90 | ); 91 | 92 | valueNode.position.lerpVectors( 93 | minNode.position, 94 | maxNode.position, 95 | value 96 | ); 97 | 98 | } 99 | 100 | } ); 101 | 102 | } ); 103 | 104 | } 105 | 106 | } 107 | 108 | /** 109 | * Walks the model's tree to find the nodes needed to animate the components and 110 | * saves them to the motionContoller components for use in the frame loop. When 111 | * touchpads are found, attaches a touch dot to them. 112 | */ 113 | function findNodes( motionController, scene ) { 114 | 115 | // Loop through the components and find the nodes needed for each components' visual responses 116 | Object.values( motionController.components ).forEach( ( component ) => { 117 | 118 | const { type, touchPointNodeName, visualResponses } = component; 119 | 120 | if ( type === MotionControllerConstants.ComponentType.TOUCHPAD ) { 121 | 122 | component.touchPointNode = scene.getObjectByName( touchPointNodeName ); 123 | if ( component.touchPointNode ) { 124 | 125 | // Attach a touch dot to the touchpad. 126 | const sphereGeometry = new SphereGeometry( 0.001 ); 127 | const material = new MeshBasicMaterial( { color: 0x0000FF } ); 128 | const sphere = new Mesh( sphereGeometry, material ); 129 | component.touchPointNode.add( sphere ); 130 | 131 | } else { 132 | 133 | console.warn( `Could not find touch dot, ${component.touchPointNodeName}, in touchpad component ${component.id}` ); 134 | 135 | } 136 | 137 | } 138 | 139 | // Loop through all the visual responses to be applied to this component 140 | Object.values( visualResponses ).forEach( ( visualResponse ) => { 141 | 142 | const { valueNodeName, minNodeName, maxNodeName, valueNodeProperty } = visualResponse; 143 | 144 | // If animating a transform, find the two nodes to be interpolated between. 145 | if ( valueNodeProperty === MotionControllerConstants.VisualResponseProperty.TRANSFORM ) { 146 | 147 | visualResponse.minNode = scene.getObjectByName( minNodeName ); 148 | visualResponse.maxNode = scene.getObjectByName( maxNodeName ); 149 | 150 | // If the extents cannot be found, skip this animation 151 | if ( ! visualResponse.minNode ) { 152 | 153 | console.warn( `Could not find ${minNodeName} in the model` ); 154 | return; 155 | 156 | } 157 | 158 | if ( ! visualResponse.maxNode ) { 159 | 160 | console.warn( `Could not find ${maxNodeName} in the model` ); 161 | return; 162 | 163 | } 164 | 165 | } 166 | 167 | // If the target node cannot be found, skip this animation 168 | visualResponse.valueNode = scene.getObjectByName( valueNodeName ); 169 | if ( ! visualResponse.valueNode ) { 170 | 171 | console.warn( `Could not find ${valueNodeName} in the model` ); 172 | 173 | } 174 | 175 | } ); 176 | 177 | } ); 178 | 179 | } 180 | 181 | function addAssetSceneToControllerModel( controllerModel, scene ) { 182 | 183 | // Find the nodes needed for animation and cache them on the motionController. 184 | findNodes( controllerModel.motionController, scene ); 185 | 186 | // Apply any environment map that the mesh already has set. 187 | if ( controllerModel.envMap ) { 188 | 189 | scene.traverse( ( child ) => { 190 | 191 | if ( child.isMesh ) { 192 | 193 | child.material.envMap = controllerModel.envMap; 194 | child.material.needsUpdate = true; 195 | 196 | } 197 | 198 | } ); 199 | 200 | } 201 | 202 | // Add the glTF scene to the controllerModel. 203 | controllerModel.add( scene ); 204 | 205 | } 206 | 207 | class XRControllerModelFactory { 208 | 209 | constructor( gltfLoader = null ) { 210 | 211 | this.gltfLoader = gltfLoader; 212 | this.path = DEFAULT_PROFILES_PATH; 213 | this._assetCache = {}; 214 | 215 | // If a GLTFLoader wasn't supplied to the constructor create a new one. 216 | if ( ! this.gltfLoader ) { 217 | 218 | this.gltfLoader = new GLTFLoader(); 219 | 220 | } 221 | 222 | } 223 | 224 | createControllerModel( controller ) { 225 | 226 | const controllerModel = new XRControllerModel(); 227 | let scene = null; 228 | 229 | controller.addEventListener( 'connected', ( event ) => { 230 | 231 | const xrInputSource = event.data; 232 | 233 | if ( xrInputSource.targetRayMode !== 'tracked-pointer' || ! xrInputSource.gamepad ) return; 234 | 235 | fetchProfile( xrInputSource, this.path, DEFAULT_PROFILE ).then( ( { profile, assetPath } ) => { 236 | 237 | controllerModel.motionController = new MotionController( 238 | xrInputSource, 239 | profile, 240 | assetPath 241 | ); 242 | 243 | const cachedAsset = this._assetCache[ controllerModel.motionController.assetUrl ]; 244 | if ( cachedAsset ) { 245 | 246 | scene = cachedAsset.scene.clone(); 247 | 248 | addAssetSceneToControllerModel( controllerModel, scene ); 249 | 250 | } else { 251 | 252 | if ( ! this.gltfLoader ) { 253 | 254 | throw new Error( 'GLTFLoader not set.' ); 255 | 256 | } 257 | 258 | this.gltfLoader.setPath( '' ); 259 | this.gltfLoader.load( controllerModel.motionController.assetUrl, ( asset ) => { 260 | 261 | this._assetCache[ controllerModel.motionController.assetUrl ] = asset; 262 | 263 | scene = asset.scene.clone(); 264 | 265 | addAssetSceneToControllerModel( controllerModel, scene ); 266 | 267 | }, 268 | null, 269 | () => { 270 | 271 | throw new Error( `Asset ${controllerModel.motionController.assetUrl} missing or malformed.` ); 272 | 273 | } ); 274 | 275 | } 276 | 277 | } ).catch( ( err ) => { 278 | 279 | console.warn( err ); 280 | 281 | } ); 282 | 283 | } ); 284 | 285 | controller.addEventListener( 'disconnected', () => { 286 | 287 | controllerModel.motionController = null; 288 | controllerModel.remove( scene ); 289 | scene = null; 290 | 291 | } ); 292 | 293 | return controllerModel; 294 | 295 | } 296 | 297 | } 298 | 299 | export { XRControllerModelFactory }; 300 | -------------------------------------------------------------------------------- /examples/jsm/webxr/XREstimatedLight.js: -------------------------------------------------------------------------------- 1 | import { 2 | DirectionalLight, 3 | Group, 4 | LightProbe, 5 | WebGLCubeRenderTarget 6 | } from '../../../build/three.module.js'; 7 | 8 | class SessionLightProbe { 9 | 10 | constructor( xrLight, renderer, lightProbe, environmentEstimation, estimationStartCallback ) { 11 | 12 | this.xrLight = xrLight; 13 | this.renderer = renderer; 14 | this.lightProbe = lightProbe; 15 | this.xrWebGLBinding = null; 16 | this.estimationStartCallback = estimationStartCallback; 17 | this.frameCallback = this.onXRFrame.bind( this ); 18 | 19 | const session = renderer.xr.getSession(); 20 | 21 | // If the XRWebGLBinding class is available then we can also query an 22 | // estimated reflection cube map. 23 | if ( environmentEstimation && 'XRWebGLBinding' in window ) { 24 | 25 | // This is the simplest way I know of to initialize a WebGL cubemap in Three. 26 | const cubeRenderTarget = new WebGLCubeRenderTarget( 16 ); 27 | xrLight.environment = cubeRenderTarget.texture; 28 | 29 | const gl = renderer.getContext(); 30 | 31 | // Ensure that we have any extensions needed to use the preferred cube map format. 32 | switch ( session.preferredReflectionFormat ) { 33 | 34 | case 'srgba8': 35 | gl.getExtension( 'EXT_sRGB' ); 36 | break; 37 | 38 | case 'rgba16f': 39 | gl.getExtension( 'OES_texture_half_float' ); 40 | break; 41 | 42 | } 43 | 44 | this.xrWebGLBinding = new XRWebGLBinding( session, gl ); 45 | 46 | this.lightProbe.addEventListener( 'reflectionchange', () => { 47 | 48 | this.updateReflection(); 49 | 50 | } ); 51 | 52 | } 53 | 54 | // Start monitoring the XR animation frame loop to look for lighting 55 | // estimation changes. 56 | session.requestAnimationFrame( this.frameCallback ); 57 | 58 | } 59 | 60 | updateReflection() { 61 | 62 | const textureProperties = this.renderer.properties.get( this.xrLight.environment ); 63 | 64 | if ( textureProperties ) { 65 | 66 | const cubeMap = this.xrWebGLBinding.getReflectionCubeMap( this.lightProbe ); 67 | 68 | if ( cubeMap ) { 69 | 70 | textureProperties.__webglTexture = cubeMap; 71 | 72 | } 73 | 74 | } 75 | 76 | } 77 | 78 | onXRFrame( time, xrFrame ) { 79 | 80 | // If either this obejct or the XREstimatedLight has been destroyed, stop 81 | // running the frame loop. 82 | if ( ! this.xrLight ) { 83 | 84 | return; 85 | 86 | } 87 | 88 | const session = xrFrame.session; 89 | session.requestAnimationFrame( this.frameCallback ); 90 | 91 | const lightEstimate = xrFrame.getLightEstimate( this.lightProbe ); 92 | if ( lightEstimate ) { 93 | 94 | // We can copy the estimate's spherical harmonics array directly into the light probe. 95 | this.xrLight.lightProbe.sh.fromArray( lightEstimate.sphericalHarmonicsCoefficients ); 96 | this.xrLight.lightProbe.intensity = 1.0; 97 | 98 | // For the directional light we have to normalize the color and set the scalar as the 99 | // intensity, since WebXR can return color values that exceed 1.0. 100 | const intensityScalar = Math.max( 1.0, 101 | Math.max( lightEstimate.primaryLightIntensity.x, 102 | Math.max( lightEstimate.primaryLightIntensity.y, 103 | lightEstimate.primaryLightIntensity.z ) ) ); 104 | 105 | this.xrLight.directionalLight.color.setRGB( 106 | lightEstimate.primaryLightIntensity.x / intensityScalar, 107 | lightEstimate.primaryLightIntensity.y / intensityScalar, 108 | lightEstimate.primaryLightIntensity.z / intensityScalar ); 109 | this.xrLight.directionalLight.intensity = intensityScalar; 110 | this.xrLight.directionalLight.position.copy( lightEstimate.primaryLightDirection ); 111 | 112 | if ( this.estimationStartCallback ) { 113 | 114 | this.estimationStartCallback(); 115 | this.estimationStartCallback = null; 116 | 117 | } 118 | 119 | } 120 | 121 | } 122 | 123 | dispose() { 124 | 125 | this.xrLight = null; 126 | this.renderer = null; 127 | this.lightProbe = null; 128 | this.xrWebGLBinding = null; 129 | 130 | } 131 | 132 | } 133 | 134 | export class XREstimatedLight extends Group { 135 | 136 | constructor( renderer, environmentEstimation = true ) { 137 | 138 | super(); 139 | 140 | this.lightProbe = new LightProbe(); 141 | this.lightProbe.intensity = 0; 142 | this.add( this.lightProbe ); 143 | 144 | this.directionalLight = new DirectionalLight(); 145 | this.directionalLight.intensity = 0; 146 | this.add( this.directionalLight ); 147 | 148 | // Will be set to a cube map in the SessionLightProbe is environment estimation is 149 | // available and requested. 150 | this.environment = null; 151 | 152 | let sessionLightProbe = null; 153 | let estimationStarted = false; 154 | renderer.xr.addEventListener( 'sessionstart', () => { 155 | 156 | const session = renderer.xr.getSession(); 157 | 158 | if ( 'requestLightProbe' in session ) { 159 | 160 | session.requestLightProbe( { 161 | 162 | reflectionFormat: session.preferredReflectionFormat 163 | 164 | } ).then( ( probe ) => { 165 | 166 | sessionLightProbe = new SessionLightProbe( this, renderer, probe, environmentEstimation, () => { 167 | 168 | estimationStarted = true; 169 | 170 | // Fired to indicate that the estimated lighting values are now being updated. 171 | this.dispatchEvent( { type: 'estimationstart' } ); 172 | 173 | } ); 174 | 175 | } ); 176 | 177 | } 178 | 179 | } ); 180 | 181 | renderer.xr.addEventListener( 'sessionend', () => { 182 | 183 | if ( sessionLightProbe ) { 184 | 185 | sessionLightProbe.dispose(); 186 | sessionLightProbe = null; 187 | 188 | } 189 | 190 | if ( estimationStarted ) { 191 | 192 | // Fired to indicate that the estimated lighting values are no longer being updated. 193 | this.dispatchEvent( { type: 'estimationend' } ); 194 | 195 | } 196 | 197 | } ); 198 | 199 | // Done inline to provide access to sessionLightProbe. 200 | this.dispose = () => { 201 | 202 | if ( sessionLightProbe ) { 203 | 204 | sessionLightProbe.dispose(); 205 | sessionLightProbe = null; 206 | 207 | } 208 | 209 | this.remove( this.lightProbe ); 210 | this.lightProbe = null; 211 | 212 | this.remove( this.directionalLight ); 213 | this.directionalLight = null; 214 | 215 | this.environment = null; 216 | 217 | }; 218 | 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /examples/jsm/webxr/XRHandMeshModel.js: -------------------------------------------------------------------------------- 1 | import { GLTFLoader } from '../loaders/GLTFLoader.js'; 2 | 3 | const DEFAULT_HAND_PROFILE_PATH = 'https://cdn.jsdelivr.net/npm/@webxr-input-profiles/assets@1.0/dist/profiles/generic-hand/'; 4 | 5 | class XRHandMeshModel { 6 | 7 | constructor( handModel, controller, path, handedness ) { 8 | 9 | this.controller = controller; 10 | this.handModel = handModel; 11 | 12 | this.bones = []; 13 | 14 | const loader = new GLTFLoader(); 15 | loader.setPath( path || DEFAULT_HAND_PROFILE_PATH ); 16 | loader.load( `${handedness}.glb`, gltf => { 17 | 18 | const object = gltf.scene.children[ 0 ]; 19 | this.handModel.add( object ); 20 | 21 | const mesh = object.getObjectByProperty( 'type', 'SkinnedMesh' ); 22 | mesh.frustumCulled = false; 23 | mesh.castShadow = true; 24 | mesh.receiveShadow = true; 25 | 26 | const joints = [ 27 | 'wrist', 28 | 'thumb-metacarpal', 29 | 'thumb-phalanx-proximal', 30 | 'thumb-phalanx-distal', 31 | 'thumb-tip', 32 | 'index-finger-metacarpal', 33 | 'index-finger-phalanx-proximal', 34 | 'index-finger-phalanx-intermediate', 35 | 'index-finger-phalanx-distal', 36 | 'index-finger-tip', 37 | 'middle-finger-metacarpal', 38 | 'middle-finger-phalanx-proximal', 39 | 'middle-finger-phalanx-intermediate', 40 | 'middle-finger-phalanx-distal', 41 | 'middle-finger-tip', 42 | 'ring-finger-metacarpal', 43 | 'ring-finger-phalanx-proximal', 44 | 'ring-finger-phalanx-intermediate', 45 | 'ring-finger-phalanx-distal', 46 | 'ring-finger-tip', 47 | 'pinky-finger-metacarpal', 48 | 'pinky-finger-phalanx-proximal', 49 | 'pinky-finger-phalanx-intermediate', 50 | 'pinky-finger-phalanx-distal', 51 | 'pinky-finger-tip', 52 | ]; 53 | 54 | joints.forEach( jointName => { 55 | 56 | const bone = object.getObjectByName( jointName ); 57 | 58 | if ( bone !== undefined ) { 59 | 60 | bone.jointName = jointName; 61 | 62 | } else { 63 | 64 | console.warn( `Couldn't find ${jointName} in ${handedness} hand mesh` ); 65 | 66 | } 67 | 68 | this.bones.push( bone ); 69 | 70 | } ); 71 | 72 | } ); 73 | 74 | } 75 | 76 | updateMesh() { 77 | 78 | // XR Joints 79 | const XRJoints = this.controller.joints; 80 | 81 | for ( let i = 0; i < this.bones.length; i ++ ) { 82 | 83 | const bone = this.bones[ i ]; 84 | 85 | if ( bone ) { 86 | 87 | const XRJoint = XRJoints[ bone.jointName ]; 88 | 89 | if ( XRJoint.visible ) { 90 | 91 | const position = XRJoint.position; 92 | 93 | if ( bone ) { 94 | 95 | bone.position.copy( position ); 96 | bone.quaternion.copy( XRJoint.quaternion ); 97 | // bone.scale.setScalar( XRJoint.jointRadius || defaultRadius ); 98 | 99 | } 100 | 101 | } 102 | 103 | } 104 | 105 | } 106 | 107 | } 108 | 109 | } 110 | 111 | export { XRHandMeshModel }; 112 | -------------------------------------------------------------------------------- /examples/jsm/webxr/XRHandModelFactory.js: -------------------------------------------------------------------------------- 1 | import { 2 | Object3D 3 | } from '../../../build/three.module.js'; 4 | 5 | import { 6 | XRHandPrimitiveModel 7 | } from './XRHandPrimitiveModel.js'; 8 | 9 | import { 10 | XRHandMeshModel 11 | } from './XRHandMeshModel.js'; 12 | 13 | class XRHandModel extends Object3D { 14 | 15 | constructor( controller ) { 16 | 17 | super(); 18 | 19 | this.controller = controller; 20 | this.motionController = null; 21 | this.envMap = null; 22 | 23 | this.mesh = null; 24 | 25 | } 26 | 27 | updateMatrixWorld( force ) { 28 | 29 | super.updateMatrixWorld( force ); 30 | 31 | if ( this.motionController ) { 32 | 33 | this.motionController.updateMesh(); 34 | 35 | } 36 | 37 | } 38 | 39 | } 40 | 41 | class XRHandModelFactory { 42 | 43 | constructor() { 44 | 45 | this.path = null; 46 | 47 | } 48 | 49 | setPath( path ) { 50 | 51 | this.path = path; 52 | 53 | return this; 54 | 55 | } 56 | 57 | createHandModel( controller, profile ) { 58 | 59 | const handModel = new XRHandModel( controller ); 60 | 61 | controller.addEventListener( 'connected', ( event ) => { 62 | 63 | const xrInputSource = event.data; 64 | 65 | if ( xrInputSource.hand && ! handModel.motionController ) { 66 | 67 | handModel.xrInputSource = xrInputSource; 68 | 69 | // @todo Detect profile if not provided 70 | if ( profile === undefined || profile === 'spheres' ) { 71 | 72 | handModel.motionController = new XRHandPrimitiveModel( handModel, controller, this.path, xrInputSource.handedness, { primitive: 'sphere' } ); 73 | 74 | } else if ( profile === 'boxes' ) { 75 | 76 | handModel.motionController = new XRHandPrimitiveModel( handModel, controller, this.path, xrInputSource.handedness, { primitive: 'box' } ); 77 | 78 | } else if ( profile === 'mesh' ) { 79 | 80 | handModel.motionController = new XRHandMeshModel( handModel, controller, this.path, xrInputSource.handedness ); 81 | 82 | } 83 | 84 | } 85 | 86 | } ); 87 | 88 | controller.addEventListener( 'disconnected', () => { 89 | 90 | // handModel.motionController = null; 91 | // handModel.remove( scene ); 92 | // scene = null; 93 | 94 | } ); 95 | 96 | return handModel; 97 | 98 | } 99 | 100 | } 101 | 102 | export { XRHandModelFactory }; 103 | -------------------------------------------------------------------------------- /examples/jsm/webxr/XRHandPrimitiveModel.js: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicDrawUsage, 3 | SphereGeometry, 4 | BoxGeometry, 5 | MeshStandardMaterial, 6 | InstancedMesh, 7 | Matrix4, 8 | Vector3 9 | } from '../../../build/three.module.js'; 10 | 11 | const _matrix = new Matrix4(); 12 | const _vector = new Vector3(); 13 | 14 | class XRHandPrimitiveModel { 15 | 16 | constructor( handModel, controller, path, handedness, options ) { 17 | 18 | this.controller = controller; 19 | this.handModel = handModel; 20 | this.envMap = null; 21 | 22 | let geometry; 23 | 24 | if ( ! options || ! options.primitive || options.primitive === 'sphere' ) { 25 | 26 | geometry = new SphereGeometry( 1, 10, 10 ); 27 | 28 | } else if ( options.primitive === 'box' ) { 29 | 30 | geometry = new BoxGeometry( 1, 1, 1 ); 31 | 32 | } 33 | 34 | const material = new MeshStandardMaterial(); 35 | 36 | this.handMesh = new InstancedMesh( geometry, material, 30 ); 37 | this.handMesh.instanceMatrix.setUsage( DynamicDrawUsage ); // will be updated every frame 38 | this.handMesh.castShadow = true; 39 | this.handMesh.receiveShadow = true; 40 | this.handModel.add( this.handMesh ); 41 | 42 | this.joints = [ 43 | 'wrist', 44 | 'thumb-metacarpal', 45 | 'thumb-phalanx-proximal', 46 | 'thumb-phalanx-distal', 47 | 'thumb-tip', 48 | 'index-finger-metacarpal', 49 | 'index-finger-phalanx-proximal', 50 | 'index-finger-phalanx-intermediate', 51 | 'index-finger-phalanx-distal', 52 | 'index-finger-tip', 53 | 'middle-finger-metacarpal', 54 | 'middle-finger-phalanx-proximal', 55 | 'middle-finger-phalanx-intermediate', 56 | 'middle-finger-phalanx-distal', 57 | 'middle-finger-tip', 58 | 'ring-finger-metacarpal', 59 | 'ring-finger-phalanx-proximal', 60 | 'ring-finger-phalanx-intermediate', 61 | 'ring-finger-phalanx-distal', 62 | 'ring-finger-tip', 63 | 'pinky-finger-metacarpal', 64 | 'pinky-finger-phalanx-proximal', 65 | 'pinky-finger-phalanx-intermediate', 66 | 'pinky-finger-phalanx-distal', 67 | 'pinky-finger-tip' 68 | ]; 69 | 70 | } 71 | 72 | updateMesh() { 73 | 74 | const defaultRadius = 0.008; 75 | const joints = this.controller.joints; 76 | 77 | let count = 0; 78 | 79 | for ( let i = 0; i < this.joints.length; i ++ ) { 80 | 81 | const joint = joints[ this.joints[ i ] ]; 82 | 83 | if ( joint.visible ) { 84 | 85 | _vector.setScalar( joint.jointRadius || defaultRadius ); 86 | _matrix.compose( joint.position, joint.quaternion, _vector ); 87 | this.handMesh.setMatrixAt( i, _matrix ); 88 | 89 | count ++; 90 | 91 | } 92 | 93 | } 94 | 95 | this.handMesh.count = count; 96 | this.handMesh.instanceMatrix.needsUpdate = true; 97 | 98 | } 99 | 100 | } 101 | 102 | export { XRHandPrimitiveModel }; 103 | -------------------------------------------------------------------------------- /examples/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #000; 4 | color: #fff; 5 | font-family: Monospace; 6 | font-size: 13px; 7 | line-height: 24px; 8 | overscroll-behavior: none; 9 | } 10 | 11 | a { 12 | color: #ff0; 13 | text-decoration: none; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | button { 21 | cursor: pointer; 22 | text-transform: uppercase; 23 | } 24 | 25 | #info { 26 | position: absolute; 27 | top: 0px; 28 | width: 100%; 29 | padding: 10px; 30 | box-sizing: border-box; 31 | text-align: center; 32 | -moz-user-select: none; 33 | -webkit-user-select: none; 34 | -ms-user-select: none; 35 | user-select: none; 36 | pointer-events: none; 37 | z-index: 1; /* TODO Solve this in HTML */ 38 | } 39 | 40 | a, button, input, select { 41 | pointer-events: auto; 42 | } 43 | 44 | .dg.ac { 45 | -moz-user-select: none; 46 | -webkit-user-select: none; 47 | -ms-user-select: none; 48 | user-select: none; 49 | z-index: 2 !important; /* TODO Solve this in HTML */ 50 | } 51 | 52 | #overlay { 53 | position: absolute; 54 | font-size: 16px; 55 | z-index: 2; 56 | top: 0; 57 | left: 0; 58 | width: 100%; 59 | height: 100%; 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | flex-direction: column; 64 | background: rgba(0,0,0,0.7); 65 | } 66 | 67 | #overlay button { 68 | background: transparent; 69 | border: 0; 70 | border: 1px solid rgb(255, 255, 255); 71 | border-radius: 4px; 72 | color: #ffffff; 73 | padding: 12px 18px; 74 | text-transform: uppercase; 75 | cursor: pointer; 76 | } 77 | 78 | #notSupported { 79 | width: 50%; 80 | margin: auto; 81 | background-color: #f00; 82 | margin-top: 20px; 83 | padding: 10px; 84 | } 85 | -------------------------------------------------------------------------------- /examples/threejs_vr_hand_input.html: -------------------------------------------------------------------------------- 1 | 52 | 53 | 54 | 55 | 56 | three.js vr Hand Input 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | three.js VR Hand Input
65 | (Oculus Browser 15.4+, no controllers, use hand tracking to enter VR) 66 |
67 | 68 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /examples/threejs_vr_hand_input2.html: -------------------------------------------------------------------------------- 1 | 52 | 53 | 54 | 55 | 56 | three.js vr Hand Input 2 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 | three.js VR Hand Input 2
65 | (Oculus Browser 15.4+, no controllers, use hand tracking to enter VR) 66 |
67 | 68 | 201 | 202 | 203 | -------------------------------------------------------------------------------- /images/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Physicslibrary/Threejs-VR-Hand-Input/52e000cc6e17e84d00d1fe74cb2ce250f577d2b8/images/1.gif -------------------------------------------------------------------------------- /images/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Physicslibrary/Threejs-VR-Hand-Input/52e000cc6e17e84d00d1fe74cb2ce250f577d2b8/images/2.gif --------------------------------------------------------------------------------