├── style.css ├── src ├── lib │ ├── Constants.js │ ├── Camera.js │ ├── Pixel.js │ ├── Maths.js │ ├── Renderer.js │ ├── ChunkManager.js │ ├── Chunk.js │ ├── Mesh.js │ ├── World.js │ ├── Application.js │ ├── NoiseGenerator.js │ └── TerrainGenerator.js └── main.js └── index.html /style.css: -------------------------------------------------------------------------------- 1 | body{ 2 | padding: 0; 3 | margin: 0; 4 | } -------------------------------------------------------------------------------- /src/lib/Constants.js: -------------------------------------------------------------------------------- 1 | const CHUNK_SIZE = 100 2 | const WINDOW_WIDTH = window.innerWidth 3 | const WINDOW_HEIGHT = window.innerHeight 4 | const SNOW_LEVEL = 400 -------------------------------------------------------------------------------- /src/lib/Camera.js: -------------------------------------------------------------------------------- 1 | class Camera{ 2 | constructor(position){ 3 | this.position = position 4 | this.speed = 5 5 | } 6 | move(dx, dz){ 7 | this.position.x += dx * this.speed 8 | this.position.z += dz * this.speed 9 | } 10 | } -------------------------------------------------------------------------------- /src/lib/Pixel.js: -------------------------------------------------------------------------------- 1 | const PixelType = { 2 | Air: 0, 3 | Grass: 1, 4 | Water: 2, 5 | Sand: 3, 6 | Dirt: 4, 7 | Snow: 5 8 | } 9 | 10 | class Pixel{ 11 | constructor(){ 12 | this.type = PixelType.Air 13 | this.heightMap = 1 14 | } 15 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const chunk = document.getElementById('chunk') 2 | const context = chunk.getContext('2d') 3 | const title = 'Noise map - Simulator' 4 | 5 | context.canvas.width = WINDOW_WIDTH 6 | context.canvas.height = WINDOW_HEIGHT 7 | 8 | const seed = Math.random() * 10000 9 | var noiseGenerator = new NoiseGenerator(seed) 10 | 11 | var camera = new Camera({x: 50000, z: 50000}) 12 | var renderer = new Renderer() 13 | var app = new Application(title, renderer, noiseGenerator, camera) 14 | 15 | app.loadEvents() 16 | app.runLoop() -------------------------------------------------------------------------------- /src/lib/Maths.js: -------------------------------------------------------------------------------- 1 | class Maths{ 2 | static smoothInterpolation(bottomLeft, topLeft, bottomRight, topRight, 3 | xMin, xMax, 4 | zMin, zMax, 5 | x, z) { 6 | var width = xMax - xMin, 7 | height = zMax - zMin 8 | var xValue = 1 - (x - xMin) / width 9 | var zValue = 1 - (z - zMin) / height 10 | 11 | var a = this.smoothstep(bottomLeft, bottomRight, xValue) 12 | var b = this.smoothstep(topLeft, topRight, xValue) 13 | return this.smoothstep(a, b, zValue) 14 | } 15 | 16 | static smoothstep(edge0, edge1, x) { 17 | x = x * x * (3 - 2 * x) 18 | return (edge0 * x) + (edge1 * (1 - x)) 19 | } 20 | } -------------------------------------------------------------------------------- /src/lib/Renderer.js: -------------------------------------------------------------------------------- 1 | class Renderer{ 2 | constructor(){ 3 | this.meshes = [] 4 | } 5 | 6 | drawChunk(chunk){ 7 | this.meshes.push(chunk.mesh) 8 | } 9 | 10 | render(camera){ 11 | for(var iMesh in this.meshes){ 12 | const mesh = this.meshes[iMesh] 13 | const cameraViewX = camera.position.x - WINDOW_WIDTH / 2 14 | const cameraViewZ = camera.position.z - WINDOW_HEIGHT / 2 15 | const meshX = parseInt(mesh.position.x) - cameraViewX 16 | const meshZ = parseInt(mesh.position.z) - cameraViewZ 17 | context.putImageData(mesh.imgData, meshX, meshZ) 18 | } 19 | this.meshes = [] 20 | } 21 | } -------------------------------------------------------------------------------- /src/lib/ChunkManager.js: -------------------------------------------------------------------------------- 1 | class ChunkManager{ 2 | constructor(noiseGenerator){ 3 | this.chunks = [] 4 | this.terrainGenerator = new TerrainGenerator(noiseGenerator) 5 | } 6 | 7 | getChunkAt(x, z){ 8 | return this.chunks.find((element) => element.x === x && element.z === z) 9 | } 10 | 11 | getChunk(x, z){ 12 | const chunk = this.getChunkAt(x, z) 13 | if(!chunk){ 14 | const element = new Chunk({ x, z }) 15 | this.chunks.push({x, z, element}) 16 | } 17 | return this.getChunkAt(x, z).element 18 | } 19 | 20 | load(x, z){ 21 | var chunk = this.getChunk(x, z) 22 | chunk.load(this.terrainGenerator) 23 | } 24 | 25 | addToBuffer(x, z){ 26 | var chunk = this.getChunk(x, z) 27 | return chunk.addToBuffer() 28 | } 29 | } -------------------------------------------------------------------------------- /src/lib/Chunk.js: -------------------------------------------------------------------------------- 1 | class Chunk { 2 | constructor(position) { 3 | this.pixels = new Array(CHUNK_SIZE * CHUNK_SIZE) 4 | this.mesh = new Mesh(position) 5 | this.isLoaded = false 6 | this.isBuffered = false 7 | this.position = position 8 | } 9 | 10 | load(generator) { 11 | if (!this.isLoaded) { 12 | generator.generate(this) 13 | this.isLoaded = true 14 | } 15 | } 16 | 17 | setPixel(x, z, type, heightMap = 1) { 18 | if (!this.pixels[z * CHUNK_SIZE + x]) { 19 | this.pixels[z * CHUNK_SIZE + x] = new Pixel() 20 | } 21 | this.pixels[z * CHUNK_SIZE + x].type = type 22 | this.pixels[z * CHUNK_SIZE + x].heightMap = heightMap 23 | } 24 | 25 | draw(renderer) { 26 | if (this.isBuffered) { 27 | renderer.drawChunk(this) 28 | } 29 | } 30 | 31 | addToBuffer() { 32 | if (!this.isBuffered) { 33 | this.mesh.add(this.pixels) 34 | this.isBuffered = true 35 | return true 36 | } 37 | return false 38 | } 39 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/Mesh.js: -------------------------------------------------------------------------------- 1 | class Mesh{ 2 | constructor(position){ 3 | this.position = position 4 | this.imgData = context.createImageData(CHUNK_SIZE, CHUNK_SIZE) 5 | this.data = this.imgData.data 6 | } 7 | 8 | add(pixels){ 9 | var l = this.data.length 10 | for(var i = 0; i < l; i += 4){ 11 | var pixel = pixels[i / 4] 12 | var rgb = null 13 | if(pixel.type === PixelType.Water){ 14 | rgb = [3, 169, 244] 15 | } else if (pixel.type == PixelType.Grass) { 16 | rgb = [64, 154, 67] 17 | } else if (pixel.type == PixelType.Sand) { 18 | rgb = [255, 193, 7] 19 | } else if (pixel.type == PixelType.Dirt){ 20 | rgb = [74, 48, 39] 21 | } else { 22 | rgb = [249, 249, 249] 23 | } 24 | var bias = this.bias(pixel.heightMap) 25 | this.data[i] = rgb[0] * bias 26 | this.data[i + 1] = rgb[1] * bias 27 | this.data[i + 2] = rgb[2] * bias 28 | this.data[i + 3] = 255 29 | } 30 | } 31 | 32 | bias(heightMap){ 33 | var dark = 0.75 34 | var light = 1 35 | return dark * heightMap + light * (1 - heightMap) 36 | } 37 | } -------------------------------------------------------------------------------- /src/lib/World.js: -------------------------------------------------------------------------------- 1 | class World { 2 | constructor(noiseGenerator) { 3 | this.chunkManager = new ChunkManager(noiseGenerator) 4 | this.renderDistance = 10 5 | this.loadDistance = 2 6 | } 7 | 8 | draw(renderer) { 9 | const chunks = this.chunkManager.chunks 10 | for (var iChunk in chunks) { 11 | const chunk = chunks[iChunk] 12 | const chunkElement = chunk.element 13 | chunkElement.draw(renderer) 14 | } 15 | } 16 | 17 | loadChunks(camera) { 18 | var isBuffered = false 19 | var cameraX = parseInt(camera.position.x / CHUNK_SIZE) 20 | var cameraZ = parseInt(camera.position.z / CHUNK_SIZE) 21 | for (var i = 0; i < this.loadDistance; i++) { 22 | const minX = Math.max(cameraX - i, 0) 23 | const minZ = Math.max(cameraZ - i, 0) 24 | const maxX = cameraX + i 25 | const maxZ = cameraZ + i 26 | for (var x = minX; x < maxX; x++) { 27 | for (var z = minZ; z < maxZ; z++) { 28 | this.chunkManager.load(x * CHUNK_SIZE, z * CHUNK_SIZE) 29 | isBuffered = this.chunkManager.addToBuffer(x * CHUNK_SIZE, z * CHUNK_SIZE) 30 | } 31 | } 32 | if(isBuffered){ 33 | break 34 | } 35 | } 36 | if(!isBuffered){ 37 | this.loadDistance++ 38 | } 39 | if(this.loadDistance > this.renderDistance){ 40 | this.loadDistance = 2 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/lib/Application.js: -------------------------------------------------------------------------------- 1 | class Application { 2 | constructor(title, renderer, noiseGenerator, camera) { 3 | this.title = title 4 | this.renderer = renderer 5 | this.camera = camera 6 | this.world = new World(noiseGenerator) 7 | this.startTime = Date.now() 8 | this.nbFrame = 0 9 | this.runLoop = this.runLoop.bind(this) 10 | } 11 | 12 | loadEvents(){ 13 | document.addEventListener('keydown', (event) => { 14 | const key = event.keyCode 15 | //up 16 | if(key === 38){ 17 | this.camera.move(0, -1) 18 | } 19 | //down 20 | else if(key === 40){ 21 | this.camera.move(0, 1) 22 | } 23 | //right 24 | else if(key === 39){ 25 | this.camera.move(1, 0) 26 | } 27 | //left 28 | else if(key === 37){ 29 | this.camera.move(-1, 0) 30 | } 31 | }) 32 | } 33 | 34 | runLoop() { 35 | this.updateFPS() 36 | this.world.loadChunks(this.camera) 37 | this.world.draw(this.renderer) 38 | this.renderer.render(this.camera) 39 | requestAnimationFrame(this.runLoop) 40 | } 41 | 42 | updateFPS() { 43 | const deltaTime = (Date.now() - this.startTime) / 1000 44 | if (deltaTime > 1) { 45 | document.title = `${title} - (${parseInt(this.nbFrame / deltaTime)} FPS)` 46 | this.nbFrame = 0 47 | this.startTime = Date.now() 48 | } else { 49 | this.nbFrame++ 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/lib/NoiseGenerator.js: -------------------------------------------------------------------------------- 1 | class NoiseGenerator{ 2 | constructor(seed){ 3 | this.seed = seed 4 | this.configs = { 5 | octaves: 9, 6 | amplitude: 80, 7 | persistance: 0.51, 8 | smoothness: 250 9 | } 10 | } 11 | 12 | setConfigs(configs){ 13 | this.configs = configs 14 | } 15 | 16 | noise(x, z){ 17 | const integerX = parseInt(x) 18 | const integerZ = parseInt(z) 19 | 20 | const fractionalX = x - integerX 21 | const fractionalZ = z - integerZ 22 | 23 | const a = this.getNoise(integerX, integerZ) 24 | const b = this.getNoise(integerX + 1, integerZ) 25 | 26 | const c = this.getNoise(integerX, integerZ + 1) 27 | const d = this.getNoise(integerX + 1, integerZ + 1) 28 | 29 | const f = this.cosineInterpolate(a, b, fractionalX) 30 | const g = this.cosineInterpolate(c, d, fractionalZ) 31 | 32 | const result = this.cosineInterpolate(f, g, fractionalZ) 33 | 34 | return result 35 | } 36 | 37 | getNoiseValue(t){ 38 | t += this.seed 39 | t = BigInt((t << 13) ^ t) 40 | t = (t * (t * t * 15731n + 789221n) + 1376312589n) 41 | t = parseInt(t.toString(2).slice(-31), 2) 42 | return 1.0 - t / 1073741824 43 | } 44 | 45 | getNoise(x, z){ 46 | return this.getNoiseValue(x + z * CHUNK_SIZE) 47 | } 48 | 49 | cosineInterpolate(a, b, t){ 50 | const c = (1 - Math.cos(t * 3.1415927)) * .5 51 | return (1. - c) * a + c * b 52 | } 53 | 54 | perlinNoise(x, z){ 55 | var r = 0 56 | for (var i = 0; i <= this.configs.octaves; i++) { 57 | var frequency = Math.pow(2, i) 58 | var amplitude = Math.pow(this.configs.persistance, i) 59 | var noise = this.noise(x * frequency / this.configs.smoothness, z * frequency / this.configs.smoothness) 60 | r += noise * amplitude 61 | } 62 | var result = (r / 2 + 1) * this.configs.amplitude - 20 63 | return result > 0 ? result : 1 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/lib/TerrainGenerator.js: -------------------------------------------------------------------------------- 1 | class TerrainGenerator { 2 | constructor(noiseGenerator) { 3 | this.noiseGenerator = noiseGenerator 4 | this.chunk = null 5 | this.configs = { 6 | octaves: 9, 7 | amplitude: 100, 8 | persistance: 0.7, 9 | smoothness: 250 10 | } 11 | this.noiseGenerator.setConfigs(this.configs) 12 | } 13 | 14 | generate(chunk) { 15 | this.chunk = chunk 16 | const heightMap = this.getHeightMap() 17 | const level = { 18 | [PixelType.Water]: 0.1, 19 | [PixelType.Sand]: 0.11, 20 | [PixelType.Grass]: 0.3, 21 | [PixelType.Dirt]: 0.7 22 | } 23 | for (var x = 0; x < CHUNK_SIZE; x++) { 24 | for (var z = 0; z < CHUNK_SIZE; z++) { 25 | var h = heightMap[x][z] 26 | if (h < level[PixelType.Water]) { 27 | chunk.setPixel(x, z, PixelType.Water, h / level[PixelType.Water]) 28 | } else if (h < level[PixelType.Sand]) { 29 | chunk.setPixel(x, z, PixelType.Sand, h / level[PixelType.Sand]) 30 | } else if (h < level[PixelType.Grass]) { 31 | chunk.setPixel(x, z, PixelType.Grass, h / level[PixelType.Grass]) 32 | } else if (h < level[PixelType.Dirt]) { 33 | chunk.setPixel(x, z, PixelType.Dirt, h / level[PixelType.Dirt]) 34 | } else { 35 | chunk.setPixel(x, z, PixelType.Snow, h) 36 | } 37 | } 38 | } 39 | } 40 | 41 | getHeightAt(x, z) { 42 | const h = this.noiseGenerator.perlinNoise(this.chunk.position.x + x, this.chunk.position.z + z) 43 | return h / SNOW_LEVEL 44 | } 45 | 46 | getHeightIn(heights, xMin, zMin, xMax, zMax) { 47 | const bottomLeft = this.getHeightAt(xMin, zMin) 48 | const bottomRight = this.getHeightAt(xMax, zMin) 49 | const topLeft = this.getHeightAt(xMin, zMax) 50 | const topRight = this.getHeightAt(xMax, zMax) 51 | for (var x = xMin; x < xMax; x++) { 52 | for (var z = zMin; z < zMax; z++) { 53 | if (x === CHUNK_SIZE) continue 54 | if (z === CHUNK_SIZE) continue 55 | 56 | var h = Maths.smoothInterpolation(bottomLeft, topLeft, bottomRight, topRight, xMin, xMax, zMin, zMax, x, z) 57 | if (!heights[x]) { 58 | heights[x] = [] 59 | } 60 | heights[x][z] = h 61 | } 62 | } 63 | } 64 | 65 | getHeightMap() { 66 | const part = 2 67 | const PART_SIZE = CHUNK_SIZE / part 68 | var heights = [] 69 | for (var zPart = 0; zPart < part; zPart++) { 70 | for (var xPart = 0; xPart < part; xPart++) { 71 | this.getHeightIn( 72 | heights, 73 | xPart * PART_SIZE, 74 | zPart * PART_SIZE, 75 | (xPart + 1) * PART_SIZE, 76 | (zPart + 1) * PART_SIZE 77 | ) 78 | } 79 | } 80 | 81 | return heights 82 | } 83 | } --------------------------------------------------------------------------------