diff --git a/assets/textures/array.png b/assets/textures/array.png deleted file mode 100644 index f511a64..0000000 Binary files a/assets/textures/array.png and /dev/null differ diff --git a/assets/textures/atlas.png b/assets/textures/atlas.png deleted file mode 100644 index 077e723..0000000 Binary files a/assets/textures/atlas.png and /dev/null differ diff --git a/assets/textures/blocks.png b/assets/textures/blocks.png deleted file mode 100644 index 2c8ae80..0000000 Binary files a/assets/textures/blocks.png and /dev/null differ diff --git a/assets/textures/cobblestone.png b/assets/textures/cobblestone.png new file mode 100644 index 0000000..da3498c Binary files /dev/null and b/assets/textures/cobblestone.png differ diff --git a/assets/textures/dirt.png b/assets/textures/dirt.png new file mode 100644 index 0000000..617d353 Binary files /dev/null and b/assets/textures/dirt.png differ diff --git a/assets/textures/glass.png b/assets/textures/glass.png new file mode 100644 index 0000000..acadb01 Binary files /dev/null and b/assets/textures/glass.png differ diff --git a/assets/textures/grass_side_overlay.png b/assets/textures/grass_side_overlay.png new file mode 100644 index 0000000..fc3fa9d Binary files /dev/null and b/assets/textures/grass_side_overlay.png differ diff --git a/assets/textures/grass_side_snowed.png b/assets/textures/grass_side_snowed.png new file mode 100644 index 0000000..41f6197 Binary files /dev/null and b/assets/textures/grass_side_snowed.png differ diff --git a/assets/textures/grass_top.png b/assets/textures/grass_top.png new file mode 100644 index 0000000..eaa7e45 Binary files /dev/null and b/assets/textures/grass_top.png differ diff --git a/assets/textures/leaves_oak.png b/assets/textures/leaves_oak.png new file mode 100644 index 0000000..a6773af Binary files /dev/null and b/assets/textures/leaves_oak.png differ diff --git a/assets/textures/leaves_spruce.png b/assets/textures/leaves_spruce.png new file mode 100644 index 0000000..602eab8 Binary files /dev/null and b/assets/textures/leaves_spruce.png differ diff --git a/assets/textures/log_oak.png b/assets/textures/log_oak.png new file mode 100644 index 0000000..914cb5f Binary files /dev/null and b/assets/textures/log_oak.png differ diff --git a/assets/textures/log_oak_top.png b/assets/textures/log_oak_top.png new file mode 100644 index 0000000..7a44e77 Binary files /dev/null and b/assets/textures/log_oak_top.png differ diff --git a/assets/textures/log_spruce.png b/assets/textures/log_spruce.png new file mode 100644 index 0000000..dc1aa2f Binary files /dev/null and b/assets/textures/log_spruce.png differ diff --git a/assets/textures/log_spruce_top.png b/assets/textures/log_spruce_top.png new file mode 100644 index 0000000..280c64e Binary files /dev/null and b/assets/textures/log_spruce_top.png differ diff --git a/assets/textures/planks_oak.png b/assets/textures/planks_oak.png new file mode 100644 index 0000000..346f77d Binary files /dev/null and b/assets/textures/planks_oak.png differ diff --git a/assets/textures/planks_spruce.png b/assets/textures/planks_spruce.png new file mode 100644 index 0000000..f45fa94 Binary files /dev/null and b/assets/textures/planks_spruce.png differ diff --git a/assets/textures/sand.png b/assets/textures/sand.png new file mode 100644 index 0000000..86b9654 Binary files /dev/null and b/assets/textures/sand.png differ diff --git a/assets/textures/snow.png b/assets/textures/snow.png new file mode 100644 index 0000000..5c146cd Binary files /dev/null and b/assets/textures/snow.png differ diff --git a/assets/textures/stone.png b/assets/textures/stone.png new file mode 100644 index 0000000..87e19ff Binary files /dev/null and b/assets/textures/stone.png differ diff --git a/assets/textures/water_flow.png b/assets/textures/water_flow.png new file mode 100644 index 0000000..e72280c Binary files /dev/null and b/assets/textures/water_flow.png differ diff --git a/assets/textures/water_still.png b/assets/textures/water_still.png new file mode 100644 index 0000000..c7e90b0 Binary files /dev/null and b/assets/textures/water_still.png differ diff --git a/imgui.ini b/imgui.ini index ec7be15..11a3090 100644 --- a/imgui.ini +++ b/imgui.ini @@ -15,8 +15,8 @@ Pos=710,245 Size=500,500 [Window][Debug] -Pos=60,60 -Size=500,541 +Pos=56,60 +Size=555,571 [Window][Fun Bullshit] Pos=325,62 @@ -40,6 +40,6 @@ Pos=696,353 Size=528,284 [Window][##CreativeInventory] -Pos=696,399 -Size=528,192 +Pos=696,371 +Size=528,248 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4fa1b2e..a1b0a3c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ set(SOURCES world/RegionManager.cpp utils/JobSystem.cpp world/TerrainGenerator.cpp + world/Biome.cpp world/CaveGenerator.cpp world/WaterSimulator.cpp rendering/ParticleSystem.cpp @@ -27,6 +28,7 @@ set(SOURCES ui/HUD.cpp core/WorldSession.cpp core/Renderer.cpp + rendering/Frustum.cpp ) add_executable(VoxelEngine ${SOURCES}) diff --git a/src/core/MainGlobals.cpp b/src/core/MainGlobals.cpp index 29a873c..128ecce 100644 --- a/src/core/MainGlobals.cpp +++ b/src/core/MainGlobals.cpp @@ -7,16 +7,20 @@ #include #include +#include +#include #include "../../libs/imgui/imgui.h" #include "../utils/BlockTypes.h" #include "../rendering/Camera.h" +#include "../rendering/ItemModelGenerator.h" #include "../rendering/ToolModelGenerator.h" #include "../utils/CoordUtils.h" #include "GameState.h" #include "../gameplay/Player.h" #include "../gameplay/Raycast.h" #include "../rendering/opengl/ShaderClass.h" +#include "../world/Biome.h" #include "../world/TerrainGenerator.h" #define STB_IMAGE_IMPLEMENTATION @@ -47,6 +51,7 @@ std::string currentWorldName; float fov = 70.0f; float mouseSensitivity = 0.1f; bool wireframeMode = false; +bool showBiomeDebugColors = false; bool drunkMode = false; float drunkIntensity = 1.0f; @@ -74,6 +79,12 @@ bool enableWaterSimulation = true; int waterTickRate = 5; float waterTickAccumulator = 0.0f; const float WATER_TICK_INTERVAL = 0.05f; +int frustumSolidTested = 0; +int frustumSolidCulled = 0; +int frustumSolidDrawn = 0; +int frustumWaterTested = 0; +int frustumWaterCulled = 0; +int frustumWaterDrawn = 0; Player* g_player = nullptr; ChunkManager* g_chunkManager = nullptr; @@ -134,29 +145,70 @@ GLuint loadHUDIcon(const std::string& path, bool useNearest) return texture; } -void loadBlockIcons(const std::string& basePath) +void generateBlockIcons(GLuint textureArray, Shader* itemModelShader) { - std::unordered_map iconFiles = { - {1, "natural_dirt.png"}, - {2, "natural_grass_block.png"}, - {3, "masonry_cobblestone.png"}, - {4, "natural_sand.png"}, - {5, "log_oak.png"}, - {6, "leaves_oak.png"}, - {7, "transparent_glass.png"}, - {8, "planks_oak.png"} - }; - - for (const auto& [blockId, filename] : iconFiles) + constexpr int ICON_SIZE = 128; + + GLuint fbo, rbo; + glGenFramebuffers(1, &fbo); + glBindFramebuffer(GL_FRAMEBUFFER, fbo); + + glGenRenderbuffers(1, &rbo); + glBindRenderbuffer(GL_RENDERBUFFER, rbo); + glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, ICON_SIZE, ICON_SIZE); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, rbo); + + GLint prevViewport[4]; + glGetIntegerv(GL_VIEWPORT, prevViewport); + glViewport(0, 0, ICON_SIZE, ICON_SIZE); + + glm::vec3 center(0.5f, 0.5f, 0.5f); + glm::mat4 proj = glm::ortho(-0.85f, 0.85f, 0.85f, -0.85f, 0.1f, 10.0f); + glm::vec3 eye = center + glm::normalize(glm::vec3(1.0f, 0.8f, 1.0f)) * 2.5f; + glm::mat4 view = glm::lookAt(eye, center, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::mat4 mvp = proj * view; + + itemModelShader->Activate(); + glUniform1f(glGetUniformLocation(itemModelShader->ID, "timeOfDay"), 1.0f); + glUniform1f(glGetUniformLocation(itemModelShader->ID, "ambientLight"), 1.0f); + glUniformMatrix4fv(glGetUniformLocation(itemModelShader->ID, "transform"), + 1, GL_FALSE, glm::value_ptr(mvp)); + glUniform1i(glGetUniformLocation(itemModelShader->ID, "textureArray"), 0); + + glEnable(GL_DEPTH_TEST); + + for (auto& [blockId, model] : g_itemModels) { - std::string fullPath = basePath + "/" + filename; - GLuint tex = loadHUDIcon(fullPath); - if (tex != 0) - { - g_blockIcons[blockId] = tex; - std::cout << "Loaded HUD icon for block " << (int)blockId << ": " << filename << std::endl; - } + GLuint iconTex; + glGenTextures(1, &iconTex); + glBindTexture(GL_TEXTURE_2D, iconTex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, ICON_SIZE, ICON_SIZE, 0, + GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_2D, iconTex, 0); + + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D_ARRAY, textureArray); + + glBindVertexArray(model.vao); + glDrawElements(GL_TRIANGLES, model.indexCount, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); + + g_blockIcons[blockId] = iconTex; } + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glDeleteFramebuffers(1, &fbo); + glDeleteRenderbuffers(1, &rbo); + glViewport(prevViewport[0], prevViewport[1], prevViewport[2], prevViewport[3]); } void unloadBlockIcons() @@ -532,7 +584,15 @@ void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods) skyLight = static_cast(c->skyLight[blockIndex(local.x, local.y, local.z)]) / static_cast(MAX_SKY_LIGHT); } } - g_particleSystem->spawnBlockBreakParticles(blockCenter, tileIndex, skyLight, 15); + glm::vec4 particleTint(1.0f); + if (g_blockTypes[oldBlock].faceTint[0]) + { + BiomeID biome = getBiomeAt(hit->blockPos.x, hit->blockPos.z); + bool isLeaf = g_blockTypes[oldBlock].transparent && g_blockTypes[oldBlock].solid; + glm::vec3 t = isLeaf ? getBiomeFoliageTint(biome) : getBiomeGrassTint(biome); + particleTint = glm::vec4(t, 1.0f); + } + g_particleSystem->spawnBlockBreakParticles(blockCenter, tileIndex, skyLight, 15, particleTint); } return; diff --git a/src/core/MainGlobals.h b/src/core/MainGlobals.h index 6dc6bd8..d23c947 100644 --- a/src/core/MainGlobals.h +++ b/src/core/MainGlobals.h @@ -14,6 +14,8 @@ #include "../rendering/ParticleSystem.h" #include "GameState.h" +class Shader; + extern const int SCREEN_WIDTH; extern const int SCREEN_HEIGHT; extern const float MAX_RAYCAST_DISTANCE; @@ -35,6 +37,7 @@ extern std::string currentWorldName; extern float fov; extern float mouseSensitivity; extern bool wireframeMode; +extern bool showBiomeDebugColors; extern bool drunkMode; extern float drunkIntensity; @@ -64,6 +67,12 @@ extern bool enableWaterSimulation; extern int waterTickRate; extern float waterTickAccumulator; extern const float WATER_TICK_INTERVAL; +extern int frustumSolidTested; +extern int frustumSolidCulled; +extern int frustumSolidDrawn; +extern int frustumWaterTested; +extern int frustumWaterCulled; +extern int frustumWaterDrawn; extern Player* g_player; extern ChunkManager* g_chunkManager; @@ -79,6 +88,6 @@ void mouseButtonCallback(GLFWwindow* window, int button, int action, int mods); void scrollCallback(GLFWwindow* window, double xoffset, double yoffset); std::string resolveTexturePath(const std::string& relativePath); GLuint loadHUDIcon(const std::string& path, bool useNearest = false); -void loadBlockIcons(const std::string& basePath); +void generateBlockIcons(GLuint textureArray, Shader* itemModelShader); void unloadBlockIcons(); void executeCommand(const std::string& input, Player& player); diff --git a/src/core/Renderer.cpp b/src/core/Renderer.cpp index d1fd3f7..2c6bd43 100644 --- a/src/core/Renderer.cpp +++ b/src/core/Renderer.cpp @@ -6,14 +6,14 @@ #include "../rendering/Meshing.h" #include "../rendering/ItemModelGenerator.h" #include "../rendering/ToolModelGenerator.h" +#include "../rendering/Frustum.h" #include "../thirdparty/stb_image.h" #include "../world/Chunk.h" +#include "glm/ext/matrix_transform.hpp" #include #include #include -#include #include -#include void Renderer::init() { @@ -44,64 +44,131 @@ void Renderer::init() glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &maxAnisotropy); glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, maxAnisotropy); - int width = 0, height = 0, nrChannels = 0; - std::string texturePath = resolveTexturePath("assets/textures/blocks.png"); - unsigned char* data = stbi_load(texturePath.c_str(), &width, &height, &nrChannels, 0); - - const int TILE_SIZE = 16; - const int TILES_X = 32; - const int TILES_Y = 32; - const int NUM_TILES = TILES_X * TILES_Y; + constexpr int TILE_SIZE = 16; + constexpr int NUM_LAYERS = TEX_COUNT; + + glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, + TILE_SIZE, TILE_SIZE, NUM_LAYERS, 0, + GL_RGBA, GL_UNSIGNED_BYTE, nullptr); + + struct TexEntry { int layer; const char* filename; }; + TexEntry entries[] = { + { TEX_DIRT, "dirt.png" }, + { TEX_GRASS_TOP, "grass_top.png" }, + { TEX_GRASS_SIDE_SNOWED,"grass_side_snowed.png" }, + { TEX_STONE, "stone.png" }, + { TEX_SAND, "sand.png" }, + { TEX_LOG_OAK, "log_oak.png" }, + { TEX_LOG_OAK_TOP, "log_oak_top.png" }, + { TEX_LEAVES_OAK, "leaves_oak.png" }, + { TEX_GLASS, "glass.png" }, + { TEX_PLANKS_OAK, "planks_oak.png" }, + { TEX_COBBLESTONE, "cobblestone.png" }, + { TEX_LOG_SPRUCE, "log_spruce.png" }, + { TEX_LOG_SPRUCE_TOP, "log_spruce_top.png" }, + { TEX_LEAVES_SPRUCE, "leaves_spruce.png" }, + { TEX_PLANKS_SPRUCE, "planks_spruce.png" }, + { TEX_SNOW, "snow.png" }, + }; - if (data) + for (const auto& e : entries) { - GLenum internalFormat = (nrChannels == 4) ? GL_RGBA8 : GL_RGB8; - GLenum format = (nrChannels == 4) ? GL_RGBA : GL_RGB; - - glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, internalFormat, - TILE_SIZE, TILE_SIZE, NUM_TILES, 0, - format, GL_UNSIGNED_BYTE, nullptr); - - std::vector tileData(TILE_SIZE * TILE_SIZE * nrChannels); - int tileSizeBytes = TILE_SIZE * nrChannels; - int atlasRowBytes = width * nrChannels; + std::string path = resolveTexturePath(std::string("assets/textures/") + e.filename); + int w = 0, h = 0, ch = 0; + unsigned char* pixels = stbi_load(path.c_str(), &w, &h, &ch, 4); + if (pixels) + { + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, + 0, 0, e.layer, + TILE_SIZE, TILE_SIZE, 1, + GL_RGBA, GL_UNSIGNED_BYTE, pixels); + stbi_image_free(pixels); + } + else + { + std::cerr << "Failed to load texture: " << path << std::endl; + unsigned char fallback[TILE_SIZE * TILE_SIZE * 4]; + for (int p = 0; p < TILE_SIZE * TILE_SIZE; p++) + { + fallback[p * 4 + 0] = 255; + fallback[p * 4 + 1] = 0; + fallback[p * 4 + 2] = 255; + fallback[p * 4 + 3] = 255; + } + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, + 0, 0, e.layer, + TILE_SIZE, TILE_SIZE, 1, + GL_RGBA, GL_UNSIGNED_BYTE, fallback); + } + } - for (int ty = 0; ty < TILES_Y; ty++) + { + std::string dirtPath = resolveTexturePath("assets/textures/dirt.png"); + std::string overlayPath = resolveTexturePath("assets/textures/grass_side_overlay.png"); + int dw = 0, dh = 0, dch = 0, ow = 0, oh = 0, och = 0; + unsigned char* dirtPx = stbi_load(dirtPath.c_str(), &dw, &dh, &dch, 4); + unsigned char* overlayPx = stbi_load(overlayPath.c_str(), &ow, &oh, &och, 4); + + unsigned char composite[TILE_SIZE * TILE_SIZE * 4]; + if (dirtPx && overlayPx) { - for (int tx = 0; tx < TILES_X; tx++) + for (int p = 0; p < TILE_SIZE * TILE_SIZE; p++) { - int tileIndex = ty * TILES_X + tx; - unsigned char* tileStart = data + (ty * TILE_SIZE) * atlasRowBytes + tx * tileSizeBytes; - for (int row = 0; row < TILE_SIZE; row++) + unsigned char oa = overlayPx[p * 4 + 3]; + if (oa > 0) { - std::copy(tileStart + row * atlasRowBytes, - tileStart + row * atlasRowBytes + tileSizeBytes, - tileData.begin() + row * tileSizeBytes); + float a = static_cast(oa) / 255.0f; + for (int c = 0; c < 3; c++) + { + float bg = static_cast(dirtPx[p * 4 + c]) / 255.0f; + float fg = static_cast(overlayPx[p * 4 + c]) / 255.0f; + composite[p * 4 + c] = static_cast( + (fg * a + bg * (1.0f - a)) * 255.0f); + } + composite[p * 4 + 3] = 255; + } + else + { + composite[p * 4 + 0] = dirtPx[p * 4 + 0]; + composite[p * 4 + 1] = dirtPx[p * 4 + 1]; + composite[p * 4 + 2] = dirtPx[p * 4 + 2]; + composite[p * 4 + 3] = 254; } - glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, - 0, 0, tileIndex, - TILE_SIZE, TILE_SIZE, 1, - format, GL_UNSIGNED_BYTE, tileData.data()); } } - glGenerateMipmap(GL_TEXTURE_2D_ARRAY); - std::cout << "Loaded texture array with " << NUM_TILES << " tiles" << std::endl; - } - else - { - std::cerr << "Failed to load texture at " << texturePath << ": " - << stbi_failure_reason() << std::endl; - unsigned char fallback[] = {255, 0, 255, 255}; - glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 1, 1, 1, 0, - GL_RGBA, GL_UNSIGNED_BYTE, fallback); + else + { + for (int p = 0; p < TILE_SIZE * TILE_SIZE; p++) + { + composite[p * 4 + 0] = 255; + composite[p * 4 + 1] = 0; + composite[p * 4 + 2] = 255; + composite[p * 4 + 3] = 255; + } + } + glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, + 0, 0, TEX_GRASS_SIDE, + TILE_SIZE, TILE_SIZE, 1, + GL_RGBA, GL_UNSIGNED_BYTE, composite); + if (dirtPx) stbi_image_free(dirtPx); + if (overlayPx) stbi_image_free(overlayPx); } - stbi_image_free(data); + + + glGenerateMipmap(GL_TEXTURE_2D_ARRAY); + std::cout << "Loaded " << NUM_LAYERS << " block textures" << std::endl; initBlockTypes(); - std::string hudIconPath = resolveTexturePath("assets/textures/hud_blocks"); - loadBlockIcons(hudIconPath); + itemModelShader = std::make_unique("item_model.vert", "item_model.frag"); + itemModelShader->Activate(); + glUniform1i(glGetUniformLocation(itemModelShader->ID, "textureArray"), 0); + itemTransformLoc = glGetUniformLocation(itemModelShader->ID, "transform"); + itemTimeOfDayLoc = glGetUniformLocation(itemModelShader->ID, "timeOfDay"); + itemAmbientLightLoc = glGetUniformLocation(itemModelShader->ID, "ambientLight"); + loadItemModels(); + generateBlockIcons(textureArray, itemModelShader.get()); loadToolModels(); for (int i = 0; i < 10; ++i) @@ -124,13 +191,6 @@ void Renderer::init() destroySkyLightLoc = glGetUniformLocation(destroyShader->ID, "SkyLight"); destroyFaceShadeLoc = glGetUniformLocation(destroyShader->ID, "FaceShade"); - itemModelShader = std::make_unique("item_model.vert", "item_model.frag"); - itemModelShader->Activate(); - glUniform1i(glGetUniformLocation(itemModelShader->ID, "textureArray"), 0); - itemTransformLoc = glGetUniformLocation(itemModelShader->ID, "transform"); - itemTimeOfDayLoc = glGetUniformLocation(itemModelShader->ID, "timeOfDay"); - itemAmbientLightLoc = glGetUniformLocation(itemModelShader->ID, "ambientLight"); - toolModelShader = std::make_unique("tool_model.vert", "tool_model.frag"); toolModelShader->Activate(); toolTransformLoc = glGetUniformLocation(toolModelShader->ID, "transform"); @@ -229,6 +289,13 @@ void Renderer::cleanup() void Renderer::beginFrame(const FrameParams& fp) { + frustumSolidTested = 0; + frustumSolidCulled = 0; + frustumSolidDrawn = 0; + frustumWaterTested = 0; + frustumWaterCulled = 0; + frustumWaterDrawn = 0; + shaderProgram->Activate(); glBindTexture(GL_TEXTURE_2D_ARRAY, textureArray); @@ -251,22 +318,39 @@ void Renderer::beginFrame(const FrameParams& fp) void Renderer::renderChunks(const FrameParams& fp, ChunkManager& cm) { + const glm::mat4 viewProj = fp.proj * fp.view; + const Frustum frustum = Frustum::fromMatrix(viewProj); + const float chunkSizeF = static_cast(CHUNK_SIZE); + for (auto& pair : cm.chunks) { Chunk* chunk = pair.second.get(); - if (chunk->indexCount > 0) + if (chunk->indexCount == 0) + continue; + frustumSolidTested++; + + const glm::vec3 chunkMin = glm::vec3(chunk->position) * chunkSizeF; + const glm::vec3 chunkMax = chunkMin + glm::vec3(chunkSizeF); + + if (!frustum.intersectsAABB(chunkMin, chunkMax)) { - glm::mat4 chunkModel = glm::translate(glm::mat4(1.0f), - glm::vec3(chunk->position.x * CHUNK_SIZE, - chunk->position.y * CHUNK_SIZE, - chunk->position.z * CHUNK_SIZE)); - glm::mat4 chunkMVP = fp.proj * fp.view * chunkModel; - glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(chunkMVP)); - glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(chunkModel)); - - glBindVertexArray(chunk->vao); - glDrawElements(GL_TRIANGLES, chunk->indexCount, GL_UNSIGNED_INT, 0); + frustumSolidCulled++; + continue; } + + glm::mat4 chunkModel = glm::translate( + glm::mat4(1.0f), + glm::vec3(chunk->position.x * CHUNK_SIZE, + chunk->position.y * CHUNK_SIZE, + chunk->position.z * CHUNK_SIZE)); + glm::mat4 chunkMVP = viewProj * chunkModel; + + glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(chunkMVP)); + glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(chunkModel)); + + glBindVertexArray(chunk->vao); + glDrawElements(GL_TRIANGLES, chunk->indexCount, GL_UNSIGNED_INT, 0); + frustumSolidDrawn++; } } @@ -288,22 +372,39 @@ void Renderer::renderWater(const FrameParams& fp, ChunkManager& cm) glUniform1f(waterAmbientLightLoc, fp.ambientLight); glUniform1i(waterEnableCausticsLoc, enableCaustics ? 1 : 0); + const glm::mat4 viewProj = fp.proj * fp.view; + const Frustum frustum = Frustum::fromMatrix(viewProj); + const float chunkSizeF = static_cast(CHUNK_SIZE); + for (auto& pair : cm.chunks) { Chunk* chunk = pair.second.get(); - if (chunk->waterIndexCount > 0) + if (chunk->waterIndexCount == 0) + continue; + frustumWaterTested++; + + const glm::vec3 chunkMin = glm::vec3(chunk->position) * chunkSizeF; + const glm::vec3 chunkMax = chunkMin + glm::vec3(chunkSizeF); + + if (!frustum.intersectsAABB(chunkMin, chunkMax)) { - glm::mat4 chunkModel = glm::translate(glm::mat4(1.0f), - glm::vec3(chunk->position.x * CHUNK_SIZE, - chunk->position.y * CHUNK_SIZE, - chunk->position.z * CHUNK_SIZE)); - glm::mat4 chunkMVP = fp.proj * fp.view * chunkModel; - glUniformMatrix4fv(waterTransformLoc, 1, GL_FALSE, glm::value_ptr(chunkMVP)); - glUniformMatrix4fv(waterModelLoc, 1, GL_FALSE, glm::value_ptr(chunkModel)); - - glBindVertexArray(chunk->waterVao); - glDrawElements(GL_TRIANGLES, chunk->waterIndexCount, GL_UNSIGNED_INT, 0); + frustumWaterCulled++; + continue; } + + glm::mat4 chunkModel = glm::translate( + glm::mat4(1.0f), + glm::vec3(chunk->position.x * CHUNK_SIZE, + chunk->position.y * CHUNK_SIZE, + chunk->position.z * CHUNK_SIZE)); + glm::mat4 chunkMVP = viewProj * chunkModel; + + glUniformMatrix4fv(waterTransformLoc, 1, GL_FALSE, glm::value_ptr(chunkMVP)); + glUniformMatrix4fv(waterModelLoc, 1, GL_FALSE, glm::value_ptr(chunkModel)); + + glBindVertexArray(chunk->waterVao); + glDrawElements(GL_TRIANGLES, chunk->waterIndexCount, GL_UNSIGNED_INT, 0); + frustumWaterDrawn++; } glDepthMask(GL_TRUE); diff --git a/src/core/WorldSession.cpp b/src/core/WorldSession.cpp index 12f0f68..30a0719 100644 --- a/src/core/WorldSession.cpp +++ b/src/core/WorldSession.cpp @@ -131,6 +131,8 @@ void WorldSession::shutdown(Player& player, GLFWwindow* window) for (auto& pair : chunkManager->chunks) { Chunk* chunk = pair.second.get(); + if (!chunk->dirtyData) + continue; regionManager->saveChunkData( chunk->position.x, chunk->position.y, chunk->position.z, chunk->blocks); } diff --git a/src/core/main.cpp b/src/core/main.cpp index 55b94f9..734f3bb 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -21,6 +21,9 @@ #include "../utils/BlockTypes.h" #include "../utils/CoordUtils.h" +#include "../world/Biome.h" +#include "../world/TerrainGenerator.h" + #include "../gameplay/Raycast.h" #include "../gameplay/Player.h" #include "../gameplay/SurvivalSystem.h" @@ -131,6 +134,8 @@ int main(int argc, char* argv[]) auto& waterSimulator = session.waterSimulator; auto& regionManager = session.regionManager; auto& selectedBlock = session.selectedBlock; + std::vector loadOffsets; + int cachedLoadRadius = -1; while (!glfwWindowShouldClose(window)) { @@ -384,27 +389,73 @@ int main(int argc, char* argv[]) const int UNLOAD_RADIUS = LOAD_RADIUS + 2; const int CHUNK_HEIGHT_MIN = 0; const int CHUNK_HEIGHT_MAX = (256 / CHUNK_SIZE) - 1; - - if (currentState == GameState::Playing) + if (cachedLoadRadius != LOAD_RADIUS) { + loadOffsets.clear(); for (int dx = -LOAD_RADIUS; dx <= LOAD_RADIUS; dx++) { for (int dz = -LOAD_RADIUS; dz <= LOAD_RADIUS; dz++) { - for (int cy = CHUNK_HEIGHT_MIN; cy <= CHUNK_HEIGHT_MAX; cy++) + loadOffsets.push_back({dx, dz}); + } + } + std::sort(loadOffsets.begin(), loadOffsets.end(), + [](const glm::ivec2& a, const glm::ivec2& b) { - int chunkX = cx + dx; - int chunkZ = cz + dz; + int da = a.x * a.x + a.y * a.y; + int db = b.x * b.x + b.y * b.y; + return da < db; + }); + cachedLoadRadius = LOAD_RADIUS; + } + size_t pendingJobs = jobSystem ? jobSystem->pendingJobCount() : 0; + int maxLoadEnqueuePerFrame = 32; + int maxMeshEnqueuePerFrame = 16; + if (pendingJobs > 200) + { + maxLoadEnqueuePerFrame = 4; + maxMeshEnqueuePerFrame = 0; + } + else if (pendingJobs > 140) + { + maxLoadEnqueuePerFrame = 8; + maxMeshEnqueuePerFrame = 2; + } + else if (pendingJobs > 90) + { + maxLoadEnqueuePerFrame = 12; + maxMeshEnqueuePerFrame = 4; + } + else if (pendingJobs > 50) + { + maxLoadEnqueuePerFrame = 20; + maxMeshEnqueuePerFrame = 8; + } - if (!chunkManager->hasChunk(chunkX, cy, chunkZ) && - !chunkManager->isLoading(chunkX, cy, chunkZ) && - !chunkManager->isSaving(chunkX, cy, chunkZ)) + if (currentState == GameState::Playing) + { + int enqueuedLoads = 0; + for (const glm::ivec2& offset : loadOffsets) + { + if (useAsyncLoading && enqueuedLoads >= maxLoadEnqueuePerFrame) + break; + int chunkX = cx + offset.x; + int chunkZ = cz + offset.y; + for (int cy = CHUNK_HEIGHT_MIN; cy <= CHUNK_HEIGHT_MAX; cy++) + { + if (useAsyncLoading && enqueuedLoads >= maxLoadEnqueuePerFrame) + break; + if (!chunkManager->hasChunk(chunkX, cy, chunkZ) && + !chunkManager->isLoading(chunkX, cy, chunkZ) && + !chunkManager->isSaving(chunkX, cy, chunkZ)) + { + if (useAsyncLoading) { - if (useAsyncLoading) - chunkManager->enqueueLoadChunk(chunkX, cy, chunkZ); - else - chunkManager->loadChunk(chunkX, cy, chunkZ); + chunkManager->enqueueLoadChunk(chunkX, cy, chunkZ); + enqueuedLoads++; } + else + chunkManager->loadChunk(chunkX, cy, chunkZ); } } } @@ -430,6 +481,8 @@ int main(int argc, char* argv[]) } } + std::vector> meshCandidates; + meshCandidates.reserve(chunkManager->chunks.size()); for (auto& pair : chunkManager->chunks) { Chunk* chunk = pair.second.get(); @@ -456,16 +509,35 @@ int main(int argc, char* argv[]) if (neighborsReady) { - if (useAsyncLoading) - chunkManager->enqueueMeshChunk(chunk->position.x, chunk->position.y, chunk->position.z); - else - { - buildChunkMesh(*chunk, *chunkManager); - chunk->dirtyMesh = false; - } + int dx = chunk->position.x - cx; + int dz = chunk->position.z - cz; + int dist2 = dx * dx + dz * dz; + meshCandidates.push_back({dist2, chunk}); } } } + std::sort(meshCandidates.begin(), meshCandidates.end(), + [](const std::pair& a, const std::pair& b) + { + return a.first < b.first; + }); + int enqueuedMeshes = 0; + for (const auto& candidate : meshCandidates) + { + Chunk* chunk = candidate.second; + if (useAsyncLoading) + { + if (enqueuedMeshes >= maxMeshEnqueuePerFrame) + break; + chunkManager->enqueueMeshChunk(chunk->position.x, chunk->position.y, chunk->position.z); + enqueuedMeshes++; + } + else + { + buildChunkMesh(*chunk, *chunkManager); + chunk->dirtyMesh = false; + } + } renderer.renderChunks(fp, *chunkManager); renderer.renderWater(fp, *chunkManager); @@ -514,7 +586,15 @@ int main(int argc, char* argv[]) skyLightVal = static_cast(c->skyLight[blockIndex(local.x, local.y, local.z)]) / static_cast(MAX_SKY_LIGHT); } } - g_particleSystem->spawnBlockBreakParticles(blockCenter, tileIndex, skyLightVal, 15); + glm::vec4 particleTint(1.0f); + if (g_blockTypes[player.breakingBlockId].faceTint[0]) + { + BiomeID biome = getBiomeAt(hit->blockPos.x, hit->blockPos.z); + bool isLeaf = g_blockTypes[player.breakingBlockId].transparent && g_blockTypes[player.breakingBlockId].solid; + glm::vec3 t = isLeaf ? getBiomeFoliageTint(biome) : getBiomeGrassTint(biome); + particleTint = glm::vec4(t, 1.0f); + } + g_particleSystem->spawnBlockBreakParticles(blockCenter, tileIndex, skyLightVal, 15, particleTint); } player.isBreaking = false; diff --git a/src/gameplay/Inventory.cpp b/src/gameplay/Inventory.cpp index 360d6d1..ad7312f 100644 --- a/src/gameplay/Inventory.cpp +++ b/src/gameplay/Inventory.cpp @@ -20,6 +20,11 @@ const std::vector& getCreativeItems() {6, "Oak Leaves"}, {7, "Glass"}, {8, "Oak Planks"}, + {18, "Snow Grass"}, + {19, "Spruce Log"}, + {20, "Spruce Planks"}, + {21, "Spruce Leaves"}, + {22, "Cobblestone"}, {TOOL_DIAMOND_PICKAXE, "Diamond Pickaxe"}, }; return items; diff --git a/src/gameplay/Raycast.cpp b/src/gameplay/Raycast.cpp index 21dd237..b075988 100644 --- a/src/gameplay/Raycast.cpp +++ b/src/gameplay/Raycast.cpp @@ -27,6 +27,7 @@ void setBlockAtWorld(int wx, int wy, int wz, uint8_t blockId, ChunkManager& chun c->blocks[blockIndex(local.x, local.y, local.z)] = blockId; c->dirtyMesh = true; c->dirtyLight = true; + c->dirtyData = true; for (int i = 0; i < 6; i++) { diff --git a/src/rendering/Frustum.cpp b/src/rendering/Frustum.cpp new file mode 100644 index 0000000..29d293b --- /dev/null +++ b/src/rendering/Frustum.cpp @@ -0,0 +1,53 @@ +#include "Frustum.h" + +#include +#include + +Frustum Frustum::fromMatrix(const glm::mat4& m) +{ + Frustum f; + + const std::array rawPlanes = { + glm::vec4(m[0][3] + m[0][0], m[1][3] + m[1][0], m[2][3] + m[2][0], m[3][3] + m[3][0]), + glm::vec4(m[0][3] - m[0][0], m[1][3] - m[1][0], m[2][3] - m[2][0], m[3][3] - m[3][0]), + glm::vec4(m[0][3] + m[0][1], m[1][3] + m[1][1], m[2][3] + m[2][1], m[3][3] + m[3][1]), + glm::vec4(m[0][3] - m[0][1], m[1][3] - m[1][1], m[2][3] - m[2][1], m[3][3] - m[3][1]), + glm::vec4(m[0][3] + m[0][2], m[1][3] + m[1][2], m[2][3] + m[2][2], m[3][3] + m[3][2]), + glm::vec4(m[0][3] - m[0][2], m[1][3] - m[1][2], m[2][3] - m[2][2], m[3][3] - m[3][2]), + }; + + for (size_t i = 0; i < rawPlanes.size(); i++) + { + glm::vec3 n(rawPlanes[i].x, rawPlanes[i].y, rawPlanes[i].z); + float len = glm::length(n); + if (len > 0.0f) + { + f.planes[i].normal = n / len; + f.planes[i].distance = rawPlanes[i].w / len; + } + else + { + f.planes[i].normal = glm::vec3(0.0f, 0.0f, 0.0f); + f.planes[i].distance = 0.0f; + } + } + + return f; +} + +bool Frustum::intersectsAABB(const glm::vec3& minPoint, const glm::vec3& maxPoint) const +{ + for (const Plane& plane : planes) + { + glm::vec3 p = minPoint; + + if (plane.normal.x >= 0.0f) p.x = maxPoint.x; + if (plane.normal.y >= 0.0f) p.y = maxPoint.y; + if (plane.normal.z >= 0.0f) p.z = maxPoint.z; + + if (glm::dot(plane.normal, p) + plane.distance < 0.0f) + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/rendering/Frustum.h b/src/rendering/Frustum.h new file mode 100644 index 0000000..b26d90f --- /dev/null +++ b/src/rendering/Frustum.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +struct Plane +{ + glm::vec3 normal; + float distance; +}; + +struct Frustum +{ + std::array planes; + + static Frustum fromMatrix(const glm::mat4& viewProjection); + bool intersectsAABB(const glm::vec3& minPoint, const glm::vec3& maxPoint) const; +}; \ No newline at end of file diff --git a/src/rendering/ItemModelGenerator.cpp b/src/rendering/ItemModelGenerator.cpp index 896425a..66f5dcc 100644 --- a/src/rendering/ItemModelGenerator.cpp +++ b/src/rendering/ItemModelGenerator.cpp @@ -1,5 +1,6 @@ #include "ItemModelGenerator.h" #include "../utils/BlockTypes.h" +#include "../world/Biome.h" #include "Meshing.h" #include #include @@ -36,6 +37,15 @@ ItemModel ItemModelGenerator::generateBlockCube(uint8_t blockId) int rotation = block.faceRotation[dir]; float faceShade = FACE_SHADE[dir]; + glm::vec3 tint(1.0f); + if (block.faceTint[dir]) + { + bool isLeaf = block.transparent && block.solid; + tint = isLeaf + ? getBiomeFoliageTint(BiomeID::Plains) + : getBiomeGrassTint(BiomeID::Plains); + } + uint32_t base = static_cast(vertices.size()); for (int vi = 0; vi < 4; vi++) @@ -74,7 +84,7 @@ ItemModel ItemModelGenerator::generateBlockCube(uint8_t blockId) break; } - vertices.push_back({pos, {localU, localV}, tileIndex, faceShade}); + vertices.push_back({pos, {localU, localV}, tileIndex, faceShade, tint}); } indices.push_back(base); @@ -119,6 +129,10 @@ ItemModel ItemModelGenerator::generateBlockCube(uint8_t blockId) (void*)offsetof(ItemVertex, faceShade)); glEnableVertexAttribArray(3); + glVertexAttribPointer(4, 3, GL_FLOAT, GL_FALSE, sizeof(ItemVertex), + (void*)offsetof(ItemVertex, biomeTint)); + glEnableVertexAttribArray(4); + glBindVertexArray(0); model.indexCount = static_cast(indices.size()); @@ -135,7 +149,7 @@ void ItemModelGenerator::destroyModel(ItemModel& model) void loadItemModels() { - uint8_t blockIds[] = {1, 2, 3, 4, 5, 6, 7, 8}; + uint8_t blockIds[] = {1, 2, 3, 4, 5, 6, 7, 8, 18, 19, 20, 21, 22}; for (uint8_t blockId : blockIds) { ItemModel model = ItemModelGenerator::generateBlockCube(blockId); diff --git a/src/rendering/ItemModelGenerator.h b/src/rendering/ItemModelGenerator.h index b56f4da..53739a4 100644 --- a/src/rendering/ItemModelGenerator.h +++ b/src/rendering/ItemModelGenerator.h @@ -11,6 +11,7 @@ struct ItemVertex { glm::vec2 uv; float tileIndex; float faceShade; + glm::vec3 biomeTint; }; struct ItemModel { diff --git a/src/rendering/Meshing.cpp b/src/rendering/Meshing.cpp index 6c6b6be..e069253 100644 --- a/src/rendering/Meshing.cpp +++ b/src/rendering/Meshing.cpp @@ -1,6 +1,7 @@ #include "Meshing.h" #include "../utils/BlockTypes.h" #include "../world/ChunkManager.h" +#include "../world/TerrainGenerator.h" #include "../world/WaterSimulator.h" #include #include @@ -24,6 +25,7 @@ static const uint32_t FACE_INDICES[6] = { 0, 1, 2, 0, 2, 3}; + static float getFluidHeight(BlockGetter getBlock, int cornerX, int cornerY, int cornerZ) { int count = 0; @@ -169,6 +171,7 @@ static WaterVertexUV calculateWaterUV(float angle) static void buildGreedyMesh( const BlockID* blocks, + const glm::ivec3& chunkWorldOrigin, BlockGetter getBlock, LightGetter getSkyLight, std::vector& outVertices, @@ -343,6 +346,17 @@ static void buildGreedyMesh( int blockX = blockWorldPos.x; int blockY = blockWorldPos.y; int blockZ = blockWorldPos.z; + glm::vec3 biomeTint(1.0f); + if (g_blockTypes[type].faceTint[dir]) + { + BiomeID biome = getBiomeAt( + chunkWorldOrigin.x + blockX, + chunkWorldOrigin.z + blockZ); + bool isLeaf = g_blockTypes[type].transparent && g_blockTypes[type].solid; + biomeTint = isLeaf + ? getBiomeFoliageTint(biome) + : getBiomeGrassTint(biome); + } for (int vIdx = 0; vIdx < 4; vIdx++) { @@ -443,6 +457,7 @@ static void buildGreedyMesh( vtx.tileIndex = static_cast(tileIndex); vtx.skyLight = skyLightNormalized; vtx.faceShade = faceShade; + vtx.biomeTint = biomeTint; outVertices.push_back(vtx); } @@ -662,8 +677,12 @@ void buildChunkMesh(Chunk &c, ChunkManager &chunkManager) std::vector waterVerts; std::vector waterInds; - buildGreedyMesh(c.blocks, getBlock, getSkyLight, verts, inds, false); - buildGreedyMesh(c.blocks, getBlock, getSkyLight, waterVerts, waterInds, true); + glm::ivec3 chunkWorldOrigin( + c.position.x * CHUNK_SIZE, + c.position.y * CHUNK_SIZE, + c.position.z * CHUNK_SIZE); + buildGreedyMesh(c.blocks, chunkWorldOrigin, getBlock, getSkyLight, verts, inds, false); + buildGreedyMesh(c.blocks, chunkWorldOrigin, getBlock, getSkyLight, waterVerts, waterInds, true); uploadToGPU(c, verts, inds); uploadWaterToGPU(c, waterVerts, waterInds); @@ -705,6 +724,10 @@ void uploadToGPU(Chunk &c, const std::vector &verts, const std::vector(inds.size()); c.vertexCount = static_cast(verts.size()); } @@ -759,6 +782,10 @@ void uploadWaterToGPU(Chunk &c, const std::vector &verts, const std::vec (void *)offsetof(Vertex, faceShade)); glEnableVertexAttribArray(4); + glVertexAttribPointer(5, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void *)offsetof(Vertex, biomeTint)); + glEnableVertexAttribArray(5); + c.waterIndexCount = static_cast(inds.size()); c.waterVertexCount = static_cast(verts.size()); } @@ -766,6 +793,7 @@ void uploadWaterToGPU(Chunk &c, const std::vector &verts, const std::vec void buildChunkMeshOffThread( const BlockID* blocks, const uint8_t* skyLight, + const glm::ivec3& chunkWorldOrigin, BlockGetter getBlock, LightGetter getSkyLight, std::vector& outVertices, @@ -773,6 +801,6 @@ void buildChunkMeshOffThread( std::vector& outWaterVertices, std::vector& outWaterIndices) { - buildGreedyMesh(blocks, getBlock, getSkyLight, outVertices, outIndices, false); - buildGreedyMesh(blocks, getBlock, getSkyLight, outWaterVertices, outWaterIndices, true); + buildGreedyMesh(blocks, chunkWorldOrigin, getBlock, getSkyLight, outVertices, outIndices, false); + buildGreedyMesh(blocks, chunkWorldOrigin, getBlock, getSkyLight, outWaterVertices, outWaterIndices, true); } diff --git a/src/rendering/Meshing.h b/src/rendering/Meshing.h index d7e52d6..6f8b97e 100644 --- a/src/rendering/Meshing.h +++ b/src/rendering/Meshing.h @@ -12,6 +12,7 @@ struct Vertex float tileIndex; float skyLight; float faceShade; + glm::vec3 biomeTint; }; enum FaceDir { @@ -44,6 +45,7 @@ using LightGetter = std::function; void buildChunkMeshOffThread( const BlockID* blocks, const uint8_t* skyLight, + const glm::ivec3& chunkWorldOrigin, BlockGetter getBlock, LightGetter getSkyLight, std::vector& outVertices, diff --git a/src/rendering/ParticleSystem.cpp b/src/rendering/ParticleSystem.cpp index 57de06a..77ba700 100644 --- a/src/rendering/ParticleSystem.cpp +++ b/src/rendering/ParticleSystem.cpp @@ -120,7 +120,7 @@ void ParticleSystem::render(const glm::mat4& view, const glm::mat4& projection, vao.Unbind(); } -void ParticleSystem::spawnBlockBreakParticles(const glm::vec3& blockCenter, int tileIndex, float skyLight, int count) +void ParticleSystem::spawnBlockBreakParticles(const glm::vec3& blockCenter, int tileIndex, float skyLight, int count, const glm::vec4& tint) { std::uniform_real_distribution posDist(-0.3f, 0.3f); std::uniform_real_distribution velXZ(-3.0f, 3.0f); @@ -137,7 +137,7 @@ void ParticleSystem::spawnBlockBreakParticles(const glm::vec3& blockCenter, int p.maxLifetime = p.lifetime; p.size = sizeDist(rng); p.tileIndex = static_cast(tileIndex); - p.color = glm::vec4(1.0f); + p.color = tint; p.skyLight = skyLight; particles.push_back(p); } diff --git a/src/rendering/ParticleSystem.h b/src/rendering/ParticleSystem.h index ac20aca..c352410 100644 --- a/src/rendering/ParticleSystem.h +++ b/src/rendering/ParticleSystem.h @@ -27,7 +27,7 @@ class ParticleSystem { void update(float dt); void render(const glm::mat4& view, const glm::mat4& projection, const glm::vec3& cameraPos, float timeOfDay, float ambientLight); - void spawnBlockBreakParticles(const glm::vec3& blockCenter, int tileIndex, float skyLight, int count = 12); + void spawnBlockBreakParticles(const glm::vec3& blockCenter, int tileIndex, float skyLight, int count = 12, const glm::vec4& tint = glm::vec4(1.0f)); void spawnParticle(const glm::vec3& pos, const glm::vec3& vel, float lifetime, float size, float tileIndex, float skyLight, const glm::vec4& color = glm::vec4(1.0f)); void clear(); diff --git a/src/shaders/default.frag b/src/shaders/default.frag index 6b2c27a..c441dbe 100644 --- a/src/shaders/default.frag +++ b/src/shaders/default.frag @@ -7,6 +7,7 @@ in float SkyLight; in float FaceShade; in float FragDepth; in vec3 WorldPos; +in vec3 BiomeTint; uniform sampler2DArray textureArray; uniform float timeOfDay; @@ -28,7 +29,9 @@ void main() float totalLight = max(skyLightContribution, ambientLight); float finalLight = totalLight * FaceShade; - vec3 litColor = texColor.rgb * finalLight; + float tintMask = step(0.998, texColor.a); + vec3 tint = mix(vec3(1.0), BiomeTint, tintMask); + vec3 litColor = texColor.rgb * tint * finalLight; float dist = length(WorldPos - cameraPos); float fogFactor = 1.0 - exp(-dist * fogDensity); @@ -40,5 +43,5 @@ void main() vec3 finalColor = mix(litColor, fogColor, fogFactor); - FragColor = vec4(finalColor, texColor.a); + FragColor = vec4(finalColor, 1.0); } diff --git a/src/shaders/default.vert b/src/shaders/default.vert index d2b7f21..b2c8fcc 100644 --- a/src/shaders/default.vert +++ b/src/shaders/default.vert @@ -4,6 +4,7 @@ layout (location = 1) in vec2 aLocalUV; layout (location = 2) in float aTileIndex; layout (location = 3) in float aSkyLight; layout (location = 4) in float aFaceShade; +layout (location = 5) in vec3 aBiomeTint; out vec2 LocalUV; flat out float TileIndex; @@ -11,6 +12,7 @@ out float SkyLight; out float FaceShade; out float FragDepth; out vec3 WorldPos; +out vec3 BiomeTint; uniform mat4 transform; uniform mat4 model; @@ -25,4 +27,5 @@ void main() FaceShade = aFaceShade; FragDepth = gl_Position.z; WorldPos = worldPosition.xyz; + BiomeTint = aBiomeTint; } diff --git a/src/shaders/item_model.frag b/src/shaders/item_model.frag index 81af8df..b867eed 100644 --- a/src/shaders/item_model.frag +++ b/src/shaders/item_model.frag @@ -4,6 +4,7 @@ out vec4 FragColor; in vec2 UV; flat in float TileIndex; in float FaceShade; +in vec3 BiomeTint; uniform sampler2DArray textureArray; uniform float timeOfDay; @@ -16,5 +17,7 @@ void main() discard; float totalLight = max(timeOfDay, ambientLight); float finalLight = totalLight * FaceShade; - FragColor = vec4(texColor.rgb * finalLight, texColor.a); + float tintMask = step(0.998, texColor.a); + vec3 tint = mix(vec3(1.0), BiomeTint, tintMask); + FragColor = vec4(texColor.rgb * tint * finalLight, 1.0); } diff --git a/src/shaders/item_model.vert b/src/shaders/item_model.vert index 59911ea..995c525 100644 --- a/src/shaders/item_model.vert +++ b/src/shaders/item_model.vert @@ -3,10 +3,12 @@ layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aUV; layout (location = 2) in float aTileIndex; layout (location = 3) in float aFaceShade; +layout (location = 4) in vec3 aBiomeTint; out vec2 UV; flat out float TileIndex; out float FaceShade; +out vec3 BiomeTint; uniform mat4 transform; @@ -16,4 +18,5 @@ void main() UV = aUV; TileIndex = aTileIndex; FaceShade = aFaceShade; -} \ No newline at end of file + BiomeTint = aBiomeTint; +} diff --git a/src/shaders/particle.frag b/src/shaders/particle.frag index ba638fb..33199ba 100644 --- a/src/shaders/particle.frag +++ b/src/shaders/particle.frag @@ -21,8 +21,9 @@ void main() float skyLightContribution = SkyLight * sunBrightness; float totalLight = max(skyLightContribution, ambientLight); + float tintMask = step(0.998, texColor.a); + vec3 tint = mix(vec3(1.0), ParticleColor.rgb, tintMask); + float fade = smoothstep(0.0, 0.3, Lifetime); - vec4 litColor = vec4(texColor.rgb * totalLight, texColor.a); - FragColor = litColor * ParticleColor; - FragColor.a *= fade; + FragColor = vec4(texColor.rgb * tint * totalLight, fade); } diff --git a/src/shaders/particle.vert b/src/shaders/particle.vert index 492d589..0d39df6 100644 --- a/src/shaders/particle.vert +++ b/src/shaders/particle.vert @@ -25,7 +25,7 @@ void main() + cameraUp * aQuadPos.y * aSize; gl_Position = projection * view * vec4(vertexPos, 1.0); - TexCoord = aQuadPos + 0.5; + TexCoord = vec2(aQuadPos.x + 0.5, 0.5 - aQuadPos.y); TileIndex = aTileIndex; ParticleColor = aColor; Lifetime = aLife; diff --git a/src/ui/DebugUI.cpp b/src/ui/DebugUI.cpp index 0c5902f..da787bf 100644 --- a/src/ui/DebugUI.cpp +++ b/src/ui/DebugUI.cpp @@ -2,6 +2,7 @@ #include "../core/MainGlobals.h" #include "../utils/BlockTypes.h" #include "../gameplay/Inventory.h" +#include "../world/TerrainGenerator.h" #include "../../libs/imgui/imgui.h" #include @@ -40,6 +41,20 @@ void drawDebugUI( int chunkZDbg = static_cast(floor(player.position.z / 16.0f)); ImGui::Text("Chunk: (%d, %d)", chunkXDbg, chunkZDbg); + int playerBlockX = static_cast(std::floor(player.position.x)); + int playerBlockZ = static_cast(std::floor(player.position.z)); + BiomeID biome = getBiomeAt(playerBlockX, playerBlockZ); + const char* biomeName = "Unknown"; + switch (biome) + { + case BiomeID::Desert: biomeName = "Desert"; break; + case BiomeID::Forest: biomeName = "Forest"; break; + case BiomeID::Tundra: biomeName = "Tundra"; break; + case BiomeID::Plains: biomeName = "Plains"; break; + default: break; + } + ImGui::Text("Biome: %s", biomeName); + if (selectedBlock.has_value()) { ImGui::Separator(); @@ -83,6 +98,8 @@ void drawDebugUI( ImGui::Text("Chunks loading: %zu", chunkManager->loadingChunks.size()); ImGui::Text("Chunks meshing: %zu", chunkManager->meshingChunks.size()); ImGui::Text("Jobs pending: %zu", jobSystem->pendingJobCount()); + ImGui::Text("Frustum solid tested:%d culled:%d drawn:%d", frustumSolidTested, frustumSolidCulled, frustumSolidDrawn); + ImGui::Text("Frustum water tested:%d culled:%d drawn:%d", frustumWaterTested, frustumWaterCulled, frustumWaterDrawn); ImGui::EndTabItem(); } @@ -93,6 +110,7 @@ void drawDebugUI( ImGui::Separator(); ImGui::Checkbox("Wireframe mode", &wireframeMode); + ImGui::Checkbox("Biome Debug Colors", &showBiomeDebugColors); ImGui::Checkbox("Noclip mode", &player.noclip); ImGui::Checkbox("Async Loading", &useAsyncLoading); ImGui::SliderFloat("Move Speed", &cameraSpeed, 0.0f, 60.0f); @@ -165,20 +183,6 @@ void drawDebugUI( ImGui::Begin("Fun Bullshit", nullptr, debugFlags); - if (ImGui::Button("Randomize Block Textures")) - { - randomizeBlockTextures(); - for (auto& pair : chunkManager->chunks) - pair.second->dirtyMesh = true; - } - - if (ImGui::Button("Reset Block Textures")) - { - resetBlockTextures(); - for (auto& pair : chunkManager->chunks) - pair.second->dirtyMesh = true; - } - ImGui::Separator(); ImGui::Text("VISUAL CHAOS"); diff --git a/src/utils/BlockTypes.cpp b/src/utils/BlockTypes.cpp index eff594c..044c71b 100644 --- a/src/utils/BlockTypes.cpp +++ b/src/utils/BlockTypes.cpp @@ -1,9 +1,34 @@ #include "BlockTypes.h" -#include std::array g_blockTypes; std::array g_defaultBlockTypes; +static void setAllFaces(BlockType& b, int tex) +{ + for (int i = 0; i < 6; i++) + { + b.faceTexture[i] = tex; + b.faceRotation[i] = 0; + b.faceTint[i] = false; + } +} + +static void setSolidOpaque(BlockType& b) +{ + b.solid = true; + b.transparent = false; + b.connectsToSame = false; + b.isLiquid = false; +} + +static void setSideRotations(BlockType& b) +{ + b.faceRotation[0] = 1; + b.faceRotation[1] = 1; + b.faceRotation[4] = 2; + b.faceRotation[5] = 2; +} + void initBlockTypes() { for (auto &block : g_blockTypes) @@ -16,122 +41,94 @@ void initBlockTypes() { block.faceTexture[i] = 0; block.faceRotation[i] = 0; + block.faceTint[i] = false; } } - // Block 0 is air + { + BlockType& b = g_blockTypes[1]; + setSolidOpaque(b); + setAllFaces(b, TEX_DIRT); + setSideRotations(b); + } - g_blockTypes[1].solid = true; - g_blockTypes[1].transparent = false; - for (int i = 0; i < 6; i++) { - g_blockTypes[1].faceTexture[i] = 229; // tile 2 = dirt - g_blockTypes[1].faceRotation[i] = 0; - } - g_blockTypes[1].faceRotation[0] = 1; // +X: 180 deg - g_blockTypes[1].faceRotation[1] = 1; // -X: 180 deg - g_blockTypes[1].faceRotation[4] = 2; // +Z: 180 deg - g_blockTypes[1].faceRotation[5] = 2; // -Z: 180 deg - - // Block 2: Grass (different top/side/bottom) - g_blockTypes[2].solid = true; - g_blockTypes[2].transparent = false; - g_blockTypes[2].faceTexture[0] = 78; // +X side (grass side) - g_blockTypes[2].faceTexture[1] = 78; // -X side - g_blockTypes[2].faceTexture[2] = 174; // +Y top (grass top) - g_blockTypes[2].faceTexture[3] = 229; // -Y bottom (dirt) - g_blockTypes[2].faceTexture[4] = 78; // +Z side - g_blockTypes[2].faceTexture[5] = 78; // -Z side - // Flip all side faces upside down (180 deg) - g_blockTypes[2].faceRotation[0] = 1; // +X: 180 deg - g_blockTypes[2].faceRotation[1] = 1; // -X: 180 deg - g_blockTypes[2].faceRotation[4] = 2; // +Z: 180 deg - g_blockTypes[2].faceRotation[5] = 2; // -Z: 180 deg - - // Block 3: Stone - g_blockTypes[3].solid = true; - g_blockTypes[3].transparent = false; - for (int i = 0; i < 6; i++) + BlockType& b = g_blockTypes[2]; + setSolidOpaque(b); + b.faceTexture[0] = TEX_GRASS_SIDE; + b.faceTexture[1] = TEX_GRASS_SIDE; + b.faceTexture[2] = TEX_GRASS_TOP; + b.faceTexture[3] = TEX_DIRT; + b.faceTexture[4] = TEX_GRASS_SIDE; + b.faceTexture[5] = TEX_GRASS_SIDE; + setSideRotations(b); + b.faceTint[0] = true; + b.faceTint[1] = true; + b.faceTint[2] = true; + b.faceTint[3] = false; + b.faceTint[4] = true; + b.faceTint[5] = true; + } + { - g_blockTypes[3].faceTexture[i] = 72; // tile 1 = stone - g_blockTypes[3].faceRotation[i] = 0; + BlockType& b = g_blockTypes[3]; + setSolidOpaque(b); + setAllFaces(b, TEX_STONE); + setSideRotations(b); } - g_blockTypes[3].faceRotation[0] = 1; // +X: 180 deg - g_blockTypes[3].faceRotation[1] = 1; // -X: 180 deg - g_blockTypes[3].faceRotation[4] = 2; // +Z: 180 deg - g_blockTypes[3].faceRotation[5] = 2; // -Z: 180 deg - // Block 4: Sand - g_blockTypes[4].solid = true; - g_blockTypes[4].transparent = false; - for (int i = 0; i < 6; i++) { - g_blockTypes[4].faceTexture[i] = 480; // sand (row 0, around column 18) - g_blockTypes[4].faceRotation[i] = 0; - } - g_blockTypes[4].faceRotation[0] = 1; // +X: 180 deg - g_blockTypes[4].faceRotation[1] = 1; // -X: 180 deg - g_blockTypes[4].faceRotation[4] = 2; // +Z: 180 deg - g_blockTypes[4].faceRotation[5] = 2; // -Z: 180 deg - - // Block 5: Oak Log (different top/bottom vs sides) - g_blockTypes[5].solid = true; - g_blockTypes[5].transparent = false; - g_blockTypes[5].faceTexture[0] = 330; // +X side (bark) - g_blockTypes[5].faceTexture[1] = 330; // -X side (bark) - g_blockTypes[5].faceTexture[2] = 329; // +Y top (log top) - g_blockTypes[5].faceTexture[3] = 329; // -Y bottom (log top) - g_blockTypes[5].faceTexture[4] = 330; // +Z side (bark) - g_blockTypes[5].faceTexture[5] = 330; // -Z side (bark) - g_blockTypes[5].faceRotation[0] = 1; - g_blockTypes[5].faceRotation[1] = 1; - g_blockTypes[5].faceRotation[4] = 2; - g_blockTypes[5].faceRotation[5] = 2; - - // Block 6: Oak Leaves (transparent - don't cull faces) - g_blockTypes[6].solid = true; - g_blockTypes[6].transparent = true; - for (int i = 0; i < 6; i++) + BlockType& b = g_blockTypes[4]; + setSolidOpaque(b); + setAllFaces(b, TEX_SAND); + setSideRotations(b); + } + { - g_blockTypes[6].faceTexture[i] = 328; - g_blockTypes[6].faceRotation[i] = 0; + BlockType& b = g_blockTypes[5]; + setSolidOpaque(b); + setAllFaces(b, TEX_LOG_OAK); + b.faceTexture[2] = TEX_LOG_OAK_TOP; + b.faceTexture[3] = TEX_LOG_OAK_TOP; + setSideRotations(b); } - // Block 7: Glass (transparent for light, but connects to same type like Minecraft) - g_blockTypes[7].solid = true; - g_blockTypes[7].transparent = true; - g_blockTypes[7].connectsToSame = true; - for (int i = 0; i < 6; i++) { - g_blockTypes[7].faceTexture[i] = 205; - g_blockTypes[7].faceRotation[i] = 0; + BlockType& b = g_blockTypes[6]; + b.solid = true; + b.transparent = true; + b.connectsToSame = false; + b.isLiquid = false; + setAllFaces(b, TEX_LEAVES_OAK); + for (int i = 0; i < 6; i++) + b.faceTint[i] = true; } - // Block 8: Oak Planks - g_blockTypes[8].solid = true; - g_blockTypes[8].transparent = false; - for (int i = 0; i < 6; i++) { - g_blockTypes[8].faceTexture[i] = 392; - g_blockTypes[8].faceRotation[i] = 0; + BlockType& b = g_blockTypes[7]; + b.solid = true; + b.transparent = true; + b.connectsToSame = true; + b.isLiquid = false; + setAllFaces(b, TEX_GLASS); + } + + { + BlockType& b = g_blockTypes[8]; + setSolidOpaque(b); + setAllFaces(b, TEX_PLANKS_OAK); + setSideRotations(b); } - g_blockTypes[8].faceRotation[0] = 1; - g_blockTypes[8].faceRotation[1] = 1; - g_blockTypes[8].faceRotation[4] = 2; - g_blockTypes[8].faceRotation[5] = 2; - // Block 9: Water Source - g_blockTypes[9].solid = false; - g_blockTypes[9].transparent = true; - g_blockTypes[9].connectsToSame = true; - g_blockTypes[9].isLiquid = true; - for (int i = 0; i < 6; i++) { - g_blockTypes[9].faceTexture[i] = 863; - g_blockTypes[9].faceRotation[i] = 0; + BlockType& b = g_blockTypes[9]; + b.solid = false; + b.transparent = true; + b.connectsToSame = true; + b.isLiquid = true; + setAllFaces(b, 0); } - // Blocks 10-17: Flowing Water (levels 7 down to 0) for (int level = 0; level < 8; level++) { int blockId = 10 + level; @@ -139,42 +136,63 @@ void initBlockTypes() g_blockTypes[blockId].transparent = true; g_blockTypes[blockId].connectsToSame = true; g_blockTypes[blockId].isLiquid = true; + setAllFaces(g_blockTypes[blockId], 0); + } + + { + BlockType& b = g_blockTypes[18]; + setSolidOpaque(b); + b.faceTexture[0] = TEX_GRASS_SIDE_SNOWED; + b.faceTexture[1] = TEX_GRASS_SIDE_SNOWED; + b.faceTexture[2] = TEX_SNOW; + b.faceTexture[3] = TEX_DIRT; + b.faceTexture[4] = TEX_GRASS_SIDE_SNOWED; + b.faceTexture[5] = TEX_GRASS_SIDE_SNOWED; + setSideRotations(b); for (int i = 0; i < 6; i++) - { - g_blockTypes[blockId].faceTexture[i] = 863; - g_blockTypes[blockId].faceRotation[i] = 0; - } + b.faceTint[i] = false; } - g_defaultBlockTypes = g_blockTypes; -} + { + BlockType& b = g_blockTypes[19]; + setSolidOpaque(b); + setAllFaces(b, TEX_LOG_SPRUCE); + b.faceTexture[2] = TEX_LOG_SPRUCE_TOP; + b.faceTexture[3] = TEX_LOG_SPRUCE_TOP; + setSideRotations(b); + } -void randomizeBlockTextures() -{ - static std::random_device rd; - static std::mt19937 gen(rd()); - std::uniform_int_distribution dist(0, ATLAS_TILES_X * ATLAS_TILES_Y - 1); + { + BlockType& b = g_blockTypes[20]; + setSolidOpaque(b); + setAllFaces(b, TEX_PLANKS_SPRUCE); + setSideRotations(b); + } - for (int blockId = 1; blockId < 256; blockId++) { - if (g_blockTypes[blockId].solid) - { - for (int face = 0; face < 6; face++) - { - g_blockTypes[blockId].faceTexture[face] = dist(gen); - } - } + BlockType& b = g_blockTypes[21]; + b.solid = true; + b.transparent = true; + b.connectsToSame = false; + b.isLiquid = false; + setAllFaces(b, TEX_LEAVES_SPRUCE); + for (int i = 0; i < 6; i++) + b.faceTint[i] = true; } -} -void resetBlockTextures() -{ - g_blockTypes = g_defaultBlockTypes; + { + BlockType& b = g_blockTypes[22]; + setSolidOpaque(b); + setAllFaces(b, TEX_COBBLESTONE); + setSideRotations(b); + } + + g_defaultBlockTypes = g_blockTypes; } float getBlockHardness(uint8_t blockId) { - switch (blockId) + switch (blockId) { case 1: return 1.0f; case 2: return 1.2f; @@ -184,6 +202,11 @@ float getBlockHardness(uint8_t blockId) case 6: return 0.2f; case 7: return 0.6f; case 8: return 1.0f; - default: return 1.0f; + case 18: return 1.2f; + case 19: return 1.0f; + case 20: return 1.0f; + case 21: return 0.2f; + case 22: return 2.0f; + default: return 1.0f; } } diff --git a/src/utils/BlockTypes.h b/src/utils/BlockTypes.h index 83acd41..aaa54cd 100644 --- a/src/utils/BlockTypes.h +++ b/src/utils/BlockTypes.h @@ -3,17 +3,35 @@ #include #include +enum BlockTex : int +{ + TEX_DIRT = 0, + TEX_GRASS_TOP, + TEX_GRASS_SIDE, + TEX_GRASS_SIDE_SNOWED, + TEX_STONE, + TEX_SAND, + TEX_LOG_OAK, + TEX_LOG_OAK_TOP, + TEX_LEAVES_OAK, + TEX_GLASS, + TEX_PLANKS_OAK, + TEX_COBBLESTONE, + TEX_LOG_SPRUCE, + TEX_LOG_SPRUCE_TOP, + TEX_LEAVES_SPRUCE, + TEX_PLANKS_SPRUCE, + TEX_SNOW, + TEX_COUNT +}; -constexpr int ATLAS_TILES_X = 32; // 512 / 16 = 32 tiles -constexpr int ATLAS_TILES_Y = 32; -constexpr float TILE_U = 1.0f / ATLAS_TILES_X; -constexpr float TILE_V = 1.0f / ATLAS_TILES_Y; float getBlockHardness(uint8_t blockId); struct BlockType { int faceTexture[6]; int faceRotation[6]; + bool faceTint[6]; bool solid; bool transparent; bool connectsToSame; @@ -24,8 +42,6 @@ extern std::array g_blockTypes; extern std::array g_defaultBlockTypes; void initBlockTypes(); -void randomizeBlockTextures(); -void resetBlockTextures(); inline bool isBlockSolid(uint8_t blockId) { @@ -44,14 +60,3 @@ inline bool isBlockLiquid(uint8_t blockId) if (blockId == 0) return false; return g_blockTypes[blockId].isLiquid; } - -inline glm::vec2 atlasUV(int tileIndex, float localU, float localV) -{ - int tx = tileIndex % ATLAS_TILES_X; - int ty = tileIndex / ATLAS_TILES_X; - - return glm::vec2( - (tx + localU) * TILE_U, - (ty + localV) * TILE_V - ); -} diff --git a/src/utils/JobSystem.cpp b/src/utils/JobSystem.cpp index 1fc2d45..1dedbd2 100644 --- a/src/utils/JobSystem.cpp +++ b/src/utils/JobSystem.cpp @@ -307,7 +307,8 @@ void JobSystem::processMeshJob(MeshChunkJob* job) return MAX_SKY_LIGHT; }; - buildChunkMeshOffThread(job->blocks, job->skyLight, getBlock, getSkyLight, + glm::ivec3 chunkWorldOrigin(job->cx * CHUNK_SIZE, job->cy * CHUNK_SIZE, job->cz * CHUNK_SIZE); + buildChunkMeshOffThread(job->blocks, job->skyLight, chunkWorldOrigin, getBlock, getSkyLight, job->vertices, job->indices, job->waterVertices, job->waterIndices); } diff --git a/src/world/Biome.cpp b/src/world/Biome.cpp new file mode 100644 index 0000000..2bc3d21 --- /dev/null +++ b/src/world/Biome.cpp @@ -0,0 +1,96 @@ +#include "Biome.h" + +namespace +{ + const BiomeDefinition DESERT{ + BiomeID::Desert, + 4, + 4, + TreeType::None, + 0.80f, + 0.0f + }; + + const BiomeDefinition FOREST{ + BiomeID::Forest, + 2, + 1, + TreeType::Oak, + 1.05f, + 1.60f + }; + + const BiomeDefinition TUNDRA{ + BiomeID::Tundra, + 18, + 1, + TreeType::Spruce, + 0.95f, + 0.45f + }; + + const BiomeDefinition PLAINS{ + BiomeID::Plains, + 2, + 1, + TreeType::Oak, + 1.00f, + 0.60f + }; +} + +const BiomeDefinition& getBiomeDefinition(BiomeID biome) +{ + switch (biome) + { + case BiomeID::Desert: return DESERT; + case BiomeID::Forest: return FOREST; + case BiomeID::Tundra: return TUNDRA; + case BiomeID::Plains: return PLAINS; + default: return PLAINS; + } +} + +BiomeID pickBiomeFromClimate(float temperature, float humidity) +{ + if (temperature > 0.68f && humidity < 0.45f) + { + return BiomeID::Desert; + } + + if (temperature < 0.35f) + { + return BiomeID::Tundra; + } + + if (humidity > 0.60f) + { + return BiomeID::Forest; + } + + return BiomeID::Plains; +} + +glm::vec3 getBiomeGrassTint(BiomeID biome) +{ + switch (biome) + { + case BiomeID::Desert: return glm::vec3(0.75f, 0.80f, 0.40f); + case BiomeID::Forest: return glm::vec3(0.47f, 0.74f, 0.32f); + case BiomeID::Tundra: return glm::vec3(0.55f, 0.70f, 0.55f); + case BiomeID::Plains: return glm::vec3(0.58f, 0.82f, 0.38f); + default: return glm::vec3(0.55f, 0.78f, 0.35f); + } +} + +glm::vec3 getBiomeFoliageTint(BiomeID biome) +{ + switch (biome) + { + case BiomeID::Desert: return glm::vec3(0.68f, 0.72f, 0.36f); + case BiomeID::Forest: return glm::vec3(0.38f, 0.62f, 0.24f); + case BiomeID::Tundra: return glm::vec3(0.42f, 0.56f, 0.42f); + case BiomeID::Plains: return glm::vec3(0.48f, 0.72f, 0.30f); + default: return glm::vec3(0.45f, 0.66f, 0.28f); + } +} \ No newline at end of file diff --git a/src/world/Biome.h b/src/world/Biome.h new file mode 100644 index 0000000..eae40fd --- /dev/null +++ b/src/world/Biome.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include + +enum class BiomeID : uint8_t +{ + Desert = 0, + Forest = 1, + Tundra = 2, + Plains = 3 +}; + +enum class TreeType : uint8_t +{ + None = 0, + Oak = 1, + Spruce = 2 +}; + +struct BiomeDefinition +{ + BiomeID id; + uint8_t surfaceBlock; + uint8_t fillerBlock; + TreeType treeType; + float terrainAmplitude; + float treeDensity; +}; + +const BiomeDefinition& getBiomeDefinition(BiomeID biome); +BiomeID pickBiomeFromClimate(float temperature, float humidity); +glm::vec3 getBiomeGrassTint(BiomeID biome); +glm::vec3 getBiomeFoliageTint(BiomeID biome); \ No newline at end of file diff --git a/src/world/Chunk.h b/src/world/Chunk.h index 3ef5055..7e0779e 100644 --- a/src/world/Chunk.h +++ b/src/world/Chunk.h @@ -20,6 +20,7 @@ struct Chunk bool dirtyMesh = true; bool dirtyLight = true; + bool dirtyData = false; GLuint vao = 0, vbo = 0, ebo = 0; uint32_t indexCount = 0; uint32_t vertexCount = 0; diff --git a/src/world/ChunkManager.cpp b/src/world/ChunkManager.cpp index d5293a2..0e3ed6b 100644 --- a/src/world/ChunkManager.cpp +++ b/src/world/ChunkManager.cpp @@ -86,7 +86,7 @@ void ChunkManager::unloadChunk(int cx, int cy, int cz) if (it != chunks.end()) { - if (regionManager) + if (regionManager && it->second->dirtyData) { regionManager->saveChunkData(cx, cy, cz, it->second->blocks); } @@ -130,7 +130,7 @@ void ChunkManager::enqueueSaveAndUnload(int cx, int cy, int cz) if (!chunk) return; - if (jobSystem && regionManager) + if (jobSystem && regionManager && chunk->dirtyData) { savingChunks.insert(key); diff --git a/src/world/RegionManager.cpp b/src/world/RegionManager.cpp index 0825210..09b6076 100644 --- a/src/world/RegionManager.cpp +++ b/src/world/RegionManager.cpp @@ -126,7 +126,16 @@ void RegionFile::saveColumn(int localX, int localZ, const ColumnData& data) totalSize += 1 + 4 + static_cast(section.compressedBlocks.size()); } - uint32_t offset = allocateSectors(totalSize); + int idx = getEntryIndex(localX, localZ); + uint32_t offset = 0; + if (header[idx].offset != 0 && header[idx].size >= totalSize) + { + offset = header[idx].offset; + } + else + { + offset = allocateSectors(totalSize); + } file.seekp(offset, std::ios::beg); @@ -143,7 +152,6 @@ void RegionFile::saveColumn(int localX, int localZ, const ColumnData& data) file.write(reinterpret_cast(section.compressedBlocks.data()), compressedSize); } - int idx = getEntryIndex(localX, localZ); header[idx].offset = offset; header[idx].size = totalSize; headerDirty = true; @@ -359,12 +367,19 @@ void RegionManager::saveChunkData(int cx, int cy, int cz, const BlockID* blocks) ColumnData columnData; region->loadColumn(localX, localZ, columnData); + std::vector compressedBlocks; + compressBlocks(blocks, compressedBlocks); + if (compressedBlocks.empty()) + return; + bool found = false; for (auto& section : columnData.sections) { if (section.y == static_cast(cy)) { - compressBlocks(blocks, section.compressedBlocks); + if (section.compressedBlocks == compressedBlocks) + return; + section.compressedBlocks = std::move(compressedBlocks); found = true; break; } @@ -374,7 +389,7 @@ void RegionManager::saveChunkData(int cx, int cy, int cz, const BlockID* blocks) { SectionData newSection; newSection.y = static_cast(cy); - compressBlocks(blocks, newSection.compressedBlocks); + newSection.compressedBlocks = std::move(compressedBlocks); columnData.sections.push_back(std::move(newSection)); std::sort(columnData.sections.begin(), columnData.sections.end(), diff --git a/src/world/TerrainGenerator.cpp b/src/world/TerrainGenerator.cpp index 6b84052..120a1cc 100644 --- a/src/world/TerrainGenerator.cpp +++ b/src/world/TerrainGenerator.cpp @@ -1,4 +1,5 @@ #include "TerrainGenerator.h" +#include "Biome.h" #include "../thirdparty/PerlinNoise.hpp" #include #include @@ -6,7 +7,8 @@ static siv::PerlinNoise::seed_type TERRAIN_SEED = 6767420; static siv::PerlinNoise perlin{TERRAIN_SEED}; static siv::PerlinNoise perlinDetail{TERRAIN_SEED + 1}; -static siv::PerlinNoise perlinTrees{TERRAIN_SEED + 2}; +static siv::PerlinNoise perlinBiomeTemp{TERRAIN_SEED + 3}; +static siv::PerlinNoise perlinBiomeHumidity{TERRAIN_SEED + 4}; constexpr int BASE_HEIGHT = 100; constexpr int HEIGHT_VARIATION = 40; @@ -19,9 +21,12 @@ constexpr uint8_t BLOCK_DIRT = 1; constexpr uint8_t BLOCK_GRASS = 2; constexpr uint8_t BLOCK_STONE = 3; constexpr uint8_t BLOCK_SAND = 4; -constexpr uint8_t BLOCK_LOG = 5; -constexpr uint8_t BLOCK_LEAVES = 6; +constexpr uint8_t BLOCK_OAK_LOG = 5; +constexpr uint8_t BLOCK_OAK_LEAVES = 6; constexpr uint8_t BLOCK_WATER = 9; +constexpr uint8_t BLOCK_SNOW_GRASS = 18; +constexpr uint8_t BLOCK_SPRUCE_LOG = 19; +constexpr uint8_t BLOCK_SPRUCE_LEAVES = 21; constexpr int SEA_LEVEL = 116; @@ -29,19 +34,27 @@ constexpr int TREE_GRID_SIZE = 7; constexpr int TREE_OFFSET_RANGE = 10; constexpr float TREE_SPAWN_CHANCE = 0.2f; -static bool shouldPlaceTree(int worldX, int worldZ) +static bool shouldPlaceTree(int worldX, int worldZ, float chance) { + if (chance <= 0.0f) + return false; + + if (chance > 1.0f) + chance = 1.0f; + int cellX = worldX >= 0 ? worldX / TREE_GRID_SIZE : (worldX - TREE_GRID_SIZE + 1) / TREE_GRID_SIZE; int cellZ = worldZ >= 0 ? worldZ / TREE_GRID_SIZE : (worldZ - TREE_GRID_SIZE + 1) / TREE_GRID_SIZE; + unsigned int seedHash = static_cast(TERRAIN_SEED) * 83492791u; unsigned int cellHash = static_cast(cellX * 73856093) ^ - static_cast(cellZ * 19349663); + static_cast(cellZ * 19349663) ^ + seedHash; float spawnChance = (cellHash % 10000) / 10000.0f; - if (spawnChance >= TREE_SPAWN_CHANCE) + if (spawnChance >= chance) return false; - unsigned int offsetHash = cellHash * 31337; + unsigned int offsetHash = cellHash * 31337u; int offsetX = static_cast(offsetHash % TREE_OFFSET_RANGE); int offsetZ = static_cast((offsetHash / TREE_OFFSET_RANGE) % TREE_OFFSET_RANGE); @@ -51,7 +64,47 @@ static bool shouldPlaceTree(int worldX, int worldZ) return worldX == treePosX && worldZ == treePosZ; } -static double getTerrainHeight(float worldX, float worldZ) +static BiomeID sampleBiome(float worldX, float worldZ) +{ + double temperature = perlinBiomeTemp.octave2D_01( + worldX * 0.0015, + worldZ * 0.0015, + 3, + 0.5 + ); + + double humidity = perlinBiomeHumidity.octave2D_01( + worldX * 0.0015, + worldZ * 0.0015, + 3, + 0.5 + ); + + return pickBiomeFromClimate( + static_cast(temperature), + static_cast(humidity) + ); +} + +static float sampleTerrainAmplitude(float worldX, float worldZ) +{ + constexpr float SAMPLE_OFFSET = 12.0f; + BiomeID b0 = sampleBiome(worldX, worldZ); + BiomeID b1 = sampleBiome(worldX + SAMPLE_OFFSET, worldZ); + BiomeID b2 = sampleBiome(worldX - SAMPLE_OFFSET, worldZ); + BiomeID b3 = sampleBiome(worldX, worldZ + SAMPLE_OFFSET); + BiomeID b4 = sampleBiome(worldX, worldZ - SAMPLE_OFFSET); + + float a0 = getBiomeDefinition(b0).terrainAmplitude; + float a1 = getBiomeDefinition(b1).terrainAmplitude; + float a2 = getBiomeDefinition(b2).terrainAmplitude; + float a3 = getBiomeDefinition(b3).terrainAmplitude; + float a4 = getBiomeDefinition(b4).terrainAmplitude; + + return a0 * 0.5f + (a1 + a2 + a3 + a4) * 0.125f; +} + +static double getTerrainHeight(float worldX, float worldZ, float terrainAmplitude) { double continentNoise = perlin.octave2D_01( worldX * 0.002, @@ -79,7 +132,7 @@ static double getTerrainHeight(float worldX, float worldZ) double blendedNoise = continentNoise * 0.4 + hillNoise * 0.5 + detailNoise * 0.1; blendedNoise = blendedNoise * blendedNoise * (3.0 - 2.0 * blendedNoise); - return BASE_HEIGHT + blendedNoise * HEIGHT_VARIATION; + return BASE_HEIGHT + blendedNoise * (HEIGHT_VARIATION * terrainAmplitude); } static void setBlockIfInChunk(BlockID* blocks, int localX, int localY, int localZ, uint8_t blockId, bool overwriteSolid = false) @@ -109,7 +162,26 @@ void generateTerrain(BlockID* blocks, int cx, int cy, int cz) float worldX = static_cast(worldOffsetX + x); float worldZ = static_cast(worldOffsetZ + z); - int terrainHeight = static_cast(std::round(getTerrainHeight(worldX, worldZ))); + BiomeID biomeId = sampleBiome(worldX, worldZ); + const BiomeDefinition& biome = getBiomeDefinition(biomeId); + float terrainAmplitude = sampleTerrainAmplitude(worldX, worldZ); + int terrainHeight = static_cast(std::round(getTerrainHeight(worldX, worldZ, terrainAmplitude))); + uint8_t surfaceBlock = biome.surfaceBlock; + uint8_t fillerBlock = biome.fillerBlock; + if (terrainHeight < SEA_LEVEL) + { + surfaceBlock = biome.fillerBlock; + } + + int beachMaxHeight = biomeId == BiomeID::Plains ? SEA_LEVEL : SEA_LEVEL + 1; + bool isBeachColumn = biomeId != BiomeID::Desert && + terrainHeight >= SEA_LEVEL - 1 && + terrainHeight <= beachMaxHeight; + if (isBeachColumn) + { + surfaceBlock = BLOCK_SAND; + fillerBlock = BLOCK_SAND; + } for (int y = 0; y < CHUNK_SIZE; y++) { @@ -129,29 +201,11 @@ void generateTerrain(BlockID* blocks, int cx, int cy, int cz) } else if (worldY == terrainHeight) { - if (terrainHeight <= SEA_LEVEL + 2 && terrainHeight >= SEA_LEVEL - 3) - { - blocks[i] = BLOCK_SAND; - } - else if (terrainHeight > SEA_LEVEL) - { - blocks[i] = BLOCK_GRASS; - } - else - { - blocks[i] = BLOCK_SAND; - } + blocks[i] = surfaceBlock; } else if (worldY > terrainHeight - DIRT_DEPTH) { - if (terrainHeight <= SEA_LEVEL + 2) - { - blocks[i] = BLOCK_SAND; - } - else - { - blocks[i] = BLOCK_DIRT; - } + blocks[i] = fillerBlock; } else { @@ -168,26 +222,47 @@ void generateTerrain(BlockID* blocks, int cx, int cy, int cz) int worldX = worldOffsetX + x; int worldZ = worldOffsetZ + z; - if (!shouldPlaceTree(worldX, worldZ)) + BiomeID biomeId = sampleBiome(static_cast(worldX), static_cast(worldZ)); + const BiomeDefinition& biome = getBiomeDefinition(biomeId); + + if (biome.treeType == TreeType::None) continue; + if (!shouldPlaceTree(worldX, worldZ, TREE_SPAWN_CHANCE * biome.treeDensity)) + continue; + + float terrainAmplitude = sampleTerrainAmplitude( + static_cast(worldX), static_cast(worldZ)); int terrainHeight = static_cast(std::round(getTerrainHeight( - static_cast(worldX), static_cast(worldZ)))); + static_cast(worldX), static_cast(worldZ), terrainAmplitude))); if (terrainHeight <= SEA_LEVEL + 2) continue; + uint8_t logBlock = BLOCK_OAK_LOG; + uint8_t leafBlock = BLOCK_OAK_LEAVES; + if (biome.treeType == TreeType::Spruce) + { + logBlock = BLOCK_SPRUCE_LOG; + leafBlock = BLOCK_SPRUCE_LEAVES; + } + int treeBaseY = terrainHeight + 1; + int trunkHeight = TREE_TRUNK_HEIGHT; + if (biome.treeType == TreeType::Spruce) + { + trunkHeight = TREE_TRUNK_HEIGHT + 1; + } - for (int ty = 0; ty < TREE_TRUNK_HEIGHT; ty++) + for (int ty = 0; ty < trunkHeight; ty++) { int localX = x; int localY = treeBaseY + ty - worldOffsetY; int localZ = z; - setBlockIfInChunk(blocks, localX, localY, localZ, BLOCK_LOG, true); + setBlockIfInChunk(blocks, localX, localY, localZ, logBlock, true); } - int leafCenterY = treeBaseY + TREE_TRUNK_HEIGHT - 1; + int leafCenterY = treeBaseY + trunkHeight - 1; for (int lx = -TREE_LEAF_RADIUS; lx <= TREE_LEAF_RADIUS; lx++) { for (int ly = -1; ly <= TREE_LEAF_RADIUS; ly++) @@ -204,7 +279,7 @@ void generateTerrain(BlockID* blocks, int cx, int cy, int cz) int localX = x + lx; int localY = leafCenterY + ly - worldOffsetY; int localZ = z + lz; - setBlockIfInChunk(blocks, localX, localY, localZ, BLOCK_LEAVES); + setBlockIfInChunk(blocks, localX, localY, localZ, leafBlock); } } } @@ -212,10 +287,18 @@ void generateTerrain(BlockID* blocks, int cx, int cy, int cz) } } +BiomeID getBiomeAt(int worldX, int worldZ) +{ + return sampleBiome(static_cast(worldX), static_cast(worldZ)); +} + int getTerrainHeightAt(int worldX, int worldZ) { + float terrainAmplitude = sampleTerrainAmplitude( + static_cast(worldX), static_cast(worldZ)); + return static_cast(std::round(getTerrainHeight( - static_cast(worldX), static_cast(worldZ)))); + static_cast(worldX), static_cast(worldZ), terrainAmplitude))); } void getTerrainHeightsForChunk(int cx, int cz, int* outHeights) @@ -242,5 +325,6 @@ void setWorldSeed(uint32_t seed) TERRAIN_SEED = seed; perlin.reseed(TERRAIN_SEED); perlinDetail.reseed(TERRAIN_SEED + 1); - perlinTrees.reseed(TERRAIN_SEED + 2); + perlinBiomeTemp.reseed(TERRAIN_SEED + 3); + perlinBiomeHumidity.reseed(TERRAIN_SEED + 4); } \ No newline at end of file diff --git a/src/world/TerrainGenerator.h b/src/world/TerrainGenerator.h index 1fd2aba..7218c1d 100644 --- a/src/world/TerrainGenerator.h +++ b/src/world/TerrainGenerator.h @@ -1,5 +1,6 @@ #pragma once #include "Chunk.h" +#include "Biome.h" #include uint32_t getWorldSeed(); @@ -7,6 +8,8 @@ void setWorldSeed(uint32_t seed); void generateTerrain(BlockID* blocks, int cx, int cy, int cz); +BiomeID getBiomeAt(int worldX, int worldZ); + int getTerrainHeightAt(int worldX, int worldZ); void getTerrainHeightsForChunk(int cx, int cz, int* outHeights);