├── .gitignore ├── LICENSE ├── README.md ├── base.css ├── index.html ├── package-lock.json ├── package.json ├── public ├── characters │ ├── README.txt │ ├── guard.glb │ ├── paladin.glb │ └── ybot.glb ├── models │ ├── README.txt │ └── mountain.glb ├── shaders │ ├── bugs-lighting-model-fsh.glsl │ ├── bugs-lighting-model-vsh.glsl │ ├── common.glsl │ ├── grass-lighting-model-fsh.glsl │ ├── grass-lighting-model-vsh.glsl │ ├── header.glsl │ ├── lighting-model-fsh.glsl │ ├── lighting-model-vsh.glsl │ ├── noise.glsl │ ├── oklab.glsl │ ├── phong-lighting-model-fsh.glsl │ ├── phong-lighting-model-vsh.glsl │ ├── sky-lighting-model-fsh.glsl │ ├── sky-lighting-model-vsh.glsl │ ├── sky.glsl │ ├── terrain-lighting-model-fsh.glsl │ ├── terrain-lighting-model-vsh.glsl │ ├── water-lighting-model-fsh.glsl │ ├── water-lighting-model-vsh.glsl │ ├── water-texture-fsh.glsl │ ├── water-texture-vsh.glsl │ ├── wind-lighting-model-fsh.glsl │ └── wind-lighting-model-vsh.glsl └── textures │ ├── butterfly.png │ ├── dust.png │ ├── grass.png │ ├── grid.png │ ├── moth.png │ ├── terrain.png │ └── whitesquare.png ├── src ├── base │ ├── entity-manager.js │ ├── entity.js │ ├── load-controller.js │ ├── math.js │ ├── passes.js │ ├── render-component.js │ ├── render-order.js │ ├── render │ │ ├── bugs-component.js │ │ ├── grass-component.js │ │ ├── light-component.js │ │ ├── terrain-component.js │ │ ├── water-component.js │ │ └── wind-component.js │ ├── three-defs.js │ └── threejs-component.js ├── demo-builder.js ├── game │ ├── player-entity.js │ ├── player-input.js │ ├── render │ │ ├── render-sky-component.js │ │ └── shaders.js │ ├── spawners.js │ └── third-person-camera.js └── main.js └── vite.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules/* 2 | *dist/* 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 simondevyoutube 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick_Grass 2 | 3 | Hi there, this is a cleaned up version of the source code for my video [How do Major Video Games Render Grass?](https://youtu.be/bp7REZBV4P4) 4 | 5 | If you're not already a subscriber, check out my channel: [SimonDev](https://www.youtube.com/channel/UCEwhtpXrg5MmwlH04ANpL8A) 6 | 7 | It's an implementation based on the GDC presentation for [Ghost of Tsushima's grass](https://www.gdcvault.com/play/1027033/Advanced-Graphics-Summit-Procedural-Grass) 8 | 9 | 10 | If you'd like to help choose the next video, consider support me on [Patreon](https://www.patreon.com/simondevyt) 11 | 12 | 13 | Lastly, this is released under the MIT license, so do whatever you want. If you do happen to use it in a project, I'd appreciate a shout-out or support, although you're under no obligation to do so. 14 | 15 | Cheers -------------------------------------------------------------------------------- /base.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@500;900&display=swap'); 2 | 3 | body { 4 | width: 100%; 5 | height: 100%; 6 | position: absolute; 7 | background: #000000; 8 | margin: 0; 9 | padding: 0; 10 | overscroll-behavior: none; 11 | } 12 | 13 | .container { 14 | width: 100%; 15 | height: 100%; 16 | position: relative; 17 | } 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SimonDev Grass Thing 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 |
14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "package", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "homepage": "https://simondevyoutube.github.io/Quick_Grass/", 7 | "scripts": { 8 | "predeploy" : "npm run build", 9 | "deploy" : "gh-pages -d dist", 10 | "dev": "vite", 11 | "build": "vite build", 12 | "preview": "vite preview" 13 | }, 14 | "devDependencies": { 15 | "gh-pages": "^6.0.0", 16 | "gltf-pipeline": "^4.1.0", 17 | "vite": "^4.4.9" 18 | }, 19 | "dependencies": { 20 | "mersenne-twister": "^1.1.0", 21 | "three": "^0.156.0", 22 | "vite-plugin-solid": "^2.7.0", 23 | "vite-plugin-top-level-await": "^1.3.0", 24 | "vite-plugin-wasm": "^3.2.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/characters/README.txt: -------------------------------------------------------------------------------- 1 | Taken from https://www.mixamo.com/ -------------------------------------------------------------------------------- /public/characters/guard.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/characters/guard.glb -------------------------------------------------------------------------------- /public/characters/paladin.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/characters/paladin.glb -------------------------------------------------------------------------------- /public/characters/ybot.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/characters/ybot.glb -------------------------------------------------------------------------------- /public/models/README.txt: -------------------------------------------------------------------------------- 1 | These mountains were made from rocks from https://quaternius.com/ -------------------------------------------------------------------------------- /public/models/mountain.glb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/models/mountain.glb -------------------------------------------------------------------------------- /public/shaders/bugs-lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | #define PHONG 2 | uniform vec3 diffuse; 3 | uniform vec3 emissive; 4 | uniform vec3 specular; 5 | uniform float shininess; 6 | uniform float opacity; 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | varying vec3 vWorldNormal; 34 | 35 | uniform sampler2D bugsTexture; 36 | 37 | void main() { 38 | #include 39 | vec4 diffuseColor = vec4( diffuse, opacity ); 40 | 41 | ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); 42 | vec3 totalEmissiveRadiance = emissive; 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; 59 | 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | #include 66 | #include 67 | } -------------------------------------------------------------------------------- /public/shaders/bugs-lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | #define PHONG 4 | varying vec3 vViewPosition; 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | varying vec3 vWorldNormal; 19 | 20 | uniform vec2 bugsSize; 21 | uniform vec4 bugsParams; 22 | uniform float time; 23 | 24 | uniform sampler2D heightmap; 25 | uniform vec3 heightmapParams; 26 | 27 | attribute vec3 offset; 28 | 29 | 30 | void main() { 31 | #include 32 | #include 33 | #include 34 | // #include 35 | 36 | vec3 objectNormal = vec3(0.0, 1.0, 0.0); 37 | #ifdef USE_TANGENT 38 | vec3 objectTangent = vec3( tangent.xyz ); 39 | #endif 40 | 41 | // #include 42 | 43 | vec3 transformed = vec3( position ); 44 | #ifdef USE_ALPHAHASH 45 | vPosition = vec3( position ); 46 | #endif 47 | 48 | vec4 bugHashVal = hash42(offset.xz); 49 | 50 | float BUG_SCALE = mix(0.35, 0.55, bugHashVal.z); 51 | transformed *= BUG_SCALE; 52 | 53 | const float FLAP_SPEED = 20.0; 54 | float flapTimeSample = time * FLAP_SPEED + bugHashVal.x * 100.0; 55 | transformed.y += mix(0.0, sin(flapTimeSample), abs(position.x)) * BUG_SCALE; 56 | transformed.x *= abs(cos(flapTimeSample)); 57 | 58 | float TIME_PERIOD = 20.0; 59 | float repeatingTime = TIME_PERIOD * 0.5 - abs(mod(time, TIME_PERIOD) - TIME_PERIOD * 0.5); 60 | 61 | float height = noise11(time * 3.0 + bugHashVal.x * 100.0); 62 | // transformed.y += height * 0.5; 63 | 64 | // Loop 65 | float loopTime = time * 0.5 + bugHashVal.x * 123.23; 66 | float loopSize = 2.0; 67 | vec3 bugsOffset = vec3(sin(loopTime) * loopSize, height * 0.125, cos(loopTime) * loopSize) + offset; 68 | 69 | // Forward 70 | transformed = rotateY(-loopTime + PI / 2.0) * transformed; 71 | transformed += bugsOffset; 72 | 73 | // Center 74 | vec3 bugCenter = offset; 75 | 76 | vec3 bugsWorldPos = (modelMatrix * vec4(bugCenter, 1.0)).xyz; 77 | vec2 heightmapUV = vec2( 78 | remap(bugsWorldPos.x, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 0.0, 1.0), 79 | remap(bugsWorldPos.z, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 1.0, 0.0)); 80 | float terrainHeight = texture2D(heightmap, heightmapUV).x * heightmapParams.x - heightmapParams.y; 81 | transformed.y += terrainHeight; 82 | 83 | if (terrainHeight < -11.0) { 84 | transformed.y -= 1000.0; 85 | } 86 | 87 | objectNormal = normal; 88 | 89 | #include 90 | #include 91 | #include 92 | #include 93 | #include 94 | 95 | #include 96 | #include 97 | #include 98 | 99 | // #include 100 | vec4 mvPosition = vec4( transformed, 1.0 ); 101 | #ifdef USE_INSTANCING 102 | mvPosition = instanceMatrix * mvPosition; 103 | #endif 104 | mvPosition = modelViewMatrix * mvPosition; 105 | gl_Position = projectionMatrix * mvPosition; 106 | 107 | #include 108 | #include 109 | vViewPosition = - mvPosition.xyz; 110 | #include 111 | #include 112 | #include 113 | #include 114 | 115 | vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz; 116 | } -------------------------------------------------------------------------------- /public/shaders/common.glsl: -------------------------------------------------------------------------------- 1 | // #define PI 3.14159265359 2 | 3 | 4 | float saturate(float x) { 5 | return clamp(x, 0.0, 1.0); 6 | } 7 | 8 | vec2 saturate2(vec2 x) { 9 | return clamp(x, vec2(0.0), vec2(1.0)); 10 | } 11 | 12 | vec3 saturate3(vec3 x) { 13 | return clamp(x, vec3(0.0), vec3(1.0)); 14 | } 15 | 16 | 17 | float linearstep(float minValue, float maxValue, float v) { 18 | return clamp((v - minValue) / (maxValue - minValue), 0.0, 1.0); 19 | } 20 | 21 | float inverseLerp(float minValue, float maxValue, float v) { 22 | return (v - minValue) / (maxValue - minValue); 23 | } 24 | 25 | float inverseLerpSat(float minValue, float maxValue, float v) { 26 | return saturate((v - minValue) / (maxValue - minValue)); 27 | } 28 | 29 | float remap(float v, float inMin, float inMax, float outMin, float outMax) { 30 | float t = inverseLerp(inMin, inMax, v); 31 | return mix(outMin, outMax, t); 32 | } 33 | 34 | vec3 LINEAR_TO_GAMMA(vec3 value) { 35 | vec3 colour = pow(value, vec3(1.0 / 2.2)); 36 | 37 | return colour; 38 | } 39 | 40 | vec3 GAMMA_TO_LINEAR(vec3 value) { 41 | vec3 colour = pow(value, vec3(2.2)); 42 | 43 | return colour; 44 | } 45 | 46 | 47 | float easeOut(float x, float t) { 48 | return 1.0 - pow(1.0 - x, t); 49 | } 50 | 51 | float easeIn(float x, float t) { 52 | return pow(x, t); 53 | } 54 | 55 | 56 | mat2 rotate2D(float angle) { 57 | float s = sin(angle); 58 | float c = cos(angle); 59 | return mat2(c, -s, s, c); 60 | } 61 | 62 | mat3 rotateX(float theta) { 63 | float c = cos(theta); 64 | float s = sin(theta); 65 | return mat3( 66 | vec3(1, 0, 0), 67 | vec3(0, c, -s), 68 | vec3(0, s, c) 69 | ); 70 | } 71 | 72 | // Rotation matrix around the Y axis. 73 | mat3 rotateY(float theta) { 74 | float c = cos(theta); 75 | float s = sin(theta); 76 | return mat3( 77 | vec3(c, 0, s), 78 | vec3(0, 1, 0), 79 | vec3(-s, 0, c) 80 | ); 81 | } 82 | 83 | // Rotation matrix around the Z axis. 84 | mat3 rotateZ(float theta) { 85 | float c = cos(theta); 86 | float s = sin(theta); 87 | return mat3( 88 | vec3(c, -s, 0), 89 | vec3(s, c, 0), 90 | vec3(0, 0, 1) 91 | ); 92 | } 93 | 94 | mat3 rotateAxis(vec3 axis, float angle) { 95 | axis = normalize(axis); 96 | float s = sin(angle); 97 | float c = cos(angle); 98 | float oc = 1.0 - c; 99 | 100 | return mat3( 101 | oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 102 | oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 103 | oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /public/shaders/grass-lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | #define PHONG 2 | uniform vec3 diffuse; 3 | uniform vec3 emissive; 4 | uniform vec3 specular; 5 | uniform float shininess; 6 | uniform float opacity; 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | // #include 26 | 27 | uniform sampler2D grassTexture; 28 | uniform vec3 grassLODColour; 29 | uniform float time; 30 | uniform mat3 normalMatrix; 31 | 32 | varying vec3 vGrassColour; 33 | varying vec4 vGrassParams; 34 | varying vec3 vNormal2; 35 | varying vec3 vWorldPosition; 36 | 37 | varying vec3 vViewPosition; 38 | struct BlinnPhongMaterial { 39 | vec3 diffuseColor; 40 | vec3 specularColor; 41 | float specularShininess; 42 | float specularStrength; 43 | }; 44 | 45 | 46 | void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { 47 | float wrap = 0.5; 48 | float dotNL = saturate( (dot( geometry.normal, directLight.direction ) + wrap) / (1.0 + wrap) ); 49 | vec3 irradiance = dotNL * directLight.color; 50 | reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); 51 | reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularShininess ) * material.specularStrength; 52 | 53 | // Backscatter fakery 54 | wrap = 0.5; 55 | float backLight = saturate((dot(geometry.viewDir, -directLight.direction) + wrap) / (1.0 + wrap)); 56 | float falloff = 0.5;//mix(0.5, pow(1.0 - saturate(dot(geometry.viewDir, geometry.normal)), 2.0), 1.0) * 0.5; 57 | vec3 scatter = directLight.color * pow(backLight, 1.0) * falloff * BRDF_Lambert(material.diffuseColor); 58 | 59 | reflectedLight.indirectDiffuse += scatter * (1.0 - vGrassParams.z); 60 | } 61 | void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { 62 | reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); 63 | } 64 | #define RE_Direct RE_Direct_BlinnPhong 65 | #define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong 66 | 67 | #include 68 | #include 69 | #include 70 | #include 71 | #include 72 | #include 73 | 74 | 75 | void main() { 76 | vec3 viewDir = normalize(cameraPosition - vWorldPosition); 77 | 78 | #include 79 | vec4 diffuseColor = vec4( diffuse, opacity ); 80 | 81 | // Grass 82 | float heightPercent = vGrassParams.x; 83 | float height = vGrassParams.y; 84 | float lodFadeIn = vGrassParams.z; 85 | float lodFadeOut = 1.0 - lodFadeIn; 86 | 87 | float grassMiddle = mix( 88 | smoothstep(abs(vGrassParams.w - 0.5), 0.0, 0.1), 1.0, lodFadeIn); 89 | 90 | float isSandy = saturate(linearstep(-11.0, -14.0, height)); 91 | 92 | float density = 1.0 - isSandy; 93 | 94 | // Density is in the range [0, 1] 95 | // 0 being no grass 96 | // 1 being full grass 97 | float aoForDensity = mix(1.0, 0.25, density); 98 | float ao = mix(aoForDensity, 1.0, easeIn(heightPercent, 2.0)); 99 | 100 | diffuseColor.rgb *= vGrassColour; 101 | diffuseColor.rgb *= mix(0.85, 1.0, grassMiddle); 102 | diffuseColor.rgb *= ao; 103 | 104 | 105 | ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); 106 | vec3 totalEmissiveRadiance = emissive; 107 | #include 108 | #include 109 | #include 110 | #include 111 | #include 112 | #include 113 | #include 114 | #include 115 | #include 116 | 117 | vec3 normal2 = normalize(vNormal2); 118 | normal = normalize(mix(vNormal, normal2, vGrassParams.w)); 119 | 120 | #include 121 | // #include 122 | 123 | BlinnPhongMaterial material; 124 | material.diffuseColor = diffuseColor.rgb; 125 | material.specularColor = specular; 126 | 127 | #include 128 | #include 129 | #include 130 | #include 131 | vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; 132 | 133 | #include 134 | #include 135 | #include 136 | #include 137 | // #include 138 | 139 | gl_FragColor.xyz = CalculateFog(gl_FragColor.xyz, viewDir, vFogDepth); 140 | 141 | #include 142 | #include 143 | } -------------------------------------------------------------------------------- /public/shaders/grass-lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | #define PHONG 4 | varying vec3 vViewPosition; 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | varying vec3 vWorldNormal; 19 | varying vec3 vGrassColour; 20 | varying vec4 vGrassParams; 21 | varying vec3 vNormal2; 22 | varying vec3 vWorldPosition; 23 | 24 | uniform vec2 grassSize; 25 | uniform vec4 grassParams; 26 | uniform vec4 grassDraw; 27 | uniform float time; 28 | uniform sampler2D heightmap; 29 | uniform vec4 heightParams; 30 | uniform vec3 playerPos; 31 | uniform mat4 viewMatrixInverse; 32 | 33 | attribute float vertIndex; 34 | 35 | 36 | void main() { 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | 43 | vec3 grassOffset = vec3(position.x, 0.0, position.y); 44 | 45 | // Blade world position 46 | vec3 grassBladeWorldPos = (modelMatrix * vec4(grassOffset, 1.0)).xyz; 47 | vec2 heightmapUV = vec2( 48 | remap(grassBladeWorldPos.x, -heightParams.x * 0.5, heightParams.x * 0.5, 0.0, 1.0), 49 | remap(grassBladeWorldPos.z, -heightParams.x * 0.5, heightParams.x * 0.5, 1.0, 0.0)); 50 | vec4 heightmapSample = texture2D(heightmap, heightmapUV); 51 | grassBladeWorldPos.y += heightmapSample.x * grassParams.z - grassParams.w; 52 | 53 | float heightmapSampleHeight = 1.0;//mix(0.5, 1.0, heightmapSample.y); 54 | 55 | vec4 hashVal1 = hash42(vec2(grassBladeWorldPos.x, grassBladeWorldPos.z)); 56 | 57 | float highLODOut = smoothstep(grassDraw.x * 0.5, grassDraw.x, distance(cameraPosition, grassBladeWorldPos)); 58 | float lodFadeIn = smoothstep(grassDraw.x, grassDraw.y, distance(cameraPosition, grassBladeWorldPos)); 59 | 60 | // Check terrain type, maybe don't allow grass blade 61 | float isSandy = linearstep(-11.0, -14.0, grassBladeWorldPos.y); 62 | float grassAllowedHash = hashVal1.w - isSandy; 63 | float isGrassAllowed = step(0.0, grassAllowedHash); 64 | 65 | float randomAngle = hashVal1.x * 2.0 * 3.14159; 66 | float randomShade = remap(hashVal1.y, -1.0, 1.0, 0.5, 1.0); 67 | float randomHeight = remap(hashVal1.z, 0.0, 1.0, 0.75, 1.5) * mix(1.0, 0.0, lodFadeIn) * isGrassAllowed * heightmapSampleHeight; 68 | float randomWidth = (1.0 - isSandy) * heightmapSampleHeight; 69 | float randomLean = remap(hashVal1.w, 0.0, 1.0, 0.1, 0.4); 70 | 71 | vec2 hashGrassColour = hash22(vec2(grassBladeWorldPos.x, grassBladeWorldPos.z)); 72 | float leanAnimation = noise12(vec2(time * 0.35) + grassBladeWorldPos.xz * 137.423) * 0.1; 73 | 74 | float GRASS_SEGMENTS = grassParams.x; 75 | float GRASS_VERTICES = grassParams.y; 76 | 77 | // Figure out vertex id, > GRASS_VERTICES is back side 78 | float vertID = mod(float(vertIndex), GRASS_VERTICES); 79 | 80 | // 1 = front, -1 = back 81 | float zSide = -(floor(vertIndex / GRASS_VERTICES) * 2.0 - 1.0); 82 | 83 | // 0 = left, 1 = right 84 | float xSide = mod(vertID, 2.0); 85 | 86 | float heightPercent = (vertID - xSide) / (GRASS_SEGMENTS * 2.0); 87 | 88 | float grassTotalHeight = grassSize.y * randomHeight; 89 | float grassTotalWidthHigh = easeOut(1.0 - heightPercent, 2.0); 90 | float grassTotalWidthLow = 1.0 - heightPercent; 91 | float grassTotalWidth = grassSize.x * mix(grassTotalWidthHigh, grassTotalWidthLow, highLODOut) * randomWidth; 92 | 93 | // Shift verts 94 | float x = (xSide - 0.5) * grassTotalWidth; 95 | float y = heightPercent * grassTotalHeight; 96 | 97 | float windDir = noise12(grassBladeWorldPos.xz * 0.05 + 0.05 * time); 98 | float windNoiseSample = noise12(grassBladeWorldPos.xz * 0.25 + time * 1.0); 99 | float windLeanAngle = remap(windNoiseSample, -1.0, 1.0, 0.25, 1.0); 100 | windLeanAngle = easeIn(windLeanAngle, 2.0) * 1.25; 101 | vec3 windAxis = vec3(cos(windDir), 0.0, sin(windDir)); 102 | 103 | windLeanAngle *= heightPercent; 104 | 105 | float distToPlayer = distance(grassBladeWorldPos.xz, playerPos.xz); 106 | float playerFalloff = smoothstep(2.5, 1.0, distToPlayer); 107 | float playerLeanAngle = mix(0.0, 0.2, playerFalloff * linearstep(0.5, 0.0, windLeanAngle)); 108 | vec3 grassToPlayer = normalize(vec3(playerPos.x, 0.0, playerPos.z) - vec3(grassBladeWorldPos.x, 0.0, grassBladeWorldPos.z)); 109 | vec3 playerLeanAxis = vec3(grassToPlayer.z, 0, -grassToPlayer.x); 110 | 111 | randomLean += leanAnimation; 112 | 113 | float easedHeight = mix(easeIn(heightPercent, 2.0), 1.0, highLODOut); 114 | float curveAmount = -randomLean * easedHeight; 115 | 116 | float ncurve1 = -randomLean * easedHeight; 117 | vec3 n1 = vec3(0.0, (heightPercent + 0.01), 0.0); 118 | n1 = rotateX(ncurve1) * n1; 119 | 120 | float ncurve2 = -randomLean * easedHeight * 0.9; 121 | vec3 n2 = vec3(0.0, (heightPercent + 0.01) * 0.9, 0.0); 122 | n2 = rotateX(ncurve2) * n2; 123 | 124 | vec3 ncurve = normalize(n1 - n2); 125 | 126 | mat3 grassMat = rotateAxis(playerLeanAxis, playerLeanAngle) * rotateAxis(windAxis, windLeanAngle) * rotateY(randomAngle); 127 | 128 | vec3 grassFaceNormal = vec3(0.0, 0.0, 1.0); 129 | grassFaceNormal = grassMat * grassFaceNormal; 130 | grassFaceNormal *= zSide; 131 | 132 | vec3 grassVertexNormal = vec3(0.0, -ncurve.z, ncurve.y); 133 | vec3 grassVertexNormal1 = rotateY(PI * 0.3 * zSide) * grassVertexNormal; 134 | vec3 grassVertexNormal2 = rotateY(PI * -0.3 * zSide) * grassVertexNormal; 135 | 136 | grassVertexNormal1 = grassMat * grassVertexNormal1; 137 | grassVertexNormal1 *= zSide; 138 | 139 | grassVertexNormal2 = grassMat * grassVertexNormal2; 140 | grassVertexNormal2 *= zSide; 141 | 142 | vec3 grassVertexPosition = vec3(x, y, 0.0); 143 | grassVertexPosition = rotateX(curveAmount) * grassVertexPosition; 144 | grassVertexPosition = grassMat * grassVertexPosition; 145 | 146 | grassVertexPosition += grassOffset; 147 | 148 | vec3 b1 = vec3(0.02, 0.075, 0.01); 149 | vec3 b2 = vec3(0.025, 0.1, 0.01); 150 | vec3 t1 = vec3(0.65, 0.8, 0.25); 151 | vec3 t2 = vec3(0.8, 0.9, 0.4); 152 | 153 | vec3 baseColour = mix(b1, b2, hashGrassColour.x); 154 | vec3 tipColour = mix(t1, t2, hashGrassColour.y); 155 | vec3 highLODColour = mix(baseColour, tipColour, easeIn(heightPercent, 4.0)) * randomShade; 156 | vec3 lowLODColour = mix(b1, t1, heightPercent); 157 | vGrassColour = mix(highLODColour, lowLODColour, highLODOut); 158 | vGrassParams = vec4(heightPercent, grassBladeWorldPos.y, highLODOut, xSide); 159 | 160 | const float SKY_RATIO = 0.25; 161 | // TODO: Grab terrain normal 162 | vec3 UP = vec3(0.0, 1.0, 0.0); 163 | // float skyFadeIn = smoothstep(grassDraw.x * 0.5, grassDraw.x, distance(cameraPosition, grassBladeWorldPos)) * SKY_RATIO; 164 | float skyFadeIn = (1.0 - highLODOut) * SKY_RATIO; 165 | vec3 normal1 = normalize(mix(UP, grassVertexNormal1, skyFadeIn)); 166 | vec3 normal2 = normalize(mix(UP, grassVertexNormal2, skyFadeIn)); 167 | 168 | transformed = grassVertexPosition; 169 | transformed.y += grassBladeWorldPos.y; 170 | 171 | vec3 cameraWorldLeft = (viewMatrixInverse * vec4(-1.0, 0.0, 0.0, 0.0)).xyz; 172 | 173 | vec3 viewDir = normalize(cameraPosition - grassBladeWorldPos); 174 | vec3 viewDirXZ = normalize(vec3(viewDir.x, 0.0, viewDir.z)); 175 | 176 | vec3 grassFaceNormalXZ = normalize(vec3(grassFaceNormal.x, 0.0, grassFaceNormal.z)); 177 | 178 | float viewDotNormal = saturate(dot(grassFaceNormal, viewDirXZ)); 179 | float viewSpaceThickenFactor = easeOut(1.0 - viewDotNormal, 4.0) * smoothstep(0.0, 0.2, viewDotNormal); 180 | 181 | objectNormal = grassVertexNormal1; 182 | 183 | #include 184 | #include 185 | #include 186 | 187 | #include 188 | #include 189 | 190 | vNormal = normalize(normalMatrix * normal1); 191 | vNormal2 = normalize(normalMatrix * normal2); 192 | 193 | #include 194 | #include 195 | #include 196 | 197 | // #include 198 | vec4 mvPosition = vec4( transformed, 1.0 ); 199 | #ifdef USE_INSTANCING 200 | mvPosition = instanceMatrix * mvPosition; 201 | #endif 202 | mvPosition = modelViewMatrix * mvPosition; 203 | 204 | // HACK 205 | mvPosition.x += viewSpaceThickenFactor * (xSide - 0.5) * grassTotalWidth * 0.5 * zSide; 206 | 207 | gl_Position = projectionMatrix * mvPosition; 208 | 209 | #include 210 | #include 211 | vViewPosition = - mvPosition.xyz; 212 | #include 213 | #include 214 | #include 215 | #include 216 | 217 | vWorldPosition = worldPosition.xyz; 218 | } -------------------------------------------------------------------------------- /public/shaders/header.glsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | vec3 COLOUR_LIGHT_BLUE = vec3(0.42, 0.65, 0.85); 4 | vec3 COLOUR_LIGHT_GREEN = vec3(0.25, 1.0, 0.25); 5 | vec3 COLOUR_PALE_GREEN = vec3(0.42, 0.85, 0.65); 6 | vec3 COLOUR_LIGHT_PURPLE = vec3(0.85, 0.25, 0.85); 7 | vec3 COLOUR_BRIGHT_PINK = vec3(1.0, 0.5, 0.5); 8 | 9 | vec3 COLOUR_BRIGHT_RED = vec3(1.0, 0.1, 0.02); 10 | vec3 COLOUR_BRIGHT_BLUE = vec3(0.01, 0.2, 1.0); 11 | vec3 COLOUR_BRIGHT_GREEN = vec3(0.01, 1.0, 0.2); 12 | vec3 COLOUR_PALE_BLUE = vec3(0.42, 0.65, 0.85); 13 | vec3 COLOUR_LIGHT_YELLOW = vec3(1.0, 1.0, 0.25); 14 | 15 | 16 | 17 | #define USE_OKLAB 18 | -------------------------------------------------------------------------------- /public/shaders/lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | #define STANDARD 2 | #ifdef PHYSICAL 3 | #define IOR 4 | #define USE_SPECULAR 5 | #endif 6 | uniform vec3 diffuse; 7 | uniform vec3 emissive; 8 | uniform float roughness; 9 | uniform float metalness; 10 | uniform float opacity; 11 | #ifdef IOR 12 | uniform float ior; 13 | #endif 14 | #ifdef USE_SPECULAR 15 | uniform float specularIntensity; 16 | uniform vec3 specularColor; 17 | #ifdef USE_SPECULAR_COLORMAP 18 | uniform sampler2D specularColorMap; 19 | #endif 20 | #ifdef USE_SPECULAR_INTENSITYMAP 21 | uniform sampler2D specularIntensityMap; 22 | #endif 23 | #endif 24 | #ifdef USE_CLEARCOAT 25 | uniform float clearcoat; 26 | uniform float clearcoatRoughness; 27 | #endif 28 | #ifdef USE_IRIDESCENCE 29 | uniform float iridescence; 30 | uniform float iridescenceIOR; 31 | uniform float iridescenceThicknessMinimum; 32 | uniform float iridescenceThicknessMaximum; 33 | #endif 34 | #ifdef USE_SHEEN 35 | uniform vec3 sheenColor; 36 | uniform float sheenRoughness; 37 | #ifdef USE_SHEEN_COLORMAP 38 | uniform sampler2D sheenColorMap; 39 | #endif 40 | #ifdef USE_SHEEN_ROUGHNESSMAP 41 | uniform sampler2D sheenRoughnessMap; 42 | #endif 43 | #endif 44 | #ifdef USE_ANISOTROPY 45 | uniform vec2 anisotropyVector; 46 | #ifdef USE_ANISOTROPYMAP 47 | uniform sampler2D anisotropyMap; 48 | #endif 49 | #endif 50 | varying vec3 vViewPosition; 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | #include 66 | #include 67 | #include 68 | #include 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #include 79 | #include 80 | #include 81 | 82 | varying vec3 vWorldNormal; 83 | 84 | void main() { 85 | #include 86 | vec4 diffuseColor = vec4( diffuse, opacity ); 87 | ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); 88 | vec3 totalEmissiveRadiance = emissive; 89 | #include 90 | #include 91 | #include 92 | #include 93 | #include 94 | #include 95 | #include 96 | #include 97 | #include 98 | #include 99 | #include 100 | #include 101 | #include 102 | #include 103 | #include 104 | #include 105 | #include 106 | #include 107 | vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; 108 | vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; 109 | #include 110 | vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; 111 | #ifdef USE_SHEEN 112 | float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor ); 113 | outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecular; 114 | #endif 115 | #ifdef USE_CLEARCOAT 116 | float dotNVcc = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) ); 117 | vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); 118 | outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat; 119 | #endif 120 | 121 | #include 122 | #include 123 | #include 124 | #include 125 | #include 126 | #include 127 | } 128 | -------------------------------------------------------------------------------- /public/shaders/lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | #define STANDARD 2 | varying vec3 vViewPosition; 3 | #ifdef USE_TRANSMISSION 4 | varying vec3 vWorldPosition; 5 | #endif 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | varying vec3 vWorldNormal; 19 | 20 | void main() { 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | vViewPosition = - mvPosition.xyz; 39 | 40 | // #include 41 | #if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 42 | vec4 worldPosition = vec4( transformed, 1.0 ); 43 | #ifdef USE_INSTANCING 44 | worldPosition = instanceMatrix * worldPosition; 45 | #endif 46 | worldPosition = modelMatrix * worldPosition; 47 | #endif 48 | 49 | #include 50 | #include 51 | #ifdef USE_TRANSMISSION 52 | vWorldPosition = worldPosition.xyz; 53 | #endif 54 | 55 | vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz; 56 | // vWorldNormal = normal.xyz; 57 | } -------------------------------------------------------------------------------- /public/shaders/noise.glsl: -------------------------------------------------------------------------------- 1 | // Virtually all of these were taken from: https://www.shadertoy.com/view/ttc3zr 2 | 3 | uvec4 murmurHash42(uvec2 src) { 4 | const uint M = 0x5bd1e995u; 5 | uvec4 h = uvec4(1190494759u, 2147483647u, 3559788179u, 179424673u); 6 | src *= M; src ^= src>>24u; src *= M; 7 | h *= M; h ^= src.x; h *= M; h ^= src.y; 8 | h ^= h>>13u; h *= M; h ^= h>>15u; 9 | return h; 10 | } 11 | 12 | uint murmurHash11(uint src) { 13 | const uint M = 0x5bd1e995u; 14 | uint h = 1190494759u; 15 | src *= M; src ^= src>>24u; src *= M; 16 | h *= M; h ^= src; 17 | h ^= h>>13u; h *= M; h ^= h>>15u; 18 | return h; 19 | } 20 | 21 | uint murmurHash12(uvec2 src) { 22 | const uint M = 0x5bd1e995u; 23 | uint h = 1190494759u; 24 | src *= M; src ^= src>>24u; src *= M; 25 | h *= M; h ^= src.x; h *= M; h ^= src.y; 26 | h ^= h>>13u; h *= M; h ^= h>>15u; 27 | return h; 28 | } 29 | 30 | uint murmurHash13(uvec3 src) { 31 | const uint M = 0x5bd1e995u; 32 | uint h = 1190494759u; 33 | src *= M; src ^= src>>24u; src *= M; 34 | h *= M; h ^= src.x; h *= M; h ^= src.y; h *= M; h ^= src.z; 35 | h ^= h>>13u; h *= M; h ^= h>>15u; 36 | return h; 37 | } 38 | 39 | uvec2 murmurHash22(uvec2 src) { 40 | const uint M = 0x5bd1e995u; 41 | uvec2 h = uvec2(1190494759u, 2147483647u); 42 | src *= M; src ^= src>>24u; src *= M; 43 | h *= M; h ^= src.x; h *= M; h ^= src.y; 44 | h ^= h>>13u; h *= M; h ^= h>>15u; 45 | return h; 46 | } 47 | 48 | uvec2 murmurHash21(uint src) { 49 | const uint M = 0x5bd1e995u; 50 | uvec2 h = uvec2(1190494759u, 2147483647u); 51 | src *= M; src ^= src>>24u; src *= M; 52 | h *= M; h ^= src; 53 | h ^= h>>13u; h *= M; h ^= h>>15u; 54 | return h; 55 | } 56 | 57 | uvec2 murmurHash23(uvec3 src) { 58 | const uint M = 0x5bd1e995u; 59 | uvec2 h = uvec2(1190494759u, 2147483647u); 60 | src *= M; src ^= src>>24u; src *= M; 61 | h *= M; h ^= src.x; h *= M; h ^= src.y; h *= M; h ^= src.z; 62 | h ^= h>>13u; h *= M; h ^= h>>15u; 63 | return h; 64 | } 65 | 66 | uvec3 murmurHash31(uint src) { 67 | const uint M = 0x5bd1e995u; 68 | uvec3 h = uvec3(1190494759u, 2147483647u, 3559788179u); 69 | src *= M; src ^= src>>24u; src *= M; 70 | h *= M; h ^= src; 71 | h ^= h>>13u; h *= M; h ^= h>>15u; 72 | return h; 73 | } 74 | 75 | uvec3 murmurHash33(uvec3 src) { 76 | const uint M = 0x5bd1e995u; 77 | uvec3 h = uvec3(1190494759u, 2147483647u, 3559788179u); 78 | src *= M; src ^= src>>24u; src *= M; 79 | h *= M; h ^= src.x; h *= M; h ^= src.y; h *= M; h ^= src.z; 80 | h ^= h>>13u; h *= M; h ^= h>>15u; 81 | return h; 82 | } 83 | 84 | // 3 outputs, 3 inputs 85 | vec3 hash33(vec3 src) { 86 | uvec3 h = murmurHash33(floatBitsToUint(src)); 87 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 88 | } 89 | 90 | // 1 output, 1 input 91 | float hash11(float src) { 92 | uint h = murmurHash11(floatBitsToUint(src)); 93 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 94 | } 95 | 96 | // 1 output, 2 inputs 97 | float hash12(vec2 src) { 98 | uint h = murmurHash12(floatBitsToUint(src)); 99 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 100 | } 101 | 102 | // 1 output, 3 inputs 103 | float hash13(vec3 src) { 104 | uint h = murmurHash13(floatBitsToUint(src)); 105 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 106 | } 107 | 108 | // 2 outputs, 1 input 109 | vec2 hash21(float src) { 110 | uvec2 h = murmurHash21(floatBitsToUint(src)); 111 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 112 | } 113 | 114 | // 3 outputs, 1 input 115 | vec3 hash31(float src) { 116 | uvec3 h = murmurHash31(floatBitsToUint(src)); 117 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 118 | } 119 | 120 | // 2 outputs, 2 inputs 121 | vec2 hash22(vec2 src) { 122 | uvec2 h = murmurHash22(floatBitsToUint(src)); 123 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 124 | } 125 | 126 | // 4 outputs, 2 inputs 127 | vec4 hash42(vec2 src) { 128 | uvec4 h = murmurHash42(floatBitsToUint(src)); 129 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 130 | } 131 | 132 | 133 | // 2 outputs, 3 inputs 134 | vec2 hash23(vec3 src) { 135 | uvec2 h = murmurHash23(floatBitsToUint(src)); 136 | return uintBitsToFloat(h & 0x007fffffu | 0x3f800000u) - 1.0; 137 | } 138 | 139 | float noise11(float p) { 140 | float i = floor(p); 141 | 142 | float f = fract(p); 143 | float u = smoothstep(0.0, 1.0, f); 144 | 145 | float val = mix( hash11(i + 0.0), 146 | hash11(i + 1.0), u); 147 | return val * 2.0 - 1.0; 148 | } 149 | 150 | float noise12(vec2 p) { 151 | vec2 i = floor(p); 152 | 153 | vec2 f = fract(p); 154 | vec2 u = smoothstep(vec2(0.0), vec2(1.0), f); 155 | 156 | float val = mix( mix( hash12( i + vec2(0.0, 0.0) ), 157 | hash12( i + vec2(1.0, 0.0) ), u.x), 158 | mix( hash12( i + vec2(0.0, 1.0) ), 159 | hash12( i + vec2(1.0, 1.0) ), u.x), u.y); 160 | return val * 2.0 - 1.0; 161 | } 162 | 163 | float noise13(vec3 x) { 164 | vec3 i = floor(x); 165 | vec3 f = fract(x); 166 | f = f*f*(3.0-2.0*f); 167 | 168 | return mix(mix(mix( hash13(i+vec3(0.0, 0.0, 0.0)), 169 | hash13(i+vec3(1.0, 0.0, 0.0)),f.x), 170 | mix( hash13(i+vec3(0.0, 1.0, 0.0)), 171 | hash13(i+vec3(1.0, 1.0, 0.0)),f.x),f.y), 172 | mix(mix( hash13(i+vec3(0.0, 0.0, 1.0)), 173 | hash13(i+vec3(1.0, 0.0, 1.0)),f.x), 174 | mix( hash13(i+vec3(0.0, 1.0, 1.0)), 175 | hash13(i+vec3(1.0, 1.0, 1.0)),f.x),f.y),f.z); 176 | } 177 | 178 | vec2 noise23(vec3 x) { 179 | vec3 i = floor(x); 180 | vec3 f = fract(x); 181 | f = f*f*(3.0-2.0*f); 182 | 183 | return mix(mix(mix( hash23(i+vec3(0.0, 0.0, 0.0)), 184 | hash23(i+vec3(1.0, 0.0, 0.0)),f.x), 185 | mix( hash23(i+vec3(0.0, 1.0, 0.0)), 186 | hash23(i+vec3(1.0, 1.0, 0.0)),f.x),f.y), 187 | mix(mix( hash23(i+vec3(0.0, 0.0, 1.0)), 188 | hash23(i+vec3(1.0, 0.0, 1.0)),f.x), 189 | mix( hash23(i+vec3(0.0, 1.0, 1.0)), 190 | hash23(i+vec3(1.0, 1.0, 1.0)),f.x),f.y),f.z); 191 | } 192 | 193 | vec2 noise22(vec2 p) { 194 | vec2 i = floor(p); 195 | 196 | vec2 f = fract(p); 197 | vec2 u = smoothstep(vec2(0.0), vec2(1.0), f); 198 | 199 | vec2 val = mix( mix( hash22( i + vec2(0.0, 0.0) ), 200 | hash22( i + vec2(1.0, 0.0) ), u.x), 201 | mix( hash22( i + vec2(0.0, 1.0) ), 202 | hash22( i + vec2(1.0, 1.0) ), u.x), u.y); 203 | return val * 2.0 - 1.0; 204 | } 205 | 206 | // The MIT License 207 | // Copyright © 2017 Inigo Quilez 208 | // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 209 | // https://www.youtube.com/c/InigoQuilez 210 | // https://iquilezles.org/ 211 | vec4 noised_1_3(vec3 x) { 212 | vec3 i = floor(x); 213 | vec3 f = fract(x); 214 | 215 | // quintic interpolant 216 | vec3 u = f*f*f*(f*(f*6.0-15.0)+10.0); 217 | vec3 du = 30.0*f*f*(f*(f-2.0)+1.0); 218 | 219 | // gradients 220 | vec3 ga = hash33( i+vec3(0.0,0.0,0.0) ) * 2.0 - 1.0; 221 | vec3 gb = hash33( i+vec3(1.0,0.0,0.0) ) * 2.0 - 1.0; 222 | vec3 gc = hash33( i+vec3(0.0,1.0,0.0) ) * 2.0 - 1.0; 223 | vec3 gd = hash33( i+vec3(1.0,1.0,0.0) ) * 2.0 - 1.0; 224 | vec3 ge = hash33( i+vec3(0.0,0.0,1.0) ) * 2.0 - 1.0; 225 | vec3 gf = hash33( i+vec3(1.0,0.0,1.0) ) * 2.0 - 1.0; 226 | vec3 gg = hash33( i+vec3(0.0,1.0,1.0) ) * 2.0 - 1.0; 227 | vec3 gh = hash33( i+vec3(1.0,1.0,1.0) ) * 2.0 - 1.0; 228 | 229 | // projections 230 | float va = dot( ga, f-vec3(0.0,0.0,0.0) ); 231 | float vb = dot( gb, f-vec3(1.0,0.0,0.0) ); 232 | float vc = dot( gc, f-vec3(0.0,1.0,0.0) ); 233 | float vd = dot( gd, f-vec3(1.0,1.0,0.0) ); 234 | float ve = dot( ge, f-vec3(0.0,0.0,1.0) ); 235 | float vf = dot( gf, f-vec3(1.0,0.0,1.0) ); 236 | float vg = dot( gg, f-vec3(0.0,1.0,1.0) ); 237 | float vh = dot( gh, f-vec3(1.0,1.0,1.0) ); 238 | 239 | // interpolations 240 | return vec4( va + u.x*(vb-va) + u.y*(vc-va) + u.z*(ve-va) + u.x*u.y*(va-vb-vc+vd) + u.y*u.z*(va-vc-ve+vg) + u.z*u.x*(va-vb-ve+vf) + (-va+vb+vc-vd+ve-vf-vg+vh)*u.x*u.y*u.z, // value 241 | ga + u.x*(gb-ga) + u.y*(gc-ga) + u.z*(ge-ga) + u.x*u.y*(ga-gb-gc+gd) + u.y*u.z*(ga-gc-ge+gg) + u.z*u.x*(ga-gb-ge+gf) + (-ga+gb+gc-gd+ge-gf-gg+gh)*u.x*u.y*u.z + // derivatives 242 | du * (vec3(vb,vc,ve) - va + u.yzx*vec3(va-vb-vc+vd,va-vc-ve+vg,va-vb-ve+vf) + u.zxy*vec3(va-vb-ve+vf,va-vb-vc+vd,va-vc-ve+vg) + u.yzx*u.zxy*(-va+vb+vc-vd+ve-vf-vg+vh) )); 243 | } 244 | 245 | float FBM_1_2(vec2 p, int octaves, float persistence, float lacunarity) { 246 | float amplitude = 1.0; 247 | float frequency = 1.0; 248 | float total = 0.0; 249 | float normalization = 0.0; 250 | 251 | for (int i = 0; i < octaves; ++i) { 252 | float noiseValue = noise12(p * frequency); 253 | total += noiseValue * amplitude; 254 | normalization += amplitude; 255 | amplitude *= persistence; 256 | frequency *= lacunarity; 257 | } 258 | 259 | total /= normalization; 260 | total = total * 0.5 + 0.5; 261 | 262 | return total; 263 | } 264 | 265 | float FBM_1_3(vec3 p, int octaves, float persistence, float lacunarity) { 266 | float amplitude = 1.0; 267 | float frequency = 1.0; 268 | float total = 0.0; 269 | float normalization = 0.0; 270 | 271 | for (int i = 0; i < octaves; ++i) { 272 | float noiseValue = noise13(p * frequency); 273 | total += noiseValue * amplitude; 274 | normalization += amplitude; 275 | amplitude *= persistence; 276 | frequency *= lacunarity; 277 | } 278 | 279 | total /= normalization; 280 | total = total * 0.5 + 0.5; 281 | 282 | return total; 283 | } 284 | 285 | const mat3 m3 = mat3( 0.00, 0.80, 0.60, 286 | -0.80, 0.36, -0.48, 287 | -0.60, -0.48, 0.64 ); 288 | const mat3 m3i = mat3( 0.00, -0.80, -0.60, 289 | 0.80, 0.36, -0.48, 290 | 0.60, -0.48, 0.64 ); 291 | 292 | vec4 FBM_D_1_4(in vec3 x, int octaves) { 293 | float f = 1.98; // could be 2.0 294 | float s = 0.49; // could be 0.5 295 | float a = 0.0; 296 | float b = 0.5; 297 | vec3 d = vec3(0.0); 298 | mat3 m = mat3( 299 | 1.0,0.0,0.0, 300 | 0.0,1.0,0.0, 301 | 0.0,0.0,1.0); 302 | for( int i=0; i < octaves; i++ ) 303 | { 304 | vec4 n = noised_1_3(x); 305 | a += b*n.x; // accumulate values 306 | d += b*m*n.yzw; // accumulate derivatives 307 | b *= s; 308 | x = f*m3*x; 309 | m = f*m3i*m; 310 | } 311 | return vec4( a, d ); 312 | } 313 | -------------------------------------------------------------------------------- /public/shaders/oklab.glsl: -------------------------------------------------------------------------------- 1 | ///////////////////////////////////////////////////////////////////////// 2 | // 3 | // OKLab stuff, mostly based off https://www.shadertoy.com/view/ttcyRS 4 | // 5 | ///////////////////////////////////////////////////////////////////////// 6 | 7 | const mat3 kLMStoCONE = mat3( 8 | 4.0767245293, -1.2681437731, -0.0041119885, 9 | -3.3072168827, 2.6093323231, -0.7034763098, 10 | 0.2307590544, -0.3411344290, 1.7068625689); 11 | 12 | const mat3 kCONEtoLMS = mat3( 13 | 0.4121656120, 0.2118591070, 0.0883097947, 14 | 0.5362752080, 0.6807189584, 0.2818474174, 15 | 0.0514575653, 0.1074065790, 0.6302613616); 16 | 17 | vec3 rgbToOklab(vec3 c) { 18 | vec3 lms = kCONEtoLMS * c; 19 | 20 | return sign(lms)*pow(abs(lms), vec3(0.3333333333333)); 21 | } 22 | 23 | vec3 oklabToRGB(vec3 c) { 24 | vec3 lms = c; 25 | 26 | return kLMStoCONE * (lms * lms * lms); 27 | } 28 | 29 | 30 | #ifndef USE_OKLAB 31 | #define col3 vec3 32 | #else 33 | vec3 col3(float r, float g, float b) { 34 | return rgbToOklab(vec3(r, g, b)); 35 | } 36 | 37 | vec3 col3(vec3 v) { 38 | return rgbToOklab(v); 39 | } 40 | 41 | vec3 col3(float v) { 42 | return rgbToOklab(vec3(v)); 43 | } 44 | #endif -------------------------------------------------------------------------------- /public/shaders/phong-lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | #define PHONG 2 | uniform vec3 diffuse; 3 | uniform vec3 emissive; 4 | uniform vec3 specular; 5 | uniform float shininess; 6 | uniform float opacity; 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | varying vec3 vViewPosition; 27 | struct BlinnPhongMaterial { 28 | vec3 diffuseColor; 29 | vec3 specularColor; 30 | float specularShininess; 31 | float specularStrength; 32 | }; 33 | void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { 34 | float dotNL = saturate( dot( geometry.normal, directLight.direction ) ); 35 | vec3 irradiance = dotNL * directLight.color; 36 | reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); 37 | reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometry.viewDir, geometry.normal, material.specularColor, material.specularShininess ) * material.specularStrength; 38 | 39 | // Backscatter 40 | float backLight = saturate(dot(geometry.viewDir, -directLight.direction)); 41 | float falloff = pow(1.0 - saturate(dot(geometry.viewDir, geometry.normal)), 2.0); 42 | vec3 scatter = directLight.color * pow(backLight, 4.0) * falloff * BRDF_Lambert(material.diffuseColor); 43 | 44 | // reflectedLight.indirectDiffuse += scatter * 2.0; 45 | } 46 | void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in GeometricContext geometry, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { 47 | reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); 48 | } 49 | #define RE_Direct RE_Direct_BlinnPhong 50 | #define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong 51 | 52 | 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | 60 | 61 | varying vec3 vWorldNormal; 62 | varying vec3 vWorldPosition; 63 | 64 | void main() { 65 | #include 66 | 67 | vec3 viewDir = normalize(cameraPosition - vWorldPosition); 68 | 69 | vec4 diffuseColor = vec4( diffuse, opacity ); 70 | ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); 71 | vec3 totalEmissiveRadiance = emissive; 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #include 79 | #include 80 | #include 81 | #include 82 | 83 | // #include 84 | BlinnPhongMaterial material; 85 | material.diffuseColor = diffuseColor.rgb; 86 | material.specularColor = specular; 87 | material.specularShininess = shininess; 88 | material.specularStrength = specularStrength; 89 | 90 | #include 91 | #include 92 | #include 93 | #include 94 | vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; 95 | 96 | // outgoingLight = normalize(vWorldNormal); 97 | // outgoingLight = reflectedLight.directSpecular; 98 | 99 | #include 100 | #include 101 | #include 102 | #include 103 | // #include 104 | 105 | gl_FragColor.xyz = CalculateFog(gl_FragColor.xyz, viewDir, vFogDepth); 106 | 107 | #include 108 | #include 109 | } -------------------------------------------------------------------------------- /public/shaders/phong-lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | #define PHONG 2 | varying vec3 vViewPosition; 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | varying vec3 vWorldNormal; 17 | varying vec3 vWorldPosition; 18 | 19 | void main() { 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | vViewPosition = - mvPosition.xyz; 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz; 43 | vWorldPosition = (modelMatrix * vec4(transformed, 1.0)).xyz; 44 | } -------------------------------------------------------------------------------- /public/shaders/sky-lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | uniform float time; 2 | 3 | 4 | varying vec3 vWorldPosition; 5 | varying vec3 vWorldNormal; 6 | 7 | 8 | 9 | void main() { 10 | vec3 viewDir = normalize(vWorldPosition - cameraPosition); 11 | vec3 colour = CalculateSkyLighting(viewDir, vWorldNormal); 12 | 13 | gl_FragColor = vec4(colour, 1.0); 14 | } -------------------------------------------------------------------------------- /public/shaders/sky-lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | varying vec3 vWorldPosition; 2 | varying vec3 vWorldNormal; 3 | varying vec2 vUv; 4 | 5 | 6 | void main() { 7 | vec4 localSpacePosition = vec4(position, 1.0); 8 | vec4 worldPosition = modelMatrix * localSpacePosition; 9 | 10 | vWorldPosition = worldPosition.xyz; 11 | vWorldNormal = normalize((modelMatrix * vec4(normal, 0.0)).xyz); 12 | vUv = uv; 13 | 14 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 15 | } -------------------------------------------------------------------------------- /public/shaders/sky.glsl: -------------------------------------------------------------------------------- 1 | 2 | vec3 SKY_lighterBlue = vec3(0.39, 0.57, 0.86) * 0.25; 3 | vec3 SKY_midBlue = vec3(0.1, 0.11, 0.1) * 0.5; 4 | vec3 SKY_darkerBlue = vec3(0.0); 5 | vec3 SKY_SUN_COLOUR = vec3(0.5); 6 | vec3 SKY_SUN_GLOW_COLOUR = vec3(0.15, 0.2, 0.25); 7 | vec3 SKY_FOG_GLOW_COLOUR = vec3(vec3(0.75, 0.75, 1.0) * 0.15); 8 | float SKY_POWER = 16.0; 9 | float SUN_POWER = 128.0; 10 | float SKY_DARK_POWER = 2.0; 11 | float SKY_fogScatterDensity = 0.0005; 12 | float SKY_fogExtinctionDensity = 0.003; 13 | vec3 SUN_DIR = vec3(-1.0, 0.45, 1.0); 14 | 15 | // This is just a bunch of nonsense since I didn't want to implement a full 16 | // sky model. It's just a simple gradient with a sun and some fog. 17 | vec3 CalculateSkyLighting(vec3 viewDir, vec3 normalDir) { 18 | vec3 lighterBlue = col3(SKY_lighterBlue); 19 | vec3 midBlue = col3(SKY_midBlue); 20 | vec3 darkerBlue = col3(SKY_darkerBlue); 21 | 22 | vec3 SUN_COLOUR = col3(SKY_SUN_COLOUR); 23 | vec3 SUN_GLOW_COLOUR = col3(SKY_SUN_GLOW_COLOUR); 24 | 25 | float viewDirY = linearstep(-0.01, 1.0, viewDir.y); 26 | 27 | float skyGradientMixFactor = saturate(viewDirY); 28 | vec3 skyGradient = mix(darkerBlue, lighterBlue, exp(-sqrt(saturate(viewDirY)) * 2.0)); 29 | 30 | vec3 sunDir = normalize(SUN_DIR); 31 | float mu = 1.0 - saturate(dot(viewDir, sunDir)); 32 | 33 | vec3 colour = skyGradient + SUN_GLOW_COLOUR * saturate(exp(-sqrt(mu) * 10.0)) * 0.75; 34 | colour += SUN_COLOUR * smoothstep(0.9997, 0.9998, 1.0 - mu); 35 | 36 | colour = oklabToRGB(colour); 37 | 38 | return colour; 39 | } 40 | 41 | vec3 CalculateSkyFog(vec3 normalDir) { 42 | return CalculateSkyLighting(normalDir, normalDir); 43 | } 44 | 45 | vec3 CalculateFog(vec3 baseColour, vec3 viewDir, float sceneDepth) { 46 | vec3 fogSkyColour = CalculateSkyFog(-viewDir); 47 | float fogDepth = sceneDepth * sceneDepth; 48 | 49 | float fogScatterFactor = exp(-SKY_fogScatterDensity * SKY_fogScatterDensity * fogDepth); 50 | float fogExtinctionFactor = exp(-SKY_fogExtinctionDensity * SKY_fogExtinctionDensity * fogDepth); 51 | 52 | vec3 finalColour = baseColour * fogExtinctionFactor + fogSkyColour * (1.0 - fogScatterFactor); 53 | return finalColour; 54 | } -------------------------------------------------------------------------------- /public/shaders/terrain-lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | #define STANDARD 2 | #ifdef PHYSICAL 3 | #define IOR 4 | #define USE_SPECULAR 5 | #endif 6 | uniform vec3 diffuse; 7 | uniform vec3 emissive; 8 | uniform float roughness; 9 | uniform float metalness; 10 | uniform float opacity; 11 | #ifdef IOR 12 | uniform float ior; 13 | #endif 14 | #ifdef USE_SPECULAR 15 | uniform float specularIntensity; 16 | uniform vec3 specularColor; 17 | #ifdef USE_SPECULAR_COLORMAP 18 | uniform sampler2D specularColorMap; 19 | #endif 20 | #ifdef USE_SPECULAR_INTENSITYMAP 21 | uniform sampler2D specularIntensityMap; 22 | #endif 23 | #endif 24 | #ifdef USE_CLEARCOAT 25 | uniform float clearcoat; 26 | uniform float clearcoatRoughness; 27 | #endif 28 | #ifdef USE_IRIDESCENCE 29 | uniform float iridescence; 30 | uniform float iridescenceIOR; 31 | uniform float iridescenceThicknessMinimum; 32 | uniform float iridescenceThicknessMaximum; 33 | #endif 34 | #ifdef USE_SHEEN 35 | uniform vec3 sheenColor; 36 | uniform float sheenRoughness; 37 | #ifdef USE_SHEEN_COLORMAP 38 | uniform sampler2D sheenColorMap; 39 | #endif 40 | #ifdef USE_SHEEN_ROUGHNESSMAP 41 | uniform sampler2D sheenRoughnessMap; 42 | #endif 43 | #endif 44 | #ifdef USE_ANISOTROPY 45 | uniform vec2 anisotropyVector; 46 | #ifdef USE_ANISOTROPYMAP 47 | uniform sampler2D anisotropyMap; 48 | #endif 49 | #endif 50 | varying vec3 vViewPosition; 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | #include 66 | #include 67 | #include 68 | #include 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | #include 76 | #include 77 | #include 78 | #include 79 | #include 80 | #include 81 | 82 | varying vec3 vWorldNormal; 83 | varying vec3 vWorldPosition; 84 | varying vec3 vTerrainColour; 85 | 86 | uniform sampler2D grid; 87 | 88 | 89 | void main() { 90 | #include 91 | 92 | vec3 viewDir = normalize(cameraPosition - vWorldPosition); 93 | 94 | vec4 diffuseColor = vec4( diffuse, opacity ); 95 | diffuseColor.rgb *= vTerrainColour; 96 | float grid1 = texture(grid, vWorldPosition.xz * 0.1).r; 97 | float grid2 = texture(grid, vWorldPosition.xz * 1.0).r; 98 | 99 | float gridHash1 = hash12(floor(vWorldPosition.xz * 1.0)); 100 | 101 | vec3 gridColour = mix(vec3(0.5 + remap(gridHash1, 0.0, 1.0, -0.2, 0.2)), vec3(0.0625), grid2); 102 | gridColour = mix(gridColour, vec3(0.0), grid1); 103 | 104 | // diffuseColor.rgb = gridColour; 105 | 106 | ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); 107 | vec3 totalEmissiveRadiance = emissive; 108 | #include 109 | #include 110 | #include 111 | #include 112 | #include 113 | #include 114 | #include 115 | #include 116 | #include 117 | #include 118 | #include 119 | #include 120 | #include 121 | #include 122 | #include 123 | #include 124 | #include 125 | #include 126 | vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; 127 | vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; 128 | #include 129 | vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; 130 | #ifdef USE_SHEEN 131 | float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor ); 132 | outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecular; 133 | #endif 134 | #ifdef USE_CLEARCOAT 135 | float dotNVcc = saturate( dot( geometry.clearcoatNormal, geometry.viewDir ) ); 136 | vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); 137 | outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + clearcoatSpecular * material.clearcoat; 138 | #endif 139 | 140 | #include 141 | #include 142 | #include 143 | // #include 144 | 145 | gl_FragColor.xyz = CalculateFog(gl_FragColor.xyz, viewDir, vFogDepth); 146 | 147 | #include 148 | #include 149 | } 150 | -------------------------------------------------------------------------------- /public/shaders/terrain-lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | #define STANDARD 2 | varying vec3 vViewPosition; 3 | #ifdef USE_TRANSMISSION 4 | varying vec3 vWorldPosition; 5 | #endif 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | uniform sampler2D heightmap; 19 | uniform vec4 heightParams; 20 | 21 | varying vec3 vWorldNormal; 22 | varying vec3 vWorldPosition; 23 | varying vec3 vTerrainColour; 24 | 25 | 26 | void main() { 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | vec4 heightSample = texture2D(heightmap, uv); 39 | float height = heightSample.x * heightParams.z - heightParams.w; 40 | 41 | vec3 terrainWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz; 42 | float distToVertex = distance(cameraPosition, terrainWorldPos); 43 | 44 | float isSandy = linearstep(-11.0, -14.0, height); 45 | 46 | vec2 hashGrassColour = hash22(vec2(position.x, position.z)); 47 | // vec3 baseColour = mix(vec3(0.02, 0.2, 0.01), vec3(0.025, 0.1, 0.01), hashGrassColour.x); 48 | // vec3 tipColour = mix(vec3(0.5, 0.7, 0.1), vec3(0.4, 0.5, 0.025), hashGrassColour.y); 49 | // vec3 tipColour = vec3(0.2, 0.35, 0.05); 50 | // vec3 baseColour = vec3(0.02, 0.2, 0.01); 51 | // vec3 tipColour = vec3(0.5, 0.5, 0.1); 52 | 53 | vec3 baseColour = vec3(0.05, 0.2, 0.01); 54 | vec3 tipColour = vec3(0.3, 0.3, 0.1); 55 | 56 | float aoDist = smoothstep(25.0, 50.0, distToVertex); 57 | float colourDist = smoothstep(50.0, 100.0, distToVertex); 58 | float ao = mix(0.25, 1.0, aoDist); 59 | ao = mix(ao, 1.0, isSandy); 60 | 61 | vec3 SAND_COLOUR = vec3(0.6, 0.4, 0.2); 62 | 63 | vTerrainColour = mix(baseColour, tipColour, colourDist); 64 | vTerrainColour = mix(vTerrainColour, SAND_COLOUR, smoothstep(-11.0, -14.0, height)); 65 | vTerrainColour *= ao; 66 | 67 | 68 | #include 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | vViewPosition = - mvPosition.xyz; 75 | #include 76 | #include 77 | #include 78 | #ifdef USE_TRANSMISSION 79 | vWorldPosition = worldPosition.xyz; 80 | #endif 81 | 82 | vWorldNormal = (modelMatrix * vec4(normal.xyz, 0.0)).xyz; 83 | vWorldPosition = worldPosition.xyz; 84 | } 85 | -------------------------------------------------------------------------------- /public/shaders/water-lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | #define PHONG 2 | uniform vec3 diffuse; 3 | uniform vec3 emissive; 4 | uniform vec3 specular; 5 | uniform float shininess; 6 | uniform float opacity; 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | 33 | uniform mat4 projectionMatrix; 34 | uniform mat4 inverseProjectionMatrix; 35 | 36 | uniform sampler2D colourTexture; 37 | uniform vec2 resolution; 38 | uniform float time; 39 | 40 | varying vec3 vWorldNormal; 41 | varying vec3 vWorldPos; 42 | 43 | vec2 ViewToScreen(vec3 pos) { 44 | vec4 clipPos = projectionMatrix * vec4(pos, 1.0); 45 | vec3 ndcPos = clipPos.xyz / clipPos.w; 46 | return ndcPos.xy * 0.5 + 0.5; 47 | } 48 | 49 | vec3 ScreenToView(vec2 uv) { 50 | vec2 ndcPos = uv * 2.0 - 1.0; 51 | vec4 clipPos = vec4(ndcPos, 0.0, 1.0); 52 | vec4 viewPos = inverse(projectionMatrix) * clipPos; 53 | return vec3(viewPos.xy / viewPos.w, 1.0); 54 | } 55 | 56 | // TODO: This was lazily done to just get it working. 57 | // Do not use this for anything other than this demo. 58 | vec4 TraceRay(vec3 rayWorldOrigin, vec3 rayWorldDir) { 59 | const int MAX_COUNT = 32; 60 | 61 | vec3 rayViewPos = (viewMatrix * vec4(rayWorldOrigin, 1.0)).xyz; 62 | vec3 rayViewDir = (viewMatrix * vec4(rayWorldDir, 0.0)).xyz; 63 | 64 | vec3 rayPos = rayViewPos; 65 | vec3 rayDir = rayViewDir; 66 | 67 | float dist = 0.01; 68 | for (int i = 0; i < MAX_COUNT; i++) { 69 | rayPos += rayDir * dist; 70 | dist *= 1.5; 71 | 72 | vec2 coords = ViewToScreen(rayPos); 73 | float depthAtCoord = texture(colourTexture, coords).w; 74 | float rayDepth = -rayPos.z; 75 | 76 | if (depthAtCoord < rayDepth) { 77 | if (depthAtCoord < -rayViewPos.z) { 78 | continue; 79 | } 80 | if (coords.y < 0.0 || coords.y > 1.0) { 81 | break; 82 | } 83 | return vec4(texture(colourTexture, coords).xyz, 1.0); 84 | } 85 | } 86 | return vec4(0.0); 87 | } 88 | 89 | vec3 WaterNormal2(vec3 pos, float falloff) { 90 | vec3 noiseNormal = FBM_D_1_4(vec3(pos.xz * 0.4, time * 0.8), 1).yzw; 91 | 92 | return normalize(vec3(0.0, 1.0, 0.0) + noiseNormal * 0.5 * falloff); 93 | } 94 | 95 | void main() { 96 | #include 97 | vec4 diffuseColor = vec4( diffuse, opacity ); 98 | 99 | ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); 100 | vec3 totalEmissiveRadiance = emissive; 101 | #include 102 | #include 103 | #include 104 | #include 105 | #include 106 | #include 107 | #include 108 | #include 109 | #include 110 | #include 111 | 112 | #include 113 | 114 | #include 115 | #include 116 | #include 117 | #include 118 | vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; 119 | 120 | vec2 coords = gl_FragCoord.xy / resolution; 121 | float sceneZ = texture(colourTexture, coords).w; 122 | float waterZ = vViewPosition.z; 123 | float waterDepth = sceneZ - waterZ; 124 | float waterFalloff = smoothstep(0.0, 2.0, waterDepth); 125 | 126 | float dist = distance(cameraPosition, vWorldPos); 127 | vec3 viewDir = normalize(cameraPosition - vWorldPos); 128 | // vec3 waterNormal = WaterNormal(vWorldPos, sceneZ); 129 | 130 | float waterNormalFalloff = pow(saturate(dot(vec3(0.0, 1.0, 0.0), viewDir)), 2.0);// * easeIn(linearstep(100.0, 0.0, dist), 3.0); 131 | vec3 waterNormal = WaterNormal2(vWorldPos, waterNormalFalloff); 132 | 133 | 134 | float fresnel = pow(saturate(dot(waterNormal, viewDir)), 1.0); 135 | 136 | vec3 reflectedDir = reflect(-viewDir, waterNormal); 137 | vec3 refractDir = refract(-viewDir, waterNormal, 1.0 / 1.33); 138 | 139 | vec4 tracedReflection = TraceRay(vWorldPos, reflectedDir); 140 | vec3 tracedSky = CalculateSkyLighting(reflectedDir, viewDir); 141 | float edgeFalloff = smoothstep(0.5, 0.3, abs(coords.x - 0.5)); 142 | edgeFalloff = remap(edgeFalloff, 0.0, 1.0, 0.25, 1.0); 143 | vec3 reflectedColour = mix(tracedSky, tracedReflection.xyz, tracedReflection.w * edgeFalloff); 144 | vec4 colourSample = texture(colourTexture, coords + noise23(vec3(vWorldPos.xz, time)) * 0.05 * waterNormalFalloff); 145 | // vec4 tracedRefraction = TraceRay(vWorldPos, refractDir); 146 | 147 | vec3 waterColour = mix(colourSample.xyz, vec3(0.2, 0.2, 0.5), waterFalloff); 148 | vec4 froth = vec4(vec3(1.0), remap(noise13(vec3(vWorldPos.xz * 10.0, time * 2.0)), -1.0, 1.0, 0.0, 1.0)) * smoothstep(0.25, 0.0, waterDepth); 149 | waterColour = mix(waterColour, froth.xyz, froth.w); 150 | 151 | outgoingLight = mix(reflectedColour, waterColour, fresnel); 152 | outgoingLight = mix(colourSample.xyz, outgoingLight, smoothstep(0.0, 0.1, waterDepth)); 153 | 154 | 155 | #include 156 | #include 157 | #include 158 | #include 159 | #include 160 | #include 161 | #include 162 | } -------------------------------------------------------------------------------- /public/shaders/water-lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | #define PHONG 2 | varying vec3 vViewPosition; 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | varying vec3 vWorldNormal; 17 | varying vec3 vWorldPos; 18 | 19 | void main() { 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | vViewPosition = - mvPosition.xyz; 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | vWorldNormal = (modelMatrix * vec4(normal, 0.0)).xyz; 45 | vWorldPos = (modelMatrix * vec4(transformed, 1.0)).xyz; 46 | } -------------------------------------------------------------------------------- /public/shaders/water-texture-fsh.glsl: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | uniform sampler2D colourTexture; 5 | uniform sampler2D depthTexture; 6 | uniform vec3 nearFar; 7 | 8 | varying vec2 vUvs; 9 | 10 | float GetDepth(float depthSample) { 11 | float nf = nearFar.x; 12 | float f_sub_n = nearFar.y; 13 | float f = nearFar.z; 14 | 15 | float z_final = depthSample; 16 | return nf / (f_sub_n * z_final - f); 17 | } 18 | 19 | void main() { 20 | vec4 colourSample = texture(colourTexture, vUvs); 21 | float depthSample = texture(depthTexture, vUvs).r; 22 | 23 | depthSample = -GetDepth(depthSample); 24 | 25 | gl_FragColor = vec4(colourSample.xyz, depthSample); 26 | } -------------------------------------------------------------------------------- /public/shaders/water-texture-vsh.glsl: -------------------------------------------------------------------------------- 1 | 2 | varying vec2 vUvs; 3 | 4 | 5 | void main() { 6 | vec4 localSpacePosition = vec4(position, 1.0); 7 | vec4 worldPosition = modelMatrix * localSpacePosition; 8 | 9 | vUvs = uv; 10 | 11 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 12 | } -------------------------------------------------------------------------------- /public/shaders/wind-lighting-model-fsh.glsl: -------------------------------------------------------------------------------- 1 | #define PHONG 2 | uniform vec3 diffuse; 3 | uniform vec3 emissive; 4 | uniform vec3 specular; 5 | uniform float shininess; 6 | uniform float opacity; 7 | 8 | 9 | varying vec2 vUVs; 10 | varying float vWindParams; 11 | 12 | uniform sampler2D diffuseTexture; 13 | 14 | void main() { 15 | vec4 colour = texture(diffuseTexture, vUVs).xyzx; 16 | 17 | colour.xyz *= vec3(0.5); 18 | colour.w *= vWindParams; 19 | 20 | gl_FragColor = colour; 21 | } -------------------------------------------------------------------------------- /public/shaders/wind-lighting-model-vsh.glsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | #define PHONG 4 | 5 | 6 | varying vec2 vUVs; 7 | varying float vWindParams; 8 | 9 | uniform vec2 dustSize; 10 | uniform float time; 11 | 12 | uniform sampler2D heightmap; 13 | uniform vec3 heightmapParams; 14 | 15 | attribute vec3 offset; 16 | 17 | 18 | const float PI = 3.1415926535897932384626433832795; 19 | 20 | void main() { 21 | 22 | vec3 transformed = vec3( position ); 23 | #ifdef USE_ALPHAHASH 24 | vPosition = vec3( position ); 25 | #endif 26 | 27 | { 28 | vec3 baseWorldPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz + offset; 29 | float hashSample = hash12(baseWorldPosition.xz); 30 | 31 | float hashedTime = time + hashSample * 100.0; 32 | 33 | float windDir = noise12(baseWorldPosition.xz * 0.05 + 0.5 * time); 34 | // float windNoiseSample = noise12(grassBladeWorldPos.xz * 0.25 + time * 1.1); 35 | // float windLeanAngle = saturate(remap(windNoiseSample, -1.0, 1.0, 0.25, 1.0)); 36 | // windLeanAngle = easeIn(windLeanAngle, 2.0) * 1.5; 37 | vec3 windAxis = vec3(sin(windDir), 0.0, -cos(windDir)); 38 | 39 | const float TIME_REPEAT_PERIOD = 4.0; 40 | float repeatingTime = mod(hashedTime, TIME_REPEAT_PERIOD); 41 | float fadeInOut = ( 42 | smoothstep(0.0, TIME_REPEAT_PERIOD * 0.25, repeatingTime) * 43 | smoothstep(TIME_REPEAT_PERIOD, TIME_REPEAT_PERIOD * 0.75, repeatingTime)); 44 | 45 | vec3 windOffset = offset + windAxis * repeatingTime * 5.0; 46 | 47 | vec3 scaledPosition = position; 48 | scaledPosition.xy *= dustSize; 49 | 50 | vec3 scaledOffsetPosition = scaledPosition + windOffset; 51 | 52 | vec3 worldPosition = (modelMatrix * vec4(scaledOffsetPosition, 1.0)).xyz; 53 | 54 | vec3 z = normalize(cameraPosition - worldPosition); 55 | vec3 x = normalize(cross(vec3(0.0, 1.0, 0.0), z)); 56 | vec3 y = normalize(cross(z, x)); 57 | mat3 alignMatrix = mat3(x, y, z); 58 | transformed = alignMatrix * scaledPosition + windOffset; 59 | 60 | vec2 heightmapUV = vec2( 61 | remap(worldPosition.x, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 0.0, 1.0), 62 | remap(worldPosition.z, -heightmapParams.z * 0.5, heightmapParams.z * 0.5, 1.0, 0.0)); 63 | float terrainHeight = texture2D(heightmap, heightmapUV).x * heightmapParams.x - heightmapParams.y; 64 | transformed.y += terrainHeight; 65 | 66 | vWindParams = fadeInOut; 67 | 68 | float randomAngle = remap(hashSample, 0.0, 1.0, 0.0, 2.0 * PI); 69 | vUVs = rotate2D(randomAngle) * uv; 70 | 71 | // vec3 worldCenter = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz + windOffset; 72 | // vec3 viewCenter = normalize(worldCenter - cameraPosition); 73 | // vec3 viewXZ = -normalize(vec3(viewCenter.x, 0.0, viewCenter.z)); 74 | 75 | // float i = floor(16.0 * (atan(viewXZ.z, viewXZ.x) + PI) / (2.0 * PI)); 76 | // float j = floor(16.0 * offset.w / (2.0 * PI)); 77 | // vBillboardLayer = vec2(i * 16.0 + j, smoothstep(350.0, 300.0, dist)); 78 | } 79 | 80 | gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0); 81 | } -------------------------------------------------------------------------------- /public/textures/butterfly.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/textures/butterfly.png -------------------------------------------------------------------------------- /public/textures/dust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/textures/dust.png -------------------------------------------------------------------------------- /public/textures/grass.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/textures/grass.png -------------------------------------------------------------------------------- /public/textures/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/textures/grid.png -------------------------------------------------------------------------------- /public/textures/moth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/textures/moth.png -------------------------------------------------------------------------------- /public/textures/terrain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/textures/terrain.png -------------------------------------------------------------------------------- /public/textures/whitesquare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simondevyoutube/Quick_Grass/6b56164272f213e353a4045d1439d071d5a968cd/public/textures/whitesquare.png -------------------------------------------------------------------------------- /src/base/entity-manager.js: -------------------------------------------------------------------------------- 1 | import * as entity from './entity.js'; 2 | import * as passes from "./passes.js"; 3 | 4 | 5 | const ROOT_ = '__root__'; 6 | 7 | export class EntityManager { 8 | static #instance_ = null; 9 | 10 | #root_; 11 | #entitiesMap_; 12 | 13 | static Init() { 14 | this.#instance_ = new EntityManager(); 15 | this.#instance_.#CreateRoot_(); 16 | return this.#instance_; 17 | } 18 | 19 | static get Instance() { 20 | return this.#instance_; 21 | } 22 | 23 | constructor() { 24 | this.#entitiesMap_ = {}; 25 | this.#root_ = null; 26 | } 27 | 28 | #CreateRoot_() { 29 | this.#root_ = new entity.Entity(ROOT_); 30 | this.#root_.Init(); 31 | } 32 | 33 | Remove(n) { 34 | delete this.#entitiesMap_[n]; 35 | } 36 | 37 | Get(n) { 38 | return this.#entitiesMap_[n]; 39 | } 40 | 41 | Add(child, parent) { 42 | this.#entitiesMap_[child.Name] = child; 43 | 44 | // Root check 45 | if (child.ID == this.#root_.ID) { 46 | parent = null; 47 | } else { 48 | parent = parent ? parent : this.#root_; 49 | } 50 | 51 | child.SetParent(parent); 52 | } 53 | 54 | Update(timeElapsed) { 55 | for (let i = passes.Passes.PASSES_MIN; i <= passes.Passes.PASSES_MAX; i = i << 1) { 56 | this.UpdatePass_(timeElapsed, i); 57 | } 58 | } 59 | 60 | UpdatePass_(timeElapsedS, pass) { 61 | this.#root_.Update(timeElapsedS, pass); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/base/entity.js: -------------------------------------------------------------------------------- 1 | import {THREE} from './three-defs.js'; 2 | 3 | import * as entity_manager from './entity-manager.js'; 4 | import * as passes from './passes.js'; 5 | 6 | 7 | const SCALE_1_ = new THREE.Vector3(1, 1, 1); 8 | 9 | let IDS_ = 0; 10 | 11 | export class Entity { 12 | constructor(name) { 13 | IDS_ += 1; 14 | 15 | this.id_ = IDS_; 16 | this.name_ = name ? name : this.GenerateName_(); 17 | this.components_ = {}; 18 | this.attributes_ = {}; 19 | 20 | this.transform_ = new THREE.Matrix4(); 21 | this.transform_.identity(); 22 | this.worldTransform_ = new THREE.Matrix4(); 23 | this.worldTransform_.identity(); 24 | 25 | this.position_ = new THREE.Vector3(); 26 | this.rotation_ = new THREE.Quaternion(); 27 | 28 | this.handlers_ = {}; 29 | this.parent_ = null; 30 | this.dead_ = false; 31 | this.active_ = true; 32 | 33 | this.childrenActive_ = []; 34 | this.children_ = []; 35 | } 36 | 37 | Destroy_() { 38 | for (let c of this.children_) { 39 | c.Destroy_(); 40 | } 41 | for (let k in this.components_) { 42 | this.components_[k].Destroy(); 43 | } 44 | this.childrenActive_ = []; 45 | this.children_ = []; 46 | this.components_ = {}; 47 | this.parent_ = null; 48 | this.handlers_ = {}; 49 | this.Manager.Remove(this.name_); 50 | } 51 | 52 | GenerateName_() { 53 | return '__name__' + this.id_; 54 | } 55 | 56 | RegisterHandler_(n, h) { 57 | if (!(n in this.handlers_)) { 58 | this.handlers_[n] = []; 59 | } 60 | this.handlers_[n].push(h); 61 | } 62 | 63 | UnregisterHandler_(n, h) { 64 | this.handlers_[n] = this.handlers_[n].filter(c => c != h); 65 | } 66 | 67 | AddChild_(e) { 68 | this.children_.push(e); 69 | this.RefreshActiveChildren_(); 70 | } 71 | 72 | RemoveChild_(e) { 73 | this.children_ = this.children_.filter(c => c != e); 74 | this.RefreshActiveChildren_(); 75 | } 76 | 77 | SetParent(p) { 78 | if (this.parent_) { 79 | this.parent_.RemoveChild_(this); 80 | } 81 | 82 | this.parent_ = p; 83 | 84 | if (this.parent_) { 85 | this.parent_.AddChild_(this); 86 | } 87 | } 88 | 89 | get Name() { 90 | return this.name_; 91 | } 92 | 93 | get ID() { 94 | return this.id_; 95 | } 96 | 97 | get Manager() { 98 | return entity_manager.EntityManager.Instance; 99 | } 100 | 101 | get Parent() { 102 | return this.parent_; 103 | } 104 | 105 | get Attributes() { 106 | return this.attributes_; 107 | } 108 | 109 | get Children() { 110 | return [...this.children_]; 111 | } 112 | 113 | get IsDead() { 114 | return this.dead_; 115 | } 116 | 117 | get IsActive() { 118 | return this.active_; 119 | } 120 | 121 | RefreshActiveChildren_() { 122 | this.childrenActive_ = this.children_.filter(c => c.IsActive); 123 | } 124 | 125 | SetActive(active) { 126 | this.active_ = active; 127 | if (this.parent_) { 128 | this.parent_.RefreshActiveChildren_(); 129 | } 130 | } 131 | 132 | SetDead() { 133 | this.dead_ = true; 134 | } 135 | 136 | AddComponent(c) { 137 | c.SetParent(this); 138 | this.components_[c.NAME] = c; 139 | 140 | c.InitComponent(); 141 | } 142 | 143 | Init(parent) { 144 | this.Manager.Add(this, parent); 145 | this.InitEntity_(); 146 | } 147 | 148 | InitEntity_() { 149 | for (let k in this.components_) { 150 | this.components_[k].InitEntity(); 151 | } 152 | this.SetActive(this.active_); 153 | } 154 | 155 | GetComponent(n) { 156 | return this.components_[n]; 157 | } 158 | 159 | FindEntity(name) { 160 | return this.Manager.Get(name); 161 | } 162 | 163 | FindChild(name, recursive) { 164 | let result = null; 165 | 166 | for (let i = 0, n = this.children_.length; i < n; ++i) { 167 | if (this.children_[i].Name == name) { 168 | result = this.children_[i]; 169 | break; 170 | } 171 | 172 | if (recursive) { 173 | result = this.children_[i].FindChild(name, recursive); 174 | if (result) { 175 | break; 176 | } 177 | } 178 | } 179 | return result; 180 | } 181 | 182 | Broadcast(msg) { 183 | if (this.IsDead) { 184 | return; 185 | } 186 | if (!(msg.topic in this.handlers_)) { 187 | return; 188 | } 189 | 190 | for (let curHandler of this.handlers_[msg.topic]) { 191 | curHandler(msg); 192 | } 193 | } 194 | 195 | SetPosition(p) { 196 | this.position_.copy(p); 197 | this.transform_.compose(this.position_, this.rotation_, SCALE_1_); 198 | this.Broadcast({ 199 | topic: 'update.position', 200 | value: this.position_, 201 | }); 202 | } 203 | 204 | SetQuaternion(r) { 205 | this.rotation_.copy(r); 206 | this.transform_.compose(this.position_, this.rotation_, SCALE_1_); 207 | this.Broadcast({ 208 | topic: 'update.rotation', 209 | value: this.rotation_, 210 | }); 211 | } 212 | 213 | get Transform() { 214 | return this.transform_; 215 | } 216 | 217 | get WorldTransform() { 218 | const m = this.worldTransform_.copy(this.transform_); 219 | if (this.parent_) { 220 | m.multiply(this.parent_.Transform); 221 | } 222 | return m; 223 | } 224 | 225 | GetWorldPosition(target) { 226 | target.setFromMatrixPosition(this.WorldTransform); 227 | return target; 228 | } 229 | 230 | get Position() { 231 | return this.position_; 232 | } 233 | 234 | get Quaternion() { 235 | return this.rotation_; 236 | } 237 | 238 | get Forward() { 239 | const forward = new THREE.Vector3(0, 0, -1); 240 | forward.applyQuaternion(this.rotation_); 241 | return forward; 242 | } 243 | 244 | get Left() { 245 | const forward = new THREE.Vector3(-1, 0, 0); 246 | forward.applyQuaternion(this.rotation_); 247 | return forward; 248 | } 249 | 250 | get Up() { 251 | const forward = new THREE.Vector3(0, 1, 0); 252 | forward.applyQuaternion(this.rotation_); 253 | return forward; 254 | } 255 | 256 | UpdateComponents_(timeElapsed, pass) { 257 | for (let k in this.components_) { 258 | const c = this.components_[k]; 259 | if (c.Pass == pass) { 260 | c.Update(timeElapsed); 261 | } 262 | } 263 | } 264 | 265 | UpdateChildren_(timeElapsed, pass) { 266 | const dead = []; 267 | const alive = []; 268 | for (let i = 0; i < this.childrenActive_.length; ++i) { 269 | const e = this.childrenActive_[i]; 270 | 271 | e.Update(timeElapsed, pass); 272 | 273 | if (e.IsDead) { 274 | dead.push(e); 275 | } else { 276 | alive.push(e); 277 | } 278 | } 279 | 280 | let hasDead = false; 281 | for (let i = 0; i < dead.length; ++i) { 282 | const e = dead[i]; 283 | 284 | e.Destroy_(); 285 | hasDead = true; 286 | } 287 | 288 | if (hasDead) { 289 | this.children_ = this.children_.filter(c => !c.IsDead); 290 | this.RefreshActiveChildren_(); 291 | } 292 | } 293 | 294 | Update(timeElapsed, pass) { 295 | this.UpdateComponents_(timeElapsed, pass); 296 | this.UpdateChildren_(timeElapsed, pass); 297 | } 298 | }; 299 | 300 | 301 | export class Component { 302 | get NAME() { 303 | console.error('Unnamed Component: ' + this.constructor.name); 304 | return '__UNNAMED__'; 305 | } 306 | 307 | constructor() { 308 | this.parent_ = null; 309 | this.pass_ = passes.Passes.DEFAULT; 310 | } 311 | 312 | Destroy() {} 313 | InitComponent() {} 314 | InitEntity() {} 315 | Update(timeElapsed) {} 316 | 317 | SetParent(parent) { 318 | this.parent_ = parent; 319 | } 320 | 321 | SetPass(pass) { 322 | this.pass_ = pass; 323 | } 324 | 325 | get Pass() { 326 | return this.pass_; 327 | } 328 | 329 | GetComponent(name) { 330 | return this.parent_.GetComponent(name); 331 | } 332 | 333 | get Manager() { 334 | return this.Parent.Manager; 335 | } 336 | 337 | get Parent() { 338 | return this.parent_; 339 | } 340 | 341 | FindEntity(name) { 342 | return this.Manager.Get(name); 343 | } 344 | 345 | Broadcast(m) { 346 | this.parent_.Broadcast(m); 347 | } 348 | 349 | RegisterHandler_(name, cb) { 350 | this.parent_.RegisterHandler_(name, cb); 351 | } 352 | }; 353 | -------------------------------------------------------------------------------- /src/base/load-controller.js: -------------------------------------------------------------------------------- 1 | import {THREE, FBXLoader, GLTFLoader, SkeletonUtils} from './three-defs.js'; 2 | 3 | import * as entity from "./entity.js"; 4 | import * as shaders from '../game/render/shaders.js' 5 | 6 | 7 | export const load_controller = (() => { 8 | 9 | class LoadController extends entity.Component { 10 | static CLASS_NAME = 'LoadController'; 11 | 12 | get NAME() { 13 | return LoadController.CLASS_NAME; 14 | } 15 | 16 | constructor() { 17 | super(); 18 | 19 | this.textures_ = {}; 20 | this.models_ = {}; 21 | this.sounds_ = {}; 22 | this.playing_ = []; 23 | } 24 | 25 | AddModel(model, path, name) { 26 | const group = new THREE.Group(); 27 | group.add(model); 28 | 29 | const fullName = path + name; 30 | this.models_[fullName] = { 31 | asset: { scene: group, animations: [] }, 32 | queue: null 33 | }; 34 | } 35 | 36 | LoadTexture(path, name) { 37 | if (!(name in this.textures_)) { 38 | const loader = new THREE.TextureLoader(); 39 | loader.setPath(path); 40 | 41 | this.textures_[name] = {loader: loader, texture: loader.load(name)}; 42 | // this.textures_[name].encoding = THREE.sRGBEncoding; 43 | } 44 | 45 | return this.textures_[name].texture; 46 | } 47 | 48 | #FinalizeLoad_(group) { 49 | const threejsController = this.FindEntity('threejs').GetComponent('ThreeJSController'); 50 | 51 | group.traverse((obj) => { 52 | if (!(obj instanceof THREE.Mesh)) { 53 | return; 54 | } 55 | 56 | let materials = ( 57 | obj.material instanceof Array ? 58 | obj.material : [obj.material]); 59 | 60 | if (obj.geometry) { 61 | obj.geometry.computeBoundingBox(); 62 | } 63 | 64 | for (let mat of materials) { 65 | if (mat) { 66 | } 67 | } 68 | }); 69 | } 70 | 71 | Load(path, name, onLoad) { 72 | if (name.endsWith('glb') || name.endsWith('gltf')) { 73 | this.LoadGLB(path, name, onLoad); 74 | } else if (name.endsWith('fbx')) { 75 | this.LoadFBX(path, name, onLoad); 76 | } else { 77 | const fullName = path + name; 78 | if (this.models_[fullName]) { 79 | const clone = this.models_[fullName].asset.scene.clone(); 80 | 81 | this.#FinalizeLoad_(clone); 82 | 83 | onLoad({scene: clone}); 84 | return; 85 | } 86 | // Silently fail, because screw you future me. 87 | } 88 | } 89 | 90 | LoadFBX(path, name, onLoad) { 91 | if (!(name in this.models_)) { 92 | const loader = new FBXLoader(); 93 | loader.setPath(path); 94 | 95 | this.models_[name] = {loader: loader, asset: null, queue: [onLoad]}; 96 | this.models_[name].loader.load(name, (fbx) => { 97 | this.models_[name].asset = fbx; 98 | 99 | const queue = this.models_[name].queue; 100 | this.models_[name].queue = null; 101 | for (let q of queue) { 102 | const clone = SkeletonUtils.clone(this.models_[name].asset); 103 | q(clone); 104 | } 105 | }); 106 | } else if (this.models_[name].asset == null) { 107 | this.models_[name].queue.push(onLoad); 108 | } else { 109 | const clone = SkeletonUtils.clone(this.models_[name].asset); 110 | onLoad(clone); 111 | } 112 | } 113 | 114 | #ConvertToGameMaterial_(group) { 115 | const threejsController = this.FindEntity('threejs').GetComponent('ThreeJSController'); 116 | 117 | group.traverse((obj) => { 118 | if (!(obj instanceof THREE.Mesh)) { 119 | return; 120 | } 121 | 122 | let materials = ( 123 | obj.material instanceof Array ? 124 | obj.material : [obj.material]); 125 | 126 | for (let mat of materials) { 127 | if (mat instanceof THREE.MeshStandardMaterial) { 128 | // mat.metalness = 0.0; 129 | // obj.material = new shaders.GamePBRMaterial('TREE'); 130 | obj.material.copy(mat); 131 | obj.material = new shaders.GameMaterial('PHONG'); 132 | // obj.material = new THREE.MeshStandardMaterial(); 133 | obj.material.map = mat.map; 134 | obj.material.color = mat.color; 135 | obj.material.normalMap = mat.normalMap; 136 | obj.material.vertexColors = mat.vertexColors; 137 | obj.material.alphaTest = mat.alphaTest; 138 | // obj.material.copy(mat); 139 | } 140 | } 141 | }); 142 | } 143 | 144 | LoadGLB(path, name, onLoad) { 145 | const fullName = path + name; 146 | if (!(fullName in this.models_)) { 147 | const loader = new GLTFLoader(); 148 | loader.setPath(path); 149 | 150 | this.models_[fullName] = {loader: loader, asset: null, queue: [onLoad]}; 151 | this.models_[fullName].loader.load(name, (glb) => { 152 | this.models_[fullName].asset = glb; 153 | 154 | this.#ConvertToGameMaterial_(glb.scene); 155 | 156 | const queue = this.models_[fullName].queue; 157 | this.models_[fullName].queue = null; 158 | for (let q of queue) { 159 | const clone = {...glb}; 160 | clone.scene = SkeletonUtils.clone(clone.scene); 161 | 162 | q(clone); 163 | } 164 | }); 165 | } else if (this.models_[fullName].asset == null) { 166 | this.models_[fullName].queue.push(onLoad); 167 | } else { 168 | const clone = {...this.models_[fullName].asset}; 169 | clone.scene = SkeletonUtils.clone(clone.scene); 170 | 171 | onLoad(clone); 172 | } 173 | } 174 | 175 | LoadSkinnedGLB(path, name, onLoad) { 176 | if (!(name in this.models_)) { 177 | const loader = new GLTFLoader(); 178 | loader.setPath(path); 179 | 180 | this.models_[name] = {loader: loader, asset: null, queue: [onLoad]}; 181 | this.models_[name].loader.load(name, (glb) => { 182 | this.models_[name].asset = glb; 183 | 184 | glb.scene.traverse(c => { 185 | // HAHAHAH 186 | c.frustumCulled = false; 187 | // Apparently this doesn't work, so just disable frustum culling. 188 | // Bugs... so many bugs... 189 | 190 | // if (c.geometry) { 191 | // // Just make our own, super crappy, super big box 192 | // c.geometry.boundingBox = new THREE.Box3( 193 | // new THREE.Vector3(-50, -50, -50), 194 | // new THREE.Vector3(50, 50, 50)); 195 | // c.geometry.boundingSphere = new THREE.Sphere(); 196 | // c.geometry.boundingBox.getBoundingSphere(c.geometry.boundingSphere); 197 | // } 198 | }); 199 | 200 | const queue = this.models_[name].queue; 201 | this.models_[name].queue = null; 202 | for (let q of queue) { 203 | const clone = {...glb}; 204 | clone.scene = SkeletonUtils.clone(clone.scene); 205 | 206 | q(clone); 207 | } 208 | }); 209 | } else if (this.models_[name].asset == null) { 210 | this.models_[name].queue.push(onLoad); 211 | } else { 212 | const clone = {...this.models_[name].asset}; 213 | clone.scene = SkeletonUtils.clone(clone.scene); 214 | 215 | onLoad(clone); 216 | } 217 | } 218 | 219 | Update(timeElapsed) { 220 | for (let i = 0; i < this.playing_.length; ++i) { 221 | if (!this.playing_[i].isPlaying) { 222 | this.playing_[i].parent.remove(this.playing_[i]); 223 | } 224 | } 225 | this.playing_ = this.playing_.filter(s => s.isPlaying); 226 | } 227 | } 228 | 229 | return { 230 | LoadController: LoadController, 231 | }; 232 | })(); -------------------------------------------------------------------------------- /src/base/math.js: -------------------------------------------------------------------------------- 1 | // export const math = (function() { 2 | // return { 3 | // rand_range: function(a, b) { 4 | // return Math.random() * (b - a) + a; 5 | // }, 6 | 7 | // rand_normalish: function() { 8 | // const r = Math.random() + Math.random() + Math.random() + Math.random(); 9 | // return (r / 4.0) * 2.0 - 1; 10 | // }, 11 | 12 | // rand_int: function(a, b) { 13 | // return Math.round(Math.random() * (b - a) + a); 14 | // }, 15 | 16 | // lerp: function(x, a, b) { 17 | // return x * (b - a) + a; 18 | // }, 19 | 20 | // smoothstep: function(x, a, b) { 21 | // x = x * x * (3.0 - 2.0 * x); 22 | // return x * (b - a) + a; 23 | // }, 24 | 25 | // smootherstep: function(x, a, b) { 26 | // x = x * x * x * (x * (x * 6 - 15) + 10); 27 | // return x * (b - a) + a; 28 | // }, 29 | 30 | // clamp: function(x, a, b) { 31 | // return Math.min(Math.max(x, a), b); 32 | // }, 33 | 34 | // sat: function(x) { 35 | // return Math.min(Math.max(x, 0.0), 1.0); 36 | // }, 37 | 38 | // in_range: (x, a, b) => { 39 | // return x >= a && x <= b; 40 | // }, 41 | // }; 42 | // })(); 43 | 44 | 45 | import MersenneTwister from 'mersenne-twister'; 46 | 47 | 48 | let RNG_ = new MersenneTwister(); 49 | 50 | export function set_seed(seed) { 51 | RNG_ = new MersenneTwister(seed); 52 | } 53 | 54 | export function clamp(x, a, b) { 55 | return Math.min(Math.max(x, a), b); 56 | } 57 | 58 | export function sat(x) { 59 | return Math.min(Math.max(x, 0.0), 1.0); 60 | } 61 | 62 | export function in_range(x, a, b) { 63 | return x >= a && x <= b; 64 | } 65 | 66 | export function easeOut(x, t) { 67 | return 1.0 - Math.pow(1.0 - x, t); 68 | } 69 | 70 | export function easeIn(x, t) { 71 | return Math.pow(x, t); 72 | } 73 | 74 | export function rand_range(a, b) { 75 | return RNG_.random() * (b - a) + a; 76 | } 77 | 78 | export function rand_normalish() { 79 | const r = RNG_.random() + RNG_.random() + RNG_.random() + RNG_.random(); 80 | return (r / 4.0) * 2.0 - 1; 81 | } 82 | 83 | export function rand_int(a, b) { 84 | return Math.round(RNG_.random() * (b - a) + a); 85 | } 86 | 87 | export function lerp(x, a, b) { 88 | return x * (b - a) + a; 89 | } 90 | 91 | export function smoothstep(edge0, edge1, x) { 92 | const t = sat((x - edge0) / (edge1 - edge0)); 93 | return t * t * (3.0 - 2.0 * t); 94 | } 95 | 96 | export function smootherstep(edge0, edge1, x) { 97 | const t = sat((x - edge0) / (edge1 - edge0)); 98 | return (t * t * t * (t * (t * 6 - 15) + 10)); 99 | } 100 | -------------------------------------------------------------------------------- /src/base/passes.js: -------------------------------------------------------------------------------- 1 | export const Passes = { 2 | PASSES_MIN: 1 << 0, 3 | 4 | INPUT : 1 << 0, 5 | CAMERA : 1 << 1, 6 | DEFAULT : 1 << 2, 7 | 8 | PASSES_MAX: 1 << 2, 9 | }; 10 | -------------------------------------------------------------------------------- /src/base/render-component.js: -------------------------------------------------------------------------------- 1 | import {THREE} from './three-defs.js'; 2 | 3 | import * as entity from './entity.js'; 4 | 5 | 6 | export const render_component = (() => { 7 | 8 | class RenderComponent extends entity.Component { 9 | static CLASS_NAME = 'RenderComponent'; 10 | 11 | get NAME() { 12 | return RenderComponent.CLASS_NAME; 13 | } 14 | 15 | constructor(params) { 16 | super(); 17 | this.group_ = new THREE.Group(); 18 | this.target_ = null; 19 | this.offset_ = null; 20 | this.params_ = params; 21 | } 22 | 23 | Destroy() { 24 | this.group_.traverse(c => { 25 | if (c.material) { 26 | c.material.dispose(); 27 | } 28 | if (c.geometry) { 29 | c.geometry.dispose(); 30 | } 31 | }); 32 | this.group_.removeFromParent(); 33 | } 34 | 35 | InitEntity() { 36 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 37 | threejs.AddSceneObject(this.group_); 38 | 39 | this.Parent.Attributes.Render = { 40 | group: this.group_, 41 | }; 42 | 43 | this.LoadModels_(); 44 | } 45 | 46 | InitComponent() { 47 | this.RegisterHandler_('update.position', (m) => { this.OnPosition_(m); }); 48 | this.RegisterHandler_('update.rotation', (m) => { this.OnRotation_(m); }); 49 | this.RegisterHandler_('render.visible', (m) => { this.OnVisible_(m); }); 50 | this.RegisterHandler_('render.offset', (m) => { this.OnOffset_(m.offset); }); 51 | } 52 | 53 | OnVisible_(m) { 54 | this.group_.visible = m.value; 55 | } 56 | 57 | OnPosition_(m) { 58 | this.group_.position.copy(m.value); 59 | } 60 | 61 | OnRotation_(m) { 62 | this.group_.quaternion.copy(m.value); 63 | } 64 | 65 | OnOffset_(offset) { 66 | this.offset_ = offset; 67 | if (!this.offset_) { 68 | return; 69 | } 70 | 71 | if (this.target_) { 72 | this.target_.position.copy(this.offset_.position); 73 | this.target_.quaternion.copy(this.offset_.quaternion); 74 | } 75 | } 76 | 77 | LoadModels_() { 78 | const loader = this.FindEntity('loader').GetComponent('LoadController'); 79 | loader.Load( 80 | this.params_.resourcePath, this.params_.resourceName, (mdl) => { 81 | this.OnLoaded_(mdl.scene); 82 | }); 83 | } 84 | 85 | OnLoaded_(obj) { 86 | this.target_ = obj; 87 | this.group_.add(this.target_); 88 | this.group_.position.copy(this.Parent.Position); 89 | this.group_.quaternion.copy(this.Parent.Quaternion); 90 | 91 | this.target_.scale.copy(this.params_.scale); 92 | if (this.params_.offset) { 93 | this.offset_ = this.params_.offset; 94 | } 95 | this.OnOffset_(this.offset_); 96 | 97 | const textures = {}; 98 | if (this.params_.textures) { 99 | const loader = this.FindEntity('loader').GetComponent('LoadController'); 100 | 101 | for (let k in this.params_.textures.names) { 102 | const t = loader.LoadTexture( 103 | this.params_.textures.resourcePath, this.params_.textures.names[k]); 104 | // t.encoding = THREE.sRGBEncoding; 105 | 106 | if (this.params_.textures.wrap) { 107 | t.wrapS = THREE.RepeatWrapping; 108 | t.wrapT = THREE.RepeatWrapping; 109 | } 110 | 111 | textures[k] = t; 112 | } 113 | } 114 | 115 | this.target_.traverse(c => { 116 | let materials = c.material; 117 | if (!(c.material instanceof Array)) { 118 | materials = [c.material]; 119 | } 120 | 121 | if (c.geometry) { 122 | c.geometry.computeBoundingBox(); 123 | } 124 | 125 | for (let m of materials) { 126 | if (m) { 127 | // HACK 128 | // m.depthWrite = true; 129 | // m.transparent = false; 130 | 131 | if (this.params_.onMaterial) { 132 | this.params_.onMaterial(m); 133 | } 134 | for (let k in textures) { 135 | if (m.name.search(k) >= 0) { 136 | m.map = textures[k]; 137 | } 138 | } 139 | if (this.params_.specular) { 140 | m.specular = this.params_.specular; 141 | } 142 | if (this.params_.emissive) { 143 | m.emissive = this.params_.emissive; 144 | } 145 | if (this.params_.colour) { 146 | m.color = this.params_.colour; 147 | } 148 | } 149 | } 150 | 151 | c.castShadow = true; 152 | c.receiveShadow = true; 153 | 154 | if (this.params_.receiveShadow !== undefined) { 155 | c.receiveShadow = this.params_.receiveShadow; 156 | } 157 | if (this.params_.castShadow !== undefined) { 158 | c.castShadow = this.params_.castShadow; 159 | } 160 | if (this.params_.visible !== undefined) { 161 | c.visible = this.params_.visible; 162 | } 163 | }); 164 | 165 | this.Broadcast({ 166 | topic: 'render.loaded', 167 | value: this.target_, 168 | }); 169 | } 170 | 171 | Update(timeInSeconds) { 172 | } 173 | }; 174 | 175 | 176 | return { 177 | RenderComponent: RenderComponent, 178 | }; 179 | 180 | })(); -------------------------------------------------------------------------------- /src/base/render-order.js: -------------------------------------------------------------------------------- 1 | export const render_order = (() => { 2 | return { 3 | DEFAULT: 0, 4 | DECALS: 1, 5 | SHIELDS: 2, 6 | PARTICLES: 3, 7 | }; 8 | })(); -------------------------------------------------------------------------------- /src/base/render/bugs-component.js: -------------------------------------------------------------------------------- 1 | import {THREE} from '../three-defs.js'; 2 | 3 | import * as shaders from '../../game/render/shaders.js'; 4 | 5 | import * as entity from "../entity.js"; 6 | 7 | import * as terrain_component from './terrain-component.js'; 8 | import MersenneTwister from 'mersenne-twister'; 9 | 10 | 11 | class InstancedFloat16BufferAttribute extends THREE.InstancedBufferAttribute { 12 | 13 | constructor( array, itemSize, normalized, meshPerAttribute = 1 ) { 14 | 15 | super( new Uint16Array( array ), itemSize, normalized, meshPerAttribute ); 16 | 17 | this.isFloat16BufferAttribute = true; 18 | } 19 | }; 20 | 21 | 22 | const NUM_BUGS = 8; 23 | const NUM_SEGMENTS = 2; 24 | const NUM_VERTICES = (NUM_SEGMENTS + 1) * 2; 25 | const BUG_SPAWN_RANGE = 40.0; 26 | const BUG_MAX_DIST = 100.0; 27 | 28 | const M_TMP = new THREE.Matrix4(); 29 | const AABB_TMP = new THREE.Box3(); 30 | 31 | 32 | export class BugsComponent extends entity.Component { 33 | static CLASS_NAME = 'BugsComponent'; 34 | 35 | get NAME() { 36 | return BugsComponent.CLASS_NAME; 37 | } 38 | 39 | #params_; 40 | #meshes_; 41 | #group_; 42 | #totalTime_; 43 | #material_; 44 | #geometry_; 45 | 46 | constructor(params) { 47 | super(); 48 | 49 | this.#params_ = params; 50 | this.#meshes_ = []; 51 | this.#group_ = new THREE.Group(); 52 | this.#totalTime_ = 0; 53 | this.#geometry_ = null; 54 | } 55 | 56 | Destroy() { 57 | for (let m of this.#meshes_) { 58 | m.removeFromParent(); 59 | } 60 | this.#group_.removeFromParent(); 61 | } 62 | 63 | #CreateGeometry_() { 64 | const rng = new MersenneTwister(1); 65 | 66 | const offsets = new Uint16Array(NUM_BUGS * 3); 67 | for (let i = 0; i < NUM_BUGS; ++i) { 68 | offsets[i*3 + 0] = THREE.DataUtils.toHalfFloat((rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2)); 69 | offsets[i*3 + 1] = THREE.DataUtils.toHalfFloat(rng.random() * 1.0 + 2.0); 70 | offsets[i*3 + 2] = THREE.DataUtils.toHalfFloat((rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2)); 71 | } 72 | 73 | const plane = new THREE.PlaneGeometry(1, 1, 2, 1); 74 | 75 | const geo = new THREE.InstancedBufferGeometry(); 76 | geo.instanceCount = NUM_BUGS; 77 | geo.setAttribute('position', plane.attributes.position); 78 | geo.setAttribute('uv', plane.attributes.uv); 79 | geo.setAttribute('normal', plane.attributes.normal); 80 | geo.setAttribute('offset', new InstancedFloat16BufferAttribute(offsets, 3)); 81 | geo.setIndex(plane.index); 82 | geo.rotateX(-Math.PI / 2); 83 | geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), BUG_SPAWN_RANGE); 84 | 85 | return geo; 86 | } 87 | 88 | InitEntity() { 89 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 90 | 91 | this.#geometry_ = this.#CreateGeometry_(); 92 | 93 | const textureLoader = new THREE.TextureLoader(); 94 | const albedo = textureLoader.load('./textures/' + 'moth.png'); 95 | albedo.colorSpace = THREE.SRGBColorSpace; 96 | 97 | // // this.#grassMaterialLow_.setVec4('grassDraw', new THREE.Vector4( 98 | // // GRASS_LOD_DIST, GRASS_MAX_DIST, 0, 0)); 99 | this.#material_ = new shaders.GameMaterial('BUGS'); 100 | this.#material_.setVec2('bugsSize', new THREE.Vector2(0.5, 1.25)); 101 | this.#material_.setVec4('bugsParams', new THREE.Vector4( 102 | NUM_SEGMENTS, NUM_VERTICES, 0, 0)); 103 | this.#material_.setTexture('heightmap', this.#params_.heightmap); 104 | this.#material_.setVec3('heightmapParams', new THREE.Vector3( 105 | this.#params_.height, this.#params_.offset, this.#params_.dims)); 106 | this.#material_.map = albedo; 107 | this.#material_.shininess = 0; 108 | this.#material_.alphaTest = 0.5; 109 | this.#material_.side = THREE.DoubleSide; 110 | 111 | threejs.AddSceneObject(this.#group_); 112 | } 113 | 114 | #CreateMesh_() { 115 | const m = new THREE.Mesh(this.#geometry_, this.#material_); 116 | m.receiveShadow = true; 117 | m.castShadow = false; 118 | m.visible = true; 119 | 120 | this.#meshes_.push(m); 121 | this.#group_.add(m); 122 | 123 | return m; 124 | } 125 | 126 | Update(timeElapsed) { 127 | this.#totalTime_ += timeElapsed; 128 | 129 | this.#material_.setFloat('time', this.#totalTime_); 130 | 131 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 132 | const camera = threejs.Camera; 133 | const frustum = new THREE.Frustum().setFromProjectionMatrix(M_TMP.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse)); 134 | 135 | const meshes = [...this.#meshes_]; 136 | 137 | const baseCellPos = camera.position.clone(); 138 | baseCellPos.divideScalar(BUG_SPAWN_RANGE); 139 | baseCellPos.floor(); 140 | baseCellPos.multiplyScalar(BUG_SPAWN_RANGE); 141 | 142 | // This is dumb and slow 143 | for (let c of this.#group_.children) { 144 | c.visible = false; 145 | } 146 | 147 | const terrain = this.#params_.terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME); 148 | 149 | const cameraPosXZ = new THREE.Vector3(camera.position.x, 0, camera.position.z); 150 | 151 | for (let x = -3; x < 3; x++) { 152 | for (let z = -3; z < 3; z++) { 153 | // Current cell 154 | const currentCell = new THREE.Vector3( 155 | baseCellPos.x + x * BUG_SPAWN_RANGE, 0, baseCellPos.z + z * BUG_SPAWN_RANGE); 156 | currentCell.y = terrain.GetHeight(currentCell.x, currentCell.z); 157 | 158 | AABB_TMP.setFromCenterAndSize(currentCell, new THREE.Vector3(BUG_SPAWN_RANGE, 100, BUG_SPAWN_RANGE)); 159 | const distToCell = AABB_TMP.distanceToPoint(cameraPosXZ); 160 | if (distToCell > BUG_MAX_DIST) { 161 | continue; 162 | } 163 | 164 | if (!frustum.intersectsBox(AABB_TMP)) { 165 | continue; 166 | } 167 | 168 | const m = meshes.length > 0 ? meshes.pop() : this.#CreateMesh_(); 169 | m.position.copy(currentCell); 170 | m.position.y = 0; 171 | m.visible = true; 172 | } 173 | } 174 | } 175 | } -------------------------------------------------------------------------------- /src/base/render/grass-component.js: -------------------------------------------------------------------------------- 1 | import {THREE} from '../three-defs.js'; 2 | 3 | import * as shaders from '../../game/render/shaders.js'; 4 | 5 | import * as entity from "../entity.js"; 6 | 7 | import * as terrain_component from './terrain-component.js'; 8 | import * as math from '../math.js'; 9 | 10 | 11 | 12 | class InstancedFloat16BufferAttribute extends THREE.InstancedBufferAttribute { 13 | 14 | constructor( array, itemSize, normalized, meshPerAttribute = 1 ) { 15 | 16 | super( new Uint16Array( array ), itemSize, normalized, meshPerAttribute ); 17 | 18 | this.isFloat16BufferAttribute = true; 19 | } 20 | }; 21 | 22 | const M_TMP = new THREE.Matrix4(); 23 | const S_TMP = new THREE.Sphere(); 24 | const AABB_TMP = new THREE.Box3(); 25 | 26 | 27 | const NUM_GRASS = (32 * 32) * 3; 28 | const GRASS_SEGMENTS_LOW = 1; 29 | const GRASS_SEGMENTS_HIGH = 6; 30 | const GRASS_VERTICES_LOW = (GRASS_SEGMENTS_LOW + 1) * 2; 31 | const GRASS_VERTICES_HIGH = (GRASS_SEGMENTS_HIGH + 1) * 2; 32 | const GRASS_LOD_DIST = 15; 33 | const GRASS_MAX_DIST = 100; 34 | 35 | const GRASS_PATCH_SIZE = 5 * 2; 36 | 37 | const GRASS_WIDTH = 0.1; 38 | const GRASS_HEIGHT = 1.5; 39 | 40 | 41 | 42 | export class GrassComponent extends entity.Component { 43 | static CLASS_NAME = 'GrassComponent'; 44 | 45 | get NAME() { 46 | return GrassComponent.CLASS_NAME; 47 | } 48 | 49 | #params_; 50 | #meshesLow_; 51 | #meshesHigh_; 52 | #group_; 53 | #totalTime_; 54 | #grassMaterialLow_; 55 | #grassMaterialHigh_; 56 | #geometryLow_; 57 | #geometryHigh_; 58 | 59 | constructor(params) { 60 | super(); 61 | 62 | this.#params_ = params; 63 | this.#meshesLow_ = []; 64 | this.#meshesHigh_ = []; 65 | this.#group_ = new THREE.Group(); 66 | this.#group_.name = "GRASS"; 67 | this.#totalTime_ = 0; 68 | this.#grassMaterialLow_ = null; 69 | this.#grassMaterialHigh_ = null; 70 | this.#geometryLow_ = null; 71 | this.#geometryHigh_ = null; 72 | } 73 | 74 | Destroy() { 75 | for (let m of this.#meshesLow_) { 76 | m.removeFromParent(); 77 | } 78 | for (let m of this.#meshesHigh_) { 79 | m.removeFromParent(); 80 | } 81 | this.#group_.removeFromParent(); 82 | } 83 | 84 | #CreateGeometry_(segments) { 85 | math.set_seed(0); 86 | 87 | const VERTICES = (segments + 1) * 2; 88 | 89 | const indices = []; 90 | for (let i = 0; i < segments; ++i) { 91 | const vi = i * 2; 92 | indices[i*12+0] = vi + 0; 93 | indices[i*12+1] = vi + 1; 94 | indices[i*12+2] = vi + 2; 95 | 96 | indices[i*12+3] = vi + 2; 97 | indices[i*12+4] = vi + 1; 98 | indices[i*12+5] = vi + 3; 99 | 100 | const fi = VERTICES + vi; 101 | indices[i*12+6] = fi + 2; 102 | indices[i*12+7] = fi + 1; 103 | indices[i*12+8] = fi + 0; 104 | 105 | indices[i*12+9] = fi + 3; 106 | indices[i*12+10] = fi + 1; 107 | indices[i*12+11] = fi + 2; 108 | } 109 | 110 | const offsets = []; 111 | for (let i = 0; i < NUM_GRASS; ++i) { 112 | offsets.push(math.rand_range(-GRASS_PATCH_SIZE * 0.5, GRASS_PATCH_SIZE * 0.5)); 113 | offsets.push(math.rand_range(-GRASS_PATCH_SIZE * 0.5, GRASS_PATCH_SIZE * 0.5)); 114 | offsets.push(0); 115 | } 116 | 117 | const offsetsData = offsets.map(THREE.DataUtils.toHalfFloat); 118 | 119 | const vertID = new Uint8Array(VERTICES*2); 120 | for (let i = 0; i < VERTICES*2; ++i) { 121 | vertID[i] = i; 122 | } 123 | 124 | const geo = new THREE.InstancedBufferGeometry(); 125 | geo.instanceCount = NUM_GRASS; 126 | geo.setAttribute('vertIndex', new THREE.Uint8BufferAttribute(vertID, 1)); 127 | geo.setAttribute('position', new InstancedFloat16BufferAttribute(offsetsData, 3)); 128 | geo.setIndex(indices); 129 | geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 1 + GRASS_PATCH_SIZE * 2); 130 | 131 | return geo; 132 | } 133 | 134 | InitEntity() { 135 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 136 | 137 | this.#grassMaterialLow_ = new shaders.GameMaterial('GRASS'); 138 | this.#grassMaterialHigh_ = new shaders.GameMaterial('GRASS'); 139 | this.#grassMaterialLow_.side = THREE.FrontSide; 140 | this.#grassMaterialHigh_.side = THREE.FrontSide; 141 | 142 | this.#geometryLow_ = this.#CreateGeometry_(GRASS_SEGMENTS_LOW); 143 | this.#geometryHigh_ = this.#CreateGeometry_(GRASS_SEGMENTS_HIGH); 144 | 145 | this.#grassMaterialLow_.setVec2('grassSize', new THREE.Vector2(GRASS_WIDTH, GRASS_HEIGHT)); 146 | this.#grassMaterialLow_.setVec4('grassParams', new THREE.Vector4( 147 | GRASS_SEGMENTS_LOW, GRASS_VERTICES_LOW, this.#params_.height, this.#params_.offset)); 148 | this.#grassMaterialLow_.setVec4('grassDraw', new THREE.Vector4( 149 | GRASS_LOD_DIST, GRASS_MAX_DIST, 0, 0)); 150 | this.#grassMaterialLow_.setTexture('heightmap', this.#params_.heightmap); 151 | this.#grassMaterialLow_.setVec4('heightParams', new THREE.Vector4(this.#params_.dims, 0, 0, 0)) 152 | this.#grassMaterialLow_.setVec3('grassLODColour', new THREE.Vector3(0, 0, 1)); 153 | this.#grassMaterialLow_.alphaTest = 0.5; 154 | 155 | this.#grassMaterialHigh_.setVec2('grassSize', new THREE.Vector2(GRASS_WIDTH, GRASS_HEIGHT)); 156 | this.#grassMaterialHigh_.setVec4('grassParams', new THREE.Vector4( 157 | GRASS_SEGMENTS_HIGH, GRASS_VERTICES_HIGH, this.#params_.height, this.#params_.offset)); 158 | this.#grassMaterialHigh_.setVec4('grassDraw', new THREE.Vector4( 159 | GRASS_LOD_DIST, GRASS_MAX_DIST, 0, 0)); 160 | this.#grassMaterialHigh_.setTexture('heightmap', this.#params_.heightmap); 161 | this.#grassMaterialHigh_.setVec4('heightParams', new THREE.Vector4(this.#params_.dims, 0, 0, 0)) 162 | this.#grassMaterialHigh_.setVec3('grassLODColour', new THREE.Vector3(1, 0, 0)); 163 | this.#grassMaterialHigh_.alphaTest = 0.5; 164 | 165 | threejs.AddSceneObject(this.#group_); 166 | } 167 | 168 | #CreateMesh_(distToCell) { 169 | const meshes = distToCell > GRASS_LOD_DIST ? this.#meshesLow_ : this.#meshesHigh_; 170 | if (meshes.length > 1000) { 171 | console.log('crap') 172 | return null; 173 | } 174 | 175 | const geo = distToCell > GRASS_LOD_DIST ? this.#geometryLow_ : this.#geometryHigh_; 176 | const mat = distToCell > GRASS_LOD_DIST ? this.#grassMaterialLow_ : this.#grassMaterialHigh_; 177 | 178 | const m = new THREE.Mesh(geo, mat); 179 | m.position.set(0, 0, 0); 180 | m.receiveShadow = true; 181 | m.castShadow = false; 182 | m.visible = false; 183 | 184 | meshes.push(m); 185 | this.#group_.add(m); 186 | return m; 187 | } 188 | 189 | Update(timeElapsed) { 190 | this.#totalTime_ += timeElapsed; 191 | 192 | this.#grassMaterialLow_.setFloat('time', this.#totalTime_); 193 | this.#grassMaterialHigh_.setFloat('time', this.#totalTime_); 194 | 195 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 196 | const camera = threejs.Camera; 197 | const frustum = new THREE.Frustum().setFromProjectionMatrix(M_TMP.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse)); 198 | 199 | const meshesLow = [...this.#meshesLow_]; 200 | const meshesHigh = [...this.#meshesHigh_]; 201 | 202 | const baseCellPos = camera.position.clone(); 203 | baseCellPos.divideScalar(GRASS_PATCH_SIZE); 204 | baseCellPos.floor(); 205 | baseCellPos.multiplyScalar(GRASS_PATCH_SIZE); 206 | 207 | // This is dumb and slow 208 | for (let c of this.#group_.children) { 209 | c.visible = false; 210 | } 211 | 212 | const terrain = this.#params_.terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME); 213 | 214 | const cameraPosXZ = new THREE.Vector3(camera.position.x, 0, camera.position.z); 215 | const playerPos = this.FindEntity('player').Position; 216 | 217 | this.#grassMaterialHigh_.setVec3('playerPos', playerPos); 218 | // this.#grassMaterialHigh_.setVec3('cameraPos', camera.position); 219 | this.#grassMaterialHigh_.setMatrix('viewMatrixInverse', camera.matrixWorld); 220 | this.#grassMaterialLow_.setMatrix('viewMatrixInverse', camera.matrixWorld); 221 | // this.#grassMaterialLow_.setVec3('cameraPos', camera.position); 222 | 223 | 224 | // const playerCellPos = this.FindEntity('player').Position.clone(); 225 | // playerCellPos.divideScalar(GRASS_PATCH_SIZE); 226 | // playerCellPos.round(); 227 | // playerCellPos.multiplyScalar(GRASS_PATCH_SIZE); 228 | // const playerCellPos = new THREE.Vector3(); 229 | 230 | // const m = meshesHigh.length > 0 ? meshesHigh.pop() : this.#CreateMesh_(0); 231 | // m.position.copy(playerCellPos); 232 | // m.position.y = 0; 233 | // m.visible = true; 234 | 235 | let totalGrass = 0; 236 | let totalVerts = 0; 237 | 238 | for (let x = -16; x < 16; x++) { 239 | for (let z = -16; z < 16; z++) { 240 | // Current cell 241 | const currentCell = new THREE.Vector3( 242 | baseCellPos.x + x * GRASS_PATCH_SIZE, 0, baseCellPos.z + z * GRASS_PATCH_SIZE); 243 | currentCell.y = terrain.GetHeight(currentCell.x, currentCell.z); 244 | 245 | AABB_TMP.setFromCenterAndSize(currentCell, new THREE.Vector3(GRASS_PATCH_SIZE, 1000, GRASS_PATCH_SIZE)); 246 | const distToCell = AABB_TMP.distanceToPoint(cameraPosXZ); 247 | if (distToCell > GRASS_MAX_DIST) { 248 | continue; 249 | } 250 | 251 | if (x == 0 && z == 0) { 252 | let a = 0; 253 | } 254 | 255 | if (!frustum.intersectsBox(AABB_TMP)) { 256 | continue; 257 | } 258 | 259 | if (distToCell > GRASS_LOD_DIST) { 260 | const m = meshesLow.length > 0 ? meshesLow.pop() : this.#CreateMesh_(distToCell); 261 | m.position.copy(currentCell); 262 | m.position.y = 0; 263 | m.visible = true; 264 | totalVerts += GRASS_VERTICES_LOW; 265 | } else { 266 | const m = meshesHigh.length > 0 ? meshesHigh.pop() : this.#CreateMesh_(distToCell); 267 | m.position.copy(currentCell); 268 | m.position.y = 0; 269 | m.visible = true; 270 | totalVerts += GRASS_VERTICES_HIGH; 271 | } 272 | totalGrass += 1; 273 | } 274 | } 275 | 276 | totalGrass *= NUM_GRASS; 277 | totalVerts *= NUM_GRASS; 278 | // console.log('total grass: ' + totalGrass + ' total verts: ' + totalVerts); 279 | } 280 | } -------------------------------------------------------------------------------- /src/base/render/light-component.js: -------------------------------------------------------------------------------- 1 | import {THREE} from '../three-defs.js'; 2 | 3 | import * as entity from "../entity.js"; 4 | 5 | 6 | export class LightComponent extends entity.Component { 7 | static CLASS_NAME = 'LightComponent'; 8 | 9 | get NAME() { 10 | return LightComponent.CLASS_NAME; 11 | } 12 | 13 | #params_; 14 | #light_; 15 | 16 | constructor(params) { 17 | super(); 18 | 19 | this.#params_ = params; 20 | this.#light_ = null; 21 | } 22 | 23 | Destroy() { 24 | this.light_.removeFromParent(); 25 | } 26 | 27 | InitEntity() { 28 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 29 | if (this.#params_.hemi) { 30 | const params = this.#params_.hemi; 31 | this.#light_ = new THREE.HemisphereLight(params.upColour, params.downColour, params.intensity); 32 | threejs.AddSceneObject(this.#light_); 33 | } 34 | } 35 | 36 | Update(timeElapsed) { 37 | const player = this.FindEntity('player'); 38 | if (!player) { 39 | return; 40 | } 41 | const pos = player.Position; 42 | 43 | if (this.sky_) { 44 | this.sky_.material.uniforms.time.value += timeElapsed; 45 | } 46 | 47 | this.sky_.position.copy(pos); 48 | } 49 | } -------------------------------------------------------------------------------- /src/base/render/terrain-component.js: -------------------------------------------------------------------------------- 1 | import {THREE, Float32ToFloat16} from '../three-defs.js'; 2 | 3 | import * as shaders from '../../game/render/shaders.js'; 4 | 5 | import * as entity from "../entity.js"; 6 | import * as math from '../math.js'; 7 | 8 | import { render_component } from '../render-component.js'; 9 | import * as grass_component from './grass-component.js'; 10 | import * as bugs_component from './bugs-component.js'; 11 | import * as wind_component from './wind-component.js'; 12 | import * as water_component from './water-component.js'; 13 | 14 | 15 | function GetImageData_(image) { 16 | const canvas = document.createElement('canvas'); 17 | canvas.width = image.width; 18 | canvas.height = image.height; 19 | 20 | const context = canvas.getContext( '2d' ); 21 | context.drawImage(image, 0, 0); 22 | 23 | return context.getImageData(0, 0, image.width, image.height); 24 | } 25 | 26 | class Heightmap { 27 | constructor(params, img) { 28 | this.params_ = params; 29 | this.data_ = GetImageData_(img); 30 | } 31 | 32 | Get(x, y) { 33 | const _GetPixelAsFloat = (x, y) => { 34 | const position = (x + this.data_.width * y) * 4; 35 | const data = this.data_.data; 36 | return data[position] / 255.0; 37 | } 38 | 39 | // Bilinear filter 40 | const offset = this.params_.offset; 41 | const dimensions = this.params_.dimensions; 42 | 43 | const xf = math.sat((x - offset.x) / dimensions.x); 44 | const yf = 1.0 - math.sat((y - offset.y) / dimensions.y); 45 | const w = this.data_.width - 1; 46 | const h = this.data_.height - 1; 47 | 48 | const x1 = Math.floor(xf * w); 49 | const y1 = Math.floor(yf * h); 50 | const x2 = math.clamp(x1 + 1, 0, w); 51 | const y2 = math.clamp(y1 + 1, 0, h); 52 | 53 | const xp = xf * w - x1; 54 | const yp = yf * h - y1; 55 | 56 | const p11 = _GetPixelAsFloat(x1, y1); 57 | const p21 = _GetPixelAsFloat(x2, y1); 58 | const p12 = _GetPixelAsFloat(x1, y2); 59 | const p22 = _GetPixelAsFloat(x2, y2); 60 | 61 | const px1 = math.lerp(xp, p11, p21); 62 | const px2 = math.lerp(xp, p12, p22); 63 | 64 | return math.lerp(yp, px1, px2) * this.params_.height; 65 | } 66 | } 67 | 68 | 69 | const TERRAIN_HEIGHT = 75; 70 | const TERRAIN_OFFSET = 50; 71 | 72 | // const TERRAIN_HEIGHT = 0; 73 | // const TERRAIN_OFFSET = 0; 74 | 75 | const TERRAIN_DIMS = 2000; 76 | 77 | export class TerrainComponent extends entity.Component { 78 | static CLASS_NAME = 'TerrainComponent'; 79 | 80 | get NAME() { 81 | return TerrainComponent.CLASS_NAME; 82 | } 83 | 84 | #params_; 85 | #heightmap_; 86 | #mesh_; 87 | 88 | constructor(params) { 89 | super(); 90 | 91 | this.#params_ = params; 92 | this.#heightmap_ = null; 93 | this.#mesh_ = null; 94 | } 95 | 96 | Destroy() { 97 | this.#mesh_.removeFromParent(); 98 | } 99 | 100 | GetHeight(x, y) { 101 | const xn = (x + TERRAIN_DIMS * 0.5) / TERRAIN_DIMS; 102 | const yn = 1 - (y + TERRAIN_DIMS * 0.5) / TERRAIN_DIMS; 103 | return this.#heightmap_.Get(xn, yn) - TERRAIN_OFFSET; 104 | } 105 | 106 | IsReady() { 107 | return this.#heightmap_ != null; 108 | } 109 | 110 | InitEntity() { 111 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 112 | const geometry = new THREE.PlaneGeometry(TERRAIN_DIMS, TERRAIN_DIMS, 256, 256); 113 | 114 | const textureLoader = new THREE.TextureLoader(); 115 | textureLoader.load( 116 | './textures/' + 'terrain.png', 117 | (heightmapTexture) => { 118 | const heightmapGenerator = new Heightmap({ 119 | dimensions: new THREE.Vector2(1.0, 1.0), 120 | offset: new THREE.Vector2(0.0, 0.0), 121 | height: TERRAIN_HEIGHT 122 | }, heightmapTexture.image); 123 | 124 | this.#heightmap_ = heightmapGenerator; 125 | 126 | const positions = geometry.attributes.position; 127 | const uv = geometry.attributes.uv; 128 | for (let i = 0; i < positions.count; i++) { 129 | const h = heightmapGenerator.Get(uv.array[i*2+0], uv.array[i*2+1]) - TERRAIN_OFFSET; 130 | positions.array[i*3+2] = h; 131 | } 132 | 133 | geometry.computeVertexNormals(); 134 | geometry.computeTangents(); 135 | 136 | const position16 = Float32ToFloat16(geometry.attributes.position.array); 137 | const normal16 = Float32ToFloat16(geometry.attributes.normal.array); 138 | const tangent16 = Float32ToFloat16(geometry.attributes.tangent.array); 139 | const uv16 = Float32ToFloat16(geometry.attributes.uv.array); 140 | 141 | geometry.setAttribute('position', new THREE.Float16BufferAttribute(position16, 3)); 142 | geometry.setAttribute('normal', new THREE.Float16BufferAttribute(normal16, 3)); 143 | geometry.setAttribute('tangent', new THREE.Float16BufferAttribute(tangent16, 3)); 144 | geometry.setAttribute('uv', new THREE.Float16BufferAttribute(uv16, 2)); 145 | geometry.rotateX(-Math.PI / 2); 146 | 147 | heightmapTexture.colorSpace = THREE.LinearSRGBColorSpace; 148 | 149 | const LOAD_ = (name) => { 150 | const albedo = textureLoader.load('./textures/' + name); 151 | albedo.magFilter = THREE.LinearFilter; 152 | albedo.minFilter = THREE.LinearMipMapLinearFilter; 153 | albedo.wrapS = THREE.RepeatWrapping; 154 | albedo.wrapT = THREE.RepeatWrapping; 155 | albedo.anisotropy = 16; 156 | albedo.repeat.set(40, 40); 157 | return albedo; 158 | } 159 | 160 | // const grassAlbedo = LOAD_('wispy-grass-meadow_albedo.png'); 161 | const grid = LOAD_('grid.png'); 162 | grid.anisotropy = 16; 163 | grid.repeat.set(1, 1); 164 | 165 | const terrainMaterial = new shaders.GamePBRMaterial('TERRAIN', {}); 166 | terrainMaterial.setTexture('heightmap', heightmapTexture); 167 | terrainMaterial.setTexture('grid', grid); 168 | terrainMaterial.setVec4('heightParams', new THREE.Vector4(TERRAIN_DIMS, TERRAIN_DIMS, TERRAIN_HEIGHT, TERRAIN_OFFSET)); 169 | 170 | this.#mesh_ = new THREE.Mesh(geometry, terrainMaterial); 171 | this.#mesh_.position.set(0, 0, 0); 172 | this.#mesh_.receiveShadow = true; 173 | this.#mesh_.castShadow = false; 174 | 175 | threejs.AddSceneObject(this.#mesh_); 176 | 177 | this.Broadcast({ 178 | topic: 'render.loaded', 179 | value: this.#mesh_, 180 | }); 181 | 182 | 183 | const mountain = new entity.Entity(); 184 | mountain.AddComponent(new render_component.RenderComponent({ 185 | resourcePath: './models/', 186 | resourceName: 'mountain.glb', 187 | scale: new THREE.Vector3(1, 1, 1), 188 | emissive: new THREE.Color(0x000000), 189 | color: new THREE.Color(0xFFFFFF), 190 | receiveShadow: false, 191 | castShadow: false, 192 | })); 193 | 194 | mountain.SetPosition(new THREE.Vector3(0, -100, 0)); 195 | mountain.SetActive(false); 196 | mountain.Init(); 197 | 198 | const water = new entity.Entity(); 199 | water.AddComponent(new water_component.WaterComponent({ 200 | terrain: this.Parent, 201 | height: TERRAIN_HEIGHT, 202 | offset: TERRAIN_OFFSET, 203 | heightmap: heightmapTexture 204 | })); 205 | water.SetActive(true); 206 | water.Init(this.Parent); 207 | 208 | const grass = new entity.Entity(); 209 | grass.AddComponent(new grass_component.GrassComponent({ 210 | terrain: this.Parent, 211 | height: TERRAIN_HEIGHT, 212 | offset: TERRAIN_OFFSET, 213 | dims: TERRAIN_DIMS, 214 | heightmap: heightmapTexture 215 | })); 216 | grass.SetActive(true); 217 | grass.Init(this.Parent); 218 | 219 | const bugs = new entity.Entity(); 220 | bugs.AddComponent(new bugs_component.BugsComponent({ 221 | terrain: this.Parent, 222 | height: TERRAIN_HEIGHT, 223 | offset: TERRAIN_OFFSET, 224 | dims: TERRAIN_DIMS, 225 | heightmap: heightmapTexture 226 | })); 227 | bugs.SetActive(true); 228 | bugs.Init(this.Parent); 229 | 230 | const wind = new entity.Entity(); 231 | wind.AddComponent(new wind_component.WindComponent({ 232 | terrain: this.Parent, 233 | height: TERRAIN_HEIGHT, 234 | offset: TERRAIN_OFFSET, 235 | dims: TERRAIN_DIMS, 236 | heightmap: heightmapTexture 237 | })); 238 | wind.SetActive(true); 239 | wind.Init(this.Parent); 240 | }); 241 | } 242 | 243 | Update(timeElapsed) { 244 | } 245 | } -------------------------------------------------------------------------------- /src/base/render/water-component.js: -------------------------------------------------------------------------------- 1 | import {THREE} from '../three-defs.js'; 2 | 3 | import * as shaders from '../../game/render/shaders.js'; 4 | 5 | import * as entity from "../entity.js"; 6 | 7 | 8 | class InstancedFloat16BufferAttribute extends THREE.InstancedBufferAttribute { 9 | 10 | constructor( array, itemSize, normalized, meshPerAttribute = 1 ) { 11 | 12 | super( new Uint16Array( array ), itemSize, normalized, meshPerAttribute ); 13 | 14 | this.isFloat16BufferAttribute = true; 15 | } 16 | }; 17 | 18 | 19 | const NUM_BUGS = 6; 20 | const NUM_SEGMENTS = 2; 21 | const NUM_VERTICES = (NUM_SEGMENTS + 1) * 2; 22 | const BUG_SPAWN_RANGE = 40.0; 23 | const BUG_MAX_DIST = 100.0; 24 | 25 | const M_TMP = new THREE.Matrix4(); 26 | const AABB_TMP = new THREE.Box3(); 27 | 28 | 29 | export class WaterComponent extends entity.Component { 30 | static CLASS_NAME = 'WaterComponent'; 31 | 32 | get NAME() { 33 | return WaterComponent.CLASS_NAME; 34 | } 35 | 36 | #params_; 37 | #mesh_; 38 | #group_; 39 | #totalTime_; 40 | #material_; 41 | #geometry_; 42 | 43 | constructor(params) { 44 | super(); 45 | 46 | this.#params_ = params; 47 | this.#mesh_ = []; 48 | this.#group_ = new THREE.Group(); 49 | this.#totalTime_ = 0; 50 | this.#geometry_ = null; 51 | } 52 | 53 | Destroy() { 54 | this.#mesh_.removeFromParent(); 55 | this.#group_.removeFromParent(); 56 | } 57 | 58 | #CreateGeometry_() { 59 | const plane = new THREE.PlaneGeometry(2000, 2000, 1, 1); 60 | plane.rotateX(-Math.PI / 2); 61 | 62 | return plane; 63 | } 64 | 65 | InitEntity() { 66 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 67 | 68 | this.#geometry_ = this.#CreateGeometry_(); 69 | this.#material_ = new shaders.GameMaterial('WATER'); 70 | this.#material_.depthWrite = false; 71 | this.#material_.depthTest = true; 72 | this.#mesh_ = this.#CreateMesh_(); 73 | this.#mesh_.position.y = -14.0; 74 | 75 | this.#group_.add(this.#mesh_); 76 | 77 | threejs.AddSceneObject(this.#group_, {pass: 'water'}); 78 | } 79 | 80 | #CreateMesh_() { 81 | const m = new THREE.Mesh(this.#geometry_, this.#material_); 82 | m.receiveShadow = true; 83 | m.castShadow = false; 84 | m.visible = true; 85 | 86 | return m; 87 | } 88 | 89 | Update(timeElapsed) { 90 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 91 | 92 | this.#totalTime_ += timeElapsed; 93 | 94 | this.#material_.setFloat('time', this.#totalTime_); 95 | this.#material_.setVec2('resolution', new THREE.Vector2(window.innerWidth, window.innerHeight)); 96 | this.#material_.setTexture('colourTexture', threejs.WaterTexture); 97 | this.#material_.setMatrix('inverseProjectMatrix', threejs.Camera.projectionMatrixInverse); 98 | } 99 | } -------------------------------------------------------------------------------- /src/base/render/wind-component.js: -------------------------------------------------------------------------------- 1 | import {THREE} from '../three-defs.js'; 2 | 3 | import * as shaders from '../../game/render/shaders.js'; 4 | 5 | import * as entity from "../entity.js"; 6 | 7 | import * as terrain_component from './terrain-component.js'; 8 | import MersenneTwister from 'mersenne-twister'; 9 | 10 | 11 | 12 | const NUM_BUGS = 8; 13 | const BUG_SPAWN_RANGE = 20.0; 14 | const BUG_MAX_DIST = 50.0; 15 | 16 | const M_TMP = new THREE.Matrix4(); 17 | const AABB_TMP = new THREE.Box3(); 18 | 19 | 20 | export class WindComponent extends entity.Component { 21 | static CLASS_NAME = 'WindComponent'; 22 | 23 | get NAME() { 24 | return WindComponent.CLASS_NAME; 25 | } 26 | 27 | #params_; 28 | #meshes_; 29 | #group_; 30 | #totalTime_; 31 | #material_; 32 | #geometry_; 33 | 34 | constructor(params) { 35 | super(); 36 | 37 | this.#params_ = params; 38 | this.#meshes_ = []; 39 | this.#group_ = new THREE.Group(); 40 | this.#totalTime_ = 0; 41 | this.#geometry_ = null; 42 | } 43 | 44 | Destroy() { 45 | for (let m of this.#meshes_) { 46 | m.removeFromParent(); 47 | } 48 | this.#group_.removeFromParent(); 49 | } 50 | 51 | #CreateGeometry_() { 52 | const rng = new MersenneTwister(1); 53 | 54 | const offsets = new Float32Array(NUM_BUGS * 3); 55 | for (let i = 0; i < NUM_BUGS; ++i) { 56 | offsets[i*3 + 0] = (rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2); 57 | offsets[i*3 + 1] = rng.random() * 1.0 + 2.0; 58 | offsets[i*3 + 2] = (rng.random() * 2.0 - 1.0) * (BUG_SPAWN_RANGE / 2); 59 | } 60 | 61 | const plane = new THREE.PlaneGeometry(1, 1, 1, 1); 62 | 63 | const geo = new THREE.InstancedBufferGeometry(); 64 | geo.instanceCount = NUM_BUGS; 65 | geo.setAttribute('position', plane.attributes.position); 66 | geo.setAttribute('uv', plane.attributes.uv); 67 | geo.setAttribute('normal', plane.attributes.normal); 68 | geo.setAttribute('offset', new THREE.InstancedBufferAttribute(offsets, 3)); 69 | geo.setIndex(plane.index); 70 | geo.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), BUG_SPAWN_RANGE); 71 | 72 | return geo; 73 | } 74 | 75 | InitEntity() { 76 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 77 | 78 | this.#geometry_ = this.#CreateGeometry_(); 79 | 80 | const textureLoader = new THREE.TextureLoader(); 81 | const albedo = textureLoader.load('./textures/' + 'dust.png'); 82 | albedo.colorSpace = THREE.SRGBColorSpace; 83 | 84 | this.#material_ = new shaders.ShaderMaterial('WIND', { 85 | uniforms: { 86 | time: { value: 0.0 }, 87 | diffuseTexture: { value: albedo }, 88 | dustSize: { value: new THREE.Vector2(0.4, 0.4) }, 89 | heightmap: { value: this.#params_.heightmap }, 90 | heightmapParams: { value: new THREE.Vector3(this.#params_.height, this.#params_.offset, this.#params_.dims) }, 91 | } 92 | }); 93 | this.#material_.transparent = true; 94 | this.#material_.side = THREE.DoubleSide; 95 | this.#material_.depthWrite = false; 96 | this.#material_.depthTest = true; 97 | 98 | threejs.AddSceneObject(this.#group_, {pass: 'transparent'}); 99 | } 100 | 101 | #CreateMesh_() { 102 | const m = new THREE.Mesh(this.#geometry_, this.#material_); 103 | m.receiveShadow = false; 104 | m.castShadow = false; 105 | m.visible = true; 106 | 107 | this.#meshes_.push(m); 108 | this.#group_.add(m); 109 | 110 | return m; 111 | } 112 | 113 | Update(timeElapsed) { 114 | this.#totalTime_ += timeElapsed; 115 | 116 | this.#material_.uniforms.time.value = this.#totalTime_; 117 | 118 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 119 | const camera = threejs.Camera; 120 | const frustum = new THREE.Frustum().setFromProjectionMatrix(M_TMP.copy(camera.projectionMatrix).multiply(camera.matrixWorldInverse)); 121 | 122 | const meshes = [...this.#meshes_]; 123 | 124 | const baseCellPos = camera.position.clone(); 125 | baseCellPos.divideScalar(BUG_SPAWN_RANGE); 126 | baseCellPos.floor(); 127 | baseCellPos.multiplyScalar(BUG_SPAWN_RANGE); 128 | 129 | // This is dumb and slow 130 | for (let c of this.#group_.children) { 131 | c.visible = false; 132 | } 133 | 134 | const terrain = this.#params_.terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME); 135 | 136 | const cameraPosXZ = new THREE.Vector3(camera.position.x, 0, camera.position.z); 137 | 138 | for (let x = -3; x < 3; x++) { 139 | for (let z = -3; z < 3; z++) { 140 | // Current cell 141 | const currentCell = new THREE.Vector3( 142 | baseCellPos.x + x * BUG_SPAWN_RANGE, 0, baseCellPos.z + z * BUG_SPAWN_RANGE); 143 | currentCell.y = terrain.GetHeight(currentCell.x, currentCell.z); 144 | 145 | AABB_TMP.setFromCenterAndSize(currentCell, new THREE.Vector3(BUG_SPAWN_RANGE, 100, BUG_SPAWN_RANGE)); 146 | const distToCell = AABB_TMP.distanceToPoint(cameraPosXZ); 147 | if (distToCell > BUG_MAX_DIST) { 148 | continue; 149 | } 150 | 151 | if (!frustum.intersectsBox(AABB_TMP)) { 152 | continue; 153 | } 154 | 155 | const m = meshes.length > 0 ? meshes.pop() : this.#CreateMesh_(); 156 | m.position.copy(currentCell); 157 | m.position.y = 0; 158 | m.visible = true; 159 | } 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /src/base/three-defs.js: -------------------------------------------------------------------------------- 1 | 2 | import * as THREE from 'three'; 3 | 4 | import {EffectComposer} from 'three/addons/postprocessing/EffectComposer.js'; 5 | import {ShaderPass} from 'three/addons/postprocessing/ShaderPass.js'; 6 | import {RenderPass} from 'three/addons/postprocessing/RenderPass.js'; 7 | import {UnrealBloomPass} from 'three/addons/postprocessing/UnrealBloomPass.js'; 8 | 9 | import {GammaCorrectionShader} from 'three/addons/shaders/GammaCorrectionShader.js'; 10 | import {ACESFilmicToneMappingShader} from 'three/addons/shaders/ACESFilmicToneMappingShader.js'; 11 | import {FXAAShader} from 'three/addons/shaders/FXAAShader.js'; 12 | 13 | import {FBXLoader} from 'three/addons/loaders/FBXLoader.js'; 14 | import {GLTFLoader} from 'three/addons/loaders/GLTFLoader.js'; 15 | 16 | import { CSM } from 'three/addons/csm/CSM.js'; 17 | import { CSMShader } from 'three/addons/csm/CSMShader.js'; 18 | 19 | import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js'; 20 | import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'; 21 | 22 | export function Float32ToFloat16(data) { 23 | const data16 = new Uint16Array(data.length); 24 | for (let i = 0; i < data.length; i++) { 25 | data16[i] = THREE.DataUtils.toHalfFloat(data[i]); 26 | } 27 | return data16; 28 | } 29 | 30 | export { 31 | THREE, EffectComposer, ShaderPass, GammaCorrectionShader, ACESFilmicToneMappingShader, 32 | RenderPass, FXAAShader, UnrealBloomPass, 33 | FBXLoader, GLTFLoader, SkeletonUtils, BufferGeometryUtils, 34 | CSM, CSMShader, 35 | }; 36 | -------------------------------------------------------------------------------- /src/base/threejs-component.js: -------------------------------------------------------------------------------- 1 | import { THREE, RenderPass, ShaderPass, FXAAShader, ACESFilmicToneMappingShader } from './three-defs.js'; 2 | 3 | import Stats from 'three/examples/jsm/libs/stats.module.js'; 4 | 5 | import * as entity from "./entity.js"; 6 | import * as light_component from './render/light-component.js'; 7 | import * as shaders from '../game/render/shaders.js'; 8 | 9 | 10 | const HEMI_UP = new THREE.Color().setHex(0x7CBFFF, THREE.SRGBColorSpace); 11 | const HEMI_DOWN = new THREE.Color().setHex(0xE5BCFF, THREE.SRGBColorSpace); 12 | const HEMI_INTENSITY = 0.25; 13 | const LIGHT_INTENSITY = 0.7; 14 | const LIGHT_COLOUR = new THREE.Color().setRGB(0.52, 0.66, 0.99, THREE.SRGBColorSpace); 15 | const LIGHT_FAR = 1000.0; 16 | 17 | 18 | const GammaCorrectionShader2 = { 19 | name: 'GammaCorrectionShader2', 20 | uniforms: { 21 | 'tDiffuse': { value: null }, 22 | 'exposure': { value: 1.0 }, 23 | }, 24 | vertexShader: /* glsl */` 25 | varying vec2 vUv; 26 | 27 | void main() { 28 | vUv = uv; 29 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 30 | 31 | }`, 32 | fragmentShader: /* glsl */` 33 | uniform sampler2D tDiffuse; 34 | varying vec2 vUv; 35 | 36 | #define saturate(a) clamp( a, 0.0, 1.0 ) 37 | 38 | float inverseLerp(float minValue, float maxValue, float v) { 39 | return (v - minValue) / (maxValue - minValue); 40 | } 41 | 42 | float remap(float v, float inMin, float inMax, float outMin, float outMax) { 43 | float t = inverseLerp(inMin, inMax, v); 44 | return mix(outMin, outMax, t); 45 | } 46 | 47 | uniform float exposure; 48 | 49 | vec3 RRTAndODTFit( vec3 v ) { 50 | 51 | vec3 a = v * ( v + 0.0245786 ) - 0.000090537; 52 | vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; 53 | return a / b; 54 | 55 | } 56 | 57 | vec3 ACESFilmicToneMapping( vec3 color ) { 58 | 59 | // sRGB => XYZ => D65_2_D60 => AP1 => RRT_SAT 60 | const mat3 ACESInputMat = mat3( 61 | vec3( 0.59719, 0.07600, 0.02840 ), // transposed from source 62 | vec3( 0.35458, 0.90834, 0.13383 ), 63 | vec3( 0.04823, 0.01566, 0.83777 ) 64 | ); 65 | 66 | // ODT_SAT => XYZ => D60_2_D65 => sRGB 67 | const mat3 ACESOutputMat = mat3( 68 | vec3( 1.60475, -0.10208, -0.00327 ), // transposed from source 69 | vec3( -0.53108, 1.10813, -0.07276 ), 70 | vec3( -0.07367, -0.00605, 1.07602 ) 71 | ); 72 | 73 | color = ACESInputMat * color; 74 | 75 | // Apply RRT and ODT 76 | color = RRTAndODTFit( color ); 77 | 78 | color = ACESOutputMat * color; 79 | 80 | // Clamp to [0, 1] 81 | return saturate( color ); 82 | 83 | } 84 | 85 | vec3 vignette(vec2 uvs) { 86 | float v1 = smoothstep(0.5, 0.3, abs(uvs.x - 0.5)); 87 | float v2 = smoothstep(0.5, 0.3, abs(uvs.y - 0.5)); 88 | float v = v1 * v2; 89 | v = pow(v, 0.125); 90 | v = remap(v, 0.0, 1.0, 0.4, 1.0); 91 | return vec3(v); 92 | } 93 | 94 | void main() { 95 | vec4 tex = texture2D( tDiffuse, vUv ); 96 | 97 | tex.rgb *= exposure / 0.6; // pre-exposed, outside of the tone mapping function 98 | tex.rgb = ACESFilmicToneMapping( tex.rgb ); 99 | 100 | tex = LinearTosRGB(tex); 101 | tex.rgb *= vignette(vUv); 102 | 103 | gl_FragColor = tex; 104 | }` 105 | }; 106 | 107 | const Copy2 = { 108 | 109 | name: 'Copy2', 110 | 111 | uniforms: { 112 | 113 | 'tDiffuse': { value: null } 114 | 115 | }, 116 | 117 | vertexShader: /* glsl */` 118 | 119 | varying vec2 vUv; 120 | 121 | void main() { 122 | 123 | vUv = uv; 124 | gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); 125 | 126 | }`, 127 | 128 | fragmentShader: /* glsl */` 129 | 130 | uniform sampler2D tDiffuse; 131 | 132 | varying vec2 vUv; 133 | 134 | void main() { 135 | 136 | vec4 tex = texture2D( tDiffuse, vUv ); 137 | 138 | gl_FragColor = tex; 139 | 140 | }` 141 | 142 | }; 143 | 144 | 145 | class FakeCSM { 146 | constructor() { 147 | this.lights = [{ 148 | color: new THREE.Color(0xFFFFFF), 149 | lightDirection: new THREE.Vector3(1, 0, 0), 150 | }]; 151 | this.lightDirection = new THREE.Vector3(1, 0, 0); 152 | } 153 | setupMaterial() {} 154 | updateFrustums() {} 155 | update() {} 156 | 157 | } 158 | 159 | export const threejs_component = (() => { 160 | 161 | class ThreeJSController extends entity.Component { 162 | static CLASS_NAME = 'ThreeJSController'; 163 | 164 | get NAME() { 165 | return ThreeJSController.CLASS_NAME; 166 | } 167 | 168 | #threejs_; 169 | #csm_; 170 | 171 | #ssaoPass_; 172 | #opaqueScene_; 173 | #opaquePass_; 174 | #waterScene_; 175 | #waterPass_; 176 | 177 | #opaqueCamera_; 178 | #waterCamera_; 179 | 180 | #transparentScene_; 181 | #transparentPass_; 182 | #transparentCamera_; 183 | 184 | #waterTexturePass_; 185 | 186 | #fxaaPass_; 187 | #acesPass_; 188 | #gammaPass_; 189 | #copyPass_; 190 | 191 | #grassTimingAvg_; 192 | 193 | constructor() { 194 | super(); 195 | 196 | this.#threejs_ = null; 197 | this.#ssaoPass_ = null; 198 | this.#opaqueScene_ = null; 199 | this.#opaquePass_ = null; 200 | this.#opaqueCamera_ = null; 201 | this.#waterScene_ = null; 202 | this.#waterCamera_ = null; 203 | this.#waterPass_ = null; 204 | this.#waterScene_ = null; 205 | this.#waterCamera_ = null; 206 | this.#waterPass_ = null; 207 | this.#waterTexturePass_ = null; 208 | this.#fxaaPass_ = null; 209 | this.#acesPass_ = null; 210 | this.#gammaPass_ = null; 211 | this.#copyPass_ = null; 212 | this.#csm_ = null; 213 | this.grassTimingAvg_ = 0; 214 | this.timerQuery = null; 215 | } 216 | 217 | InitEntity() { 218 | shaders.SetThreeJS(this); 219 | 220 | this.#threejs_ = new THREE.WebGLRenderer({ 221 | antialias: false, 222 | powerPreference: 'high-performance', 223 | }); 224 | this.#threejs_.shadowMap.enabled = true; 225 | this.#threejs_.shadowMap.type = THREE.PCFSoftShadowMap; 226 | this.#threejs_.setSize(window.innerWidth, window.innerHeight); 227 | this.#threejs_.domElement.id = 'threejs'; 228 | this.#threejs_.outputColorSpace = THREE.LinearSRGBColorSpace; 229 | 230 | document.getElementById('container').appendChild(this.#threejs_.domElement); 231 | 232 | window.addEventListener('resize', () => { 233 | this.onWindowResize_(); 234 | }, false); 235 | 236 | const fov = 60; 237 | const aspect = 1920 / 1080; 238 | const near = 0.1; 239 | const far = 10000.0; 240 | this.#opaqueCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far); 241 | this.#opaqueCamera_.position.set(20, 5, 15); 242 | 243 | this.#waterCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far); 244 | 245 | this.#opaqueScene_ = new THREE.Scene(); 246 | this.#opaqueScene_.add(this.#opaqueCamera_); 247 | 248 | this.#waterScene_ = new THREE.Scene(); 249 | this.#waterScene_.add(this.#waterCamera_); 250 | 251 | this.#transparentScene_ = new THREE.Scene(); 252 | this.#transparentCamera_ = new THREE.PerspectiveCamera(fov, aspect, near, far); 253 | this.#transparentScene_.add(this.#transparentCamera_); 254 | 255 | this.uiCamera_ = new THREE.OrthographicCamera( 256 | -1, 1, 1, -1, 1, 1000); 257 | this.uiScene_ = new THREE.Scene(); 258 | 259 | this.#opaqueScene_.fog = new THREE.FogExp2(0xDFE9F3, 0.0001); 260 | this.#opaqueScene_.fog.color.setRGB(0.45, 0.8, 1.0, THREE.SRGBColorSpace); 261 | 262 | let light = new THREE.DirectionalLight(0xFFFFFF, LIGHT_INTENSITY); 263 | light.position.set(-20, 20, 20); 264 | light.target.position.set(0, 0, 0); 265 | light.color.copy(LIGHT_COLOUR); 266 | 267 | this.#csm_ = new FakeCSM(); 268 | 269 | // VIDEO HACK 270 | light.castShadow = true; 271 | light.shadow.bias = -0.001; 272 | light.shadow.mapSize.width = 4096; 273 | light.shadow.mapSize.height = 4096; 274 | light.shadow.camera.near = 1.0; 275 | light.shadow.camera.far = 100.0; 276 | light.shadow.camera.left = 32; 277 | light.shadow.camera.right = -32; 278 | light.shadow.camera.top = 32; 279 | light.shadow.camera.bottom = -32; 280 | this.#opaqueScene_.add(light); 281 | this.#opaqueScene_.add(light.target); 282 | 283 | const lightDir = light.position.clone(); 284 | lightDir.normalize(); 285 | lightDir.multiplyScalar(-1); 286 | 287 | const csmFar = LIGHT_FAR; 288 | // this.#csm_ = new CSM({ 289 | // maxFar: csmFar, 290 | // fade: true, 291 | // cascades: 6, 292 | // mode: 'practical', 293 | // parent: this.#opaqueScene_, 294 | // shadowMapSize: 2048, 295 | // lightIntensity: LIGHT_INTENSITY, 296 | // lightNear: 1.0, 297 | // lightFar: csmFar, 298 | // lightDirection: lightDir, 299 | // camera: this.#opaqueCamera_ 300 | // }); 301 | // this.#csm_.fade = true; 302 | 303 | for (let i = 0; i < this.#csm_.lights.length; i++) { 304 | this.#csm_.lights[i].color.copy(LIGHT_COLOUR); 305 | } 306 | 307 | this.#csm_.updateFrustums(); 308 | 309 | this.sun_ = light; 310 | 311 | const waterParams = { 312 | type: THREE.HalfFloatType, 313 | magFilter: THREE.NearestFilter, 314 | minFilter: THREE.NearestFilter, 315 | wrapS: THREE.ClampToEdgeWrapping, 316 | wrapT: THREE.ClampToEdgeWrapping, 317 | generateMipmaps: false, 318 | }; 319 | this.waterBuffer_ = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, waterParams); 320 | this.waterBuffer_.stencilBuffer = false; 321 | 322 | const bufferParams = { 323 | type: THREE.HalfFloatType, 324 | magFilter: THREE.LinearFilter, 325 | minFilter: THREE.LinearFilter, 326 | wrapS: THREE.ClampToEdgeWrapping, 327 | wrapT: THREE.ClampToEdgeWrapping, 328 | generateMipmaps: false, 329 | }; 330 | this.readBuffer_ = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, bufferParams); 331 | this.readBuffer_.stencilBuffer = false; 332 | this.readBuffer_.depthTexture = new THREE.DepthTexture(); 333 | this.readBuffer_.depthTexture.format = THREE.DepthStencilFormat; 334 | this.readBuffer_.depthTexture.type = THREE.UnsignedInt248Type; 335 | 336 | this.writeBuffer_ = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight, bufferParams); 337 | this.writeBuffer_.stencilBuffer = false; 338 | this.writeBuffer_.depthTexture = new THREE.DepthTexture(); 339 | this.writeBuffer_.depthTexture.format = THREE.DepthStencilFormat; 340 | this.writeBuffer_.depthTexture.type = THREE.UnsignedInt248Type; 341 | 342 | this.#opaquePass_ = new RenderPass(this.#opaqueScene_, this.#opaqueCamera_); 343 | this.#waterPass_ = new RenderPass(this.#waterScene_, this.#opaqueCamera_); 344 | this.#transparentPass_ = new RenderPass(this.#transparentScene_, this.#transparentCamera_); 345 | 346 | const f = this.#opaqueCamera_.far; 347 | const n = this.#opaqueCamera_.near; 348 | const shader = new shaders.ShaderMaterial('WATER-TEXTURE', { 349 | uniforms: { 350 | colourTexture: { value: null }, 351 | depthTexture: { value: null }, 352 | nearFar: { value: new THREE.Vector3(f * n, f - n, f) }, 353 | } 354 | }); 355 | 356 | this.#waterTexturePass_ = new ShaderPass(shader); 357 | this.#fxaaPass_ = new ShaderPass(FXAAShader); 358 | this.#acesPass_ = new ShaderPass(ACESFilmicToneMappingShader); 359 | this.#gammaPass_ = new ShaderPass(GammaCorrectionShader2); 360 | this.#copyPass_ = new ShaderPass(Copy2); 361 | 362 | const hemiLight = new entity.Entity(); 363 | hemiLight.AddComponent(new light_component.LightComponent({ 364 | hemi: { 365 | // upColour: 0x7CBFFF, 366 | // downColour: 0xFFE5BC, 367 | upColour: HEMI_UP, 368 | downColour: HEMI_DOWN, 369 | intensity: HEMI_INTENSITY, 370 | } 371 | })); 372 | hemiLight.SetActive(false); 373 | hemiLight.Init(this.Parent); 374 | 375 | this.stats_ = new Stats(); 376 | this.grassStats_ = new Stats.Panel('Grass MS', '#0f0', '#020'); 377 | this.stats_.addPanel(this.grassStats_); 378 | this.stats_.showPanel(0); 379 | document.body.appendChild(this.stats_.dom); 380 | 381 | this.onWindowResize_(); 382 | } 383 | 384 | get Scene() { 385 | return this.#opaqueScene_; 386 | } 387 | 388 | get Camera() { 389 | return this.#opaqueCamera_; 390 | } 391 | 392 | get WaterTexture() { 393 | return this.waterBuffer_.texture; 394 | } 395 | 396 | get WaterDepthTexture() { 397 | return this.waterBuffer_.texture; 398 | } 399 | 400 | getMaxAnisotropy() { 401 | return this.#threejs_.capabilities.getMaxAnisotropy(); 402 | } 403 | 404 | onWindowResize_() { 405 | const w = window.innerWidth; 406 | const h = window.innerHeight 407 | this.#opaqueCamera_.aspect = w / h; 408 | this.#opaqueCamera_.updateProjectionMatrix(); 409 | 410 | this.#waterCamera_.aspect = this.#opaqueCamera_.aspect; 411 | this.#waterCamera_.updateProjectionMatrix(); 412 | 413 | this.#transparentCamera_.aspect = this.#opaqueCamera_.aspect; 414 | this.#transparentCamera_.updateProjectionMatrix(); 415 | 416 | this.#threejs_.setSize(w, h); 417 | // this.composer_.setSize(window.innerWidth, window.innerHeight); 418 | 419 | this.waterBuffer_.setSize(w, h); 420 | this.writeBuffer_.setSize(w, h); 421 | this.readBuffer_.setSize(w, h); 422 | // this.csm_.updateFrustums(); 423 | 424 | this.#waterTexturePass_.setSize(w, h); 425 | 426 | this.#fxaaPass_.material.uniforms['resolution'].value.x = 1 / w; 427 | this.#fxaaPass_.material.uniforms['resolution'].value.y = 1 / h; 428 | 429 | this.#csm_.updateFrustums(); 430 | } 431 | 432 | swapBuffers_() { 433 | const tmp = this.writeBuffer_; 434 | this.writeBuffer_ = this.readBuffer_; 435 | this.readBuffer_ = tmp; 436 | } 437 | 438 | SetupMaterial(material) { 439 | this.#csm_.setupMaterial(material); 440 | } 441 | 442 | AddSceneObject(obj, params) { 443 | params = params || {}; 444 | 445 | if (params.pass == 'water') { 446 | this.#waterScene_.add(obj); 447 | } else if (params.pass == 'transparent') { 448 | this.#transparentScene_.add(obj); 449 | } else { 450 | this.#opaqueScene_.add(obj); 451 | } 452 | } 453 | 454 | Render(timeElapsedS) { 455 | this.#waterCamera_.position.copy(this.#opaqueCamera_.position); 456 | this.#waterCamera_.quaternion.copy(this.#opaqueCamera_.quaternion); 457 | 458 | this.#transparentCamera_.position.copy(this.#opaqueCamera_.position); 459 | this.#transparentCamera_.quaternion.copy(this.#opaqueCamera_.quaternion); 460 | 461 | this.stats_.begin(); 462 | 463 | this.#threejs_.autoClear = true; 464 | this.#threejs_.autoClearColor = true; 465 | this.#threejs_.autoClearDepth = true; 466 | this.#threejs_.autoClearStencil = true; 467 | this.#threejs_.setRenderTarget(this.writeBuffer_); 468 | this.#threejs_.clear(); 469 | this.#threejs_.setRenderTarget(this.readBuffer_); 470 | this.#threejs_.clear(); 471 | this.#threejs_.setRenderTarget(null); 472 | this.#opaquePass_.renderToScreen = false; 473 | this.#opaquePass_.render(this.#threejs_, null, this.writeBuffer_, timeElapsedS, false); 474 | this.writeBuffer_.ACTIVE_HAS_OPAQUE = true; 475 | this.readBuffer_.ACTIVE_HAS_OPAQUE = false; 476 | this.swapBuffers_(); 477 | 478 | this.#threejs_.autoClear = false; 479 | this.#threejs_.autoClearColor = false; 480 | this.#threejs_.autoClearDepth = false; 481 | this.#threejs_.autoClearStencil = false; 482 | this.swapBuffers_(); 483 | 484 | this.#waterTexturePass_.clear = false; 485 | this.#waterTexturePass_.renderToScreen = false; 486 | this.#waterTexturePass_.material.uniforms.colourTexture.value = this.writeBuffer_.texture; 487 | this.#waterTexturePass_.material.uniforms.depthTexture.value = this.writeBuffer_.depthTexture; 488 | this.#waterTexturePass_.render(this.#threejs_, this.waterBuffer_, null, timeElapsedS, false); 489 | 490 | this.#waterPass_.clear = false; 491 | this.#waterPass_.render(this.#threejs_, this.null, this.writeBuffer_, timeElapsedS, false); 492 | 493 | this.#transparentPass_.renderToScreen = false; 494 | this.#transparentPass_.clear = false; 495 | this.#transparentPass_.render(this.#threejs_, null, this.writeBuffer_, timeElapsedS, false); 496 | this.writeBuffer_.ACTIVE_HAS_WATER = true; 497 | this.readBuffer_.ACTIVE_HAS_WATER = false; 498 | this.swapBuffers_(); 499 | 500 | this.#gammaPass_.clear = false; 501 | this.#gammaPass_.renderToScreen = true; 502 | this.#gammaPass_.render(this.#threejs_, null, this.readBuffer_, timeElapsedS, false); 503 | 504 | this.stats_.end(); 505 | } 506 | 507 | Update(timeElapsed) { 508 | const player = this.FindEntity('player'); 509 | if (!player) { 510 | return; 511 | } 512 | const pos = player.Position; 513 | 514 | this.#csm_.update(); 515 | 516 | this.sun_.position.copy(pos); 517 | this.sun_.position.add(new THREE.Vector3(-10, 40, 10)); 518 | this.sun_.target.position.copy(pos); 519 | this.sun_.updateMatrixWorld(); 520 | this.sun_.target.updateMatrixWorld(); 521 | } 522 | } 523 | 524 | return { 525 | ThreeJSController: ThreeJSController, 526 | }; 527 | })(); -------------------------------------------------------------------------------- /src/demo-builder.js: -------------------------------------------------------------------------------- 1 | import {THREE} from './base/three-defs.js'; 2 | 3 | import * as entity from './base/entity.js'; 4 | import * as terrain_component from './base/render/terrain-component.js'; 5 | import * as shaders from './game/render/shaders.js'; 6 | 7 | 8 | export const demo_builder = (() => { 9 | 10 | class DemoBuilder extends entity.Component { 11 | static CLASS_NAME = 'DemoBuilder'; 12 | 13 | get NAME() { 14 | return DemoBuilder.CLASS_NAME; 15 | } 16 | 17 | constructor(params) { 18 | super(); 19 | 20 | this.params_ = params; 21 | this.spawned_ = false; 22 | this.materials_ = {}; 23 | } 24 | 25 | LoadMaterial_(albedoName, normalName, roughnessName, metalnessName) { 26 | const textureLoader = new THREE.TextureLoader(); 27 | const albedo = textureLoader.load('./textures/' + albedoName); 28 | albedo.anisotropy = this.FindEntity('threejs').GetComponent('ThreeJSController').getMaxAnisotropy(); 29 | albedo.wrapS = THREE.RepeatWrapping; 30 | albedo.wrapT = THREE.RepeatWrapping; 31 | albedo.colorSpace = THREE.SRGBColorSpace; 32 | 33 | const material = new shaders.GameMaterial('PHONG', { 34 | map: albedo, 35 | color: 0x303030, 36 | }); 37 | 38 | return material; 39 | } 40 | 41 | BuildHackModel_() { 42 | this.materials_.checkerboard = this.LoadMaterial_( 43 | 'whitesquare.png', null, null, null); 44 | 45 | const ground = new THREE.Mesh( 46 | new THREE.BoxGeometry(1, 1, 1, 10, 10, 10), 47 | this.materials_.checkerboard); 48 | ground.castShadow = true; 49 | ground.receiveShadow = true; 50 | 51 | this.FindEntity('loader').GetComponent('LoadController').AddModel(ground, 'built-in.', 'ground'); 52 | 53 | const box = new THREE.Mesh( 54 | new THREE.BoxGeometry(1, 1, 1, 10, 10, 10), 55 | this.materials_.checkerboard); 56 | box.castShadow = true; 57 | box.receiveShadow = true; 58 | 59 | this.FindEntity('loader').GetComponent('LoadController').AddModel(box, 'built-in.', 'box'); 60 | 61 | const sphere = new THREE.Mesh( 62 | new THREE.SphereGeometry(1, 16, 16), 63 | this.materials_.checkerboard); 64 | sphere.castShadow = true; 65 | sphere.receiveShadow = true; 66 | 67 | this.FindEntity('loader').GetComponent('LoadController').AddModel(sphere, 'built-in.', 'sphere'); 68 | 69 | this.currentTime_ = 0.0; 70 | } 71 | 72 | Update(timeElapsed) { 73 | this.currentTime_ += timeElapsed; 74 | 75 | if (this.materials_.checkerboard && this.materials_.checkerboard.userData.shader) { 76 | this.materials_.checkerboard.userData.shader.uniforms.iTime.value = this.currentTime_; 77 | this.materials_.checkerboard.needsUpdate = true; 78 | } 79 | 80 | if (this.spawned_) { 81 | return; 82 | } 83 | 84 | this.spawned_ = true; 85 | 86 | this.BuildHackModel_(); 87 | 88 | const terrain = new entity.Entity('terrain'); 89 | terrain.AddComponent(new terrain_component.TerrainComponent({})); 90 | terrain.SetActive(true); 91 | terrain.Init(); 92 | } 93 | }; 94 | 95 | return { 96 | DemoBuilder: DemoBuilder 97 | }; 98 | 99 | })(); -------------------------------------------------------------------------------- /src/game/player-entity.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.124/build/three.module.js'; 2 | 3 | import * as entity from '../base/entity.js'; 4 | import {player_input} from './player-input.js'; 5 | 6 | 7 | export const player_entity = (() => { 8 | 9 | class BasicCharacterController extends entity.Component { 10 | static CLASS_NAME = 'BasicCharacterController'; 11 | 12 | get NAME() { 13 | return BasicCharacterController.CLASS_NAME; 14 | } 15 | 16 | constructor(params) { 17 | super(); 18 | this.params_ = params; 19 | } 20 | 21 | InitEntity() { 22 | this.Init_(); 23 | } 24 | 25 | Init_() { 26 | this.decceleration_ = new THREE.Vector3(-0.0005, -0.0001, -5.0); 27 | this.acceleration_ = new THREE.Vector3(1, 0.125, 20.0); 28 | this.velocity_ = new THREE.Vector3(0, 0, 0); 29 | this.group_ = new THREE.Group(); 30 | 31 | // let light = new THREE.DirectionalLight(0xFFFFFF, 0.5); 32 | // light.position.set(-20, 20, 20); 33 | // light.target.position.set(0, 0, 0); 34 | // light.intensity = 10; 35 | // this.group_.add(light); 36 | 37 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 38 | threejs.AddSceneObject(this.group_, {type: 'player'}); 39 | 40 | this.rotation_ = new THREE.Quaternion(); 41 | this.translation_ = new THREE.Vector3(0, 3, 0); 42 | 43 | this.animations_ = {}; 44 | 45 | this.LoadModels_(); 46 | } 47 | 48 | InitComponent() { 49 | this.RegisterHandler_('health.death', (m) => { this.OnDeath_(m); }); 50 | this.RegisterHandler_( 51 | 'update.position', (m) => { this.OnUpdatePosition_(m); }); 52 | this.RegisterHandler_( 53 | 'update.rotation', (m) => { this.OnUpdateRotation_(m); }); 54 | } 55 | 56 | OnUpdatePosition_(msg) { 57 | this.group_.position.copy(msg.value); 58 | } 59 | 60 | OnUpdateRotation_(msg) { 61 | this.group_.quaternion.copy(msg.value); 62 | } 63 | 64 | OnDeath_(msg) { 65 | this.stateMachine_.SetState('death'); 66 | } 67 | 68 | LoadModels_() { 69 | } 70 | 71 | Update(timeInSeconds) { 72 | const input = this.GetComponent('PlayerInput'); 73 | 74 | 75 | const controlObject = this.group_; 76 | const _Q = new THREE.Quaternion(); 77 | const _A = new THREE.Vector3(); 78 | const _R = controlObject.quaternion.clone(); 79 | 80 | if (input.key(player_input.KEYS.a)) { 81 | _A.set(0, 1, 0); 82 | _Q.setFromAxisAngle(_A, 2.0 * Math.PI * timeInSeconds * this.acceleration_.y); 83 | _R.multiply(_Q); 84 | } 85 | if (input.key(player_input.KEYS.d)) { 86 | _A.set(0, 1, 0); 87 | _Q.setFromAxisAngle(_A, 2.0 * -Math.PI * timeInSeconds * this.acceleration_.y); 88 | _R.multiply(_Q); 89 | } 90 | 91 | this.Parent.SetQuaternion(_R); 92 | } 93 | }; 94 | 95 | return { 96 | BasicCharacterController: BasicCharacterController, 97 | }; 98 | 99 | })(); -------------------------------------------------------------------------------- /src/game/player-input.js: -------------------------------------------------------------------------------- 1 | import * as entity from "../base/entity.js"; 2 | import * as passes from '../base/passes.js'; 3 | 4 | 5 | export const player_input = (() => { 6 | 7 | const KEYS = { 8 | 'a': 65, 9 | 's': 83, 10 | 'w': 87, 11 | 'd': 68, 12 | 'SPACE': 32, 13 | 'SHIFT_L': 16, 14 | 'CTRL_L': 17, 15 | 'BACKSPACE': 8, 16 | }; 17 | 18 | class PlayerInput extends entity.Component { 19 | static CLASS_NAME = 'PlayerInput'; 20 | 21 | get NAME() { 22 | return PlayerInput.CLASS_NAME; 23 | } 24 | 25 | constructor(params) { 26 | super(); 27 | this.params_ = params; 28 | } 29 | 30 | InitEntity() { 31 | this.current_ = { 32 | leftButton: false, 33 | rightButton: false, 34 | mouseXDelta: 0, 35 | mouseYDelta: 0, 36 | mouseX: 0, 37 | mouseY: 0, 38 | }; 39 | this.previous_ = null; 40 | this.keys_ = {}; 41 | this.previousKeys_ = {}; 42 | this.target_ = document; 43 | this.target_.addEventListener('mousedown', (e) => this.onMouseDown_(e), false); 44 | this.target_.addEventListener('mousemove', (e) => this.onMouseMove_(e), false); 45 | this.target_.addEventListener('mouseup', (e) => this.onMouseUp_(e), false); 46 | this.target_.addEventListener('keydown', (e) => this.onKeyDown_(e), false); 47 | this.target_.addEventListener('keyup', (e) => this.onKeyUp_(e), false); 48 | 49 | this.Parent.Attributes.Input = { 50 | Keyboard: { 51 | Current: this.keys_, 52 | Previous: this.previousKeys_ 53 | }, 54 | Mouse: { 55 | Current: this.current_, 56 | Previous: this.previous_ 57 | }, 58 | }; 59 | 60 | this.SetPass(passes.Passes.INPUT); 61 | } 62 | 63 | onMouseMove_(e) { 64 | this.current_.mouseX = e.pageX - window.innerWidth / 2; 65 | this.current_.mouseY = e.pageY - window.innerHeight / 2; 66 | 67 | if (this.previous_ === null) { 68 | this.previous_ = {...this.current_}; 69 | } 70 | 71 | this.current_.mouseXDelta = this.current_.mouseX - this.previous_.mouseX; 72 | this.current_.mouseYDelta = this.current_.mouseY - this.previous_.mouseY; 73 | } 74 | 75 | onMouseDown_(e) { 76 | this.onMouseMove_(e); 77 | 78 | switch (e.button) { 79 | case 0: { 80 | this.current_.leftButton = true; 81 | break; 82 | } 83 | case 2: { 84 | this.current_.rightButton = true; 85 | break; 86 | } 87 | } 88 | } 89 | 90 | onMouseUp_(e) { 91 | this.onMouseMove_(e); 92 | 93 | switch (e.button) { 94 | case 0: { 95 | this.current_.leftButton = false; 96 | break; 97 | } 98 | case 2: { 99 | this.current_.rightButton = false; 100 | break; 101 | } 102 | } 103 | } 104 | 105 | onKeyDown_(e) { 106 | this.keys_[e.keyCode] = true; 107 | } 108 | 109 | onKeyUp_(e) { 110 | this.keys_[e.keyCode] = false; 111 | } 112 | 113 | key(keyCode) { 114 | return !!this.keys_[keyCode]; 115 | } 116 | 117 | mouseLeftReleased(checkPrevious=true) { 118 | return (!this.current_.leftButton && this.previous_.leftButton); 119 | } 120 | 121 | isReady() { 122 | return this.previous_ !== null; 123 | } 124 | 125 | Update(_) { 126 | if (this.previous_ !== null) { 127 | this.current_.mouseXDelta = this.current_.mouseX - this.previous_.mouseX; 128 | this.current_.mouseYDelta = this.current_.mouseY - this.previous_.mouseY; 129 | 130 | this.previous_ = {...this.current_}; 131 | this.previousKeys_ = {...this.keys_}; 132 | } 133 | } 134 | }; 135 | 136 | return { 137 | PlayerInput: PlayerInput, 138 | KEYS: KEYS, 139 | }; 140 | 141 | })(); -------------------------------------------------------------------------------- /src/game/render/render-sky-component.js: -------------------------------------------------------------------------------- 1 | import {THREE} from '../../base/three-defs.js'; 2 | 3 | import * as entity from "../../base/entity.js"; 4 | import * as shaders from "./shaders.js"; 5 | 6 | 7 | export class RenderSkyComponent extends entity.Component { 8 | static CLASS_NAME = 'RenderSkyComponent'; 9 | 10 | get NAME() { 11 | return RenderSkyComponent.CLASS_NAME; 12 | } 13 | 14 | constructor() { 15 | super(); 16 | } 17 | 18 | InitEntity() { 19 | const uniforms = { 20 | "time": { value: 0.0 }, 21 | }; 22 | 23 | const skyGeo = new THREE.SphereGeometry(5000, 32, 15); 24 | const skyMat = new shaders.ShaderMaterial('SKY', { 25 | uniforms: uniforms, 26 | side: THREE.BackSide 27 | }); 28 | 29 | this.sky_ = new THREE.Mesh(skyGeo, skyMat); 30 | this.sky_.castShadow = false; 31 | this.sky_.receiveShadow = false; 32 | 33 | const threejs = this.FindEntity('threejs').GetComponent('ThreeJSController'); 34 | threejs.AddSceneObject(this.sky_); 35 | } 36 | 37 | Update(timeElapsed) { 38 | const player = this.FindEntity('player'); 39 | if (!player) { 40 | return; 41 | } 42 | const pos = player.Position; 43 | 44 | if (this.sky_) { 45 | this.sky_.material.uniforms.time.value += timeElapsed; 46 | } 47 | 48 | this.sky_.position.copy(pos); 49 | } 50 | } -------------------------------------------------------------------------------- /src/game/render/shaders.js: -------------------------------------------------------------------------------- 1 | import { THREE, CSMShader } from '../../base/three-defs.js'; 2 | 3 | 4 | class ShaderManager { 5 | static shaderCode = {}; 6 | static threejs = null; 7 | }; 8 | 9 | export function SetThreeJS(threejs) { 10 | ShaderManager.threejs = threejs; 11 | } 12 | 13 | export async function loadShaders() { 14 | const loadText = async (url) => { 15 | const d = await fetch(url); 16 | return await d.text(); 17 | }; 18 | 19 | const globalShaders = [ 20 | 'header.glsl', 21 | 'common.glsl', 22 | 'oklab.glsl', 23 | 'noise.glsl', 24 | 'sky.glsl', 25 | ]; 26 | 27 | const globalShadersCode = []; 28 | for (let i = 0; i < globalShaders.length; ++i) { 29 | globalShadersCode.push(await loadText('shaders/' + globalShaders[i])); 30 | } 31 | 32 | const loadShader = async (url) => { 33 | const d = await fetch(url); 34 | let shaderCode = ''; 35 | for (let i = 0; i < globalShadersCode.length; ++i) { 36 | shaderCode += globalShadersCode[i] + '\n'; 37 | } 38 | return shaderCode + '\n' + await d.text(); 39 | } 40 | 41 | ShaderManager.shaderCode['PHONG'] = { 42 | vsh: await loadShader('shaders/phong-lighting-model-vsh.glsl'), 43 | fsh: await loadShader('shaders/phong-lighting-model-fsh.glsl'), 44 | }; 45 | 46 | ShaderManager.shaderCode['GRASS'] = { 47 | vsh: await loadShader('shaders/grass-lighting-model-vsh.glsl'), 48 | fsh: await loadShader('shaders/grass-lighting-model-fsh.glsl'), 49 | }; 50 | 51 | ShaderManager.shaderCode['TERRAIN'] = { 52 | vsh: await loadShader('shaders/terrain-lighting-model-vsh.glsl'), 53 | fsh: await loadShader('shaders/terrain-lighting-model-fsh.glsl'), 54 | }; 55 | 56 | ShaderManager.shaderCode['BUGS'] = { 57 | vsh: await loadShader('shaders/bugs-lighting-model-vsh.glsl'), 58 | fsh: await loadShader('shaders/bugs-lighting-model-fsh.glsl'), 59 | }; 60 | 61 | ShaderManager.shaderCode['WIND'] = { 62 | vsh: await loadShader('shaders/wind-lighting-model-vsh.glsl'), 63 | fsh: await loadShader('shaders/wind-lighting-model-fsh.glsl'), 64 | }; 65 | 66 | ShaderManager.shaderCode['SKY'] = { 67 | vsh: await loadShader('shaders/sky-lighting-model-vsh.glsl'), 68 | fsh: await loadShader('shaders/sky-lighting-model-fsh.glsl'), 69 | }; 70 | 71 | ShaderManager.shaderCode['WATER'] = { 72 | vsh: await loadShader('shaders/water-lighting-model-vsh.glsl'), 73 | fsh: await loadShader('shaders/water-lighting-model-fsh.glsl'), 74 | }; 75 | 76 | ShaderManager.shaderCode['WATER-TEXTURE'] = { 77 | vsh: await loadShader('shaders/water-texture-vsh.glsl'), 78 | fsh: await loadShader('shaders/water-texture-fsh.glsl'), 79 | }; 80 | } 81 | 82 | 83 | export class ShaderMaterial extends THREE.ShaderMaterial { 84 | constructor(shaderType, parameters) { 85 | parameters.vertexShader = ShaderManager.shaderCode[shaderType].vsh; 86 | parameters.fragmentShader = ShaderManager.shaderCode[shaderType].fsh; 87 | 88 | super(parameters); 89 | } 90 | }; 91 | 92 | export class GamePBRMaterial extends THREE.MeshStandardMaterial { 93 | 94 | #uniforms_ = {}; 95 | #shader_ = null; 96 | 97 | constructor(shaderType, parameters) { 98 | super(parameters); 99 | 100 | this.#shader_ = null; 101 | this.#uniforms_ = {}; 102 | 103 | ShaderManager.threejs.SetupMaterial(this); 104 | 105 | const previousCallback = this.onBeforeCompile; 106 | 107 | this.onBeforeCompile = (shader) => { 108 | shader.fragmentShader = ShaderManager.shaderCode[shaderType].fsh; 109 | shader.vertexShader = ShaderManager.shaderCode[shaderType].vsh; 110 | shader.uniforms.time = { value: 0.0 }; 111 | shader.uniforms.playerPos = { value: new THREE.Vector3(0.0) }; 112 | 113 | for (let k in this.#uniforms_) { 114 | shader.uniforms[k] = this.#uniforms_[k]; 115 | } 116 | 117 | this.#shader_ = shader; 118 | 119 | previousCallback(shader); 120 | }; 121 | 122 | this.onBeforeRender = () => { 123 | if (shaderType == 'BUGS') { 124 | let a = 0; 125 | } 126 | let a = 0; 127 | } 128 | 129 | this.customProgramCacheKey = () => { 130 | let uniformStr = ''; 131 | for (let k in this.#uniforms_) { 132 | uniformStr += k + ':' + this.#uniforms_[k].value + ';'; 133 | } 134 | return shaderType + uniformStr; 135 | } 136 | } 137 | 138 | setFloat(name, value) { 139 | this.#uniforms_[name] = { value: value }; 140 | if (this.#shader_) { 141 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 142 | } 143 | } 144 | 145 | setVec2(name, value) { 146 | this.#uniforms_[name] = { value: value }; 147 | if (this.#shader_) { 148 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 149 | } 150 | } 151 | 152 | setVec3(name, value) { 153 | this.#uniforms_[name] = { value: value }; 154 | if (this.#shader_) { 155 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 156 | } 157 | } 158 | 159 | setVec4(name, value) { 160 | this.#uniforms_[name] = { value: value }; 161 | if (this.#shader_) { 162 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 163 | } 164 | } 165 | 166 | setMatrix(name, value) { 167 | this.#uniforms_[name] = { value: value }; 168 | if (this.#shader_) { 169 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 170 | } 171 | } 172 | 173 | setTexture(name, value) { 174 | this.#uniforms_[name] = { value: value }; 175 | if (this.#shader_) { 176 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 177 | } 178 | } 179 | 180 | setTextureArray(name, value) { 181 | this.#uniforms_[name] = { value: value }; 182 | if (this.#shader_) { 183 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 184 | } 185 | } 186 | } 187 | 188 | export class GameMaterial extends THREE.MeshPhongMaterial { 189 | 190 | #uniforms_ = {}; 191 | #shader_ = null; 192 | 193 | constructor(shaderType, parameters) { 194 | super(parameters); 195 | 196 | this.#shader_ = null; 197 | this.#uniforms_ = {}; 198 | 199 | ShaderManager.threejs.SetupMaterial(this); 200 | 201 | const previousCallback = this.onBeforeCompile; 202 | 203 | this.onBeforeCompile = (shader) => { 204 | shader.fragmentShader = ShaderManager.shaderCode[shaderType].fsh; 205 | shader.vertexShader = ShaderManager.shaderCode[shaderType].vsh; 206 | shader.uniforms.time = { value: 0.0 }; 207 | shader.uniforms.playerPos = { value: new THREE.Vector3(0.0) }; 208 | 209 | for (let k in this.#uniforms_) { 210 | shader.uniforms[k] = this.#uniforms_[k]; 211 | } 212 | 213 | this.#shader_ = shader; 214 | 215 | previousCallback(shader); 216 | }; 217 | 218 | this.onBeforeRender = () => { 219 | if (shaderType == 'BUGS') { 220 | let a = 0; 221 | } 222 | let a = 0; 223 | } 224 | 225 | this.customProgramCacheKey = () => { 226 | let uniformStr = ''; 227 | for (let k in this.#uniforms_) { 228 | uniformStr += k + ':' + this.#uniforms_[k].value + ';'; 229 | } 230 | return shaderType + uniformStr; 231 | } 232 | } 233 | 234 | setFloat(name, value) { 235 | this.#uniforms_[name] = { value: value }; 236 | if (this.#shader_) { 237 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 238 | } 239 | } 240 | 241 | setVec2(name, value) { 242 | this.#uniforms_[name] = { value: value }; 243 | if (this.#shader_) { 244 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 245 | } 246 | } 247 | 248 | setVec3(name, value) { 249 | this.#uniforms_[name] = { value: value }; 250 | if (this.#shader_) { 251 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 252 | } 253 | } 254 | 255 | setVec4(name, value) { 256 | this.#uniforms_[name] = { value: value }; 257 | if (this.#shader_) { 258 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 259 | } 260 | } 261 | 262 | setMatrix(name, value) { 263 | this.#uniforms_[name] = { value: value }; 264 | if (this.#shader_) { 265 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 266 | } 267 | } 268 | 269 | setTexture(name, value) { 270 | this.#uniforms_[name] = { value: value }; 271 | if (this.#shader_) { 272 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 273 | } 274 | } 275 | 276 | setTextureArray(name, value) { 277 | this.#uniforms_[name] = { value: value }; 278 | if (this.#shader_) { 279 | this.#shader_.uniforms[name] = this.#uniforms_[name]; 280 | } 281 | } 282 | } 283 | 284 | 285 | -------------------------------------------------------------------------------- /src/game/spawners.js: -------------------------------------------------------------------------------- 1 | import {THREE} from '../base/three-defs.js'; 2 | 3 | import * as entity from '../base/entity.js'; 4 | 5 | import {player_input} from './player-input.js'; 6 | import {player_entity} from './player-entity.js' 7 | import {third_person_camera} from './third-person-camera.js'; 8 | import {demo_builder} from '../demo-builder.js'; 9 | 10 | 11 | export const spawners = (() => { 12 | 13 | class PlayerSpawner extends entity.Component { 14 | static CLASS_NAME = 'PlayerSpawner'; 15 | 16 | get NAME() { 17 | return PlayerSpawner.CLASS_NAME; 18 | } 19 | 20 | constructor(params) { 21 | super(); 22 | this.params_ = params; 23 | } 24 | 25 | Spawn() { 26 | const player = new entity.Entity('player'); 27 | player.SetPosition(new THREE.Vector3(316, 15, -560)); 28 | player.AddComponent(new player_input.PlayerInput(this.params_)); 29 | player.AddComponent(new player_entity.BasicCharacterController(this.params_)); 30 | player.AddComponent(new third_person_camera.ThirdPersonCamera(this.params_)); 31 | player.Init(); 32 | player.SetQuaternion(new THREE.Quaternion(0, 0.448, 0, -0.892)); 33 | 34 | return player; 35 | } 36 | }; 37 | 38 | class DemoSpawner extends entity.Component { 39 | static CLASS_NAME = 'DemoSpawner'; 40 | 41 | get NAME() { 42 | return DemoSpawner.CLASS_NAME; 43 | } 44 | 45 | constructor(params) { 46 | super(); 47 | this.params_ = params; 48 | } 49 | 50 | Spawn() { 51 | const e = new entity.Entity(); 52 | e.SetPosition(new THREE.Vector3(0, 0, 0)); 53 | e.AddComponent(new demo_builder.DemoBuilder(this.params_)); 54 | e.Init(); 55 | 56 | return e; 57 | } 58 | }; 59 | 60 | 61 | return { 62 | PlayerSpawner: PlayerSpawner, 63 | DemoSpawner: DemoSpawner, 64 | }; 65 | })(); -------------------------------------------------------------------------------- /src/game/third-person-camera.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.124/build/three.module.js'; 2 | import * as entity from '../base/entity.js'; 3 | import * as passes from '../base/passes.js'; 4 | import * as terrain_component from '../base/render/terrain-component.js'; 5 | 6 | export const third_person_camera = (() => { 7 | 8 | class ThirdPersonCamera extends entity.Component { 9 | static CLASS_NAME = 'ThirdPersonCamera'; 10 | 11 | get NAME() { 12 | return ThirdPersonCamera.CLASS_NAME; 13 | } 14 | 15 | constructor(params) { 16 | super(); 17 | 18 | this.params_ = params; 19 | this.camera_ = params.camera; 20 | 21 | this.currentPosition_ = new THREE.Vector3(); 22 | this.currentLookat_ = new THREE.Vector3(); 23 | } 24 | 25 | InitEntity() { 26 | this.SetPass(passes.Passes.CAMERA); 27 | } 28 | 29 | CalculateIdealOffset_() { 30 | const idealOffset = new THREE.Vector3(0, 0.5, -8); 31 | // idealOffset.multiplyScalar(10.0); 32 | idealOffset.applyQuaternion(this.Parent.Quaternion); 33 | idealOffset.add(this.Parent.Position); 34 | 35 | // idealOffset.y = Math.min(idealOffset.y, height + 1.5); 36 | // idealOffset.y += (this.Parent.Position.y - 1.5 + height); 37 | 38 | return idealOffset; 39 | } 40 | 41 | CalculateIdealLookat_() { 42 | const idealLookat = new THREE.Vector3(0, 1.25, 4); 43 | idealLookat.applyQuaternion(this.Parent.Quaternion); 44 | idealLookat.add(this.Parent.Position); 45 | return idealLookat; 46 | } 47 | 48 | Update(timeElapsed) { 49 | const terrain = this.FindEntity('terrain'); 50 | if (terrain) { 51 | const terrainComponent = terrain.GetComponent(terrain_component.TerrainComponent.CLASS_NAME); 52 | if (!terrainComponent.IsReady()) { 53 | return; 54 | } 55 | 56 | const idealOffset = this.CalculateIdealOffset_(); 57 | const idealLookat = this.CalculateIdealLookat_(); 58 | 59 | const height = terrainComponent.GetHeight(idealOffset.x, idealOffset.z); 60 | idealOffset.y = height + 4.25; 61 | 62 | // const t = 0.05; 63 | // const t = 4.0 * timeElapsed; 64 | const t = 1.0 - Math.pow(0.0001, timeElapsed); 65 | 66 | this.currentPosition_.lerp(idealOffset, t); 67 | this.currentLookat_.lerp(idealLookat, t); 68 | 69 | this.camera_.position.copy(this.currentPosition_); 70 | this.camera_.lookAt(this.currentLookat_); 71 | } 72 | } 73 | } 74 | 75 | return { 76 | ThirdPersonCamera: ThirdPersonCamera 77 | }; 78 | 79 | })(); -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import * as entity_manager from './base/entity-manager.js'; 2 | import * as entity from './base/entity.js'; 3 | 4 | import {load_controller} from './base/load-controller.js'; 5 | import {spawners} from './game/spawners.js'; 6 | 7 | import {threejs_component} from './base/threejs-component.js'; 8 | 9 | import * as render_sky_component from './game/render/render-sky-component.js'; 10 | import * as shaders from './game/render/shaders.js'; 11 | 12 | 13 | class QuickFPS1 { 14 | constructor() { 15 | } 16 | 17 | async Init() { 18 | await shaders.loadShaders(); 19 | 20 | this.Initialize_(); 21 | } 22 | 23 | Initialize_() { 24 | this.entityManager_ = entity_manager.EntityManager.Init(); 25 | 26 | this.OnGameStarted_(); 27 | } 28 | 29 | OnGameStarted_() { 30 | this.LoadControllers_(); 31 | 32 | this.previousRAF_ = null; 33 | this.RAF_(); 34 | } 35 | 36 | LoadControllers_() { 37 | const threejs = new entity.Entity('threejs'); 38 | threejs.AddComponent(new threejs_component.ThreeJSController()); 39 | threejs.Init(); 40 | 41 | const sky = new entity.Entity(); 42 | sky.AddComponent(new render_sky_component.RenderSkyComponent()); 43 | sky.Init(threejs); 44 | 45 | // Hack 46 | this.camera_ = threejs.GetComponent('ThreeJSController').Camera; 47 | this.threejs_ = threejs.GetComponent('ThreeJSController'); 48 | 49 | const loader = new entity.Entity('loader'); 50 | loader.AddComponent(new load_controller.LoadController()); 51 | loader.Init(); 52 | 53 | const basicParams = { 54 | camera: this.camera_, 55 | }; 56 | 57 | const spawner = new entity.Entity('spawners'); 58 | spawner.AddComponent(new spawners.PlayerSpawner(basicParams)); 59 | spawner.AddComponent(new spawners.DemoSpawner(basicParams)); 60 | spawner.Init(); 61 | 62 | spawner.GetComponent('PlayerSpawner').Spawn(); 63 | spawner.GetComponent('DemoSpawner').Spawn(); 64 | } 65 | 66 | RAF_() { 67 | requestAnimationFrame((t) => { 68 | if (this.previousRAF_ === null) { 69 | this.previousRAF_ = t; 70 | } else { 71 | this.Step_(t - this.previousRAF_); 72 | this.previousRAF_ = t; 73 | } 74 | 75 | setTimeout(() => { 76 | this.RAF_(); 77 | }, 1); 78 | }); 79 | } 80 | 81 | Step_(timeElapsed) { 82 | const timeElapsedS = Math.min(1.0 / 30.0, timeElapsed * 0.001); 83 | 84 | this.entityManager_.Update(timeElapsedS); 85 | 86 | this.threejs_.Render(timeElapsedS); 87 | } 88 | } 89 | 90 | 91 | let _APP = null; 92 | 93 | window.addEventListener('DOMContentLoaded', async () => { 94 | _APP = new QuickFPS1(); 95 | await _APP.Init(); 96 | }); 97 | let a = 0; -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import wasm from 'vite-plugin-wasm'; 3 | import topLevelAwait from 'vite-plugin-top-level-await'; 4 | import solidPlugin from 'vite-plugin-solid'; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [wasm(), topLevelAwait(), solidPlugin()], 9 | resolve: { 10 | alias: { 11 | }, 12 | }, 13 | build: { 14 | sourcemap: true, 15 | }, 16 | server: { 17 | port: 5200, 18 | hmr: { 19 | clientPort: 5200, 20 | } 21 | }, 22 | base: "/Quick_Grass/" 23 | }); 24 | --------------------------------------------------------------------------------