├── LICENSE ├── README.md ├── img ├── light.png └── sky.jpg ├── index.html ├── lib ├── orbitcontrols.js └── three.js ├── main.js └── shaders ├── default.frag ├── default.vert ├── light.frag └── light.vert /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Emanuel Farauanu 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 | # Procedural Skyscraper Generation with Night-like Shaders 2 | 3 | A 3D procedural skyscraper generator with shaders in Three.js. 4 | 5 | ## Running on local 6 | 7 | Run using `python3 -m http.server` or with your favourite static site server tool. 8 | 9 | ## Requirements 10 | 11 | Working internet connection, Python, a modern browser 12 | -------------------------------------------------------------------------------- /img/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rototu/procedural-skyscraper-city-generator-and-shader/c1ac28c2819f80005e194e6adbbcdaed65dada50/img/light.png -------------------------------------------------------------------------------- /img/sky.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rototu/procedural-skyscraper-city-generator-and-shader/c1ac28c2819f80005e194e6adbbcdaed65dada50/img/sky.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | My first three.js app 5 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /lib/orbitcontrols.js: -------------------------------------------------------------------------------- 1 | import { 2 | EventDispatcher, 3 | MOUSE, 4 | Quaternion, 5 | Spherical, 6 | TOUCH, 7 | Vector2, 8 | Vector3, 9 | Plane, 10 | Ray, 11 | MathUtils 12 | } from './three.js'; 13 | 14 | // OrbitControls performs orbiting, dollying (zooming), and panning. 15 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 16 | // 17 | // Orbit - left mouse / touch: one-finger move 18 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 19 | // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move 20 | 21 | const _changeEvent = { type: 'change' }; 22 | const _startEvent = { type: 'start' }; 23 | const _endEvent = { type: 'end' }; 24 | const _ray = new Ray(); 25 | const _plane = new Plane(); 26 | const TILT_LIMIT = Math.cos( 70 * MathUtils.DEG2RAD ); 27 | 28 | class OrbitControls extends EventDispatcher { 29 | 30 | constructor( object, domElement ) { 31 | 32 | super(); 33 | 34 | this.object = object; 35 | this.domElement = domElement; 36 | this.domElement.style.touchAction = 'none'; // disable touch scroll 37 | 38 | // Set to false to disable this control 39 | this.enabled = true; 40 | 41 | // "target" sets the location of focus, where the object orbits around 42 | this.target = new Vector3(); 43 | 44 | // How far you can dolly in and out ( PerspectiveCamera only ) 45 | this.minDistance = 0; 46 | this.maxDistance = Infinity; 47 | 48 | // How far you can zoom in and out ( OrthographicCamera only ) 49 | this.minZoom = 0; 50 | this.maxZoom = Infinity; 51 | 52 | // How far you can orbit vertically, upper and lower limits. 53 | // Range is 0 to Math.PI radians. 54 | this.minPolarAngle = 0; // radians 55 | this.maxPolarAngle = Math.PI; // radians 56 | 57 | // How far you can orbit horizontally, upper and lower limits. 58 | // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ) 59 | this.minAzimuthAngle = - Infinity; // radians 60 | this.maxAzimuthAngle = Infinity; // radians 61 | 62 | // Set to true to enable damping (inertia) 63 | // If damping is enabled, you must call controls.update() in your animation loop 64 | this.enableDamping = false; 65 | this.dampingFactor = 0.05; 66 | 67 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 68 | // Set to false to disable zooming 69 | this.enableZoom = true; 70 | this.zoomSpeed = 1.0; 71 | 72 | // Set to false to disable rotating 73 | this.enableRotate = true; 74 | this.rotateSpeed = 1.0; 75 | 76 | // Set to false to disable panning 77 | this.enablePan = true; 78 | this.panSpeed = 1.0; 79 | this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up 80 | this.keyPanSpeed = 7.0; // pixels moved per arrow key push 81 | this.zoomToCursor = false; 82 | 83 | // Set to true to automatically rotate around the target 84 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 85 | this.autoRotate = false; 86 | this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60 87 | 88 | // The four arrow keys 89 | this.keys = { LEFT: 'ArrowLeft', UP: 'ArrowUp', RIGHT: 'ArrowRight', BOTTOM: 'ArrowDown' }; 90 | 91 | // Mouse buttons 92 | this.mouseButtons = { LEFT: MOUSE.ROTATE, MIDDLE: MOUSE.DOLLY, RIGHT: MOUSE.PAN }; 93 | 94 | // Touch fingers 95 | this.touches = { ONE: TOUCH.ROTATE, TWO: TOUCH.DOLLY_PAN }; 96 | 97 | // for reset 98 | this.target0 = this.target.clone(); 99 | this.position0 = this.object.position.clone(); 100 | this.zoom0 = this.object.zoom; 101 | 102 | // the target DOM element for key events 103 | this._domElementKeyEvents = null; 104 | 105 | // 106 | // public methods 107 | // 108 | 109 | this.getPolarAngle = function () { 110 | 111 | return spherical.phi; 112 | 113 | }; 114 | 115 | this.getAzimuthalAngle = function () { 116 | 117 | return spherical.theta; 118 | 119 | }; 120 | 121 | this.getDistance = function () { 122 | 123 | return this.object.position.distanceTo( this.target ); 124 | 125 | }; 126 | 127 | this.listenToKeyEvents = function ( domElement ) { 128 | 129 | domElement.addEventListener( 'keydown', onKeyDown ); 130 | this._domElementKeyEvents = domElement; 131 | 132 | }; 133 | 134 | this.stopListenToKeyEvents = function () { 135 | 136 | this._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 137 | this._domElementKeyEvents = null; 138 | 139 | }; 140 | 141 | this.saveState = function () { 142 | 143 | scope.target0.copy( scope.target ); 144 | scope.position0.copy( scope.object.position ); 145 | scope.zoom0 = scope.object.zoom; 146 | 147 | }; 148 | 149 | this.reset = function () { 150 | 151 | scope.target.copy( scope.target0 ); 152 | scope.object.position.copy( scope.position0 ); 153 | scope.object.zoom = scope.zoom0; 154 | 155 | scope.object.updateProjectionMatrix(); 156 | scope.dispatchEvent( _changeEvent ); 157 | 158 | scope.update(); 159 | 160 | state = STATE.NONE; 161 | 162 | }; 163 | 164 | // this method is exposed, but perhaps it would be better if we can make it private... 165 | this.update = function () { 166 | 167 | const offset = new Vector3(); 168 | 169 | // so camera.up is the orbit axis 170 | const quat = new Quaternion().setFromUnitVectors( object.up, new Vector3( 0, 1, 0 ) ); 171 | const quatInverse = quat.clone().invert(); 172 | 173 | const lastPosition = new Vector3(); 174 | const lastQuaternion = new Quaternion(); 175 | const lastTargetPosition = new Vector3(); 176 | 177 | const twoPI = 2 * Math.PI; 178 | 179 | return function update( deltaTime = null ) { 180 | 181 | const position = scope.object.position; 182 | 183 | offset.copy( position ).sub( scope.target ); 184 | 185 | // rotate offset to "y-axis-is-up" space 186 | offset.applyQuaternion( quat ); 187 | 188 | // angle from z-axis around y-axis 189 | spherical.setFromVector3( offset ); 190 | 191 | if ( scope.autoRotate && state === STATE.NONE ) { 192 | 193 | rotateLeft( getAutoRotationAngle( deltaTime ) ); 194 | 195 | } 196 | 197 | if ( scope.enableDamping ) { 198 | 199 | spherical.theta += sphericalDelta.theta * scope.dampingFactor; 200 | spherical.phi += sphericalDelta.phi * scope.dampingFactor; 201 | 202 | } else { 203 | 204 | spherical.theta += sphericalDelta.theta; 205 | spherical.phi += sphericalDelta.phi; 206 | 207 | } 208 | 209 | // restrict theta to be between desired limits 210 | 211 | let min = scope.minAzimuthAngle; 212 | let max = scope.maxAzimuthAngle; 213 | 214 | if ( isFinite( min ) && isFinite( max ) ) { 215 | 216 | if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI; 217 | 218 | if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI; 219 | 220 | if ( min <= max ) { 221 | 222 | spherical.theta = Math.max( min, Math.min( max, spherical.theta ) ); 223 | 224 | } else { 225 | 226 | spherical.theta = ( spherical.theta > ( min + max ) / 2 ) ? 227 | Math.max( min, spherical.theta ) : 228 | Math.min( max, spherical.theta ); 229 | 230 | } 231 | 232 | } 233 | 234 | // restrict phi to be between desired limits 235 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 236 | 237 | spherical.makeSafe(); 238 | 239 | 240 | // move target to panned location 241 | 242 | if ( scope.enableDamping === true ) { 243 | 244 | scope.target.addScaledVector( panOffset, scope.dampingFactor ); 245 | 246 | } else { 247 | 248 | scope.target.add( panOffset ); 249 | 250 | } 251 | 252 | // adjust the camera position based on zoom only if we're not zooming to the cursor or if it's an ortho camera 253 | // we adjust zoom later in these cases 254 | if ( scope.zoomToCursor && performCursorZoom || scope.object.isOrthographicCamera ) { 255 | 256 | spherical.radius = clampDistance( spherical.radius ); 257 | 258 | } else { 259 | 260 | spherical.radius = clampDistance( spherical.radius * scale ); 261 | 262 | } 263 | 264 | 265 | offset.setFromSpherical( spherical ); 266 | 267 | // rotate offset back to "camera-up-vector-is-up" space 268 | offset.applyQuaternion( quatInverse ); 269 | 270 | position.copy( scope.target ).add( offset ); 271 | 272 | scope.object.lookAt( scope.target ); 273 | 274 | if ( scope.enableDamping === true ) { 275 | 276 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 277 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 278 | 279 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 280 | 281 | } else { 282 | 283 | sphericalDelta.set( 0, 0, 0 ); 284 | 285 | panOffset.set( 0, 0, 0 ); 286 | 287 | } 288 | 289 | // adjust camera position 290 | let zoomChanged = false; 291 | if ( scope.zoomToCursor && performCursorZoom ) { 292 | 293 | let newRadius = null; 294 | if ( scope.object.isPerspectiveCamera ) { 295 | 296 | // move the camera down the pointer ray 297 | // this method avoids floating point error 298 | const prevRadius = offset.length(); 299 | newRadius = clampDistance( prevRadius * scale ); 300 | 301 | const radiusDelta = prevRadius - newRadius; 302 | scope.object.position.addScaledVector( dollyDirection, radiusDelta ); 303 | scope.object.updateMatrixWorld(); 304 | 305 | } else if ( scope.object.isOrthographicCamera ) { 306 | 307 | // adjust the ortho camera position based on zoom changes 308 | const mouseBefore = new Vector3( mouse.x, mouse.y, 0 ); 309 | mouseBefore.unproject( scope.object ); 310 | 311 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 312 | scope.object.updateProjectionMatrix(); 313 | zoomChanged = true; 314 | 315 | const mouseAfter = new Vector3( mouse.x, mouse.y, 0 ); 316 | mouseAfter.unproject( scope.object ); 317 | 318 | scope.object.position.sub( mouseAfter ).add( mouseBefore ); 319 | scope.object.updateMatrixWorld(); 320 | 321 | newRadius = offset.length(); 322 | 323 | } else { 324 | 325 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled.' ); 326 | scope.zoomToCursor = false; 327 | 328 | } 329 | 330 | // handle the placement of the target 331 | if ( newRadius !== null ) { 332 | 333 | if ( this.screenSpacePanning ) { 334 | 335 | // position the orbit target in front of the new camera position 336 | scope.target.set( 0, 0, - 1 ) 337 | .transformDirection( scope.object.matrix ) 338 | .multiplyScalar( newRadius ) 339 | .add( scope.object.position ); 340 | 341 | } else { 342 | 343 | // get the ray and translation plane to compute target 344 | _ray.origin.copy( scope.object.position ); 345 | _ray.direction.set( 0, 0, - 1 ).transformDirection( scope.object.matrix ); 346 | 347 | // if the camera is 20 degrees above the horizon then don't adjust the focus target to avoid 348 | // extremely large values 349 | if ( Math.abs( scope.object.up.dot( _ray.direction ) ) < TILT_LIMIT ) { 350 | 351 | object.lookAt( scope.target ); 352 | 353 | } else { 354 | 355 | _plane.setFromNormalAndCoplanarPoint( scope.object.up, scope.target ); 356 | _ray.intersectPlane( _plane, scope.target ); 357 | 358 | } 359 | 360 | } 361 | 362 | } 363 | 364 | } else if ( scope.object.isOrthographicCamera ) { 365 | 366 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / scale ) ); 367 | scope.object.updateProjectionMatrix(); 368 | zoomChanged = true; 369 | 370 | } 371 | 372 | scale = 1; 373 | performCursorZoom = false; 374 | 375 | // update condition is: 376 | // min(camera displacement, camera rotation in radians)^2 > EPS 377 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 378 | 379 | if ( zoomChanged || 380 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 381 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS || 382 | lastTargetPosition.distanceToSquared( scope.target ) > 0 ) { 383 | 384 | scope.dispatchEvent( _changeEvent ); 385 | 386 | lastPosition.copy( scope.object.position ); 387 | lastQuaternion.copy( scope.object.quaternion ); 388 | lastTargetPosition.copy( scope.target ); 389 | 390 | zoomChanged = false; 391 | 392 | return true; 393 | 394 | } 395 | 396 | return false; 397 | 398 | }; 399 | 400 | }(); 401 | 402 | this.dispose = function () { 403 | 404 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu ); 405 | 406 | scope.domElement.removeEventListener( 'pointerdown', onPointerDown ); 407 | scope.domElement.removeEventListener( 'pointercancel', onPointerUp ); 408 | scope.domElement.removeEventListener( 'wheel', onMouseWheel ); 409 | 410 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 411 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 412 | 413 | 414 | if ( scope._domElementKeyEvents !== null ) { 415 | 416 | scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown ); 417 | scope._domElementKeyEvents = null; 418 | 419 | } 420 | 421 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 422 | 423 | }; 424 | 425 | // 426 | // internals 427 | // 428 | 429 | const scope = this; 430 | 431 | const STATE = { 432 | NONE: - 1, 433 | ROTATE: 0, 434 | DOLLY: 1, 435 | PAN: 2, 436 | TOUCH_ROTATE: 3, 437 | TOUCH_PAN: 4, 438 | TOUCH_DOLLY_PAN: 5, 439 | TOUCH_DOLLY_ROTATE: 6 440 | }; 441 | 442 | let state = STATE.NONE; 443 | 444 | const EPS = 0.000001; 445 | 446 | // current position in spherical coordinates 447 | const spherical = new Spherical(); 448 | const sphericalDelta = new Spherical(); 449 | 450 | let scale = 1; 451 | const panOffset = new Vector3(); 452 | 453 | const rotateStart = new Vector2(); 454 | const rotateEnd = new Vector2(); 455 | const rotateDelta = new Vector2(); 456 | 457 | const panStart = new Vector2(); 458 | const panEnd = new Vector2(); 459 | const panDelta = new Vector2(); 460 | 461 | const dollyStart = new Vector2(); 462 | const dollyEnd = new Vector2(); 463 | const dollyDelta = new Vector2(); 464 | 465 | const dollyDirection = new Vector3(); 466 | const mouse = new Vector2(); 467 | let performCursorZoom = false; 468 | 469 | const pointers = []; 470 | const pointerPositions = {}; 471 | 472 | function getAutoRotationAngle( deltaTime ) { 473 | 474 | if ( deltaTime !== null ) { 475 | 476 | return ( 2 * Math.PI / 60 * scope.autoRotateSpeed ) * deltaTime; 477 | 478 | } else { 479 | 480 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 481 | 482 | } 483 | 484 | } 485 | 486 | function getZoomScale() { 487 | 488 | return Math.pow( 0.95, scope.zoomSpeed ); 489 | 490 | } 491 | 492 | function rotateLeft( angle ) { 493 | 494 | sphericalDelta.theta -= angle; 495 | 496 | } 497 | 498 | function rotateUp( angle ) { 499 | 500 | sphericalDelta.phi -= angle; 501 | 502 | } 503 | 504 | const panLeft = function () { 505 | 506 | const v = new Vector3(); 507 | 508 | return function panLeft( distance, objectMatrix ) { 509 | 510 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 511 | v.multiplyScalar( - distance ); 512 | 513 | panOffset.add( v ); 514 | 515 | }; 516 | 517 | }(); 518 | 519 | const panUp = function () { 520 | 521 | const v = new Vector3(); 522 | 523 | return function panUp( distance, objectMatrix ) { 524 | 525 | if ( scope.screenSpacePanning === true ) { 526 | 527 | v.setFromMatrixColumn( objectMatrix, 1 ); 528 | 529 | } else { 530 | 531 | v.setFromMatrixColumn( objectMatrix, 0 ); 532 | v.crossVectors( scope.object.up, v ); 533 | 534 | } 535 | 536 | v.multiplyScalar( distance ); 537 | 538 | panOffset.add( v ); 539 | 540 | }; 541 | 542 | }(); 543 | 544 | // deltaX and deltaY are in pixels; right and down are positive 545 | const pan = function () { 546 | 547 | const offset = new Vector3(); 548 | 549 | return function pan( deltaX, deltaY ) { 550 | 551 | const element = scope.domElement; 552 | 553 | if ( scope.object.isPerspectiveCamera ) { 554 | 555 | // perspective 556 | const position = scope.object.position; 557 | offset.copy( position ).sub( scope.target ); 558 | let targetDistance = offset.length(); 559 | 560 | // half of the fov is center to top of screen 561 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 562 | 563 | // we use only clientHeight here so aspect ratio does not distort speed 564 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 565 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 566 | 567 | } else if ( scope.object.isOrthographicCamera ) { 568 | 569 | // orthographic 570 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 571 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 572 | 573 | } else { 574 | 575 | // camera neither orthographic nor perspective 576 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 577 | scope.enablePan = false; 578 | 579 | } 580 | 581 | }; 582 | 583 | }(); 584 | 585 | function dollyOut( dollyScale ) { 586 | 587 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 588 | 589 | scale /= dollyScale; 590 | 591 | } else { 592 | 593 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 594 | scope.enableZoom = false; 595 | 596 | } 597 | 598 | } 599 | 600 | function dollyIn( dollyScale ) { 601 | 602 | if ( scope.object.isPerspectiveCamera || scope.object.isOrthographicCamera ) { 603 | 604 | scale *= dollyScale; 605 | 606 | } else { 607 | 608 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 609 | scope.enableZoom = false; 610 | 611 | } 612 | 613 | } 614 | 615 | function updateMouseParameters( event ) { 616 | 617 | if ( ! scope.zoomToCursor ) { 618 | 619 | return; 620 | 621 | } 622 | 623 | performCursorZoom = true; 624 | 625 | const rect = scope.domElement.getBoundingClientRect(); 626 | const x = event.clientX - rect.left; 627 | const y = event.clientY - rect.top; 628 | const w = rect.width; 629 | const h = rect.height; 630 | 631 | mouse.x = ( x / w ) * 2 - 1; 632 | mouse.y = - ( y / h ) * 2 + 1; 633 | 634 | dollyDirection.set( mouse.x, mouse.y, 1 ).unproject( scope.object ).sub( scope.object.position ).normalize(); 635 | 636 | } 637 | 638 | function clampDistance( dist ) { 639 | 640 | return Math.max( scope.minDistance, Math.min( scope.maxDistance, dist ) ); 641 | 642 | } 643 | 644 | // 645 | // event callbacks - update the object state 646 | // 647 | 648 | function handleMouseDownRotate( event ) { 649 | 650 | rotateStart.set( event.clientX, event.clientY ); 651 | 652 | } 653 | 654 | function handleMouseDownDolly( event ) { 655 | 656 | updateMouseParameters( event ); 657 | dollyStart.set( event.clientX, event.clientY ); 658 | 659 | } 660 | 661 | function handleMouseDownPan( event ) { 662 | 663 | panStart.set( event.clientX, event.clientY ); 664 | 665 | } 666 | 667 | function handleMouseMoveRotate( event ) { 668 | 669 | rotateEnd.set( event.clientX, event.clientY ); 670 | 671 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 672 | 673 | const element = scope.domElement; 674 | 675 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 676 | 677 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 678 | 679 | rotateStart.copy( rotateEnd ); 680 | 681 | scope.update(); 682 | 683 | } 684 | 685 | function handleMouseMoveDolly( event ) { 686 | 687 | dollyEnd.set( event.clientX, event.clientY ); 688 | 689 | dollyDelta.subVectors( dollyEnd, dollyStart ); 690 | 691 | if ( dollyDelta.y > 0 ) { 692 | 693 | dollyOut( getZoomScale() ); 694 | 695 | } else if ( dollyDelta.y < 0 ) { 696 | 697 | dollyIn( getZoomScale() ); 698 | 699 | } 700 | 701 | dollyStart.copy( dollyEnd ); 702 | 703 | scope.update(); 704 | 705 | } 706 | 707 | function handleMouseMovePan( event ) { 708 | 709 | panEnd.set( event.clientX, event.clientY ); 710 | 711 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 712 | 713 | pan( panDelta.x, panDelta.y ); 714 | 715 | panStart.copy( panEnd ); 716 | 717 | scope.update(); 718 | 719 | } 720 | 721 | function handleMouseWheel( event ) { 722 | 723 | updateMouseParameters( event ); 724 | 725 | if ( event.deltaY < 0 ) { 726 | 727 | dollyIn( getZoomScale() ); 728 | 729 | } else if ( event.deltaY > 0 ) { 730 | 731 | dollyOut( getZoomScale() ); 732 | 733 | } 734 | 735 | scope.update(); 736 | 737 | } 738 | 739 | function handleKeyDown( event ) { 740 | 741 | let needsUpdate = false; 742 | 743 | switch ( event.code ) { 744 | 745 | case scope.keys.UP: 746 | 747 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 748 | 749 | rotateUp( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 750 | 751 | } else { 752 | 753 | pan( 0, scope.keyPanSpeed ); 754 | 755 | } 756 | 757 | needsUpdate = true; 758 | break; 759 | 760 | case scope.keys.BOTTOM: 761 | 762 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 763 | 764 | rotateUp( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 765 | 766 | } else { 767 | 768 | pan( 0, - scope.keyPanSpeed ); 769 | 770 | } 771 | 772 | needsUpdate = true; 773 | break; 774 | 775 | case scope.keys.LEFT: 776 | 777 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 778 | 779 | rotateLeft( 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 780 | 781 | } else { 782 | 783 | pan( scope.keyPanSpeed, 0 ); 784 | 785 | } 786 | 787 | needsUpdate = true; 788 | break; 789 | 790 | case scope.keys.RIGHT: 791 | 792 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 793 | 794 | rotateLeft( - 2 * Math.PI * scope.rotateSpeed / scope.domElement.clientHeight ); 795 | 796 | } else { 797 | 798 | pan( - scope.keyPanSpeed, 0 ); 799 | 800 | } 801 | 802 | needsUpdate = true; 803 | break; 804 | 805 | } 806 | 807 | if ( needsUpdate ) { 808 | 809 | // prevent the browser from scrolling on cursor keys 810 | event.preventDefault(); 811 | 812 | scope.update(); 813 | 814 | } 815 | 816 | 817 | } 818 | 819 | function handleTouchStartRotate() { 820 | 821 | if ( pointers.length === 1 ) { 822 | 823 | rotateStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 824 | 825 | } else { 826 | 827 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 828 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 829 | 830 | rotateStart.set( x, y ); 831 | 832 | } 833 | 834 | } 835 | 836 | function handleTouchStartPan() { 837 | 838 | if ( pointers.length === 1 ) { 839 | 840 | panStart.set( pointers[ 0 ].pageX, pointers[ 0 ].pageY ); 841 | 842 | } else { 843 | 844 | const x = 0.5 * ( pointers[ 0 ].pageX + pointers[ 1 ].pageX ); 845 | const y = 0.5 * ( pointers[ 0 ].pageY + pointers[ 1 ].pageY ); 846 | 847 | panStart.set( x, y ); 848 | 849 | } 850 | 851 | } 852 | 853 | function handleTouchStartDolly() { 854 | 855 | const dx = pointers[ 0 ].pageX - pointers[ 1 ].pageX; 856 | const dy = pointers[ 0 ].pageY - pointers[ 1 ].pageY; 857 | 858 | const distance = Math.sqrt( dx * dx + dy * dy ); 859 | 860 | dollyStart.set( 0, distance ); 861 | 862 | } 863 | 864 | function handleTouchStartDollyPan() { 865 | 866 | if ( scope.enableZoom ) handleTouchStartDolly(); 867 | 868 | if ( scope.enablePan ) handleTouchStartPan(); 869 | 870 | } 871 | 872 | function handleTouchStartDollyRotate() { 873 | 874 | if ( scope.enableZoom ) handleTouchStartDolly(); 875 | 876 | if ( scope.enableRotate ) handleTouchStartRotate(); 877 | 878 | } 879 | 880 | function handleTouchMoveRotate( event ) { 881 | 882 | if ( pointers.length == 1 ) { 883 | 884 | rotateEnd.set( event.pageX, event.pageY ); 885 | 886 | } else { 887 | 888 | const position = getSecondPointerPosition( event ); 889 | 890 | const x = 0.5 * ( event.pageX + position.x ); 891 | const y = 0.5 * ( event.pageY + position.y ); 892 | 893 | rotateEnd.set( x, y ); 894 | 895 | } 896 | 897 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 898 | 899 | const element = scope.domElement; 900 | 901 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height 902 | 903 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 904 | 905 | rotateStart.copy( rotateEnd ); 906 | 907 | } 908 | 909 | function handleTouchMovePan( event ) { 910 | 911 | if ( pointers.length === 1 ) { 912 | 913 | panEnd.set( event.pageX, event.pageY ); 914 | 915 | } else { 916 | 917 | const position = getSecondPointerPosition( event ); 918 | 919 | const x = 0.5 * ( event.pageX + position.x ); 920 | const y = 0.5 * ( event.pageY + position.y ); 921 | 922 | panEnd.set( x, y ); 923 | 924 | } 925 | 926 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 927 | 928 | pan( panDelta.x, panDelta.y ); 929 | 930 | panStart.copy( panEnd ); 931 | 932 | } 933 | 934 | function handleTouchMoveDolly( event ) { 935 | 936 | const position = getSecondPointerPosition( event ); 937 | 938 | const dx = event.pageX - position.x; 939 | const dy = event.pageY - position.y; 940 | 941 | const distance = Math.sqrt( dx * dx + dy * dy ); 942 | 943 | dollyEnd.set( 0, distance ); 944 | 945 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 946 | 947 | dollyOut( dollyDelta.y ); 948 | 949 | dollyStart.copy( dollyEnd ); 950 | 951 | } 952 | 953 | function handleTouchMoveDollyPan( event ) { 954 | 955 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 956 | 957 | if ( scope.enablePan ) handleTouchMovePan( event ); 958 | 959 | } 960 | 961 | function handleTouchMoveDollyRotate( event ) { 962 | 963 | if ( scope.enableZoom ) handleTouchMoveDolly( event ); 964 | 965 | if ( scope.enableRotate ) handleTouchMoveRotate( event ); 966 | 967 | } 968 | 969 | // 970 | // event handlers - FSM: listen for events and reset state 971 | // 972 | 973 | function onPointerDown( event ) { 974 | 975 | if ( scope.enabled === false ) return; 976 | 977 | if ( pointers.length === 0 ) { 978 | 979 | scope.domElement.setPointerCapture( event.pointerId ); 980 | 981 | scope.domElement.addEventListener( 'pointermove', onPointerMove ); 982 | scope.domElement.addEventListener( 'pointerup', onPointerUp ); 983 | 984 | } 985 | 986 | // 987 | 988 | addPointer( event ); 989 | 990 | if ( event.pointerType === 'touch' ) { 991 | 992 | onTouchStart( event ); 993 | 994 | } else { 995 | 996 | onMouseDown( event ); 997 | 998 | } 999 | 1000 | } 1001 | 1002 | function onPointerMove( event ) { 1003 | 1004 | if ( scope.enabled === false ) return; 1005 | 1006 | if ( event.pointerType === 'touch' ) { 1007 | 1008 | onTouchMove( event ); 1009 | 1010 | } else { 1011 | 1012 | onMouseMove( event ); 1013 | 1014 | } 1015 | 1016 | } 1017 | 1018 | function onPointerUp( event ) { 1019 | 1020 | removePointer( event ); 1021 | 1022 | if ( pointers.length === 0 ) { 1023 | 1024 | scope.domElement.releasePointerCapture( event.pointerId ); 1025 | 1026 | scope.domElement.removeEventListener( 'pointermove', onPointerMove ); 1027 | scope.domElement.removeEventListener( 'pointerup', onPointerUp ); 1028 | 1029 | } 1030 | 1031 | scope.dispatchEvent( _endEvent ); 1032 | 1033 | state = STATE.NONE; 1034 | 1035 | } 1036 | 1037 | function onMouseDown( event ) { 1038 | 1039 | let mouseAction; 1040 | 1041 | switch ( event.button ) { 1042 | 1043 | case 0: 1044 | 1045 | mouseAction = scope.mouseButtons.LEFT; 1046 | break; 1047 | 1048 | case 1: 1049 | 1050 | mouseAction = scope.mouseButtons.MIDDLE; 1051 | break; 1052 | 1053 | case 2: 1054 | 1055 | mouseAction = scope.mouseButtons.RIGHT; 1056 | break; 1057 | 1058 | default: 1059 | 1060 | mouseAction = - 1; 1061 | 1062 | } 1063 | 1064 | switch ( mouseAction ) { 1065 | 1066 | case MOUSE.DOLLY: 1067 | 1068 | if ( scope.enableZoom === false ) return; 1069 | 1070 | handleMouseDownDolly( event ); 1071 | 1072 | state = STATE.DOLLY; 1073 | 1074 | break; 1075 | 1076 | case MOUSE.ROTATE: 1077 | 1078 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1079 | 1080 | if ( scope.enablePan === false ) return; 1081 | 1082 | handleMouseDownPan( event ); 1083 | 1084 | state = STATE.PAN; 1085 | 1086 | } else { 1087 | 1088 | if ( scope.enableRotate === false ) return; 1089 | 1090 | handleMouseDownRotate( event ); 1091 | 1092 | state = STATE.ROTATE; 1093 | 1094 | } 1095 | 1096 | break; 1097 | 1098 | case MOUSE.PAN: 1099 | 1100 | if ( event.ctrlKey || event.metaKey || event.shiftKey ) { 1101 | 1102 | if ( scope.enableRotate === false ) return; 1103 | 1104 | handleMouseDownRotate( event ); 1105 | 1106 | state = STATE.ROTATE; 1107 | 1108 | } else { 1109 | 1110 | if ( scope.enablePan === false ) return; 1111 | 1112 | handleMouseDownPan( event ); 1113 | 1114 | state = STATE.PAN; 1115 | 1116 | } 1117 | 1118 | break; 1119 | 1120 | default: 1121 | 1122 | state = STATE.NONE; 1123 | 1124 | } 1125 | 1126 | if ( state !== STATE.NONE ) { 1127 | 1128 | scope.dispatchEvent( _startEvent ); 1129 | 1130 | } 1131 | 1132 | } 1133 | 1134 | function onMouseMove( event ) { 1135 | 1136 | switch ( state ) { 1137 | 1138 | case STATE.ROTATE: 1139 | 1140 | if ( scope.enableRotate === false ) return; 1141 | 1142 | handleMouseMoveRotate( event ); 1143 | 1144 | break; 1145 | 1146 | case STATE.DOLLY: 1147 | 1148 | if ( scope.enableZoom === false ) return; 1149 | 1150 | handleMouseMoveDolly( event ); 1151 | 1152 | break; 1153 | 1154 | case STATE.PAN: 1155 | 1156 | if ( scope.enablePan === false ) return; 1157 | 1158 | handleMouseMovePan( event ); 1159 | 1160 | break; 1161 | 1162 | } 1163 | 1164 | } 1165 | 1166 | function onMouseWheel( event ) { 1167 | 1168 | if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE ) return; 1169 | 1170 | event.preventDefault(); 1171 | 1172 | scope.dispatchEvent( _startEvent ); 1173 | 1174 | handleMouseWheel( event ); 1175 | 1176 | scope.dispatchEvent( _endEvent ); 1177 | 1178 | } 1179 | 1180 | function onKeyDown( event ) { 1181 | 1182 | if ( scope.enabled === false || scope.enablePan === false ) return; 1183 | 1184 | handleKeyDown( event ); 1185 | 1186 | } 1187 | 1188 | function onTouchStart( event ) { 1189 | 1190 | trackPointer( event ); 1191 | 1192 | switch ( pointers.length ) { 1193 | 1194 | case 1: 1195 | 1196 | switch ( scope.touches.ONE ) { 1197 | 1198 | case TOUCH.ROTATE: 1199 | 1200 | if ( scope.enableRotate === false ) return; 1201 | 1202 | handleTouchStartRotate(); 1203 | 1204 | state = STATE.TOUCH_ROTATE; 1205 | 1206 | break; 1207 | 1208 | case TOUCH.PAN: 1209 | 1210 | if ( scope.enablePan === false ) return; 1211 | 1212 | handleTouchStartPan(); 1213 | 1214 | state = STATE.TOUCH_PAN; 1215 | 1216 | break; 1217 | 1218 | default: 1219 | 1220 | state = STATE.NONE; 1221 | 1222 | } 1223 | 1224 | break; 1225 | 1226 | case 2: 1227 | 1228 | switch ( scope.touches.TWO ) { 1229 | 1230 | case TOUCH.DOLLY_PAN: 1231 | 1232 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1233 | 1234 | handleTouchStartDollyPan(); 1235 | 1236 | state = STATE.TOUCH_DOLLY_PAN; 1237 | 1238 | break; 1239 | 1240 | case TOUCH.DOLLY_ROTATE: 1241 | 1242 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1243 | 1244 | handleTouchStartDollyRotate(); 1245 | 1246 | state = STATE.TOUCH_DOLLY_ROTATE; 1247 | 1248 | break; 1249 | 1250 | default: 1251 | 1252 | state = STATE.NONE; 1253 | 1254 | } 1255 | 1256 | break; 1257 | 1258 | default: 1259 | 1260 | state = STATE.NONE; 1261 | 1262 | } 1263 | 1264 | if ( state !== STATE.NONE ) { 1265 | 1266 | scope.dispatchEvent( _startEvent ); 1267 | 1268 | } 1269 | 1270 | } 1271 | 1272 | function onTouchMove( event ) { 1273 | 1274 | trackPointer( event ); 1275 | 1276 | switch ( state ) { 1277 | 1278 | case STATE.TOUCH_ROTATE: 1279 | 1280 | if ( scope.enableRotate === false ) return; 1281 | 1282 | handleTouchMoveRotate( event ); 1283 | 1284 | scope.update(); 1285 | 1286 | break; 1287 | 1288 | case STATE.TOUCH_PAN: 1289 | 1290 | if ( scope.enablePan === false ) return; 1291 | 1292 | handleTouchMovePan( event ); 1293 | 1294 | scope.update(); 1295 | 1296 | break; 1297 | 1298 | case STATE.TOUCH_DOLLY_PAN: 1299 | 1300 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 1301 | 1302 | handleTouchMoveDollyPan( event ); 1303 | 1304 | scope.update(); 1305 | 1306 | break; 1307 | 1308 | case STATE.TOUCH_DOLLY_ROTATE: 1309 | 1310 | if ( scope.enableZoom === false && scope.enableRotate === false ) return; 1311 | 1312 | handleTouchMoveDollyRotate( event ); 1313 | 1314 | scope.update(); 1315 | 1316 | break; 1317 | 1318 | default: 1319 | 1320 | state = STATE.NONE; 1321 | 1322 | } 1323 | 1324 | } 1325 | 1326 | function onContextMenu( event ) { 1327 | 1328 | if ( scope.enabled === false ) return; 1329 | 1330 | event.preventDefault(); 1331 | 1332 | } 1333 | 1334 | function addPointer( event ) { 1335 | 1336 | pointers.push( event ); 1337 | 1338 | } 1339 | 1340 | function removePointer( event ) { 1341 | 1342 | delete pointerPositions[ event.pointerId ]; 1343 | 1344 | for ( let i = 0; i < pointers.length; i ++ ) { 1345 | 1346 | if ( pointers[ i ].pointerId == event.pointerId ) { 1347 | 1348 | pointers.splice( i, 1 ); 1349 | return; 1350 | 1351 | } 1352 | 1353 | } 1354 | 1355 | } 1356 | 1357 | function trackPointer( event ) { 1358 | 1359 | let position = pointerPositions[ event.pointerId ]; 1360 | 1361 | if ( position === undefined ) { 1362 | 1363 | position = new Vector2(); 1364 | pointerPositions[ event.pointerId ] = position; 1365 | 1366 | } 1367 | 1368 | position.set( event.pageX, event.pageY ); 1369 | 1370 | } 1371 | 1372 | function getSecondPointerPosition( event ) { 1373 | 1374 | const pointer = ( event.pointerId === pointers[ 0 ].pointerId ) ? pointers[ 1 ] : pointers[ 0 ]; 1375 | 1376 | return pointerPositions[ pointer.pointerId ]; 1377 | 1378 | } 1379 | 1380 | // 1381 | 1382 | scope.domElement.addEventListener( 'contextmenu', onContextMenu ); 1383 | 1384 | scope.domElement.addEventListener( 'pointerdown', onPointerDown ); 1385 | scope.domElement.addEventListener( 'pointercancel', onPointerUp ); 1386 | scope.domElement.addEventListener( 'wheel', onMouseWheel, { passive: false } ); 1387 | 1388 | // force an update at start 1389 | 1390 | this.update(); 1391 | 1392 | } 1393 | 1394 | } 1395 | 1396 | export { OrbitControls }; -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import * as THREE from './lib/three.js' 2 | import { OrbitControls } from './lib/orbitcontrols.js' 3 | 4 | // Add any resources you want to load here 5 | // You will then be able to reference them in initialise_scene 6 | // e.g. as "resources.vert_shader" 7 | const RESOURCES = [ 8 | // format is: 9 | // ["name", "path-to-resource"]` 10 | ["vert_shader", "./shaders/default.vert"], 11 | ["frag_shader", "./shaders/default.frag"], 12 | ["vertl_shader", "./shaders/light.vert"], 13 | ["fragl_shader", "./shaders/light.frag"] 14 | ]; 15 | 16 | /* 17 | 18 | Procedural Three.js city generator with custom shader 19 | made for first 2019-2020 Computer Graphics Practical 20 | 21 | */ 22 | 23 | /* 24 | Main function 25 | Receives loaded shaders from server 26 | Creates the procedurally generated city and handles the rendering 27 | */ 28 | const main = function (resources) { 29 | 30 | 31 | // Constants 32 | const near = 0.1; 33 | const far = 10000; 34 | const repeatCount = 20; 35 | const cellSize = 200; 36 | const height = 1000; 37 | const streetWidth = 20; 38 | const groundColor = new THREE.Vector3(0, 0.5, 0); 39 | const streetColor = new THREE.Vector3(0, 0, 0); 40 | const lightColor = new THREE.Vector3(0.5, 0.2, 1); 41 | const carColor = new THREE.Vector3(1, 0.3, 0); 42 | const carSpeed = 3; 43 | const carRadius = 8; 44 | const cameraDist = 2200; 45 | const cameraHeight = 1800; 46 | 47 | // Three.js init 48 | 49 | const scene = new THREE.Scene(); 50 | const camera = new THREE.PerspectiveCamera(90, window.innerWidth / window.innerHeight, near, far); 51 | 52 | const renderer = new THREE.WebGLRenderer({ 53 | antialias: true, 54 | alpha: true 55 | }); 56 | renderer.setSize(window.innerWidth, window.innerHeight); 57 | renderer.setPixelRatio(window.devicePixelRatio); 58 | document.body.appendChild(renderer.domElement); 59 | 60 | // Light direction angle, will be passed as uniform to shaders 61 | const light_dir = { 62 | value: Math.PI 63 | }; 64 | 65 | // Will contain an array with all the lights (cars) 66 | let lights = null; 67 | 68 | init(); 69 | 70 | function init() { 71 | 72 | // generate city 73 | addSkyBox(); 74 | const map = generateMap(); 75 | const base = createBaseGeometry(map); 76 | makeCity(map.cells); 77 | lights = createLights(500); 78 | addLightsToGroup(lights, base); 79 | scene.add(base); 80 | 81 | // camera controls 82 | camera.position.set(cameraDist, cameraHeight, cameraDist); 83 | const controls = new OrbitControls(camera, renderer.domElement); 84 | controls.maxPolarAngle = Math.PI * 0.5; 85 | controls.minDistance = near; 86 | controls.maxDistance = far; 87 | 88 | animate(); 89 | } 90 | 91 | function animate(timestamp) { 92 | 93 | requestAnimationFrame(animate); 94 | 95 | light_dir.value += 0.05; // move light direction 96 | moveLights(lights); // moves cars 97 | 98 | renderer.render(scene, camera); 99 | } 100 | 101 | function addSkyBox() { 102 | const loader = new THREE.CubeTextureLoader(); 103 | loader.setPath('img/'); 104 | 105 | // for skybox 106 | const textureCube = loader.load([ 107 | 'sky.jpg', 'sky.jpg', 108 | 'sky.jpg', 'sky.jpg', 109 | 'sky.jpg', 'sky.jpg' 110 | ]); 111 | 112 | scene.background = textureCube; 113 | // scene.fog = new THREE.FogExp2(0xcccccc, 0.2); 114 | } 115 | 116 | // shader for everything besides the moving lights (cars) 117 | function getColoredShader(color) { 118 | return new THREE.ShaderMaterial({ 119 | uniforms: { 120 | light_dir, 121 | color: { 122 | value: color 123 | }, 124 | light_color: { 125 | value: lightColor 126 | }, 127 | camera_pos: { 128 | value: camera.position 129 | } 130 | }, 131 | vertexShader: resources.vert_shader, 132 | fragmentShader: resources.frag_shader, 133 | }); 134 | } 135 | 136 | function getRandomIntInclusive(min, max) { 137 | min = Math.ceil(min); 138 | max = Math.floor(max); 139 | return Math.floor(Math.random() * (max - min + 1)) + min; //The maximum is inclusive and the minimum is inclusive 140 | } 141 | 142 | // basically a step function 143 | function getRandWithLimits(low, high) { 144 | const rand = Math.random(); 145 | if (rand < low) return low; 146 | if (rand > high) return high; 147 | return rand; 148 | } 149 | 150 | // mimic of Python range function 151 | function createRange(n) { 152 | return [...Array(n).keys()]; 153 | } 154 | 155 | // given the index of a flattened 2D array and the no. of columns, returns 2D index 156 | function convert1DIndexTo2D(index, repeatCount) { 157 | return { 158 | row: Math.floor(index / repeatCount), 159 | col: index % repeatCount 160 | }; 161 | } 162 | 163 | // generates the base of the city with the cells which will hold the buidlings 164 | function generateMap() { 165 | 166 | const baseSize = { 167 | width: repeatCount * cellSize + (repeatCount - 1) * streetWidth, 168 | depth: repeatCount * cellSize + (repeatCount - 1) * streetWidth 169 | }; 170 | 171 | const cellPositions = createRange(repeatCount * repeatCount).map(i => convert1DIndexTo2D(i, repeatCount)); 172 | 173 | const cells = cellPositions.map(pos => 174 | createCell( 175 | pos.col * (cellSize + streetWidth), 176 | 0, 177 | -(pos.row * (cellSize + streetWidth)) 178 | ) 179 | ); 180 | 181 | return { 182 | baseSize, 183 | cells 184 | }; 185 | 186 | } 187 | 188 | function createCell(x, y, z) { 189 | const group = new THREE.Group(); 190 | group.position.set(x, y, z); 191 | return group; 192 | } 193 | 194 | // generates geometries for base of the city (grid with cells and streets) 195 | function createBaseGeometry(map) { 196 | 197 | const { 198 | baseSize, 199 | cells 200 | } = map; 201 | 202 | const base = new THREE.Group(); 203 | base.position.x -= baseSize.width / 2; // center 204 | base.position.z += baseSize.depth / 2; 205 | 206 | cells.forEach(cell => { 207 | const square = generateRectShape(cellSize, cellSize); 208 | const mesh = generateShapeMesh(square, groundColor, 0, 0, 0, -Math.PI / 2, 0, 0, 1); 209 | cell.add(mesh); 210 | base.add(cell); 211 | }); 212 | 213 | // generate streets 214 | for (let i = 0; i < repeatCount - 1; ++i) { 215 | const streetShapes = [generateRectShape(streetWidth, baseSize.depth), generateRectShape(baseSize.width, streetWidth)]; 216 | const zAxisStreet = generateShapeMesh(streetShapes[0], streetColor, (i + 1) * cellSize + i * streetWidth, 0.1, 0, -Math.PI / 2, 0, 0, 1); 217 | const xAxisStreet = generateShapeMesh(streetShapes[1], streetColor, 0, 0.1, -((i + 1) * cellSize + i * streetWidth), -Math.PI / 2, 0, 0, 1); 218 | base.add(xAxisStreet, zAxisStreet); 219 | } 220 | 221 | return base; 222 | 223 | } 224 | 225 | function generateShapeMesh(shape, color, x, y, z, rx, ry, rz, s) { 226 | const geometry = new THREE.ShapeGeometry(shape); 227 | const mesh = new THREE.Mesh(geometry, getColoredShader(color)); 228 | mesh.position.set(x, y, z); 229 | mesh.rotation.set(rx, ry, rz); 230 | mesh.scale.set(s, s, s); 231 | return mesh; 232 | } 233 | 234 | function generateRectShape(width, height) { 235 | const shape = new THREE.Shape(); 236 | shape.moveTo(0, 0); 237 | shape.lineTo(width, 0); 238 | shape.lineTo(width, height); 239 | shape.lineTo(0, height); 240 | shape.lineTo(0, 0); 241 | return shape; 242 | } 243 | 244 | function createBlock(width, depth) { 245 | const grayShade = 0.15 + Math.random() / 5; 246 | const geometry = new THREE.BoxGeometry(width, height * getRandWithLimits(0.2, 1), depth); 247 | const material = getColoredShader(new THREE.Vector3(grayShade, grayShade, grayShade)); 248 | const block = new THREE.Mesh(geometry, material); 249 | return block; 250 | } 251 | 252 | function divideSquareIntoRegions() { 253 | return { 254 | x: getRandWithLimits(0.2, 0.8), 255 | y: getRandWithLimits(0.2, 0.8) 256 | }; 257 | } 258 | 259 | // puts four randomly generated blocks in a given cell 260 | function fillCellWithRandomBlocks(cell) { 261 | 262 | const getHeight = block => block.geometry.parameters.height; 263 | const innerMargin = cellSize / 20; 264 | const margin = cellSize / 10; 265 | const innerSize = cellSize - 2 * margin - innerMargin; 266 | 267 | const { 268 | x, 269 | y 270 | } = divideSquareIntoRegions(); 271 | 272 | const dimX1 = innerSize * x; 273 | const dimX2 = innerSize - dimX1; 274 | 275 | const dimY1 = innerSize * y; 276 | const dimY2 = innerSize - dimY1; 277 | 278 | const block1 = createBlock(dimX1, dimY1); 279 | const block2 = createBlock(dimX2, dimY1); 280 | const block3 = createBlock(dimX1, dimY2); 281 | const block4 = createBlock(dimX2, dimY2); 282 | 283 | block1.position.set( 284 | margin + dimX1 / 2, 285 | getHeight(block1) / 2, 286 | -dimY1 / 2 - margin); 287 | 288 | block2.position.set( 289 | innerMargin + margin + dimX1 + dimX2 / 2, 290 | getHeight(block2) / 2, 291 | -dimY1 / 2 - margin); 292 | 293 | block3.position.set( 294 | margin + dimX1 / 2, 295 | getHeight(block3) / 2, 296 | -dimY1 - dimY2 / 2 - margin - innerMargin); 297 | 298 | block4.position.set( 299 | innerMargin + margin + dimX1 + dimX2 / 2, 300 | getHeight(block4) / 2, 301 | -dimY1 - dimY2 / 2 - margin - innerMargin); 302 | 303 | cell.add(block1, block2, block3, block4); 304 | 305 | } 306 | 307 | function makeCity(cells) { 308 | cells.forEach(fillCellWithRandomBlocks); 309 | } 310 | 311 | /* 312 | Lights section 313 | */ 314 | 315 | // separate shader for moving lights (they have a texture) 316 | function getLightShader(center) { 317 | const texture = new THREE.TextureLoader().load("img/light.png") 318 | return new THREE.ShaderMaterial({ 319 | uniforms: { 320 | uTexture: { 321 | type: "t", 322 | value: texture 323 | }, 324 | center: { 325 | value: center 326 | } 327 | }, 328 | vertexShader: resources.vertl_shader, 329 | fragmentShader: resources.fragl_shader, 330 | }); 331 | } 332 | 333 | // creates four collections of moving lights (one collection for each NESW orientation) 334 | function createLights(count) { 335 | 336 | const lights = { 337 | xpos: [], 338 | xneg: [], 339 | zpos: [], 340 | zneg: [] 341 | }; 342 | 343 | const collectionKeys = Object.keys(lights); 344 | 345 | for (let l = 0; l < count; ++l) { 346 | const streetNo = getRandomIntInclusive(1, repeatCount - 1); 347 | const direction = getRandomIntInclusive(1, 4); 348 | const collectionName = collectionKeys[direction - 1]; 349 | lights[collectionName].push(createLight(streetNo, direction)); 350 | } 351 | 352 | return lights; 353 | 354 | } 355 | 356 | // create cars with random initial positions (on generated streets) and moving directions 357 | function createLight(no, direction) { 358 | 359 | let x, 360 | y = carRadius, // they float above the ground 361 | z; 362 | 363 | const streetLength = repeatCount * cellSize + (repeatCount - 1) * streetWidth; 364 | 365 | // center car on lane and put into a random position on the given street 366 | switch (direction) { 367 | case 1: 368 | z = -(no * cellSize + (no - 1) * streetWidth + streetWidth / 4); 369 | x = getRandomIntInclusive(0, streetLength); 370 | break; 371 | case 2: 372 | z = -(no * cellSize + (no - 1) * streetWidth + streetWidth * 3 / 4); 373 | x = getRandomIntInclusive(0, streetLength); 374 | break; 375 | case 3: 376 | x = no * cellSize + (no - 1) * streetWidth + streetWidth / 4; 377 | z = -getRandomIntInclusive(0, streetLength); 378 | break; 379 | case 4: 380 | x = no * cellSize + (no - 1) * streetWidth + streetWidth * 3 / 4; 381 | z = -getRandomIntInclusive(0, streetLength); 382 | break; 383 | default: 384 | break; 385 | } 386 | 387 | const centerPos = new THREE.Vector3(x, y, z); 388 | 389 | const geometry = new THREE.CircleGeometry(carRadius, 32); 390 | const material = getLightShader(carColor, centerPos); 391 | const circle = new THREE.Mesh(geometry, material); 392 | 393 | circle.position.set(x, y, z); 394 | circle.rotation.x -= Math.PI / 2; 395 | 396 | return circle; 397 | 398 | } 399 | 400 | function addLightsToGroup(lights, group) { 401 | for (let collection in lights) { 402 | lights[collection].forEach(light => group.add(light)); 403 | } 404 | } 405 | 406 | function moveLights(lights) { 407 | 408 | const streetLength = repeatCount * cellSize + (repeatCount - 1) * streetWidth; 409 | 410 | lights.xpos.forEach(l => { 411 | const pos = l.position.x; 412 | l.position.x = pos + carSpeed > streetLength ? 0 : pos + carSpeed; 413 | }); 414 | 415 | lights.xneg.forEach(l => { 416 | const pos = l.position.x; 417 | l.position.x = pos - carSpeed < 0 ? streetLength : pos - carSpeed; 418 | }); 419 | 420 | lights.zpos.forEach(l => { 421 | const pos = l.position.z; 422 | l.position.z = pos - carSpeed < -streetLength ? 0 : pos - carSpeed; 423 | }); 424 | 425 | lights.zneg.forEach(l => { 426 | const pos = l.position.z; 427 | l.position.z = pos + carSpeed > 0 ? -streetLength : pos + carSpeed; 428 | }); 429 | 430 | } 431 | 432 | }; 433 | 434 | 435 | 436 | 437 | /* Asynchronously load resources 438 | 439 | You shouldn't need to change this - you can add 440 | more resources by changing RESOURCES above */ 441 | 442 | function load_resources() { 443 | const promises = []; 444 | 445 | for (let r of RESOURCES) { 446 | promises.push(fetch(r[1]) 447 | .then(res => res.text())); 448 | } 449 | 450 | return Promise.all(promises).then(function (res) { 451 | let resources = {}; 452 | for (let i in RESOURCES) { 453 | resources[RESOURCES[i][0]] = res[i]; 454 | } 455 | return resources; 456 | }); 457 | } 458 | 459 | // Load the resources and then create the scene when resources are loaded 460 | load_resources().then(res => main(res)); -------------------------------------------------------------------------------- /shaders/default.frag: -------------------------------------------------------------------------------- 1 | uniform vec3 color; 2 | uniform float light_dir; 3 | uniform vec3 light_color; 4 | 5 | varying vec3 world_normal; 6 | varying float cam_dist; 7 | 8 | void main() { 9 | vec3 light_vec = vec3(10.0 * sin(light_dir), 10, 10.0 * cos(light_dir)); 10 | float light = 0.5 + dot(world_normal, normalize(light_vec)) / 2.0; 11 | vec3 full_color = color * (light_color * light); 12 | float fog_intensity = smoothstep(0.0, 10000.0, cam_dist); 13 | vec3 fog_color = full_color * (1.0 - fog_intensity) + vec3(0.1, 0.1, 0.1) * fog_intensity; 14 | gl_FragColor = vec4(fog_color, 1.0); 15 | } -------------------------------------------------------------------------------- /shaders/default.vert: -------------------------------------------------------------------------------- 1 | uniform vec3 camera_pos; 2 | 3 | varying vec3 world_normal; 4 | varying float cam_dist; 5 | 6 | void main() { 7 | world_normal = normalize( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal ); 8 | 9 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); 10 | cam_dist = distance(position, camera_pos); 11 | } -------------------------------------------------------------------------------- /shaders/light.frag: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | uniform sampler2D uTexture; 3 | 4 | void main() { 5 | gl_FragColor = texture(uTexture, vUv); 6 | } -------------------------------------------------------------------------------- /shaders/light.vert: -------------------------------------------------------------------------------- 1 | varying vec3 world_normal; 2 | varying vec2 vUv; 3 | uniform vec3 center; 4 | 5 | void main() { 6 | world_normal = normalize( mat3( modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz ) * normal ); 7 | 8 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); 9 | vUv = uv; 10 | } --------------------------------------------------------------------------------