├── 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 |