├── .gitignore ├── Gruntfile.js ├── README.md ├── dist ├── assets │ └── tiles │ │ ├── arbre.jpg │ │ ├── basetexture.jpg │ │ └── tiles.babylon ├── css │ └── main.css ├── index.html └── js │ ├── .baseDir.js │ ├── Game.js │ ├── Preloader.js │ ├── Tile.js │ ├── Timer.js │ └── libs │ └── babylon.max.js ├── package.json ├── sass ├── _loader.scss └── main.scss ├── screen.jpg └── ts ├── .baseDir.ts ├── Game.ts ├── Preloader.ts ├── Tile.ts ├── Timer.ts ├── tsconfig.json └── typings └── babylon.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | .tmp 4 | *.map 5 | .tscache 6 | .history -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | 3 | require('time-grunt')(grunt); 4 | 5 | // load all grunt tasks 6 | require('jit-grunt')(grunt); 7 | 8 | grunt.initConfig({ 9 | 10 | // Clean dist folder (all except lib folder) 11 | clean: { 12 | js: ["dist/css","dist/js/.baseDir*","dist/js/*", "!dist/js/libs/**"], 13 | dist: ["dist/js/*", "!dist/js/index.js", "!dist/js/libs/**"] 14 | }, 15 | 16 | // Compilation from TypeScript to ES5² 17 | ts: { 18 | dev: { 19 | src : ['ts/**/*.ts', 'ts/typings/**/*'], 20 | outDir: "dist/js", 21 | options:{ 22 | module: 'amd', 23 | target: 'es5', 24 | declaration: false, 25 | sourceMap:true, 26 | removeComments:false 27 | } 28 | }, 29 | dist: { 30 | src : ['ts/**/*.ts'], 31 | outDir: "dist/js", 32 | options:{ 33 | module: 'amd', 34 | target: 'es5', 35 | declaration: false, 36 | sourceMap:false, 37 | removeComments:true 38 | } 39 | } 40 | }, 41 | // Watches content related changes 42 | watch : { 43 | js : { 44 | files: ['ts/**/*.ts'], 45 | tasks: ['ts:dev'] 46 | }, 47 | sass : { 48 | files: ['sass/**/*.scss'], 49 | tasks: ['sass','postcss:debug'] 50 | }, 51 | html : { 52 | files: ['html/**/*.html'], 53 | tasks: ['bake'] 54 | } 55 | }, 56 | // Build dist version 57 | uglify : { 58 | dist: { 59 | options: { 60 | compress:true, 61 | beautify: false 62 | }, 63 | files: { 64 | 'dist/js/index.js': ['dist/js/**/*.js', '!dist/js/libs/**/*.js'] 65 | } 66 | } 67 | }, 68 | // Sass compilation. Produce an extended css file in css folder 69 | sass : { 70 | options: { 71 | sourcemap:'none', 72 | style: 'expanded' 73 | }, 74 | dist : { 75 | files: { 76 | 'dist/css/main.css': 'sass/main.scss' 77 | } 78 | } 79 | }, 80 | // Auto prefixer css 81 | postcss : { 82 | debug : { 83 | options: { 84 | processors: [ 85 | require('autoprefixer')({browsers: 'last 2 versions'}) 86 | ] 87 | }, 88 | src: 'dist/css/main.css' 89 | }, 90 | dist: { 91 | options: { 92 | processors: [ 93 | require('autoprefixer')({browsers: 'last 2 versions'}), 94 | require('cssnano')() 95 | ] 96 | }, 97 | src: 'dist/css/main.css' 98 | } 99 | }, 100 | //Server creation 101 | connect: { 102 | server: { 103 | options: { 104 | port: 3000, 105 | base: '.' 106 | } 107 | }, 108 | test: { 109 | options: { 110 | port: 3000, 111 | base: '.', 112 | keepalive:true 113 | } 114 | } 115 | }, 116 | // Open default browser 117 | open: { 118 | local: { 119 | path: 'http://localhost:3000/dist' 120 | } 121 | } 122 | }); 123 | 124 | grunt.registerTask('default', 'Compile and watch source files', [ 125 | 'dev', 126 | 'connect:server', 127 | 'open', 128 | 'watch' 129 | ]); 130 | 131 | grunt.registerTask('run', 'Run the webserver and watch files', [ 132 | 'connect:server', 133 | 'open', 134 | 'watch' 135 | ]); 136 | 137 | grunt.registerTask('dev', 'build dev version', [ 138 | 'clean:js', 139 | 'ts:dev', 140 | 'sass', 141 | 'postcss:debug' 142 | ]); 143 | 144 | grunt.registerTask('test', 'test dist version', [ 145 | 'open', 146 | 'connect:test' 147 | ]); 148 | 149 | grunt.registerTask('dist', 'build dist version', [ 150 | 'clean:js', 151 | 'ts:dist', 152 | 'sass', 153 | 'postcss:dist', 154 | 'uglify', // compile js files in index.js 155 | 'clean:dist' // remove js file 156 | ]); 157 | 158 | }; 159 | 160 | 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Procedural City 2 | 3 | ![](https://raw.githubusercontent.com/Temechon/procedural-city/master/screen.jpg) 4 | 5 | Made with Babylon.js. 6 | -------------------------------------------------------------------------------- /dist/assets/tiles/arbre.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kimoen/procedural-city/0da8339f69749c4f4d33446e92c2ff4508556dfe/dist/assets/tiles/arbre.jpg -------------------------------------------------------------------------------- /dist/assets/tiles/basetexture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kimoen/procedural-city/0da8339f69749c4f4d33446e92c2ff4508556dfe/dist/assets/tiles/basetexture.jpg -------------------------------------------------------------------------------- /dist/css/main.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | font-family: sans-serif; } 8 | 9 | #game-canvas { 10 | width: 100%; 11 | height: 100%; } 12 | 13 | .loader { 14 | display: -ms-flexbox; 15 | display: flex; 16 | -ms-flex-pack: center; 17 | justify-content: center; 18 | -ms-flex-align: center; 19 | align-items: center; 20 | background-color: #333; 21 | font-size: 40pt; 22 | font-family: sans-serif; 23 | color: #eee; 24 | letter-spacing: 10px; 25 | position: absolute; 26 | width: 100%; 27 | height: 100%; 28 | top: 0; 29 | left: 0; 30 | z-index: 2; } 31 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Starter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | LOADING... 24 |
25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /dist/js/.baseDir.js: -------------------------------------------------------------------------------- 1 | // Ignore this file. See https://github.com/grunt-ts/grunt-ts/issues/77 2 | //# sourceMappingURL=.baseDir.js.map -------------------------------------------------------------------------------- /dist/js/Game.js: -------------------------------------------------------------------------------- 1 | var Game = (function () { 2 | function Game(canvasId) { 3 | var _this = this; 4 | this._allTiles = []; 5 | var canvas = document.getElementById(canvasId); 6 | this.engine = new BABYLON.Engine(canvas, true); 7 | this.engine.enableOfflineSupport = false; 8 | this.scene = null; 9 | window.addEventListener("resize", function () { 10 | _this.engine.resize(); 11 | }); 12 | this._initScene(); 13 | } 14 | Game.prototype._run = function () { 15 | var _this = this; 16 | this.scene.executeWhenReady(function () { 17 | // Remove loader 18 | var loader = document.querySelector("#loader"); 19 | loader.style.display = "none"; 20 | _this._initGame(); 21 | _this.engine.runRenderLoop(function () { 22 | _this.scene.render(); 23 | }); 24 | }); 25 | }; 26 | Game.prototype._initScene = function () { 27 | this.scene = new BABYLON.Scene(this.engine); 28 | this.scene.clearColor = BABYLON.Color3.FromInts(0, 163, 136); 29 | var camera = new BABYLON.FreeCamera('', new BABYLON.Vector3(-6, 21, -36), this.scene); 30 | camera.rotation = new BABYLON.Vector3(0.44, 0.73, 0); 31 | camera.attachControl(this.engine.getRenderingCanvas()); 32 | var light = new BABYLON.HemisphericLight('', new BABYLON.Vector3(0, 1, 0), this.scene); 33 | light.intensity = 0.4; 34 | var dir = new BABYLON.DirectionalLight('', new BABYLON.Vector3(0.5, -1, 0.25), this.scene); 35 | dir.position.copyFrom(dir.direction.scale(-10)); 36 | var shadowGenerator = new BABYLON.ShadowGenerator(2048, dir); 37 | shadowGenerator.useBlurVarianceShadowMap = true; 38 | shadowGenerator.setDarkness(0.05); 39 | shadowGenerator.bias = 0.0001; 40 | shadowGenerator.blurScale = 2; 41 | this.shadows = shadowGenerator; 42 | var loader = new Preloader(this); 43 | loader.callback = this._run.bind(this); 44 | loader.loadAssets(); 45 | }; 46 | Game.prototype._initGame = function () { 47 | // this.scene.debugLayer.show(); 48 | var _this = this; 49 | // Get available tiles 50 | for (var _i = 0, _a = this.scene.meshes; _i < _a.length; _i++) { 51 | var mesh = _a[_i]; 52 | if (mesh.name.substr(0, 1) == '0' || mesh.name.substr(0, 1) == '1') { 53 | var tile = new Tile(mesh.name); 54 | this._allTiles.push(tile); 55 | this._allTiles.push(tile.rotatedCopy(1)); 56 | this._allTiles.push(tile.rotatedCopy(2)); 57 | this._allTiles.push(tile.rotatedCopy(3)); 58 | } 59 | } 60 | // Create base building 61 | var baseBuilding = BABYLON.MeshBuilder.CreateBox('baseBuilding', { size: 0.5 }, this.scene); 62 | var buildingMat = new BABYLON.StandardMaterial('baseBuildingMat', this.scene); 63 | buildingMat.diffuseColor = BABYLON.Color3.White(); 64 | buildingMat.specularColor = BABYLON.Color3.Black(); 65 | baseBuilding.material = buildingMat; 66 | baseBuilding.setEnabled(false); 67 | // Build city 68 | var len = 30; 69 | var city = []; 70 | var timers = []; 71 | var delay = 0; 72 | var _loop_1 = function(j) { 73 | city[j] = []; 74 | var _loop_2 = function(i) { 75 | var timer = new Timer(delay, this_1.scene, { autodestroy: true }); 76 | timer.callback = function () { 77 | var sel = _this.findNextTile(i, j, city); 78 | sel.pos.x = i * 3; 79 | sel.pos.z = -j * 3; 80 | sel.draw(_this.createAsset(sel.name, Game.CLONE, sel.toString()), _this); 81 | city[j][i] = sel; 82 | }; 83 | timers.push(timer); 84 | delay += 150; 85 | }; 86 | for (var i = 0; i < len; i++) { 87 | _loop_2(i); 88 | } 89 | }; 90 | var this_1 = this; 91 | for (var j = 0; j < len; j++) { 92 | _loop_1(j); 93 | } 94 | for (var _b = 0, timers_1 = timers; _b < timers_1.length; _b++) { 95 | var t = timers_1[_b]; 96 | t.start(); 97 | } 98 | }; 99 | // i -> line 100 | // j -> line 101 | Game.prototype.findNextTile = function (i, j, city) { 102 | // Get last tile of the city 103 | var previousLine = null, topTile = null, leftTile = null; 104 | if (j > 0) { 105 | previousLine = city[j - 1]; 106 | topTile = city[j - 1][i]; 107 | } 108 | if (i > 0) { 109 | leftTile = city[j][i - 1]; 110 | } 111 | // random sort tiles 112 | this._shuffle(this._allTiles); 113 | return this._findMatchingTile(topTile, Tile.BOT, leftTile, Tile.RIGHT); 114 | }; 115 | Game.prototype._findMatchingTile = function (tile, face, tile2, face2) { 116 | for (var i = 0; i < this._allTiles.length; i++) { 117 | if (this._allTiles[i].canBe2(face, tile, face2, tile2)) { 118 | return this._allTiles[i]; 119 | } 120 | } 121 | }; 122 | Game.prototype.createAsset = function (name, mode, newName) { 123 | if (mode === void 0) { mode = Game.SELF; } 124 | if (newName === void 0) { newName = ''; } 125 | var mesh = this.scene.getMeshByName(name); 126 | var res = null; 127 | switch (mode) { 128 | case Game.SELF: 129 | res = mesh; 130 | mesh.setEnabled(true); 131 | break; 132 | case Game.CLONE: 133 | res = mesh.clone(newName); 134 | break; 135 | case Game.INSTANCE: 136 | res = mesh.createInstance(newName); 137 | break; 138 | } 139 | return res; 140 | }; 141 | Game.prototype._shuffle = function (array) { 142 | var currentIndex = array.length, temporaryValue, randomIndex; 143 | // While there remain elements to shuffle... 144 | while (0 !== currentIndex) { 145 | // Pick a remaining element... 146 | randomIndex = Math.floor(Math.random() * currentIndex); 147 | currentIndex -= 1; 148 | // And swap it with the current element. 149 | temporaryValue = array[currentIndex]; 150 | array[currentIndex] = array[randomIndex]; 151 | array[randomIndex] = temporaryValue; 152 | } 153 | return array; 154 | }; 155 | Game.SELF = 0; 156 | Game.CLONE = 1; 157 | Game.INSTANCE = 2; 158 | return Game; 159 | }()); 160 | //# sourceMappingURL=Game.js.map -------------------------------------------------------------------------------- /dist/js/Preloader.js: -------------------------------------------------------------------------------- 1 | var Preloader = (function () { 2 | function Preloader(game) { 3 | this._loader = null; 4 | this._game = game; 5 | this._scene = this._game.scene; 6 | this._loader = new BABYLON.AssetsManager(this._scene); 7 | this._loader.useDefaultLoadingScreen = false; 8 | this._loader.onFinish = this._onFinish.bind(this); 9 | } 10 | Preloader.prototype.loadAssets = function () { 11 | this._addMesh('tiles'); 12 | this._loader.load(); 13 | }; 14 | Preloader.prototype._onFinish = function () { 15 | this.callback(); 16 | }; 17 | Preloader.prototype._addMesh = function (folder, name) { 18 | if (name) { 19 | var task = this._loader.addMeshTask(name, '', "assets/" + folder + "/", name + ".babylon"); 20 | } 21 | else { 22 | var task = this._loader.addMeshTask(folder, '', "assets/" + folder + "/", folder + ".babylon"); 23 | } 24 | task.onSuccess = this._addMeshAssetToGame.bind(this); 25 | }; 26 | Preloader.prototype._addMeshAssetToGame = function (t) { 27 | console.group(); 28 | for (var _i = 0, _a = t.loadedMeshes; _i < _a.length; _i++) { 29 | var m = _a[_i]; 30 | m.setEnabled(false); 31 | console.log("%c Loaded : " + m.name, 'background: #333; color: #bada55'); 32 | } 33 | console.log("%c Finished : " + t.name, 'background: #333; color: #bada55'); 34 | console.groupEnd(); 35 | }; 36 | return Preloader; 37 | }()); 38 | //# sourceMappingURL=Preloader.js.map -------------------------------------------------------------------------------- /dist/js/Tile.js: -------------------------------------------------------------------------------- 1 | // 0 1 2 2 | // 3 4 5 3 | // 6 7 8 4 | var Tile = (function () { 5 | function Tile(name) { 6 | this._numbers = []; 7 | this.pos = new BABYLON.Vector3(0, 0, 0); 8 | this._angle = 0; 9 | this.name = name; 10 | // Creates numbers 11 | var tmp = name.split(''); 12 | for (var a in tmp) { 13 | this._numbers[a] = parseInt(tmp[a], 10); 14 | } 15 | } 16 | Tile.prototype.toString = function () { 17 | return this._numbers.toString(); 18 | }; 19 | // => top : (0 1 2) 20 | // => right : (2 5 8) 21 | // => bot : (6 7 8) 22 | // => left : (0 3 6) 23 | Tile.prototype.getFace = function (face) { 24 | var res = []; 25 | switch (face) { 26 | case Tile.TOP: 27 | res = [this._numbers[0], this._numbers[1], this._numbers[2]]; 28 | break; 29 | case Tile.RIGHT: 30 | res = [this._numbers[2], this._numbers[5], this._numbers[8]]; 31 | break; 32 | case Tile.BOT: 33 | res = [this._numbers[6], this._numbers[7], this._numbers[8]]; 34 | break; 35 | case Tile.LEFT: 36 | res = [this._numbers[0], this._numbers[3], this._numbers[6]]; 37 | break; 38 | } 39 | return res; 40 | }; 41 | // 0 : (0,3,6) 42 | // 1 : (1,4,7) 43 | // 2 : (2,5,8) 44 | Tile.prototype._getCol = function (nb) { 45 | var res = []; 46 | switch (nb) { 47 | case 0: 48 | res = [this._numbers[0], this._numbers[3], this._numbers[6]]; 49 | break; 50 | case 1: 51 | res = [this._numbers[1], this._numbers[4], this._numbers[7]]; 52 | break; 53 | case 2: 54 | res = [this._numbers[2], this._numbers[5], this._numbers[8]]; 55 | break; 56 | } 57 | return res; 58 | }; 59 | // anticlockwise rotate 60 | Tile.prototype._rotate = function () { 61 | this._numbers = this._getCol(2).concat(this._getCol(1).concat(this._getCol(0))); 62 | this._angle += Math.PI / 2; 63 | }; 64 | // anticlockwise rotate 'nb' times 65 | Tile.prototype.rotate = function (nb) { 66 | for (var i = 0; i < nb; i++) { 67 | this._rotate(); 68 | } 69 | }; 70 | Tile.prototype.rotatedCopy = function (nb) { 71 | var tile = new Tile(this.name); 72 | tile.rotate(nb); 73 | return tile; 74 | }; 75 | // This tile can be set on the 'face' of the 'otherTile' ? 76 | Tile.prototype.canBe = function (onFace, otherTile) { 77 | // IF no other tile is given, then this tile can fit anywhere 78 | if (!otherTile) { 79 | return true; 80 | } 81 | var face = this.getFace((onFace + 2) % 4); 82 | var otherface = otherTile.getFace(onFace); 83 | var res = true; 84 | for (var i = 0; i < 3; i++) { 85 | res = res && face[i] == otherface[i]; 86 | } 87 | return res; 88 | }; 89 | // This tile can be on the 'face' of otherTile1 and on the 'face2" of the otherTile2 ? 90 | Tile.prototype.canBe2 = function (onFace1, otherTile1, onFace2, otherTile2) { 91 | return this.canBe(onFace1, otherTile1) && this.canBe(onFace2, otherTile2); 92 | }; 93 | Tile.prototype.draw = function (mesh, game) { 94 | mesh.position.copyFrom(this.pos); 95 | mesh.receiveShadows = true; 96 | var dx = [-1, 0, 1]; 97 | var dz = [1, 0, -1]; 98 | // get original numbers 99 | var tmp = []; 100 | tmp = this.name.split(''); 101 | for (var a in tmp) { 102 | tmp[a] = parseInt(tmp[a], 10); 103 | } 104 | for (var i = 0; i < tmp.length; i++) { 105 | if (tmp[i] == 0 && Math.random() > 0.6) { 106 | var box = game.createAsset('baseBuilding', Game.INSTANCE); 107 | box.scaling.y = this._randomInt(1, 5); 108 | box.scaling.x = this._randomInt(1, 3); 109 | box.scaling.z = this._randomInt(1, 3); 110 | game.shadows.getShadowMap().renderList.push(box); 111 | box.position.x = dx[i % 3]; 112 | box.position.y = box.scaling.y / 4; 113 | box.position.z = dz[Math.floor(i / 3)]; 114 | box.parent = mesh; 115 | } 116 | else if (tmp[i] == 0 && Math.random() > 0.7) { 117 | var arbre = game.createAsset('arbre', Game.INSTANCE); 118 | game.shadows.getShadowMap().renderList.push(arbre); 119 | arbre.position.x = dx[i % 3]; 120 | arbre.position.z = dz[Math.floor(i / 3)]; 121 | arbre.parent = mesh; 122 | } 123 | } 124 | mesh.rotation.y = -this._angle; 125 | // Magic optimisation ! 126 | mesh.freezeWorldMatrix(); 127 | mesh.getDescendants().forEach(function (m) { 128 | m.freezeWorldMatrix(); 129 | }); 130 | }; 131 | Tile.prototype._randomInt = function (min, max) { 132 | if (min === max) { 133 | return (min); 134 | } 135 | var random = Math.random(); 136 | return Math.floor(((random * (max - min)) + min)); 137 | }; 138 | Tile.TOP = 0; 139 | Tile.RIGHT = 1; 140 | Tile.BOT = 2; 141 | Tile.LEFT = 3; 142 | return Tile; 143 | }()); 144 | //# sourceMappingURL=Tile.js.map -------------------------------------------------------------------------------- /dist/js/Timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds a new timer. A timer can delay an action, and repeat this action X times 3 | * @param time The time in milliseconds 4 | * @param scene The scene 5 | * @param callback The callback function called when the timer is finished 6 | * @param options.autostart If set to true, the timer will autostart. False by default. 7 | *@param options.autodestroy If set to true, the timer will autodestroy at the end of all callback functions. False by default 8 | *@param options.repeat If set, the callback action will be repeated the specified number of times. 1 by default. Set to -1 if repeat infinitely 9 | *@param options.immediate If set, the callback action will be called before waiting 'time' ms. false by default. 10 | */ 11 | var Timer = (function () { 12 | function Timer(time, scene, options) { 13 | if (options === void 0) { options = {}; } 14 | this.scene = scene; 15 | this.maxTime = this.currentTime = time; 16 | // True if the timer is finished, false otherwise 17 | this.isOver = false; 18 | // True if the timer is paused, false otherwise 19 | this.paused = false; 20 | // True if the timer has been started, false otherwise 21 | this.started = false; 22 | // Function to be repeated when the timer is finished 23 | this.callback = null; 24 | // Function to be called when the timer is finished (no more repeat counts) 25 | this.onFinish = null; 26 | //If set, the callback action will be repeated the specified number of times 27 | this.repeat = options.repeat || 1; 28 | this.repeatInit = this.repeat; 29 | // Should the timer start directly after its creation ? 30 | this.autostart = options.autostart || false; 31 | // Should the timer destroy itself after completion ? 32 | this.autodestroy = options.autodestroy || false; 33 | // Should the timer call 'callback function' immediately after starting ? 34 | this.immediate = options.immediate || false; 35 | this.registeredFunction = this._checkIfUpdate.bind(this); 36 | scene.registerBeforeRender(this.registeredFunction); 37 | // Start the timer is set to autostart 38 | if (this.autostart) { 39 | this.start(); 40 | } 41 | } 42 | /** 43 | * Check if this timer can be updated 44 | */ 45 | Timer.prototype._checkIfUpdate = function () { 46 | if (this.started && !this.isOver && !this.paused) { 47 | this._update(); 48 | } 49 | }; 50 | /** 51 | * Reset the timer. Do not reset its options! 52 | */ 53 | Timer.prototype.reset = function () { 54 | this.currentTime = this.maxTime; 55 | this.isOver = false; 56 | this.started = false; 57 | this.paused = false; 58 | }; 59 | /** 60 | * Reset the timer and its repeat number 61 | */ 62 | Timer.prototype.hardReset = function () { 63 | this.reset(); 64 | this.repeat = this.repeatInit; 65 | }; 66 | /** 67 | * Start the timer 68 | */ 69 | Timer.prototype.start = function () { 70 | this.started = true; 71 | }; 72 | /** 73 | * Pause the timer 74 | */ 75 | Timer.prototype.pause = function () { 76 | this.paused = true; 77 | }; 78 | /** 79 | * Stop the timer, and reset it. 80 | * @param destroy If set to true, the timer is deleted. 81 | */ 82 | Timer.prototype.stop = function (destroy) { 83 | this.started = false; 84 | this.reset(); 85 | if (this.autodestroy || destroy) { 86 | this.destroy(); 87 | } 88 | }; 89 | /** 90 | * Destory the timer 91 | * @private 92 | */ 93 | Timer.prototype.destroy = function () { 94 | // Unregister update function 95 | this.scene.unregisterBeforeRender(this.registeredFunction); 96 | }; 97 | /** 98 | * Unpause the timer 99 | */ 100 | Timer.prototype.resume = function () { 101 | this.paused = false; 102 | }; 103 | /** 104 | * The update function 105 | * @private 106 | */ 107 | Timer.prototype._update = function () { 108 | this.currentTime -= this.scene.getEngine().getDeltaTime(); 109 | if (this.currentTime <= 0 || this.immediate) { 110 | // reset immediate 111 | this.immediate = false; 112 | // The delay is finished, run the callback 113 | this.isOver = true; 114 | if (this.repeat != -1) { 115 | this.repeat--; 116 | } 117 | if (this.callback) { 118 | this.callback(); 119 | } 120 | if (this.repeat > 0 || this.repeat == -1) { 121 | this.reset(); 122 | this.start(); 123 | } 124 | else { 125 | // Call the onFinish action 126 | if (this.onFinish) { 127 | this.onFinish(); 128 | } 129 | // Autodestroy 130 | if (this.autodestroy) { 131 | this.destroy(); 132 | } 133 | } 134 | } 135 | }; 136 | return Timer; 137 | }()); 138 | //# sourceMappingURL=Timer.js.map -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bjs-starter", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "autoprefixer": "^6.1.2", 6 | "cssnano": "^3.4.0", 7 | "grunt": "latest", 8 | "grunt-contrib-clean": "^0.7.0", 9 | "grunt-contrib-connect": "^0.11.2", 10 | "grunt-contrib-watch": "latest", 11 | "grunt-newer": "latest", 12 | "grunt-open": "^0.2.3", 13 | "grunt-postcss": "^0.7.1", 14 | "grunt-sass": "latest", 15 | "grunt-ts": "5.5.1", 16 | "jit-grunt": "latest", 17 | "time-grunt": "latest" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sass/_loader.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | display:flex; 3 | justify-content: center; 4 | align-items: center; 5 | 6 | background-color:#333; 7 | 8 | font-size:40pt; 9 | font-family: sans-serif; 10 | color:#eee; 11 | letter-spacing: 10px; 12 | 13 | position:absolute; 14 | width:100%; 15 | height:100%; 16 | top:0; 17 | left:0; 18 | z-index:2; 19 | } -------------------------------------------------------------------------------- /sass/main.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height:100%; 4 | margin:0; 5 | padding:0; 6 | overflow:hidden; 7 | font-family : sans-serif; 8 | } 9 | 10 | #game-canvas { 11 | width:100%; 12 | height:100%; 13 | } 14 | 15 | @import "loader"; -------------------------------------------------------------------------------- /screen.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kimoen/procedural-city/0da8339f69749c4f4d33446e92c2ff4508556dfe/screen.jpg -------------------------------------------------------------------------------- /ts/.baseDir.ts: -------------------------------------------------------------------------------- 1 | // Ignore this file. See https://github.com/grunt-ts/grunt-ts/issues/77 -------------------------------------------------------------------------------- /ts/Game.ts: -------------------------------------------------------------------------------- 1 | class Game { 2 | 3 | private engine: BABYLON.Engine; 4 | public scene: BABYLON.Scene; 5 | 6 | private _allTiles : Array = []; 7 | 8 | public shadows : BABYLON.ShadowGenerator; 9 | 10 | constructor(canvasId: string) { 11 | 12 | let canvas: HTMLCanvasElement = document.getElementById(canvasId); 13 | this.engine = new BABYLON.Engine(canvas, true); 14 | this.engine.enableOfflineSupport = false; 15 | 16 | this.scene = null; 17 | 18 | window.addEventListener("resize", () => { 19 | this.engine.resize(); 20 | }); 21 | 22 | this._initScene(); 23 | } 24 | 25 | private _run() { 26 | this.scene.executeWhenReady(() => { 27 | 28 | // Remove loader 29 | var loader = document.querySelector("#loader"); 30 | loader.style.display = "none"; 31 | 32 | this._initGame(); 33 | 34 | this.engine.runRenderLoop(() => { 35 | this.scene.render(); 36 | }); 37 | }); 38 | } 39 | 40 | private _initScene() { 41 | 42 | this.scene = new BABYLON.Scene(this.engine); 43 | this.scene.clearColor = BABYLON.Color3.FromInts(0,163,136); 44 | 45 | let camera = new BABYLON.FreeCamera('', new BABYLON.Vector3(-6, 21, -36), this.scene); 46 | camera.rotation = new BABYLON.Vector3(0.44,0.73, 0); 47 | camera.attachControl(this.engine.getRenderingCanvas()); 48 | let light = new BABYLON.HemisphericLight('', new BABYLON.Vector3(0, 1, 0), this.scene); 49 | light.intensity = 0.4; 50 | 51 | let dir = new BABYLON.DirectionalLight('', new BABYLON.Vector3(0.5,-1,0.25), this.scene); 52 | dir.position.copyFrom(dir.direction.scale(-10)); 53 | 54 | var shadowGenerator = new BABYLON.ShadowGenerator(2048, dir); 55 | shadowGenerator.useBlurVarianceShadowMap = true; 56 | shadowGenerator.setDarkness(0.05); 57 | shadowGenerator.bias = 0.0001; 58 | shadowGenerator.blurScale = 2; 59 | this.shadows = shadowGenerator; 60 | 61 | let loader = new Preloader(this); 62 | loader.callback = this._run.bind(this); 63 | loader.loadAssets(); 64 | 65 | } 66 | 67 | 68 | private _initGame () { 69 | // this.scene.debugLayer.show(); 70 | 71 | // Get available tiles 72 | for (let mesh of this.scene.meshes) { 73 | if (mesh.name.substr(0,1) == '0' || mesh.name.substr(0,1) == '1' ){ 74 | let tile = new Tile(mesh.name); 75 | this._allTiles.push(tile); 76 | this._allTiles.push(tile.rotatedCopy(1)); 77 | this._allTiles.push(tile.rotatedCopy(2)); 78 | this._allTiles.push(tile.rotatedCopy(3)); 79 | } 80 | } 81 | 82 | // Create base building 83 | let baseBuilding = BABYLON.MeshBuilder.CreateBox('baseBuilding', {size:0.5}, this.scene); 84 | let buildingMat = new BABYLON.StandardMaterial('baseBuildingMat', this.scene); 85 | buildingMat.diffuseColor = BABYLON.Color3.White(); 86 | buildingMat.specularColor = BABYLON.Color3.Black(); 87 | baseBuilding.material = buildingMat; 88 | baseBuilding.setEnabled(false); 89 | 90 | // Build city 91 | let len = 30; 92 | let city = []; 93 | 94 | var timers = []; 95 | var delay = 0; 96 | 97 | for (let j=0; j { 102 | let sel = this.findNextTile(i, j, city); 103 | 104 | sel.pos.x = i*3; 105 | sel.pos.z = -j*3; 106 | sel.draw(this.createAsset(sel.name, Game.CLONE, sel.toString()), this); 107 | city[j][i] = sel; 108 | }; 109 | timers.push(timer); 110 | delay += 150; 111 | } 112 | } 113 | 114 | for (let t of timers) { 115 | t.start(); 116 | } 117 | } 118 | 119 | // i -> line 120 | // j -> line 121 | private findNextTile(i:number, j:number, city:Array>) { 122 | // Get last tile of the city 123 | let previousLine = null, topTile = null, leftTile = null; 124 | if (j>0){ 125 | previousLine = city[j-1]; 126 | topTile = city[j-1][i]; 127 | } 128 | if (i>0) { 129 | leftTile = city[j][i-1]; 130 | } 131 | 132 | // random sort tiles 133 | this._shuffle(this._allTiles); 134 | return this._findMatchingTile(topTile, Tile.BOT, leftTile, Tile.RIGHT); 135 | } 136 | 137 | private _findMatchingTile(tile:Tile, face:number, tile2:Tile, face2:number) { 138 | for (var i = 0; i < this._allTiles.length; i++) { 139 | if (this._allTiles[i].canBe2(face, tile, face2, tile2)) { 140 | return this._allTiles[i]; 141 | } 142 | } 143 | } 144 | 145 | public static SELF : number = 0; 146 | public static CLONE : number = 1; 147 | public static INSTANCE : number = 2; 148 | 149 | public createAsset(name:string, mode:number=Game.SELF, newName:string='') : BABYLON.Mesh { 150 | let mesh : BABYLON.Mesh = this.scene.getMeshByName(name); 151 | let res = null; 152 | switch (mode) { 153 | case Game.SELF: 154 | res = mesh; 155 | mesh.setEnabled(true); 156 | break; 157 | case Game.CLONE: 158 | res = mesh.clone(newName); 159 | break; 160 | 161 | case Game.INSTANCE: 162 | res = mesh.createInstance(newName); 163 | break; 164 | } 165 | return res; 166 | } 167 | 168 | 169 | private _shuffle(array) { 170 | var currentIndex = array.length, temporaryValue, randomIndex; 171 | 172 | // While there remain elements to shuffle... 173 | while (0 !== currentIndex) { 174 | // Pick a remaining element... 175 | randomIndex = Math.floor(Math.random() * currentIndex); 176 | currentIndex -= 1; 177 | 178 | // And swap it with the current element. 179 | temporaryValue = array[currentIndex]; 180 | array[currentIndex] = array[randomIndex]; 181 | array[randomIndex] = temporaryValue; 182 | } 183 | 184 | return array; 185 | } 186 | 187 | 188 | } 189 | -------------------------------------------------------------------------------- /ts/Preloader.ts: -------------------------------------------------------------------------------- 1 | class Preloader { 2 | 3 | private _game : Game; 4 | private _scene : BABYLON.Scene; 5 | private _loader : BABYLON.AssetsManager = null; 6 | 7 | // Function called when the loader is over 8 | public callback : () => void; 9 | 10 | constructor(game:Game) { 11 | this._game = game; 12 | this._scene = this._game.scene; 13 | 14 | this._loader = new BABYLON.AssetsManager(this._scene); 15 | this._loader.useDefaultLoadingScreen = false; 16 | this._loader.onFinish = this._onFinish.bind(this); 17 | 18 | } 19 | 20 | public loadAssets() { 21 | 22 | this._addMesh('tiles'); 23 | this._loader.load(); 24 | } 25 | 26 | private _onFinish() { 27 | this.callback(); 28 | } 29 | 30 | private _addMesh(folder :string, name?:string ) { 31 | if (name) { 32 | var task = this._loader.addMeshTask(name, '', `assets/${folder}/`, `${name}.babylon`); 33 | } else { 34 | var task = this._loader.addMeshTask(folder, '', `assets/${folder}/`, `${folder}.babylon`); 35 | } 36 | task.onSuccess = this._addMeshAssetToGame.bind(this); 37 | } 38 | 39 | private _addMeshAssetToGame(t: BABYLON.MeshAssetTask) { 40 | 41 | console.group(); 42 | for (let m of t.loadedMeshes) { 43 | m.setEnabled(false); 44 | console.log(`%c Loaded : ${m.name}`, 'background: #333; color: #bada55'); 45 | } 46 | console.log(`%c Finished : ${t.name}`, 'background: #333; color: #bada55'); 47 | 48 | console.groupEnd(); 49 | } 50 | } -------------------------------------------------------------------------------- /ts/Tile.ts: -------------------------------------------------------------------------------- 1 | // 0 1 2 2 | // 3 4 5 3 | // 6 7 8 4 | class Tile { 5 | 6 | private _numbers : Array = []; 7 | public pos : BABYLON.Vector3 = new BABYLON.Vector3(0,0,0); 8 | private _angle : number = 0; 9 | public name : string; 10 | 11 | public static TOP:number = 0; 12 | public static RIGHT:number = 1; 13 | public static BOT:number = 2; 14 | public static LEFT:number = 3; 15 | 16 | constructor(name) { 17 | 18 | this.name = name; 19 | 20 | // Creates numbers 21 | let tmp = name.split(''); 22 | for (let a in tmp) { 23 | this._numbers[a] = parseInt(tmp[a], 10); 24 | } 25 | } 26 | 27 | public toString() : string { 28 | return this._numbers.toString(); 29 | } 30 | 31 | // => top : (0 1 2) 32 | // => right : (2 5 8) 33 | // => bot : (6 7 8) 34 | // => left : (0 3 6) 35 | public getFace(face:number) : Array { 36 | let res = []; 37 | switch (face) { 38 | case Tile.TOP: 39 | res = [this._numbers[0], this._numbers[1], this._numbers[2]] 40 | break; 41 | case Tile.RIGHT: 42 | res = [this._numbers[2], this._numbers[5], this._numbers[8]]; 43 | break; 44 | case Tile.BOT: 45 | res = [this._numbers[6], this._numbers[7], this._numbers[8]]; 46 | break; 47 | case Tile.LEFT: 48 | res = [this._numbers[0], this._numbers[3], this._numbers[6]]; 49 | break; 50 | } 51 | return res; 52 | } 53 | 54 | 55 | // 0 : (0,3,6) 56 | // 1 : (1,4,7) 57 | // 2 : (2,5,8) 58 | private _getCol(nb) : Array { 59 | let res = []; 60 | switch (nb) { 61 | case 0: 62 | res = [this._numbers[0], this._numbers[3], this._numbers[6]]; 63 | break; 64 | case 1: 65 | res = [this._numbers[1], this._numbers[4], this._numbers[7]]; 66 | break; 67 | case 2: 68 | res = [this._numbers[2], this._numbers[5], this._numbers[8]]; 69 | break; 70 | } 71 | return res; 72 | } 73 | // anticlockwise rotate 74 | private _rotate() { 75 | this._numbers = this._getCol(2).concat(this._getCol(1).concat(this._getCol(0))); 76 | this._angle += Math.PI/2; 77 | } 78 | 79 | // anticlockwise rotate 'nb' times 80 | public rotate(nb:number) { 81 | for (let i=0; i 0.6) { 128 | let box = game.createAsset('baseBuilding', Game.INSTANCE); 129 | box.scaling.y = this._randomInt(1,5); 130 | box.scaling.x = this._randomInt(1,3); 131 | box.scaling.z = this._randomInt(1,3); 132 | game.shadows.getShadowMap().renderList.push(box); 133 | 134 | box.position.x = dx[i % 3]; 135 | box.position.y = box.scaling.y/4; 136 | box.position.z = dz[Math.floor(i / 3)]; 137 | box.parent = mesh; 138 | } else if (tmp[i]==0 && Math.random() > 0.7) { 139 | 140 | let arbre = game.createAsset('arbre', Game.INSTANCE); 141 | game.shadows.getShadowMap().renderList.push(arbre); 142 | arbre.position.x = dx[i % 3]; 143 | arbre.position.z = dz[Math.floor(i / 3)]; 144 | arbre.parent = mesh; 145 | } 146 | } 147 | mesh.rotation.y = -this._angle; 148 | 149 | // Magic optimisation ! 150 | mesh.freezeWorldMatrix(); 151 | mesh.getDescendants().forEach((m) => { 152 | ( m).freezeWorldMatrix(); 153 | }); 154 | } 155 | 156 | private _randomInt(min, max) { 157 | if (min === max) { 158 | return (min); 159 | } 160 | var random = Math.random(); 161 | return Math.floor(((random * (max - min)) + min)); 162 | } 163 | } -------------------------------------------------------------------------------- /ts/Timer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds a new timer. A timer can delay an action, and repeat this action X times 3 | * @param time The time in milliseconds 4 | * @param scene The scene 5 | * @param callback The callback function called when the timer is finished 6 | * @param options.autostart If set to true, the timer will autostart. False by default. 7 | *@param options.autodestroy If set to true, the timer will autodestroy at the end of all callback functions. False by default 8 | *@param options.repeat If set, the callback action will be repeated the specified number of times. 1 by default. Set to -1 if repeat infinitely 9 | *@param options.immediate If set, the callback action will be called before waiting 'time' ms. false by default. 10 | */ 11 | class Timer { 12 | 13 | private scene : BABYLON.Scene; 14 | private registeredFunction : () => void; 15 | 16 | /** The timer max time (in ms) */ 17 | public maxTime : number; 18 | /** The timer current time */ 19 | public currentTime : number; 20 | /** True if the timer is finished, false otherwise */ 21 | public isOver : boolean; 22 | /** True if the timer is paused, false otherwise */ 23 | public paused : boolean; 24 | /** True if the timer has been started, false otherwise */ 25 | public started : boolean; 26 | /** Function to be repeated when the timer is finished */ 27 | public callback: () => void; 28 | /** Function to be called when the timer is finished (no more repeat counts) */ 29 | public onFinish: () => void; 30 | /**If set, the callback action will be repeated the specified number of times */ 31 | public repeat : number; 32 | private repeatInit : number; 33 | /** Should the timer start directly after its creation ? */ 34 | public autostart : boolean; 35 | /** Should the timer call 'callback function' immediately after starting ? */ 36 | public immediate : boolean; 37 | /** Should the timer destroy itself after completion ? */ 38 | public autodestroy : boolean; 39 | 40 | 41 | constructor(time: number, scene:BABYLON.Scene, options:{repeat?:number, autostart?:boolean, autodestroy?:boolean, immediate?:boolean} = {}) { 42 | 43 | this.scene = scene; 44 | 45 | this.maxTime = this.currentTime = time; 46 | 47 | // True if the timer is finished, false otherwise 48 | this.isOver = false; 49 | 50 | // True if the timer is paused, false otherwise 51 | this.paused = false; 52 | 53 | // True if the timer has been started, false otherwise 54 | this.started = false; 55 | 56 | // Function to be repeated when the timer is finished 57 | this.callback = null; 58 | 59 | // Function to be called when the timer is finished (no more repeat counts) 60 | this.onFinish = null; 61 | 62 | //If set, the callback action will be repeated the specified number of times 63 | this.repeat = options.repeat || 1; 64 | this.repeatInit = this.repeat; 65 | 66 | // Should the timer start directly after its creation ? 67 | this.autostart = options.autostart || false; 68 | 69 | // Should the timer destroy itself after completion ? 70 | this.autodestroy = options.autodestroy || false; 71 | 72 | // Should the timer call 'callback function' immediately after starting ? 73 | this.immediate = options.immediate || false; 74 | 75 | this.registeredFunction = this._checkIfUpdate.bind(this); 76 | scene.registerBeforeRender(this.registeredFunction); 77 | 78 | // Start the timer is set to autostart 79 | if (this.autostart) { 80 | this.start(); 81 | } 82 | } 83 | 84 | /** 85 | * Check if this timer can be updated 86 | */ 87 | private _checkIfUpdate () : void { 88 | if (this.started && !this.isOver && !this.paused) { 89 | this._update(); 90 | } 91 | } 92 | 93 | /** 94 | * Reset the timer. Do not reset its options! 95 | */ 96 | public reset() { 97 | this.currentTime = this.maxTime; 98 | this.isOver = false; 99 | this.started = false; 100 | this.paused = false; 101 | } 102 | 103 | /** 104 | * Reset the timer and its repeat number 105 | */ 106 | public hardReset() { 107 | this.reset(); 108 | this.repeat = this.repeatInit; 109 | } 110 | 111 | /** 112 | * Start the timer 113 | */ 114 | public start() { 115 | this.started = true; 116 | } 117 | 118 | /** 119 | * Pause the timer 120 | */ 121 | public pause() { 122 | this.paused = true; 123 | } 124 | 125 | /** 126 | * Stop the timer, and reset it. 127 | * @param destroy If set to true, the timer is deleted. 128 | */ 129 | public stop(destroy : boolean) { 130 | this.started = false; 131 | this.reset(); 132 | if (this.autodestroy || destroy) { 133 | this.destroy(); 134 | } 135 | } 136 | 137 | /** 138 | * Destory the timer 139 | * @private 140 | */ 141 | private destroy() { 142 | // Unregister update function 143 | this.scene.unregisterBeforeRender(this.registeredFunction); 144 | } 145 | 146 | /** 147 | * Unpause the timer 148 | */ 149 | public resume() { 150 | this.paused = false; 151 | } 152 | 153 | /** 154 | * The update function 155 | * @private 156 | */ 157 | private _update() { 158 | 159 | this.currentTime -= this.scene.getEngine().getDeltaTime(); 160 | 161 | if (this.currentTime <= 0 || this.immediate) { 162 | // reset immediate 163 | this.immediate = false; 164 | 165 | // The delay is finished, run the callback 166 | this.isOver = true; 167 | if (this.repeat != -1) { 168 | this.repeat--; 169 | } 170 | if (this.callback) { 171 | this.callback(); 172 | } 173 | 174 | if (this.repeat > 0 || this.repeat == -1) { 175 | this.reset(); 176 | this.start(); 177 | } else { 178 | // Call the onFinish action 179 | if (this.onFinish) { 180 | this.onFinish(); 181 | } 182 | // Autodestroy 183 | if (this.autodestroy) { 184 | this.destroy(); 185 | } 186 | } 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5" 4 | }, 5 | "files": [ 6 | "typings/babylon.d.ts", 7 | "Game.ts", 8 | "Tile.ts", 9 | "Preloader.ts", 10 | "Timer.ts" 11 | ] 12 | } --------------------------------------------------------------------------------