├── LICENSE ├── README.md ├── assets └── textures │ └── dot.png ├── index.html ├── js ├── controls │ └── OrbitControls.js ├── main.js └── three.r87.min.js └── screenshots ├── Spherical Bias 1.png ├── Spherical bias 0.5.png ├── Spherical.png ├── Uniform.png └── Vec3.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David Lochhead 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 | # Random Position On The Surface Of A Sphere 2 | Sometimes you just need to postion things on the surface of a sphere. Here are a few methods I've used over the years. 3 | 4 | Blog post coming soon. 5 | 6 | getCartesianPositions(100000, 50) Note the vertical clustering. 😬 ![getCartesianPositions(100000, 50)](screenshots/Vec3.png?raw=true) 7 | getSphericalPositions(100000, 50) Note the clustering at the poles. 😠 ![getSphericalPositions(100000, 50)](screenshots/Spherical.png?raw=true) 8 | getSphericalPositionsWithBias(100000, 50, 1) Clustering at the equator! 😢 ![getSphericalPositionsWithBias(100000, 50, 1)](screenshots/Spherical%20Bias%201.png?raw=true) 9 | getSphericalPositionsWithBias(100000, 50, 0.5) 🙂 ![getSphericalPositionsWithBias(100000, 50, 0.5)](screenshots/Spherical%20bias%200.5.png?raw=true) 10 | getUniformPositions(10000, 50) ![getUniformPositions(10000, 50)](screenshots/Uniform.png?raw=true) 11 | -------------------------------------------------------------------------------- /assets/textures/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defmech/random-position-on-the-surface-of-a-sphere/fe5cd9cadb72b023b5799ba63153a3b7a9297b35/assets/textures/dot.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Random Position On The Surface Of A Sphere 5 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /js/controls/OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | 9 | // This set of controls performs orbiting, dollying (zooming), and panning. 10 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 11 | // 12 | // Orbit - left mouse / touch: one finger move 13 | // Zoom - middle mouse, or mousewheel / touch: two finger spread or squish 14 | // Pan - right mouse, or arrow keys / touch: three finger swipe 15 | 16 | THREE.OrbitControls = function ( object, domElement ) { 17 | 18 | this.object = object; 19 | 20 | this.domElement = ( domElement !== undefined ) ? domElement : document; 21 | 22 | // Set to false to disable this control 23 | this.enabled = true; 24 | 25 | // "target" sets the location of focus, where the object orbits around 26 | this.target = new THREE.Vector3(); 27 | 28 | // How far you can dolly in and out ( PerspectiveCamera only ) 29 | this.minDistance = 0; 30 | this.maxDistance = Infinity; 31 | 32 | // How far you can zoom in and out ( OrthographicCamera only ) 33 | this.minZoom = 0; 34 | this.maxZoom = Infinity; 35 | 36 | // How far you can orbit vertically, upper and lower limits. 37 | // Range is 0 to Math.PI radians. 38 | this.minPolarAngle = 0; // radians 39 | this.maxPolarAngle = Math.PI; // radians 40 | 41 | // How far you can orbit horizontally, upper and lower limits. 42 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 43 | this.minAzimuthAngle = - Infinity; // radians 44 | this.maxAzimuthAngle = Infinity; // radians 45 | 46 | // Set to true to enable damping (inertia) 47 | // If damping is enabled, you must call controls.update() in your animation loop 48 | this.enableDamping = false; 49 | this.dampingFactor = 0.25; 50 | 51 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 52 | // Set to false to disable zooming 53 | this.enableZoom = true; 54 | this.zoomSpeed = 1.0; 55 | 56 | // Set to false to disable rotating 57 | this.enableRotate = true; 58 | this.rotateSpeed = 1.0; 59 | 60 | // Set to false to disable panning 61 | this.enablePan = true; 62 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 63 | 64 | // Set to true to automatically rotate around the target 65 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 66 | this.autoRotate = false; 67 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 68 | 69 | // Set to false to disable use of the keys 70 | this.enableKeys = true; 71 | 72 | // The four arrow keys 73 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 74 | 75 | // Mouse buttons 76 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 77 | 78 | // for reset 79 | this.target0 = this.target.clone(); 80 | this.position0 = this.object.position.clone(); 81 | this.zoom0 = this.object.zoom; 82 | 83 | // 84 | // public methods 85 | // 86 | 87 | this.getPolarAngle = function () { 88 | 89 | return spherical.phi; 90 | 91 | }; 92 | 93 | this.getAzimuthalAngle = function () { 94 | 95 | return spherical.theta; 96 | 97 | }; 98 | 99 | this.saveState = function () { 100 | 101 | scope.target0.copy( scope.target ); 102 | scope.position0.copy( scope.object.position ); 103 | scope.zoom0 = scope.object.zoom; 104 | 105 | }; 106 | 107 | this.reset = function () { 108 | 109 | scope.target.copy( scope.target0 ); 110 | scope.object.position.copy( scope.position0 ); 111 | scope.object.zoom = scope.zoom0; 112 | 113 | scope.object.updateProjectionMatrix(); 114 | scope.dispatchEvent( changeEvent ); 115 | 116 | scope.update(); 117 | 118 | state = STATE.NONE; 119 | 120 | }; 121 | 122 | // this method is exposed, but perhaps it would be better if we can make it private... 123 | this.update = function () { 124 | 125 | var offset = new THREE.Vector3(); 126 | 127 | // so camera.up is the orbit axis 128 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 129 | var quatInverse = quat.clone().inverse(); 130 | 131 | var lastPosition = new THREE.Vector3(); 132 | var lastQuaternion = new THREE.Quaternion(); 133 | 134 | return function update() { 135 | 136 | var position = scope.object.position; 137 | 138 | offset.copy( position ).sub( scope.target ); 139 | 140 | // rotate offset to "y-axis-is-up" space 141 | offset.applyQuaternion( quat ); 142 | 143 | // angle from z-axis around y-axis 144 | spherical.setFromVector3( offset ); 145 | 146 | if ( scope.autoRotate && state === STATE.NONE ) { 147 | 148 | rotateLeft( getAutoRotationAngle() ); 149 | 150 | } 151 | 152 | spherical.theta += sphericalDelta.theta; 153 | spherical.phi += sphericalDelta.phi; 154 | 155 | // restrict theta to be between desired limits 156 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 157 | 158 | // restrict phi to be between desired limits 159 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 160 | 161 | spherical.makeSafe(); 162 | 163 | 164 | spherical.radius *= scale; 165 | 166 | // restrict radius to be between desired limits 167 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 168 | 169 | // move target to panned location 170 | scope.target.add( panOffset ); 171 | 172 | offset.setFromSpherical( spherical ); 173 | 174 | // rotate offset back to "camera-up-vector-is-up" space 175 | offset.applyQuaternion( quatInverse ); 176 | 177 | position.copy( scope.target ).add( offset ); 178 | 179 | scope.object.lookAt( scope.target ); 180 | 181 | if ( scope.enableDamping === true ) { 182 | 183 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 184 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 185 | 186 | } else { 187 | 188 | sphericalDelta.set( 0, 0, 0 ); 189 | 190 | } 191 | 192 | scale = 1; 193 | panOffset.set( 0, 0, 0 ); 194 | 195 | // update condition is: 196 | // min(camera displacement, camera rotation in radians)^2 > EPS 197 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 198 | 199 | if ( zoomChanged || 200 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 201 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 202 | 203 | scope.dispatchEvent( changeEvent ); 204 | 205 | lastPosition.copy( scope.object.position ); 206 | lastQuaternion.copy( scope.object.quaternion ); 207 | zoomChanged = false; 208 | 209 | return true; 210 | 211 | } 212 | 213 | return false; 214 | 215 | }; 216 | 217 | }(); 218 | 219 | this.dispose = function () { 220 | 221 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 222 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 223 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 224 | 225 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 226 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 227 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 228 | 229 | document.removeEventListener( 'mousemove', onMouseMove, false ); 230 | document.removeEventListener( 'mouseup', onMouseUp, false ); 231 | 232 | window.removeEventListener( 'keydown', onKeyDown, false ); 233 | 234 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 235 | 236 | }; 237 | 238 | // 239 | // internals 240 | // 241 | 242 | var scope = this; 243 | 244 | var changeEvent = { type: 'change' }; 245 | var startEvent = { type: 'start' }; 246 | var endEvent = { type: 'end' }; 247 | 248 | var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY: 4, TOUCH_PAN: 5 }; 249 | 250 | var state = STATE.NONE; 251 | 252 | var EPS = 0.000001; 253 | 254 | // current position in spherical coordinates 255 | var spherical = new THREE.Spherical(); 256 | var sphericalDelta = new THREE.Spherical(); 257 | 258 | var scale = 1; 259 | var panOffset = new THREE.Vector3(); 260 | var zoomChanged = false; 261 | 262 | var rotateStart = new THREE.Vector2(); 263 | var rotateEnd = new THREE.Vector2(); 264 | var rotateDelta = new THREE.Vector2(); 265 | 266 | var panStart = new THREE.Vector2(); 267 | var panEnd = new THREE.Vector2(); 268 | var panDelta = new THREE.Vector2(); 269 | 270 | var dollyStart = new THREE.Vector2(); 271 | var dollyEnd = new THREE.Vector2(); 272 | var dollyDelta = new THREE.Vector2(); 273 | 274 | function getAutoRotationAngle() { 275 | 276 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 277 | 278 | } 279 | 280 | function getZoomScale() { 281 | 282 | return Math.pow( 0.95, scope.zoomSpeed ); 283 | 284 | } 285 | 286 | function rotateLeft( angle ) { 287 | 288 | sphericalDelta.theta -= angle; 289 | 290 | } 291 | 292 | function rotateUp( angle ) { 293 | 294 | sphericalDelta.phi -= angle; 295 | 296 | } 297 | 298 | var panLeft = function () { 299 | 300 | var v = new THREE.Vector3(); 301 | 302 | return function panLeft( distance, objectMatrix ) { 303 | 304 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 305 | v.multiplyScalar( - distance ); 306 | 307 | panOffset.add( v ); 308 | 309 | }; 310 | 311 | }(); 312 | 313 | var panUp = function () { 314 | 315 | var v = new THREE.Vector3(); 316 | 317 | return function panUp( distance, objectMatrix ) { 318 | 319 | v.setFromMatrixColumn( objectMatrix, 1 ); // get Y column of objectMatrix 320 | v.multiplyScalar( distance ); 321 | 322 | panOffset.add( v ); 323 | 324 | }; 325 | 326 | }(); 327 | 328 | // deltaX and deltaY are in pixels; right and down are positive 329 | var pan = function () { 330 | 331 | var offset = new THREE.Vector3(); 332 | 333 | return function pan( deltaX, deltaY ) { 334 | 335 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 336 | 337 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 338 | 339 | // perspective 340 | var position = scope.object.position; 341 | offset.copy( position ).sub( scope.target ); 342 | var targetDistance = offset.length(); 343 | 344 | // half of the fov is center to top of screen 345 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 346 | 347 | // we actually don't use screenWidth, since perspective camera is fixed to screen height 348 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 349 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 350 | 351 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 352 | 353 | // orthographic 354 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 355 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 356 | 357 | } else { 358 | 359 | // camera neither orthographic nor perspective 360 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 361 | scope.enablePan = false; 362 | 363 | } 364 | 365 | }; 366 | 367 | }(); 368 | 369 | function dollyIn( dollyScale ) { 370 | 371 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 372 | 373 | scale /= dollyScale; 374 | 375 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 376 | 377 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 378 | scope.object.updateProjectionMatrix(); 379 | zoomChanged = true; 380 | 381 | } else { 382 | 383 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 384 | scope.enableZoom = false; 385 | 386 | } 387 | 388 | } 389 | 390 | function dollyOut( dollyScale ) { 391 | 392 | if ( scope.object instanceof THREE.PerspectiveCamera ) { 393 | 394 | scale *= dollyScale; 395 | 396 | } else if ( scope.object instanceof THREE.OrthographicCamera ) { 397 | 398 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 399 | scope.object.updateProjectionMatrix(); 400 | zoomChanged = true; 401 | 402 | } else { 403 | 404 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 405 | scope.enableZoom = false; 406 | 407 | } 408 | 409 | } 410 | 411 | // 412 | // event callbacks - update the object state 413 | // 414 | 415 | function handleMouseDownRotate( event ) { 416 | 417 | //console.log( 'handleMouseDownRotate' ); 418 | 419 | rotateStart.set( event.clientX, event.clientY ); 420 | 421 | } 422 | 423 | function handleMouseDownDolly( event ) { 424 | 425 | //console.log( 'handleMouseDownDolly' ); 426 | 427 | dollyStart.set( event.clientX, event.clientY ); 428 | 429 | } 430 | 431 | function handleMouseDownPan( event ) { 432 | 433 | //console.log( 'handleMouseDownPan' ); 434 | 435 | panStart.set( event.clientX, event.clientY ); 436 | 437 | } 438 | 439 | function handleMouseMoveRotate( event ) { 440 | 441 | //console.log( 'handleMouseMoveRotate' ); 442 | 443 | rotateEnd.set( event.clientX, event.clientY ); 444 | rotateDelta.subVectors( rotateEnd, rotateStart ); 445 | 446 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 447 | 448 | // rotating across whole screen goes 360 degrees around 449 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 450 | 451 | // rotating up and down along whole screen attempts to go 360, but limited to 180 452 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 453 | 454 | rotateStart.copy( rotateEnd ); 455 | 456 | scope.update(); 457 | 458 | } 459 | 460 | function handleMouseMoveDolly( event ) { 461 | 462 | //console.log( 'handleMouseMoveDolly' ); 463 | 464 | dollyEnd.set( event.clientX, event.clientY ); 465 | 466 | dollyDelta.subVectors( dollyEnd, dollyStart ); 467 | 468 | if ( dollyDelta.y > 0 ) { 469 | 470 | dollyIn( getZoomScale() ); 471 | 472 | } else if ( dollyDelta.y < 0 ) { 473 | 474 | dollyOut( getZoomScale() ); 475 | 476 | } 477 | 478 | dollyStart.copy( dollyEnd ); 479 | 480 | scope.update(); 481 | 482 | } 483 | 484 | function handleMouseMovePan( event ) { 485 | 486 | //console.log( 'handleMouseMovePan' ); 487 | 488 | panEnd.set( event.clientX, event.clientY ); 489 | 490 | panDelta.subVectors( panEnd, panStart ); 491 | 492 | pan( panDelta.x, panDelta.y ); 493 | 494 | panStart.copy( panEnd ); 495 | 496 | scope.update(); 497 | 498 | } 499 | 500 | function handleMouseUp( event ) { 501 | 502 | // console.log( 'handleMouseUp' ); 503 | 504 | } 505 | 506 | function handleMouseWheel( event ) { 507 | 508 | // console.log( 'handleMouseWheel' ); 509 | 510 | if ( event.deltaY < 0 ) { 511 | 512 | dollyOut( getZoomScale() ); 513 | 514 | } else if ( event.deltaY > 0 ) { 515 | 516 | dollyIn( getZoomScale() ); 517 | 518 | } 519 | 520 | scope.update(); 521 | 522 | } 523 | 524 | function handleKeyDown( event ) { 525 | 526 | //console.log( 'handleKeyDown' ); 527 | 528 | switch ( event.keyCode ) { 529 | 530 | case scope.keys.UP: 531 | pan( 0, scope.keyPanSpeed ); 532 | scope.update(); 533 | break; 534 | 535 | case scope.keys.BOTTOM: 536 | pan( 0, - scope.keyPanSpeed ); 537 | scope.update(); 538 | break; 539 | 540 | case scope.keys.LEFT: 541 | pan( scope.keyPanSpeed, 0 ); 542 | scope.update(); 543 | break; 544 | 545 | case scope.keys.RIGHT: 546 | pan( - scope.keyPanSpeed, 0 ); 547 | scope.update(); 548 | break; 549 | 550 | } 551 | 552 | } 553 | 554 | function handleTouchStartRotate( event ) { 555 | 556 | //console.log( 'handleTouchStartRotate' ); 557 | 558 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 559 | 560 | } 561 | 562 | function handleTouchStartDolly( event ) { 563 | 564 | //console.log( 'handleTouchStartDolly' ); 565 | 566 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 567 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 568 | 569 | var distance = Math.sqrt( dx * dx + dy * dy ); 570 | 571 | dollyStart.set( 0, distance ); 572 | 573 | } 574 | 575 | function handleTouchStartPan( event ) { 576 | 577 | //console.log( 'handleTouchStartPan' ); 578 | 579 | panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 580 | 581 | } 582 | 583 | function handleTouchMoveRotate( event ) { 584 | 585 | //console.log( 'handleTouchMoveRotate' ); 586 | 587 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 588 | rotateDelta.subVectors( rotateEnd, rotateStart ); 589 | 590 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 591 | 592 | // rotating across whole screen goes 360 degrees around 593 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth * scope.rotateSpeed ); 594 | 595 | // rotating up and down along whole screen attempts to go 360, but limited to 180 596 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight * scope.rotateSpeed ); 597 | 598 | rotateStart.copy( rotateEnd ); 599 | 600 | scope.update(); 601 | 602 | } 603 | 604 | function handleTouchMoveDolly( event ) { 605 | 606 | //console.log( 'handleTouchMoveDolly' ); 607 | 608 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 609 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 610 | 611 | var distance = Math.sqrt( dx * dx + dy * dy ); 612 | 613 | dollyEnd.set( 0, distance ); 614 | 615 | dollyDelta.subVectors( dollyEnd, dollyStart ); 616 | 617 | if ( dollyDelta.y > 0 ) { 618 | 619 | dollyOut( getZoomScale() ); 620 | 621 | } else if ( dollyDelta.y < 0 ) { 622 | 623 | dollyIn( getZoomScale() ); 624 | 625 | } 626 | 627 | dollyStart.copy( dollyEnd ); 628 | 629 | scope.update(); 630 | 631 | } 632 | 633 | function handleTouchMovePan( event ) { 634 | 635 | //console.log( 'handleTouchMovePan' ); 636 | 637 | panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 638 | 639 | panDelta.subVectors( panEnd, panStart ); 640 | 641 | pan( panDelta.x, panDelta.y ); 642 | 643 | panStart.copy( panEnd ); 644 | 645 | scope.update(); 646 | 647 | } 648 | 649 | function handleTouchEnd( event ) { 650 | 651 | //console.log( 'handleTouchEnd' ); 652 | 653 | } 654 | 655 | // 656 | // event handlers - FSM: listen for events and reset state 657 | // 658 | 659 | function onMouseDown( event ) { 660 | 661 | if ( scope.enabled === false ) return; 662 | 663 | event.preventDefault(); 664 | 665 | switch ( event.button ) { 666 | 667 | case scope.mouseButtons.ORBIT: 668 | 669 | if ( scope.enableRotate === false ) return; 670 | 671 | handleMouseDownRotate( event ); 672 | 673 | state = STATE.ROTATE; 674 | 675 | break; 676 | 677 | case scope.mouseButtons.ZOOM: 678 | 679 | if ( scope.enableZoom === false ) return; 680 | 681 | handleMouseDownDolly( event ); 682 | 683 | state = STATE.DOLLY; 684 | 685 | break; 686 | 687 | case scope.mouseButtons.PAN: 688 | 689 | if ( scope.enablePan === false ) return; 690 | 691 | handleMouseDownPan( event ); 692 | 693 | state = STATE.PAN; 694 | 695 | break; 696 | 697 | } 698 | 699 | if ( state !== STATE.NONE ) { 700 | 701 | document.addEventListener( 'mousemove', onMouseMove, false ); 702 | document.addEventListener( 'mouseup', onMouseUp, false ); 703 | 704 | scope.dispatchEvent( startEvent ); 705 | 706 | } 707 | 708 | } 709 | 710 | function onMouseMove( event ) { 711 | 712 | if ( scope.enabled === false ) return; 713 | 714 | event.preventDefault(); 715 | 716 | switch ( state ) { 717 | 718 | case STATE.ROTATE: 719 | 720 | if ( scope.enableRotate === false ) return; 721 | 722 | handleMouseMoveRotate( event ); 723 | 724 | break; 725 | 726 | case STATE.DOLLY: 727 | 728 | if ( scope.enableZoom === false ) return; 729 | 730 | handleMouseMoveDolly( event ); 731 | 732 | break; 733 | 734 | case STATE.PAN: 735 | 736 | if ( scope.enablePan === false ) return; 737 | 738 | handleMouseMovePan( event ); 739 | 740 | break; 741 | 742 | } 743 | 744 | } 745 | 746 | function onMouseUp( event ) { 747 | 748 | if ( scope.enabled === false ) return; 749 | 750 | handleMouseUp( event ); 751 | 752 | document.removeEventListener( 'mousemove', onMouseMove, false ); 753 | document.removeEventListener( 'mouseup', onMouseUp, false ); 754 | 755 | scope.dispatchEvent( endEvent ); 756 | 757 | state = STATE.NONE; 758 | 759 | } 760 | 761 | function onMouseWheel( event ) { 762 | 763 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 764 | 765 | event.preventDefault(); 766 | event.stopPropagation(); 767 | 768 | handleMouseWheel( event ); 769 | 770 | scope.dispatchEvent( startEvent ); // not sure why these are here... 771 | scope.dispatchEvent( endEvent ); 772 | 773 | } 774 | 775 | function onKeyDown( event ) { 776 | 777 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 778 | 779 | handleKeyDown( event ); 780 | 781 | } 782 | 783 | function onTouchStart( event ) { 784 | 785 | if ( scope.enabled === false ) return; 786 | 787 | switch ( event.touches.length ) { 788 | 789 | case 1: // one-fingered touch: rotate 790 | 791 | if ( scope.enableRotate === false ) return; 792 | 793 | handleTouchStartRotate( event ); 794 | 795 | state = STATE.TOUCH_ROTATE; 796 | 797 | break; 798 | 799 | case 2: // two-fingered touch: dolly 800 | 801 | if ( scope.enableZoom === false ) return; 802 | 803 | handleTouchStartDolly( event ); 804 | 805 | state = STATE.TOUCH_DOLLY; 806 | 807 | break; 808 | 809 | case 3: // three-fingered touch: pan 810 | 811 | if ( scope.enablePan === false ) return; 812 | 813 | handleTouchStartPan( event ); 814 | 815 | state = STATE.TOUCH_PAN; 816 | 817 | break; 818 | 819 | default: 820 | 821 | state = STATE.NONE; 822 | 823 | } 824 | 825 | if ( state !== STATE.NONE ) { 826 | 827 | scope.dispatchEvent( startEvent ); 828 | 829 | } 830 | 831 | } 832 | 833 | function onTouchMove( event ) { 834 | 835 | if ( scope.enabled === false ) return; 836 | 837 | event.preventDefault(); 838 | event.stopPropagation(); 839 | 840 | switch ( event.touches.length ) { 841 | 842 | case 1: // one-fingered touch: rotate 843 | 844 | if ( scope.enableRotate === false ) return; 845 | if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed?... 846 | 847 | handleTouchMoveRotate( event ); 848 | 849 | break; 850 | 851 | case 2: // two-fingered touch: dolly 852 | 853 | if ( scope.enableZoom === false ) return; 854 | if ( state !== STATE.TOUCH_DOLLY ) return; // is this needed?... 855 | 856 | handleTouchMoveDolly( event ); 857 | 858 | break; 859 | 860 | case 3: // three-fingered touch: pan 861 | 862 | if ( scope.enablePan === false ) return; 863 | if ( state !== STATE.TOUCH_PAN ) return; // is this needed?... 864 | 865 | handleTouchMovePan( event ); 866 | 867 | break; 868 | 869 | default: 870 | 871 | state = STATE.NONE; 872 | 873 | } 874 | 875 | } 876 | 877 | function onTouchEnd( event ) { 878 | 879 | if ( scope.enabled === false ) return; 880 | 881 | handleTouchEnd( event ); 882 | 883 | scope.dispatchEvent( endEvent ); 884 | 885 | state = STATE.NONE; 886 | 887 | } 888 | 889 | function onContextMenu( event ) { 890 | 891 | if ( scope.enabled === false ) return; 892 | 893 | event.preventDefault(); 894 | 895 | } 896 | 897 | // 898 | 899 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 900 | 901 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 902 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 903 | 904 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 905 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 906 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 907 | 908 | window.addEventListener( 'keydown', onKeyDown, false ); 909 | 910 | // force an update at start 911 | 912 | this.update(); 913 | 914 | }; 915 | 916 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 917 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 918 | 919 | Object.defineProperties( THREE.OrbitControls.prototype, { 920 | 921 | center: { 922 | 923 | get: function () { 924 | 925 | console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); 926 | return this.target; 927 | 928 | } 929 | 930 | }, 931 | 932 | // backward compatibility 933 | 934 | noZoom: { 935 | 936 | get: function () { 937 | 938 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 939 | return ! this.enableZoom; 940 | 941 | }, 942 | 943 | set: function ( value ) { 944 | 945 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 946 | this.enableZoom = ! value; 947 | 948 | } 949 | 950 | }, 951 | 952 | noRotate: { 953 | 954 | get: function () { 955 | 956 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 957 | return ! this.enableRotate; 958 | 959 | }, 960 | 961 | set: function ( value ) { 962 | 963 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 964 | this.enableRotate = ! value; 965 | 966 | } 967 | 968 | }, 969 | 970 | noPan: { 971 | 972 | get: function () { 973 | 974 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 975 | return ! this.enablePan; 976 | 977 | }, 978 | 979 | set: function ( value ) { 980 | 981 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 982 | this.enablePan = ! value; 983 | 984 | } 985 | 986 | }, 987 | 988 | noKeys: { 989 | 990 | get: function () { 991 | 992 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 993 | return ! this.enableKeys; 994 | 995 | }, 996 | 997 | set: function ( value ) { 998 | 999 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 1000 | this.enableKeys = ! value; 1001 | 1002 | } 1003 | 1004 | }, 1005 | 1006 | staticMoving: { 1007 | 1008 | get: function () { 1009 | 1010 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1011 | return ! this.enableDamping; 1012 | 1013 | }, 1014 | 1015 | set: function ( value ) { 1016 | 1017 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1018 | this.enableDamping = ! value; 1019 | 1020 | } 1021 | 1022 | }, 1023 | 1024 | dynamicDampingFactor: { 1025 | 1026 | get: function () { 1027 | 1028 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1029 | return this.dampingFactor; 1030 | 1031 | }, 1032 | 1033 | set: function ( value ) { 1034 | 1035 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1036 | this.dampingFactor = value; 1037 | 1038 | } 1039 | 1040 | } 1041 | 1042 | } ); 1043 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | // Set up the scene and renderer 2 | var scene = new THREE.Scene(); 3 | var camera = new THREE.PerspectiveCamera( 4 | 75, 5 | window.innerWidth / window.innerHeight, 6 | 0.1, 7 | 1000 8 | ); 9 | 10 | var renderer = new THREE.WebGLRenderer(); 11 | renderer.setSize(window.innerWidth, window.innerHeight); 12 | document.body.appendChild(renderer.domElement); 13 | 14 | camera.position.z = 150; 15 | 16 | var controls = new THREE.OrbitControls(camera, renderer.domElement); 17 | 18 | // End of scene setup 19 | 20 | // These are our setup variables. 21 | 22 | var numberOfPoints = 100000; 23 | var radiusOfSphere = 50; 24 | 25 | /* 26 | 27 | Comment out alternate lines to see the different positioning in effect 28 | 29 | */ 30 | 31 | var positionData = getCartesianPositions(numberOfPoints, radiusOfSphere); 32 | // var positionData = getSphericalPositions(numberOfPoints, radiusOfSphere); 33 | // var positionData = getSphericalPositionsWithBias(numberOfPoints,radiusOfSphere); 34 | // var positionData = getSphericalPositionsWithBias(numberOfPoints,radiusOfSphere, 1); 35 | // var positionData = getSphericalPositionsWithBias(numberOfPoints,radiusOfSphere, 0.5); 36 | // var positionData = getUniformPositions(numberOfPoints, radiusOfSphere); 37 | 38 | // Convert out positionData into a float32Array for handing to the buffer geometry 39 | const positions = new Float32Array(positionData.length * 3); 40 | positionData.forEach(function(vert, vertIndex) { 41 | vert.toArray(positions, vertIndex * 3); 42 | }, this); 43 | 44 | var geometry = new THREE.BufferGeometry(); 45 | geometry.addAttribute("position", new THREE.BufferAttribute(positions, 3)); 46 | 47 | var texture = new THREE.TextureLoader().load("./assets/textures/dot.png"); 48 | 49 | var texture = new THREE.TextureLoader().load("./assets/textures/dot.png"); 50 | 51 | var materialOptions = { 52 | color: 0x888888, 53 | map: texture, 54 | transparent: true, 55 | depthTest: false, 56 | opacity: 0.25, 57 | blending: THREE.AdditiveBlending, 58 | }; 59 | 60 | var material = new THREE.PointsMaterial(materialOptions); 61 | 62 | const points = new THREE.Points(geometry, material); 63 | scene.add(points); 64 | 65 | function animate() { 66 | requestAnimationFrame(animate); 67 | 68 | // animation here - lets rotate our dots 69 | points.rotation.y += 0.005; 70 | 71 | controls.update(); 72 | 73 | renderer.render(scene, camera); 74 | } 75 | 76 | animate(); 77 | 78 | /* 79 | The following are the position generating methods 80 | */ 81 | 82 | function getCartesianPositions(howMany, radius) { 83 | // Create and array to store our vector3 point data 84 | var vectors = []; 85 | 86 | // Create new points using random x,y and z properties then setting vector length to radius 87 | 88 | for (var i = 0; i < howMany; i += 1) { 89 | var vec3 = new THREE.Vector3(); 90 | 91 | vec3.x = THREE.Math.randFloatSpread(1); 92 | vec3.y = THREE.Math.randFloatSpread(1); 93 | vec3.z = THREE.Math.randFloatSpread(1); 94 | 95 | vec3.setLength(radius); 96 | 97 | vectors.push(vec3); 98 | } 99 | 100 | return vectors; 101 | } 102 | 103 | function getSphericalPositions(howMany, radius) { 104 | // Create and array to store our vector3 point data 105 | var vectors = []; 106 | 107 | // Create a spherical object 108 | var spherical = new THREE.Spherical(); 109 | 110 | // Set radius of spherical 111 | spherical.radius = radius; 112 | 113 | // Create new points using random phi and theta properties of the spherical object 114 | for (var i = 0; i < howMany; i += 1) { 115 | spherical.phi = THREE.Math.randFloat(0, Math.PI); // Phi is between 0 - PI 116 | spherical.theta = THREE.Math.randFloat(0, Math.PI * 2); // Phi is between 0 - 2 PI 117 | 118 | var vec3 = new THREE.Vector3().setFromSpherical(spherical); 119 | 120 | vectors.push(vec3); 121 | } 122 | 123 | return vectors; 124 | } 125 | 126 | function getSphericalPositionsWithBias(howMany, radius, bias) { 127 | var vectors = []; 128 | 129 | var spherical = new THREE.Spherical(); 130 | 131 | spherical.radius = radius; 132 | 133 | for (var i = 0; i < howMany; i += 1) { 134 | spherical.phi = getRndBias(0, Math.PI, Math.PI / 2, bias); // Phi is between 0 - PI 135 | spherical.theta = THREE.Math.randFloat(0, Math.PI * 2); // Theta is between 0 - 2 PI 136 | 137 | var vec3 = new THREE.Vector3().setFromSpherical(spherical); 138 | 139 | vectors.push(vec3); 140 | } 141 | 142 | return vectors; 143 | } 144 | 145 | function getRndBias(min, max, bias, influence) { 146 | const rnd = Math.random() * (max - min) + min; // random in range 147 | const mix = Math.random() * influence; // random mixer 148 | return rnd * (1 - mix) + bias * mix; // mix full range and bias 149 | } 150 | 151 | // This method distributes the particles uniformly accross the surface of a sphere 152 | 153 | function getUniformPositions(howMany, radius) { 154 | var vectors = []; 155 | 156 | var inc = Math.PI * (3 - Math.sqrt(5)); 157 | 158 | var x = 0; 159 | var y = 0; 160 | var z = 0; 161 | var r = 0; 162 | var phi = 0; 163 | 164 | for (var k = 0; k < howMany; k++) { 165 | var off = 2 / howMany; 166 | var vec3 = new THREE.Vector3(); 167 | 168 | y = k * off - 1 + off / 2; 169 | r = Math.sqrt(1 - y * y); 170 | 171 | phi = k * inc; 172 | 173 | x = Math.cos(phi) * r; 174 | 175 | z = (0, Math.sin(phi) * r); 176 | 177 | x *= radius; 178 | y *= radius; 179 | z *= radius; 180 | 181 | vec3.x = x; 182 | vec3.y = y; 183 | vec3.z = z; 184 | 185 | vectors.push(vec3); 186 | } 187 | 188 | return vectors; 189 | } 190 | -------------------------------------------------------------------------------- /screenshots/Spherical Bias 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defmech/random-position-on-the-surface-of-a-sphere/fe5cd9cadb72b023b5799ba63153a3b7a9297b35/screenshots/Spherical Bias 1.png -------------------------------------------------------------------------------- /screenshots/Spherical bias 0.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defmech/random-position-on-the-surface-of-a-sphere/fe5cd9cadb72b023b5799ba63153a3b7a9297b35/screenshots/Spherical bias 0.5.png -------------------------------------------------------------------------------- /screenshots/Spherical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defmech/random-position-on-the-surface-of-a-sphere/fe5cd9cadb72b023b5799ba63153a3b7a9297b35/screenshots/Spherical.png -------------------------------------------------------------------------------- /screenshots/Uniform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defmech/random-position-on-the-surface-of-a-sphere/fe5cd9cadb72b023b5799ba63153a3b7a9297b35/screenshots/Uniform.png -------------------------------------------------------------------------------- /screenshots/Vec3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defmech/random-position-on-the-surface-of-a-sphere/fe5cd9cadb72b023b5799ba63153a3b7a9297b35/screenshots/Vec3.png --------------------------------------------------------------------------------