1 /** 2 * Copyright: Copyright (C) 2018 Gabriel Gheorghe, All Rights Reserved 3 * Authors: $(Gabriel Gheorghe) 4 * License: $(LINK2 https://www.gnu.org/licenses/gpl-3.0.txt, GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007) 5 * Source: $(LINK2 https://github.com/GabyForceQ/LibertyEngine/blob/master/source/liberty/framework/terrain/impl.d) 6 * Documentation: 7 * Coverage: 8 * TODO: 9 * - use texture_io to load png within a texture 10 * - setTexCoordMultiplier += -= 11 **/ 12 module liberty.framework.terrain.impl; 13 14 import liberty.framework.terrain.vertex; 15 import liberty.graphics.shader; 16 import liberty.image.format.bmp; 17 import liberty.image.io; 18 import liberty.material.impl; 19 import liberty.math.functions; 20 import liberty.math.transform; 21 import liberty.math.vector; 22 import liberty.model.impl; 23 import liberty.model.io; 24 import liberty.scene.entity; 25 import liberty.scene.meta; 26 import liberty.scene.services; 27 28 /** 29 * 30 **/ 31 final class Terrain : Entity { 32 mixin NodeBody; 33 34 private { 35 const float maxPixelColor = 256 ^^ 3; 36 37 // getSize 38 float size; 39 // getMaxHeight 40 float maxHeight = 0; 41 float** heights; 42 int vertexCount; 43 44 Material[] materials; 45 46 // getTexCoordMultiplier, setTexCoordMultiplier 47 Vector2F texCoordMultiplier = Vector2F.one; 48 49 Shader shader; 50 } 51 52 /** 53 * 54 **/ 55 this(string id) { 56 super(id); 57 register; 58 59 shader = Shader 60 .getOrCreate("Terrain", (shader) { 61 shader 62 .addGlobalRenderMethod((program) { 63 program.loadUniform("uSkyColor", scene.world.getExpHeightFogColor); 64 }) 65 .addPerEntityRenderMethod((program) { 66 program.loadUniform("uTexCoordMultiplier", getTexCoordMultiplier); 67 }); 68 }); 69 70 shader.registerEntity(this); 71 scene.shaderMap[shader.id] = shader; 72 } 73 74 ~this() { 75 import core.stdc.stdlib : free; 76 77 foreach (i; 0..vertexCount) 78 free(heights[i]); 79 80 free(heights); 81 } 82 83 /** 84 * 85 * Returns reference to this so it can be used in a stream. 86 **/ 87 typeof(this) build(float size, float maxHeight, Material[] materials) { 88 this.size = size; 89 this.maxHeight = maxHeight; 90 this.materials = materials; 91 92 generateTerrain("res/textures/heightMap.bmp"); 93 94 component!Transform 95 .setLocation(-size / 2.0f, 0.0f, -size / 2.0f); 96 97 texCoordMultiplier = size; 98 99 return this; 100 } 101 102 /** 103 * 104 **/ 105 Vector2F getTexCoordMultiplier() const { 106 return texCoordMultiplier; 107 } 108 109 /** 110 * 111 * Returns reference to this so it can be used in a stream. 112 **/ 113 typeof(this) setTexCoordMultiplier(Vector2F multiplier) { 114 texCoordMultiplier = multiplier; 115 return this; 116 } 117 118 /** 119 * 120 * Returns reference to this so it can be used in a stream. 121 **/ 122 typeof(this) setTexCoordMultiplier(float x, float y) { 123 texCoordMultiplier = Vector2F(x, y); 124 return this; 125 } 126 127 /** 128 * 129 * Returns reference to this so it can be used in a stream. 130 **/ 131 typeof(this) increaseTexCoordMultiplier(Vector2F multiplier) { 132 texCoordMultiplier += multiplier; 133 return this; 134 } 135 136 /** 137 * 138 * Returns reference to this so it can be used in a stream. 139 **/ 140 typeof(this) increaseTexCoordMultiplier(float x, float y) { 141 texCoordMultiplier += Vector2F(x, y); 142 return this; 143 } 144 145 /** 146 * 147 * Returns reference to this so it can be used in a stream. 148 **/ 149 typeof(this) decreaseTexCoordMultiplier(Vector2F multiplier) { 150 texCoordMultiplier -= multiplier; 151 return this; 152 } 153 154 /** 155 * 156 * Returns reference to this so it can be used in a stream. 157 **/ 158 typeof(this) decreaseTexCoordMultiplier(float x, float y) { 159 texCoordMultiplier -= Vector2F(x, y); 160 return this; 161 } 162 163 /** 164 * 165 **/ 166 float getHeight(float worldX, float worldZ) { 167 const float terrainX = worldX - component!Transform.getLocation.x; 168 const float terrainZ = worldZ - component!Transform.getLocation.z; 169 170 const int heightLen = vertexCount - 1; 171 const float gridSqareSize = size / cast(float)heightLen; 172 173 int gridX = cast(int)floor(terrainX / gridSqareSize); 174 int gridZ = cast(int)floor(terrainZ / gridSqareSize); 175 176 if (gridX >= heightLen || gridZ >= heightLen || gridX < 0 || gridZ < 0) 177 return int.min; 178 179 float xCoord = (terrainX % gridSqareSize) / gridSqareSize; 180 float zCoord = (terrainZ % gridSqareSize) / gridSqareSize; 181 182 float finalPos; 183 if (xCoord <= (1 - zCoord)) { 184 finalPos = barryCentric( 185 Vector3F(0, heights[gridX][gridZ], 0), 186 Vector3F(1, heights[gridX + 1][gridZ], 0), 187 Vector3F(0, heights[gridX][gridZ + 1], 1), 188 Vector2F(xCoord, zCoord) 189 ); 190 } else { 191 finalPos = barryCentric( 192 Vector3F(1, heights[gridX + 1][gridZ], 0), 193 Vector3F(1, heights[gridX + 1][gridZ + 1], 1), 194 Vector3F(0, heights[gridX][gridZ + 1], 1), 195 Vector2F(xCoord, zCoord) 196 ); 197 } 198 199 return finalPos; 200 } 201 202 private void generateTerrain(string heightMapPath) { 203 import core.stdc.stdlib : malloc; 204 205 // Load height map form file 206 auto image = cast(BMPImage)ImageIO.loadImage(heightMapPath); 207 208 vertexCount = image.getHeight(); 209 const int count = vertexCount * vertexCount; 210 211 heights = cast(float**)malloc(vertexCount * (float*).sizeof); 212 foreach (i; 0..vertexCount) 213 heights[i] = cast(float*)malloc(vertexCount * float.sizeof); 214 215 TerrainVertex[] vertices = new TerrainVertex[count * 3]; 216 uint[] indices = new uint[6 * (vertexCount - 1) * (vertexCount - 1)]; 217 218 int vertexPtr; 219 for (int i; i < vertexCount; i++) { 220 for (int j; j < vertexCount; j++) { 221 const float height = getHeight(j, i, image); 222 heights[j][i] = height; 223 224 vertices[vertexPtr].position.x = cast(float)j / (cast(float)vertexCount - 1) * size; 225 vertices[vertexPtr].position.y = height; 226 vertices[vertexPtr].position.z = cast(float)i / (cast(float)vertexCount - 1) * size; 227 228 Vector3F normal = computeNormal(j, i, image); 229 230 vertices[vertexPtr].normal.x = normal.x; 231 vertices[vertexPtr].normal.y = normal.y; 232 vertices[vertexPtr].normal.z = normal.z; 233 vertices[vertexPtr].texCoord.x = cast(float)j / (cast(float)vertexCount - 1); 234 vertices[vertexPtr].texCoord.y = cast(float)i / (cast(float)vertexCount - 1); 235 vertexPtr++; 236 } 237 } 238 int indexPtr; 239 for (int gz; gz < vertexCount - 1; gz++) { 240 for (int gx; gx < vertexCount - 1; gx++) { 241 const int topLeft = (gz * vertexCount) + gx; 242 const int topRight = topLeft + 1; 243 const int bottomLeft = ((gz + 1) * vertexCount) + gx; 244 const int bottomRight = bottomLeft + 1; 245 indices[indexPtr++] = topLeft; 246 indices[indexPtr++] = bottomLeft; 247 indices[indexPtr++] = topRight; 248 indices[indexPtr++] = topRight; 249 indices[indexPtr++] = bottomLeft; 250 indices[indexPtr++] = bottomRight; 251 } 252 } 253 254 model = new Model(ModelIO.loadRawModel(vertices, indices), materials); 255 } 256 257 private float getHeight(int x, int y, BMPImage image) { 258 if (x < 0 || x >= image.getHeight() || y < 0 || y >= image.getHeight()) 259 return 0; 260 261 float height = image.getRGBPixelColor(x, y); 262 height += maxPixelColor / 2.0f; 263 height /= maxPixelColor / 2.0f; 264 height *= maxHeight; 265 266 return height; 267 } 268 269 private Vector3F computeNormal(int x, int y, BMPImage image) { 270 const float heightL = getHeight(x - 1, y, image); 271 const float heightR = getHeight(x + 1, y, image); 272 const float heightD = getHeight(x, y - 1, image); 273 const float heightU = getHeight(x, y + 1, image); 274 275 Vector3F normal = Vector3F(heightL - heightR, 2.0f, heightD - heightU); 276 normal.normalize(); 277 278 return normal; 279 } 280 281 /** 282 * 283 **/ 284 float getSize() const { 285 return size; 286 } 287 288 /** 289 * 290 **/ 291 float getMaxHeight() const { 292 return maxHeight; 293 } 294 }