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