feat(client-chunk): add greedy meshing, receive_chunk, and biome field

This commit is contained in:
2026-06-25 14:31:11 +08:00
parent c92e2249a4
commit df2e26e731
5 changed files with 565 additions and 5 deletions

View File

@@ -0,0 +1,521 @@
#include "Cubed/gameplay/client_chunk.hpp"
#include "Cubed/tools/cubed_assert.hpp"
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[ClientChunk::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] = 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 = ClientChunk::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
ClientChunk::ClientChunk(ClientWorld& world) : m_world(world) {}
ClientChunk::~ClientChunk() {}
ClientChunk::ClientChunk(ClientChunk&& other) noexcept
: m_dirty(other.is_dirty()), m_need_upload(other.m_need_upload.load()),
m_is_on_gen_vertex_data(other.m_is_on_gen_vertex_data.load()),
m_biome(other.m_biome.load()), m_chunk_pos(std::move(other.m_chunk_pos)),
m_world(other.m_world), m_blocks(std::move(other.m_blocks)),
m_vertex_data(std::move(other.m_vertex_data)), m_seed(other.m_seed) {}
ClientChunk& ClientChunk::operator=(ClientChunk&& other) noexcept {
// Logger::info("other Chunk pos {} {} in Chunk& Chunk::operator=(Chunk&&
// other) this {}", other.m_chunk_pos.x, other.m_chunk_pos.z,
// static_cast<const void*>(&other));
m_chunk_pos = std::move(other.m_chunk_pos);
m_blocks = std::move(other.m_blocks);
m_dirty = other.is_dirty();
m_vertex_data = std::move(other.m_vertex_data);
m_biome = other.m_biome.load();
m_is_on_gen_vertex_data = other.m_is_on_gen_vertex_data.load();
m_need_upload = other.m_need_upload.load();
m_seed = other.m_seed;
return *this;
}
int ClientChunk::index(int x, int y, int z) {
ASSERT(!(x < 0 || y < 0 || z < 0 || x >= CHUNK_SIZE || y >= WORLD_SIZE_Y ||
z >= CHUNK_SIZE));
if ((x * WORLD_SIZE_Y + y) * CHUNK_SIZE + z < 0 ||
(x * WORLD_SIZE_Y + y) * CHUNK_SIZE + z >=
CHUNK_SIZE * CHUNK_SIZE * WORLD_SIZE_Y) {
Logger::error("block pos x {} y {} z {} range error", x, y, z);
ASSERT(0);
}
return (x * WORLD_SIZE_Y + y) * CHUNK_SIZE + z;
}
int ClientChunk::index(const glm::vec3& pos) {
return ClientChunk::index(pos.x, pos.y, pos.z);
}
BiomeType ClientChunk::get_biome() const { return m_biome.load(); }
ChunkPos ClientChunk::get_chunk_pos() const { return m_chunk_pos; }
const std::vector<BlockType>& ClientChunk::get_chunk_blocks() const {
return m_blocks;
}
void ClientChunk::gen_vertex_data(
const OptionalBlockVectorArray& neighbor_block) {
if (m_is_on_gen_vertex_data.exchange(true)) {
return;
}
std::lock_guard lk(m_vertexs_data_mutex);
for (auto& data : m_vertex_data) {
data.m_vertices.clear();
}
gen_vertices(neighbor_block);
for (auto& data : m_vertex_data) {
data.update_sum();
}
m_need_upload = true;
m_is_on_gen_vertex_data = false;
}
GLuint ClientChunk::get_normal_vao() const { return m_vertex_data[0].m_vao; }
size_t ClientChunk::get_normal_vertices_sum() const {
if (m_vertex_data[0].m_sum == 0) {
Logger::warn("m_normal_vertices_sum is 0");
}
return m_vertex_data[0].m_sum.load();
}
GLuint ClientChunk::get_cross_vao() const { return m_vertex_data[1].m_vao; }
size_t ClientChunk::get_cross_vertices_sum() const {
return m_vertex_data[1].m_sum.load();
}
GLuint ClientChunk::get_normal_discard_vao() const {
return m_vertex_data[2].m_vao;
}
size_t ClientChunk::get_normal_discard_vertices_sum() const {
return m_vertex_data[2].m_sum.load();
}
GLuint ClientChunk::get_normal_blend_vao() const {
return m_vertex_data[3].m_vao;
}
size_t ClientChunk::get_normal_blend_vertices_sum() const {
return m_vertex_data[3].m_sum.load();
}
GLuint ClientChunk::get_water_vao() const { return m_vertex_data[4].m_vao; }
size_t ClientChunk::get_water_vertices_sum() const {
return m_vertex_data[4].m_sum.load();
}
void ClientChunk::upload_to_gpu() {
ASSERT(is_need_upload());
std::lock_guard lk(m_vertexs_data_mutex);
for (auto& data : m_vertex_data) {
data.upload();
}
// after fininshed it, can use
clear_dirty();
m_need_upload = false;
}
bool ClientChunk::is_dirty() const { return m_dirty.load(); }
void ClientChunk::mark_dirty() { m_dirty = true; }
void ClientChunk::clear_dirty() { m_dirty = false; }
bool ClientChunk::is_need_upload() const { return m_need_upload.load(); }
void ClientChunk::need_upload() { m_need_upload = true; }
void ClientChunk::set_chunk_block(int index, unsigned id) {
m_blocks[index] = id;
mark_dirty();
}
ChunkPos ClientChunk::chunk_pos() const { return m_chunk_pos; }
BiomeType ClientChunk::biome() const { return m_biome; }
void ClientChunk::biome(BiomeType b) { m_biome = b; }
std::vector<BlockType>& ClientChunk::blocks() { return m_blocks; }
ClientWorld& ClientChunk::world() { return m_world; }
unsigned ClientChunk::seed() const {
if (m_seed == 0) {
Logger::warn("Seed Not Generator");
}
return m_seed;
}
void ClientChunk::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 ClientChunk::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 ClientChunk::gen_cross_plane_vertices(int world_x, int world_y,
int world_z, BlockType id) {
if (!BlockManager::is_cross_plane(id)) {
Logger::warn("Block {} {} {} id {} is not cross plane", world_x,
world_y, world_z, id);
return;
}
for (int face = 0; face < 2; face++) {
for (int i = 0; i < 6; i++) {
Vertex3D vex = {
CROSS_VERTICES_POS[face][i][0] + (float)world_x * 1.0f,
CROSS_VERTICES_POS[face][i][1] + (float)world_y * 1.0f,
CROSS_VERTICES_POS[face][i][2] + (float)world_z * 1.0f,
CROSS_TEX_COORDS[face][i][0],
CROSS_TEX_COORDS[face][i][1],
static_cast<float>(BlockManager::cross_plane_index(id)),
CROSS_NORMALS[face][i][0],
CROSS_NORMALS[face][i][1],
CROSS_NORMALS[face][i][2],
BlockManager::roughness(id),
CROSS_TANGENTS[face][i][0],
CROSS_TANGENTS[face][i][1],
CROSS_TANGENTS[face][i][2]
};
m_vertex_data[1].m_vertices.emplace_back(vex);
}
}
}
void ClientChunk::receive_chunk(const ChunkDataRsp& data) {
OptionalBlockVectorArray neighbor;
for (int i = 0; i < 4; i++) {
neighbor[i] = std::nullopt;
}
if (data.chunk_blocks_size() != BLOCK_SIZE) {
Logger::error("Bad Chunk, size {}", data.chunk_blocks_size());
return;
}
m_blocks.reserve(BLOCK_SIZE);
for (const auto& b : data.chunk_blocks()) {
m_blocks.push_back(static_cast<BlockType>(b));
}
// temp neighbor block data
if (data.neighbor_blocks_1_size() == BLOCK_SIZE) {
neighbor[0] = std::vector<BlockType>();
neighbor[0]->reserve(BLOCK_SIZE);
for (const auto& b : data.chunk_blocks()) {
neighbor[0]->push_back(static_cast<BlockType>(b));
}
}
if (data.neighbor_blocks_2_size() == BLOCK_SIZE) {
neighbor[1] = std::vector<BlockType>();
neighbor[1]->reserve(BLOCK_SIZE);
for (const auto& b : data.chunk_blocks()) {
neighbor[1]->push_back(static_cast<BlockType>(b));
}
}
if (data.neighbor_blocks_3_size() == BLOCK_SIZE) {
neighbor[2] = std::vector<BlockType>();
neighbor[2]->reserve(BLOCK_SIZE);
for (const auto& b : data.chunk_blocks()) {
neighbor[2]->push_back(static_cast<BlockType>(b));
}
}
if (data.neighbor_blocks_3_size() == BLOCK_SIZE) {
neighbor[3] = std::vector<BlockType>();
neighbor[3]->reserve(BLOCK_SIZE);
for (const auto& b : data.chunk_blocks()) {
neighbor[3]->push_back(static_cast<BlockType>(b));
}
}
gen_vertex_data(neighbor);
}
} // namespace Cubed

View File

@@ -437,6 +437,7 @@ void ServerWorld::handle_chunk_req(const std::string& uuid, ChunkPos pos) {
return;
}
rsq.set_chunk_seed(it->second.seed());
rsq.set_biome_type(std::to_underlying(it->second.biome()));
auto* blocks = rsq.mutable_chunk_blocks();
auto& chunk_blocks = it->second.get_chunk_blocks();
blocks->Assign(chunk_blocks.begin(), chunk_blocks.end());

View File

@@ -10,9 +10,10 @@ message ChunkDataReq {
message ChunkDataRsp {
ChunkPos pos = 1;
uint32 chunk_seed = 2;
repeated uint32 chunk_blocks = 3 [packed=true];
repeated uint32 neighbor_blocks_1 = 4 [packed=true];
repeated uint32 neighbor_blocks_2 = 5 [packed=true];
repeated uint32 neighbor_blocks_3 = 6 [packed=true];
repeated uint32 neighbor_blocks_4 = 7 [packed=true];
int32 biome_type = 3;
repeated uint32 chunk_blocks = 4 [packed=true];
repeated uint32 neighbor_blocks_1 = 5 [packed=true];
repeated uint32 neighbor_blocks_2 = 6 [packed=true];
repeated uint32 neighbor_blocks_3 = 7 [packed=true];
repeated uint32 neighbor_blocks_4 = 8 [packed=true];
}