├── .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 |
46 | fullscreen
47 |
48 |
49 |
52 | pointer lock
53 |
54 |
60 |
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 | 
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 |
--------------------------------------------------------------------------------