#include "client/pch.hh" #include "client/world/chunk_mesher.hh" #include "core/math/crc64.hh" #include "shared/world/chunk.hh" #include "shared/world/dimension.hh" #include "shared/world/voxel_registry.hh" #include "shared/coord.hh" #include "shared/threading.hh" #include "client/world/chunk_quad.hh" #include "client/world/voxel_atlas.hh" #include "client/globals.hh" #include "client/session.hh" using QuadBuilder = std::vector; using CachedChunkCoord = unsigned short; constexpr static CachedChunkCoord CPOS_ITSELF = 0x0000; constexpr static CachedChunkCoord CPOS_NORTH = 0x0001; constexpr static CachedChunkCoord CPOS_SOUTH = 0x0002; constexpr static CachedChunkCoord CPOS_EAST = 0x0003; constexpr static CachedChunkCoord CPOS_WEST = 0x0004; constexpr static CachedChunkCoord CPOS_TOP = 0x0005; constexpr static CachedChunkCoord CPOS_BOTTOM = 0x0006; constexpr static const size_t NUM_CACHED_CPOS = 7; static const CachedChunkCoord get_cached_cpos(const chunk_pos& pivot, const chunk_pos& cpos) { static const CachedChunkCoord nx[3] = { CPOS_WEST, 0, CPOS_EAST }; static const CachedChunkCoord ny[3] = { CPOS_BOTTOM, 0, CPOS_TOP }; static const CachedChunkCoord nz[3] = { CPOS_NORTH, 0, CPOS_SOUTH }; if(pivot != cpos) { chunk_pos delta = pivot - cpos; delta[0] = math::clamp(delta[0], -1, 1); delta[1] = math::clamp(delta[1], -1, 1); delta[2] = math::clamp(delta[2], -1, 1); if(delta[0]) { return nx[delta[0] + 1]; } else if(delta[1]) { return ny[delta[1] + 1]; } else { return nz[delta[2] + 1]; } } return CPOS_ITSELF; } static world::voxel_facing get_facing(world::voxel_face face, world::voxel_type type) { if(type == world::voxel_type::CROSS) { switch(face) { case world::voxel_face::CROSS_NESW: return world::voxel_facing::NESW; case world::voxel_face::CROSS_NWSE: return world::voxel_facing::NWSE; default: return world::voxel_facing::NORTH; } } switch(face) { case world::voxel_face::CUBE_NORTH: return world::voxel_facing::NORTH; case world::voxel_face::CUBE_SOUTH: return world::voxel_facing::SOUTH; case world::voxel_face::CUBE_EAST: return world::voxel_facing::EAST; case world::voxel_face::CUBE_WEST: return world::voxel_facing::WEST; case world::voxel_face::CUBE_TOP: return world::voxel_facing::UP; case world::voxel_face::CUBE_BOTTOM: return world::voxel_facing::DOWN; default: return world::voxel_facing::NORTH; } } class GL_MeshingTask final : public Task { public: explicit GL_MeshingTask(entt::entity entity, const chunk_pos& cpos); virtual ~GL_MeshingTask(void) = default; virtual void process(void) override; virtual void finalize(void) override; private: bool vis_test(voxel_id voxel, const world::VoxelInfo* info, const local_pos& lpos) const; void push_quad_a(const world::VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, world::voxel_face face); void push_quad_v( const world::VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, world::voxel_face face, std::size_t entropy); void make_cube(voxel_id voxel, const world::VoxelInfo* info, const local_pos& lpos, world::voxel_vis vis, std::size_t entropy); void cache_chunk(const chunk_pos& cpos); private: std::array m_cache; std::vector m_quads_b; // blending std::vector m_quads_s; // solid entt::entity m_entity; chunk_pos m_cpos; }; GL_MeshingTask::GL_MeshingTask(entt::entity entity, const chunk_pos& cpos) { m_entity = entity; m_cpos = cpos; cache_chunk(m_cpos); cache_chunk(m_cpos + DIR_NORTH); cache_chunk(m_cpos + DIR_SOUTH); cache_chunk(m_cpos + DIR_EAST); cache_chunk(m_cpos + DIR_WEST); cache_chunk(m_cpos + DIR_DOWN); cache_chunk(m_cpos + DIR_UP); } void GL_MeshingTask::process(void) { m_quads_b.resize(world::voxel_atlas::plane_count()); m_quads_s.resize(world::voxel_atlas::plane_count()); const auto& voxels = m_cache.at(CPOS_ITSELF); for(std::size_t i = 0; i < CHUNK_VOLUME; ++i) { if(m_status == task_status::CANCELLED) { m_quads_b.clear(); m_quads_s.clear(); return; } const auto voxel = voxels[i]; const auto lpos = coord::to_local(i); const auto info = world::voxel_registry::find(voxel); if(info == nullptr) { // Either a NULL_VOXEL_ID or something went // horribly wrong and we don't what this is continue; } world::voxel_vis vis = 0; if(vis_test(voxel, info, lpos + DIR_NORTH)) { vis |= world::VIS_NORTH; } if(vis_test(voxel, info, lpos + DIR_SOUTH)) { vis |= world::VIS_SOUTH; } if(vis_test(voxel, info, lpos + DIR_EAST)) { vis |= world::VIS_EAST; } if(vis_test(voxel, info, lpos + DIR_WEST)) { vis |= world::VIS_WEST; } if(vis_test(voxel, info, lpos + DIR_UP)) { vis |= world::VIS_UP; } if(vis_test(voxel, info, lpos + DIR_DOWN)) { vis |= world::VIS_DOWN; } const auto vpos = coord::to_voxel(m_cpos, lpos); const auto entropy_src = vpos[0] * vpos[1] * vpos[2]; const auto entropy = math::crc64(&entropy_src, sizeof(entropy_src)); // FIXME: handle different voxel types make_cube(voxel, info, lpos, vis, entropy); } } void GL_MeshingTask::finalize(void) { if(!globals::dimension || !globals::dimension->chunks.valid(m_entity)) { // We either disconnected or something // else happened that invalidated the entity return; } auto& component = globals::dimension->chunks.emplace_or_replace(m_entity); const std::size_t plane_count_nb = m_quads_s.size(); const std::size_t plane_count_b = m_quads_b.size(); bool has_no_submeshes_b = true; bool has_no_submeshes_nb = true; component.quad_nb.resize(plane_count_nb); component.quad_b.resize(plane_count_b); for(std::size_t plane = 0; plane < plane_count_nb; ++plane) { auto& builder = m_quads_s[plane]; auto& buffer = component.quad_nb[plane]; if(builder.empty()) { if(buffer.handle) { glDeleteBuffers(1, &buffer.handle); buffer.handle = 0; buffer.size = 0; } } else { if(!buffer.handle) { glGenBuffers(1, &buffer.handle); } glBindBuffer(GL_ARRAY_BUFFER, buffer.handle); glBufferData(GL_ARRAY_BUFFER, sizeof(world::ChunkQuad) * builder.size(), builder.data(), GL_STATIC_DRAW); buffer.size = builder.size(); has_no_submeshes_nb = false; } } for(std::size_t plane = 0; plane < plane_count_b; ++plane) { auto& builder = m_quads_b[plane]; auto& buffer = component.quad_b[plane]; if(builder.empty()) { if(buffer.handle) { glDeleteBuffers(1, &buffer.handle); buffer.handle = 0; buffer.size = 0; } } else { if(!buffer.handle) { glGenBuffers(1, &buffer.handle); } glBindBuffer(GL_ARRAY_BUFFER, buffer.handle); glBufferData(GL_ARRAY_BUFFER, sizeof(world::ChunkQuad) * builder.size(), builder.data(), GL_STATIC_DRAW); buffer.size = builder.size(); has_no_submeshes_b = false; } } if(has_no_submeshes_b && has_no_submeshes_nb) { globals::dimension->chunks.remove(m_entity); } } bool GL_MeshingTask::vis_test(voxel_id voxel, const world::VoxelInfo* info, const local_pos& lpos) const { const auto pvpos = coord::to_voxel(m_cpos, lpos); const auto pcpos = coord::to_chunk(pvpos); const auto plpos = coord::to_local(pvpos); const auto index = coord::to_index(plpos); const auto cached_cpos = get_cached_cpos(m_cpos, pcpos); const auto& voxels = m_cache.at(cached_cpos); const auto neighbour = voxels[index]; bool result; if(neighbour == NULL_VOXEL_ID) { result = true; } else if(neighbour == voxel) { result = false; } else if(auto neighbour_info = world::voxel_registry::find(neighbour)) { if(neighbour_info->blending != info->blending) { // Voxel types that use blending are semi-transparent; // this means they're rendered using a different setup // and they must have visible faces with opaque voxels result = neighbour_info->blending; } else { result = false; } } else { result = false; } return result; } void GL_MeshingTask::push_quad_a(const world::VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, world::voxel_face face) { const world::voxel_facing facing = get_facing(face, info->type); const world::VoxelTexture& vtex = info->textures[static_cast(face)]; if(info->blending) { m_quads_b[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset, vtex.paths.size())); } else { m_quads_s[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset, vtex.paths.size())); } } void GL_MeshingTask::push_quad_v( const world::VoxelInfo* info, const glm::fvec3& pos, const glm::fvec2& size, world::voxel_face face, std::size_t entropy) { const world::voxel_facing facing = get_facing(face, info->type); const world::VoxelTexture& vtex = info->textures[static_cast(face)]; const std::size_t entropy_mod = entropy % vtex.paths.size(); if(info->blending) { m_quads_b[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset + entropy_mod, 0)); } else { m_quads_s[vtex.cached_plane].push_back(make_chunk_quad(pos, size, facing, vtex.cached_offset + entropy_mod, 0)); } } void GL_MeshingTask::make_cube( voxel_id voxel, const world::VoxelInfo* info, const local_pos& lpos, world::voxel_vis vis, std::size_t entropy) { const glm::fvec3 fpos = glm::fvec3(lpos); const glm::fvec2 fsize = glm::fvec2(1.0f, 1.0f); if(info->animated) { if(vis & world::VIS_NORTH) { push_quad_a(info, fpos, fsize, world::voxel_face::CUBE_NORTH); } if(vis & world::VIS_SOUTH) { push_quad_a(info, fpos, fsize, world::voxel_face::CUBE_SOUTH); } if(vis & world::VIS_EAST) { push_quad_a(info, fpos, fsize, world::voxel_face::CUBE_EAST); } if(vis & world::VIS_WEST) { push_quad_a(info, fpos, fsize, world::voxel_face::CUBE_WEST); } if(vis & world::VIS_UP) { push_quad_a(info, fpos, fsize, world::voxel_face::CUBE_TOP); } if(vis & world::VIS_DOWN) { push_quad_a(info, fpos, fsize, world::voxel_face::CUBE_BOTTOM); } } else { if(vis & world::VIS_NORTH) { push_quad_v(info, fpos, fsize, world::voxel_face::CUBE_NORTH, entropy); } if(vis & world::VIS_SOUTH) { push_quad_v(info, fpos, fsize, world::voxel_face::CUBE_SOUTH, entropy); } if(vis & world::VIS_EAST) { push_quad_v(info, fpos, fsize, world::voxel_face::CUBE_EAST, entropy); } if(vis & world::VIS_WEST) { push_quad_v(info, fpos, fsize, world::voxel_face::CUBE_WEST, entropy); } if(vis & world::VIS_UP) { push_quad_v(info, fpos, fsize, world::voxel_face::CUBE_TOP, entropy); } if(vis & world::VIS_DOWN) { push_quad_v(info, fpos, fsize, world::voxel_face::CUBE_BOTTOM, entropy); } } } void GL_MeshingTask::cache_chunk(const chunk_pos& cpos) { const auto index = get_cached_cpos(m_cpos, cpos); if(const auto chunk = globals::dimension->find_chunk(cpos)) { m_cache[index] = chunk->get_voxels(); return; } } // Bogus internal flag component struct NeedsMeshingComponent final {}; static void on_chunk_create(const world::ChunkCreateEvent& event) { const std::array neighbours = { event.cpos + DIR_NORTH, event.cpos + DIR_SOUTH, event.cpos + DIR_EAST, event.cpos + DIR_WEST, event.cpos + DIR_UP, event.cpos + DIR_DOWN, }; globals::dimension->chunks.emplace_or_replace(event.chunk->get_entity()); for(const chunk_pos& cpos : neighbours) { if(const world::Chunk* chunk = globals::dimension->find_chunk(cpos)) { globals::dimension->chunks.emplace_or_replace(chunk->get_entity()); continue; } } } static void on_chunk_update(const world::ChunkUpdateEvent& event) { const std::array neighbours = { event.cpos + DIR_NORTH, event.cpos + DIR_SOUTH, event.cpos + DIR_EAST, event.cpos + DIR_WEST, event.cpos + DIR_UP, event.cpos + DIR_DOWN, }; globals::dimension->chunks.emplace_or_replace(event.chunk->get_entity()); for(const chunk_pos& cpos : neighbours) { if(const world::Chunk* chunk = globals::dimension->find_chunk(cpos)) { globals::dimension->chunks.emplace_or_replace(chunk->get_entity()); continue; } } } static void on_voxel_set(const world::VoxelSetEvent& event) { globals::dimension->chunks.emplace_or_replace(event.chunk->get_entity()); std::vector neighbours; for(int dim = 0; dim < 3; dim += 1) { chunk_pos offset = chunk_pos(0, 0, 0); offset[dim] = 1; if(event.lpos[dim] == 0) { neighbours.push_back(event.cpos - offset); continue; } if(event.lpos[dim] == (CHUNK_SIZE - 1)) { neighbours.push_back(event.cpos + offset); continue; } } for(const chunk_pos& cpos : neighbours) { if(const world::Chunk* chunk = globals::dimension->find_chunk(cpos)) { globals::dimension->chunks.emplace_or_replace(chunk->get_entity()); continue; } } } void world::chunk_mesher::init(void) { globals::dispatcher.sink().connect<&on_chunk_create>(); globals::dispatcher.sink().connect<&on_chunk_update>(); globals::dispatcher.sink().connect<&on_voxel_set>(); } void world::chunk_mesher::shutdown(void) { } void world::chunk_mesher::update(void) { if(session::is_ingame()) { const auto group = globals::dimension->chunks.group(entt::get); for(const auto [entity, chunk] : group.each()) { globals::dimension->chunks.remove(entity); threading::submit(entity, chunk.cpos); } } }