From 97993b72fee39803a2c21e8d3e6adf6e3a686fc6 Mon Sep 17 00:00:00 2001 From: zhenyan121 <104683324+zhenyan121@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:48:25 +0800 Subject: [PATCH] perf:greedy meshing (#23) * feat(gameplay): implement greedy meshing for chunk generation Replace the per-face vertex generation with a greedy meshing algorithm that merges adjacent faces of the same block type into larger quads. Introduce `FaceKey` struct and helper functions (`axis_dir_to_face`, `get_block_safe`, `is_face_culled`, `choose_buf`) to support the new algorithm. Comment out the old `gen_vertices` implementation. In texture management, rename `m_pbr_texture_array` to `m_normal_texture_array` to reflect its actual usage, and set texture wrap mode to `GL_REPEAT` for both the block and normal texture arrays. * fix(primitive_data): correct back face texture coordinates and tangents * fix(texture-manager): correct texture deletion and refactor reload Make hot_reload private and add public need_reload method. Update UI to call need_reload instead of hot_reload. Fix incorrect use of glDeleteBuffers for normal texture array by using glDeleteTextures. --- include/Cubed/gameplay/chunk.hpp | 14 ++ include/Cubed/primitive_data.hpp | 24 +-- include/Cubed/texture_manager.hpp | 5 +- src/dev_panel.cpp | 2 +- src/gameplay/chunk.cpp | 290 +++++++++++++++++++++++++++++- src/texture_manager.cpp | 20 ++- 6 files changed, 331 insertions(+), 24 deletions(-) diff --git a/include/Cubed/gameplay/chunk.hpp b/include/Cubed/gameplay/chunk.hpp index 0c80c9b..c686210 100644 --- a/include/Cubed/gameplay/chunk.hpp +++ b/include/Cubed/gameplay/chunk.hpp @@ -25,6 +25,18 @@ class Chunk { private: using OptionalBlockVectorArray = std::array>, 4>; + + struct FaceKey { + BlockType block_id = 0; + int face = -1; // 0-5, used to index NORMALS/TANGENTS/TEX_COORDS + + bool valid() const { return block_id != 0; } + bool operator==(const FaceKey& o) const { + return block_id == o.block_id && face == o.face; + } + bool operator!=(const FaceKey& o) const { return !(*this == o); } + }; + static constexpr int SIZE_X = CHUNK_SIZE; static constexpr int SIZE_Y = WORLD_SIZE_Y; static constexpr int SIZE_Z = CHUNK_SIZE; @@ -66,6 +78,8 @@ private: void gen_vertices(const OptionalBlockVectorArray& neighbor_block); void gen_cross_plane_vertices(int world_x, int world_y, int world_z, BlockType id); + void emit_quad(int axis, int face_dir, int layer, int i, int j, int w, + int h, int u_axis, int v_axis, FaceKey key); public: Chunk(World& world, ChunkPos chunk_pos, bool temp_chunk = false); diff --git a/include/Cubed/primitive_data.hpp b/include/Cubed/primitive_data.hpp index 713b49b..3b8a79e 100644 --- a/include/Cubed/primitive_data.hpp +++ b/include/Cubed/primitive_data.hpp @@ -63,12 +63,12 @@ constexpr float TEX_COORDS[6][6][2] = { {0.0f, 0.0f}, // top front {0.0f, 1.0f}}, // bottom front // ===== back (z = -1) ===== - {{1.0f, 1.0f}, // bottom left - {0.0f, 1.0f}, // bottom right - {0.0f, 0.0f}, // top right - {0.0f, 0.0f}, // top right - {1.0f, 0.0f}, // top left - {1.0f, 1.0f}}, // bottom left + {{0.0f, 1.0f}, // bottom left + {0.0f, 0.0f}, // top left + {1.0f, 0.0f}, // top right + {1.0f, 0.0f}, // top right + {1.0f, 1.0f}, // bottom right + {0.0f, 1.0f}}, // bottom left // ===== left (x = -1) ===== {{1.0f, 1.0f}, // bottom back {0.0f, 1.0f}, // bottom front @@ -152,12 +152,12 @@ constexpr float TANGENTS[6][6][3] = { {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, -1.0f}}, // ===== back (z = -1) ===== - {{-1.0f, 0.0f, 0.0f}, - {-1.0f, 0.0f, 0.0f}, - {-1.0f, 0.0f, 0.0f}, - {-1.0f, 0.0f, 0.0f}, - {-1.0f, 0.0f, 0.0f}, - {-1.0f, 0.0f, 0.0f}}, + {{1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}}, // ===== left (x = -1) ===== {{0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, diff --git a/include/Cubed/texture_manager.hpp b/include/Cubed/texture_manager.hpp index eca2aba..c62043a 100644 --- a/include/Cubed/texture_manager.hpp +++ b/include/Cubed/texture_manager.hpp @@ -12,7 +12,7 @@ private: GLuint m_texture_array = 0; GLuint m_cross_plane_array = 0; GLuint m_ui_array = 0; - GLuint m_pbr_texture_array = 0; + GLuint m_normal_texture_array = 0; GLfloat m_max_aniso = 0.0f; int m_aniso = 1; @@ -29,6 +29,7 @@ private: void init_block(); void init_ui(); void init_block_status(); + void hot_reload(); public: TextureManager(); @@ -43,7 +44,7 @@ public: const std::vector& item_textures() const; // Must call after MapTable::init_map() and glfwMakeContextCurrent(window); void init_texture(); - void hot_reload(); + void need_reload(); void update(); int max_aniso() const; diff --git a/src/dev_panel.cpp b/src/dev_panel.cpp index c421a21..49feada 100644 --- a/src/dev_panel.cpp +++ b/src/dev_panel.cpp @@ -430,7 +430,7 @@ void DevPanel::show_settings_tab_item() { } if (ImGui::Button("ReloadTexture")) { Config::get().set("texture.aniso", m_config.aniso); - m_app.texture_manager().hot_reload(); + m_app.texture_manager().need_reload(); m_config.is_reload = true; } if (!m_config.is_reload) { diff --git a/src/gameplay/chunk.cpp b/src/gameplay/chunk.cpp index 9237d3a..7710e66 100644 --- a/src/gameplay/chunk.cpp +++ b/src/gameplay/chunk.cpp @@ -7,6 +7,99 @@ #include namespace Cubed { +using OptionalBlockVectorArray = + std::array>, 4>; +namespace { +// ──────────────────────────────────────────────────────────────────────────── +// Face direction mapping +// Original DIR[6]: {+Z,+X,-Z,-X,+Y,-Y} => face index 0-5 +// Axis × direction => face: +// axis=2(Z) dir=+1 => face 0 (+Z) +// axis=0(X) dir=+1 => face 1 (+X) +// axis=2(Z) dir=-1 => face 2 (-Z) +// axis=0(X) dir=-1 => face 3 (-X) +// axis=1(Y) dir=+1 => face 4 (+Y) +// axis=1(Y) dir=-1 => face 5 (-Y) +// ──────────────────────────────────────────────────────────────────────────── + +inline int axis_dir_to_face(int axis, int dir) { + // axis: 0=X 1=Y 2=Z + // dir: +1 or -1 + static const int TABLE[3][2] = { + {3, 1}, // X: dir=-1->face3(-X), dir=+1->face1(+X) + {5, 4}, // Y: dir=-1->face5(-Y), dir=+1->face4(+Y) + {2, 0}, // Z: dir=-1->face2(-Z), dir=+1->face0(+Z) + }; + return TABLE[axis][dir > 0 ? 1 : 0]; +} + +inline BlockType +get_block_safe(int lx, int ly, int lz, ChunkPos& chunk_pos, + const std::vector& blocks, + const OptionalBlockVectorArray& neighbor_block) { + if (lx >= 0 && lx < CHUNK_SIZE && ly >= 0 && ly < WORLD_SIZE_Y && lz >= 0 && + lz < CHUNK_SIZE) { + return blocks[Chunk::index(lx, ly, lz)]; + } + + // Out of bounds: check neighbors + int world_x = lx + chunk_pos.x * CHUNK_SIZE; + int world_z = lz + chunk_pos.z * CHUNK_SIZE; + + auto [nb_cx, nb_cz] = World::get_chunk_pos(world_x, world_z); + + const std::optional>* nb = nullptr; + if (nb_cx == chunk_pos.x + 1) + nb = &neighbor_block[0]; + else if (nb_cx == chunk_pos.x - 1) + nb = &neighbor_block[1]; + else if (nb_cz == chunk_pos.z + 1) + nb = &neighbor_block[2]; + else if (nb_cz == chunk_pos.z - 1) + nb = &neighbor_block[3]; + + if (!nb || !nb->has_value()) + return 0; // Neighbor does not exist, treat as opaque + + int nbx = world_x - nb_cx * CHUNK_SIZE; + int nby = ly; + int nbz = world_z - nb_cz * CHUNK_SIZE; + + if (nbx < 0 || nby < 0 || nbz < 0 || nbx >= CHUNK_SIZE || + nby >= WORLD_SIZE_Y || nbz >= CHUNK_SIZE) + return 0; + + int idx = Chunk::index(nbx, nby, nbz); + if (static_cast(idx) >= (*nb)->size()) { + return 0; + } + + return (**nb)[idx]; +} +// Determine whether the face from cur_id looking towards neighbor_id should be +// culled (does not need to be rendered) +inline bool is_face_culled(BlockType cur_id, BlockType neighbor_id) { + if (!BlockManager::is_transparent(neighbor_id)) + return true; // Neighbor is opaque, blocking + // Neighbor transparency: same block type culls each other (e.g., water + // adjacent to water does not render internal faces) + if (neighbor_id == cur_id) + return true; + return false; +} + +inline int choose_buf(BlockType id) { + if (!BlockManager::is_transparent(id)) + return 0; + if (BlockManager::is_discard(id)) + return 2; + if (BlockManager::is_blend(id)) { + return (id == 7) ? 4 : 3; // water=4, other blend=3 + } + return 3; // fallback +} + +} // namespace Chunk::Chunk(World& world, ChunkPos chunk_pos, bool temp_chunk) : m_temp_chunk(temp_chunk), m_chunk_pos(chunk_pos), m_world(world) { @@ -276,7 +369,7 @@ ChunkInfo Chunk::get_info() const { } return m_info; } - +/* void Chunk::gen_vertices(const OptionalBlockVectorArray& neighbor_block) { static const glm::ivec3 DIR[6] = {{0, 0, 1}, {1, 0, 0}, {0, 0, -1}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}}; @@ -432,6 +525,201 @@ void Chunk::gen_vertices(const OptionalBlockVectorArray& neighbor_block) { } } } +*/ +void Chunk::gen_vertices(const OptionalBlockVectorArray& neighbor_block) { + + // SIZE_X=SIZE_Z=CHUNK_SIZE=16, SIZE_Y=WORLD_SIZE_Y=256 + // Axis order: axis 0=X, 1=Y, 2=Z + // Two slice dimensions of each axis + const int DIMS[3] = {CHUNK_SIZE, WORLD_SIZE_Y, CHUNK_SIZE}; + + // Maximum mask size: max(16*256, 16*16) = 4096 + static thread_local FaceKey mask[CHUNK_SIZE * WORLD_SIZE_Y]; + static thread_local bool visited[CHUNK_SIZE * WORLD_SIZE_Y]; + + for (int axis = 0; axis < 3; axis++) { + int u_axis = (axis + 1) % 3; // horizontal + int v_axis = (axis + 2) % 3; // vertical + + int u = DIMS[u_axis]; + int v = DIMS[v_axis]; + int d = DIMS[axis]; // Depth along the normal axis + + for (int face_dir : {1, -1}) { + int face_idx = axis_dir_to_face(axis, face_dir); + + for (int layer = 0; layer < d; layer++) { + + // ── 1. Build mask ────────────────────────────────────────── + for (int vi = 0; vi < v; vi++) { + for (int ui = 0; ui < u; ui++) { + // Current cell local coordinates + int lpos[3]; + lpos[axis] = layer; + lpos[u_axis] = ui; + lpos[v_axis] = vi; + + // Neighbor (offset one cell along the normal direction) + int npos[3]; + npos[axis] = layer + face_dir; + npos[u_axis] = ui; + npos[v_axis] = vi; + + BlockType cur_id = get_block_safe( + lpos[0], lpos[1], lpos[2], m_chunk_pos, m_blocks, + neighbor_block); + + // Air / cross plane are not involved in greedy meshing + if (cur_id == 0 || + BlockManager::is_cross_plane(cur_id)) { + mask[vi * u + ui] = {}; + continue; + } + + BlockType nb_id = get_block_safe( + npos[0], npos[1], npos[2], m_chunk_pos, m_blocks, + neighbor_block); + + if (is_face_culled(cur_id, nb_id)) { + mask[vi * u + ui] = {}; + } else { + mask[vi * u + ui] = {cur_id, face_idx}; + } + } + } + + // ── 2. Greedy Merge ────────────────────────────────────── + std::fill(visited, visited + u * v, false); + + for (int vi = 0; vi < v; vi++) { + for (int ui = 0; ui < u; ui++) { + if (visited[vi * u + ui]) + continue; + FaceKey cur = mask[vi * u + ui]; + if (!cur.valid()) + continue; + + // Extend width in the u direction + int w = 1; + while (ui + w < u && !visited[vi * u + (ui + w)] && + mask[vi * u + (ui + w)] == cur) { + w++; + } + + // Extend height in the v direction + int h = 1; + bool can_expand = true; + while (vi + h < v && can_expand) { + for (int k = 0; k < w; k++) { + int idx = (vi + h) * u + (ui + k); + if (visited[idx] || mask[idx] != cur) { + can_expand = false; + break; + } + } + if (can_expand) + h++; + } + + // mark visited + for (int dv = 0; dv < h; dv++) + for (int du = 0; du < w; du++) + visited[(vi + dv) * u + (ui + du)] = true; + + // output quad + emit_quad(axis, face_dir, layer, ui, vi, w, h, u_axis, + v_axis, cur); + } + } + } + } + } + + for (int x = 0; x < CHUNK_SIZE; x++) { + for (int y = 0; y < WORLD_SIZE_Y; y++) { + for (int z = 0; z < CHUNK_SIZE; z++) { + BlockType id = m_blocks[index(x, y, z)]; + if (id != 0 && BlockManager::is_cross_plane(id)) { + int world_x = x + m_chunk_pos.x * CHUNK_SIZE; + int world_z = z + m_chunk_pos.z * CHUNK_SIZE; + gen_cross_plane_vertices(world_x, y, world_z, id); + } + } + } + } +} +void Chunk::emit_quad(int axis, int face_dir, int layer, int i, int j, int w, + int h, int u_axis, int v_axis, FaceKey key) { + float axis_val = (float)(layer + (face_dir > 0 ? 1 : 0)); + float wx_base = (float)(m_chunk_pos.x * CHUNK_SIZE); + float wz_base = (float)(m_chunk_pos.z * CHUNK_SIZE); + + // Offsets of the four corners along the u_axis/v_axis + int su[4] = {0, w, w, 0}; + int sv[4] = {0, 0, h, h}; + + // Each face's UV: directly read from the four corners of TEX_COORDS, then + // scaled by w/h TEX_COORDS vertex order: 0=BL, 1=TL, 2=TR, 3=TR, 4=BR, 5=BL + // (two triangles) Four unique corners correspond to indices: BL=0, TL=1, + // TR=2, BR=4 Extract the UVs of the four corners from TEX_COORDS (unique + // corners after removing duplicate vertices) Vertices 0,1,2,4 correspond to + // BL, TL, TR, BR + float u0 = TEX_COORDS[key.face][0][0]; // BL.u + float v0 = TEX_COORDS[key.face][0][1]; // BL.v + float u1 = TEX_COORDS[key.face][4][0]; // BR.u + float v1 = TEX_COORDS[key.face][4][1]; // BR.v + float u3 = TEX_COORDS[key.face][1][0]; // TL.u + float v3 = TEX_COORDS[key.face][1][1]; // TL.v + + float du_u = u1 - u0; // Change in u when su increases (per block) + float dv_u = v1 - v0; + float du_v = u3 - u0; // Change in u when sv increases + float dv_v = v3 - v0; + + float uvs[4][2] = { + {u0, v0}, // (0, 0 ) + {u0 + du_u * (float)w, v0 + dv_u * (float)w}, // (w, 0 ) + {u0 + du_u * (float)w + du_v * (float)h, + v0 + dv_u * (float)w + dv_v * (float)h}, // (w, h ) + {u0 + du_v * (float)h, v0 + dv_v * (float)h}, // (0, h ) + }; + + int tri[6] = {0, 1, 2, 0, 2, 3}; + + float pos[4][3]; + for (int c = 0; c < 4; c++) { + pos[c][axis] = axis_val; + pos[c][u_axis] = (float)(i + su[c]); + pos[c][v_axis] = (float)(j + sv[c]); + pos[c][0] += wx_base; + pos[c][2] += wz_base; + } + + float layer_id = (float)(key.block_id * 6 + key.face); + float roughness = BlockManager::roughness(key.block_id); + int buf = choose_buf(key.block_id); + + for (int vi = 0; vi < 6; vi++) { + int c = tri[vi]; + Vertex3D vex = { + pos[c][0], + pos[c][1], + pos[c][2], + uvs[c][0], + uvs[c][1], + layer_id, + NORMALS[key.face][0][0], + NORMALS[key.face][0][1], + NORMALS[key.face][0][2], + roughness, + TANGENTS[key.face][0][0], + TANGENTS[key.face][0][1], + TANGENTS[key.face][0][2], + }; + m_vertex_data[buf].m_vertices.emplace_back(vex); + } +} + void Chunk::gen_cross_plane_vertices(int world_x, int world_y, int world_z, BlockType id) { diff --git a/src/texture_manager.cpp b/src/texture_manager.cpp index 8dffe03..24df2db 100644 --- a/src/texture_manager.cpp +++ b/src/texture_manager.cpp @@ -39,7 +39,7 @@ void TextureManager::delet_texture() { glDeleteTextures(1, &m_texture_array); glDeleteTextures(1, &m_block_status_array); glDeleteTextures(1, &m_cross_plane_array); - glDeleteBuffers(1, &m_pbr_texture_array); + glDeleteTextures(1, &m_normal_texture_array); for (auto& id : m_item_textures) { glDeleteTextures(1, &id); } @@ -57,7 +57,9 @@ GLuint TextureManager::get_cross_plane_array() const { } GLuint TextureManager::get_ui_array() const { return m_ui_array; } -GLuint TextureManager::get_pbr_texture() const { return m_pbr_texture_array; } +GLuint TextureManager::get_pbr_texture() const { + return m_normal_texture_array; +} const std::vector& TextureManager::item_textures() const { return m_item_textures; @@ -181,7 +183,7 @@ void TextureManager::load_pbr_texture(unsigned id) { image_data[4] = (Tools::load_image_data(path + "/top_n.png", false)); image_data[5] = (Tools::load_image_data(path + "/base_n.png", false)); - glBindTexture(GL_TEXTURE_2D_ARRAY, m_pbr_texture_array); + glBindTexture(GL_TEXTURE_2D_ARRAY, m_normal_texture_array); for (int i = 0; i < 6; i++) { unsigned char* data = image_data[i]; bool is_fallback = false; @@ -213,8 +215,8 @@ void TextureManager::init_block() { glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA, CROSS_PLANE_SIZE, CROSS_PLANE_SIZE, BlockManager::cross_plane_sum(), 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); - glGenTextures(1, &m_pbr_texture_array); - glBindTexture(GL_TEXTURE_2D_ARRAY, m_pbr_texture_array); + glGenTextures(1, &m_normal_texture_array); + glBindTexture(GL_TEXTURE_2D_ARRAY, m_normal_texture_array); glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, BLOCK_NORMAL_SIZE, BLOCK_NORMAL_SIZE, BlockManager::sums() * 6, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr); @@ -228,6 +230,8 @@ void TextureManager::init_block() { glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT); glGenerateMipmap(GL_TEXTURE_2D_ARRAY); if (m_aniso >= 1) { glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY, @@ -246,12 +250,12 @@ void TextureManager::init_block() { static_cast(m_aniso)); } - glBindTexture(GL_TEXTURE_2D_ARRAY, m_pbr_texture_array); + glBindTexture(GL_TEXTURE_2D_ARRAY, m_normal_texture_array); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); - glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT); + glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT); glGenerateMipmap(GL_TEXTURE_2D_ARRAY); if (m_aniso >= 1) { glTexParameterf(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_ANISOTROPY,