diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index 0600488c138ce..e22bfc12c7395 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -472,6 +472,31 @@ viewing_range (Viewing range) int 190 20 4000 # to a non-default value. undersampling (Undersampling) int 1 1 8 +[**LOD] + +# Use lower levels of detail (LOD) for distant nodes. +enable_lod (Enable LOD) bool true + +# Distance of mapblocks, when LODs should be applied. +# +#Requires: enable_lod +lod_threshold (LOD Threshold) int 20 10 50 + +# Quality of LODs from 1 (low) to 5 (high). +# +# This setting controls the rate at which the detail is decreased. +# At 1, the quality decreases at every distance where the LOD Threshold is doubled. +# At 2, the quality decreases at every distance where the LOD Threshold is doubled twice. +# Since this setting interacts with LOD Threshold, a lower quality can still look good if the threshold is sufficiently high. +# +# Requires: enable_lod +lod_quality (LOD Quality) float 1.5 0.5 5 + +# LOD level at which nodes should be just a single color with no textures applied. +# +# Requires: enable_lod +lod_texture_threshold (LOD Texture Threshold) int 2 1 5 + [**3D] # 3D support. diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl index da5ba84070bc6..a9e7e0f5c23bd 100644 --- a/client/shaders/nodes_shader/opengl_fragment.glsl +++ b/client/shaders/nodes_shader/opengl_fragment.glsl @@ -430,7 +430,9 @@ void main(void) { vec2 uv = varTexCoord.st; -#ifdef USE_ARRAY_TEXTURE +#ifdef TEXTURELESS + vec4 base = vec4(1); // white, so color is fully controlled by vertex color +#elif USE_ARRAY_TEXTURE vec4 base = texture(baseTexture, vec3(uv, varTexLayer)).rgba; #else vec4 base = texture2D(baseTexture, uv).rgba; diff --git a/shell.nix b/shell.nix index 02d2de37cb268..0439a87f3c545 100644 --- a/shell.nix +++ b/shell.nix @@ -5,23 +5,31 @@ pkgs.mkShell { env.LANG = "C.UTF-8"; env.LC_ALL = "C.UTF-8"; - packages = [ - pkgs.gcc - pkgs.cmake - pkgs.zlib - pkgs.zstd - pkgs.libjpeg - pkgs.libpng - pkgs.libGL - pkgs.luajit - pkgs.SDL2 - pkgs.openal - pkgs.curl - pkgs.libvorbis - pkgs.libogg - pkgs.gettext - pkgs.gmp - pkgs.freetype - pkgs.sqlite + packages = with pkgs; [ + gcc + cmake + zlib + zstd + libjpeg + libpng + libGL + luajit + SDL2 + openal + curl + libvorbis + libogg + gettext + gmp + freetype + sqlite + + mangohud + python313 + python313Packages.fire + python313Packages.matplotlib + python313Packages.numpy + python313Packages.pandas + python313Packages.scipy ]; } diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index ac98d4bd8830f..390e0837b0ced 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -49,6 +49,7 @@ set(client_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/content_cao.cpp ${CMAKE_CURRENT_SOURCE_DIR}/content_cso.cpp ${CMAKE_CURRENT_SOURCE_DIR}/content_mapblock.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/lod_mapblock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/filecache.cpp ${CMAKE_CURRENT_SOURCE_DIR}/fontengine.cpp ${CMAKE_CURRENT_SOURCE_DIR}/game.cpp diff --git a/src/client/clientmap.cpp b/src/client/clientmap.cpp index 84a539fb9b69e..d00d52139544b 100644 --- a/src/client/clientmap.cpp +++ b/src/client/clientmap.cpp @@ -1044,9 +1044,7 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) // Mesh animation if (pass == scene::ESNRP_SOLID) { - // 50 nodes is pretty arbitrary but it should work somewhat nicely - float distance_sq = camera_position.getDistanceFromSQ(mesh_sphere_center); - bool faraway = distance_sq >= std::pow(BS * 50 + mesh_sphere_radius, 2.0f); + bool faraway = block_mesh->m_lod > 0; if (block_mesh->isAnimationForced() || !faraway || mesh_animate_count < (m_control.range_all ? 200 : 50)) { diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index cf02e5e129d4f..eb5644b1b11f0 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -20,6 +20,8 @@ #include #include +#include "profiler.h" + // Distance of light extrapolation (for oversized nodes) // After this distance, it gives up and considers light level constant #define SMOOTH_LIGHTING_OVERSIZE 1.0 @@ -1809,6 +1811,7 @@ void MapblockMeshGenerator::drawNode() void MapblockMeshGenerator::generate() { ZoneScoped; + ScopeProfiler sp(g_profiler, "Client: Mesh Making Regular", SPT_AVG); for (cur_node.p.Z = 0; cur_node.p.Z < data->m_side_length; cur_node.p.Z++) for (cur_node.p.Y = 0; cur_node.p.Y < data->m_side_length; cur_node.p.Y++) @@ -1817,4 +1820,4 @@ void MapblockMeshGenerator::generate() cur_node.f = &nodedef->get(cur_node.n); drawNode(); } -} +} \ No newline at end of file diff --git a/src/client/content_mapblock.h b/src/client/content_mapblock.h index d1dd6f20b4dd7..ef2e9bc89dead 100644 --- a/src/client/content_mapblock.h +++ b/src/client/content_mapblock.h @@ -5,43 +5,11 @@ #pragma once #include "nodedef.h" +#include struct MeshMakeData; struct MeshCollector; -struct LightPair { - u8 lightDay; - u8 lightNight; - - LightPair() = default; - explicit LightPair(u16 value) : lightDay(value & 0xff), lightNight(value >> 8) {} - LightPair(u8 valueA, u8 valueB) : lightDay(valueA), lightNight(valueB) {} - LightPair(float valueA, float valueB) : - lightDay(core::clamp(core::round32(valueA), 0, 255)), - lightNight(core::clamp(core::round32(valueB), 0, 255)) {} - operator u16() const { return lightDay | lightNight << 8; } -}; - -struct LightInfo { - float light_day; - float light_night; - float light_boosted; - - LightPair getPair(float sunlight_boost = 0.0f) const - { - return LightPair( - (1 - sunlight_boost) * light_day - + sunlight_boost * light_boosted, - light_night); - } -}; - -struct LightFrame { - f32 lightsDay[8]; - f32 lightsNight[8]; - bool sunlight[8]; -}; - class MapblockMeshGenerator { public: @@ -168,4 +136,4 @@ class MapblockMeshGenerator // common void errorUnknownDrawtype(); void drawNode(); -}; +}; \ No newline at end of file diff --git a/src/client/lod_mapblock.cpp b/src/client/lod_mapblock.cpp new file mode 100644 index 0000000000000..9ea3e1f5fc205 --- /dev/null +++ b/src/client/lod_mapblock.cpp @@ -0,0 +1,361 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2010-2013 celeron55, Perttu Ahola + +#include +#include "lod_mapblock.h" +#include "util/basic_macros.h" +#include "util/numeric.h" +#include "util/tracy_wrapper.h" +#include "mapblock_mesh.h" +#include "settings.h" +#include "nodedef.h" +#include "client/tile.h" +#include "client/meshgen/collector.h" +#include "client/renderingengine.h" +#include "client.h" + +#include "profiler.h" + +static constexpr u16 quad_indices_02[] = {0, 1, 2, 2, 3, 0}; +static const auto &quad_indices = quad_indices_02; + +LodMeshGenerator::LodMeshGenerator(MeshMakeData *input, MeshCollector *output, const bool is_textureless): + m_data(input), + m_collector(output), + m_nodedef(m_data->m_nodedef), + m_blockpos_nodes(m_data->m_blockpos * MAP_BLOCKSIZE), + m_is_textureless(is_textureless) +{ +} + +void LodMeshGenerator::generateBitsetMesh(const MapNode n, const u8 width, + const v3s16 seg_start, const video::SColor color_in) +{ + const core::vector3df seg_offset(seg_start.X * BS, seg_start.Y * BS, seg_start.Z * BS); + const f32 scaled_BS = BS * width; + + core::vector3df vertices[4]; + video::S3DVertex irr_vertices[4]; + for (u8 direction = 0; direction < Direction_END; direction++) { + TileSpec tile; + video::SColor color; + if (m_is_textureless) { + // When generating a mesh with no texture, we have to color the vertices instead. + video::SColor c2 = m_nodedef->get(n).average_colors[direction]; + color = video::SColor( + color_in.getAlpha(), + color_in.getRed() * c2.getRed() / 255U, + color_in.getGreen() * c2.getGreen() / 255U, + color_in.getBlue() * c2.getBlue() / 255U); + } else { + getNodeTileN(n, m_blockpos_nodes, direction, m_data, tile); + color = color_in; + } + + const u64 direction_offset = BITSET_MAX_NOPAD2 * direction; + for (u8 slice_i = 0; slice_i < BITSET_MAX_NOPAD; slice_i++) { + const u64 slice_offset = direction_offset + BITSET_MAX_NOPAD * slice_i; + for (u8 u = 0; u < BITSET_MAX_NOPAD; u++) { + + bitset column = m_slices[slice_offset + u]; + while (column) { + #if defined(__GNUC__) || defined(__clang__) || defined(__MINGW32__) || defined(__MINGW64__) + s32 v0 = __builtin_ctzll(column); + // Shift the bitset down, so it has no low 0s anymore, + // then count the numer of low 1s to get the length of the greedy quad. + s32 v1 = __builtin_ctzll(~(column >> v0)); + #elif defined(_MSC_VER) + unsigned long int tmp; + _BitScanForward64(&tmp, column); + s32 v0 = static_cast(tmp); + // Shift the bitset down, so it has no low 0s anymore, + // then count the numer of low 1s to get the length of the greedy quad. + _BitScanForward64(&tmp, ~(column >> v0)); + s32 v1 = static_cast(tmp); + #endif + const bitset mask = ((1ULL << v1) - 1) << v0; + column ^= mask; + // Determine the width of the greedy quad + s32 u1 = 1; + while (u + u1 < BITSET_MAX_NOPAD && // while still in current chunk + (m_slices[slice_offset + u + u1] & mask) == mask // and next column shares faces + ) { + // then increase width and unset the bits + m_slices[slice_offset + u + u1] ^= mask; + u1++; + } + + const core::vector2d uvs[4] = { + core::vector2d{0, static_cast(v1*width)}, + core::vector2d{0, 0}, + core::vector2d{static_cast(u1*width), 0}, + core::vector2d{static_cast(u1*width), static_cast(v1*width)} + }; + + // calculate low (0) and high (1) coordinates for u and v axis + u1 = (u + u1) * scaled_BS - BS / 2; + const s32 u0 = u * scaled_BS - BS / 2; + v1 = (v0 + v1) * scaled_BS - BS / 2; + v0 = v0 * scaled_BS - BS / 2; + // calculate depth at which to place the quad + const s32 w = ((slice_i + 1) * width - 1 + + (direction % 2 == 1 ? -width + 1 : 1)) * BS + - BS / 2; + + switch (direction) { + case 0: + case 1: + vertices[0] = core::vector3df(u0, w, v0); + vertices[1] = core::vector3df(u1, w, v0); + vertices[2] = core::vector3df(u1, w, v1); + vertices[3] = core::vector3df(u0, w, v1); + break; + case 2: + vertices[0] = core::vector3df(w, u0, v0); + vertices[1] = core::vector3df(w, u0, v1); + vertices[2] = core::vector3df(w, u1, v1); + vertices[3] = core::vector3df(w, u1, v0); + break; + case 3: + vertices[0] = core::vector3df(w, u0, v1); + vertices[1] = core::vector3df(w, u1, v1); + vertices[2] = core::vector3df(w, u1, v0); + vertices[3] = core::vector3df(w, u0, v0); + break; + case 4: + vertices[0] = core::vector3df(u1, v0, w); + vertices[1] = core::vector3df(u0, v0, w); + vertices[2] = core::vector3df(u0, v1, w); + vertices[3] = core::vector3df(u1, v1, w); + break; + default: + vertices[0] = core::vector3df(u0, v0, w); + vertices[1] = core::vector3df(u0, v1, w); + vertices[2] = core::vector3df(u1, v1, w); + vertices[3] = core::vector3df(u1, v0, w); + break; + } + for (core::vector3df& v : vertices) + v += seg_offset; + + // set winding order + switch (direction) { + case 1: + case 3: + case 5: + irr_vertices[0] = video::S3DVertex(vertices[0], s_normals[direction], color, uvs[0]); + irr_vertices[1] = video::S3DVertex(vertices[1], s_normals[direction], color, uvs[1]); + irr_vertices[2] = video::S3DVertex(vertices[2], s_normals[direction], color, uvs[2]); + irr_vertices[3] = video::S3DVertex(vertices[3], s_normals[direction], color, uvs[3]); + break; + default: + irr_vertices[0] = video::S3DVertex(vertices[0], s_normals[direction], color, uvs[0]); + irr_vertices[1] = video::S3DVertex(vertices[3], s_normals[direction], color, uvs[1]); + irr_vertices[2] = video::S3DVertex(vertices[2], s_normals[direction], color, uvs[2]); + irr_vertices[3] = video::S3DVertex(vertices[1], s_normals[direction], color, uvs[3]); + } + m_collector->append(m_is_textureless ? s_static_tile : tile, irr_vertices, 4, quad_indices, 6); + } + } + } + } +} + +LightPair LodMeshGenerator::computeMaxFaceLight(const MapNode n, const v3s16 p, const v3s16 dir) const +{ + const u16 lp1 = getFaceLight(n, m_data->m_vmanip.getNodeNoExNoEmerge(p + dir), m_nodedef); + const u16 lp2 = getFaceLight(n, m_data->m_vmanip.getNodeNoExNoEmerge(p - dir), m_nodedef); + return static_cast(std::max(lp1, lp2)); +} + +void LodMeshGenerator::generateGreedyLod(const std::bitset types, const v3s16 seg_start, + const v3s16 seg_size, const u8 width) +{ + // all nodes in this volume, , for finding node faces + bitset all_set_nodes[3 * BITSET_MAX * BITSET_MAX] = {0}; + std::map node_types; + // all nodes in this volume, on each of the 3 axes, grouped by type and brightness, for use in actual mesh generation + std::unordered_map set_nodes; + + const v3s16 to = seg_start + seg_size; + const s16 max_light_step = std::min(MAP_BLOCKSIZE, width); + + v3s16 p; + for (p.Z = seg_start.Z - 1; p.Z < to.Z + width; p.Z += width) + for (p.Y = seg_start.Y - 1; p.Y < to.Y + width; p.Y += width) + for (p.X = seg_start.X - 1; p.X < to.X + width; p.X += width) { + MapNode n = m_data->m_vmanip.getNodeNoExNoEmerge(p); + if (n.getContent() == CONTENT_IGNORE) { + continue; + } + // when our sample is air, take more samples in a straight line down, to make sure we always hit the surface + // otherwise, snowy mountains or grassy hills would display lumps of dirt and stone + const ContentFeatures* f = &m_nodedef->get(n); + for (u8 subtr = 1; subtr < width && f->drawtype == NDT_AIRLIKE; subtr++) { + n = m_data->m_vmanip.getNodeNoExNoEmerge(p - v3s16(0, subtr, 0)); + f = &m_nodedef->get(n); + } + if (!types.test(f->drawtype)) { + continue; + } + + const content_t node_type = n.getContent(); + const v3s16 p_scaled = (p - seg_start + 1) / width; + node_types.try_emplace(node_type, n); + + if (f->drawtype == NDT_NORMAL) { + // take a light sample for each side of a node, on each axis and take the maximum. + // it would be more accurate to take a sample for each of the 6 directions intead of each axis, + // but that would take twice the ram + LightPair lp = computeMaxFaceLight(n, p, v3s16(max_light_step, 0, 0)); + NodeKey key = NodeKey{node_type, lp}; + set_nodes[key][BITSET_MAX * p_scaled.Y + p_scaled.Z] |= 1ULL << p_scaled.X; // x axis + + lp = computeMaxFaceLight(n, p, v3s16(0, max_light_step, 0)); + key = NodeKey{node_type, lp}; + set_nodes[key][BITSET_MAX2 + BITSET_MAX * p_scaled.X + p_scaled.Z] |= 1ULL << p_scaled.Y; // y axis + + lp = computeMaxFaceLight(n, p, v3s16(0, 0, max_light_step)); + key = NodeKey{node_type, lp}; + set_nodes[key][2 * BITSET_MAX2 + BITSET_MAX * p_scaled.X + p_scaled.Y] |= 1ULL << p_scaled.Z; // z axis + } + else { + const LightPair lp = static_cast(getInteriorLight(n, 0, m_nodedef)); + + NodeKey key{node_type, lp}; + auto &nodes = set_nodes[key]; + nodes[BITSET_MAX * p_scaled.Y + p_scaled.Z] |= 1ULL << p_scaled.X; // x axis + nodes[BITSET_MAX2 + BITSET_MAX * p_scaled.X + p_scaled.Z] |= 1ULL << p_scaled.Y; // y axis + nodes[2 * BITSET_MAX2 + BITSET_MAX * p_scaled.X + p_scaled.Y] |= 1ULL << p_scaled.Z; // z axis + } + + all_set_nodes[BITSET_MAX * p_scaled.Y + p_scaled.Z] |= 1ULL << p_scaled.X; // x axis + all_set_nodes[BITSET_MAX2 + BITSET_MAX * p_scaled.X + p_scaled.Z] |= 1ULL << p_scaled.Y; // y axis + all_set_nodes[2 * BITSET_MAX2 + BITSET_MAX * p_scaled.X + p_scaled.Y] |= 1ULL << p_scaled.Z; // z axis + } + + static constexpr bitset U62_MAX = U64_MAX >> 2; + + for (const auto& [node_key, nodes] : set_nodes) { + for (u8 u = 1; u <= BITSET_MAX_NOPAD; u++) + for (u8 v = 1; v <= BITSET_MAX_NOPAD; v++) { + // Shifting the bitset of set nodes in a column to the right, inverting it, then &-ing it with another bitset + // leaves only the bits with no neighbors to their left. So this calculates all left facing node faces + // in that column. + // To do it like this means we need a bit of padding on each side, which is why we are limited to only 62 + // nodes per volume. + // These operations are considerably faster on a regular u64 (here aliased as bitset) instead of an + // std::bitset, so we *cant* flatten these bitsets into one large std::bitset. + m_nodes_faces[BITSET_MAX_NOPAD * (u - 1) + v - 1] = + (nodes[BITSET_MAX2 + BITSET_MAX * u + v] & + ~(all_set_nodes[BITSET_MAX2 + BITSET_MAX * u + v] >> 1)) + >> 1 & U62_MAX; + + m_nodes_faces[BITSET_MAX_NOPAD2 + BITSET_MAX_NOPAD * (u - 1) + v - 1] = + (nodes[BITSET_MAX2 + BITSET_MAX * u + v] & + ~(all_set_nodes[BITSET_MAX2 + BITSET_MAX * u + v] << 1)) + >> 1 & U62_MAX; + + m_nodes_faces[2 * BITSET_MAX_NOPAD2 + BITSET_MAX_NOPAD * (u - 1) + v - 1] = + (nodes[BITSET_MAX * u + v] & + ~(all_set_nodes[BITSET_MAX * u + v] >> 1)) + >> 1 & U62_MAX; + + m_nodes_faces[3 * BITSET_MAX_NOPAD2 + BITSET_MAX_NOPAD * (u - 1) + v - 1] = + (nodes[BITSET_MAX * u + v] & + ~(all_set_nodes[BITSET_MAX * u + v] << 1)) + >> 1 & U62_MAX; + + m_nodes_faces[4 * BITSET_MAX_NOPAD2 + BITSET_MAX_NOPAD * (u - 1) + v - 1] = + (nodes[2 * BITSET_MAX2 + BITSET_MAX * u + v] & + ~(all_set_nodes[2 * BITSET_MAX2 + BITSET_MAX * u + v] >> 1)) + >> 1 & U62_MAX; + + m_nodes_faces[5 * BITSET_MAX_NOPAD2 + BITSET_MAX_NOPAD * (u - 1) + v - 1] = + (nodes[2 * BITSET_MAX2 + BITSET_MAX * u + v] & + ~(all_set_nodes[2 * BITSET_MAX2 + BITSET_MAX * u + v] << 1)) + >> 1 & U62_MAX; + } + + // We only calculated the visible node faces per column, so far. + // But to use greedy meshing, we need the faces *next* to each other, not behind each other. + // Each node face is mapped to their corresponding slice/plane + memset(m_slices, 0, sizeof(m_slices)); + for (u8 direction = 0; direction < Direction_END; direction++) { + const u64 direction_offset = BITSET_MAX_NOPAD2 * direction; + for (u8 u = 0; u < BITSET_MAX_NOPAD; u++) { + const u64 u_offset = direction_offset + BITSET_MAX_NOPAD * u; + for (u8 v = 0; v < BITSET_MAX_NOPAD; v++) { + bitset column = m_nodes_faces[u_offset + v]; + while (column) { + #if defined( __GNUC__ ) || defined( __clang__ ) || defined(__MINGW32__) + const u8 first_filled = __builtin_ctzll(column); + #elif defined(_MSC_VER) + unsigned long int tmp; + _BitScanForward64(&tmp, column); + const u8 first_filled = static_cast(tmp); + #endif + m_slices[direction_offset + BITSET_MAX_NOPAD * first_filled + u] |= 1ULL << v; + column &= column - 1; + } + } + } + } + + MapNode n = node_types[node_key.content]; + const video::SColor color = encode_light(node_key.light, m_nodedef->getLightingFlags(n).light_source); + + generateBitsetMesh(n, width, seg_start - m_blockpos_nodes, color); + } +} + +void LodMeshGenerator::generateLodChunks(const std::bitset types, const u8 width) +{ + ScopeProfiler sp(g_profiler, "Client: Mesh Making LOD Greedy", SPT_AVG); + + // split chunk into 62^3 segments to be able to use 64 bit ints as bitsets + // the other two bits are used as padding to find node faces + const int attempted_seg_size = BITSET_MAX_NOPAD * width; + + for (u16 x = 0; x < m_data->m_side_length; x += attempted_seg_size) + for (u16 y = 0; y < m_data->m_side_length; y += attempted_seg_size) + for (u16 z = 0; z < m_data->m_side_length; z += attempted_seg_size) { + const v3s16 seg_start( + x + m_blockpos_nodes.X, + y + m_blockpos_nodes.Y, + z + m_blockpos_nodes.Z); + const v3s16 seg_size( + std::min(m_data->m_side_length - x - 1, attempted_seg_size), + std::min(m_data->m_side_length - y - 1, attempted_seg_size), + std::min(m_data->m_side_length - z - 1, attempted_seg_size)); + generateGreedyLod(types, seg_start, seg_size, width); + } +} + +void LodMeshGenerator::generate(const u8 lod) +{ + ZoneScoped; + + // cap LODs to 8, since there is no use for larger than 256 node LODs + u8 width = 1 << MYMIN(lod - 1, 7); + + // cap LODs width to chunk size to account for different mesh chunk settings + if (width > m_data->m_side_length) + width = m_data->m_side_length; + + // transparents are always rendered separately + std::bitset transparent_set; + transparent_set.set(NDT_LIQUID); + transparent_set.set(NDT_GLASSLIKE); + + generateLodChunks(transparent_set, width); + + std::bitset solid_set; + solid_set.set(NDT_NORMAL); + solid_set.set(NDT_NODEBOX); + solid_set.set(NDT_ALLFACES); + solid_set.set(NDT_MESH); + + generateLodChunks(solid_set, width); +} diff --git a/src/client/lod_mapblock.h b/src/client/lod_mapblock.h new file mode 100644 index 0000000000000..fd36fda384f60 --- /dev/null +++ b/src/client/lod_mapblock.h @@ -0,0 +1,72 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2010-2013 celeron55, Perttu Ahola + +#pragma once + +#include "nodedef.h" +#include + +struct MeshMakeData; +struct MeshCollector; + +class LodMeshGenerator +{ +public: + LodMeshGenerator(MeshMakeData *input, MeshCollector *output, bool is_textureless); + void generate(u8 lod); + +private: + MeshMakeData *const m_data; + MeshCollector *const m_collector; + const NodeDefManager *const m_nodedef; + const v3s16 m_blockpos_nodes; + const bool m_is_textureless; + + // max bits the fit in a bitset + static constexpr s16 BITSET_MAX = 64; + // max bits the fit in a bitset squared + static constexpr s16 BITSET_MAX2 = BITSET_MAX * BITSET_MAX; + // max bits the fit in a bitset without padding nodes + static constexpr s16 BITSET_MAX_NOPAD = 62; + // max bits the fit in a bitset without padding nodes squared + static constexpr s16 BITSET_MAX_NOPAD2 = BITSET_MAX_NOPAD * BITSET_MAX_NOPAD; + + using bitset = u64; + bitset m_nodes_faces[6 * BITSET_MAX_NOPAD2]; + bitset m_slices[6 * BITSET_MAX_NOPAD2]; + + static constexpr core::vector3df s_normals[6] = { + core::vector3df(0, 1, 0), core::vector3df(0, -1, 0), + core::vector3df(1, 0, 0), core::vector3df(-1, 0, 0), + core::vector3df(0, 0, 1), core::vector3df(0, 0, -1) + }; + static constexpr TileSpec s_static_tile = [] { + TileSpec tile; + TileLayer layer; + layer.shader_id = -1; + tile.layers[0] = layer; + return tile; + }(); + + void generateGreedyLod(std::bitset types, v3s16 seg_start, v3s16 seg_size, u8 width); + void generateBitsetMesh(MapNode n, u8 width, v3s16 seg_start, video::SColor color_in); + LightPair computeMaxFaceLight(MapNode n, v3s16 p, v3s16 dir) const; + void generateLodChunks(std::bitset types, u8 width); +}; + +struct NodeKey { + content_t content; + LightPair light; + + bool operator==(const NodeKey& other) const { + return content == other.content && light == other.light; + } +}; + +template<> +struct std::hash { + std::size_t operator()(const NodeKey& k) const { + return std::hash()(k.content) ^ (std::hash()(k.light) << 1); + } +}; diff --git a/src/client/mapblock_mesh.cpp b/src/client/mapblock_mesh.cpp index 7a9dc994c76a2..d4a14e10a4fef 100644 --- a/src/client/mapblock_mesh.cpp +++ b/src/client/mapblock_mesh.cpp @@ -13,6 +13,7 @@ #include "mesh.h" #include "minimap.h" #include "content_mapblock.h" +#include "lod_mapblock.h" #include "util/directiontables.h" #include "util/tracy_wrapper.h" #include "client/meshgen/collector.h" @@ -29,8 +30,7 @@ MeshMakeData */ -MeshMakeData::MeshMakeData(const NodeDefManager *ndef, - u16 side_length, MeshGrid mesh_grid) : +MeshMakeData::MeshMakeData(const NodeDefManager *ndef, u16 side_length, MeshGrid mesh_grid): m_side_length(side_length), m_mesh_grid(mesh_grid), m_nodedef(ndef) @@ -588,7 +588,9 @@ void PartialMeshBuffer::draw(video::IVideoDriver *driver) const MapBlockMesh */ -MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data): +MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data, const u8 lod, const video::SMaterial mono_material): + m_lod(lod), + m_mono_material(mono_material), m_tsrc(client->getTextureSource()), m_shdrsrc(client->getShaderSource()), m_bounding_sphere_center((data->m_side_length * 0.5f - 0.5f) * BS), @@ -626,18 +628,54 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data): v3f offset = intToFloat((data->m_blockpos - mesh_grid.getMeshPos(data->m_blockpos)) * MAP_BLOCKSIZE, BS); MeshCollector collector(m_bounding_sphere_center, offset); + const bool is_lod_enabled = g_settings->getBool("enable_lod"); + const bool is_textureless = is_lod_enabled && lod >= g_settings->getU16("lod_texture_threshold"); { // Generate everything - MapblockMeshGenerator(data, &collector).generate(); + if (lod == 0 || !is_lod_enabled) + MapblockMeshGenerator(data, &collector).generate(); + else + LodMeshGenerator(data, &collector, is_textureless).generate(lod); } /* Convert MeshCollector to SMesh */ - m_bounding_radius = std::sqrt(collector.m_bounding_radius_sq); + if (is_textureless) + generateMonoMesh(collector); + else + generateMesh(collector); + + m_bsp_tree.buildTree(&m_transparent_triangles, data->m_side_length); + + // Check if animation is required for this mesh + m_has_animation = + !m_crack_materials.empty() || + !m_animation_info.empty(); +} + +void MapBlockMesh::generateMonoMesh(MeshCollector &collector) const { + scene::SMesh *mesh = static_cast(m_mesh[0].get()); + + for(u32 i = 0; i < collector.prebuffers[0].size(); i++) { + scene::SMeshBuffer *buf = new scene::SMeshBuffer(); + buf->Material = m_mono_material; + PreMeshBuffer &p = collector.prebuffers[0][i]; + buf->append(&p.vertices[0], p.vertices.size(), &p.indices[0], p.indices.size()); + + mesh->addMeshBuffer(buf); + std::ignore = buf->drop(); + } + if (mesh) { + // Use VBO for mesh (this just would set this for every buffer) + mesh->setHardwareMappingHint(scene::EHM_STATIC); + } +} + +void MapBlockMesh::generateMesh(MeshCollector &collector) { for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) { scene::SMesh *mesh = static_cast(m_mesh[layer].get()); @@ -706,13 +744,6 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data): mesh->setHardwareMappingHint(scene::EHM_STATIC); } } - - m_bsp_tree.buildTree(&m_transparent_triangles, data->m_side_length); - - // Check if animation is required for this mesh - m_has_animation = - !m_crack_materials.empty() || - !m_animation_info.empty(); } MapBlockMesh::~MapBlockMesh() diff --git a/src/client/mapblock_mesh.h b/src/client/mapblock_mesh.h index a4c7a4a791c5f..acf24596e2a7d 100644 --- a/src/client/mapblock_mesh.h +++ b/src/client/mapblock_mesh.h @@ -16,6 +16,8 @@ #include #include +#include "meshgen/collector.h" + namespace video { class IVideoDriver; } @@ -178,7 +180,7 @@ class MapBlockMesh { public: // Builds the mesh given - MapBlockMesh(Client *client, MeshMakeData *data); + MapBlockMesh(Client *client, MeshMakeData *data, u8 lod, video::SMaterial mono_material); ~MapBlockMesh(); // Main animation function, parameters: @@ -275,7 +277,10 @@ class MapBlockMesh return std::make_pair((n & 0xffff) - 1, (n >> 16) & 0xff); } + const u8 m_lod; + private: + video::SMaterial m_mono_material; typedef std::pair MeshIndex; @@ -309,6 +314,10 @@ class MapBlockMesh std::vector m_transparent_buffers; // Is m_transparent_buffers currently in consolidated form? bool m_transparent_buffers_consolidated = false; + + void generateMonoMesh(MeshCollector& collector) const; + + void generateMesh(MeshCollector& collector); }; /*! diff --git a/src/client/mesh_generator_thread.cpp b/src/client/mesh_generator_thread.cpp index fe60fd105183f..7156e3fea5acc 100644 --- a/src/client/mesh_generator_thread.cpp +++ b/src/client/mesh_generator_thread.cpp @@ -6,10 +6,13 @@ #include "settings.h" #include "profiler.h" #include "client.h" +#include "camera.h" #include "mapblock.h" #include "map.h" +#include "nodedef.h" #include "util/directiontables.h" #include "porting.h" +#include "shader.h" /* QueuedMeshUpdate @@ -117,6 +120,18 @@ bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, MutexAutoLock lock(m_mutex); + /* + * Calculate LOD + */ + const v3s16 cam_pos = floatToInt(m_client->getCamera()->getPosition(), BS) / MAP_BLOCKSIZE // current player block + // other block positions are on the corner, so offset this position as well for dist calcs + - m_client->getMeshGrid().cell_size / 2; + const u16 dist2 = cam_pos.getDistanceFromSQ(mesh_grid.getMeshPos(p)); + u16 lod_threshold = g_settings->getU16("lod_threshold"); + lod_threshold *= lod_threshold; + const u8 lod = dist2 < lod_threshold ? 0 : + 1 + static_cast(std::log2(dist2 / lod_threshold) / g_settings->getFloat("lod_quality")); + /* Mark the block as urgent if requested */ @@ -135,6 +150,7 @@ bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, q->crack_pos = m_client->getCrackPos(); q->urgent |= urgent; q->retrieveBlocks(map, mesh_grid.cell_size); + q->lod = lod; return true; } } @@ -150,6 +166,7 @@ bool MeshUpdateQueue::addBlock(Map *map, v3s16 p, bool ack_block_to_server, q->crack_pos = m_client->getCrackPos(); q->urgent = urgent; q->retrieveBlocks(map, mesh_grid.cell_size); + q->lod = lod; /* Air blocks won't suddenly become visible due to a neighbor update, so @@ -230,8 +247,8 @@ void MeshUpdateQueue::fillDataFromMapBlocks(QueuedMeshUpdate *q) MeshUpdateWorkerThread */ -MeshUpdateWorkerThread::MeshUpdateWorkerThread(Client *client, MeshUpdateQueue *queue_in, MeshUpdateManager *manager) : - UpdateThread("Mesh"), m_client(client), m_queue_in(queue_in), m_manager(manager) +MeshUpdateWorkerThread::MeshUpdateWorkerThread(Client *client, MeshUpdateQueue *queue_in, MeshUpdateManager *manager, video::SMaterial mono_material) : + UpdateThread("Mesh"), m_client(client), m_queue_in(queue_in), m_manager(manager), m_mono_material(mono_material) { m_generation_interval = g_settings->getU16("mesh_generation_interval"); m_generation_interval = rangelim(m_generation_interval, 0, 25); @@ -244,7 +261,7 @@ void MeshUpdateWorkerThread::doUpdate() ScopeProfiler sp(g_profiler, "Client: Mesh making (sum)"); // This generates the mesh: - MapBlockMesh *mesh_new = new MapBlockMesh(m_client, q->data); + MapBlockMesh *mesh_new = new MapBlockMesh(m_client, q->data, q->lod, m_mono_material); MeshUpdateResult r; r.p = q->p; @@ -285,8 +302,14 @@ MeshUpdateManager::MeshUpdateManager(Client *client): number_of_threads = std::max(1, number_of_threads); infostream << "MeshUpdateManager: using " << number_of_threads << " threads" << std::endl; + // getSHader only works in this thread, so the material has to be passed along from here + const u32 shader_id = client->getShaderSource()->getShader( + "nodes_shader", TILE_MATERIAL_BASIC, NDT_NORMAL, true); + video::SMaterial mono_material; + mono_material.MaterialType = client->getShaderSource()->getShaderInfo(shader_id).material; + for (int i = 0; i < number_of_threads; i++) - m_workers.push_back(std::make_unique(client, &m_queue_in, this)); + m_workers.push_back(std::make_unique(client, &m_queue_in, this, mono_material)); } void MeshUpdateManager::updateBlock(Map *map, v3s16 p, bool ack_block_to_server, diff --git a/src/client/mesh_generator_thread.h b/src/client/mesh_generator_thread.h index e016763a1ed1d..88af141d0e274 100644 --- a/src/client/mesh_generator_thread.h +++ b/src/client/mesh_generator_thread.h @@ -25,6 +25,7 @@ struct QueuedMeshUpdate v3s16 crack_pos; MeshMakeData *data = nullptr; // This is generated in MeshUpdateQueue::pop() std::vector map_blocks; + u8 lod; bool urgent = false; QueuedMeshUpdate() = default; @@ -122,7 +123,7 @@ class MeshUpdateManager; class MeshUpdateWorkerThread : public UpdateThread { public: - MeshUpdateWorkerThread(Client *client, MeshUpdateQueue *queue_in, MeshUpdateManager *manager); + MeshUpdateWorkerThread(Client *client, MeshUpdateQueue *queue_in, MeshUpdateManager *manager, video::SMaterial mono_material); protected: virtual void doUpdate(); @@ -131,6 +132,7 @@ class MeshUpdateWorkerThread : public UpdateThread Client *m_client; MeshUpdateQueue *m_queue_in; MeshUpdateManager *m_manager; + video::SMaterial m_mono_material; // TODO: Add callback to update these when g_settings changes int m_generation_interval; diff --git a/src/client/shader.cpp b/src/client/shader.cpp index 9055ec3f77fae..7831a5916e49d 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -863,13 +863,15 @@ void ShaderSource::generateShader(ShaderInfo &shaderinfo) */ u32 IShaderSource::getShader(const std::string &name, - MaterialType material_type, NodeDrawType drawtype, bool array_texture) + MaterialType material_type, NodeDrawType drawtype, bool is_textureless, bool array_texture) { ShaderConstants input_const; input_const["MATERIAL_TYPE"] = (int)material_type; (void) drawtype; // unused if (array_texture) input_const["USE_ARRAY_TEXTURE"] = 1; + if (is_textureless) + input_const["TEXTURELESS"] = 1; video::E_MATERIAL_TYPE base_mat = video::EMT_SOLID; switch (material_type) { diff --git a/src/client/shader.h b/src/client/shader.h index bdbf3b38fe59b..bcfc5b01dff55 100644 --- a/src/client/shader.h +++ b/src/client/shader.h @@ -267,7 +267,8 @@ class IShaderSource { /// @brief Helper: Generates or gets a shader suitable for nodes and entities u32 getShader(const std::string &name, - MaterialType material_type, NodeDrawType drawtype = NDT_NORMAL, + MaterialType material_type, NodeDrawType drawtype = NDT_NORMAL, + bool is_textureless = false, bool array_texture = false); /** diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index e074f8137b5fe..edc52cb757129 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -246,6 +246,10 @@ void set_default_settings() settings->setDefault("fps_max", "60"); settings->setDefault("fps_max_unfocused", "10"); settings->setDefault("viewing_range", "190"); + settings->setDefault("enable_lod", "true"); + settings->setDefault("lod_threshold", "10"); + settings->setDefault("lod_quality", "1.5"); + settings->setDefault("lod_texture_threshold", "2"); settings->setDefault("client_mesh_chunk", "1"); settings->setDefault("screen_w", "1024"); settings->setDefault("screen_h", "600"); diff --git a/src/nodedef.cpp b/src/nodedef.cpp index a2d2a95b6b991..beb2483a9b1e0 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -413,6 +413,8 @@ void ContentFeatures::reset() #if CHECK_CLIENT_BUILD() mesh_ptr = nullptr; minimap_color = video::SColor(0, 0, 0, 0); + for (u8 d = 0; d < Direction_END; d++) + average_colors[d] = minimap_color; #endif visual_scale = 1.0; for (auto &i : tiledef) @@ -1064,6 +1066,18 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc ShaderIds overlay_shader = getNodeShader(overlay_material, drawtype); + if (drawtype != NDT_AIRLIKE && !tdef[0].name.empty()) { + for (u8 d = 0; d < Direction_END; d++) { + if (!tdef_overlay[d].name.empty()) { + // Merge overlay and base texture + std::string combined = tdef[d].name + "^(" + tdef_overlay[d].name + ")"; + average_colors[d] = tsrc->getTextureAverageColor(combined); + } else { + average_colors[d] = tsrc->getTextureAverageColor(tdef[d].name); + } + } + } + // minimap pixel color = average color of top tile if (tsettings.enable_minimap && drawtype != NDT_AIRLIKE && !tdef[0].name.empty()) { diff --git a/src/nodedef.h b/src/nodedef.h index bb2852d40200f..5d12c103ea69e 100644 --- a/src/nodedef.h +++ b/src/nodedef.h @@ -93,6 +93,17 @@ enum NodeBoxType : u8 NODEBOX_CONNECTED, // optionally draws nodeboxes if a neighbor node attaches }; +enum Direction : u8 +{ + UP = 0, // y increase + DOWN = 1, // y decrease + LEFT = 2, // x increase + RIGHT = 3, // x decrease + BACK = 4, // z increase + FRONT = 5, // z decrease + Direction_END // Dummy for validity check +}; + struct NodeBoxConnected { std::vector connect_top; @@ -327,7 +338,7 @@ struct ContentFeatures #if CHECK_CLIENT_BUILD() // 0 1 2 3 4 5 // up down right left back front - TileSpec tiles[6]; + TileSpec tiles[Direction_END]; // Special tiles TileSpec special_tiles[CF_SPECIAL_COUNT]; u8 solidness; // Used when choosing which face is drawn @@ -363,6 +374,7 @@ struct ContentFeatures #if CHECK_CLIENT_BUILD() scene::SMesh *mesh_ptr; // mesh in case of mesh node video::SColor minimap_color; + video::SColor average_colors[Direction_END]; #endif float visual_scale; // Misc. scale parameter TileDef tiledef[6]; @@ -901,3 +913,36 @@ class NodeResolver { u32 m_nnlistsizes_idx = 0; bool m_resolve_done = false; }; + +struct LightPair { + u8 lightDay; + u8 lightNight; + + LightPair() = default; + explicit LightPair(u16 value) : lightDay(value & 0xff), lightNight(value >> 8) {} + LightPair(u8 valueA, u8 valueB) : lightDay(valueA), lightNight(valueB) {} + LightPair(float valueA, float valueB) : + lightDay(core::clamp(core::round32(valueA), 0, 255)), + lightNight(core::clamp(core::round32(valueB), 0, 255)) {} + operator u16() const { return lightDay | lightNight << 8; } +}; + +struct LightInfo { + float light_day; + float light_night; + float light_boosted; + + LightPair getPair(float sunlight_boost = 0.0f) const + { + return LightPair( + (1 - sunlight_boost) * light_day + + sunlight_boost * light_boosted, + light_night); + } +}; + +struct LightFrame { + f32 lightsDay[8]; + f32 lightsNight[8]; + bool sunlight[8]; +};