summaryrefslogtreecommitdiffstats
path: root/game/client/world
diff options
context:
space:
mode:
Diffstat (limited to 'game/client/world')
-rw-r--r--game/client/world/CMakeLists.txt21
-rw-r--r--game/client/world/chunk_mesher.cc472
-rw-r--r--game/client/world/chunk_mesher.hh22
-rw-r--r--game/client/world/chunk_quad.hh45
-rw-r--r--game/client/world/chunk_renderer.cc200
-rw-r--r--game/client/world/chunk_renderer.hh12
-rw-r--r--game/client/world/chunk_vbo.hh26
-rw-r--r--game/client/world/chunk_visibility.cc89
-rw-r--r--game/client/world/chunk_visibility.hh10
-rw-r--r--game/client/world/outline.cc150
-rw-r--r--game/client/world/outline.hh20
-rw-r--r--game/client/world/player_target.cc68
-rw-r--r--game/client/world/player_target.hh22
-rw-r--r--game/client/world/skybox.cc11
-rw-r--r--game/client/world/skybox.hh15
-rw-r--r--game/client/world/voxel_anims.cc31
-rw-r--r--game/client/world/voxel_anims.hh17
-rw-r--r--game/client/world/voxel_atlas.cc183
-rw-r--r--game/client/world/voxel_atlas.hh32
-rw-r--r--game/client/world/voxel_sounds.cc86
-rw-r--r--game/client/world/voxel_sounds.hh23
21 files changed, 1555 insertions, 0 deletions
diff --git a/game/client/world/CMakeLists.txt b/game/client/world/CMakeLists.txt
new file mode 100644
index 0000000..fdf96bf
--- /dev/null
+++ b/game/client/world/CMakeLists.txt
@@ -0,0 +1,21 @@
+target_sources(vclient PRIVATE
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_mesher.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_mesher.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_quad.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_renderer.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_renderer.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_vbo.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_visibility.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/chunk_visibility.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/outline.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/outline.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/player_target.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/player_target.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/skybox.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/skybox.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_anims.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_anims.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_atlas.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_atlas.hh"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_sounds.cc"
+ "${CMAKE_CURRENT_LIST_DIR}/voxel_sounds.hh")
diff --git a/game/client/world/chunk_mesher.cc b/game/client/world/chunk_mesher.cc
new file mode 100644
index 0000000..5e70626
--- /dev/null
+++ b/game/client/world/chunk_mesher.cc
@@ -0,0 +1,472 @@
+#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<world::ChunkQuad>;
+
+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<std::int64_t>(delta[0], -1, 1);
+ delta[1] = math::clamp<std::int64_t>(delta[1], -1, 1);
+ delta[2] = math::clamp<std::int64_t>(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<world::VoxelStorage, NUM_CACHED_CPOS> m_cache;
+ std::vector<QuadBuilder> m_quads_b; // blending
+ std::vector<QuadBuilder> 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<chunk_pos::value_type>);
+ cache_chunk(m_cpos + DIR_SOUTH<chunk_pos::value_type>);
+ cache_chunk(m_cpos + DIR_EAST<chunk_pos::value_type>);
+ cache_chunk(m_cpos + DIR_WEST<chunk_pos::value_type>);
+ cache_chunk(m_cpos + DIR_DOWN<chunk_pos::value_type>);
+ cache_chunk(m_cpos + DIR_UP<chunk_pos::value_type>);
+}
+
+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<local_pos::value_type>)) {
+ vis |= world::VIS_NORTH;
+ }
+
+ if(vis_test(voxel, info, lpos + DIR_SOUTH<local_pos::value_type>)) {
+ vis |= world::VIS_SOUTH;
+ }
+
+ if(vis_test(voxel, info, lpos + DIR_EAST<local_pos::value_type>)) {
+ vis |= world::VIS_EAST;
+ }
+
+ if(vis_test(voxel, info, lpos + DIR_WEST<local_pos::value_type>)) {
+ vis |= world::VIS_WEST;
+ }
+
+ if(vis_test(voxel, info, lpos + DIR_UP<local_pos::value_type>)) {
+ vis |= world::VIS_UP;
+ }
+
+ if(vis_test(voxel, info, lpos + DIR_DOWN<local_pos::value_type>)) {
+ 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<world::ChunkMesh>(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<world::ChunkMesh>(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<std::size_t>(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<std::size_t>(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<chunk_pos, 6> neighbours = {
+ event.cpos + DIR_NORTH<chunk_pos::value_type>,
+ event.cpos + DIR_SOUTH<chunk_pos::value_type>,
+ event.cpos + DIR_EAST<chunk_pos::value_type>,
+ event.cpos + DIR_WEST<chunk_pos::value_type>,
+ event.cpos + DIR_UP<chunk_pos::value_type>,
+ event.cpos + DIR_DOWN<chunk_pos::value_type>,
+ };
+
+ globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(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<NeedsMeshingComponent>(chunk->get_entity());
+ continue;
+ }
+ }
+}
+
+static void on_chunk_update(const world::ChunkUpdateEvent& event)
+{
+ const std::array<chunk_pos, 6> neighbours = {
+ event.cpos + DIR_NORTH<chunk_pos::value_type>,
+ event.cpos + DIR_SOUTH<chunk_pos::value_type>,
+ event.cpos + DIR_EAST<chunk_pos::value_type>,
+ event.cpos + DIR_WEST<chunk_pos::value_type>,
+ event.cpos + DIR_UP<chunk_pos::value_type>,
+ event.cpos + DIR_DOWN<chunk_pos::value_type>,
+ };
+
+ globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(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<NeedsMeshingComponent>(chunk->get_entity());
+ continue;
+ }
+ }
+}
+
+static void on_voxel_set(const world::VoxelSetEvent& event)
+{
+ globals::dimension->chunks.emplace_or_replace<NeedsMeshingComponent>(event.chunk->get_entity());
+
+ std::vector<chunk_pos> 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<NeedsMeshingComponent>(chunk->get_entity());
+ continue;
+ }
+ }
+}
+
+void world::chunk_mesher::init(void)
+{
+ globals::dispatcher.sink<ChunkCreateEvent>().connect<&on_chunk_create>();
+ globals::dispatcher.sink<ChunkUpdateEvent>().connect<&on_chunk_update>();
+ globals::dispatcher.sink<VoxelSetEvent>().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<NeedsMeshingComponent>(entt::get<ChunkComponent>);
+ for(const auto [entity, chunk] : group.each()) {
+ globals::dimension->chunks.remove<NeedsMeshingComponent>(entity);
+ threading::submit<GL_MeshingTask>(entity, chunk.cpos);
+ }
+ }
+}
diff --git a/game/client/world/chunk_mesher.hh b/game/client/world/chunk_mesher.hh
new file mode 100644
index 0000000..14df019
--- /dev/null
+++ b/game/client/world/chunk_mesher.hh
@@ -0,0 +1,22 @@
+#ifndef CLIENT_CHUNK_MESHER_HH
+#define CLIENT_CHUNK_MESHER_HH 1
+#pragma once
+
+#include "client/world/chunk_vbo.hh"
+
+namespace world
+{
+struct ChunkMesh final {
+ std::vector<ChunkVBO> quad_nb;
+ std::vector<ChunkVBO> quad_b;
+};
+} // namespace world
+
+namespace world::chunk_mesher
+{
+void init(void);
+void shutdown(void);
+void update(void);
+} // namespace world::chunk_mesher
+
+#endif // CLIENT_CHUNK_MESHER_HH
diff --git a/game/client/world/chunk_quad.hh b/game/client/world/chunk_quad.hh
new file mode 100644
index 0000000..9b3ca47
--- /dev/null
+++ b/game/client/world/chunk_quad.hh
@@ -0,0 +1,45 @@
+#ifndef CLIENT_CHUNK_QUAD_HH
+#define CLIENT_CHUNK_QUAD_HH 1
+#pragma once
+
+#include "core/math/constexpr.hh"
+
+#include "shared/world/voxel_registry.hh"
+
+namespace world
+{
+// [0] XXXXXXXXYYYYYYYYZZZZZZZZWWWWHHHH
+// [1] FFFFTTTTTTTTTTTAAAAA------------
+using ChunkQuad = std::array<std::uint32_t, 2>;
+} // namespace world
+
+namespace world
+{
+constexpr inline static ChunkQuad make_chunk_quad(
+ const glm::fvec3& position, const glm::fvec2& size, voxel_facing facing, std::size_t texture, std::size_t frames)
+{
+ ChunkQuad result = {};
+ result[0] = 0x00000000;
+ result[1] = 0x00000000;
+
+ // [0] XXXXXXXXYYYYYYYYZZZZZZZZ--------
+ result[0] |= (0x000000FFU & static_cast<std::uint32_t>(position.x * 16.0f)) << 24U;
+ result[0] |= (0x000000FFU & static_cast<std::uint32_t>(position.y * 16.0f)) << 16U;
+ result[0] |= (0x000000FFU & static_cast<std::uint32_t>(position.z * 16.0f)) << 8U;
+
+ // [0] ------------------------WWWWHHHH
+ result[0] |= (0x0000000FU & static_cast<std::uint32_t>(size.x * 16.0f - 1.0f)) << 4U;
+ result[0] |= (0x0000000FU & static_cast<std::uint32_t>(size.y * 16.0f - 1.0f));
+
+ // [1] FFFF----------------------------
+ result[1] |= (0x0000000FU & static_cast<std::uint32_t>(facing)) << 28U;
+
+ // [1] ----TTTTTTTTTTTAAAAA------------
+ result[1] |= (0x000007FFU & static_cast<std::uint32_t>(texture)) << 17U;
+ result[1] |= (0x0000001FU & static_cast<std::uint32_t>(frames)) << 12U;
+
+ return result;
+}
+} // namespace world
+
+#endif // CLIENT_CHUNK_QUAD_HH
diff --git a/game/client/world/chunk_renderer.cc b/game/client/world/chunk_renderer.cc
new file mode 100644
index 0000000..0962010
--- /dev/null
+++ b/game/client/world/chunk_renderer.cc
@@ -0,0 +1,200 @@
+#include "client/pch.hh"
+
+#include "client/world/chunk_renderer.hh"
+
+#include "core/config/boolean.hh"
+#include "core/config/number.hh"
+#include "core/io/config_map.hh"
+
+#include "shared/world/chunk.hh"
+#include "shared/world/dimension.hh"
+
+#include "shared/coord.hh"
+
+#include "client/entity/camera.hh"
+#include "client/gui/settings.hh"
+#include "client/world/chunk_mesher.hh"
+#include "client/world/chunk_quad.hh"
+#include "client/world/outline.hh"
+#include "client/world/skybox.hh"
+#include "client/world/voxel_anims.hh"
+#include "client/world/voxel_atlas.hh"
+
+#include "client/game.hh"
+#include "client/globals.hh"
+#include "client/program.hh"
+#include "client/toggles.hh"
+
+// ONLY TOUCH THESE IF THE RESPECTIVE SHADER
+// VARIANT MACRO DECLARATIONS LAYOUT CHANGED AS WELL
+constexpr static unsigned int WORLD_CURVATURE = 0U;
+constexpr static unsigned int WORLD_FOG = 1U;
+
+static config::Boolean depth_sort_chunks(true);
+
+static GL_Program quad_program;
+static std::size_t u_quad_vproj_matrix;
+static std::size_t u_quad_world_position;
+static std::size_t u_quad_timings;
+static std::size_t u_quad_fog_color;
+static std::size_t u_quad_view_distance;
+static std::size_t u_quad_textures;
+static GLuint quad_vaobj;
+static GLuint quad_vbo;
+
+void world::chunk_renderer::init(void)
+{
+ globals::client_config.add_value("chunk_renderer.depth_sort_chunks", depth_sort_chunks);
+
+ settings::add_checkbox(5, depth_sort_chunks, settings_location::VIDEO, "chunk_renderer.depth_sort_chunks", false);
+
+ if(!quad_program.setup("shaders/chunk_quad.vert", "shaders/chunk_quad.frag")) {
+ spdlog::critical("chunk_renderer: quad_program: setup failed");
+ std::terminate();
+ }
+
+ u_quad_vproj_matrix = quad_program.add_uniform("u_ViewProjMatrix");
+ u_quad_world_position = quad_program.add_uniform("u_WorldPosition");
+ u_quad_timings = quad_program.add_uniform("u_Timings");
+ u_quad_fog_color = quad_program.add_uniform("u_FogColor");
+ u_quad_view_distance = quad_program.add_uniform("u_ViewDistance");
+ u_quad_textures = quad_program.add_uniform("u_Textures");
+
+ const glm::fvec3 vertices[4] = {
+ glm::fvec3(1.0f, 0.0f, 1.0f),
+ glm::fvec3(1.0f, 0.0f, 0.0f),
+ glm::fvec3(0.0f, 0.0f, 1.0f),
+ glm::fvec3(0.0f, 0.0f, 0.0f),
+ };
+
+ glGenVertexArrays(1, &quad_vaobj);
+ glBindVertexArray(quad_vaobj);
+
+ glGenBuffers(1, &quad_vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, quad_vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
+
+ glEnableVertexAttribArray(0);
+ glVertexAttribDivisor(0, 0);
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(glm::fvec3), nullptr);
+}
+
+void world::chunk_renderer::shutdown(void)
+{
+ glDeleteBuffers(1, &quad_vbo);
+ glDeleteVertexArrays(1, &quad_vaobj);
+ quad_program.destroy();
+}
+
+void world::chunk_renderer::render(void)
+{
+ glEnable(GL_DEPTH_TEST);
+ glDepthFunc(GL_LEQUAL);
+ glLineWidth(1.0f);
+
+ if(toggles::get(TOGGLE_WIREFRAME)) {
+ glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
+ } else {
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+ }
+
+ quad_program.set_variant_vert(WORLD_CURVATURE, client_game::world_curvature.get_value());
+ quad_program.set_variant_vert(WORLD_FOG, client_game::fog_mode.get_value());
+ quad_program.set_variant_frag(WORLD_FOG, client_game::fog_mode.get_value());
+
+ if(!quad_program.update()) {
+ spdlog::critical("chunk_renderer: quad_program: update failed");
+ quad_program.destroy();
+ std::terminate();
+ }
+
+ GLuint timings[3];
+ timings[0] = globals::window_frametime;
+ timings[1] = globals::window_frametime_avg;
+ timings[2] = world::voxel_anims::frame;
+
+ const auto group = globals::dimension->chunks.group<ChunkComponent>(entt::get<world::ChunkMesh>);
+
+ if(depth_sort_chunks.get_value()) {
+ // FIXME: speed! sorting every frame doesn't look
+ // like a good idea. Can we store the group elsewhere and
+ // still have all the up-to-date chunk things inside?
+ group.sort([](entt::entity ea, entt::entity eb) {
+ const auto dir_a = globals::dimension->chunks.get<ChunkComponent>(ea).cpos - entity::camera::position_chunk;
+ const auto dir_b = globals::dimension->chunks.get<ChunkComponent>(eb).cpos - entity::camera::position_chunk;
+
+ const auto da = dir_a[0] * dir_a[0] + dir_a[1] * dir_a[1] + dir_a[2] * dir_a[2];
+ const auto db = dir_b[0] * dir_b[0] + dir_b[1] * dir_b[1] + dir_b[2] * dir_b[2];
+
+ return da > db;
+ });
+ }
+
+ for(std::size_t plane_id = 0; plane_id < world::voxel_atlas::plane_count(); ++plane_id) {
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D_ARRAY, world::voxel_atlas::plane_texture(plane_id));
+
+ glBindVertexArray(quad_vaobj);
+
+ glUseProgram(quad_program.handle);
+ glUniformMatrix4fv(quad_program.uniforms[u_quad_vproj_matrix].location, 1, false, glm::value_ptr(entity::camera::matrix));
+ glUniform3uiv(quad_program.uniforms[u_quad_timings].location, 1, timings);
+ glUniform4fv(quad_program.uniforms[u_quad_fog_color].location, 1, glm::value_ptr(world::skybox::fog_color));
+ glUniform1f(quad_program.uniforms[u_quad_view_distance].location, entity::camera::view_distance.get_value() * CHUNK_SIZE);
+ glUniform1i(quad_program.uniforms[u_quad_textures].location, 0); // GL_TEXTURE0
+
+ glDisable(GL_BLEND);
+
+ glEnable(GL_CULL_FACE);
+ glCullFace(GL_BACK);
+ glFrontFace(GL_CCW);
+
+ for(const auto [entity, chunk, mesh] : group.each()) {
+ if(plane_id < mesh.quad_nb.size() && mesh.quad_nb[plane_id].handle && mesh.quad_nb[plane_id].size) {
+ const auto wpos = coord::to_fvec3(chunk.cpos - entity::camera::position_chunk);
+ glUniform3fv(quad_program.uniforms[u_quad_world_position].location, 1, glm::value_ptr(wpos));
+
+ glBindBuffer(GL_ARRAY_BUFFER, mesh.quad_nb[plane_id].handle);
+
+ glEnableVertexAttribArray(1);
+ glVertexAttribDivisor(1, 1);
+ glVertexAttribIPointer(1, 2, GL_UNSIGNED_INT, sizeof(ChunkQuad), nullptr);
+
+ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, mesh.quad_nb[plane_id].size);
+
+ globals::num_drawcalls += 1;
+ globals::num_triangles += 2 * mesh.quad_nb[plane_id].size;
+ }
+ }
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+
+ for(const auto [entity, chunk, mesh] : group.each()) {
+ if(plane_id < mesh.quad_b.size() && mesh.quad_b[plane_id].handle && mesh.quad_b[plane_id].size) {
+ const auto wpos = coord::to_fvec3(chunk.cpos - entity::camera::position_chunk);
+ glUniform3fv(quad_program.uniforms[u_quad_world_position].location, 1, glm::value_ptr(wpos));
+
+ glBindBuffer(GL_ARRAY_BUFFER, mesh.quad_b[plane_id].handle);
+
+ glEnableVertexAttribArray(1);
+ glVertexAttribDivisor(1, 1);
+ glVertexAttribIPointer(1, 2, GL_UNSIGNED_INT, sizeof(ChunkQuad), nullptr);
+
+ glDrawArraysInstanced(GL_TRIANGLE_STRIP, 0, 4, mesh.quad_b[plane_id].size);
+
+ globals::num_drawcalls += 1;
+ globals::num_triangles += 2 * mesh.quad_b[plane_id].size;
+ }
+ }
+ }
+
+ if(toggles::get(TOGGLE_CHUNK_AABB)) {
+ world::outline::prepare();
+
+ for(const auto [entity, chunk, mesh] : group.each()) {
+ const auto size = glm::fvec3(CHUNK_SIZE, CHUNK_SIZE, CHUNK_SIZE);
+ world::outline::cube(chunk.cpos, glm::fvec3(0.0f, 0.0f, 0.0f), size, 1.0f, glm::fvec4(1.0f, 1.0f, 0.0f, 1.0f));
+ }
+ }
+}
diff --git a/game/client/world/chunk_renderer.hh b/game/client/world/chunk_renderer.hh
new file mode 100644
index 0000000..fdc5eea
--- /dev/null
+++ b/game/client/world/chunk_renderer.hh
@@ -0,0 +1,12 @@
+#ifndef CLIENT_CHUNK_RENDERER_HH
+#define CLIENT_CHUNK_RENDERER_HH 1
+#pragma once
+
+namespace world::chunk_renderer
+{
+void init(void);
+void shutdown(void);
+void render(void);
+} // namespace world::chunk_renderer
+
+#endif // CLIENT_CHUNK_RENDERER_HH
diff --git a/game/client/world/chunk_vbo.hh b/game/client/world/chunk_vbo.hh
new file mode 100644
index 0000000..586c313
--- /dev/null
+++ b/game/client/world/chunk_vbo.hh
@@ -0,0 +1,26 @@
+#ifndef CLIENT_CHUNK_VBO_HH
+#define CLIENT_CHUNK_VBO_HH 1
+#pragma once
+
+namespace world
+{
+class ChunkVBO final {
+public:
+ std::size_t size;
+ GLuint handle;
+
+public:
+ inline ~ChunkVBO(void)
+ {
+ // The ChunkVBO structure is meant to be a part
+ // of the ChunkMesh component within the EnTT registry;
+ // When the registry is cleaned or a chunk is removed, components
+ // are expected to be safely disposed of so we need a destructor;
+ if(handle) {
+ glDeleteBuffers(1, &handle);
+ }
+ }
+};
+} // namespace world
+
+#endif // CLIENT_CHUNK_VBO_HH
diff --git a/game/client/world/chunk_visibility.cc b/game/client/world/chunk_visibility.cc
new file mode 100644
index 0000000..8f76755
--- /dev/null
+++ b/game/client/world/chunk_visibility.cc
@@ -0,0 +1,89 @@
+#include "client/pch.hh"
+
+#include "client/world/chunk_visibility.hh"
+
+#include "core/config/number.hh"
+#include "core/math/vectors.hh"
+
+#include "shared/world/chunk.hh"
+#include "shared/world/chunk_aabb.hh"
+#include "shared/world/dimension.hh"
+
+#include "shared/protocol.hh"
+
+#include "client/entity/camera.hh"
+
+#include "client/globals.hh"
+#include "client/session.hh"
+
+// Sending a somewhat large amount of network packets
+// can easily overwhelm both client, server and the network
+// channel created between the two. To prevent this from happening
+// we throttle the client's ever increasing itch for new chunks
+constexpr static unsigned int MAX_CHUNKS_REQUESTS_PER_FRAME = 16U;
+
+static world::ChunkAABB current_view_box;
+static world::ChunkAABB previous_view_box;
+static std::vector<chunk_pos> requests;
+
+static void update_requests(void)
+{
+ requests.clear();
+
+ for(auto cx = current_view_box.min.x; cx != current_view_box.max.x; cx += 1)
+ for(auto cy = current_view_box.min.y; cy != current_view_box.max.y; cy += 1)
+ for(auto cz = current_view_box.min.z; cz != current_view_box.max.z; cz += 1) {
+ auto cpos = chunk_pos(cx, cy, cz);
+
+ if(!globals::dimension->find_chunk(cpos)) {
+ requests.push_back(cpos);
+ }
+ }
+
+ std::sort(requests.begin(), requests.end(), [](const chunk_pos& cpos_a, const chunk_pos& cpos_b) {
+ auto da = math::distance2(cpos_a, entity::camera::position_chunk);
+ auto db = math::distance2(cpos_b, entity::camera::position_chunk);
+ return da > db;
+ });
+}
+
+void world::chunk_visibility::update_late(void)
+{
+ current_view_box.min = entity::camera::position_chunk - static_cast<chunk_pos::value_type>(entity::camera::view_distance.get_value());
+ current_view_box.max = entity::camera::position_chunk + static_cast<chunk_pos::value_type>(entity::camera::view_distance.get_value());
+
+ if(!session::is_ingame()) {
+ // This makes sure the previous view box
+ // is always different from the current one
+ previous_view_box.min = chunk_pos(INT32_MIN, INT32_MIN, INT32_MIN);
+ previous_view_box.max = chunk_pos(INT32_MAX, INT32_MAX, INT32_MAX);
+ return;
+ }
+
+ if((current_view_box.min != previous_view_box.min) || (current_view_box.max != previous_view_box.max)) {
+ update_requests();
+ }
+
+ for(unsigned int i = 0U; i < MAX_CHUNKS_REQUESTS_PER_FRAME; ++i) {
+ if(requests.empty()) {
+ // Done sending requests
+ break;
+ }
+
+ protocol::RequestChunk packet;
+ packet.cpos = requests.back();
+ protocol::send(session::peer, protocol::encode(packet));
+
+ requests.pop_back();
+ }
+
+ auto view = globals::dimension->chunks.view<ChunkComponent>();
+
+ for(const auto [entity, chunk] : view.each()) {
+ if(!current_view_box.contains(chunk.cpos)) {
+ globals::dimension->remove_chunk(entity);
+ }
+ }
+
+ previous_view_box = current_view_box;
+}
diff --git a/game/client/world/chunk_visibility.hh b/game/client/world/chunk_visibility.hh
new file mode 100644
index 0000000..a57baa1
--- /dev/null
+++ b/game/client/world/chunk_visibility.hh
@@ -0,0 +1,10 @@
+#ifndef CLIENT_CHUNK_VISIBILITY_HH
+#define CLIENT_CHUNK_VISIBILITY_HH 1
+#pragma once
+
+namespace world::chunk_visibility
+{
+void update_late(void);
+} // namespace world::chunk_visibility
+
+#endif // CLIENT_CHUNK_VISIBILITY_HH
diff --git a/game/client/world/outline.cc b/game/client/world/outline.cc
new file mode 100644
index 0000000..396d297
--- /dev/null
+++ b/game/client/world/outline.cc
@@ -0,0 +1,150 @@
+#include "client/pch.hh"
+
+#include "client/world/outline.hh"
+
+#include "core/config/boolean.hh"
+#include "core/config/number.hh"
+
+#include "shared/coord.hh"
+
+#include "client/entity/camera.hh"
+
+#include "client/const.hh"
+#include "client/game.hh"
+#include "client/program.hh"
+
+// ONLY TOUCH THESE IF THE RESPECTIVE SHADER
+// VARIANT MACRO DECLARATIONS LAYOUT CHANGED AS WELL
+constexpr static unsigned int WORLD_CURVATURE = 0U;
+
+static GL_Program program;
+static std::size_t u_vpmatrix;
+static std::size_t u_worldpos;
+static std::size_t u_viewdist;
+static std::size_t u_modulate;
+static std::size_t u_scale;
+
+static GLuint vaobj;
+static GLuint cube_vbo;
+static GLuint line_vbo;
+
+void world::outline::init(void)
+{
+ if(!program.setup("shaders/outline.vert", "shaders/outline.frag")) {
+ spdlog::critical("outline: program setup failed");
+ std::terminate();
+ }
+
+ u_vpmatrix = program.add_uniform("u_ViewProjMatrix");
+ u_worldpos = program.add_uniform("u_WorldPosition");
+ u_viewdist = program.add_uniform("u_ViewDistance");
+ u_modulate = program.add_uniform("u_Modulate");
+ u_scale = program.add_uniform("u_Scale");
+
+ const glm::fvec3 cube_vertices[24] = {
+ glm::fvec3(0.0f, 0.0f, 0.0f),
+ glm::fvec3(0.0f, 1.0f, 0.0f),
+ glm::fvec3(0.0f, 1.0f, 0.0f),
+ glm::fvec3(1.0f, 1.0f, 0.0f),
+ glm::fvec3(1.0f, 1.0f, 0.0f),
+ glm::fvec3(1.0f, 0.0f, 0.0f),
+ glm::fvec3(1.0f, 0.0f, 0.0f),
+ glm::fvec3(0.0f, 0.0f, 0.0f),
+
+ glm::fvec3(0.0f, 0.0f, 1.0f),
+ glm::fvec3(0.0f, 1.0f, 1.0f),
+ glm::fvec3(0.0f, 1.0f, 1.0f),
+ glm::fvec3(1.0f, 1.0f, 1.0f),
+ glm::fvec3(1.0f, 1.0f, 1.0f),
+ glm::fvec3(1.0f, 0.0f, 1.0f),
+ glm::fvec3(1.0f, 0.0f, 1.0f),
+ glm::fvec3(0.0f, 0.0f, 1.0f),
+
+ glm::fvec3(0.0f, 0.0f, 0.0f),
+ glm::fvec3(0.0f, 0.0f, 1.0f),
+ glm::fvec3(0.0f, 1.0f, 0.0f),
+ glm::fvec3(0.0f, 1.0f, 1.0f),
+ glm::fvec3(1.0f, 0.0f, 0.0f),
+ glm::fvec3(1.0f, 0.0f, 1.0f),
+ glm::fvec3(1.0f, 1.0f, 0.0f),
+ glm::fvec3(1.0f, 1.0f, 1.0f),
+ };
+
+ glGenBuffers(1, &cube_vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, cube_vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(cube_vertices), cube_vertices, GL_STATIC_DRAW);
+
+ const glm::fvec3 line_vertices[2] = {
+ glm::fvec3(0.0f, 0.0f, 0.0f),
+ glm::fvec3(1.0f, 1.0f, 1.0f),
+ };
+
+ glGenBuffers(1, &line_vbo);
+ glBindBuffer(GL_ARRAY_BUFFER, line_vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(line_vertices), line_vertices, GL_STATIC_DRAW);
+
+ glGenVertexArrays(1, &vaobj);
+
+ glBindVertexArray(vaobj);
+ glEnableVertexAttribArray(0);
+ glVertexAttribDivisor(0, 0);
+}
+
+void world::outline::shutdown(void)
+{
+ glDeleteVertexArrays(1, &vaobj);
+ glDeleteBuffers(1, &line_vbo);
+ glDeleteBuffers(1, &cube_vbo);
+ program.destroy();
+}
+
+void world::outline::prepare(void)
+{
+ program.set_variant_vert(WORLD_CURVATURE, client_game::world_curvature.get_value());
+
+ if(!program.update()) {
+ spdlog::critical("outline_renderer: program update failed");
+ std::terminate();
+ }
+
+ glDisable(GL_CULL_FACE);
+ glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
+
+ glUseProgram(program.handle);
+ glUniformMatrix4fv(program.uniforms[u_vpmatrix].location, 1, false, glm::value_ptr(entity::camera::matrix));
+ glUniform1f(program.uniforms[u_viewdist].location, CHUNK_SIZE * entity::camera::view_distance.get_value());
+
+ glBindVertexArray(vaobj);
+ glEnableVertexAttribArray(0);
+ glVertexAttribDivisor(0, 0);
+}
+
+void world::outline::cube(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color)
+{
+ auto patch_cpos = cpos - entity::camera::position_chunk;
+
+ glLineWidth(thickness);
+
+ glUniform3fv(program.uniforms[u_worldpos].location, 1, glm::value_ptr(coord::to_fvec3(patch_cpos, fpos)));
+ glUniform4fv(program.uniforms[u_modulate].location, 1, glm::value_ptr(color));
+ glUniform3fv(program.uniforms[u_scale].location, 1, glm::value_ptr(size));
+
+ glBindBuffer(GL_ARRAY_BUFFER, cube_vbo);
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(glm::fvec3), nullptr);
+ glDrawArrays(GL_LINES, 0, 24);
+}
+
+void world::outline::line(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color)
+{
+ auto patch_cpos = cpos - entity::camera::position_chunk;
+
+ glLineWidth(thickness);
+
+ glUniform3fv(program.uniforms[u_worldpos].location, 1, glm::value_ptr(coord::to_fvec3(patch_cpos, fpos)));
+ glUniform4fv(program.uniforms[u_modulate].location, 1, glm::value_ptr(color));
+ glUniform3fv(program.uniforms[u_scale].location, 1, glm::value_ptr(size));
+
+ glBindBuffer(GL_ARRAY_BUFFER, line_vbo);
+ glVertexAttribPointer(0, 3, GL_FLOAT, false, sizeof(glm::fvec3), nullptr);
+ glDrawArrays(GL_LINES, 0, 2);
+}
diff --git a/game/client/world/outline.hh b/game/client/world/outline.hh
new file mode 100644
index 0000000..5cef7ed
--- /dev/null
+++ b/game/client/world/outline.hh
@@ -0,0 +1,20 @@
+#ifndef CLIENT_OUTLINE_HH
+#define CLIENT_OUTLINE_HH 1
+#pragma once
+
+#include "shared/types.hh"
+
+namespace world::outline
+{
+void init(void);
+void shutdown(void);
+void prepare(void);
+} // namespace world::outline
+
+namespace world::outline
+{
+void cube(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color);
+void line(const chunk_pos& cpos, const glm::fvec3& fpos, const glm::fvec3& size, float thickness, const glm::fvec4& color);
+} // namespace world::outline
+
+#endif // CLIENT_OUTLINE_HH
diff --git a/game/client/world/player_target.cc b/game/client/world/player_target.cc
new file mode 100644
index 0000000..6da8129
--- /dev/null
+++ b/game/client/world/player_target.cc
@@ -0,0 +1,68 @@
+#include "client/pch.hh"
+
+#include "client/world/player_target.hh"
+
+#include "shared/world/dimension.hh"
+#include "shared/world/ray_dda.hh"
+
+#include "shared/coord.hh"
+
+#include "client/entity/camera.hh"
+#include "client/world/outline.hh"
+
+#include "client/game.hh"
+#include "client/globals.hh"
+#include "client/session.hh"
+
+constexpr static float MAX_REACH = 16.0f;
+
+voxel_id world::player_target::voxel;
+voxel_pos world::player_target::coord;
+voxel_pos world::player_target::normal;
+const world::VoxelInfo* world::player_target::info;
+
+void world::player_target::init(void)
+{
+ world::player_target::voxel = NULL_VOXEL_ID;
+ world::player_target::coord = voxel_pos();
+ world::player_target::normal = voxel_pos();
+ world::player_target::info = nullptr;
+}
+
+void world::player_target::update(void)
+{
+ if(session::is_ingame()) {
+ RayDDA ray(globals::dimension, entity::camera::position_chunk, entity::camera::position_local, entity::camera::direction);
+
+ do {
+ world::player_target::voxel = ray.step();
+
+ if(world::player_target::voxel != NULL_VOXEL_ID) {
+ world::player_target::coord = ray.vpos;
+ world::player_target::normal = ray.vnormal;
+ world::player_target::info = world::voxel_registry::find(world::player_target::voxel);
+ break;
+ }
+
+ world::player_target::coord = voxel_pos();
+ world::player_target::normal = voxel_pos();
+ world::player_target::info = nullptr;
+ } while(ray.distance < MAX_REACH);
+ } else {
+ world::player_target::voxel = NULL_VOXEL_ID;
+ world::player_target::coord = voxel_pos();
+ world::player_target::normal = voxel_pos();
+ world::player_target::info = nullptr;
+ }
+}
+
+void world::player_target::render(void)
+{
+ if((world::player_target::voxel != NULL_VOXEL_ID) && !client_game::hide_hud) {
+ auto cpos = coord::to_chunk(world::player_target::coord);
+ auto fpos = coord::to_local(world::player_target::coord);
+
+ world::outline::prepare();
+ world::outline::cube(cpos, glm::fvec3(fpos), glm::fvec3(1.0f), 2.0f, glm::fvec4(0.0f, 0.0f, 0.0f, 1.0f));
+ }
+}
diff --git a/game/client/world/player_target.hh b/game/client/world/player_target.hh
new file mode 100644
index 0000000..33d90fb
--- /dev/null
+++ b/game/client/world/player_target.hh
@@ -0,0 +1,22 @@
+#ifndef CLIENT_PLAYER_TARGET_HH
+#define CLIENT_PLAYER_TARGET_HH 1
+#pragma once
+
+#include "shared/world/voxel_registry.hh"
+
+namespace world::player_target
+{
+extern voxel_id voxel;
+extern voxel_pos coord;
+extern voxel_pos normal;
+extern const VoxelInfo* info;
+} // namespace world::player_target
+
+namespace world::player_target
+{
+void init(void);
+void update(void);
+void render(void);
+} // namespace world::player_target
+
+#endif // CLIENT_PLAYER_TARGET_HH
diff --git a/game/client/world/skybox.cc b/game/client/world/skybox.cc
new file mode 100644
index 0000000..5e52fa4
--- /dev/null
+++ b/game/client/world/skybox.cc
@@ -0,0 +1,11 @@
+#include "client/pch.hh"
+
+#include "client/world/skybox.hh"
+
+glm::fvec3 world::skybox::fog_color;
+
+void world::skybox::init(void)
+{
+ // https://convertingcolors.com/hex-color-B1F3FF.html
+ world::skybox::fog_color = glm::fvec3(0.690f, 0.950f, 1.000f);
+}
diff --git a/game/client/world/skybox.hh b/game/client/world/skybox.hh
new file mode 100644
index 0000000..6dd82f9
--- /dev/null
+++ b/game/client/world/skybox.hh
@@ -0,0 +1,15 @@
+#ifndef CLIENT_SKYBOX_HH
+#define CLIENT_SKYBOX_HH 1
+#pragma once
+
+namespace world::skybox
+{
+extern glm::fvec3 fog_color;
+} // namespace world::skybox
+
+namespace world::skybox
+{
+void init(void);
+} // namespace world::skybox
+
+#endif // CLIENT_SKYBOX_HH
diff --git a/game/client/world/voxel_anims.cc b/game/client/world/voxel_anims.cc
new file mode 100644
index 0000000..9e5e035
--- /dev/null
+++ b/game/client/world/voxel_anims.cc
@@ -0,0 +1,31 @@
+#include "client/pch.hh"
+
+#include "client/world/voxel_anims.hh"
+
+#include "core/config/number.hh"
+#include "core/io/config_map.hh"
+#include "core/math/constexpr.hh"
+
+#include "client/globals.hh"
+
+static config::Unsigned base_framerate(16U, 1U, 16U);
+
+std::uint64_t world::voxel_anims::nextframe = 0U;
+std::uint32_t world::voxel_anims::frame = 0U;
+
+void world::voxel_anims::init(void)
+{
+ globals::client_config.add_value("voxel_anims.base_framerate", base_framerate);
+
+ world::voxel_anims::nextframe = 0U;
+ world::voxel_anims::frame = 0U;
+}
+
+void world::voxel_anims::update(void)
+{
+ if(globals::curtime >= world::voxel_anims::nextframe) {
+ world::voxel_anims::nextframe =
+ globals::curtime + static_cast<std::uint64_t>(1000000.0 / static_cast<float>(base_framerate.get_value()));
+ world::voxel_anims::frame += 1U;
+ }
+}
diff --git a/game/client/world/voxel_anims.hh b/game/client/world/voxel_anims.hh
new file mode 100644
index 0000000..5e9c56f
--- /dev/null
+++ b/game/client/world/voxel_anims.hh
@@ -0,0 +1,17 @@
+#ifndef CLIENT_VOXEL_ANIMS_HH
+#define CLIENT_VOXEL_ANIMS_HH 1
+#pragma once
+
+namespace world::voxel_anims
+{
+extern std::uint64_t nextframe;
+extern std::uint32_t frame;
+} // namespace world::voxel_anims
+
+namespace world::voxel_anims
+{
+void init(void);
+void update(void);
+} // namespace world::voxel_anims
+
+#endif // CLIENT_VOXEL_ANIMS_HH
diff --git a/game/client/world/voxel_atlas.cc b/game/client/world/voxel_atlas.cc
new file mode 100644
index 0000000..c327deb
--- /dev/null
+++ b/game/client/world/voxel_atlas.cc
@@ -0,0 +1,183 @@
+#include "client/pch.hh"
+
+#include "client/world/voxel_atlas.hh"
+
+#include "core/math/constexpr.hh"
+#include "core/math/crc64.hh"
+#include "core/resource/image.hh"
+#include "core/resource/resource.hh"
+
+struct AtlasPlane final {
+ std::unordered_map<std::size_t, std::size_t> lookup;
+ std::vector<world::AtlasStrip> strips;
+ std::size_t layer_count_max;
+ std::size_t layer_count;
+ std::size_t plane_id;
+ GLuint gl_texture;
+};
+
+static int atlas_width;
+static int atlas_height;
+static std::size_t atlas_count;
+static std::vector<AtlasPlane> planes;
+
+// Certain animated and varied voxels just double their
+// textures (see the "default" texture part in VoxelInfoBuilder::build)
+// so there could either be six UNIQUE atlas strips or only one
+// https://crypto.stackexchange.com/questions/55162/best-way-to-hash-two-values-into-one
+static std::size_t vector_hash(const std::vector<std::string>& strings)
+{
+ std::size_t source = 0;
+ for(const std::string& str : strings)
+ source += math::crc64(str);
+ return math::crc64(&source, sizeof(source));
+}
+
+static void plane_setup(AtlasPlane& plane)
+{
+ glGenTextures(1, &plane.gl_texture);
+ glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture);
+ glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, atlas_width, atlas_height, plane.layer_count_max, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
+ glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
+}
+
+static world::AtlasStrip* plane_lookup(AtlasPlane& plane, std::size_t hash_value)
+{
+ const auto it = plane.lookup.find(hash_value);
+
+ if(it != plane.lookup.cend()) {
+ return &plane.strips[it->second];
+ }
+
+ return nullptr;
+}
+
+static world::AtlasStrip* plane_new_strip(AtlasPlane& plane, const std::vector<std::string>& paths, std::size_t hash_value)
+{
+ world::AtlasStrip strip = {};
+ strip.offset = plane.layer_count;
+ strip.plane = plane.plane_id;
+
+ glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture);
+
+ for(std::size_t i = 0; i < paths.size(); ++i) {
+ if(auto image = resource::load<Image>(paths[i].c_str(), IMAGE_LOAD_FLIP)) {
+ if((image->size.x != atlas_width) || (image->size.y != atlas_height)) {
+ spdlog::warn("atlas: {}: size mismatch", paths[i]);
+ continue;
+ }
+
+ const std::size_t offset = strip.offset + i;
+ glTexSubImage3D(
+ GL_TEXTURE_2D_ARRAY, 0, 0, 0, offset, image->size.x, image->size.y, 1, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels);
+ }
+ }
+
+ plane.layer_count += paths.size();
+
+ const std::size_t index = plane.strips.size();
+ plane.lookup.emplace(hash_value, index);
+ plane.strips.push_back(std::move(strip));
+ return &plane.strips[index];
+}
+
+void world::voxel_atlas::create(int width, int height, std::size_t count)
+{
+ GLint max_plane_layers;
+
+ atlas_width = 1 << math::log2(width);
+ atlas_height = 1 << math::log2(height);
+
+ // Clipping this at OpenGL 4.5 limit of 2048 is important due to
+ // how voxel quad meshes are packed in memory: each texture index is
+ // confined in 11 bits so having bigger atlas planes makes no sense;
+ glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_plane_layers);
+ max_plane_layers = math::clamp(max_plane_layers, 256, 2048);
+
+ for(long i = count; i > 0L; i -= max_plane_layers) {
+ AtlasPlane plane = {};
+ plane.plane_id = planes.size();
+ plane.layer_count_max = math::min<std::size_t>(max_plane_layers, i);
+ plane.layer_count = 0;
+
+ const std::size_t save_id = plane.plane_id;
+ planes.push_back(std::move(plane));
+ plane_setup(planes[save_id]);
+ }
+
+ spdlog::debug("voxel_atlas: count={}", count);
+ spdlog::debug("voxel_atlas: atlas_size=[{}x{}]", atlas_width, atlas_height);
+ spdlog::debug("voxel_atlas: max_plane_layers={}", max_plane_layers);
+}
+
+void world::voxel_atlas::destroy(void)
+{
+ for(const AtlasPlane& plane : planes)
+ glDeleteTextures(1, &plane.gl_texture);
+ atlas_width = 0;
+ atlas_height = 0;
+ planes.clear();
+}
+
+std::size_t world::voxel_atlas::plane_count(void)
+{
+ return planes.size();
+}
+
+GLuint world::voxel_atlas::plane_texture(std::size_t plane_id)
+{
+ if(plane_id < planes.size()) {
+ return planes[plane_id].gl_texture;
+ } else {
+ return 0;
+ }
+}
+
+void world::voxel_atlas::generate_mipmaps(void)
+{
+ for(const AtlasPlane& plane : planes) {
+ glBindTexture(GL_TEXTURE_2D_ARRAY, plane.gl_texture);
+ glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
+ }
+}
+
+world::AtlasStrip* world::voxel_atlas::find_or_load(const std::vector<std::string>& paths)
+{
+ const std::size_t hash_value = vector_hash(paths);
+
+ for(AtlasPlane& plane : planes) {
+ if(AtlasStrip* strip = plane_lookup(plane, hash_value)) {
+ return strip;
+ }
+
+ continue;
+ }
+
+ for(AtlasPlane& plane : planes) {
+ if((plane.layer_count + paths.size()) <= plane.layer_count_max) {
+ return plane_new_strip(plane, paths, hash_value);
+ }
+
+ continue;
+ }
+
+ return nullptr;
+}
+
+world::AtlasStrip* world::voxel_atlas::find(const std::vector<std::string>& paths)
+{
+ const std::size_t hash_value = vector_hash(paths);
+
+ for(AtlasPlane& plane : planes) {
+ if(AtlasStrip* strip = plane_lookup(plane, hash_value)) {
+ return strip;
+ }
+
+ continue;
+ }
+
+ return nullptr;
+}
diff --git a/game/client/world/voxel_atlas.hh b/game/client/world/voxel_atlas.hh
new file mode 100644
index 0000000..f94c6bd
--- /dev/null
+++ b/game/client/world/voxel_atlas.hh
@@ -0,0 +1,32 @@
+#ifndef CLIENT_VOXEL_ATLAS_HH
+#define CLIENT_VOXEL_ATLAS_HH 1
+#pragma once
+
+namespace world
+{
+struct AtlasStrip final {
+ std::size_t offset;
+ std::size_t plane;
+};
+} // namespace world
+
+namespace world::voxel_atlas
+{
+void create(int width, int height, std::size_t count);
+void destroy(void);
+} // namespace world::voxel_atlas
+
+namespace world::voxel_atlas
+{
+std::size_t plane_count(void);
+GLuint plane_texture(std::size_t plane_id);
+void generate_mipmaps(void);
+} // namespace world::voxel_atlas
+
+namespace world::voxel_atlas
+{
+AtlasStrip* find_or_load(const std::vector<std::string>& paths);
+AtlasStrip* find(const std::vector<std::string>& paths);
+} // namespace world::voxel_atlas
+
+#endif // CLIENT_VOXEL_ATLAS_HH
diff --git a/game/client/world/voxel_sounds.cc b/game/client/world/voxel_sounds.cc
new file mode 100644
index 0000000..235a851
--- /dev/null
+++ b/game/client/world/voxel_sounds.cc
@@ -0,0 +1,86 @@
+#include "client/pch.hh"
+
+#include "client/world/voxel_sounds.hh"
+
+#include "client/resource/sound_effect.hh"
+
+constexpr static std::size_t NUM_SURFACES = static_cast<std::size_t>(world::voxel_surface::COUNT);
+
+static std::vector<resource_ptr<SoundEffect>> footsteps_sounds[NUM_SURFACES];
+static std::mt19937_64 randomizer;
+
+static void add_footsteps_effect(world::voxel_surface surface, const char* name)
+{
+ if(auto effect = resource::load<SoundEffect>(name)) {
+ auto surface_index = static_cast<std::size_t>(surface);
+ footsteps_sounds[surface_index].push_back(effect);
+ }
+}
+
+static resource_ptr<SoundEffect> get_footsteps_effect(world::voxel_surface surface)
+{
+ auto surface_index = static_cast<std::size_t>(surface);
+
+ if(surface_index >= NUM_SURFACES) {
+ // Surface index out of range
+ return nullptr;
+ }
+
+ const auto& sounds = footsteps_sounds[surface_index];
+
+ if(sounds.empty()) {
+ // No sounds for this surface
+ return nullptr;
+ }
+
+ auto dist = std::uniform_int_distribution<std::size_t>(0, sounds.size() - 1);
+ return sounds.at(dist(randomizer));
+}
+
+void world::voxel_sounds::init(void)
+{
+ add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default1.wav");
+ add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default2.wav");
+ add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default3.wav");
+ add_footsteps_effect(voxel_surface::DEFAULT, "sounds/surface/default4.wav");
+
+ add_footsteps_effect(voxel_surface::DIRT, "sounds/surface/dirt1.wav");
+
+ add_footsteps_effect(voxel_surface::GRASS, "sounds/surface/grass1.wav");
+ add_footsteps_effect(voxel_surface::GRASS, "sounds/surface/grass2.wav");
+ add_footsteps_effect(voxel_surface::GRASS, "sounds/surface/grass3.wav");
+
+ add_footsteps_effect(voxel_surface::GRAVEL, "sounds/surface/gravel1.wav");
+
+ add_footsteps_effect(voxel_surface::SAND, "sounds/surface/sand1.wav");
+ add_footsteps_effect(voxel_surface::SAND, "sounds/surface/sand2.wav");
+
+ add_footsteps_effect(voxel_surface::WOOD, "sounds/surface/wood1.wav");
+ add_footsteps_effect(voxel_surface::WOOD, "sounds/surface/wood2.wav");
+ add_footsteps_effect(voxel_surface::WOOD, "sounds/surface/wood3.wav");
+}
+
+void world::voxel_sounds::shutdown(void)
+{
+ for(std::size_t i = 0; i < NUM_SURFACES; ++i) {
+ footsteps_sounds[i].clear();
+ }
+}
+
+resource_ptr<SoundEffect> world::voxel_sounds::get_footsteps(voxel_surface surface)
+{
+ if(auto effect = get_footsteps_effect(surface)) {
+ return effect;
+ }
+
+ if(auto effect = get_footsteps_effect(voxel_surface::DEFAULT)) {
+ return effect;
+ }
+
+ return nullptr;
+}
+
+resource_ptr<SoundEffect> world::voxel_sounds::get_placebreak(voxel_surface surface)
+{
+ return nullptr;
+}
diff --git a/game/client/world/voxel_sounds.hh b/game/client/world/voxel_sounds.hh
new file mode 100644
index 0000000..d6f860f
--- /dev/null
+++ b/game/client/world/voxel_sounds.hh
@@ -0,0 +1,23 @@
+#ifndef CLIENT_VOXEL_SOUNDS_HH
+#define CLIENT_VOXEL_SOUNDS_HH 1
+#pragma once
+
+#include "core/resource/resource.hh"
+
+#include "shared/world/voxel_registry.hh"
+
+struct SoundEffect;
+
+namespace world::voxel_sounds
+{
+void init(void);
+void shutdown(void);
+} // namespace world::voxel_sounds
+
+namespace world::voxel_sounds
+{
+resource_ptr<SoundEffect> get_footsteps(voxel_surface surface);
+resource_ptr<SoundEffect> get_placebreak(voxel_surface surface);
+} // namespace world::voxel_sounds
+
+#endif // CLIENT_VOXEL_SOUNDS_HH