├── .gitignore ├── index.html ├── package-lock.json ├── package.json ├── public └── social │ ├── share-1200x600.png │ ├── share-1200x630.png │ └── share-1200x630.psd ├── readme.md ├── sources ├── Game │ ├── Debug │ │ ├── Debug.js │ │ ├── Stats.js │ │ └── UI.js │ ├── Game.js │ ├── State │ │ ├── Camera.js │ │ ├── CameraFly.js │ │ ├── CameraThirdPerson.js │ │ ├── Chunk.js │ │ ├── Chunks.js │ │ ├── Controls.js │ │ ├── DayCycle.js │ │ ├── Player.js │ │ ├── State.js │ │ ├── Sun.js │ │ ├── Terrain.js │ │ ├── Terrains.js │ │ ├── Time.js │ │ └── Viewport.js │ ├── View │ │ ├── Camera.js │ │ ├── Chunk.js │ │ ├── ChunkHelper.js │ │ ├── Chunks.js │ │ ├── Grass.js │ │ ├── Materials │ │ │ ├── GrassMaterial.js │ │ │ ├── NoisesMaterial.js │ │ │ ├── PlayerMaterial.js │ │ │ ├── SkyBackgroundMaterial.js │ │ │ ├── SkySphereMaterial.js │ │ │ ├── StarsMaterial.js │ │ │ ├── TerrainMaterial.js │ │ │ └── shaders │ │ │ │ ├── grass │ │ │ │ ├── fragment.glsl │ │ │ │ └── vertex.glsl │ │ │ │ ├── noises │ │ │ │ ├── fragment.glsl │ │ │ │ └── vertex.glsl │ │ │ │ ├── partials │ │ │ │ ├── getFogColor.glsl │ │ │ │ ├── getGrassAttenuation.glsl │ │ │ │ ├── getRotatePivot2d.glsl │ │ │ │ ├── getSunReflection.glsl │ │ │ │ ├── getSunReflectionColor.glsl │ │ │ │ ├── getSunShade.glsl │ │ │ │ ├── getSunShadeColor.glsl │ │ │ │ ├── inverseLerp.glsl │ │ │ │ ├── perlin2d.glsl │ │ │ │ ├── perlin3dPeriodic.glsl │ │ │ │ ├── perlin4d.glsl │ │ │ │ └── remap.glsl │ │ │ │ ├── player │ │ │ │ ├── fragment.glsl │ │ │ │ └── vertex.glsl │ │ │ │ ├── skyBackground │ │ │ │ ├── fragment.glsl │ │ │ │ └── vertex.glsl │ │ │ │ ├── skySphere │ │ │ │ ├── fragment.glsl │ │ │ │ └── vertex.glsl │ │ │ │ ├── stars │ │ │ │ ├── fragment.glsl │ │ │ │ └── vertex.glsl │ │ │ │ └── terrain │ │ │ │ ├── fragment.glsl │ │ │ │ └── vertex.glsl │ │ ├── Noises.js │ │ ├── Player.js │ │ ├── Renderer.js │ │ ├── Sky.js │ │ ├── Terrain.js │ │ ├── TerrainGradient.js │ │ ├── Terrains.js │ │ ├── View.js │ │ └── Water.js │ └── Workers │ │ ├── SimplexNoise.js │ │ └── Terrain.js ├── index.js └── style.css └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | .cache 5 | 6 | # Editor directories and files 7 | .idea 8 | .vscode 9 | *.suo 10 | *.ntvs* 11 | *.njsproj 12 | *.sln 13 | *.sw? 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Infinite World 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |
28 |
29 | This experience isn't suited for mobile devices 30 |
31 |
32 |
33 |
34 |
35 |
A
36 |
S
37 |
D
38 |
W
39 |
40 | move 41 |
42 |
43 |
44 |
F
45 |
46 | fullscreen 47 |
48 |
49 |
50 |
P
51 |
52 | pointer lock 53 |
54 |
55 |
56 |
V
57 |
58 | view mode 59 |
60 |
61 |
62 |
B
63 |
64 | debug 65 |
66 |
67 |
68 | Infinite procedurally generated world — GitHub 69 |
70 |
71 |
72 |
73 | 74 | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infinite-world", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vite build" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@jniac/three-point-text-helper": "^1.0.8", 15 | "events": "^3.3.0", 16 | "gl-matrix": "^3.4.3", 17 | "lil-gui": "^0.17.0", 18 | "seedrandom": "^3.0.5", 19 | "simplex-noise": "^3.0.1", 20 | "stats.js": "^0.17.0", 21 | "three": "^0.149.0", 22 | "vite": "^4.1.0", 23 | "vite-plugin-glsl": "^1.1.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/social/share-1200x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunosimon/infinite-world/b7a952ff935df0bf1d6ba56b2b9279e0c1420059/public/social/share-1200x600.png -------------------------------------------------------------------------------- /public/social/share-1200x630.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunosimon/infinite-world/b7a952ff935df0bf1d6ba56b2b9279e0c1420059/public/social/share-1200x630.png -------------------------------------------------------------------------------- /public/social/share-1200x630.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brunosimon/infinite-world/b7a952ff935df0bf1d6ba56b2b9279e0c1420059/public/social/share-1200x630.psd -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Infinite World 2 | 3 | Just an infinite procedurally generated world in WebGL with Three.js. 4 | 5 | No purpose, just having fun. 6 | 7 | ![Infinite World Screenshot](public/social/share-1200x630.png?raw=true "Infinite World Screenshot") 8 | 9 | ## Instructions 10 | 11 | ``` 12 | npm install 13 | npm run dev 14 | ``` -------------------------------------------------------------------------------- /sources/Game/Debug/Debug.js: -------------------------------------------------------------------------------- 1 | import Stats from './Stats.js' 2 | import UI from './UI.js' 3 | 4 | export default class Debug 5 | { 6 | static instance 7 | 8 | static getInstance() 9 | { 10 | return Debug.instance 11 | } 12 | 13 | constructor() 14 | { 15 | if(Debug.instance) 16 | return Debug.instance 17 | 18 | Debug.instance = this 19 | 20 | this.active = false 21 | 22 | if(location.hash === '#debug') 23 | { 24 | this.activate() 25 | } 26 | } 27 | 28 | activate() 29 | { 30 | if(this.active) 31 | return 32 | 33 | this.active = true 34 | this.ui = new UI() 35 | this.stats = new Stats() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sources/Game/Debug/Stats.js: -------------------------------------------------------------------------------- 1 | import StatsJs from 'stats.js' 2 | 3 | export default class Stats 4 | { 5 | constructor() 6 | { 7 | this.instance = new StatsJs() 8 | this.instance.showPanel(3) 9 | 10 | this.active = false 11 | this.max = 40 12 | this.ignoreMaxed = true 13 | 14 | this.activate() 15 | } 16 | 17 | activate() 18 | { 19 | this.active = true 20 | 21 | document.body.appendChild(this.instance.dom) 22 | } 23 | 24 | deactivate() 25 | { 26 | this.active = false 27 | 28 | document.body.removeChild(this.instance.dom) 29 | } 30 | 31 | setRenderPanel(_context) 32 | { 33 | this.render = {} 34 | this.render.context = _context 35 | this.render.extension = this.render.context.getExtension('EXT_disjoint_timer_query_webgl2') 36 | this.render.panel = this.instance.addPanel(new StatsJs.Panel('Render (ms)', '#f8f', '#212')) 37 | 38 | const webGL2 = typeof WebGL2RenderingContext !== 'undefined' && _context instanceof WebGL2RenderingContext 39 | 40 | if(!webGL2 || !this.render.extension) 41 | { 42 | this.deactivate() 43 | } 44 | } 45 | 46 | beforeRender() 47 | { 48 | if(!this.active) 49 | { 50 | return 51 | } 52 | 53 | // Setup 54 | this.queryCreated = false 55 | let queryResultAvailable = false 56 | 57 | // Test if query result available 58 | if(this.render.query) 59 | { 60 | queryResultAvailable = this.render.context.getQueryParameter(this.render.query, this.render.context.QUERY_RESULT_AVAILABLE) 61 | const disjoint = this.render.context.getParameter(this.render.extension.GPU_DISJOINT_EXT) 62 | 63 | if(queryResultAvailable && !disjoint) 64 | { 65 | const elapsedNanos = this.render.context.getQueryParameter(this.render.query, this.render.context.QUERY_RESULT) 66 | const panelValue = Math.min(elapsedNanos / 1000 / 1000, this.max) 67 | 68 | if(panelValue === this.max && this.ignoreMaxed) 69 | { 70 | 71 | } 72 | else 73 | { 74 | this.render.panel.update(panelValue, this.max) 75 | } 76 | } 77 | } 78 | 79 | // If query result available or no query yet 80 | if(queryResultAvailable || !this.render.query) 81 | { 82 | // Create new query 83 | this.queryCreated = true 84 | this.render.query = this.render.context.createQuery() 85 | this.render.context.beginQuery(this.render.extension.TIME_ELAPSED_EXT, this.render.query) 86 | } 87 | 88 | } 89 | 90 | afterRender() 91 | { 92 | if(!this.active) 93 | { 94 | return 95 | } 96 | 97 | // End the query (result will be available "later") 98 | if(this.queryCreated) 99 | { 100 | this.render.context.endQuery(this.render.extension.TIME_ELAPSED_EXT) 101 | } 102 | } 103 | 104 | update() 105 | { 106 | if(!this.active) 107 | { 108 | return 109 | } 110 | 111 | this.instance.update() 112 | } 113 | 114 | destroy() 115 | { 116 | this.deactivate() 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /sources/Game/Debug/UI.js: -------------------------------------------------------------------------------- 1 | import * as dat from 'lil-gui' 2 | 3 | export default class UI 4 | { 5 | constructor() 6 | { 7 | this.instance = new dat.GUI({ width: 320, title: 'debug' }) 8 | 9 | const sheet = window.document.styleSheets[0] 10 | sheet.insertRule(` 11 | .lil-gui .lil-gui > .children 12 | { 13 | border: none; 14 | margin-left: var(--folder-indent); 15 | border-left: 2px solid var(--widget-color); 16 | } 17 | `, sheet.cssRules.length) 18 | sheet.insertRule(` 19 | .lil-gui.root > .children > .lil-gui > .title 20 | { 21 | border-width: 1px 0 0 0; 22 | } 23 | `, sheet.cssRules.length) 24 | 25 | this.tree = {} 26 | this.tree.folder = this.instance 27 | this.tree.children = {} 28 | } 29 | 30 | getFolder(path) 31 | { 32 | const parts = path.split('/') 33 | 34 | let branch = this.tree 35 | 36 | for(const part of parts) 37 | { 38 | let newBranch = branch.children[part] 39 | 40 | if(!newBranch) 41 | { 42 | newBranch = {} 43 | newBranch.folder = branch.folder.addFolder(part) 44 | newBranch.folder.close() 45 | newBranch.children = {} 46 | } 47 | 48 | branch.children[part] = newBranch 49 | branch = newBranch 50 | } 51 | 52 | return branch.folder 53 | } 54 | } -------------------------------------------------------------------------------- /sources/Game/Game.js: -------------------------------------------------------------------------------- 1 | import Debug from '@/Debug/Debug.js' 2 | import State from '@/State/State.js' 3 | import View from '@/View/View.js' 4 | 5 | export default class Game 6 | { 7 | static instance 8 | 9 | static getInstance() 10 | { 11 | return Game.instance 12 | } 13 | 14 | constructor() 15 | { 16 | if(Game.instance) 17 | return Game.instance 18 | 19 | Game.instance = this 20 | 21 | this.seed = 'p' 22 | this.debug = new Debug() 23 | this.state = new State() 24 | this.view = new View() 25 | 26 | window.addEventListener('resize', () => 27 | { 28 | this.resize() 29 | }) 30 | 31 | this.update() 32 | } 33 | 34 | update() 35 | { 36 | this.state.update() 37 | this.view.update() 38 | 39 | window.requestAnimationFrame(() => 40 | { 41 | this.update() 42 | }) 43 | } 44 | 45 | resize() 46 | { 47 | this.state.resize() 48 | this.view.resize() 49 | } 50 | 51 | destroy() 52 | { 53 | 54 | } 55 | } -------------------------------------------------------------------------------- /sources/Game/State/Camera.js: -------------------------------------------------------------------------------- 1 | import { vec3, quat2 } from 'gl-matrix' 2 | 3 | import Game from '@/Game.js' 4 | import State from '@/State/State.js' 5 | import CameraThirdPerson from './CameraThirdPerson.js' 6 | import CameraFly from './CameraFly.js' 7 | 8 | export default class Camera 9 | { 10 | static MODE_THIRDPERSON = 1 11 | static MODE_FLY = 2 12 | 13 | constructor(player) 14 | { 15 | this.game = Game.getInstance() 16 | this.state = State.getInstance() 17 | 18 | this.controls = this.state.controls 19 | 20 | this.player = player 21 | 22 | this.position = vec3.create() 23 | this.quaternion = quat2.create() 24 | this.mode = Camera.MODE_THIRDPERSON 25 | 26 | this.thirdPerson = new CameraThirdPerson(this.player) 27 | this.fly = new CameraFly(this.player) 28 | 29 | // Activate 30 | if(this.mode === Camera.MODE_THIRDPERSON) 31 | this.thirdPerson.activate() 32 | 33 | else if(this.mode === Camera.MODE_FLY) 34 | this.fly.activate() 35 | 36 | this.controls.events.on('cameraModeDown', () => 37 | { 38 | if(this.mode === Camera.MODE_THIRDPERSON) 39 | { 40 | this.mode = Camera.MODE_FLY 41 | this.fly.activate(this.position, this.quaternion) 42 | this.thirdPerson.deactivate() 43 | } 44 | 45 | else if(this.mode === Camera.MODE_FLY) 46 | { 47 | this.mode = Camera.MODE_THIRDPERSON 48 | this.fly.deactivate() 49 | this.thirdPerson.activate() 50 | } 51 | }) 52 | 53 | this.setDebug() 54 | } 55 | 56 | update() 57 | { 58 | this.thirdPerson.update() 59 | this.fly.update() 60 | 61 | if(this.mode === Camera.MODE_THIRDPERSON) 62 | { 63 | vec3.copy(this.position, this.thirdPerson.position) 64 | quat2.copy(this.quaternion, this.thirdPerson.quaternion) 65 | } 66 | 67 | else if(this.mode === Camera.MODE_FLY) 68 | { 69 | vec3.copy(this.position, this.fly.position) 70 | quat2.copy(this.quaternion, this.fly.quaternion) 71 | } 72 | } 73 | 74 | setDebug() 75 | { 76 | const debug = this.game.debug 77 | 78 | if(!debug.active) 79 | return 80 | 81 | const folder = debug.ui.getFolder('state/player/view') 82 | 83 | folder 84 | .add( 85 | this, 86 | 'mode', 87 | { 88 | 'MODE_THIRDPERSON': Camera.MODE_THIRDPERSON, 89 | 'MODE_FLY': Camera.MODE_FLY 90 | } 91 | ) 92 | .onChange(() => 93 | { 94 | if(this.mode === Camera.MODE_THIRDPERSON) 95 | { 96 | this.fly.deactivate() 97 | this.thirdPerson.activate() 98 | } 99 | 100 | else if(this.mode === Camera.MODE_FLY) 101 | { 102 | this.fly.activate(this.position, this.quaternion) 103 | this.thirdPerson.deactivate() 104 | } 105 | }) 106 | } 107 | } -------------------------------------------------------------------------------- /sources/Game/State/CameraFly.js: -------------------------------------------------------------------------------- 1 | import { vec3, quat2, mat4 } from 'gl-matrix' 2 | 3 | import Game from '@/Game.js' 4 | import State from '@/State/State.js' 5 | 6 | export default class CameraFly 7 | { 8 | constructor(player) 9 | { 10 | this.game = Game.getInstance() 11 | this.state = State.getInstance() 12 | this.viewport = this.state.viewport 13 | this.time = this.state.time 14 | this.controls = this.state.controls 15 | 16 | this.player = player 17 | 18 | this.active = false 19 | 20 | this.gameUp = vec3.fromValues(0, 1, 0) 21 | 22 | this.defaultForward = vec3.fromValues(0, 0, 1) 23 | 24 | this.forward = vec3.clone(this.defaultForward) 25 | this.rightward = vec3.create() 26 | this.upward = vec3.create() 27 | this.backward = vec3.create() 28 | this.leftward = vec3.create() 29 | this.downward = vec3.create() 30 | 31 | vec3.cross(this.rightward, this.gameUp, this.forward) 32 | vec3.cross(this.upward, this.forward, this.rightward) 33 | vec3.negate(this.backward, this.forward) 34 | vec3.negate(this.leftward, this.rightward) 35 | vec3.negate(this.downward, this.upward) 36 | 37 | this.position = vec3.fromValues(40, 10, 40) 38 | this.quaternion = quat2.create() 39 | this.rotateX = - Math.PI * 0.15 40 | this.rotateY = Math.PI * 0.25 41 | this.rotateXLimits = { min: - Math.PI * 0.5, max: Math.PI * 0.5 } 42 | } 43 | 44 | activate(position = null, quaternion = null) 45 | { 46 | this.active = true 47 | 48 | if(position !== null && quaternion !== null) 49 | { 50 | // Position 51 | vec3.copy(this.position, position) 52 | 53 | // Rotations 54 | const rotatedForward = vec3.clone(this.defaultForward) 55 | vec3.transformQuat(rotatedForward, rotatedForward, quaternion) 56 | 57 | // Rotation Y 58 | const rotatedYForward = vec3.clone(rotatedForward) 59 | rotatedYForward[1] = 0 60 | this.rotateY = vec3.angle(this.defaultForward, rotatedYForward) 61 | 62 | if(vec3.dot(rotatedForward, vec3.fromValues(1, 0, 0)) < 0) 63 | this.rotateY *= - 1 64 | 65 | // Rotation X 66 | this.rotateX = vec3.angle(rotatedForward, rotatedYForward) 67 | 68 | if(vec3.dot(rotatedForward, vec3.fromValues(0, 1, 0)) > 0) 69 | this.rotateX *= - 1 70 | } 71 | } 72 | 73 | deactivate() 74 | { 75 | this.active = false 76 | } 77 | 78 | update() 79 | { 80 | if(!this.active) 81 | return 82 | 83 | // Rotation X and Y 84 | if(this.controls.pointer.down || this.viewport.pointerLock.active) 85 | { 86 | const normalisedPointer = this.viewport.normalise(this.controls.pointer.delta) 87 | this.rotateX -= normalisedPointer.y * 2 88 | this.rotateY -= normalisedPointer.x * 2 89 | 90 | if(this.rotateX < this.rotateXLimits.min) 91 | this.rotateX = this.rotateXLimits.min 92 | if(this.rotateX > this.rotateXLimits.max) 93 | this.rotateX = this.rotateXLimits.max 94 | } 95 | 96 | // console.log('this.rotateY', this.rotateY) 97 | 98 | // Rotation Matrix 99 | const rotationMatrix = mat4.create() 100 | mat4.rotateY(rotationMatrix, rotationMatrix, this.rotateY) 101 | mat4.rotateX(rotationMatrix, rotationMatrix, this.rotateX) 102 | quat2.fromMat4(this.quaternion, rotationMatrix) 103 | 104 | // Update directions 105 | vec3.copy(this.forward, this.defaultForward) 106 | vec3.transformMat4(this.forward, this.forward, rotationMatrix) 107 | vec3.cross(this.rightward, this.gameUp, this.forward) 108 | vec3.cross(this.upward, this.forward, this.rightward) 109 | vec3.negate(this.backward, this.forward) 110 | vec3.negate(this.leftward, this.rightward) 111 | vec3.negate(this.downward, this.upward) 112 | 113 | // Position 114 | const direction = vec3.create() 115 | if(this.controls.keys.down.forward) 116 | vec3.add(direction, direction, this.backward) 117 | 118 | if(this.controls.keys.down.backward) 119 | vec3.add(direction, direction, this.forward) 120 | 121 | if(this.controls.keys.down.strafeRight) 122 | vec3.add(direction, direction, this.rightward) 123 | 124 | if(this.controls.keys.down.strafeLeft) 125 | vec3.add(direction, direction, this.leftward) 126 | 127 | if(this.controls.keys.down.jump) 128 | vec3.add(direction, direction, this.upward) 129 | 130 | if(this.controls.keys.down.crouch) 131 | vec3.add(direction, direction, this.downward) 132 | 133 | const speed = (this.controls.keys.down.boost ? 30 : 10) * this.time.delta 134 | 135 | vec3.normalize(direction, direction) 136 | vec3.scale(direction, direction, speed) 137 | vec3.add(this.position, this.position, direction) 138 | } 139 | } -------------------------------------------------------------------------------- /sources/Game/State/CameraThirdPerson.js: -------------------------------------------------------------------------------- 1 | import { vec3, quat2, mat4 } from 'gl-matrix' 2 | 3 | import State from '@/State/State.js' 4 | 5 | export default class CameraThirdPerson 6 | { 7 | constructor(player) 8 | { 9 | this.state = State.getInstance() 10 | this.viewport = this.state.viewport 11 | this.controls = this.state.controls 12 | 13 | this.player = player 14 | 15 | this.active = false 16 | this.gameUp = vec3.fromValues(0, 1, 0) 17 | this.position = vec3.create() 18 | this.quaternion = quat2.create() 19 | this.distance = 15 20 | this.phi = Math.PI * 0.45 21 | this.theta = - Math.PI * 0.25 22 | this.aboveOffset = 2 23 | this.phiLimits = { min: 0.1, max: Math.PI - 0.1 } 24 | } 25 | 26 | activate() 27 | { 28 | this.active = true 29 | } 30 | 31 | deactivate() 32 | { 33 | this.active = false 34 | } 35 | 36 | update() 37 | { 38 | if(!this.active) 39 | return 40 | 41 | // Phi and theta 42 | if(this.controls.pointer.down || this.viewport.pointerLock.active) 43 | { 44 | const normalisedPointer = this.viewport.normalise(this.controls.pointer.delta) 45 | this.phi -= normalisedPointer.y * 2 46 | this.theta -= normalisedPointer.x * 2 47 | 48 | if(this.phi < this.phiLimits.min) 49 | this.phi = this.phiLimits.min 50 | if(this.phi > this.phiLimits.max) 51 | this.phi = this.phiLimits.max 52 | } 53 | 54 | // Position 55 | const sinPhiRadius = Math.sin(this.phi) * this.distance 56 | const sphericalPosition = vec3.fromValues( 57 | sinPhiRadius * Math.sin(this.theta), 58 | Math.cos(this.phi) * this.distance, 59 | sinPhiRadius * Math.cos(this.theta) 60 | ) 61 | vec3.add(this.position, this.player.position.current, sphericalPosition) 62 | 63 | // Target 64 | const target = vec3.fromValues( 65 | this.player.position.current[0], 66 | this.player.position.current[1] + this.aboveOffset, 67 | this.player.position.current[2] 68 | ) 69 | 70 | // Quaternion 71 | const toTargetMatrix = mat4.create() 72 | mat4.targetTo(toTargetMatrix, this.position, target, this.gameUp) 73 | quat2.fromMat4(this.quaternion, toTargetMatrix) 74 | 75 | // Clamp to ground 76 | const chunks = this.state.chunks 77 | const elevation = chunks.getElevationForPosition(this.position[0], this.position[2]) 78 | 79 | if(elevation && this.position[1] < elevation + 1) 80 | this.position[1] = elevation + 1 81 | } 82 | } -------------------------------------------------------------------------------- /sources/Game/State/Chunk.js: -------------------------------------------------------------------------------- 1 | import EventsEmitter from 'events' 2 | 3 | import State from '@/State/State.js' 4 | 5 | // Cardinal directions 6 | // N 7 | // 8 | // x 9 | // W + → E 10 | // z↓ 11 | // 12 | // S 13 | 14 | // Children chunks keys: 15 | // +-----+-----+ 16 | // | nw | ne | 17 | // +-----+-----+ 18 | // | sw | se | 19 | // +-----+-----+ 20 | 21 | // Neighbour chunks keys: 22 | // +-----+ 23 | // | n | 24 | // +-----+-----+-----+ 25 | // | w | | e | 26 | // +-----+-----+-----+ 27 | // | s | 28 | // +-----+ 29 | 30 | export default class Chunk 31 | { 32 | constructor(id, chunks, parent, quadPosition, size, x, z, depth) 33 | { 34 | this.state = State.getInstance() 35 | 36 | this.id = id 37 | this.chunks = chunks 38 | this.parent = parent 39 | this.quadPosition = quadPosition 40 | this.size = size 41 | this.x = x 42 | this.z = z 43 | this.depth = depth 44 | 45 | this.precision = this.depth / this.chunks.maxDepth 46 | this.maxSplit = this.depth === this.chunks.maxDepth 47 | this.splitted = false 48 | this.splitting = false 49 | this.unsplitting = false 50 | this.needsCheck = true 51 | this.terrainNeedsUpdate = true 52 | this.neighbours = new Map() 53 | this.children = new Map() 54 | this.ready = false 55 | this.final = false 56 | this.halfSize = size * 0.5 57 | this.quarterSize = this.halfSize * 0.5 58 | this.bounding = { 59 | xMin: this.x - this.halfSize, 60 | xMax: this.x + this.halfSize, 61 | zMin: this.z - this.halfSize, 62 | zMax: this.z + this.halfSize 63 | } 64 | 65 | this.events = new EventsEmitter() 66 | 67 | this.check() 68 | 69 | if(!this.splitted) 70 | { 71 | this.createFinal() 72 | } 73 | 74 | this.testReady() 75 | } 76 | 77 | check() 78 | { 79 | if(!this.needsCheck) 80 | return 81 | 82 | this.needsCheck = false 83 | 84 | const underSplitDistance = this.chunks.underSplitDistance(this.size, this.x, this.z) 85 | 86 | if(underSplitDistance) 87 | { 88 | if(!this.maxSplit && !this.splitted) 89 | this.split() 90 | } 91 | 92 | else 93 | { 94 | if(this.splitted) 95 | this.unsplit() 96 | } 97 | 98 | for(const [key, chunk] of this.children) 99 | chunk.check() 100 | } 101 | 102 | update() 103 | { 104 | if(this.final && this.terrainNeedsUpdate && this.neighbours.size === 4) 105 | { 106 | this.createTerrain() 107 | this.terrainNeedsUpdate = false 108 | } 109 | 110 | for(const [key, chunk] of this.children) 111 | chunk.update() 112 | } 113 | 114 | setNeighbours(nChunk, eChunk, sChunk, wChunk) 115 | { 116 | this.neighbours.set('n', nChunk) 117 | this.neighbours.set('e', eChunk) 118 | this.neighbours.set('s', sChunk) 119 | this.neighbours.set('w', wChunk) 120 | } 121 | 122 | testReady() 123 | { 124 | if(this.splitted) 125 | { 126 | let chunkReadyCount = 0 127 | 128 | for(const [key, chunk] of this.children) 129 | { 130 | if(chunk.ready) 131 | chunkReadyCount++ 132 | } 133 | 134 | if(chunkReadyCount === 4) 135 | { 136 | this.setReady() 137 | } 138 | } 139 | else 140 | { 141 | if(this.terrain && this.terrain.ready) 142 | { 143 | this.setReady() 144 | } 145 | } 146 | } 147 | 148 | setReady() 149 | { 150 | if(this.ready) 151 | return 152 | 153 | this.ready = true 154 | 155 | // Is splitting 156 | if(this.splitting) 157 | { 158 | this.splitting = false 159 | 160 | this.destroyFinal() 161 | } 162 | 163 | // Is unsplitting 164 | if(this.unsplitting) 165 | { 166 | this.unsplitting = false 167 | 168 | // Destroy chunks 169 | for(const [key, chunk] of this.children) 170 | chunk.destroy() 171 | 172 | this.children.clear() 173 | } 174 | 175 | this.events.emit('ready') 176 | } 177 | 178 | unsetReady() 179 | { 180 | if(!this.ready) 181 | return 182 | 183 | this.ready = false 184 | 185 | this.events.emit('unready') 186 | } 187 | 188 | split() 189 | { 190 | this.splitting = true 191 | this.splitted = true 192 | 193 | this.unsetReady() 194 | 195 | // Create 4 neighbours chunks 196 | const neChunk = this.chunks.create(this, 'ne', this.halfSize, this.x + this.quarterSize, this.z - this.quarterSize, this.depth + 1) 197 | this.children.set('ne', neChunk) 198 | 199 | const nwChunk = this.chunks.create(this, 'nw', this.halfSize, this.x - this.quarterSize, this.z - this.quarterSize, this.depth + 1) 200 | this.children.set('nw', nwChunk) 201 | 202 | const swChunk = this.chunks.create(this, 'sw', this.halfSize, this.x - this.quarterSize, this.z + this.quarterSize, this.depth + 1) 203 | this.children.set('sw', swChunk) 204 | 205 | const seChunk = this.chunks.create(this, 'se', this.halfSize, this.x + this.quarterSize, this.z + this.quarterSize, this.depth + 1) 206 | this.children.set('se', seChunk) 207 | 208 | for(const [key, chunk] of this.children) 209 | { 210 | chunk.events.on('ready', () => 211 | { 212 | this.testReady() 213 | }) 214 | } 215 | } 216 | 217 | unsplit() 218 | { 219 | if(!this.splitted) 220 | return 221 | 222 | this.splitted = false 223 | this.unsplitting = true 224 | 225 | this.unsetReady() 226 | 227 | this.createFinal() 228 | } 229 | 230 | createTerrain() 231 | { 232 | this.destroyTerrain() 233 | 234 | this.terrain = this.state.terrains.create( 235 | this.size, 236 | this.x, 237 | this.z, 238 | this.precision 239 | ) 240 | this.terrain.events.on('ready', () => 241 | { 242 | this.testReady() 243 | }) 244 | } 245 | 246 | destroyTerrain() 247 | { 248 | if(!this.terrain) 249 | return 250 | 251 | this.state.terrains.destroyTerrain(this.terrain.id) 252 | } 253 | 254 | createFinal() 255 | { 256 | if(this.final) 257 | return 258 | 259 | this.final = true 260 | this.terrainNeedsUpdate = true 261 | } 262 | 263 | destroyFinal() 264 | { 265 | if(!this.final) 266 | return 267 | 268 | this.final = false 269 | this.terrainNeedsUpdate = false 270 | 271 | this.destroyTerrain() 272 | } 273 | 274 | destroy() 275 | { 276 | for(const [key, chunk] of this.children) 277 | chunk.off('ready') 278 | 279 | if(this.splitted) 280 | { 281 | this.unsplit() 282 | } 283 | else 284 | { 285 | // Was unsplitting 286 | if(this.unsplitting) 287 | { 288 | // Destroy chunks 289 | for(const [key, chunk] of this.children) 290 | chunk.destroy() 291 | 292 | this.children.clear() 293 | } 294 | } 295 | 296 | this.destroyFinal() 297 | // this.chunkHelper.destroy() 298 | 299 | this.events.emit('destroy') 300 | } 301 | 302 | isInside(x, z) 303 | { 304 | return x > this.bounding.xMin && x < this.bounding.xMax && z > this.bounding.zMin && z < this.bounding.zMax 305 | } 306 | 307 | getChildChunkForPosition(x, z) 308 | { 309 | if(!this.splitted) 310 | return this 311 | 312 | for(const [key, chunk] of this.children) 313 | { 314 | if(chunk.isInside(x, z)) 315 | return chunk.getChildChunkForPosition(x, z) 316 | } 317 | 318 | return false 319 | } 320 | } -------------------------------------------------------------------------------- /sources/Game/State/Chunks.js: -------------------------------------------------------------------------------- 1 | import EventsEmitter from 'events' 2 | import { vec2 } from 'gl-matrix' 3 | 4 | import State from '@/State/State.js' 5 | import Chunk from './Chunk.js' 6 | 7 | export default class Chunks 8 | { 9 | constructor() 10 | { 11 | this.state = State.getInstance() 12 | 13 | this.minSize = 64 14 | this.maxDepth = 4 15 | this.maxSize = this.minSize * Math.pow(2, this.maxDepth) 16 | this.splitRatioPerSize = 1.3 17 | this.lastId = 0 18 | 19 | this.events = new EventsEmitter() 20 | this.mainChunks = new Map() 21 | this.allChunks = new Map() 22 | this.playerChunkKey = null 23 | 24 | this.check() 25 | } 26 | 27 | check() 28 | { 29 | // Set all children flag for check 30 | for(const [key, chunk] of this.allChunks) 31 | chunk.needsCheck = true 32 | 33 | // Get the coordinates to main chunks around the player 34 | const mainChunksCoordinates = this.getMainChunksCoordinates() 35 | 36 | // Destroy main chunks not in proximity anymore 37 | for(const [key, chunk] of this.mainChunks) 38 | { 39 | if(!mainChunksCoordinates.find((coordinates) => coordinates.key === key)) 40 | { 41 | chunk.destroy() 42 | this.mainChunks.delete(key) 43 | } 44 | } 45 | 46 | // Create new main chunks 47 | for(const coordinates of mainChunksCoordinates) 48 | { 49 | if(!this.mainChunks.has(coordinates.key)) 50 | { 51 | const chunk = this.create(null, null, this.maxSize, coordinates.x, coordinates.z, 0) 52 | this.mainChunks.set(coordinates.key, chunk) 53 | } 54 | } 55 | 56 | // Check chunks 57 | for(const [ key, chunk ] of this.mainChunks) 58 | chunk.check() 59 | 60 | // Update neighbours 61 | this.updateAllNeighbours() 62 | } 63 | 64 | update() 65 | { 66 | // Check only if player coordinates changed to to another minimal chunk 67 | const player = this.state.player 68 | const playerChunkKey = `${Math.round(player.position.current[0] / this.minSize * 2 + 0.5)}${Math.round(player.position.current[2] / this.minSize * 2 + 0.5)}` 69 | 70 | if(playerChunkKey !== this.playerChunkKey) 71 | { 72 | this.playerChunkKey = playerChunkKey 73 | this.check() 74 | } 75 | 76 | // Update main chunks 77 | for(const [ key, chunk ] of this.mainChunks) 78 | chunk.update() 79 | } 80 | 81 | create(parent, quadPosition, halfSize, x, z, depth) 82 | { 83 | const id = this.lastId++ 84 | const chunk = new Chunk(id, this, parent, quadPosition, halfSize, x, z, depth) 85 | 86 | this.allChunks.set(id, chunk) 87 | 88 | this.events.emit('create', chunk) 89 | 90 | return chunk 91 | } 92 | 93 | updateAllNeighbours() 94 | { 95 | // Update main chunks neighbours 96 | for(const [key, chunk] of this.mainChunks) 97 | { 98 | const coordinates = key.split(',') 99 | const x = parseFloat(coordinates[0]) 100 | const z = parseFloat(coordinates[1]) 101 | 102 | const nChunkKey = `${x},${z - 1}` 103 | const eChunkKey = `${x + 1},${z}` 104 | const sChunkKey = `${x},${z + 1}` 105 | const wChunkKey = `${x - 1},${z}` 106 | 107 | const nChunk = this.mainChunks.get(nChunkKey) ?? false 108 | const eChunk = this.mainChunks.get(eChunkKey) ?? false 109 | const sChunk = this.mainChunks.get(sChunkKey) ?? false 110 | const wChunk = this.mainChunks.get(wChunkKey) ?? false 111 | 112 | chunk.setNeighbours(nChunk, eChunk, sChunk, wChunk) 113 | } 114 | 115 | // All not main chunks in depth order 116 | const chunks = [...this.allChunks.values()].filter(chunk => chunk.depth > 0).sort((a, b) => a.depth - b.depth) 117 | 118 | for(const chunk of chunks) 119 | { 120 | let nChunk = false 121 | let eChunk = false 122 | let sChunk = false 123 | let wChunk = false 124 | 125 | /** 126 | * North 127 | */ 128 | // From quad 129 | if(chunk.quadPosition === 'sw') 130 | nChunk = chunk.parent.children.get('nw') 131 | 132 | // From quad 133 | else if(chunk.quadPosition === 'se') 134 | nChunk = chunk.parent.children.get('ne') 135 | 136 | // From parent neighbours 137 | else 138 | { 139 | const parentNeighbour = chunk.parent.neighbours.get('n') 140 | if(parentNeighbour) 141 | { 142 | if(parentNeighbour.splitted) 143 | nChunk = parentNeighbour.children.get(chunk.quadPosition === 'nw' ? 'sw' : 'se') 144 | else 145 | nChunk = parentNeighbour 146 | } 147 | } 148 | 149 | /** 150 | * East 151 | */ 152 | // From quad 153 | if(chunk.quadPosition === 'nw') 154 | eChunk = chunk.parent.children.get('ne') 155 | 156 | // From quad 157 | else if(chunk.quadPosition === 'sw') 158 | eChunk = chunk.parent.children.get('se') 159 | 160 | // From parent neighbours 161 | else 162 | { 163 | const parentNeighbour = chunk.parent.neighbours.get('e') 164 | if(parentNeighbour) 165 | { 166 | if(parentNeighbour.splitted) 167 | eChunk = parentNeighbour.children.get(chunk.quadPosition === 'ne' ? 'nw' : 'sw') 168 | else 169 | eChunk = parentNeighbour 170 | } 171 | } 172 | 173 | /** 174 | * South 175 | */ 176 | // From quad 177 | if(chunk.quadPosition === 'nw') 178 | sChunk = chunk.parent.children.get('sw') 179 | 180 | // From quad 181 | else if(chunk.quadPosition === 'ne') 182 | sChunk = chunk.parent.children.get('se') 183 | 184 | // From parent neighbours 185 | else 186 | { 187 | const parentNeighbour = chunk.parent.neighbours.get('s') 188 | if(parentNeighbour) 189 | { 190 | if(parentNeighbour.splitted) 191 | sChunk = parentNeighbour.children.get(chunk.quadPosition === 'sw' ? 'nw' : 'ne') 192 | else 193 | sChunk = parentNeighbour 194 | } 195 | } 196 | 197 | /** 198 | * West 199 | */ 200 | // From quad 201 | if(chunk.quadPosition === 'ne') 202 | wChunk = chunk.parent.children.get('nw') 203 | 204 | // From quad 205 | else if(chunk.quadPosition === 'se') 206 | wChunk = chunk.parent.children.get('sw') 207 | 208 | // From parent neighbours 209 | else 210 | { 211 | const parentNeighbour = chunk.parent.neighbours.get('w') 212 | if(parentNeighbour) 213 | { 214 | if(parentNeighbour.splitted) 215 | wChunk = parentNeighbour.children.get(chunk.quadPosition === 'nw' ? 'ne' : 'se') 216 | else 217 | wChunk = parentNeighbour 218 | } 219 | } 220 | 221 | chunk.setNeighbours(nChunk, eChunk, sChunk, wChunk) 222 | } 223 | } 224 | 225 | getMainChunksCoordinates() 226 | { 227 | const player = this.state.player 228 | const currentX = Math.round(player.position.current[0] / this.maxSize) 229 | const currentZ = Math.round(player.position.current[2] / this.maxSize) 230 | 231 | // Find normalize neighbours 232 | const mainChunksCoordinates = [ 233 | { x: currentX, z: currentZ }, // Current 234 | { x: currentX, z: currentZ + 1 }, // Up 235 | { x: currentX + 1, z: currentZ + 1, }, // Up right 236 | { x: currentX + 1, z: currentZ }, // Right 237 | { x: currentX + 1, z: currentZ - 1 }, // Down right 238 | { x: currentX, z: currentZ - 1 }, // Down 239 | { x: currentX - 1, z: currentZ - 1 }, // Down left 240 | { x: currentX - 1, z: currentZ }, // Left 241 | { x: currentX - 1, z: currentZ + 1 }, // Up left 242 | ] 243 | 244 | // Create key and multiply by max size of chunks 245 | for(const coordinates of mainChunksCoordinates) 246 | { 247 | coordinates.coordinatesX = coordinates.x 248 | coordinates.coordinatesZ = coordinates.z 249 | coordinates.key = `${coordinates.x},${coordinates.z}` 250 | coordinates.x *= this.maxSize 251 | coordinates.z *= this.maxSize 252 | } 253 | 254 | return mainChunksCoordinates 255 | } 256 | 257 | underSplitDistance(size, chunkX, chunkY) 258 | { 259 | const player = this.state.player 260 | const distance = Math.hypot(player.position.current[0] - chunkX, player.position.current[2] - chunkY) 261 | return distance < size * this.splitRatioPerSize 262 | } 263 | 264 | getChildChunkForPosition(x, z) 265 | { 266 | for(const [key, chunk] of this.mainChunks) 267 | { 268 | if(chunk.isInside(x, z)) 269 | { 270 | return chunk 271 | } 272 | } 273 | } 274 | 275 | getDeepestChunkForPosition(x, z) 276 | { 277 | const baseChunk = this.getChildChunkForPosition(x, z) 278 | if(!baseChunk) 279 | return false 280 | 281 | const chunk = baseChunk.getChildChunkForPosition(x, z) 282 | return chunk 283 | } 284 | 285 | getElevationForPosition(x, z) 286 | { 287 | const currentChunk = this.getDeepestChunkForPosition(x, z) 288 | 289 | if(!currentChunk || !currentChunk.terrain) 290 | return false 291 | 292 | const elevation = currentChunk.terrain.getElevationForPosition(x, z) 293 | return elevation 294 | } 295 | } -------------------------------------------------------------------------------- /sources/Game/State/Controls.js: -------------------------------------------------------------------------------- 1 | import EventsEmitter from 'events' 2 | 3 | import Game from '@/Game.js' 4 | import State from '@/State/State.js' 5 | 6 | export default class Controls 7 | { 8 | constructor() 9 | { 10 | this.game = Game.getInstance() 11 | this.state = State.getInstance() 12 | 13 | this.events = new EventsEmitter() 14 | 15 | this.setKeys() 16 | this.setPointer() 17 | 18 | this.events.on('debugDown', () => 19 | { 20 | if(location.hash === '#debug') 21 | location.hash = '' 22 | else 23 | location.hash = 'debug' 24 | 25 | location.reload() 26 | }) 27 | } 28 | 29 | setKeys() 30 | { 31 | this.keys = {} 32 | 33 | // Map 34 | this.keys.map = [ 35 | { 36 | codes: [ 'ArrowUp', 'KeyW' ], 37 | name: 'forward' 38 | }, 39 | { 40 | codes: [ 'ArrowRight', 'KeyD' ], 41 | name: 'strafeRight' 42 | }, 43 | { 44 | codes: [ 'ArrowDown', 'KeyS' ], 45 | name: 'backward' 46 | }, 47 | { 48 | codes: [ 'ArrowLeft', 'KeyA' ], 49 | name: 'strafeLeft' 50 | }, 51 | { 52 | codes: [ 'ShiftLeft', 'ShiftRight' ], 53 | name: 'boost' 54 | }, 55 | { 56 | codes: [ 'KeyP' ], 57 | name: 'pointerLock' 58 | }, 59 | { 60 | codes: [ 'KeyV' ], 61 | name: 'cameraMode' 62 | }, 63 | { 64 | codes: [ 'KeyB' ], 65 | name: 'debug' 66 | }, 67 | { 68 | codes: [ 'KeyF' ], 69 | name: 'fullscreen' 70 | }, 71 | { 72 | codes: [ 'Space' ], 73 | name: 'jump' 74 | }, 75 | { 76 | codes: [ 'ControlLeft', 'KeyC' ], 77 | name: 'crouch' 78 | }, 79 | ] 80 | 81 | // Down keys 82 | this.keys.down = {} 83 | 84 | for(const mapItem of this.keys.map) 85 | { 86 | this.keys.down[mapItem.name] = false 87 | } 88 | 89 | // Find in map per code 90 | this.keys.findPerCode = (key) => 91 | { 92 | return this.keys.map.find((mapItem) => mapItem.codes.includes(key)) 93 | } 94 | 95 | // Event 96 | window.addEventListener('keydown', (event) => 97 | { 98 | const mapItem = this.keys.findPerCode(event.code) 99 | 100 | if(mapItem) 101 | { 102 | this.events.emit('keyDown', mapItem.name) 103 | this.events.emit(`${mapItem.name}Down`) 104 | this.keys.down[mapItem.name] = true 105 | } 106 | }) 107 | 108 | window.addEventListener('keyup', (event) => 109 | { 110 | const mapItem = this.keys.findPerCode(event.code) 111 | 112 | if(mapItem) 113 | { 114 | this.events.emit('keyUp', mapItem.name) 115 | this.events.emit(`${mapItem.name}Up`) 116 | this.keys.down[mapItem.name] = false 117 | } 118 | }) 119 | } 120 | 121 | setPointer() 122 | { 123 | this.pointer = {} 124 | this.pointer.down = false 125 | this.pointer.deltaTemp = { x: 0, y: 0 } 126 | this.pointer.delta = { x: 0, y: 0 } 127 | 128 | window.addEventListener('pointerdown', (event) => 129 | { 130 | this.pointer.down = true 131 | }) 132 | 133 | window.addEventListener('pointermove', (event) => 134 | { 135 | this.pointer.deltaTemp.x += event.movementX 136 | this.pointer.deltaTemp.y += event.movementY 137 | }) 138 | 139 | window.addEventListener('pointerup', () => 140 | { 141 | this.pointer.down = false 142 | }) 143 | } 144 | 145 | update() 146 | { 147 | this.pointer.delta.x = this.pointer.deltaTemp.x 148 | this.pointer.delta.y = this.pointer.deltaTemp.y 149 | 150 | this.pointer.deltaTemp.x = 0 151 | this.pointer.deltaTemp.y = 0 152 | } 153 | } -------------------------------------------------------------------------------- /sources/Game/State/DayCycle.js: -------------------------------------------------------------------------------- 1 | import Game from '@/Game.js' 2 | import State from '@/State/State.js' 3 | import Debug from '@/Debug/Debug.js' 4 | 5 | export default class DayCycle 6 | { 7 | constructor() 8 | { 9 | this.game = Game.getInstance() 10 | this.state = State.getInstance() 11 | this.debug = Debug.getInstance() 12 | 13 | this.autoUpdate = true 14 | this.timeProgress = 0 15 | this.progress = 0 16 | this.duration = 15 // Seconds 17 | 18 | this.setDebug() 19 | } 20 | 21 | update() 22 | { 23 | const time = this.state.time 24 | 25 | if(this.autoUpdate) 26 | { 27 | this.timeProgress += time.delta / this.duration 28 | this.progress = this.timeProgress % 1 29 | } 30 | } 31 | 32 | setDebug() 33 | { 34 | if(!this.debug.active) 35 | return 36 | 37 | const folder = this.debug.ui.getFolder('state/dayCycle') 38 | 39 | folder 40 | .add(this, 'autoUpdate') 41 | 42 | folder 43 | .add(this, 'progress') 44 | .min(0) 45 | .max(1) 46 | .step(0.001) 47 | 48 | folder 49 | .add(this, 'duration') 50 | .min(5) 51 | .max(100) 52 | .step(1) 53 | } 54 | } -------------------------------------------------------------------------------- /sources/Game/State/Player.js: -------------------------------------------------------------------------------- 1 | import { vec3 } from 'gl-matrix' 2 | 3 | import Game from '@/Game.js' 4 | import State from '@/State/State.js' 5 | import Camera from './Camera.js' 6 | 7 | export default class Player 8 | { 9 | constructor() 10 | { 11 | this.game = Game.getInstance() 12 | this.state = State.getInstance() 13 | this.time = this.state.time 14 | this.controls = this.state.controls 15 | 16 | this.rotation = 0 17 | this.inputSpeed = 10 18 | this.inputBoostSpeed = 30 19 | this.speed = 0 20 | 21 | this.position = {} 22 | this.position.current = vec3.fromValues(10, 0, 1) 23 | this.position.previous = vec3.clone(this.position.current) 24 | this.position.delta = vec3.create() 25 | 26 | this.camera = new Camera(this) 27 | } 28 | 29 | update() 30 | { 31 | if(this.camera.mode !== Camera.MODE_FLY && (this.controls.keys.down.forward || this.controls.keys.down.backward || this.controls.keys.down.strafeLeft || this.controls.keys.down.strafeRight)) 32 | { 33 | this.rotation = this.camera.thirdPerson.theta 34 | 35 | if(this.controls.keys.down.forward) 36 | { 37 | if(this.controls.keys.down.strafeLeft) 38 | this.rotation += Math.PI * 0.25 39 | else if(this.controls.keys.down.strafeRight) 40 | this.rotation -= Math.PI * 0.25 41 | } 42 | else if(this.controls.keys.down.backward) 43 | { 44 | if(this.controls.keys.down.strafeLeft) 45 | this.rotation += Math.PI * 0.75 46 | else if(this.controls.keys.down.strafeRight) 47 | this.rotation -= Math.PI * 0.75 48 | else 49 | this.rotation -= Math.PI 50 | } 51 | else if(this.controls.keys.down.strafeLeft) 52 | { 53 | this.rotation += Math.PI * 0.5 54 | } 55 | else if(this.controls.keys.down.strafeRight) 56 | { 57 | this.rotation -= Math.PI * 0.5 58 | } 59 | 60 | const speed = this.controls.keys.down.boost ? this.inputBoostSpeed : this.inputSpeed 61 | 62 | const x = Math.sin(this.rotation) * this.time.delta * speed 63 | const z = Math.cos(this.rotation) * this.time.delta * speed 64 | 65 | this.position.current[0] -= x 66 | this.position.current[2] -= z 67 | } 68 | 69 | vec3.sub(this.position.delta, this.position.current, this.position.previous) 70 | vec3.copy(this.position.previous, this.position.current) 71 | 72 | this.speed = vec3.len(this.position.delta) 73 | 74 | // Update view 75 | this.camera.update() 76 | 77 | // Update elevation 78 | const chunks = this.state.chunks 79 | const elevation = chunks.getElevationForPosition(this.position.current[0], this.position.current[2]) 80 | 81 | if(elevation) 82 | this.position.current[1] = elevation 83 | else 84 | this.position.current[1] = 0 85 | } 86 | } -------------------------------------------------------------------------------- /sources/Game/State/State.js: -------------------------------------------------------------------------------- 1 | import Time from './Time.js' 2 | import Controls from './Controls.js' 3 | import Viewport from './Viewport.js' 4 | import DayCycle from './DayCycle.js' 5 | import Sun from './Sun.js' 6 | import Player from './Player.js' 7 | import Terrains from './Terrains.js' 8 | import Chunks from './Chunks.js' 9 | 10 | export default class State 11 | { 12 | static instance 13 | 14 | static getInstance() 15 | { 16 | return State.instance 17 | } 18 | 19 | constructor() 20 | { 21 | if(State.instance) 22 | return State.instance 23 | 24 | State.instance = this 25 | 26 | this.time = new Time() 27 | this.controls = new Controls() 28 | this.viewport = new Viewport() 29 | this.day = new DayCycle() 30 | this.sun = new Sun() 31 | this.player = new Player() 32 | this.terrains = new Terrains() 33 | this.chunks = new Chunks() 34 | } 35 | 36 | resize() 37 | { 38 | this.viewport.resize() 39 | } 40 | 41 | update() 42 | { 43 | this.time.update() 44 | this.controls.update() 45 | this.day.update() 46 | this.sun.update() 47 | this.player.update() 48 | this.chunks.update() 49 | } 50 | } -------------------------------------------------------------------------------- /sources/Game/State/Sun.js: -------------------------------------------------------------------------------- 1 | import Game from '@/Game.js' 2 | import State from '@/State/State.js' 3 | 4 | export default class Sun 5 | { 6 | constructor() 7 | { 8 | this.game = Game.getInstance() 9 | this.state = State.getInstance() 10 | 11 | this.theta = Math.PI * 0.8 // All around the sphere 12 | this.phi = Math.PI * 0.45 // Elevation 13 | this.position = { x: 0, y: 0, z: 0 } 14 | } 15 | 16 | update() 17 | { 18 | const dayState = this.state.day 19 | 20 | const angle = - (dayState.progress + 0.25) * Math.PI * 2 21 | this.phi = (Math.sin(angle) * 0.3 + 0.5) * Math.PI 22 | this.theta = (Math.cos(angle) * 0.3 + 0.5) * Math.PI 23 | 24 | const sinPhiRadius = Math.sin(this.phi) 25 | 26 | this.position.x = sinPhiRadius * Math.sin(this.theta) 27 | this.position.y = Math.cos(this.phi) 28 | this.position.z = sinPhiRadius * Math.cos(this.theta) 29 | } 30 | } -------------------------------------------------------------------------------- /sources/Game/State/Terrain.js: -------------------------------------------------------------------------------- 1 | import EventsEmitter from 'events' 2 | 3 | export default class Terrain 4 | { 5 | constructor(terrains, id, size, x, z, precision, elevationOffset) 6 | { 7 | this.terrains = terrains 8 | this.id = id 9 | this.size = size 10 | this.x = x 11 | this.z = z 12 | this.precision = precision 13 | this.elevationOffset = elevationOffset 14 | 15 | this.halfSize = this.size * 0.5 16 | this.ready = false 17 | this.renderInstance = null 18 | 19 | this.events = new EventsEmitter() 20 | } 21 | 22 | create(data) 23 | { 24 | this.positions = data.positions 25 | this.normals = data.normals 26 | this.indices = data.indices 27 | this.texture = data.texture 28 | this.uv = data.uv 29 | 30 | this.ready = true 31 | 32 | this.events.emit('ready') 33 | } 34 | 35 | getElevationForPosition(x, z) 36 | { 37 | if(!this.ready) 38 | { 39 | // console.warn('terrain not ready') 40 | return 41 | } 42 | 43 | const subdivisions = this.terrains.subdivisions 44 | const segments = subdivisions + 1 45 | const subSize = this.size / subdivisions 46 | 47 | // Relative position 48 | const relativeX = x - this.x + this.halfSize 49 | const relativeZ = z - this.z + this.halfSize 50 | 51 | // Ratio 52 | const xRatio = (relativeX / subSize) % 1 53 | const zRatio = (relativeZ / subSize) % 1 54 | 55 | // Indexes 56 | const aIndexX = Math.floor(relativeX / subSize) 57 | const aIndexZ = Math.floor(relativeZ / subSize) 58 | 59 | const cIndexX = aIndexX + 1 60 | const cIndexZ = aIndexZ + 1 61 | 62 | const bIndexX = xRatio < zRatio ? aIndexX : aIndexX + 1 63 | const bIndexZ = xRatio < zRatio ? aIndexZ + 1 : aIndexZ 64 | 65 | const aStrideIndex = (aIndexZ * segments + aIndexX) * 3 66 | const bStrideIndex = (bIndexZ * segments + bIndexX) * 3 67 | const cStrideIndex = (cIndexZ * segments + cIndexX) * 3 68 | 69 | // Weights 70 | const weight1 = xRatio < zRatio ? 1 - zRatio : 1 - xRatio 71 | const weight2 = xRatio < zRatio ? - (xRatio - zRatio) : xRatio - zRatio 72 | const weight3 = 1 - weight1 - weight2 73 | 74 | // Elevation 75 | const aElevation = this.positions[aStrideIndex + 1] 76 | const bElevation = this.positions[bStrideIndex + 1] 77 | const cElevation = this.positions[cStrideIndex + 1] 78 | const elevation = aElevation * weight1 + bElevation * weight2 + cElevation * weight3 79 | 80 | // // Normal 81 | // const aNormalX = this.normals[aStrideIndex] 82 | // const aNormalY = this.normals[aStrideIndex + 1] 83 | // const aNormalZ = this.normals[aStrideIndex + 2] 84 | 85 | // const bNormalX = this.normals[bStrideIndex] 86 | // const bNormalY = this.normals[bStrideIndex + 1] 87 | // const bNormalZ = this.normals[bStrideIndex + 2] 88 | 89 | // const cNormalX = this.normals[cStrideIndex] 90 | // const cNormalY = this.normals[cStrideIndex + 1] 91 | // const cNormalZ = this.normals[cStrideIndex + 2] 92 | 93 | // const normal = [ 94 | // aNormalX * weight1 + bNormalX * weight2 + cNormalX * weight3, 95 | // aNormalY * weight1 + bNormalY * weight2 + cNormalY * weight3, 96 | // aNormalZ * weight1 + bNormalZ * weight2 + cNormalZ * weight3 97 | // ] 98 | 99 | return elevation 100 | } 101 | 102 | destroy() 103 | { 104 | this.events.emit('destroy') 105 | } 106 | } -------------------------------------------------------------------------------- /sources/Game/State/Terrains.js: -------------------------------------------------------------------------------- 1 | import EventsEmitter from 'events' 2 | import seedrandom from 'seedrandom' 3 | 4 | import Game from '@/Game.js' 5 | import State from '@/State/State.js' 6 | import Debug from '@/Debug/Debug.js' 7 | import TerrainWorker from '@/Workers/Terrain.js?worker' 8 | import Terrain from './Terrain.js' 9 | 10 | export default class Terrains 11 | { 12 | static ITERATIONS_FORMULA_MAX = 1 13 | static ITERATIONS_FORMULA_MIN = 2 14 | static ITERATIONS_FORMULA_MIX = 3 15 | static ITERATIONS_FORMULA_POWERMIX = 4 16 | 17 | constructor() 18 | { 19 | this.game = Game.getInstance() 20 | this.state = State.getInstance() 21 | this.debug = Debug.getInstance() 22 | 23 | this.seed = this.game.seed + 'b' 24 | this.random = new seedrandom(this.seed) 25 | this.subdivisions = 40 26 | this.lacunarity = 2.05 27 | this.persistence = 0.45 28 | this.maxIterations = 6 29 | this.baseFrequency = 0.003 30 | this.baseAmplitude = 180 31 | this.power = 2 32 | this.elevationOffset = 1 33 | 34 | this.segments = this.subdivisions + 1 35 | this.iterationsFormula = Terrains.ITERATIONS_FORMULA_POWERMIX 36 | 37 | this.lastId = 0 38 | this.terrains = new Map() 39 | 40 | this.events = new EventsEmitter() 41 | 42 | // Iterations offsets 43 | this.iterationsOffsets = [] 44 | 45 | for(let i = 0; i < this.maxIterations; i++) 46 | this.iterationsOffsets.push([(this.random() - 0.5) * 200000, (this.random() - 0.5) * 200000]) 47 | 48 | this.setWorkers() 49 | this.setDebug() 50 | } 51 | 52 | setWorkers() 53 | { 54 | this.worker = TerrainWorker() 55 | 56 | this.worker.onmessage = (event) => 57 | { 58 | // console.timeEnd(`terrains: worker (${event.data.id})`) 59 | 60 | const terrain = this.terrains.get(event.data.id) 61 | 62 | if(terrain) 63 | { 64 | terrain.create(event.data) 65 | } 66 | } 67 | } 68 | 69 | getIterationsForPrecision(precision) 70 | { 71 | if(this.iterationsFormula === Terrains.ITERATIONS_FORMULA_MAX) 72 | return this.maxIterations 73 | 74 | if(this.iterationsFormula === Terrains.ITERATIONS_FORMULA_MIN) 75 | return Math.floor((this.maxIterations - 1) * precision) + 1 76 | 77 | if(this.iterationsFormula === Terrains.ITERATIONS_FORMULA_MIX) 78 | return Math.round((this.maxIterations * precision + this.maxIterations) / 2) 79 | 80 | if(this.iterationsFormula === Terrains.ITERATIONS_FORMULA_POWERMIX) 81 | return Math.round((this.maxIterations * (precision, 1 - Math.pow(1 - precision, 2)) + this.maxIterations) / 2) 82 | } 83 | 84 | create(size, x, z, precision) 85 | { 86 | // Create id 87 | const id = this.lastId++ 88 | 89 | // Create terrain 90 | const iterations = this.getIterationsForPrecision(precision) 91 | const terrain = new Terrain(this, id, size, x, z, precision) 92 | this.terrains.set(terrain.id, terrain) 93 | 94 | // Post to worker 95 | // console.time(`terrains: worker (${terrain.id})`) 96 | this.worker.postMessage({ 97 | id: terrain.id, 98 | x, 99 | z, 100 | seed: this.seed, 101 | subdivisions: this.subdivisions, 102 | size: size, 103 | lacunarity: this.lacunarity, 104 | persistence: this.persistence, 105 | iterations: iterations, 106 | baseFrequency: this.baseFrequency, 107 | baseAmplitude: this.baseAmplitude, 108 | power: this.power, 109 | elevationOffset: this.elevationOffset, 110 | iterationsOffsets: this.iterationsOffsets 111 | }) 112 | 113 | this.events.emit('create', terrain) 114 | 115 | return terrain 116 | } 117 | 118 | destroyTerrain(id) 119 | { 120 | const terrain = this.terrains.get(id) 121 | 122 | if(terrain) 123 | { 124 | terrain.destroy() 125 | this.terrains.delete(id) 126 | } 127 | } 128 | 129 | recreate() 130 | { 131 | for(const [key, terrain] of this.terrains) 132 | { 133 | // this.create(terrain.size, terrain.x, terrain.z) 134 | 135 | // console.time(`terrains: worker (${terrain.id})`) 136 | const iterations = this.getIterationsForPrecision(terrain.precision) 137 | this.worker.postMessage({ 138 | id: terrain.id, 139 | size: terrain.size, 140 | x: terrain.x, 141 | z: terrain.z, 142 | seed: this.seed, 143 | subdivisions: this.subdivisions, 144 | lacunarity: this.lacunarity, 145 | persistence: this.persistence, 146 | iterations: iterations, 147 | baseFrequency: this.baseFrequency, 148 | baseAmplitude: this.baseAmplitude, 149 | power: this.power, 150 | elevationOffset: this.elevationOffset, 151 | iterationsOffsets: this.iterationsOffsets 152 | }) 153 | } 154 | } 155 | 156 | setDebug() 157 | { 158 | if(!this.debug.active) 159 | return 160 | 161 | const folder = this.debug.ui.getFolder('state/terrains') 162 | 163 | folder 164 | .add(this, 'subdivisions') 165 | .min(1) 166 | .max(400) 167 | .step(1) 168 | .onFinishChange(() => this.recreate()) 169 | 170 | folder 171 | .add(this, 'lacunarity') 172 | .min(1) 173 | .max(5) 174 | .step(0.01) 175 | .onFinishChange(() => this.recreate()) 176 | 177 | folder 178 | .add(this, 'persistence') 179 | .min(0) 180 | .max(1) 181 | .step(0.01) 182 | .onFinishChange(() => this.recreate()) 183 | 184 | folder 185 | .add(this, 'maxIterations') 186 | .min(1) 187 | .max(10) 188 | .step(1) 189 | .onFinishChange(() => this.recreate()) 190 | 191 | folder 192 | .add(this, 'baseFrequency') 193 | .min(0) 194 | .max(0.01) 195 | .step(0.0001) 196 | .onFinishChange(() => this.recreate()) 197 | 198 | folder 199 | .add(this, 'baseAmplitude') 200 | .min(0) 201 | .max(500) 202 | .step(0.1) 203 | .onFinishChange(() => this.recreate()) 204 | 205 | folder 206 | .add(this, 'power') 207 | .min(1) 208 | .max(10) 209 | .step(1) 210 | .onFinishChange(() => this.recreate()) 211 | 212 | folder 213 | .add(this, 'elevationOffset') 214 | .min(- 10) 215 | .max(10) 216 | .step(1) 217 | .onFinishChange(() => this.recreate()) 218 | 219 | folder 220 | .add( 221 | this, 222 | 'iterationsFormula', 223 | { 224 | 'max': Terrains.ITERATIONS_FORMULA_MAX, 225 | 'min': Terrains.ITERATIONS_FORMULA_MIN, 226 | 'mix': Terrains.ITERATIONS_FORMULA_MIX, 227 | 'powerMix': Terrains.ITERATIONS_FORMULA_POWERMIX, 228 | } 229 | ) 230 | .onFinishChange(() => this.recreate()) 231 | 232 | 233 | // this.material.uniforms.uFresnelOffset.value = 0 234 | // this.material.uniforms.uFresnelScale.value = 0.5 235 | // this.material.uniforms.uFresnelPower.value = 2 236 | } 237 | } -------------------------------------------------------------------------------- /sources/Game/State/Time.js: -------------------------------------------------------------------------------- 1 | export default class Time 2 | { 3 | /** 4 | * Constructor 5 | */ 6 | constructor() 7 | { 8 | this.start = Date.now() / 1000 9 | this.current = this.start 10 | this.elapsed = 0 11 | this.delta = 16 / 1000 12 | } 13 | 14 | /** 15 | * Tick 16 | */ 17 | update() 18 | { 19 | const current = Date.now() / 1000 20 | 21 | this.delta = current - this.current 22 | this.elapsed += this.delta 23 | this.current = current 24 | 25 | if(this.delta > 60 / 1000) 26 | { 27 | this.delta = 60 / 1000 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /sources/Game/State/Viewport.js: -------------------------------------------------------------------------------- 1 | import Game from '@/Game.js' 2 | import State from '@/State/State.js' 3 | 4 | export default class Viewport 5 | { 6 | constructor() 7 | { 8 | this.game = Game.getInstance() 9 | this.state = State.getInstance() 10 | this.controls = this.state.controls 11 | this.domElement = this.game.domElement 12 | 13 | this.width = null 14 | this.height = null 15 | this.smallestSide = null 16 | this.biggestSide = null 17 | this.pixelRatio = null 18 | this.clampedPixelRatio = null 19 | 20 | this.setPointerLock() 21 | this.setFullscreen() 22 | 23 | this.controls.events.on('pointerLockDown', () => 24 | { 25 | this.pointerLock.toggle() 26 | }) 27 | 28 | this.controls.events.on('fullscreenDown', () => 29 | { 30 | this.fullscreen.toggle() 31 | }) 32 | 33 | this.resize() 34 | } 35 | 36 | setPointerLock() 37 | { 38 | this.pointerLock = {} 39 | this.pointerLock.active = false 40 | 41 | this.pointerLock.toggle = () => 42 | { 43 | if(this.pointerLock.active) 44 | this.pointerLock.deactivate() 45 | else 46 | this.pointerLock.activate() 47 | } 48 | 49 | this.pointerLock.activate = () => 50 | { 51 | this.domElement.requestPointerLock() 52 | } 53 | 54 | this.pointerLock.deactivate = () => 55 | { 56 | document.exitPointerLock() 57 | } 58 | 59 | document.addEventListener('pointerlockchange', () => 60 | { 61 | this.pointerLock.active = !!document.pointerLockElement 62 | }) 63 | } 64 | 65 | setFullscreen() 66 | { 67 | this.fullscreen = {} 68 | this.fullscreen.active = false 69 | 70 | this.fullscreen.toggle = () => 71 | { 72 | if(this.fullscreen.active) 73 | this.fullscreen.deactivate() 74 | else 75 | this.fullscreen.activate() 76 | } 77 | 78 | this.fullscreen.activate = () => 79 | { 80 | this.domElement.requestFullscreen() 81 | } 82 | 83 | this.fullscreen.deactivate = () => 84 | { 85 | document.exitFullscreen() 86 | } 87 | 88 | document.addEventListener('fullscreenchange', () => 89 | { 90 | this.fullscreen.active = !!document.fullscreenElement 91 | }) 92 | } 93 | 94 | normalise(pixelCoordinates) 95 | { 96 | const minSize = Math.min(this.width, this.height) 97 | return { 98 | x: pixelCoordinates.x / minSize, 99 | y: pixelCoordinates.y / minSize, 100 | } 101 | } 102 | 103 | resize() 104 | { 105 | this.width = window.innerWidth 106 | this.height = window.innerHeight 107 | this.smallestSide = this.width < this.height ? this.width : this.height 108 | this.biggestSide = this.width > this.height ? this.width : this.height 109 | this.pixelRatio = window.devicePixelRatio 110 | this.clampedPixelRatio = Math.min(this.pixelRatio, 2) 111 | } 112 | } -------------------------------------------------------------------------------- /sources/Game/View/Camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import View from '@/View/View.js' 4 | import State from '@/State/State.js' 5 | 6 | export default class Camera 7 | { 8 | constructor(_options) 9 | { 10 | // Options 11 | this.state = State.getInstance() 12 | this.view = View.getInstance() 13 | this.scene = this.view.scene 14 | this.viewport = this.state.viewport 15 | 16 | this.setInstance() 17 | } 18 | 19 | setInstance() 20 | { 21 | // Set up 22 | this.instance = new THREE.PerspectiveCamera(45, this.viewport.width / this.viewport.height, 0.1, 5000) 23 | this.instance.rotation.reorder('YXZ') 24 | 25 | this.scene.add(this.instance) 26 | } 27 | 28 | resize() 29 | { 30 | this.instance.aspect = this.viewport.width / this.viewport.height 31 | this.instance.updateProjectionMatrix() 32 | } 33 | 34 | update() 35 | { 36 | const playerSate = this.state.player 37 | 38 | // Apply coordinates from view 39 | this.instance.position.set(playerSate.camera.position[0], playerSate.camera.position[1], playerSate.camera.position[2]) 40 | this.instance.quaternion.set(playerSate.camera.quaternion[0], playerSate.camera.quaternion[1], playerSate.camera.quaternion[2], playerSate.camera.quaternion[3]) 41 | // this.instance.updateMatrixGame() // To be used in projection 42 | } 43 | 44 | destroy() 45 | { 46 | } 47 | } -------------------------------------------------------------------------------- /sources/Game/View/Chunk.js: -------------------------------------------------------------------------------- 1 | import Game from '@/Game.js' 2 | import State from '@/State/State.js' 3 | import ChunkHelper from './ChunkHelper.js' 4 | 5 | export default class Chunk 6 | { 7 | constructor(chunkState) 8 | { 9 | this.game = Game.getInstance() 10 | this.state = State.getInstance() 11 | this.scene = this.game.scene 12 | 13 | this.chunkState = chunkState 14 | 15 | this.helper = new ChunkHelper(this.chunkState) 16 | } 17 | 18 | update() 19 | { 20 | 21 | } 22 | 23 | destroy() 24 | { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sources/Game/View/ChunkHelper.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { PointTextHelper } from '@jniac/three-point-text-helper' 3 | 4 | import View from '@/View/View.js' 5 | import State from '@/State/State.js' 6 | 7 | export default class ChunkHelper 8 | { 9 | constructor(chunkSate) 10 | { 11 | this.state = State.getInstance() 12 | this.view = View.getInstance() 13 | this.scene = this.view.scene 14 | 15 | this.chunkState = chunkSate 16 | 17 | this.areaVisible = true 18 | this.idVisible = true 19 | this.neighboursIdVisible = true 20 | 21 | // this.setGroup() 22 | // this.setArea() 23 | // this.setId() 24 | // this.setNeighboursIds() 25 | } 26 | 27 | setGroup() 28 | { 29 | this.group = new THREE.Group() 30 | this.group.position.x = this.chunkState.x 31 | this.group.position.z = this.chunkState.z 32 | this.scene.add(this.group) 33 | } 34 | 35 | destroyGroup() 36 | { 37 | if(!this.group) 38 | return 39 | 40 | this.scene.remove(this.group) 41 | } 42 | 43 | setArea() 44 | { 45 | this.destroyArea() 46 | 47 | if(!this.areaVisible) 48 | return 49 | 50 | this.area = new THREE.Mesh( 51 | new THREE.PlaneGeometry(this.chunkState.size, this.chunkState.size), 52 | new THREE.MeshBasicMaterial({ wireframe: true }) 53 | ) 54 | this.area.geometry.rotateX(Math.PI * 0.5) 55 | 56 | this.area.material.color.multiplyScalar((this.chunkState.depth + 1) / (this.state.chunks.maxDepth)) 57 | 58 | this.group.add(this.area) 59 | } 60 | 61 | destroyArea() 62 | { 63 | if(!this.area) 64 | return 65 | 66 | this.area.geometry.dispose() 67 | this.area.material.dispose() 68 | this.group.remove() 69 | } 70 | 71 | setId() 72 | { 73 | this.destroyId() 74 | 75 | if(!this.idVisible) 76 | return 77 | 78 | this.id = new PointTextHelper({ charMax: 4 }) 79 | this.id.material.depthTest = false 80 | this.id.material.onBeforeRender = () => {} 81 | this.id.material.onBuild = () => {} 82 | this.id.display({ 83 | text: this.chunkState.id, 84 | color: '#ffc800', 85 | size: (this.state.chunks.maxDepth - this.chunkState.depth + 1) * 6, 86 | position: new THREE.Vector3(0, (this.state.chunks.maxDepth - this.chunkState.depth) * 10, 0) 87 | }) 88 | this.group.add(this.id) 89 | } 90 | 91 | destroyId() 92 | { 93 | if(!this.id) 94 | return 95 | 96 | this.id.geometry.dispose() 97 | this.id.material.dispose() 98 | this.group.remove(this.id) 99 | } 100 | 101 | setNeighboursIds() 102 | { 103 | this.destroyNeighboursIds() 104 | 105 | if(!this.neighboursIdVisible) 106 | return 107 | 108 | if(this.chunkState.neighbours.size === 0) 109 | return 110 | 111 | this.neighboursIds = new PointTextHelper({ charMax: 4 }) 112 | this.neighboursIds.material.depthTest = false 113 | this.neighboursIds.material.onBeforeRender = () => {} 114 | this.neighboursIds.material.onBuild = () => {} 115 | this.group.add(this.neighboursIds) 116 | 117 | const nChunk = this.chunkState.neighbours.get('n') 118 | const eChunk = this.chunkState.neighbours.get('e') 119 | const sChunk = this.chunkState.neighbours.get('s') 120 | const wChunk = this.chunkState.neighbours.get('w') 121 | 122 | const size = (this.state.chunks.maxDepth - this.chunkState.depth + 1) * 6 123 | const y = (this.state.chunks.maxDepth - this.chunkState.depth) * 10 124 | 125 | const nLabel = nChunk ? nChunk.id : '' 126 | this.neighboursIds.display({ 127 | text: nLabel, 128 | color: '#00bfff', 129 | size: size, 130 | position: new THREE.Vector3( 131 | 0, 132 | y, 133 | - this.chunkState.quarterSize 134 | ) 135 | }) 136 | 137 | const eLabel = eChunk ? eChunk.id : '' 138 | this.neighboursIds.display({ 139 | text: eLabel, 140 | color: '#00bfff', 141 | size: size, 142 | position: new THREE.Vector3( 143 | this.chunkState.quarterSize, 144 | y, 145 | 0 146 | ) 147 | }) 148 | 149 | const sLabel = sChunk ? sChunk.id : '' 150 | this.neighboursIds.display({ 151 | text: sLabel, 152 | color: '#00bfff', 153 | size: size, 154 | position: new THREE.Vector3( 155 | 0, 156 | y, 157 | this.chunkState.quarterSize 158 | ) 159 | }) 160 | 161 | const wLabel = wChunk ? wChunk.id : '' 162 | this.neighboursIds.display({ 163 | text: wLabel, 164 | color: '#00bfff', 165 | size: size, 166 | position: new THREE.Vector3( 167 | - this.chunkState.quarterSize, 168 | y, 169 | 0 170 | ) 171 | }) 172 | } 173 | 174 | destroyNeighboursIds() 175 | { 176 | if(!this.neighboursIds) 177 | return 178 | 179 | this.neighboursIds.geometry.dispose() 180 | this.neighboursIds.material.dispose() 181 | this.group.remove(this.neighboursIds) 182 | } 183 | 184 | destroy() 185 | { 186 | this.destroyGroup() 187 | this.destroyArea() 188 | this.destroyId() 189 | this.destroyNeighboursIds() 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /sources/Game/View/Chunks.js: -------------------------------------------------------------------------------- 1 | import State from '@/State/State.js' 2 | import Chunk from './Chunk.js' 3 | 4 | export default class Chunks 5 | { 6 | constructor() 7 | { 8 | this.state = State.getInstance() 9 | 10 | this.state.chunks.events.on('create', (chunkState) => 11 | { 12 | const chunk = new Chunk(chunkState) 13 | 14 | chunkState.events.on('destroy', () => 15 | { 16 | chunk.destroy() 17 | }) 18 | }) 19 | } 20 | 21 | update() 22 | { 23 | 24 | } 25 | } -------------------------------------------------------------------------------- /sources/Game/View/Grass.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import Game from '@/Game.js' 4 | import View from '@/View/View.js' 5 | import State from '@/State/State.js' 6 | import GrassMaterial from './Materials/GrassMaterial.js' 7 | 8 | export default class Grass 9 | { 10 | constructor() 11 | { 12 | this.game = Game.getInstance() 13 | this.view = View.getInstance() 14 | this.state = State.getInstance() 15 | 16 | this.time = this.state.time 17 | this.scene = this.view.scene 18 | this.noises = this.view.noises 19 | 20 | this.details = 200 21 | this.size = this.state.chunks.minSize 22 | this.count = this.details * this.details 23 | this.fragmentSize = this.size / this.details 24 | this.bladeWidthRatio = 1.5 25 | this.bladeHeightRatio = 4 26 | this.bladeHeightRandomness = 0.5 27 | this.positionRandomness = 0.5 28 | this.noiseTexture = this.noises.create(128, 128) 29 | 30 | this.setGeometry() 31 | this.setMaterial() 32 | this.setMesh() 33 | } 34 | 35 | setGeometry() 36 | { 37 | const centers = new Float32Array(this.count * 3 * 2) 38 | const positions = new Float32Array(this.count * 3 * 3) 39 | // const tipness = new Float32Array(this.count * 3) 40 | 41 | for(let iX = 0; iX < this.details; iX++) 42 | { 43 | const fragmentX = (iX / this.details - 0.5) * this.size + this.fragmentSize * 0.5 44 | 45 | for(let iZ = 0; iZ < this.details; iZ++) 46 | { 47 | const fragmentZ = (iZ / this.details - 0.5) * this.size + this.fragmentSize * 0.5 48 | 49 | const iStride9 = (iX * this.details + iZ) * 9 50 | const iStride6 = (iX * this.details + iZ) * 6 51 | // const iStride3 = (iX * this.details + iZ) * 3 52 | 53 | // Center (for blade rotation) 54 | const centerX = fragmentX + (Math.random() - 0.5) * this.fragmentSize * this.positionRandomness 55 | const centerZ = fragmentZ + (Math.random() - 0.5) * this.fragmentSize * this.positionRandomness 56 | 57 | centers[iStride6 ] = centerX 58 | centers[iStride6 + 1] = centerZ 59 | 60 | centers[iStride6 + 2] = centerX 61 | centers[iStride6 + 3] = centerZ 62 | 63 | centers[iStride6 + 4] = centerX 64 | centers[iStride6 + 5] = centerZ 65 | 66 | // Position 67 | const bladeWidth = this.fragmentSize * this.bladeWidthRatio 68 | const bladeHalfWidth = bladeWidth * 0.5 69 | const bladeHeight = this.fragmentSize * this.bladeHeightRatio * (1 - this.bladeHeightRandomness + Math.random() * this.bladeHeightRandomness) 70 | 71 | positions[iStride9 ] = - bladeHalfWidth 72 | positions[iStride9 + 1] = 0 73 | positions[iStride9 + 2] = 0 74 | 75 | positions[iStride9 + 3] = 0 76 | positions[iStride9 + 4] = bladeHeight 77 | positions[iStride9 + 5] = 0 78 | 79 | positions[iStride9 + 6] = bladeHalfWidth 80 | positions[iStride9 + 7] = 0 81 | positions[iStride9 + 8] = 0 82 | 83 | // // Tipness 84 | // tipness[iStride3 ] = 0 85 | // tipness[iStride3 + 1] = 1 86 | // tipness[iStride3 + 2] = 0 87 | } 88 | } 89 | 90 | this.geometry = new THREE.BufferGeometry() 91 | this.geometry.setAttribute('center', new THREE.Float32BufferAttribute(centers, 2)) 92 | this.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)) 93 | // this.geometry.setAttribute('tipness', new THREE.Float32BufferAttribute(tipness, 1)) 94 | } 95 | 96 | setMaterial() 97 | { 98 | const engineChunks = this.state.chunks 99 | const engineTerrains = this.state.terrains 100 | 101 | // this.material = new THREE.MeshBasicMaterial({ wireframe: true, color: 'green' }) 102 | this.material = new GrassMaterial() 103 | this.material.uniforms.uTime.value = 0 104 | this.material.uniforms.uGrassDistance.value = this.size 105 | this.material.uniforms.uPlayerPosition.value = new THREE.Vector3() 106 | this.material.uniforms.uTerrainSize.value = engineChunks.minSize 107 | this.material.uniforms.uTerrainTextureSize.value = engineTerrains.segments 108 | this.material.uniforms.uTerrainATexture.value = null 109 | this.material.uniforms.uTerrainAOffset.value = new THREE.Vector2() 110 | this.material.uniforms.uTerrainBTexture.value = null 111 | this.material.uniforms.uTerrainBOffset.value = new THREE.Vector2() 112 | this.material.uniforms.uTerrainCTexture.value = null 113 | this.material.uniforms.uTerrainCOffset.value = new THREE.Vector2() 114 | this.material.uniforms.uTerrainDTexture.value = null 115 | this.material.uniforms.uTerrainDOffset.value = new THREE.Vector2() 116 | this.material.uniforms.uNoiseTexture.value = this.noiseTexture 117 | this.material.uniforms.uFresnelOffset.value = 0 118 | this.material.uniforms.uFresnelScale.value = 0.5 119 | this.material.uniforms.uFresnelPower.value = 2 120 | this.material.uniforms.uSunPosition.value = new THREE.Vector3(- 0.5, - 0.5, - 0.5) 121 | // this.material.wireframe = true 122 | } 123 | 124 | setMesh() 125 | { 126 | this.mesh = new THREE.Mesh( 127 | this.geometry, 128 | this.material 129 | ) 130 | this.mesh.frustumCulled = false 131 | this.scene.add(this.mesh) 132 | } 133 | 134 | update() 135 | { 136 | const playerState = this.state.player 137 | const playerPosition = playerState.position.current 138 | const engineChunks = this.state.chunks 139 | const sunState = this.state.sun 140 | 141 | this.material.uniforms.uTime.value = this.time.elapsed 142 | this.material.uniforms.uSunPosition.value.set(sunState.position.x, sunState.position.y, sunState.position.z) 143 | 144 | this.mesh.position.set(playerPosition[0], 0, playerPosition[2]) 145 | // this.mesh.position.set(playerPosition[0], playerPosition[1], playerPosition[2]) 146 | this.material.uniforms.uPlayerPosition.value.set(playerPosition[0], playerPosition[1], playerPosition[2]) 147 | 148 | // Get terrain data 149 | const aChunkState = engineChunks.getDeepestChunkForPosition(playerPosition[0], playerPosition[2]) 150 | 151 | if(aChunkState && aChunkState.terrain && aChunkState.terrain.renderInstance.texture) 152 | { 153 | // Texture A 154 | this.material.uniforms.uTerrainATexture.value = aChunkState.terrain.renderInstance.texture 155 | this.material.uniforms.uTerrainAOffset.value.set( 156 | aChunkState.x - aChunkState.size * 0.5, 157 | aChunkState.z - aChunkState.size * 0.5 158 | ) 159 | 160 | const chunkPositionRatioX = (playerPosition[0] - aChunkState.x + aChunkState.size * 0.5) / aChunkState.size 161 | const chunkPositionRatioZ = (playerPosition[2] - aChunkState.z + aChunkState.size * 0.5) / aChunkState.size 162 | 163 | // Texture B 164 | const bChunkSate = aChunkState.neighbours.get(chunkPositionRatioX < 0.5 ? 'w' : 'e') 165 | 166 | if(bChunkSate && bChunkSate.terrain && bChunkSate.terrain.renderInstance.texture) 167 | { 168 | this.material.uniforms.uTerrainBTexture.value = bChunkSate.terrain.renderInstance.texture 169 | this.material.uniforms.uTerrainBOffset.value.set( 170 | bChunkSate.x - bChunkSate.size * 0.5, 171 | bChunkSate.z - bChunkSate.size * 0.5 172 | ) 173 | } 174 | 175 | // Texture C 176 | const cChunkSate = aChunkState.neighbours.get(chunkPositionRatioZ < 0.5 ? 'n' : 's') 177 | 178 | if(cChunkSate && cChunkSate.terrain && cChunkSate.terrain.renderInstance.texture) 179 | { 180 | this.material.uniforms.uTerrainCTexture.value = cChunkSate.terrain.renderInstance.texture 181 | this.material.uniforms.uTerrainCOffset.value.set( 182 | cChunkSate.x - cChunkSate.size * 0.5, 183 | cChunkSate.z - cChunkSate.size * 0.5 184 | ) 185 | } 186 | 187 | // Texture D 188 | const dChunkSate = bChunkSate.neighbours.get(chunkPositionRatioZ < 0.5 ? 'n' : 's') 189 | 190 | if(dChunkSate && dChunkSate.terrain && dChunkSate.terrain.renderInstance.texture) 191 | { 192 | this.material.uniforms.uTerrainDTexture.value = dChunkSate.terrain.renderInstance.texture 193 | this.material.uniforms.uTerrainDOffset.value.set( 194 | dChunkSate.x - dChunkSate.size * 0.5, 195 | dChunkSate.z - dChunkSate.size * 0.5 196 | ) 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /sources/Game/View/Materials/GrassMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import vertexShader from './shaders/grass/vertex.glsl' 4 | import fragmentShader from './shaders/grass/fragment.glsl' 5 | 6 | export default function GrassMaterial() 7 | { 8 | const material = new THREE.ShaderMaterial({ 9 | uniforms: 10 | { 11 | uTime: { value: null }, 12 | uGrassDistance: { value: null }, 13 | uPlayerPosition: { value: null }, 14 | uTerrainSize: { value: null }, 15 | uTerrainTextureSize: { value: null }, 16 | uTerrainATexture: { value: null }, 17 | uTerrainAOffset: { value: null }, 18 | uTerrainBTexture: { value: null }, 19 | uTerrainBOffset: { value: null }, 20 | uTerrainCTexture: { value: null }, 21 | uTerrainCOffset: { value: null }, 22 | uTerrainDTexture: { value: null }, 23 | uTerrainDOffset: { value: null }, 24 | uNoiseTexture: { value: null }, 25 | uFresnelOffset: { value: null }, 26 | uFresnelScale: { value: null }, 27 | uFresnelPower: { value: null }, 28 | uSunPosition: { value: null }, 29 | }, 30 | vertexShader: vertexShader, 31 | fragmentShader: fragmentShader 32 | }) 33 | 34 | return material 35 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/NoisesMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import vertexShader from './shaders/noises/vertex.glsl' 4 | import fragmentShader from './shaders/noises/fragment.glsl' 5 | 6 | export default function NoisesMaterial() 7 | { 8 | const material = new THREE.ShaderMaterial({ 9 | uniforms: 10 | { 11 | }, 12 | vertexShader: vertexShader, 13 | fragmentShader: fragmentShader 14 | }) 15 | 16 | return material 17 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/PlayerMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import vertexShader from './shaders/player/vertex.glsl' 4 | import fragmentShader from './shaders/player/fragment.glsl' 5 | 6 | export default function PlayerMaterial() 7 | { 8 | const material = new THREE.ShaderMaterial({ 9 | uniforms: 10 | { 11 | uColor: { value: null }, 12 | uLightnessSmoothness: { value: null }, 13 | uSunPosition: { value: null } 14 | }, 15 | vertexShader: vertexShader, 16 | fragmentShader: fragmentShader 17 | }) 18 | 19 | return material 20 | } 21 | -------------------------------------------------------------------------------- /sources/Game/View/Materials/SkyBackgroundMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import vertexShader from './shaders/skyBackground/vertex.glsl' 4 | import fragmentShader from './shaders/skyBackground/fragment.glsl' 5 | 6 | export default function SkyBackgroundMaterial() 7 | { 8 | const material = new THREE.ShaderMaterial({ 9 | uniforms: 10 | { 11 | uTexture: { value: null } 12 | }, 13 | vertexShader: vertexShader, 14 | fragmentShader: fragmentShader 15 | }) 16 | 17 | return material 18 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/SkySphereMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import vertexShader from './shaders/skySphere/vertex.glsl' 4 | import fragmentShader from './shaders/skySphere/fragment.glsl' 5 | 6 | export default function SkySphereMaterial() 7 | { 8 | const material = new THREE.ShaderMaterial({ 9 | uniforms: 10 | { 11 | uSunPosition: { value: new THREE.Vector3() }, 12 | uAtmosphereElevation: { value: 0.5 }, 13 | uAtmospherePower: { value: 10 }, 14 | uColorDayCycleLow: { value: new THREE.Color() }, 15 | uColorDayCycleHigh: { value: new THREE.Color() }, 16 | uColorNightLow: { value: new THREE.Color() }, 17 | uColorNightHigh: { value: new THREE.Color() }, 18 | uDawnAngleAmplitude: { value: 1 }, 19 | uDawnElevationAmplitude: { value: 0.2 }, 20 | uColorDawn: { value: new THREE.Color() }, 21 | uSunAmplitude: { value: 0.75 }, 22 | uSunMultiplier: { value: 1 }, 23 | uColorSun: { value: new THREE.Color() }, 24 | uDayCycleProgress: { value: 0 } 25 | }, 26 | vertexShader: vertexShader, 27 | fragmentShader: fragmentShader 28 | }) 29 | 30 | return material 31 | } 32 | -------------------------------------------------------------------------------- /sources/Game/View/Materials/StarsMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import vertexShader from './shaders/stars/vertex.glsl' 4 | import fragmentShader from './shaders/stars/fragment.glsl' 5 | 6 | export default function StarsMaterial() 7 | { 8 | const material = new THREE.ShaderMaterial({ 9 | uniforms: 10 | { 11 | uSunPosition: { value: new THREE.Vector3() }, 12 | uSize: { value: 0.01 }, 13 | uBrightness: { value: 0.5 }, 14 | uHeightFragments: { value: null } 15 | }, 16 | vertexShader: vertexShader, 17 | fragmentShader: fragmentShader 18 | }) 19 | 20 | return material 21 | } 22 | -------------------------------------------------------------------------------- /sources/Game/View/Materials/TerrainMaterial.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import vertexShader from './shaders/terrain/vertex.glsl' 4 | import fragmentShader from './shaders/terrain/fragment.glsl' 5 | 6 | export default function TerrainMaterial() 7 | { 8 | const material = new THREE.ShaderMaterial({ 9 | uniforms: 10 | { 11 | uPlayerPosition: { value: null }, 12 | uGradientTexture: { value: null }, 13 | uLightnessSmoothness: { value: null }, 14 | uFresnelOffset: { value: null }, 15 | uFresnelScale: { value: null }, 16 | uFresnelPower: { value: null }, 17 | uSunPosition: { value: null }, 18 | uFogTexture: { value: null }, 19 | uGrassDistance: { value: null }, 20 | uTexture: { value: null } 21 | }, 22 | vertexShader: vertexShader, 23 | fragmentShader: fragmentShader 24 | }) 25 | 26 | return material 27 | } 28 | -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/grass/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vColor; 2 | 3 | void main() 4 | { 5 | gl_FragColor = vec4(vColor, 1.0); 6 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/grass/vertex.glsl: -------------------------------------------------------------------------------- 1 | #define M_PI 3.1415926535897932384626433832795 2 | 3 | uniform float uTime; 4 | uniform float uGrassDistance; 5 | uniform vec3 uPlayerPosition; 6 | uniform float uTerrainSize; 7 | uniform float uTerrainTextureSize; 8 | uniform sampler2D uTerrainATexture; 9 | uniform vec2 uTerrainAOffset; 10 | uniform sampler2D uTerrainBTexture; 11 | uniform vec2 uTerrainBOffset; 12 | uniform sampler2D uTerrainCTexture; 13 | uniform vec2 uTerrainCOffset; 14 | uniform sampler2D uTerrainDTexture; 15 | uniform vec2 uTerrainDOffset; 16 | uniform sampler2D uNoiseTexture; 17 | uniform float uFresnelOffset; 18 | uniform float uFresnelScale; 19 | uniform float uFresnelPower; 20 | uniform vec3 uSunPosition; 21 | 22 | attribute vec2 center; 23 | // attribute float tipness; 24 | 25 | varying vec3 vColor; 26 | 27 | #include ../partials/inverseLerp.glsl 28 | #include ../partials/remap.glsl 29 | #include ../partials/getSunShade.glsl; 30 | #include ../partials/getSunShadeColor.glsl; 31 | #include ../partials/getSunReflection.glsl; 32 | #include ../partials/getSunReflectionColor.glsl; 33 | #include ../partials/getGrassAttenuation.glsl; 34 | #include ../partials/getRotatePivot2d.glsl; 35 | 36 | void main() 37 | { 38 | // Recalculate center and keep around player 39 | vec2 newCenter = center; 40 | newCenter -= uPlayerPosition.xz; 41 | float halfSize = uGrassDistance * 0.5; 42 | newCenter.x = mod(newCenter.x + halfSize, uGrassDistance) - halfSize; 43 | newCenter.y = mod(newCenter.y + halfSize, uGrassDistance) - halfSize; // Y considered as Z 44 | vec4 modelCenter = modelMatrix * vec4(newCenter.x, 0.0, newCenter.y, 1.0); 45 | 46 | // Move grass to center 47 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 48 | modelPosition.xz += newCenter; // Y considered as Z 49 | 50 | // Rotate blade to face camera 51 | float angleToCamera = atan(modelCenter.x - cameraPosition.x, modelCenter.z - cameraPosition.z); 52 | modelPosition.xz = getRotatePivot2d(modelPosition.xz, angleToCamera, modelCenter.xz); 53 | 54 | // Terrains data 55 | vec2 terrainAUv = (modelPosition.xz - uTerrainAOffset.xy) / uTerrainSize; 56 | vec2 terrainBUv = (modelPosition.xz - uTerrainBOffset.xy) / uTerrainSize; 57 | vec2 terrainCUv = (modelPosition.xz - uTerrainCOffset.xy) / uTerrainSize; 58 | vec2 terrainDUv = (modelPosition.xz - uTerrainDOffset.xy) / uTerrainSize; 59 | 60 | float fragmentSize = 1.0 / uTerrainTextureSize; 61 | vec4 terrainAColor = texture2D(uTerrainATexture, terrainAUv * (1.0 - fragmentSize) + fragmentSize * 0.5); 62 | vec4 terrainBColor = texture2D(uTerrainBTexture, terrainBUv * (1.0 - fragmentSize) + fragmentSize * 0.5); 63 | vec4 terrainCColor = texture2D(uTerrainCTexture, terrainCUv * (1.0 - fragmentSize) + fragmentSize * 0.5); 64 | vec4 terrainDColor = texture2D(uTerrainDTexture, terrainDUv * (1.0 - fragmentSize) + fragmentSize * 0.5); 65 | 66 | vec4 terrainData = vec4(0); 67 | terrainData += step(0.0, terrainAUv.x) * step(terrainAUv.x, 1.0) * step(0.0, terrainAUv.y) * step(terrainAUv.y, 1.0) * terrainAColor; 68 | terrainData += step(0.0, terrainBUv.x) * step(terrainBUv.x, 1.0) * step(0.0, terrainBUv.y) * step(terrainBUv.y, 1.0) * terrainBColor; 69 | terrainData += step(0.0, terrainCUv.x) * step(terrainCUv.x, 1.0) * step(0.0, terrainCUv.y) * step(terrainCUv.y, 1.0) * terrainCColor; 70 | terrainData += step(0.0, terrainDUv.x) * step(terrainDUv.x, 1.0) * step(0.0, terrainDUv.y) * step(terrainDUv.y, 1.0) * terrainDColor; 71 | 72 | vec3 normal = terrainData.rgb; 73 | 74 | modelPosition.y += terrainData.a; 75 | modelCenter.y += terrainData.a; 76 | 77 | // Slope 78 | float slope = 1.0 - abs(dot(vec3(0.0, 1.0, 0.0), normal)); 79 | 80 | // Attenuation 81 | float distanceScale = getGrassAttenuation(modelCenter.xz); 82 | float slopeScale = smoothstep(remap(slope, 0.4, 0.5, 1.0, 0.0), 0.0, 1.0); 83 | float scale = distanceScale * slopeScale; 84 | modelPosition.xyz = mix(modelCenter.xyz, modelPosition.xyz, scale); 85 | 86 | // Tipness 87 | float tipness = step(2.0, mod(float(gl_VertexID) + 1.0, 3.0)); 88 | 89 | // Wind 90 | vec2 noiseUv = modelPosition.xz * 0.02 + uTime * 0.05; 91 | vec4 noiseColor = texture2D(uNoiseTexture, noiseUv); 92 | modelPosition.x += (noiseColor.x - 0.5) * tipness * scale; 93 | modelPosition.z += (noiseColor.y - 0.5) * tipness * scale; 94 | 95 | // Final position 96 | vec4 viewPosition = viewMatrix * modelPosition; 97 | gl_Position = projectionMatrix * viewPosition; 98 | 99 | vec3 viewDirection = normalize(modelPosition.xyz - cameraPosition); 100 | // vec3 normal = vec3(0.0, 1.0, 0.0); 101 | vec3 worldNormal = normalize(mat3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz) * normal); 102 | vec3 viewNormal = normalize(normalMatrix * normal); 103 | 104 | // Grass color 105 | vec3 uGrassDefaultColor = vec3(0.52, 0.65, 0.26); 106 | vec3 uGrassShadedColor = vec3(0.52 / 1.3, 0.65 / 1.3, 0.26 / 1.3); 107 | vec3 lowColor = mix(uGrassShadedColor, uGrassDefaultColor, 1.0 - scale); // Match the terrain 108 | vec3 color = mix(lowColor, uGrassDefaultColor, tipness); 109 | 110 | // Sun shade 111 | float sunShade = getSunShade(normal); 112 | color = getSunShadeColor(color, sunShade); 113 | 114 | // Sun reflection 115 | float sunReflection = getSunReflection(viewDirection, worldNormal, viewNormal); 116 | color = getSunReflectionColor(color, sunReflection); 117 | 118 | vColor = color; 119 | // vColor = vec3(slope); 120 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/noises/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | #include ../partials/perlin3dPeriodic.glsl; 4 | 5 | void main() 6 | { 7 | float uFrequency = 8.0; 8 | 9 | float noiseR = perlin3dPeriodic(vec3(vUv * uFrequency, 123.456), vec3(uFrequency)) * 0.5 + 0.5; 10 | float noiseG = perlin3dPeriodic(vec3(vUv * uFrequency, 456.789), vec3(uFrequency)) * 0.5 + 0.5; 11 | float noiseB = perlin3dPeriodic(vec3(vUv * uFrequency, 789.123), vec3(uFrequency)) * 0.5 + 0.5; 12 | 13 | gl_FragColor = vec4(noiseR, noiseG, noiseB, 1.0); 14 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/noises/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() 4 | { 5 | gl_Position = vec4(position, 1.0); 6 | 7 | vUv = uv; 8 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/getFogColor.glsl: -------------------------------------------------------------------------------- 1 | vec3 getFogColor(vec3 baseColor, float depth, vec2 screenUv) 2 | { 3 | float uFogIntensity = 0.0025; 4 | vec3 fogColor = texture2D(uFogTexture, screenUv).rgb; 5 | 6 | float fogIntensity = 1.0 - exp(- uFogIntensity * uFogIntensity * depth * depth ); 7 | return mix(baseColor, fogColor, fogIntensity); 8 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/getGrassAttenuation.glsl: -------------------------------------------------------------------------------- 1 | float getGrassAttenuation(vec2 position) 2 | { 3 | float distanceAttenuation = distance(uPlayerPosition.xz, position) / uGrassDistance * 2.0; 4 | return 1.0 - clamp(0.0, 1.0, smoothstep(0.3, 1.0, distanceAttenuation)); 5 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/getRotatePivot2d.glsl: -------------------------------------------------------------------------------- 1 | vec2 getRotatePivot2d(vec2 uv, float rotation, vec2 pivot) 2 | { 3 | return vec2( 4 | cos(rotation) * (uv.x - pivot.x) + sin(rotation) * (uv.y - pivot.y) + pivot.x, 5 | cos(rotation) * (uv.y - pivot.y) - sin(rotation) * (uv.x - pivot.x) + pivot.y 6 | ); 7 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/getSunReflection.glsl: -------------------------------------------------------------------------------- 1 | float getSunReflection(vec3 viewDirection, vec3 worldNormal, vec3 viewNormal) 2 | { 3 | vec3 sunViewReflection = reflect(uSunPosition, viewNormal); 4 | float sunViewStrength = max(0.2, dot(sunViewReflection, viewDirection)); 5 | 6 | float fresnel = uFresnelOffset + uFresnelScale * (1.0 + dot(viewDirection, worldNormal)); 7 | float sunReflection = fresnel * sunViewStrength; 8 | sunReflection = pow(sunReflection, uFresnelPower); 9 | 10 | return sunReflection; 11 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/getSunReflectionColor.glsl: -------------------------------------------------------------------------------- 1 | vec3 getSunReflectionColor(vec3 baseColor, float sunReflection) 2 | { 3 | return mix(baseColor, vec3(1.0, 1.0, 1.0), clamp(sunReflection, 0.0, 1.0)); 4 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/getSunShade.glsl: -------------------------------------------------------------------------------- 1 | float getSunShade(vec3 normal) 2 | { 3 | float sunShade = dot(normal, - uSunPosition); 4 | sunShade = sunShade * 0.5 + 0.5; 5 | 6 | return sunShade; 7 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/getSunShadeColor.glsl: -------------------------------------------------------------------------------- 1 | vec3 getSunShadeColor(vec3 baseColor, float sunShade) 2 | { 3 | vec3 shadeColor = baseColor * vec3(0.0, 0.5, 0.7); 4 | return mix(baseColor, shadeColor, sunShade); 5 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/inverseLerp.glsl: -------------------------------------------------------------------------------- 1 | float inverseLerp(float v, float minValue, float maxValue) 2 | { 3 | return (v - minValue) / (maxValue - minValue); 4 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/perlin2d.glsl: -------------------------------------------------------------------------------- 1 | // Classic Perlin 2D Noise 2 | // by Stefan Gustavson 3 | // 4 | vec2 fade(vec2 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);} 5 | vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} 6 | 7 | float perlin2d(vec2 P){ 8 | vec4 Pi = floor(P.xyxy) + vec4(0.0, 0.0, 1.0, 1.0); 9 | vec4 Pf = fract(P.xyxy) - vec4(0.0, 0.0, 1.0, 1.0); 10 | Pi = mod(Pi, 289.0); // To avoid truncation effects in permutation 11 | vec4 ix = Pi.xzxz; 12 | vec4 iy = Pi.yyww; 13 | vec4 fx = Pf.xzxz; 14 | vec4 fy = Pf.yyww; 15 | vec4 i = permute(permute(ix) + iy); 16 | vec4 gx = 2.0 * fract(i * 0.0243902439) - 1.0; // 1/41 = 0.024... 17 | vec4 gy = abs(gx) - 0.5; 18 | vec4 tx = floor(gx + 0.5); 19 | gx = gx - tx; 20 | vec2 g00 = vec2(gx.x,gy.x); 21 | vec2 g10 = vec2(gx.y,gy.y); 22 | vec2 g01 = vec2(gx.z,gy.z); 23 | vec2 g11 = vec2(gx.w,gy.w); 24 | vec4 norm = 1.79284291400159 - 0.85373472095314 * 25 | vec4(dot(g00, g00), dot(g01, g01), dot(g10, g10), dot(g11, g11)); 26 | g00 *= norm.x; 27 | g01 *= norm.y; 28 | g10 *= norm.z; 29 | g11 *= norm.w; 30 | float n00 = dot(g00, vec2(fx.x, fy.x)); 31 | float n10 = dot(g10, vec2(fx.y, fy.y)); 32 | float n01 = dot(g01, vec2(fx.z, fy.z)); 33 | float n11 = dot(g11, vec2(fx.w, fy.w)); 34 | vec2 fade_xy = fade(Pf.xy); 35 | vec2 n_x = mix(vec2(n00, n01), vec2(n10, n11), fade_xy.x); 36 | float n_xy = mix(n_x.x, n_x.y, fade_xy.y); 37 | return 2.3 * n_xy; 38 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/perlin3dPeriodic.glsl: -------------------------------------------------------------------------------- 1 | // Classic Perlin 3D Noise 2 | // by Stefan Gustavson 3 | // 4 | vec3 mod289(vec3 x) 5 | { 6 | return x - floor(x * (1.0 / 289.0)) * 289.0; 7 | } 8 | 9 | vec4 mod289(vec4 x) 10 | { 11 | return x - floor(x * (1.0 / 289.0)) * 289.0; 12 | } 13 | 14 | vec4 permute(vec4 x) 15 | { 16 | return mod289(((x*34.0)+10.0)*x); 17 | } 18 | 19 | vec4 taylorInvSqrt(vec4 r) 20 | { 21 | return 1.79284291400159 - 0.85373472095314 * r; 22 | } 23 | 24 | vec3 fade(vec3 t) { 25 | return t*t*t*(t*(t*6.0-15.0)+10.0); 26 | } 27 | 28 | float perlin3dPeriodic(vec3 P, vec3 rep) 29 | { 30 | vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period 31 | vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period 32 | Pi0 = mod289(Pi0); 33 | Pi1 = mod289(Pi1); 34 | vec3 Pf0 = fract(P); // Fractional part for interpolation 35 | vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 36 | vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 37 | vec4 iy = vec4(Pi0.yy, Pi1.yy); 38 | vec4 iz0 = Pi0.zzzz; 39 | vec4 iz1 = Pi1.zzzz; 40 | 41 | vec4 ixy = permute(permute(ix) + iy); 42 | vec4 ixy0 = permute(ixy + iz0); 43 | vec4 ixy1 = permute(ixy + iz1); 44 | 45 | vec4 gx0 = ixy0 * (1.0 / 7.0); 46 | vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; 47 | gx0 = fract(gx0); 48 | vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); 49 | vec4 sz0 = step(gz0, vec4(0.0)); 50 | gx0 -= sz0 * (step(0.0, gx0) - 0.5); 51 | gy0 -= sz0 * (step(0.0, gy0) - 0.5); 52 | 53 | vec4 gx1 = ixy1 * (1.0 / 7.0); 54 | vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; 55 | gx1 = fract(gx1); 56 | vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); 57 | vec4 sz1 = step(gz1, vec4(0.0)); 58 | gx1 -= sz1 * (step(0.0, gx1) - 0.5); 59 | gy1 -= sz1 * (step(0.0, gy1) - 0.5); 60 | 61 | vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); 62 | vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); 63 | vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); 64 | vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); 65 | vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); 66 | vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); 67 | vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); 68 | vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); 69 | 70 | vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); 71 | g000 *= norm0.x; 72 | g010 *= norm0.y; 73 | g100 *= norm0.z; 74 | g110 *= norm0.w; 75 | vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); 76 | g001 *= norm1.x; 77 | g011 *= norm1.y; 78 | g101 *= norm1.z; 79 | g111 *= norm1.w; 80 | 81 | float n000 = dot(g000, Pf0); 82 | float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); 83 | float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); 84 | float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); 85 | float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); 86 | float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); 87 | float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); 88 | float n111 = dot(g111, Pf1); 89 | 90 | vec3 fade_xyz = fade(Pf0); 91 | vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); 92 | vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); 93 | float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); 94 | return 2.2 * n_xyz; 95 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/perlin4d.glsl: -------------------------------------------------------------------------------- 1 | // Classic Perlin 3D Noise 2 | // by Stefan Gustavson 3 | // 4 | vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} 5 | vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} 6 | vec4 fade(vec4 t) {return t*t*t*(t*(t*6.0-15.0)+10.0);} 7 | 8 | float perlin4d(vec4 P){ 9 | vec4 Pi0 = floor(P); // Integer part for indexing 10 | vec4 Pi1 = Pi0 + 1.0; // Integer part + 1 11 | Pi0 = mod(Pi0, 289.0); 12 | Pi1 = mod(Pi1, 289.0); 13 | vec4 Pf0 = fract(P); // Fractional part for interpolation 14 | vec4 Pf1 = Pf0 - 1.0; // Fractional part - 1.0 15 | vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); 16 | vec4 iy = vec4(Pi0.yy, Pi1.yy); 17 | vec4 iz0 = vec4(Pi0.zzzz); 18 | vec4 iz1 = vec4(Pi1.zzzz); 19 | vec4 iw0 = vec4(Pi0.wwww); 20 | vec4 iw1 = vec4(Pi1.wwww); 21 | 22 | vec4 ixy = permute(permute(ix) + iy); 23 | vec4 ixy0 = permute(ixy + iz0); 24 | vec4 ixy1 = permute(ixy + iz1); 25 | vec4 ixy00 = permute(ixy0 + iw0); 26 | vec4 ixy01 = permute(ixy0 + iw1); 27 | vec4 ixy10 = permute(ixy1 + iw0); 28 | vec4 ixy11 = permute(ixy1 + iw1); 29 | 30 | vec4 gx00 = ixy00 / 7.0; 31 | vec4 gy00 = floor(gx00) / 7.0; 32 | vec4 gz00 = floor(gy00) / 6.0; 33 | gx00 = fract(gx00) - 0.5; 34 | gy00 = fract(gy00) - 0.5; 35 | gz00 = fract(gz00) - 0.5; 36 | vec4 gw00 = vec4(0.75) - abs(gx00) - abs(gy00) - abs(gz00); 37 | vec4 sw00 = step(gw00, vec4(0.0)); 38 | gx00 -= sw00 * (step(0.0, gx00) - 0.5); 39 | gy00 -= sw00 * (step(0.0, gy00) - 0.5); 40 | 41 | vec4 gx01 = ixy01 / 7.0; 42 | vec4 gy01 = floor(gx01) / 7.0; 43 | vec4 gz01 = floor(gy01) / 6.0; 44 | gx01 = fract(gx01) - 0.5; 45 | gy01 = fract(gy01) - 0.5; 46 | gz01 = fract(gz01) - 0.5; 47 | vec4 gw01 = vec4(0.75) - abs(gx01) - abs(gy01) - abs(gz01); 48 | vec4 sw01 = step(gw01, vec4(0.0)); 49 | gx01 -= sw01 * (step(0.0, gx01) - 0.5); 50 | gy01 -= sw01 * (step(0.0, gy01) - 0.5); 51 | 52 | vec4 gx10 = ixy10 / 7.0; 53 | vec4 gy10 = floor(gx10) / 7.0; 54 | vec4 gz10 = floor(gy10) / 6.0; 55 | gx10 = fract(gx10) - 0.5; 56 | gy10 = fract(gy10) - 0.5; 57 | gz10 = fract(gz10) - 0.5; 58 | vec4 gw10 = vec4(0.75) - abs(gx10) - abs(gy10) - abs(gz10); 59 | vec4 sw10 = step(gw10, vec4(0.0)); 60 | gx10 -= sw10 * (step(0.0, gx10) - 0.5); 61 | gy10 -= sw10 * (step(0.0, gy10) - 0.5); 62 | 63 | vec4 gx11 = ixy11 / 7.0; 64 | vec4 gy11 = floor(gx11) / 7.0; 65 | vec4 gz11 = floor(gy11) / 6.0; 66 | gx11 = fract(gx11) - 0.5; 67 | gy11 = fract(gy11) - 0.5; 68 | gz11 = fract(gz11) - 0.5; 69 | vec4 gw11 = vec4(0.75) - abs(gx11) - abs(gy11) - abs(gz11); 70 | vec4 sw11 = step(gw11, vec4(0.0)); 71 | gx11 -= sw11 * (step(0.0, gx11) - 0.5); 72 | gy11 -= sw11 * (step(0.0, gy11) - 0.5); 73 | 74 | vec4 g0000 = vec4(gx00.x,gy00.x,gz00.x,gw00.x); 75 | vec4 g1000 = vec4(gx00.y,gy00.y,gz00.y,gw00.y); 76 | vec4 g0100 = vec4(gx00.z,gy00.z,gz00.z,gw00.z); 77 | vec4 g1100 = vec4(gx00.w,gy00.w,gz00.w,gw00.w); 78 | vec4 g0010 = vec4(gx10.x,gy10.x,gz10.x,gw10.x); 79 | vec4 g1010 = vec4(gx10.y,gy10.y,gz10.y,gw10.y); 80 | vec4 g0110 = vec4(gx10.z,gy10.z,gz10.z,gw10.z); 81 | vec4 g1110 = vec4(gx10.w,gy10.w,gz10.w,gw10.w); 82 | vec4 g0001 = vec4(gx01.x,gy01.x,gz01.x,gw01.x); 83 | vec4 g1001 = vec4(gx01.y,gy01.y,gz01.y,gw01.y); 84 | vec4 g0101 = vec4(gx01.z,gy01.z,gz01.z,gw01.z); 85 | vec4 g1101 = vec4(gx01.w,gy01.w,gz01.w,gw01.w); 86 | vec4 g0011 = vec4(gx11.x,gy11.x,gz11.x,gw11.x); 87 | vec4 g1011 = vec4(gx11.y,gy11.y,gz11.y,gw11.y); 88 | vec4 g0111 = vec4(gx11.z,gy11.z,gz11.z,gw11.z); 89 | vec4 g1111 = vec4(gx11.w,gy11.w,gz11.w,gw11.w); 90 | 91 | vec4 norm00 = taylorInvSqrt(vec4(dot(g0000, g0000), dot(g0100, g0100), dot(g1000, g1000), dot(g1100, g1100))); 92 | g0000 *= norm00.x; 93 | g0100 *= norm00.y; 94 | g1000 *= norm00.z; 95 | g1100 *= norm00.w; 96 | 97 | vec4 norm01 = taylorInvSqrt(vec4(dot(g0001, g0001), dot(g0101, g0101), dot(g1001, g1001), dot(g1101, g1101))); 98 | g0001 *= norm01.x; 99 | g0101 *= norm01.y; 100 | g1001 *= norm01.z; 101 | g1101 *= norm01.w; 102 | 103 | vec4 norm10 = taylorInvSqrt(vec4(dot(g0010, g0010), dot(g0110, g0110), dot(g1010, g1010), dot(g1110, g1110))); 104 | g0010 *= norm10.x; 105 | g0110 *= norm10.y; 106 | g1010 *= norm10.z; 107 | g1110 *= norm10.w; 108 | 109 | vec4 norm11 = taylorInvSqrt(vec4(dot(g0011, g0011), dot(g0111, g0111), dot(g1011, g1011), dot(g1111, g1111))); 110 | g0011 *= norm11.x; 111 | g0111 *= norm11.y; 112 | g1011 *= norm11.z; 113 | g1111 *= norm11.w; 114 | 115 | float n0000 = dot(g0000, Pf0); 116 | float n1000 = dot(g1000, vec4(Pf1.x, Pf0.yzw)); 117 | float n0100 = dot(g0100, vec4(Pf0.x, Pf1.y, Pf0.zw)); 118 | float n1100 = dot(g1100, vec4(Pf1.xy, Pf0.zw)); 119 | float n0010 = dot(g0010, vec4(Pf0.xy, Pf1.z, Pf0.w)); 120 | float n1010 = dot(g1010, vec4(Pf1.x, Pf0.y, Pf1.z, Pf0.w)); 121 | float n0110 = dot(g0110, vec4(Pf0.x, Pf1.yz, Pf0.w)); 122 | float n1110 = dot(g1110, vec4(Pf1.xyz, Pf0.w)); 123 | float n0001 = dot(g0001, vec4(Pf0.xyz, Pf1.w)); 124 | float n1001 = dot(g1001, vec4(Pf1.x, Pf0.yz, Pf1.w)); 125 | float n0101 = dot(g0101, vec4(Pf0.x, Pf1.y, Pf0.z, Pf1.w)); 126 | float n1101 = dot(g1101, vec4(Pf1.xy, Pf0.z, Pf1.w)); 127 | float n0011 = dot(g0011, vec4(Pf0.xy, Pf1.zw)); 128 | float n1011 = dot(g1011, vec4(Pf1.x, Pf0.y, Pf1.zw)); 129 | float n0111 = dot(g0111, vec4(Pf0.x, Pf1.yzw)); 130 | float n1111 = dot(g1111, Pf1); 131 | 132 | vec4 fade_xyzw = fade(Pf0); 133 | vec4 n_0w = mix(vec4(n0000, n1000, n0100, n1100), vec4(n0001, n1001, n0101, n1101), fade_xyzw.w); 134 | vec4 n_1w = mix(vec4(n0010, n1010, n0110, n1110), vec4(n0011, n1011, n0111, n1111), fade_xyzw.w); 135 | vec4 n_zw = mix(n_0w, n_1w, fade_xyzw.z); 136 | vec2 n_yzw = mix(n_zw.xy, n_zw.zw, fade_xyzw.y); 137 | float n_xyzw = mix(n_yzw.x, n_yzw.y, fade_xyzw.x); 138 | return 2.2 * n_xyzw; 139 | } 140 | -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/partials/remap.glsl: -------------------------------------------------------------------------------- 1 | float remap(float v, float inMin, float inMax, float outMin, float outMax) 2 | { 3 | float t = inverseLerp(v, inMin, inMax); 4 | return mix(outMin, outMax, t); 5 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/player/fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform vec3 uSunPosition; 2 | uniform vec3 uColor; 3 | 4 | varying vec3 vGameNormal; 5 | 6 | #include ../partials/getSunShade.glsl; 7 | #include ../partials/getSunShadeColor.glsl; 8 | 9 | void main() 10 | { 11 | vec3 color = uColor; 12 | 13 | float sunShade = getSunShade(vGameNormal); 14 | color = getSunShadeColor(color, sunShade); 15 | 16 | gl_FragColor = vec4(color, 1.0); 17 | // gl_FragColor = vec4(vColor, 1.0); 18 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/player/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vGameNormal; 2 | 3 | void main() 4 | { 5 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 6 | vec4 viewPosition = viewMatrix * modelPosition; 7 | gl_Position = projectionMatrix * viewPosition; 8 | 9 | vec3 worldNormal = normalize(mat3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz) * normal); 10 | 11 | // Sun shade 12 | vGameNormal = worldNormal; 13 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/skyBackground/fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D uTexture; 2 | 3 | varying vec2 vUv; 4 | 5 | void main() 6 | { 7 | vec3 color = texture2D(uTexture, vUv).rgb; 8 | gl_FragColor = vec4(color, 1.0); 9 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/skyBackground/vertex.glsl: -------------------------------------------------------------------------------- 1 | varying vec2 vUv; 2 | 3 | void main() 4 | { 5 | gl_Position = vec4(position, 1.0); 6 | 7 | vUv = uv; 8 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/skySphere/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vColor; 2 | 3 | void main() 4 | { 5 | gl_FragColor = vec4(vColor, 1.0); 6 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/skySphere/vertex.glsl: -------------------------------------------------------------------------------- 1 | #define M_PI 3.1415926535897932384626433832795 2 | 3 | uniform vec3 uSunPosition; 4 | 5 | uniform float uAtmosphereElevation; 6 | uniform float uAtmospherePower; 7 | uniform vec3 uColorDayCycleLow; 8 | uniform vec3 uColorDayCycleHigh; 9 | uniform vec3 uColorNightLow; 10 | uniform vec3 uColorNightHigh; 11 | 12 | uniform float uDawnAngleAmplitude; 13 | uniform float uDawnElevationAmplitude; 14 | uniform vec3 uColorDawn; 15 | 16 | uniform float uSunAmplitude; 17 | uniform float uSunMultiplier; 18 | uniform vec3 uColorSun; 19 | 20 | uniform float uDayCycleProgress; 21 | 22 | varying vec3 vColor; 23 | 24 | vec3 blendAdd(vec3 base, vec3 blend) 25 | { 26 | return min(base + blend, vec3(1.0)); 27 | } 28 | 29 | vec3 blendAdd(vec3 base, vec3 blend, float opacity) 30 | { 31 | return (blendAdd(base, blend) * opacity + base * (1.0 - opacity)); 32 | } 33 | 34 | void main() 35 | { 36 | // Vertex position 37 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 38 | gl_Position = projectionMatrix * viewMatrix * modelPosition; 39 | 40 | vec3 normalizedPosition = normalize(position); 41 | 42 | /** 43 | * Sky and atmosphere 44 | */ 45 | // Horizon intensity 46 | float horizonIntensity = (uv.y - 0.5) / uAtmosphereElevation; 47 | horizonIntensity = pow(1.0 - horizonIntensity, uAtmospherePower); 48 | 49 | // Color day 50 | vec3 colorDayCycle = mix(uColorDayCycleHigh, uColorDayCycleLow, horizonIntensity); 51 | 52 | // Color night 53 | vec3 colorNight = mix(uColorNightHigh, uColorNightLow, horizonIntensity); 54 | 55 | // Mix between day and night 56 | float dayIntensity = abs(uDayCycleProgress - 0.5) * 2.0; 57 | vec3 color = mix(colorNight, colorDayCycle, dayIntensity); 58 | 59 | /** 60 | * Sun glow 61 | */ 62 | // Distance to sun 63 | float distanceToSun = distance(normalizedPosition, uSunPosition); 64 | 65 | /** 66 | * Dawn 67 | */ 68 | // Dawn angle intensity 69 | float dawnAngleIntensity = dot(normalize(uSunPosition.xz), normalize(normalizedPosition.xz)); 70 | dawnAngleIntensity = smoothstep(0.0, 1.0, (dawnAngleIntensity - (1.0 - uDawnAngleAmplitude)) / uDawnAngleAmplitude); 71 | 72 | // Dawn elevation intensity 73 | float dawnElevationIntensity = 1.0 - min(1.0, (uv.y - 0.5) / uDawnElevationAmplitude); 74 | 75 | // Dawn day progress intensity 76 | float dawnDayCycleIntensity = cos(uDayCycleProgress * 4.0 * M_PI + M_PI) * 0.5 + 0.5; 77 | 78 | // Final dawn intensity and color 79 | float dawnIntensity = clamp(dawnAngleIntensity * dawnElevationIntensity * dawnDayCycleIntensity, 0.0, 1.0); 80 | color = blendAdd(color, uColorDawn, dawnIntensity); 81 | 82 | /** 83 | * Sun glow 84 | */ 85 | // Sun intensity 86 | float sunIntensity = smoothstep(0.0, 1.0, clamp(1.0 - distanceToSun / uSunAmplitude, 0.0, 1.0)) * uSunMultiplier; 87 | color = blendAdd(color, uColorSun, sunIntensity); 88 | 89 | float sunGlowStrength = pow(max(0.0, 1.0 + 0.05 - distanceToSun * 2.5), 2.0); 90 | color = blendAdd(color, uColorSun, sunGlowStrength); 91 | 92 | vColor = vec3(color); 93 | // vColor = vec3(sunIntensity); 94 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/stars/fragment.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vColor; 2 | 3 | void main() 4 | { 5 | gl_FragColor = vec4(vColor, 1.0); 6 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/stars/vertex.glsl: -------------------------------------------------------------------------------- 1 | #define M_PI 3.1415926535897932384626433832795 2 | 3 | uniform vec3 uSunPosition; 4 | uniform float uSize; 5 | uniform float uBrightness; 6 | uniform float uHeightFragments; 7 | 8 | attribute float aSize; 9 | attribute vec3 aColor; 10 | 11 | varying vec3 vColor; 12 | 13 | void main() 14 | { 15 | // Vertex position 16 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 17 | gl_Position = projectionMatrix * viewMatrix * modelPosition; 18 | 19 | // Sun size multiplier 20 | vec3 normalizedPosition = normalize(modelPosition.xyz); 21 | float sunSizeMultiplier = 1.0 - (dot(normalize(uSunPosition), normalizedPosition) * 0.5 + 0.5); 22 | // sunSizeMultiplier = smoothstep(0.1, 1.0, sunSizeMultiplier); 23 | 24 | gl_PointSize = aSize * uSize * sunSizeMultiplier * uHeightFragments; 25 | 26 | // Clip out if too small 27 | if(gl_PointSize < 0.5) 28 | gl_Position = vec4(2.0, 2.0, 2.0, 1.0); 29 | 30 | vColor = mix(aColor, vec3(1.0), uBrightness); 31 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/terrain/fragment.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D uGradientTexture; 2 | 3 | varying vec3 vColor; 4 | 5 | void main() 6 | { 7 | vec3 color = vColor; 8 | 9 | gl_FragColor = vec4(color, 1.0); 10 | // gl_FragColor = vec4(vColor, 1.0); 11 | } -------------------------------------------------------------------------------- /sources/Game/View/Materials/shaders/terrain/vertex.glsl: -------------------------------------------------------------------------------- 1 | uniform vec3 uPlayerPosition; 2 | uniform float uLightnessSmoothness; 3 | uniform float uFresnelOffset; 4 | uniform float uFresnelScale; 5 | uniform float uFresnelPower; 6 | uniform vec3 uSunPosition; 7 | uniform float uGrassDistance; 8 | uniform sampler2D uTexture; 9 | uniform sampler2D uFogTexture; 10 | 11 | varying vec3 vColor; 12 | 13 | #include ../partials/inverseLerp.glsl 14 | #include ../partials/remap.glsl 15 | #include ../partials/getSunShade.glsl; 16 | #include ../partials/getSunShadeColor.glsl; 17 | #include ../partials/getSunReflection.glsl; 18 | #include ../partials/getSunReflectionColor.glsl; 19 | #include ../partials/getFogColor.glsl; 20 | #include ../partials/getGrassAttenuation.glsl; 21 | 22 | void main() 23 | { 24 | vec4 modelPosition = modelMatrix * vec4(position, 1.0); 25 | vec4 viewPosition = viewMatrix * modelPosition; 26 | float depth = - viewPosition.z; 27 | gl_Position = projectionMatrix * viewPosition; 28 | 29 | // Terrain data 30 | vec4 terrainData = texture2D(uTexture, uv); 31 | vec3 normal = terrainData.rgb; 32 | 33 | // Slope 34 | float slope = 1.0 - abs(dot(vec3(0.0, 1.0, 0.0), normal)); 35 | 36 | vec3 viewDirection = normalize(modelPosition.xyz - cameraPosition); 37 | vec3 worldNormal = normalize(mat3(modelMatrix[0].xyz, modelMatrix[1].xyz, modelMatrix[2].xyz) * normal); 38 | vec3 viewNormal = normalize(normalMatrix * normal); 39 | 40 | // Color 41 | vec3 uGrassDefaultColor = vec3(0.52, 0.65, 0.26); 42 | vec3 uGrassShadedColor = vec3(0.52 / 1.3, 0.65 / 1.3, 0.26 / 1.3); 43 | 44 | // Grass distance attenuation 45 | // Terrain must match the bottom of the grass which is darker 46 | float grassDistanceAttenuation = getGrassAttenuation(modelPosition.xz); 47 | float grassSlopeAttenuation = smoothstep(remap(slope, 0.4, 0.5, 1.0, 0.0), 0.0, 1.0); 48 | float grassAttenuation = grassDistanceAttenuation * grassSlopeAttenuation; 49 | vec3 grassColor = mix(uGrassShadedColor, uGrassDefaultColor, 1.0 - grassAttenuation); 50 | 51 | vec3 color = grassColor; 52 | 53 | // Sun shade 54 | float sunShade = getSunShade(normal); 55 | color = getSunShadeColor(color, sunShade); 56 | 57 | // Sun reflection 58 | float sunReflection = getSunReflection(viewDirection, worldNormal, viewNormal); 59 | color = getSunReflectionColor(color, sunReflection); 60 | 61 | // Fog 62 | vec2 screenUv = (gl_Position.xy / gl_Position.w * 0.5) + 0.5; 63 | color = getFogColor(color, depth, screenUv); 64 | 65 | // vec3 dirtColor = vec3(0.3, 0.2, 0.1); 66 | // vec3 color = mix(dirtColor, grassColor, terrainData.g); 67 | 68 | // Varyings 69 | vColor = color; 70 | } -------------------------------------------------------------------------------- /sources/Game/View/Noises.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import View from '@/View/View.js' 4 | import NoisesMaterial from './Materials/NoisesMaterial.js' 5 | 6 | export default class Noises 7 | { 8 | constructor() 9 | { 10 | this.view = View.getInstance() 11 | this.renderer = this.view.renderer 12 | this.scene = this.view.scene 13 | 14 | this.setCustomRender() 15 | this.setMaterial() 16 | this.setPlane() 17 | // this.setHelper() 18 | 19 | // const texture = this.createNoise(128, 128) 20 | } 21 | 22 | setCustomRender() 23 | { 24 | this.customRender = {} 25 | this.customRender.scene = new THREE.Scene() 26 | this.customRender.camera = new THREE.OrthographicCamera(- 1, 1, 1, - 1, 0.1, 10) 27 | } 28 | 29 | setMaterial() 30 | { 31 | this.material = new NoisesMaterial() 32 | } 33 | 34 | setPlane() 35 | { 36 | this.plane = new THREE.Mesh( 37 | new THREE.PlaneGeometry(2, 2), 38 | this.material 39 | ) 40 | this.plane.frustumCulled = false 41 | this.customRender.scene.add(this.plane) 42 | } 43 | 44 | setHelper() 45 | { 46 | this.helper = {} 47 | this.helper.geometry = new THREE.PlaneGeometry(1, 1) 48 | this.helper.material = new THREE.MeshBasicMaterial() 49 | 50 | const meshA = new THREE.Mesh( 51 | this.helper.geometry, 52 | this.helper.material 53 | ) 54 | meshA.position.y = 5 + 1 55 | meshA.position.x = - 1 56 | meshA.scale.set(2, 2, 2) 57 | 58 | const meshB = new THREE.Mesh( 59 | this.helper.geometry, 60 | this.helper.material 61 | ) 62 | meshB.position.y = 5 + 1 63 | meshB.position.x = 1 64 | meshB.scale.set(2, 2, 2) 65 | 66 | const meshC = new THREE.Mesh( 67 | this.helper.geometry, 68 | this.helper.material 69 | ) 70 | meshC.position.y = 5 - 1 71 | meshC.position.x = - 1 72 | meshC.scale.set(2, 2, 2) 73 | 74 | const meshD = new THREE.Mesh( 75 | this.helper.geometry, 76 | this.helper.material 77 | ) 78 | meshD.position.y = 5 - 1 79 | meshD.position.x = 1 80 | meshD.scale.set(2, 2, 2) 81 | 82 | window.requestAnimationFrame(() => 83 | { 84 | this.scene.add(meshA) 85 | // this.scene.add(meshB) 86 | // this.scene.add(meshC) 87 | // this.scene.add(meshD) 88 | }) 89 | } 90 | 91 | create(width, height) 92 | { 93 | const renderTarget = new THREE.WebGLRenderTarget( 94 | width, 95 | height, 96 | { 97 | generateMipmaps: false, 98 | wrapS: THREE.RepeatWrapping, 99 | wrapT: THREE.RepeatWrapping 100 | } 101 | ) 102 | 103 | this.renderer.instance.setRenderTarget(renderTarget) 104 | this.renderer.instance.render(this.customRender.scene, this.customRender.camera) 105 | this.renderer.instance.setRenderTarget(null) 106 | 107 | const texture = renderTarget.texture 108 | // texture.wrapS = THREE.RepeatWrapping 109 | // texture.wrapT = THREE.RepeatWrapping 110 | 111 | if(this.helper) 112 | this.helper.material.map = texture 113 | 114 | return texture 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sources/Game/View/Player.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import Game from '@/Game.js' 4 | import View from '@/View/View.js' 5 | import Debug from '@/Debug/Debug.js' 6 | import State from '@/State/State.js' 7 | import PlayerMaterial from './Materials/PlayerMaterial.js' 8 | 9 | export default class Player 10 | { 11 | constructor() 12 | { 13 | this.game = Game.getInstance() 14 | this.state = State.getInstance() 15 | this.view = View.getInstance() 16 | this.debug = Debug.getInstance() 17 | 18 | this.scene = this.view.scene 19 | 20 | this.setGroup() 21 | this.setHelper() 22 | this.setDebug() 23 | } 24 | 25 | setGroup() 26 | { 27 | this.group = new THREE.Group() 28 | this.scene.add(this.group) 29 | } 30 | 31 | setHelper() 32 | { 33 | this.helper = new THREE.Mesh() 34 | this.helper.material = new PlayerMaterial() 35 | this.helper.material.uniforms.uColor.value = new THREE.Color('#fff8d6') 36 | this.helper.material.uniforms.uSunPosition.value = new THREE.Vector3(- 0.5, - 0.5, - 0.5) 37 | 38 | this.helper.geometry = new THREE.CapsuleGeometry(0.5, 0.8, 3, 16), 39 | this.helper.geometry.translate(0, 0.9, 0) 40 | this.group.add(this.helper) 41 | 42 | // const arrow = new THREE.Mesh( 43 | // new THREE.ConeGeometry(0.2, 0.2, 4), 44 | // new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: false }) 45 | // ) 46 | // arrow.rotation.x = - Math.PI * 0.5 47 | // arrow.position.y = 1.5 48 | // arrow.position.z = - 0.5 49 | // this.helper.add(arrow) 50 | 51 | // // Axis helper 52 | // this.axisHelper = new THREE.AxesHelper(3) 53 | // this.group.add(this.axisHelper) 54 | } 55 | 56 | setDebug() 57 | { 58 | if(!this.debug.active) 59 | return 60 | 61 | // Sphere 62 | const playerFolder = this.debug.ui.getFolder('view/player') 63 | 64 | playerFolder.addColor(this.helper.material.uniforms.uColor, 'value') 65 | } 66 | 67 | 68 | update() 69 | { 70 | const playerState = this.state.player 71 | const sunState = this.state.sun 72 | 73 | this.group.position.set( 74 | playerState.position.current[0], 75 | playerState.position.current[1], 76 | playerState.position.current[2] 77 | ) 78 | 79 | // Helper 80 | this.helper.rotation.y = playerState.rotation 81 | this.helper.material.uniforms.uSunPosition.value.set(sunState.position.x, sunState.position.y, sunState.position.z) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /sources/Game/View/Renderer.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import Game from '@/Game.js' 4 | import View from '@/View/View.js' 5 | import Debug from '@/Debug/Debug.js' 6 | import State from '@/State/State.js' 7 | 8 | export default class Renderer 9 | { 10 | constructor(_options = {}) 11 | { 12 | this.game = Game.getInstance() 13 | this.view = View.getInstance() 14 | this.state = State.getInstance() 15 | this.debug = Debug.getInstance() 16 | 17 | this.scene = this.view.scene 18 | this.domElement = this.game.domElement 19 | this.viewport = this.state.viewport 20 | this.time = this.state.time 21 | this.camera = this.view.camera 22 | 23 | this.setInstance() 24 | } 25 | 26 | setInstance() 27 | { 28 | this.clearColor = '#222222' 29 | 30 | // Renderer 31 | this.instance = new THREE.WebGLRenderer({ 32 | alpha: false, 33 | antialias: true 34 | }) 35 | 36 | this.instance.sortObjects = false 37 | this.instance.domElement.style.position = 'absolute' 38 | this.instance.domElement.style.top = 0 39 | this.instance.domElement.style.left = 0 40 | this.instance.domElement.style.width = '100%' 41 | this.instance.domElement.style.height = '100%' 42 | 43 | // this.instance.setClearColor(0x414141, 1) 44 | this.instance.setClearColor(this.clearColor, 1) 45 | this.instance.setSize(this.viewport.width, this.viewport.height) 46 | this.instance.setPixelRatio(this.viewport.clampedPixelRatio) 47 | 48 | // this.instance.physicallyCorrectLights = true 49 | // this.instance.gammaOutPut = true 50 | // this.instance.outputEncoding = THREE.sRGBEncoding 51 | // this.instance.shadowMap.type = THREE.PCFSoftShadowMap 52 | // this.instance.shadowMap.enabled = false 53 | // this.instance.toneMapping = THREE.ReinhardToneMapping 54 | // this.instance.toneMapping = THREE.ReinhardToneMapping 55 | // this.instance.toneMappingExposure = 1.3 56 | 57 | this.context = this.instance.getContext() 58 | 59 | // Add stats panel 60 | if(this.debug.stats) 61 | { 62 | this.debug.stats.setRenderPanel(this.context) 63 | } 64 | } 65 | 66 | resize() 67 | { 68 | // Instance 69 | this.instance.setSize(this.viewport.width, this.viewport.height) 70 | this.instance.setPixelRatio(this.viewport.clampedPixelRatio) 71 | } 72 | 73 | update() 74 | { 75 | if(this.debug.stats) 76 | this.debug.stats.beforeRender() 77 | 78 | this.instance.render(this.scene, this.camera.instance) 79 | 80 | if(this.debug.stats) 81 | this.debug.stats.afterRender() 82 | } 83 | 84 | destroy() 85 | { 86 | this.instance.renderLists.dispose() 87 | this.instance.dispose() 88 | this.renderTarget.dispose() 89 | } 90 | } -------------------------------------------------------------------------------- /sources/Game/View/Sky.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import Game from '@/Game.js' 4 | import View from '@/View/View.js' 5 | import State from '@/State/State.js' 6 | import Debug from '@/Debug/Debug.js' 7 | import SkyBackgroundMaterial from './Materials/SkyBackgroundMaterial.js' 8 | import SkySphereMaterial from './Materials/SkySphereMaterial.js' 9 | import StarsMaterial from './Materials/StarsMaterial.js' 10 | 11 | export default class Sky 12 | { 13 | constructor() 14 | { 15 | this.game = Game.getInstance() 16 | this.view = View.getInstance() 17 | this.state = State.getInstance() 18 | this.debug = Debug.getInstance() 19 | 20 | this.viewport = this.state.viewport 21 | this.renderer = this.view.renderer 22 | this.scene = this.view.scene 23 | 24 | this.outerDistance = 1000 25 | 26 | this.group = new THREE.Group() 27 | this.scene.add(this.group) 28 | 29 | this.setCustomRender() 30 | this.setBackground() 31 | this.setSphere() 32 | this.setSun() 33 | this.setStars() 34 | this.setDebug() 35 | } 36 | 37 | setCustomRender() 38 | { 39 | this.customRender = {} 40 | this.customRender.scene = new THREE.Scene() 41 | this.customRender.camera = this.view.camera.instance.clone() 42 | this.customRender.resolutionRatio = 0.1 43 | this.customRender.renderTarget = new THREE.WebGLRenderTarget( 44 | this.viewport.width * this.customRender.resolutionRatio, 45 | this.viewport.height * this.customRender.resolutionRatio, 46 | { 47 | generateMipmaps: false 48 | } 49 | ) 50 | this.customRender.texture = this.customRender.renderTarget.texture 51 | } 52 | 53 | setBackground() 54 | { 55 | this.background = {} 56 | 57 | this.background.geometry = new THREE.PlaneGeometry(2, 2) 58 | 59 | // this.background.material = new THREE.MeshBasicMaterial({ wireframe: false, map: this.customRender.renderTarget.texture }) 60 | this.background.material = new SkyBackgroundMaterial() 61 | this.background.material.uniforms.uTexture.value = this.customRender.renderTarget.texture 62 | // this.background.material.wireframe = true 63 | this.background.material.depthTest = false 64 | this.background.material.depthWrite = false 65 | 66 | this.background.mesh = new THREE.Mesh(this.background.geometry, this.background.material) 67 | this.background.mesh.frustumCulled = false 68 | 69 | this.group.add(this.background.mesh) 70 | } 71 | 72 | setSphere() 73 | { 74 | this.sphere = {} 75 | this.sphere.widthSegments = 128 76 | this.sphere.heightSegments = 64 77 | this.sphere.update = () => 78 | { 79 | const geometry = new THREE.SphereGeometry(10, this.sphere.widthSegments, this.sphere.heightSegments) 80 | if(this.sphere.geometry) 81 | { 82 | this.sphere.geometry.dispose() 83 | this.sphere.mesh.geometry = this.sphere.geometry 84 | } 85 | 86 | this.sphere.geometry = geometry 87 | } 88 | this.sphere.material = new SkySphereMaterial() 89 | 90 | this.sphere.material.uniforms.uColorDayCycleLow.value.set('#f0fff9') 91 | this.sphere.material.uniforms.uColorDayCycleHigh.value.set('#2e89ff') 92 | this.sphere.material.uniforms.uColorNightLow.value.set('#004794') 93 | this.sphere.material.uniforms.uColorNightHigh.value.set('#001624') 94 | this.sphere.material.uniforms.uColorSun.value.set('#ff531a') 95 | this.sphere.material.uniforms.uColorDawn.value.set('#ff1900') 96 | this.sphere.material.uniforms.uDayCycleProgress.value = 0 97 | this.sphere.material.side = THREE.BackSide 98 | 99 | this.sphere.update() 100 | 101 | // this.sphere.material.wireframe = true 102 | this.sphere.mesh = new THREE.Mesh(this.sphere.geometry, this.sphere.material) 103 | this.customRender.scene.add(this.sphere.mesh) 104 | } 105 | 106 | setSun() 107 | { 108 | this.sun = {} 109 | this.sun.distance = this.outerDistance - 50 110 | 111 | const geometry = new THREE.CircleGeometry(0.02 * this.sun.distance, 32) 112 | const material = new THREE.MeshBasicMaterial({ color: 0xffffff }) 113 | this.sun.mesh = new THREE.Mesh(geometry, material) 114 | this.group.add(this.sun.mesh) 115 | } 116 | 117 | setStars() 118 | { 119 | this.stars = {} 120 | this.stars.count = 1000 121 | this.stars.distance = this.outerDistance 122 | 123 | this.stars.update = () => 124 | { 125 | // Create geometry 126 | const positionArray = new Float32Array(this.stars.count * 3) 127 | const sizeArray = new Float32Array(this.stars.count) 128 | const colorArray = new Float32Array(this.stars.count * 3) 129 | 130 | for(let i = 0; i < this.stars.count; i++) 131 | { 132 | const iStride3 = i * 3 133 | 134 | // Position 135 | const position = new THREE.Vector3() 136 | position.setFromSphericalCoords(this.stars.distance, Math.acos(Math.random()), 2 * Math.PI * Math.random()) 137 | 138 | positionArray[iStride3 ] = position.x 139 | positionArray[iStride3 + 1] = position.y 140 | positionArray[iStride3 + 2] = position.z 141 | 142 | // Size 143 | sizeArray[i] = Math.pow(Math.random() * 0.9, 10) + 0.1 144 | 145 | // Color 146 | const color = new THREE.Color() 147 | color.setHSL(Math.random(), 1, 0.5 + Math.random() * 0.5) 148 | colorArray[iStride3 ] = color.r 149 | colorArray[iStride3 + 1] = color.g 150 | colorArray[iStride3 + 2] = color.b 151 | } 152 | 153 | const geometry = new THREE.BufferGeometry() 154 | geometry.setAttribute('position', new THREE.Float32BufferAttribute(positionArray, 3)) 155 | geometry.setAttribute('aSize', new THREE.Float32BufferAttribute(sizeArray, 1)) 156 | geometry.setAttribute('aColor', new THREE.Float32BufferAttribute(colorArray, 3)) 157 | 158 | // Dispose of old one 159 | if(this.stars.geometry) 160 | { 161 | this.stars.geometry.dispose() 162 | this.stars.points.geometry = this.stars.geometry 163 | } 164 | 165 | this.stars.geometry = geometry 166 | } 167 | 168 | // Geometry 169 | this.stars.update() 170 | 171 | // Material 172 | // this.stars.material = new THREE.PointsMaterial({ size: 5, sizeAttenuation: false }) 173 | this.stars.material = new StarsMaterial() 174 | this.stars.material.uniforms.uHeightFragments.value = this.viewport.height * this.viewport.clampedPixelRatio 175 | 176 | // Points 177 | this.stars.points = new THREE.Points(this.stars.geometry, this.stars.material) 178 | this.group.add(this.stars.points) 179 | } 180 | 181 | setDebug() 182 | { 183 | if(!this.debug.active) 184 | return 185 | 186 | // Sphere 187 | const sphereGeometryFolder = this.debug.ui.getFolder('view/sky/sphere/geometry') 188 | 189 | sphereGeometryFolder.add(this.sphere, 'widthSegments').min(4).max(512).step(1).name('widthSegments').onChange(() => { this.sphere.update() }) 190 | sphereGeometryFolder.add(this.sphere, 'heightSegments').min(4).max(512).step(1).name('heightSegments').onChange(() => { this.sphere.update() }) 191 | 192 | const sphereMaterialFolder = this.debug.ui.getFolder('view/sky/sphere/material') 193 | 194 | sphereMaterialFolder.add(this.sphere.material.uniforms.uAtmosphereElevation, 'value').min(0).max(5).step(0.01).name('uAtmosphereElevation') 195 | sphereMaterialFolder.add(this.sphere.material.uniforms.uAtmospherePower, 'value').min(0).max(20).step(1).name('uAtmospherePower') 196 | sphereMaterialFolder.addColor(this.sphere.material.uniforms.uColorDayCycleLow, 'value').name('uColorDayCycleLow') 197 | sphereMaterialFolder.addColor(this.sphere.material.uniforms.uColorDayCycleHigh, 'value').name('uColorDayCycleHigh') 198 | sphereMaterialFolder.addColor(this.sphere.material.uniforms.uColorNightLow, 'value').name('uColorNightLow') 199 | sphereMaterialFolder.addColor(this.sphere.material.uniforms.uColorNightHigh, 'value').name('uColorNightHigh') 200 | sphereMaterialFolder.add(this.sphere.material.uniforms.uDawnAngleAmplitude, 'value').min(0).max(1).step(0.001).name('uDawnAngleAmplitude') 201 | sphereMaterialFolder.add(this.sphere.material.uniforms.uDawnElevationAmplitude, 'value').min(0).max(1).step(0.01).name('uDawnElevationAmplitude') 202 | sphereMaterialFolder.addColor(this.sphere.material.uniforms.uColorDawn, 'value').name('uColorDawn') 203 | sphereMaterialFolder.add(this.sphere.material.uniforms.uSunAmplitude, 'value').min(0).max(3).step(0.01).name('uSunAmplitude') 204 | sphereMaterialFolder.add(this.sphere.material.uniforms.uSunMultiplier, 'value').min(0).max(1).step(0.01).name('uSunMultiplier') 205 | sphereMaterialFolder.addColor(this.sphere.material.uniforms.uColorSun, 'value').name('uColorSun') 206 | 207 | // Stars 208 | const starsFolder = this.debug.ui.getFolder('view/sky/stars') 209 | 210 | starsFolder.add(this.stars, 'count').min(100).max(50000).step(100).name('count').onChange(() => { this.stars.update() }) 211 | starsFolder.add(this.stars.material.uniforms.uSize, 'value').min(0).max(1).step(0.0001).name('uSize') 212 | starsFolder.add(this.stars.material.uniforms.uBrightness, 'value').min(0).max(1).step(0.001).name('uBrightness') 213 | } 214 | 215 | update() 216 | { 217 | const dayState = this.state.day 218 | const sunState = this.state.sun 219 | const playerState = this.state.player 220 | 221 | // Group 222 | this.group.position.set( 223 | playerState.position.current[0], 224 | playerState.position.current[1], 225 | playerState.position.current[2] 226 | ) 227 | 228 | // Sphere 229 | this.sphere.material.uniforms.uSunPosition.value.set(sunState.position.x, sunState.position.y, sunState.position.z) 230 | this.sphere.material.uniforms.uDayCycleProgress.value = dayState.progress 231 | 232 | // Sun 233 | this.sun.mesh.position.set( 234 | sunState.position.x * this.sun.distance, 235 | sunState.position.y * this.sun.distance, 236 | sunState.position.z * this.sun.distance 237 | ) 238 | this.sun.mesh.lookAt( 239 | playerState.position.current[0], 240 | playerState.position.current[1], 241 | playerState.position.current[2] 242 | ) 243 | 244 | // Stars 245 | this.stars.material.uniforms.uSunPosition.value.set(sunState.position.x, sunState.position.y, sunState.position.z) 246 | this.stars.material.uniforms.uHeightFragments.value = this.viewport.height * this.viewport.clampedPixelRatio 247 | 248 | // Render in render target 249 | this.customRender.camera.quaternion.copy(this.view.camera.instance.quaternion) 250 | this.renderer.instance.setRenderTarget(this.customRender.renderTarget) 251 | this.renderer.instance.render(this.customRender.scene, this.customRender.camera) 252 | this.renderer.instance.setRenderTarget(null) 253 | } 254 | 255 | resize() 256 | { 257 | this.customRender.renderTarget.width = this.viewport.width * this.customRender.resolutionRatio 258 | this.customRender.renderTarget.height = this.viewport.height * this.customRender.resolutionRatio 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /sources/Game/View/Terrain.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import View from '@/View/View.js' 4 | import State from '@/State/State.js' 5 | 6 | export default class Terrain 7 | { 8 | constructor(terrains, terrainState) 9 | { 10 | this.state = State.getInstance() 11 | this.view = View.getInstance() 12 | this.scene = this.view.scene 13 | 14 | this.terrains = terrains 15 | this.terrainState = terrainState 16 | this.terrainState.renderInstance = this 17 | 18 | this.created = false 19 | 20 | this.terrainState.events.on('ready', () => 21 | { 22 | this.create() 23 | }) 24 | } 25 | 26 | create() 27 | { 28 | const terrainsState = this.state.terrains 29 | 30 | // Recreate 31 | if(this.created) 32 | { 33 | // Dispose of old geometry 34 | this.geometry.dispose() 35 | 36 | // Create new geometry 37 | this.geometry = new THREE.BufferGeometry() 38 | this.geometry.setAttribute('position', new THREE.BufferAttribute(this.terrainState.positions, 3)) 39 | this.geometry.index = new THREE.BufferAttribute(this.terrainState.indices, 1, false) 40 | 41 | this.mesh.geometry = this.geometry 42 | } 43 | 44 | // Create 45 | else 46 | { 47 | // Create geometry 48 | this.geometry = new THREE.BufferGeometry() 49 | this.geometry.setAttribute('position', new THREE.Float32BufferAttribute(this.terrainState.positions, 3)) 50 | this.geometry.setAttribute('uv', new THREE.Float32BufferAttribute(this.terrainState.uv, 2)) 51 | this.geometry.index = new THREE.BufferAttribute(this.terrainState.indices, 1, false) 52 | 53 | // Texture 54 | this.texture = new THREE.DataTexture( 55 | this.terrainState.texture, 56 | terrainsState.segments, 57 | terrainsState.segments, 58 | THREE.RGBAFormat, 59 | THREE.FloatType, 60 | THREE.UVMapping, 61 | THREE.ClampToEdgeWrapping, 62 | THREE.ClampToEdgeWrapping, 63 | THREE.LinearFilter, 64 | THREE.LinearFilter 65 | ) 66 | this.texture.flipY = false 67 | this.texture.needsUpdate = true 68 | 69 | // // Material 70 | // this.material = this.terrains.material.clone() 71 | // this.material.uniforms.uTexture.value = this.texture 72 | 73 | // Create mesh 74 | this.mesh = new THREE.Mesh(this.geometry, this.terrains.material) 75 | this.mesh.userData.texture = this.texture 76 | // this.mesh = new THREE.Mesh(this.geometry, new THREE.MeshNormalMaterial()) 77 | this.scene.add(this.mesh) 78 | 79 | this.created = true 80 | } 81 | } 82 | 83 | update() 84 | { 85 | 86 | } 87 | 88 | destroy() 89 | { 90 | if(this.created) 91 | { 92 | this.geometry.dispose() 93 | this.scene.remove(this.mesh) 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /sources/Game/View/TerrainGradient.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import Game from '@/Game.js' 4 | 5 | export default class TerrainGradient 6 | { 7 | constructor() 8 | { 9 | this.game = Game.getInstance() 10 | 11 | this.canvas = document.createElement('canvas') 12 | this.context = this.canvas.getContext('2d') 13 | this.texture = new THREE.Texture(this.canvas) 14 | 15 | this.colors = { 16 | aboveFar: '#ffffff', 17 | aboveClose: '#a6c33c', 18 | belowClose: '#2f3d36', 19 | belowFar: '#011018', 20 | 21 | // aboveFar: '#f4e5ff', 22 | // aboveClose: '#5900ff', 23 | // belowClose: '#0d0061', 24 | // belowFar: '#003242', 25 | } 26 | 27 | this.width = 1 28 | this.height = 512 29 | 30 | // this.canvas.width = this.width 31 | // this.canvas.height = this.height 32 | 33 | // this.canvas.style.position = 'fixed' 34 | // this.canvas.style.top = '48px' 35 | // this.canvas.style.left = '0' 36 | // this.canvas.style.zIndex = 1 37 | // document.body.append(this.canvas) 38 | 39 | this.update() 40 | this.setDebug() 41 | } 42 | 43 | update() 44 | { 45 | const fill = this.context.createLinearGradient(0, 0, 0, this.height) 46 | fill.addColorStop(0, this.colors.aboveFar) 47 | fill.addColorStop(0.49999, this.colors.aboveClose) 48 | fill.addColorStop(0.51111, this.colors.belowClose) 49 | fill.addColorStop(1, this.colors.belowFar) 50 | 51 | this.context.fillStyle = fill 52 | this.context.fillRect(0, 0, this.width, this.height) 53 | 54 | this.texture.needsUpdate = true 55 | } 56 | 57 | setDebug() 58 | { 59 | const debug = this.game.debug 60 | 61 | if(!debug.active) 62 | return 63 | 64 | const folder = debug.ui.getFolder('view/terrains/gradient') 65 | 66 | for(const colorKey in this.colors) 67 | { 68 | folder 69 | .addColor(this.colors, colorKey) 70 | .onChange(() => this.update()) 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /sources/Game/View/Terrains.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import Game from '@/Game.js' 4 | import View from '@/View/View.js' 5 | import State from '@/State/State.js' 6 | import Terrain from './Terrain.js' 7 | import TerrainGradient from './TerrainGradient.js' 8 | import TerrainMaterial from './Materials/TerrainMaterial.js' 9 | 10 | export default class Terrains 11 | { 12 | constructor() 13 | { 14 | this.game = Game.getInstance() 15 | this.state = State.getInstance() 16 | this.view = View.getInstance() 17 | this.debug = View.getInstance() 18 | 19 | this.viewport = this.state.viewport 20 | this.sky = this.view.sky 21 | 22 | this.setGradient() 23 | this.setMaterial() 24 | this.setDebug() 25 | 26 | this.state.terrains.events.on('create', (engineTerrain) => 27 | { 28 | const terrain = new Terrain(this, engineTerrain) 29 | 30 | engineTerrain.events.on('destroy', () => 31 | { 32 | terrain.destroy() 33 | }) 34 | }) 35 | } 36 | 37 | setGradient() 38 | { 39 | this.gradient = new TerrainGradient() 40 | } 41 | 42 | setMaterial() 43 | { 44 | this.material = new TerrainMaterial() 45 | this.material.uniforms.uPlayerPosition.value = new THREE.Vector3() 46 | this.material.uniforms.uGradientTexture.value = this.gradient.texture 47 | this.material.uniforms.uLightnessSmoothness.value = 0.25 48 | this.material.uniforms.uFresnelOffset.value = 0 49 | this.material.uniforms.uFresnelScale.value = 0.5 50 | this.material.uniforms.uFresnelPower.value = 2 51 | this.material.uniforms.uSunPosition.value = new THREE.Vector3(- 0.5, - 0.5, - 0.5) 52 | this.material.uniforms.uFogTexture.value = this.sky.customRender.texture 53 | this.material.uniforms.uGrassDistance.value = this.state.chunks.minSize 54 | 55 | this.material.onBeforeRender = (renderer, scene, camera, geometry, mesh) => 56 | { 57 | this.material.uniforms.uTexture.value = mesh.userData.texture 58 | this.material.uniformsNeedUpdate = true 59 | } 60 | 61 | // this.material.wireframe = true 62 | 63 | // const dummy = new THREE.Mesh( 64 | // new THREE.SphereGeometry(30, 64, 32), 65 | // this.material 66 | // ) 67 | // dummy.position.y = 50 68 | // this.scene.add(dummy) 69 | } 70 | 71 | setDebug() 72 | { 73 | if(!this.debug.active) 74 | return 75 | 76 | const folder = debug.ui.getFolder('view/terrains') 77 | 78 | folder 79 | .add(this.material, 'wireframe') 80 | 81 | folder 82 | .add(this.material.uniforms.uLightnessSmoothness, 'value') 83 | .min(0) 84 | .max(1) 85 | .step(0.001) 86 | .name('uLightnessSmoothness') 87 | 88 | folder 89 | .add(this.material.uniforms.uFresnelOffset, 'value') 90 | .min(- 1) 91 | .max(1) 92 | .step(0.001) 93 | .name('uFresnelOffset') 94 | 95 | folder 96 | .add(this.material.uniforms.uFresnelScale, 'value') 97 | .min(0) 98 | .max(2) 99 | .step(0.001) 100 | .name('uFresnelScale') 101 | 102 | folder 103 | .add(this.material.uniforms.uFresnelPower, 'value') 104 | .min(1) 105 | .max(10) 106 | .step(1) 107 | .name('uFresnelPower') 108 | } 109 | 110 | update() 111 | { 112 | const playerState = this.state.player 113 | const playerPosition = playerState.position.current 114 | const sunState = this.state.sun 115 | 116 | this.material.uniforms.uPlayerPosition.value.set(playerPosition[0], playerPosition[1], playerPosition[2]) 117 | this.material.uniforms.uSunPosition.value.set(sunState.position.x, sunState.position.y, sunState.position.z) 118 | } 119 | 120 | resize() 121 | { 122 | } 123 | } -------------------------------------------------------------------------------- /sources/Game/View/View.js: -------------------------------------------------------------------------------- 1 | import Camera from './Camera.js' 2 | import Chunks from './Chunks.js' 3 | import Grass from './Grass.js' 4 | import Noises from './Noises.js' 5 | import Player from './Player.js' 6 | import Renderer from './Renderer.js' 7 | import Sky from './Sky.js' 8 | import Terrains from './Terrains.js' 9 | import Water from './Water.js' 10 | 11 | import * as THREE from 'three' 12 | 13 | export default class View 14 | { 15 | static instance 16 | 17 | static getInstance() 18 | { 19 | return View.instance 20 | } 21 | 22 | constructor() 23 | { 24 | if(View.instance) 25 | return View.instance 26 | 27 | View.instance = this 28 | 29 | this.scene = new THREE.Scene() 30 | 31 | this.camera = new Camera() 32 | this.renderer = new Renderer() 33 | this.noises = new Noises() 34 | this.sky = new Sky() 35 | this.water = new Water() 36 | this.terrains = new Terrains() 37 | this.chunks = new Chunks() 38 | this.player = new Player() 39 | this.grass = new Grass() 40 | } 41 | 42 | resize() 43 | { 44 | this.camera.resize() 45 | this.renderer.resize() 46 | this.sky.resize() 47 | this.terrains.resize() 48 | } 49 | 50 | update() 51 | { 52 | this.sky.update() 53 | this.water.update() 54 | this.terrains.update() 55 | this.chunks.update() 56 | this.player.update() 57 | this.grass.update() 58 | this.camera.update() 59 | this.renderer.update() 60 | } 61 | 62 | destroy() 63 | { 64 | } 65 | } -------------------------------------------------------------------------------- /sources/Game/View/Water.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | import View from '@/View/View.js' 4 | import State from '@/State/State.js' 5 | 6 | export default class Water 7 | { 8 | constructor() 9 | { 10 | this.view = View.getInstance() 11 | this.state = State.getInstance() 12 | this.scene = this.view.scene 13 | 14 | this.mesh = new THREE.Mesh( 15 | new THREE.PlaneGeometry(1000, 1000), 16 | new THREE.MeshBasicMaterial({ color: '#1d3456' }) 17 | ) 18 | this.mesh.geometry.rotateX(- Math.PI * 0.5) 19 | // this.scene.add(this.mesh) 20 | } 21 | 22 | update() 23 | { 24 | const playerState = this.state.player 25 | 26 | this.mesh.position.set( 27 | playerState.position.current[0], 28 | 0, 29 | playerState.position.current[2] 30 | ) 31 | } 32 | } -------------------------------------------------------------------------------- /sources/Game/Workers/SimplexNoise.js: -------------------------------------------------------------------------------- 1 | /* 2 | * A fast javascript implementation of simplex noise by Jonas Wagner 3 | 4 | Based on a speed-improved simplex noise algorithm for 2D, 3D and 4D in Java. 5 | Which is based on example code by Stefan Gustavson (stegu@itn.liu.se). 6 | With Optimisations by Peter Eastman (peastman@drizzle.stanford.edu). 7 | Better rank ordering method by Stefan Gustavson in 2012. 8 | 9 | Copyright (c) 2021 Jonas Wagner 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE. 28 | */ 29 | const F2 = 0.5 * (Math.sqrt(3.0) - 1.0); 30 | const G2 = (3.0 - Math.sqrt(3.0)) / 6.0; 31 | const F3 = 1.0 / 3.0; 32 | const G3 = 1.0 / 6.0; 33 | const F4 = (Math.sqrt(5.0) - 1.0) / 4.0; 34 | const G4 = (5.0 - Math.sqrt(5.0)) / 20.0; 35 | const grad3 = new Float32Array([1, 1, 0, 36 | -1, 1, 0, 37 | 1, -1, 0, 38 | -1, -1, 0, 39 | 1, 0, 1, 40 | -1, 0, 1, 41 | 1, 0, -1, 42 | -1, 0, -1, 43 | 0, 1, 1, 44 | 0, -1, 1, 45 | 0, 1, -1, 46 | 0, -1, -1]); 47 | const grad4 = new Float32Array([0, 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1, 48 | 0, -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1, 49 | 1, 0, 1, 1, 1, 0, 1, -1, 1, 0, -1, 1, 1, 0, -1, -1, 50 | -1, 0, 1, 1, -1, 0, 1, -1, -1, 0, -1, 1, -1, 0, -1, -1, 51 | 1, 1, 0, 1, 1, 1, 0, -1, 1, -1, 0, 1, 1, -1, 0, -1, 52 | -1, 1, 0, 1, -1, 1, 0, -1, -1, -1, 0, 1, -1, -1, 0, -1, 53 | 1, 1, 1, 0, 1, 1, -1, 0, 1, -1, 1, 0, 1, -1, -1, 0, 54 | -1, 1, 1, 0, -1, 1, -1, 0, -1, -1, 1, 0, -1, -1, -1, 0]); 55 | /** Deterministic simplex noise generator suitable for 2D, 3D and 4D spaces. */ 56 | export class SimplexNoise { 57 | /** 58 | * Creates a new `SimplexNoise` instance. 59 | * This involves some setup. You can save a few cpu cycles by reusing the same instance. 60 | * @param randomOrSeed A random number generator or a seed (string|number). 61 | * Defaults to Math.random (random irreproducible initialization). 62 | */ 63 | constructor(randomOrSeed = Math.random) { 64 | const random = typeof randomOrSeed == 'function' ? randomOrSeed : alea(randomOrSeed); 65 | this.p = buildPermutationTable(random); 66 | this.perm = new Uint8Array(512); 67 | this.permMod12 = new Uint8Array(512); 68 | for (let i = 0; i < 512; i++) { 69 | this.perm[i] = this.p[i & 255]; 70 | this.permMod12[i] = this.perm[i] % 12; 71 | } 72 | } 73 | /** 74 | * Samples the noise field in 2 dimensions 75 | * @param x 76 | * @param y 77 | * @returns a number in the interval [-1, 1] 78 | */ 79 | noise2D(x, y) { 80 | const permMod12 = this.permMod12; 81 | const perm = this.perm; 82 | let n0 = 0; // Noise contributions from the three corners 83 | let n1 = 0; 84 | let n2 = 0; 85 | // Skew the input space to determine which simplex cell we're in 86 | const s = (x + y) * F2; // Hairy factor for 2D 87 | const i = Math.floor(x + s); 88 | const j = Math.floor(y + s); 89 | const t = (i + j) * G2; 90 | const X0 = i - t; // Unskew the cell origin back to (x,y) space 91 | const Y0 = j - t; 92 | const x0 = x - X0; // The x,y distances from the cell origin 93 | const y0 = y - Y0; 94 | // For the 2D case, the simplex shape is an equilateral triangle. 95 | // Determine which simplex we are in. 96 | let i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords 97 | if (x0 > y0) { 98 | i1 = 1; 99 | j1 = 0; 100 | } // lower triangle, XY order: (0,0)->(1,0)->(1,1) 101 | else { 102 | i1 = 0; 103 | j1 = 1; 104 | } // upper triangle, YX order: (0,0)->(0,1)->(1,1) 105 | // A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and 106 | // a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where 107 | // c = (3-sqrt(3))/6 108 | const x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords 109 | const y1 = y0 - j1 + G2; 110 | const x2 = x0 - 1.0 + 2.0 * G2; // Offsets for last corner in (x,y) unskewed coords 111 | const y2 = y0 - 1.0 + 2.0 * G2; 112 | // Work out the hashed gradient indices of the three simplex corners 113 | const ii = i & 255; 114 | const jj = j & 255; 115 | // Calculate the contribution from the three corners 116 | let t0 = 0.5 - x0 * x0 - y0 * y0; 117 | if (t0 >= 0) { 118 | const gi0 = permMod12[ii + perm[jj]] * 3; 119 | t0 *= t0; 120 | n0 = t0 * t0 * (grad3[gi0] * x0 + grad3[gi0 + 1] * y0); // (x,y) of grad3 used for 2D gradient 121 | } 122 | let t1 = 0.5 - x1 * x1 - y1 * y1; 123 | if (t1 >= 0) { 124 | const gi1 = permMod12[ii + i1 + perm[jj + j1]] * 3; 125 | t1 *= t1; 126 | n1 = t1 * t1 * (grad3[gi1] * x1 + grad3[gi1 + 1] * y1); 127 | } 128 | let t2 = 0.5 - x2 * x2 - y2 * y2; 129 | if (t2 >= 0) { 130 | const gi2 = permMod12[ii + 1 + perm[jj + 1]] * 3; 131 | t2 *= t2; 132 | n2 = t2 * t2 * (grad3[gi2] * x2 + grad3[gi2 + 1] * y2); 133 | } 134 | // Add contributions from each corner to get the final noise value. 135 | // The result is scaled to return values in the interval [-1,1]. 136 | return 70.0 * (n0 + n1 + n2); 137 | } 138 | /** 139 | * Samples the noise field in 3 dimensions 140 | * @param x 141 | * @param y 142 | * @param z 143 | * @returns a number in the interval [-1, 1] 144 | */ 145 | noise3D(x, y, z) { 146 | const permMod12 = this.permMod12; 147 | const perm = this.perm; 148 | let n0, n1, n2, n3; // Noise contributions from the four corners 149 | // Skew the input space to determine which simplex cell we're in 150 | const s = (x + y + z) * F3; // Very nice and simple skew factor for 3D 151 | const i = Math.floor(x + s); 152 | const j = Math.floor(y + s); 153 | const k = Math.floor(z + s); 154 | const t = (i + j + k) * G3; 155 | const X0 = i - t; // Unskew the cell origin back to (x,y,z) space 156 | const Y0 = j - t; 157 | const Z0 = k - t; 158 | const x0 = x - X0; // The x,y,z distances from the cell origin 159 | const y0 = y - Y0; 160 | const z0 = z - Z0; 161 | // For the 3D case, the simplex shape is a slightly irregular tetrahedron. 162 | // Determine which simplex we are in. 163 | let i1, j1, k1; // Offsets for second corner of simplex in (i,j,k) coords 164 | let i2, j2, k2; // Offsets for third corner of simplex in (i,j,k) coords 165 | if (x0 >= y0) { 166 | if (y0 >= z0) { 167 | i1 = 1; 168 | j1 = 0; 169 | k1 = 0; 170 | i2 = 1; 171 | j2 = 1; 172 | k2 = 0; 173 | } // X Y Z order 174 | else if (x0 >= z0) { 175 | i1 = 1; 176 | j1 = 0; 177 | k1 = 0; 178 | i2 = 1; 179 | j2 = 0; 180 | k2 = 1; 181 | } // X Z Y order 182 | else { 183 | i1 = 0; 184 | j1 = 0; 185 | k1 = 1; 186 | i2 = 1; 187 | j2 = 0; 188 | k2 = 1; 189 | } // Z X Y order 190 | } 191 | else { // x0 y0) 306 | rankx++; 307 | else 308 | ranky++; 309 | if (x0 > z0) 310 | rankx++; 311 | else 312 | rankz++; 313 | if (x0 > w0) 314 | rankx++; 315 | else 316 | rankw++; 317 | if (y0 > z0) 318 | ranky++; 319 | else 320 | rankz++; 321 | if (y0 > w0) 322 | ranky++; 323 | else 324 | rankw++; 325 | if (z0 > w0) 326 | rankz++; 327 | else 328 | rankw++; 329 | // simplex[c] is a 4-vector with the numbers 0, 1, 2 and 3 in some order. 330 | // Many values of c will never occur, since e.g. x>y>z>w makes x= 3 ? 1 : 0; 338 | const j1 = ranky >= 3 ? 1 : 0; 339 | const k1 = rankz >= 3 ? 1 : 0; 340 | const l1 = rankw >= 3 ? 1 : 0; 341 | // The integer offsets for the third simplex corner 342 | const i2 = rankx >= 2 ? 1 : 0; 343 | const j2 = ranky >= 2 ? 1 : 0; 344 | const k2 = rankz >= 2 ? 1 : 0; 345 | const l2 = rankw >= 2 ? 1 : 0; 346 | // The integer offsets for the fourth simplex corner 347 | const i3 = rankx >= 1 ? 1 : 0; 348 | const j3 = ranky >= 1 ? 1 : 0; 349 | const k3 = rankz >= 1 ? 1 : 0; 350 | const l3 = rankw >= 1 ? 1 : 0; 351 | // The fifth corner has all coordinate offsets = 1, so no need to compute that. 352 | const x1 = x0 - i1 + G4; // Offsets for second corner in (x,y,z,w) coords 353 | const y1 = y0 - j1 + G4; 354 | const z1 = z0 - k1 + G4; 355 | const w1 = w0 - l1 + G4; 356 | const x2 = x0 - i2 + 2.0 * G4; // Offsets for third corner in (x,y,z,w) coords 357 | const y2 = y0 - j2 + 2.0 * G4; 358 | const z2 = z0 - k2 + 2.0 * G4; 359 | const w2 = w0 - l2 + 2.0 * G4; 360 | const x3 = x0 - i3 + 3.0 * G4; // Offsets for fourth corner in (x,y,z,w) coords 361 | const y3 = y0 - j3 + 3.0 * G4; 362 | const z3 = z0 - k3 + 3.0 * G4; 363 | const w3 = w0 - l3 + 3.0 * G4; 364 | const x4 = x0 - 1.0 + 4.0 * G4; // Offsets for last corner in (x,y,z,w) coords 365 | const y4 = y0 - 1.0 + 4.0 * G4; 366 | const z4 = z0 - 1.0 + 4.0 * G4; 367 | const w4 = w0 - 1.0 + 4.0 * G4; 368 | // Work out the hashed gradient indices of the five simplex corners 369 | const ii = i & 255; 370 | const jj = j & 255; 371 | const kk = k & 255; 372 | const ll = l & 255; 373 | // Calculate the contribution from the five corners 374 | let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0; 375 | if (t0 < 0) 376 | n0 = 0.0; 377 | else { 378 | const gi0 = (perm[ii + perm[jj + perm[kk + perm[ll]]]] % 32) * 4; 379 | t0 *= t0; 380 | n0 = t0 * t0 * (grad4[gi0] * x0 + grad4[gi0 + 1] * y0 + grad4[gi0 + 2] * z0 + grad4[gi0 + 3] * w0); 381 | } 382 | let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1; 383 | if (t1 < 0) 384 | n1 = 0.0; 385 | else { 386 | const gi1 = (perm[ii + i1 + perm[jj + j1 + perm[kk + k1 + perm[ll + l1]]]] % 32) * 4; 387 | t1 *= t1; 388 | n1 = t1 * t1 * (grad4[gi1] * x1 + grad4[gi1 + 1] * y1 + grad4[gi1 + 2] * z1 + grad4[gi1 + 3] * w1); 389 | } 390 | let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2; 391 | if (t2 < 0) 392 | n2 = 0.0; 393 | else { 394 | const gi2 = (perm[ii + i2 + perm[jj + j2 + perm[kk + k2 + perm[ll + l2]]]] % 32) * 4; 395 | t2 *= t2; 396 | n2 = t2 * t2 * (grad4[gi2] * x2 + grad4[gi2 + 1] * y2 + grad4[gi2 + 2] * z2 + grad4[gi2 + 3] * w2); 397 | } 398 | let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3; 399 | if (t3 < 0) 400 | n3 = 0.0; 401 | else { 402 | const gi3 = (perm[ii + i3 + perm[jj + j3 + perm[kk + k3 + perm[ll + l3]]]] % 32) * 4; 403 | t3 *= t3; 404 | n3 = t3 * t3 * (grad4[gi3] * x3 + grad4[gi3 + 1] * y3 + grad4[gi3 + 2] * z3 + grad4[gi3 + 3] * w3); 405 | } 406 | let t4 = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4; 407 | if (t4 < 0) 408 | n4 = 0.0; 409 | else { 410 | const gi4 = (perm[ii + 1 + perm[jj + 1 + perm[kk + 1 + perm[ll + 1]]]] % 32) * 4; 411 | t4 *= t4; 412 | n4 = t4 * t4 * (grad4[gi4] * x4 + grad4[gi4 + 1] * y4 + grad4[gi4 + 2] * z4 + grad4[gi4 + 3] * w4); 413 | } 414 | // Sum up and scale the result to cover the range [-1,1] 415 | return 27.0 * (n0 + n1 + n2 + n3 + n4); 416 | } 417 | } 418 | export default SimplexNoise; 419 | /** 420 | * Builds a random permutation table. 421 | * This is exported only for (internal) testing purposes. 422 | * Do not rely on this export. 423 | * @private 424 | */ 425 | export function buildPermutationTable(random) { 426 | const p = new Uint8Array(256); 427 | for (let i = 0; i < 256; i++) { 428 | p[i] = i; 429 | } 430 | for (let i = 0; i < 255; i++) { 431 | const r = i + ~~(random() * (256 - i)); 432 | const aux = p[i]; 433 | p[i] = p[r]; 434 | p[r] = aux; 435 | } 436 | return p; 437 | } 438 | /* 439 | The ALEA PRNG and masher code used by simplex-noise.js 440 | is based on code by Johannes Baagøe, modified by Jonas Wagner. 441 | See alea.md for the full license. 442 | */ 443 | function alea(seed) { 444 | let s0 = 0; 445 | let s1 = 0; 446 | let s2 = 0; 447 | let c = 1; 448 | const mash = masher(); 449 | s0 = mash(' '); 450 | s1 = mash(' '); 451 | s2 = mash(' '); 452 | s0 -= mash(seed); 453 | if (s0 < 0) { 454 | s0 += 1; 455 | } 456 | s1 -= mash(seed); 457 | if (s1 < 0) { 458 | s1 += 1; 459 | } 460 | s2 -= mash(seed); 461 | if (s2 < 0) { 462 | s2 += 1; 463 | } 464 | return function () { 465 | const t = 2091639 * s0 + c * 2.3283064365386963e-10; // 2^-32 466 | s0 = s1; 467 | s1 = s2; 468 | return s2 = t - (c = t | 0); 469 | }; 470 | } 471 | function masher() { 472 | let n = 0xefc8249d; 473 | return function (data) { 474 | data = data.toString(); 475 | for (let i = 0; i < data.length; i++) { 476 | n += data.charCodeAt(i); 477 | let h = 0.02519603282416938 * n; 478 | n = h >>> 0; 479 | h -= n; 480 | h *= n; 481 | n = h >>> 0; 482 | h -= n; 483 | n += h * 0x100000000; // 2^32 484 | } 485 | return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 486 | }; 487 | } -------------------------------------------------------------------------------- /sources/Game/Workers/Terrain.js: -------------------------------------------------------------------------------- 1 | import SimplexNoise from './SimplexNoise.js' 2 | import { vec3 } from 'gl-matrix' 3 | 4 | let elevationRandom = null 5 | 6 | const linearStep = (edgeMin, edgeMax, value) => 7 | { 8 | return Math.max(0.0, Math.min(1.0, (value - edgeMin) / (edgeMax - edgeMin))) 9 | } 10 | 11 | const getElevation = (x, y, lacunarity, persistence, iterations, baseFrequency, baseAmplitude, power, elevationOffset, iterationsOffsets) => 12 | { 13 | let elevation = 0 14 | let frequency = baseFrequency 15 | let amplitude = 1 16 | let normalisation = 0 17 | 18 | for(let i = 0; i < iterations; i++) 19 | { 20 | const noise = elevationRandom.noise2D(x * frequency + iterationsOffsets[i][0], y * frequency + iterationsOffsets[i][1]) 21 | elevation += noise * amplitude 22 | 23 | normalisation += amplitude 24 | amplitude *= persistence 25 | frequency *= lacunarity 26 | } 27 | 28 | elevation /= normalisation 29 | elevation = Math.pow(Math.abs(elevation), power) * Math.sign(elevation) 30 | elevation *= baseAmplitude 31 | elevation += elevationOffset 32 | 33 | return elevation 34 | } 35 | 36 | onmessage = function(event) 37 | { 38 | const id = event.data.id 39 | const size = event.data.size 40 | const baseX = event.data.x 41 | const baseZ = event.data.z 42 | const seed = event.data.seed 43 | const subdivisions = event.data.subdivisions 44 | const lacunarity = event.data.lacunarity 45 | const persistence = event.data.persistence 46 | const iterations = event.data.iterations 47 | const baseFrequency = event.data.baseFrequency 48 | const baseAmplitude = event.data.baseAmplitude 49 | const power = event.data.power 50 | const elevationOffset = event.data.elevationOffset 51 | const iterationsOffsets = event.data.iterationsOffsets 52 | 53 | const segments = subdivisions + 1 54 | elevationRandom = new SimplexNoise(seed) 55 | const grassRandom = new SimplexNoise(seed) 56 | 57 | /** 58 | * Elevation 59 | */ 60 | const overflowElevations = new Float32Array((segments + 1) * (segments + 1)) // Bigger to calculate normals more accurately 61 | const elevations = new Float32Array(segments * segments) 62 | 63 | for(let iX = 0; iX < segments + 1; iX++) 64 | { 65 | const x = baseX + (iX / subdivisions - 0.5) * size 66 | 67 | for(let iZ = 0; iZ < segments + 1; iZ++) 68 | { 69 | const z = baseZ + (iZ / subdivisions - 0.5) * size 70 | const elevation = getElevation(x, z, lacunarity, persistence, iterations, baseFrequency, baseAmplitude, power, elevationOffset, iterationsOffsets) 71 | 72 | const i = iZ * (segments + 1) + iX 73 | overflowElevations[i] = elevation 74 | 75 | if(iX < segments && iZ < segments) 76 | { 77 | const i = iZ * segments + iX 78 | elevations[i] = elevation 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Positions 85 | */ 86 | const skirtCount = subdivisions * 4 + 4 87 | const positions = new Float32Array(segments * segments * 3 + skirtCount * 3) 88 | 89 | for(let iZ = 0; iZ < segments; iZ++) 90 | { 91 | const z = baseZ + (iZ / subdivisions - 0.5) * size 92 | for(let iX = 0; iX < segments; iX++) 93 | { 94 | const x = baseX + (iX / subdivisions - 0.5) * size 95 | 96 | const elevation = elevations[iZ * segments + iX] 97 | 98 | const iStride = (iZ * segments + iX) * 3 99 | positions[iStride ] = x 100 | positions[iStride + 1] = elevation 101 | positions[iStride + 2] = z 102 | } 103 | } 104 | 105 | /** 106 | * Normals 107 | */ 108 | const normals = new Float32Array(segments * segments * 3 + skirtCount * 3) 109 | 110 | const interSegmentX = - size / subdivisions 111 | const interSegmentZ = - size / subdivisions 112 | 113 | for(let iZ = 0; iZ < segments; iZ++) 114 | { 115 | for(let iX = 0; iX < segments; iX++) 116 | { 117 | // Indexes 118 | const iOverflowStride = iZ * (segments + 1) + iX 119 | 120 | // Elevations 121 | const currentElevation = overflowElevations[iOverflowStride] 122 | const neighbourXElevation = overflowElevations[iOverflowStride + 1] 123 | const neighbourZElevation = overflowElevations[iOverflowStride + segments + 1] 124 | 125 | // Deltas 126 | const deltaX = vec3.fromValues( 127 | interSegmentX, 128 | currentElevation - neighbourXElevation, 129 | 0 130 | ) 131 | 132 | const deltaZ = vec3.fromValues( 133 | 0, 134 | currentElevation - neighbourZElevation, 135 | interSegmentZ 136 | ) 137 | 138 | // Normal 139 | const normal = vec3.create() 140 | vec3.cross(normal, deltaZ, deltaX) 141 | vec3.normalize(normal, normal) 142 | 143 | const iStride = (iZ * segments + iX) * 3 144 | normals[iStride ] = normal[0] 145 | normals[iStride + 1] = normal[1] 146 | normals[iStride + 2] = normal[2] 147 | } 148 | } 149 | 150 | /** 151 | * UV 152 | */ 153 | const uv = new Float32Array(segments * segments * 2 + skirtCount * 2) 154 | 155 | for(let iZ = 0; iZ < segments; iZ++) 156 | { 157 | for(let iX = 0; iX < segments; iX++) 158 | { 159 | const iStride = (iZ * segments + iX) * 2 160 | uv[iStride ] = iX / (segments - 1) 161 | uv[iStride + 1] = iZ / (segments - 1) 162 | } 163 | } 164 | 165 | /** 166 | * Indices 167 | */ 168 | const indicesCount = subdivisions * subdivisions 169 | const indices = new (indicesCount < 65535 ? Uint16Array : Uint32Array)(indicesCount * 6 + subdivisions * 4 * 6 * 4) 170 | 171 | for(let iZ = 0; iZ < subdivisions; iZ++) 172 | { 173 | for(let iX = 0; iX < subdivisions; iX++) 174 | { 175 | const row = subdivisions + 1 176 | const a = iZ * row + iX 177 | const b = iZ * row + (iX + 1) 178 | const c = (iZ + 1) * row + iX 179 | const d = (iZ + 1) * row + (iX + 1) 180 | 181 | const iStride = (iZ * subdivisions + iX) * 6 182 | indices[iStride ] = a 183 | indices[iStride + 1] = d 184 | indices[iStride + 2] = b 185 | 186 | indices[iStride + 3] = d 187 | indices[iStride + 4] = a 188 | indices[iStride + 5] = c 189 | } 190 | } 191 | 192 | /** 193 | * Skirt 194 | */ 195 | let skirtIndex = segments * segments 196 | let indicesSkirtIndex = segments * segments 197 | 198 | // North (negative Z) 199 | for(let iX = 0; iX < segments; iX++) 200 | { 201 | const iZ = 0 202 | const iPosition = iZ * segments + iX 203 | const iPositionStride = iPosition * 3 204 | 205 | // Position 206 | positions[skirtIndex * 3 ] = positions[iPositionStride + 0] 207 | positions[skirtIndex * 3 + 1] = positions[iPositionStride + 1] - 15 208 | positions[skirtIndex * 3 + 2] = positions[iPositionStride + 2] 209 | 210 | // Normal 211 | normals[skirtIndex * 3 ] = normals[iPositionStride + 0] 212 | normals[skirtIndex * 3 + 1] = normals[iPositionStride + 1] 213 | normals[skirtIndex * 3 + 2] = normals[iPositionStride + 2] 214 | 215 | // UV 216 | uv[skirtIndex * 2 ] = iZ / (segments - 1) 217 | uv[skirtIndex * 2 + 1] = iX / (segments - 1) 218 | 219 | // Index 220 | if(iX < segments - 1) 221 | { 222 | const a = iPosition 223 | const b = iPosition + 1 224 | const c = skirtIndex 225 | const d = skirtIndex + 1 226 | 227 | const iIndexStride = indicesSkirtIndex * 6 228 | indices[iIndexStride ] = b 229 | indices[iIndexStride + 1] = d 230 | indices[iIndexStride + 2] = a 231 | 232 | indices[iIndexStride + 3] = c 233 | indices[iIndexStride + 4] = a 234 | indices[iIndexStride + 5] = d 235 | 236 | indicesSkirtIndex++ 237 | } 238 | 239 | skirtIndex++ 240 | } 241 | 242 | // South (positive Z) 243 | for(let iX = 0; iX < segments; iX++) 244 | { 245 | const iZ = segments - 1 246 | const iPosition = iZ * segments + iX 247 | const iPositionStride = iPosition * 3 248 | 249 | // Position 250 | positions[skirtIndex * 3 ] = positions[iPositionStride + 0] 251 | positions[skirtIndex * 3 + 1] = positions[iPositionStride + 1] - 15 252 | positions[skirtIndex * 3 + 2] = positions[iPositionStride + 2] 253 | 254 | // Normal 255 | normals[skirtIndex * 3 ] = normals[iPositionStride + 0] 256 | normals[skirtIndex * 3 + 1] = normals[iPositionStride + 1] 257 | normals[skirtIndex * 3 + 2] = normals[iPositionStride + 2] 258 | 259 | // UV 260 | uv[skirtIndex * 2 ] = iZ / (segments - 1) 261 | uv[skirtIndex * 2 + 1] = iX / (segments - 1) 262 | 263 | // Index 264 | if(iX < segments - 1) 265 | { 266 | const a = iPosition 267 | const b = iPosition + 1 268 | const c = skirtIndex 269 | const d = skirtIndex + 1 270 | 271 | const iIndexStride = indicesSkirtIndex * 6 272 | indices[iIndexStride ] = a 273 | indices[iIndexStride + 1] = c 274 | indices[iIndexStride + 2] = b 275 | 276 | indices[iIndexStride + 3] = d 277 | indices[iIndexStride + 4] = b 278 | indices[iIndexStride + 5] = c 279 | 280 | indicesSkirtIndex++ 281 | } 282 | 283 | skirtIndex++ 284 | } 285 | 286 | // West (negative X) 287 | for(let iZ = 0; iZ < segments; iZ++) 288 | { 289 | const iX = 0 290 | const iPosition = (iZ * segments + iX) 291 | const iPositionStride = iPosition * 3 292 | 293 | // Position 294 | positions[skirtIndex * 3 ] = positions[iPositionStride + 0] 295 | positions[skirtIndex * 3 + 1] = positions[iPositionStride + 1] - 15 296 | positions[skirtIndex * 3 + 2] = positions[iPositionStride + 2] 297 | 298 | // Normal 299 | normals[skirtIndex * 3 ] = normals[iPositionStride + 0] 300 | normals[skirtIndex * 3 + 1] = normals[iPositionStride + 1] 301 | normals[skirtIndex * 3 + 2] = normals[iPositionStride + 2] 302 | 303 | // UV 304 | uv[skirtIndex * 2 ] = iZ / (segments - 1) 305 | uv[skirtIndex * 2 + 1] = iX 306 | 307 | // Index 308 | if(iZ < segments - 1) 309 | { 310 | const a = iPosition 311 | const b = iPosition + segments 312 | const c = skirtIndex 313 | const d = skirtIndex + 1 314 | 315 | const iIndexStride = indicesSkirtIndex * 6 316 | indices[iIndexStride ] = a 317 | indices[iIndexStride + 1] = c 318 | indices[iIndexStride + 2] = b 319 | 320 | indices[iIndexStride + 3] = d 321 | indices[iIndexStride + 4] = b 322 | indices[iIndexStride + 5] = c 323 | 324 | indicesSkirtIndex++ 325 | } 326 | 327 | skirtIndex++ 328 | } 329 | 330 | for(let iZ = 0; iZ < segments; iZ++) 331 | { 332 | const iX = segments - 1 333 | const iPosition = (iZ * segments + iX) 334 | const iPositionStride = iPosition * 3 335 | 336 | // Position 337 | positions[skirtIndex * 3 ] = positions[iPositionStride + 0] 338 | positions[skirtIndex * 3 + 1] = positions[iPositionStride + 1] - 15 339 | positions[skirtIndex * 3 + 2] = positions[iPositionStride + 2] 340 | 341 | // Normal 342 | normals[skirtIndex * 3 ] = normals[iPositionStride + 0] 343 | normals[skirtIndex * 3 + 1] = normals[iPositionStride + 1] 344 | normals[skirtIndex * 3 + 2] = normals[iPositionStride + 2] 345 | 346 | // UV 347 | uv[skirtIndex * 2 ] = iZ / (segments - 1) 348 | uv[skirtIndex * 2 + 1] = iX / (segments - 1) 349 | 350 | // Index 351 | if(iZ < segments - 1) 352 | { 353 | const a = iPosition 354 | const b = iPosition + segments 355 | const c = skirtIndex 356 | const d = skirtIndex + 1 357 | 358 | const iIndexStride = indicesSkirtIndex * 6 359 | indices[iIndexStride ] = b 360 | indices[iIndexStride + 1] = d 361 | indices[iIndexStride + 2] = a 362 | 363 | indices[iIndexStride + 3] = c 364 | indices[iIndexStride + 4] = a 365 | indices[iIndexStride + 5] = d 366 | 367 | indicesSkirtIndex++ 368 | } 369 | 370 | skirtIndex++ 371 | } 372 | 373 | /** 374 | * Texture 375 | */ 376 | const texture = new Float32Array(segments * segments * 4) 377 | 378 | for(let iZ = 0; iZ < segments; iZ++) 379 | { 380 | for(let iX = 0; iX < segments; iX++) 381 | { 382 | const iPositionStride = (iZ * segments + iX) * 3 383 | const position = vec3.fromValues( 384 | positions[iPositionStride ], 385 | positions[iPositionStride + 1], 386 | positions[iPositionStride + 2] 387 | ) 388 | 389 | // Normal 390 | const iNormalStride = (iZ * segments + iX) * 3 391 | const normal = vec3.fromValues( 392 | normals[iNormalStride ], 393 | normals[iNormalStride + 1], 394 | normals[iNormalStride + 2] 395 | ) 396 | 397 | // Grass 398 | const upward = Math.max(0, normal[1]) 399 | let grass = 0; 400 | 401 | if(position[1] > 0) 402 | { 403 | const grassFrequency = 0.05 404 | let grassNoise = grassRandom.noise2D(position[0] * grassFrequency + iterationsOffsets[0][0], position[2] * grassFrequency + iterationsOffsets[0][0]) 405 | grassNoise = linearStep(- 0.5, 0, grassNoise); 406 | 407 | const grassUpward = linearStep(0.9, 1, upward); 408 | 409 | grass = grassNoise * grassUpward 410 | } 411 | 412 | // Final texture 413 | const iTextureStride = (iZ * segments + iX) * 4 414 | texture[iTextureStride ] = normals[iNormalStride ] 415 | texture[iTextureStride + 1] = normals[iNormalStride + 1] 416 | texture[iTextureStride + 2] = normals[iNormalStride + 2] 417 | texture[iTextureStride + 3] = position[1] 418 | } 419 | } 420 | 421 | // Post 422 | postMessage({ 423 | id: id, 424 | positions: positions, 425 | normals: normals, 426 | indices: indices, 427 | texture: texture, 428 | uv: uv 429 | }) 430 | } -------------------------------------------------------------------------------- /sources/index.js: -------------------------------------------------------------------------------- 1 | import Game from '@/Game.js' 2 | 3 | const game = new Game() 4 | 5 | if(game.view) 6 | document.querySelector('.game').append(game.view.renderer.instance.domElement) -------------------------------------------------------------------------------- /sources/style.css: -------------------------------------------------------------------------------- 1 | * 2 | { 3 | box-sizing: border-box; 4 | } 5 | 6 | .game 7 | { 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | width: 100vw; 12 | height: 100%; 13 | touch-action: none; 14 | } 15 | 16 | .ui 17 | { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | width: 100%; 22 | height: 100%; 23 | z-index: 1; 24 | font-family: Helvetica, Arial, sans-serif; 25 | font-size: 12px; 26 | color: #ffffff; 27 | pointer-events: none; 28 | user-select: none; 29 | } 30 | 31 | .ui a 32 | { 33 | pointer-events: auto; 34 | } 35 | 36 | .ui .mobile-warning 37 | { 38 | display: none; 39 | align-items: center; 40 | justify-content: center; 41 | position: absolute; 42 | top: 0; 43 | left: 0; 44 | width: 100%; 45 | height: 200px; 46 | } 47 | 48 | @media (orientation: portrait) 49 | { 50 | .ui .mobile-warning 51 | { 52 | display: flex; 53 | } 54 | } 55 | 56 | .ui .controls 57 | { 58 | display: flex; 59 | justify-content: space-between; 60 | position: absolute; 61 | bottom: 0; 62 | left: 0; 63 | width: 100%; 64 | padding: 8px; 65 | } 66 | 67 | .ui a 68 | { 69 | color: inherit; 70 | } 71 | 72 | .ui .group 73 | { 74 | display: flex; 75 | } 76 | 77 | .ui .element 78 | { 79 | display: flex; 80 | align-items: center; 81 | margin-right: 10px; 82 | } 83 | 84 | .ui .keys 85 | { 86 | --key-size: 15px; 87 | --key-margin: 2px; 88 | 89 | font-size: 0; 90 | margin-right: 2px; 91 | white-space: nowrap; 92 | } 93 | 94 | .ui .keys.is-arrows 95 | { 96 | position: relative; 97 | } 98 | 99 | .ui .keys.is-arrows .key:last-child 100 | { 101 | position: absolute; 102 | bottom: calc(100% + var(--key-margin)); 103 | left: calc(var(--key-size) + var(--key-margin)); 104 | } 105 | 106 | .ui .key 107 | { 108 | display: inline-flex; 109 | align-items: center; 110 | justify-content: center; 111 | width: var(--key-size); 112 | height: var(--key-size); 113 | margin-right: var(--key-margin); 114 | border: 1px solid #ffffff; 115 | font-size: 9px; 116 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import glsl from 'vite-plugin-glsl' 2 | import { defineConfig } from 'vite' 3 | import path from 'path' 4 | 5 | const dirname = path.resolve() 6 | 7 | export default defineConfig({ 8 | resolve: 9 | { 10 | alias: 11 | { 12 | '@' : path.resolve(dirname, './sources/Game') 13 | } 14 | }, 15 | plugins: 16 | [ 17 | glsl({ watch: true }) 18 | ], 19 | server: 20 | { 21 | host: true, 22 | open: true 23 | } 24 | }) 25 | --------------------------------------------------------------------------------