diff --git a/include/Cubed/gameplay/biome.hpp b/include/Cubed/gameplay/biome.hpp index f4af4ed..3cab7ab 100644 --- a/include/Cubed/gameplay/biome.hpp +++ b/include/Cubed/gameplay/biome.hpp @@ -1,6 +1,9 @@ #pragma once +#include "Cubed/tools/cubed_assert.hpp" + #include #include +#include #include namespace Cubed { @@ -77,4 +80,32 @@ DesertParams& desert_params(); MountainParams& mountain_params(); RiverParams& river_params(); +inline BiomeType get_biome(int id) { + using enum BiomeType; + auto to = std::to_underlying; + if (id == to(PLAIN)) { + return PLAIN; + } + if (id == to(FOREST)) { + return FOREST; + } + if (id == to(DESERT)) { + return DESERT; + } + if (id == to(MOUNTAIN)) { + return MOUNTAIN; + } + if (id == to(RIVER)) { + return RIVER; + } + if (id == to(SNOWY_PLAIN)) { + return SNOWY_PLAIN; + } + if (id == to(OCEAN)) { + return OCEAN; + } + ASSERT_MSG(false, "Unknown Biome Id"); + throw std::invalid_argument("Unknown Biome Id"); +} + } // namespace Cubed diff --git a/include/Cubed/gameplay/client_chunk.hpp b/include/Cubed/gameplay/client_chunk.hpp index c69384b..17cf169 100644 --- a/include/Cubed/gameplay/client_chunk.hpp +++ b/include/Cubed/gameplay/client_chunk.hpp @@ -4,6 +4,7 @@ #include "Cubed/gameplay/block.hpp" #include "Cubed/gameplay/chunk_pos.hpp" #include "Cubed/gameplay/vertex_data.hpp" +#include "world/chunk_data.pb.h" #include #include @@ -34,9 +35,13 @@ public: ClientChunk(ClientChunk&&) noexcept; ClientChunk& operator=(ClientChunk&&) noexcept; + static int index(int x, int y, int z); + static int index(const glm::vec3& pos); + BiomeType get_biome() const; ChunkPos get_chunk_pos() const; const std::vector& get_chunk_blocks() const; + void receive_chunk(const ChunkDataRsp& data); void gen_vertex_data(const OptionalBlockVectorArray& neighbor_block); void upload_to_gpu(); @@ -85,6 +90,7 @@ private: static constexpr int SIZE_X = CHUNK_SIZE; static constexpr int SIZE_Y = WORLD_SIZE_Y; static constexpr int SIZE_Z = CHUNK_SIZE; + static constexpr int BLOCK_SIZE = SIZE_X * SIZE_Y * SIZE_Z; static constexpr int VERTEX_DATA_SUM = 5; std::atomic m_dirty{false}; std::atomic m_need_upload{true}; diff --git a/src/gameplay/client_chunk.cpp b/src/gameplay/client_chunk.cpp index e69de29..2657cea 100644 --- a/src/gameplay/client_chunk.cpp +++ b/src/gameplay/client_chunk.cpp @@ -0,0 +1,521 @@ +#include "Cubed/gameplay/client_chunk.hpp" + +#include "Cubed/tools/cubed_assert.hpp" + +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[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>* 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(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(&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& 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& 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(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(b)); + } + // temp neighbor block data + if (data.neighbor_blocks_1_size() == BLOCK_SIZE) { + neighbor[0] = std::vector(); + neighbor[0]->reserve(BLOCK_SIZE); + for (const auto& b : data.chunk_blocks()) { + neighbor[0]->push_back(static_cast(b)); + } + } + if (data.neighbor_blocks_2_size() == BLOCK_SIZE) { + neighbor[1] = std::vector(); + neighbor[1]->reserve(BLOCK_SIZE); + for (const auto& b : data.chunk_blocks()) { + neighbor[1]->push_back(static_cast(b)); + } + } + if (data.neighbor_blocks_3_size() == BLOCK_SIZE) { + neighbor[2] = std::vector(); + neighbor[2]->reserve(BLOCK_SIZE); + for (const auto& b : data.chunk_blocks()) { + neighbor[2]->push_back(static_cast(b)); + } + } + if (data.neighbor_blocks_3_size() == BLOCK_SIZE) { + neighbor[3] = std::vector(); + neighbor[3]->reserve(BLOCK_SIZE); + for (const auto& b : data.chunk_blocks()) { + neighbor[3]->push_back(static_cast(b)); + } + } + + gen_vertex_data(neighbor); +} + +} // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/server_world.cpp b/src/gameplay/server_world.cpp index 216bf22..5e613a9 100644 --- a/src/gameplay/server_world.cpp +++ b/src/gameplay/server_world.cpp @@ -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()); diff --git a/src/proto/world/chunk_data.proto b/src/proto/world/chunk_data.proto index 2801849..a025362 100644 --- a/src/proto/world/chunk_data.proto +++ b/src/proto/world/chunk_data.proto @@ -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]; } \ No newline at end of file