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