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.
This commit is contained in:
zhenyan121
2026-06-22 19:48:25 +08:00
committed by GitHub
parent 7ecdab08fc
commit 97993b72fe
6 changed files with 331 additions and 24 deletions

View File

@@ -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) {

View File

@@ -7,6 +7,99 @@
#include <utility>
namespace Cubed {
using OptionalBlockVectorArray =
std::array<std::optional<std::vector<BlockType>>, 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<BlockType>& 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<std::vector<BlockType>>* 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<size_t>(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) {

View File

@@ -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<GLuint>& 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<GLfloat>(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,