From de5ef4ca298efbbc90e6b2a3950769de279e986e Mon Sep 17 00:00:00 2001 From: sfan5 Date: Sat, 1 Nov 2025 17:21:41 +0100 Subject: [PATCH] Introduce array textures for node rendering (#16574) --- builtin/settingtypes.txt | 4 + .../inventory_shader/opengl_fragment.glsl | 43 ++ .../inventory_shader/opengl_vertex.glsl | 27 ++ .../shaders/nodes_shader/opengl_fragment.glsl | 17 +- .../shaders/nodes_shader/opengl_vertex.glsl | 8 +- .../object_shader/opengl_fragment.glsl | 17 +- .../shaders/object_shader/opengl_vertex.glsl | 6 + .../shaders/shadow/pass1/opengl_fragment.glsl | 3 +- .../shadow/pass1_trans/opengl_fragment.glsl | 1 + irr/include/EVertexAttributes.h | 2 + irr/include/S3DVertex.h | 45 ++- irr/src/OpenGL/Driver.cpp | 2 + src/client/client.cpp | 13 +- src/client/client.h | 2 +- src/client/clientmap.cpp | 26 +- src/client/content_cao.cpp | 11 +- src/client/content_mapblock.cpp | 5 +- src/client/game.cpp | 15 +- src/client/mapblock_mesh.cpp | 3 - src/client/meshgen/collector.cpp | 4 +- src/client/particles.cpp | 7 +- src/client/shader.cpp | 10 +- src/client/shader.h | 3 +- src/client/texturesource.cpp | 138 ++++++- src/client/texturesource.h | 15 +- src/client/tile.h | 28 +- src/client/wieldmesh.cpp | 85 ++-- src/client/wieldmesh.h | 15 + src/defaultsettings.cpp | 1 + src/nodedef.cpp | 371 +++++++++++++++--- src/nodedef.h | 37 +- 31 files changed, 777 insertions(+), 187 deletions(-) create mode 100644 client/shaders/inventory_shader/opengl_fragment.glsl create mode 100644 client/shaders/inventory_shader/opengl_vertex.glsl diff --git a/builtin/settingtypes.txt b/builtin/settingtypes.txt index b06e105ce4..a5484fb4fe 100644 --- a/builtin/settingtypes.txt +++ b/builtin/settingtypes.txt @@ -2057,6 +2057,10 @@ world_aligned_mode (World-aligned textures mode) enum enable disable,enable,forc # Warning: This option is EXPERIMENTAL! autoscale_mode (Autoscaling mode) enum disable disable,enable,force +# Caps the maximum number of layers used with array textures (if supported). +# This is only useful for debugging. +array_texture_max (Array texture max layers) int 65535 0 65535 + # When using bilinear/trilinear filtering, low-resolution textures # can be blurred, so this option automatically upscales them to preserve # crisp pixels. This defines the minimum texture size for the upscaled textures; diff --git a/client/shaders/inventory_shader/opengl_fragment.glsl b/client/shaders/inventory_shader/opengl_fragment.glsl new file mode 100644 index 0000000000..12def3ca19 --- /dev/null +++ b/client/shaders/inventory_shader/opengl_fragment.glsl @@ -0,0 +1,43 @@ +#ifdef USE_ARRAY_TEXTURE + uniform sampler2DArray baseTexture; +#else + uniform sampler2D baseTexture; +#endif + +varying vec3 vNormal; +varying vec3 vPosition; +#ifdef GL_ES +varying lowp vec4 varColor; +varying mediump vec2 varTexCoord; +varying float varTexLayer; +#else +centroid varying vec4 varColor; +centroid varying vec2 varTexCoord; +centroid varying float varTexLayer; // actually int +#endif + + +void main(void) +{ + vec2 uv = varTexCoord.st; + +#ifdef USE_ARRAY_TEXTURE + vec4 base = texture(baseTexture, vec3(uv, varTexLayer)).rgba; +#else + vec4 base = texture2D(baseTexture, uv).rgba; +#endif + + // Handle transparency by discarding pixel as appropriate. +#ifdef USE_DISCARD + if (base.a == 0.0) + discard; +#endif +#ifdef USE_DISCARD_REF + if (base.a < 0.5) + discard; +#endif + + vec4 col = vec4(base.rgb * varColor.rgb, base.a); + + gl_FragColor = col; +} diff --git a/client/shaders/inventory_shader/opengl_vertex.glsl b/client/shaders/inventory_shader/opengl_vertex.glsl new file mode 100644 index 0000000000..a112ed4a81 --- /dev/null +++ b/client/shaders/inventory_shader/opengl_vertex.glsl @@ -0,0 +1,27 @@ +varying vec3 vNormal; +varying vec3 vPosition; +#ifdef GL_ES +varying lowp vec4 varColor; +varying mediump vec2 varTexCoord; +varying float varTexLayer; +#else +centroid varying vec4 varColor; +centroid varying vec2 varTexCoord; +centroid varying float varTexLayer; // actually int +#endif + +void main(void) +{ +#ifdef USE_ARRAY_TEXTURE + varTexLayer = inVertexAux; +#endif + varTexCoord = inTexCoord0.st; + + vec4 pos = inVertexPosition; + gl_Position = mWorldViewProj * pos; + vPosition = gl_Position.xyz; + vNormal = inVertexNormal; + + vec4 color = inVertexColor; + varColor = clamp(color, 0.0, 1.0); +} diff --git a/client/shaders/nodes_shader/opengl_fragment.glsl b/client/shaders/nodes_shader/opengl_fragment.glsl index 09811bfe87..f3498d83d2 100644 --- a/client/shaders/nodes_shader/opengl_fragment.glsl +++ b/client/shaders/nodes_shader/opengl_fragment.glsl @@ -1,4 +1,8 @@ -uniform sampler2D baseTexture; +#ifdef USE_ARRAY_TEXTURE + uniform sampler2DArray baseTexture; +#else + uniform sampler2D baseTexture; +#endif #define crackTexture texture1 uniform sampler2D crackTexture; @@ -48,10 +52,12 @@ varying vec3 worldPosition; #ifdef GL_ES varying lowp vec4 varColor; varying mediump vec2 varTexCoord; +varying float varTexLayer; varying float nightRatio; #else centroid varying lowp vec4 varColor; centroid varying vec2 varTexCoord; +centroid varying float varTexLayer; // actually int centroid varying float nightRatio; #endif varying highp vec3 eyeVec; @@ -431,10 +437,13 @@ void main(void) { vec2 uv = varTexCoord.st; +#ifdef USE_ARRAY_TEXTURE + vec4 base = texture(baseTexture, vec3(uv, varTexLayer)).rgba; +#else vec4 base = texture2D(baseTexture, uv).rgba; - // If alpha is zero, we can just discard the pixel. This fixes transparency - // on GPUs like GC7000L, where GL_ALPHA_TEST is not implemented in mesa, - // and also on GLES 2, where GL_ALPHA_TEST is missing entirely. +#endif + + // Handle transparency by discarding pixel as appropriate. #ifdef USE_DISCARD if (base.a == 0.0) discard; diff --git a/client/shaders/nodes_shader/opengl_vertex.glsl b/client/shaders/nodes_shader/opengl_vertex.glsl index 6fe7acd85e..8c57d1655f 100644 --- a/client/shaders/nodes_shader/opengl_vertex.glsl +++ b/client/shaders/nodes_shader/opengl_vertex.glsl @@ -20,10 +20,12 @@ varying vec3 worldPosition; #ifdef GL_ES varying lowp vec4 varColor; varying mediump vec2 varTexCoord; +varying float varTexLayer; varying float nightRatio; #else -centroid varying lowp vec4 varColor; +centroid varying vec4 varColor; centroid varying vec2 varTexCoord; +centroid varying float varTexLayer; // actually int centroid varying float nightRatio; #endif #ifdef ENABLE_DYNAMIC_SHADOWS @@ -149,6 +151,9 @@ float snoise(vec3 p) void main(void) { +#ifdef USE_ARRAY_TEXTURE + varTexLayer = inVertexAux; +#endif varTexCoord = inTexCoord0.st; float disp_x; @@ -181,6 +186,7 @@ void main(void) pos.y += disp_z * 0.1; pos.z += disp_z; #elif MATERIAL_TYPE == TILE_MATERIAL_WAVING_PLANTS && ENABLE_WAVING_PLANTS + // bottom of plant doesn't wave if (varTexCoord.y < 0.05) { pos.x += disp_x; pos.z += disp_z; diff --git a/client/shaders/object_shader/opengl_fragment.glsl b/client/shaders/object_shader/opengl_fragment.glsl index db72d7f7dc..02183bd25d 100644 --- a/client/shaders/object_shader/opengl_fragment.glsl +++ b/client/shaders/object_shader/opengl_fragment.glsl @@ -1,4 +1,8 @@ -uniform sampler2D baseTexture; +#ifdef USE_ARRAY_TEXTURE + uniform sampler2DArray baseTexture; +#else + uniform sampler2D baseTexture; +#endif uniform vec3 dayLight; uniform lowp vec4 fogColor; @@ -41,8 +45,10 @@ varying vec3 worldPosition; varying lowp vec4 varColor; #ifdef GL_ES varying mediump vec2 varTexCoord; +varying float varTexLayer; #else centroid varying vec2 varTexCoord; +centroid varying float varTexLayer; // actually int #endif varying highp vec3 eyeVec; varying float nightRatio; @@ -365,10 +371,13 @@ void main(void) vec3 color; vec2 uv = varTexCoord.st; +#ifdef USE_ARRAY_TEXTURE + vec4 base = texture(baseTexture, vec3(uv, varTexLayer)).rgba; +#else vec4 base = texture2D(baseTexture, uv).rgba; - // If alpha is zero, we can just discard the pixel. This fixes transparency - // on GPUs like GC7000L, where GL_ALPHA_TEST is not implemented in mesa, - // and also on GLES 2, where GL_ALPHA_TEST is missing entirely. +#endif + + // Handle transparency by discarding pixel as appropriate. #ifdef USE_DISCARD if (base.a == 0.0) discard; diff --git a/client/shaders/object_shader/opengl_vertex.glsl b/client/shaders/object_shader/opengl_vertex.glsl index be76a19444..6d39232ccf 100644 --- a/client/shaders/object_shader/opengl_vertex.glsl +++ b/client/shaders/object_shader/opengl_vertex.glsl @@ -9,8 +9,10 @@ varying vec3 worldPosition; varying lowp vec4 varColor; #ifdef GL_ES varying mediump vec2 varTexCoord; +varying float varTexLayer; #else centroid varying vec2 varTexCoord; +centroid varying float varTexLayer; // actually int #endif #ifdef ENABLE_DYNAMIC_SHADOWS @@ -91,7 +93,11 @@ float directional_ambient(vec3 normal) void main(void) { +#ifdef USE_ARRAY_TEXTURE + varTexLayer = inVertexAux; +#endif varTexCoord = (mTexture * vec4(inTexCoord0.xy, 1.0, 1.0)).st; + gl_Position = mWorldViewProj * inVertexPosition; vPosition = gl_Position.xyz; diff --git a/client/shaders/shadow/pass1/opengl_fragment.glsl b/client/shaders/shadow/pass1/opengl_fragment.glsl index 9b1e657afc..99bfbc4147 100644 --- a/client/shaders/shadow/pass1/opengl_fragment.glsl +++ b/client/shaders/shadow/pass1/opengl_fragment.glsl @@ -1,3 +1,4 @@ +// FIXME missing array texture handling uniform sampler2D ColorMapSampler; varying vec4 tPos; @@ -10,7 +11,7 @@ centroid varying vec2 varTexCoord; void main() { vec4 col = texture2D(ColorMapSampler, varTexCoord); - + // FIXME: magic number??? if (col.a < 0.70) discard; diff --git a/client/shaders/shadow/pass1_trans/opengl_fragment.glsl b/client/shaders/shadow/pass1_trans/opengl_fragment.glsl index e9f2c7efa9..3746a42c54 100644 --- a/client/shaders/shadow/pass1_trans/opengl_fragment.glsl +++ b/client/shaders/shadow/pass1_trans/opengl_fragment.glsl @@ -1,3 +1,4 @@ +// FIXME missing array texture handling uniform sampler2D ColorMapSampler; varying vec4 tPos; diff --git a/irr/include/EVertexAttributes.h b/irr/include/EVertexAttributes.h index 2c6eac2cd2..9af92953c7 100644 --- a/irr/include/EVertexAttributes.h +++ b/irr/include/EVertexAttributes.h @@ -9,6 +9,7 @@ enum E_VERTEX_ATTRIBUTES EVA_POSITION = 0, EVA_NORMAL, EVA_COLOR, + EVA_AUX, EVA_TCOORD0, EVA_TCOORD1, EVA_TANGENT, @@ -21,6 +22,7 @@ const char *const sBuiltInVertexAttributeNames[] = { "inVertexPosition", "inVertexNormal", "inVertexColor", + "inVertexAux", "inTexCoord0", "inTexCoord1", "inVertexTangent", diff --git a/irr/include/S3DVertex.h b/irr/include/S3DVertex.h index 0c269a635a..5eceee90d1 100644 --- a/irr/include/S3DVertex.h +++ b/irr/include/S3DVertex.h @@ -41,17 +41,17 @@ struct S3DVertex { //! default constructor constexpr S3DVertex() : - Color(0xffffffff) {} + Color(0xffffffff), Aux(0) {} //! constructor - constexpr S3DVertex(f32 x, f32 y, f32 z, f32 nx, f32 ny, f32 nz, SColor c, f32 tu, f32 tv) : - Pos(x, y, z), Normal(nx, ny, nz), Color(c), TCoords(tu, tv) {} + constexpr S3DVertex(f32 x, f32 y, f32 z, f32 nx, f32 ny, f32 nz, SColor c, f32 tu, f32 tv, u16 a = 0) : + Pos(x, y, z), Normal(nx, ny, nz), Color(c), TCoords(tu, tv), Aux(a) {} //! constructor constexpr S3DVertex(const core::vector3df &pos, const core::vector3df &normal, - SColor color, const core::vector2df &tcoords) : + SColor color, const core::vector2df &tcoords, u16 aux = 0) : Pos(pos), - Normal(normal), Color(color), TCoords(tcoords) {} + Normal(normal), Color(color), TCoords(tcoords), Aux(aux) {} //! Position core::vector3df Pos; @@ -65,24 +65,42 @@ struct S3DVertex //! Texture coordinates core::vector2df TCoords; + //! Auxiliary value (free to use) + u16 Aux; + constexpr bool operator==(const S3DVertex &other) const { return ((Pos == other.Pos) && (Normal == other.Normal) && - (Color == other.Color) && (TCoords == other.TCoords)); + (Color == other.Color) && (TCoords == other.TCoords) && + (Aux == other.Aux)); } constexpr bool operator!=(const S3DVertex &other) const { return ((Pos != other.Pos) || (Normal != other.Normal) || - (Color != other.Color) || (TCoords != other.TCoords)); + (Color != other.Color) || (TCoords != other.TCoords) || + (Aux != other.Aux)); } constexpr bool operator<(const S3DVertex &other) const { - return ((Pos < other.Pos) || - ((Pos == other.Pos) && (Normal < other.Normal)) || - ((Pos == other.Pos) && (Normal == other.Normal) && (Color < other.Color)) || - ((Pos == other.Pos) && (Normal == other.Normal) && (Color == other.Color) && (TCoords < other.TCoords))); + if (Pos < other.Pos) + return true; + if (Pos != other.Pos) + return false; + if (Normal < other.Normal) + return true; + if (Normal != other.Normal) + return false; + if (Color < other.Color) + return true; + if (Color != other.Color) + return false; + if (TCoords < other.TCoords) + return true; + if (TCoords != other.TCoords) + return false; + return Aux < other.Aux; } //! Get type of the class @@ -98,10 +116,13 @@ struct S3DVertex return S3DVertex(Pos.getInterpolated(other.Pos, d), Normal.getInterpolated(other.Normal, d), Color.getInterpolated(other.Color, d), - TCoords.getInterpolated(other.TCoords, d)); + TCoords.getInterpolated(other.TCoords, d), + d == 0.0f ? other.Aux : Aux); } }; +// FIXME: the following types don't handle `Aux`, but we don't use them in situations where it's relevant. + //! Vertex with two texture coordinates. /** Usually used for geometry with lightmaps or other special materials. diff --git a/irr/src/OpenGL/Driver.cpp b/irr/src/OpenGL/Driver.cpp index 71470c573b..e801b4e6a1 100644 --- a/irr/src/OpenGL/Driver.cpp +++ b/irr/src/OpenGL/Driver.cpp @@ -58,6 +58,7 @@ static const VertexType vtStandard = { {EVA_NORMAL, 3, GL_FLOAT, VertexAttribute::Mode::Regular, offsetof(S3DVertex, Normal)}, {EVA_COLOR, 4, GL_UNSIGNED_BYTE, VertexAttribute::Mode::Normalized, offsetof(S3DVertex, Color)}, {EVA_TCOORD0, 2, GL_FLOAT, VertexAttribute::Mode::Regular, offsetof(S3DVertex, TCoords)}, + {EVA_AUX, 1, GL_UNSIGNED_SHORT, VertexAttribute::Mode::Regular, offsetof(S3DVertex, Aux)}, }, }; @@ -67,6 +68,7 @@ static const VertexType vtStandard = { #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Winvalid-offsetof" + static const VertexType vt2TCoords = { sizeof(S3DVertex2TCoords), { diff --git a/src/client/client.cpp b/src/client/client.cpp index 78509baee8..e14f0db815 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -1799,10 +1799,10 @@ struct TextureUpdateArgs { std::wstring text_base; }; -void Client::showUpdateProgressTexture(void *args, u32 progress, u32 max_progress) +void Client::showUpdateProgressTexture(void *args, float progress) { auto *targs = reinterpret_cast(args); - u16 cur_percent = std::ceil(progress * 100.f / max_progress); + u16 cur_percent = std::ceil(100 * progress); // Throttle menu drawing bool do_draw = false; @@ -1817,7 +1817,8 @@ void Client::showUpdateProgressTexture(void *args, u32 progress, u32 max_progres std::wostringstream strm; strm << targs->text_base << L" " << cur_percent << L"%..."; - int shown_progress = 72 + std::ceil(0.18f * cur_percent); + // 70% -> 99% + int shown_progress = 70 + std::ceil(0.29f * cur_percent); m_rendering_engine->draw_load_screen(strm.str(), guienv, m_tsrc, 0, shown_progress); } @@ -1837,19 +1838,19 @@ void Client::afterContentReceived() // Rebuild inherited images and recreate textures infostream<<"- Rebuilding images and textures"<draw_load_screen(wstrgettext("Loading textures..."), - guienv, m_tsrc, 0, 70); + guienv, m_tsrc, 0, 66); m_tsrc->rebuildImagesAndTextures(); // Rebuild shaders infostream<<"- Rebuilding shaders"<draw_load_screen(wstrgettext("Rebuilding shaders..."), - guienv, m_tsrc, 0, 71); + guienv, m_tsrc, 0, 68); m_shsrc->rebuildShaders(); // Update node aliases infostream<<"- Updating node aliases"<draw_load_screen(wstrgettext("Initializing nodes..."), - guienv, m_tsrc, 0, 72); + guienv, m_tsrc, 0, 70); m_nodedef->updateAliases(m_itemdef); for (const auto &path : getTextureDirs()) { TextureOverrideSource override_source(path + DIR_DELIM + "override.txt"); diff --git a/src/client/client.h b/src/client/client.h index 2d1a47bca4..bcd2d11c0a 100644 --- a/src/client/client.h +++ b/src/client/client.h @@ -357,7 +357,7 @@ public: void drawLoadScreen(const std::wstring &text, float dtime, int percent); void afterContentReceived(); - void showUpdateProgressTexture(void *args, u32 progress, u32 max_progress); + void showUpdateProgressTexture(void *args, float progress); float getRTT(); float getCurRate(); diff --git a/src/client/clientmap.cpp b/src/client/clientmap.cpp index 158f526de8..e74e4cb892 100644 --- a/src/client/clientmap.cpp +++ b/src/client/clientmap.cpp @@ -1088,6 +1088,7 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) u32 vertex_count = 0; u32 drawcall_count = 0; u32 material_swaps = 0; + u32 array_texture_use = 0; // Render all mesh buffers in order drawcall_count += draw_order.size(); @@ -1117,8 +1118,12 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) layer.MagFilter = video::ETMAGF_NEAREST; layer.AnisotropicFilter = 0; } + driver->setMaterial(material); ++material_swaps; + if (auto *tex = material.getTexture(0); tex && tex->getType() == video::ETT_2D_ARRAY) + ++array_texture_use; + material.TextureLayers[ShadowRenderer::TEXTURE_LAYER_SHADOW].Texture = nullptr; } @@ -1158,6 +1163,10 @@ void ClientMap::renderMap(video::IVideoDriver* driver, s32 pass) g_profiler->avg(prefix + "vertices drawn [#]", vertex_count); g_profiler->avg(prefix + "drawcalls [#]", drawcall_count); g_profiler->avg(prefix + "material swaps [#]", material_swaps); + if (material_swaps && array_texture_use) { + int percent = (100.0f * array_texture_use) / material_swaps; + g_profiler->avg(prefix + "array texture use [%]", percent); + } } void ClientMap::invalidateMapBlockMesh(MapBlockMesh *mesh) @@ -1480,14 +1489,14 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver, bool translucent_foliage = g_settings->getBool("enable_translucent_foliage"); - video::E_MATERIAL_TYPE leaves_material = video::EMT_SOLID; - // For translucent leaves, we want to use backface culling instead of frontface. + std::vector leaves_material; if (translucent_foliage) { - // this is the material leaves would use, compare to nodedef.cpp - auto* shdsrc = m_client->getShaderSource(); - const u32 leaves_shader = shdsrc->getShader("nodes_shader", TILE_MATERIAL_WAVING_LEAVES, NDT_ALLFACES); - leaves_material = shdsrc->getShaderInfo(leaves_shader).material; + auto *shdsrc = m_client->getShaderSource(); + // Find out all materials used by leaves so we can identify them + leaves_material.reserve(m_nodedef->m_leaves_materials.size()); + for (u32 shader_id : m_nodedef->m_leaves_materials) + leaves_material.push_back(shdsrc->getShaderInfo(shader_id).material); } for (auto &descriptor : draw_order) { @@ -1502,7 +1511,7 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver, local_material.BackfaceCulling = material.BackfaceCulling; local_material.FrontfaceCulling = material.FrontfaceCulling; } - if (local_material.MaterialType == leaves_material && translucent_foliage) { + if (translucent_foliage && CONTAINS(leaves_material, local_material.MaterialType)) { local_material.BackfaceCulling = true; local_material.FrontfaceCulling = false; } @@ -1522,7 +1531,8 @@ void ClientMap::renderMapShadows(video::IVideoDriver *driver, video::SMaterial clean; clean.BlendOperation = video::EBO_ADD; driver->setMaterial(clean); // reset material to defaults - // FIXME: why is this here? + // This is somehow needed to fully reset the rendering state, or later operations + // will be broken. driver->draw3DLine(v3f(), v3f(), video::SColor(0)); g_profiler->avg(prefix + "draw meshes [ms]", draw.stop(true)); diff --git a/src/client/content_cao.cpp b/src/client/content_cao.cpp index 0aab95f87e..e2ad60e9ec 100644 --- a/src/client/content_cao.cpp +++ b/src/client/content_cao.cpp @@ -212,6 +212,8 @@ static scene::SMesh *generateNodeMesh(Client *client, MapNode n, MapblockMeshGenerator(&mmd, &collector).generate(); } + const AlphaMode alpha_mode = ndef->get(n).alpha; + auto mesh = make_irr(); animation.clear(); for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) { @@ -224,10 +226,8 @@ static scene::SMesh *generateNodeMesh(Client *client, MapNode n, p.applyTileColor(); if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) { - const FrameSpec &frame = (*p.layer.frames)[0]; - p.layer.texture = frame.texture; - - animation.emplace_back(MeshAnimationInfo{mesh->getMeshBufferCount(), 0, p.layer}); + animation.emplace_back(MeshAnimationInfo{ + mesh->getMeshBufferCount(), 0, p.layer}); } auto buf = make_irr(); @@ -236,9 +236,8 @@ static scene::SMesh *generateNodeMesh(Client *client, MapNode n, // Set up material auto &mat = buf->Material; - u32 shader_id = shdsrc->getShader("object_shader", p.layer.material_type, NDT_NORMAL); - mat.MaterialType = shdsrc->getShaderInfo(shader_id).material; p.layer.applyMaterialOptions(mat, layer); + getAdHocNodeShader(mat, shdsrc, "object_shader", alpha_mode, layer == 1); mesh->addMeshBuffer(buf.get()); } diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index 9f402a0078..cf02e5e129 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -1021,8 +1021,9 @@ void MapblockMeshGenerator::drawGlasslikeFramedNode() // Optionally render internal liquid level defined by param2 // Liquid is textured with 1 tile defined in nodedef 'special_tiles' - if (param2 > 0 && cur_node.f->param_type_2 == CPT2_GLASSLIKE_LIQUID_LEVEL && - cur_node.f->special_tiles[0].layers[0].texture) { + auto &cf = *cur_node.f; + if (param2 > 0 && cf.param_type_2 == CPT2_GLASSLIKE_LIQUID_LEVEL && + !cf.special_tiles[0].layers[0].empty()) { // Internal liquid level has param2 range 0 .. 63, // convert it to -0.5 .. 0.5 float vlev = (param2 / 63.0f) * 2.0f - 1.0f; diff --git a/src/client/game.cpp b/src/client/game.cpp index c92735f189..9426cf1290 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -961,12 +961,12 @@ bool Game::createClient(const GameStartData &start_data) } // Pre-calculate crack length - video::ITexture *t = texture_src->getTexture("crack_anylength.png"); - if (t) { - v2u32 size = t->getOriginalSize(); - crack_animation_length = size.Y / size.X; - } else { - crack_animation_length = 5; + { + auto size = texture_src->getTextureDimensions("crack_anylength.png"); + if (size.Width && size.Height) + crack_animation_length = size.Height / size.Width; + else + crack_animation_length = 5; } shader_src->addShaderConstantSetter( @@ -1283,7 +1283,8 @@ bool Game::getServerContent(bool *aborted) message << " (" << cur << ' ' << cur_unit << ")"; } - progress = 30 + client->mediaReceiveProgress() * 35 + 0.5; + // 30% -> 65% + progress = 30 + std::ceil(client->mediaReceiveProgress() * 35 + 0.5f); m_rendering_engine->draw_load_screen(utf8_to_wide(message.str()), guienv, texture_src, dtime, progress); } diff --git a/src/client/mapblock_mesh.cpp b/src/client/mapblock_mesh.cpp index ce939980a1..7a9dc994c7 100644 --- a/src/client/mapblock_mesh.cpp +++ b/src/client/mapblock_mesh.cpp @@ -651,9 +651,6 @@ MapBlockMesh::MapBlockMesh(Client *client, MeshMakeData *data): if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) { // Add to MapBlockMesh in order to animate these tiles m_animation_info.emplace(std::make_pair(layer, i), AnimationInfo(p.layer)); - // Replace tile texture with the first animation frame - assert(p.layer.frames); - p.layer.texture = (*p.layer.frames)[0].texture; } // Create material diff --git a/src/client/meshgen/collector.cpp b/src/client/meshgen/collector.cpp index f0261acc6a..9cf45fd5e3 100644 --- a/src/client/meshgen/collector.cpp +++ b/src/client/meshgen/collector.cpp @@ -23,10 +23,12 @@ void MeshCollector::append(const TileLayer &layer, const video::S3DVertex *verti { PreMeshBuffer &p = findBuffer(layer, layernum, numVertices); + const u16 aux = layer.texture_layer_idx; + u32 vertex_count = p.vertices.size(); for (u32 i = 0; i < numVertices; i++) { p.vertices.emplace_back(vertices[i].Pos + offset, vertices[i].Normal, - vertices[i].Color, vertices[i].TCoords); + vertices[i].Color, vertices[i].TCoords, aux); m_bounding_radius_sq = std::max(m_bounding_radius_sq, (vertices[i].Pos - m_center_pos).getLengthSQ()); } diff --git a/src/client/particles.cpp b/src/client/particles.cpp index 9f734631f9..35a7946522 100644 --- a/src/client/particles.cpp +++ b/src/client/particles.cpp @@ -42,8 +42,11 @@ static video::ITexture *extractTexture(const TileDef &def, const TileLayer &laye { // If animated take first frame from tile layer (so we don't have to handle // that manually), otherwise look up by name. - if (!layer.empty() && (layer.material_flags & MATERIAL_FLAG_ANIMATION)) - return (*layer.frames)[0].texture; + if (!layer.empty() && (layer.material_flags & MATERIAL_FLAG_ANIMATION)) { + auto *ret = (*layer.frames)[0].texture; + assert(ret->getType() == video::ETT_2D); + return ret; + } if (!def.name.empty()) return tsrc->getTexture(def.name); return nullptr; diff --git a/src/client/shader.cpp b/src/client/shader.cpp index ad4a5e670e..444ff9c662 100644 --- a/src/client/shader.cpp +++ b/src/client/shader.cpp @@ -666,9 +666,11 @@ void ShaderSource::generateShader(ShaderInfo &shaderinfo) uniform mediump mat4 mTexture; attribute highp vec4 inVertexPosition; - attribute lowp vec4 inVertexColor; - attribute mediump vec2 inTexCoord0; attribute mediump vec3 inVertexNormal; + attribute lowp vec4 inVertexColor; + attribute mediump float inVertexAux; + attribute mediump vec2 inTexCoord0; + attribute mediump vec2 inTexCoord1; attribute mediump vec4 inVertexTangent; attribute mediump vec4 inVertexBinormal; )"; @@ -787,11 +789,13 @@ void ShaderSource::generateShader(ShaderInfo &shaderinfo) */ u32 IShaderSource::getShader(const std::string &name, - MaterialType material_type, NodeDrawType drawtype) + MaterialType material_type, NodeDrawType drawtype, 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; 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 2e5a7ff63e..69ea6ac699 100644 --- a/src/client/shader.h +++ b/src/client/shader.h @@ -267,7 +267,8 @@ public: /// @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 array_texture = false); /** * Helper: Generates or gets a shader for common, general use. diff --git a/src/client/texturesource.cpp b/src/client/texturesource.cpp index f8d9cf353a..a8aded74a8 100644 --- a/src/client/texturesource.cpp +++ b/src/client/texturesource.cpp @@ -14,17 +14,25 @@ #include "texturepaths.h" #include "util/thread.h" - +// Represents a to-be-generated texture for queuing purposes struct TextureRequest { - std::string image; + video::E_TEXTURE_TYPE type = video::ETT_2D; + std::vector images; void print(std::ostream &to) const { - to << "image=\"" << image << "\""; + if (images.size() == 1) { + to << "image=\"" << images[0] << "\""; + } else { + to << "images={"; + for (auto &image : images) + to << "\"" << image << "\" "; + to << "}"; + } } bool operator==(const TextureRequest &other) const { - return image == other.image; + return type == other.type && images == other.images; } bool operator!=(const TextureRequest &other) const { return !(*this == other); @@ -72,6 +80,9 @@ public: video::ITexture* getTexture(const std::string &name, u32 *id = nullptr); + video::ITexture *addArrayTexture( + const std::vector &images, u32 *id = nullptr); + bool needFilterForMesh() const { return mesh_filter_needed; } @@ -104,6 +115,8 @@ public: video::SColor getTextureAverageColor(const std::string &name); + core::dimension2du getTextureDimensions(const std::string &image); + void setImageCaching(bool enabled); private: @@ -137,6 +150,9 @@ private: // Generate standard texture u32 generateTexture(const std::string &name); + // Generate array texture + u32 generateArrayTexture(const std::vector &names); + // Thread-safe cache of what source images are known (true = known) MutexedMap m_source_image_existence; @@ -270,15 +286,97 @@ u32 TextureSource::getTextureId(const std::string &name) return n->second; } - TextureRequest req{name}; + TextureRequest req{video::ETT_2D, {name}}; return processRequestQueued(req); } +video::ITexture *TextureSource::addArrayTexture( + const std::vector &images, u32 *ret_id) +{ + if (images.empty()) + return NULL; + + TextureRequest req{video::ETT_2D_ARRAY, images}; + + u32 id = processRequestQueued(req); + if (ret_id) + *ret_id = id; + return getTexture(id); +} + u32 TextureSource::processRequest(const TextureRequest &req) { - // No different types yet (TODO) - return generateTexture(req.image); + if (req.type == video::ETT_2D) { + assert(req.images.size() == 1); + return generateTexture(req.images[0]); + } + + if (req.type == video::ETT_2D_ARRAY) { + assert(!req.images.empty()); + return generateArrayTexture(req.images); + } + + errorstream << "TextureSource::processRequest(): unknown type " + << (int)req.type << std::endl; + return 0; +} + +u32 TextureSource::generateArrayTexture(const std::vector &images) +{ + std::set source_image_names; + std::vector imgs; + const auto &drop_imgs = [&imgs] () { + for (auto *img : imgs) { + if (img) + img->drop(); + } + imgs.clear(); + }; + for (auto &name : images) { + video::IImage *img = getOrGenerateImage(name, source_image_names); + if (!img) { + // Since the caller needs to make sure of the dimensions beforehand + // anyway, this should not ever happen. So the "unhelpful" error is ok. + errorstream << "generateArrayTexture(): one of " << images.size() + << " images failed to generate, aborting." << std::endl; + drop_imgs(); + return 0; + } + imgs.push_back(img); + } + assert(!imgs.empty()); + + video::IVideoDriver *driver = RenderingEngine::get_video_driver(); + sanity_check(driver); + assert(driver->queryFeature(video::EVDF_TEXTURE_2D_ARRAY)); + + MutexAutoLock lock(m_textureinfo_cache_mutex); + const u32 id = m_textureinfo_cache.size(); + std::string name; + { // automatically choose a name + char buf[64]; + porting::mt_snprintf(buf, sizeof(buf), "array#%u %ux%ux%u", id, + imgs[0]->getDimension().Width, imgs[0]->getDimension().Height, + imgs.size()); + name = buf; + } + + video::ITexture *tex = driver->addArrayTexture(name, imgs.data(), imgs.size()); + drop_imgs(); + + if (!tex) { + warningstream << "generateArrayTexture(): failed to upload texture \"" + << name << "\"" << std::endl; + } + + // Add texture to caches (add NULL textures too) + + TextureInfo ti{video::ETT_2D_ARRAY, name, images, tex, std::move(source_image_names)}; + m_textureinfo_cache.emplace_back(std::move(ti)); + m_name_to_id[name] = id; + + return id; } u32 TextureSource::generateTexture(const std::string &name) @@ -302,7 +400,6 @@ u32 TextureSource::generateTexture(const std::string &name) video::IVideoDriver *driver = RenderingEngine::get_video_driver(); sanity_check(driver); - // passed into texture info for dynamic media tracking std::set source_image_names; video::IImage *img = getOrGenerateImage(name, source_image_names); @@ -314,12 +411,16 @@ u32 TextureSource::generateTexture(const std::string &name) guiScalingCache(io::path(name.c_str()), driver, img); img->drop(); } + if (!tex) { + warningstream << "generateTexture(): failed to upload texture \"" + << name << "\"" << std::endl; + } // Add texture to caches (add NULL textures too) MutexAutoLock lock(m_textureinfo_cache_mutex); - u32 id = m_textureinfo_cache.size(); + const u32 id = m_textureinfo_cache.size(); TextureInfo ti{video::ETT_2D, name, {name}, tex, std::move(source_image_names)}; m_textureinfo_cache.emplace_back(std::move(ti)); m_name_to_id[name] = id; @@ -535,6 +636,8 @@ void TextureSource::rebuildTexture(video::IVideoDriver *driver, TextureInfo &ti) video::SColor TextureSource::getTextureAverageColor(const std::string &name) { assert(std::this_thread::get_id() == m_main_thread); + if (name.empty()) + return {0, 0, 0, 0}; std::set unused; auto *image = getOrGenerateImage(name, unused); @@ -547,6 +650,23 @@ video::SColor TextureSource::getTextureAverageColor(const std::string &name) return c; } +core::dimension2du TextureSource::getTextureDimensions(const std::string &name) +{ + assert(std::this_thread::get_id() == m_main_thread); + + core::dimension2du ret; + if (!name.empty()) { + std::set unused; + auto *image = getOrGenerateImage(name, unused); + if (image) { + ret = image->getDimension(); + image->drop(); + } + } + + return ret; +} + void TextureSource::setImageCaching(bool enabled) { m_image_cache_enabled = enabled; diff --git a/src/client/texturesource.h b/src/client/texturesource.h index 445885e1fb..7cb09828ac 100644 --- a/src/client/texturesource.h +++ b/src/client/texturesource.h @@ -6,6 +6,7 @@ #include "irrlichttypes.h" #include +#include #include #include @@ -59,8 +60,11 @@ public: /// @brief Returns existing texture by ID virtual video::ITexture *getTexture(u32 id)=0; - /// @return true if getTextureForMesh will apply a filter - virtual bool needFilterForMesh() const = 0; + /// @brief Generates texture string(s) into an array texture + /// @note Unlike the other getters this will always add a *new* texture. + /// @return its ID + virtual video::ITexture *addArrayTexture( + const std::vector &images, u32 *id = nullptr) = 0; /** * @brief Generates a texture string into a standard texture @@ -76,6 +80,9 @@ public: return getTexture(image, id); } + /// @return true if getTextureForMesh will apply a filter + virtual bool needFilterForMesh() const = 0; + /// Filter needed for mesh-suitable textures, including leading ^ static constexpr const char *FILTER_FOR_MESH = "^[applyfiltersformesh"; @@ -90,6 +97,10 @@ public: /// @brief Check if given image name exists virtual bool isKnownSourceImage(const std::string &name)=0; + /// @brief Return dimensions of a texture string + /// (will avoid actually creating the texture) + virtual core::dimension2du getTextureDimensions(const std::string &image)=0; + /// @brief Return average color of a texture string virtual video::SColor getTextureAverageColor(const std::string &image)=0; diff --git a/src/client/tile.h b/src/client/tile.h index 3b84035ecb..bb37e8f6bd 100644 --- a/src/client/tile.h +++ b/src/client/tile.h @@ -25,6 +25,21 @@ enum MaterialType : u8 { TILE_MATERIAL_PLAIN_ALPHA }; +/** + * @brief change type so it has at least simple transparency + */ +static inline MaterialType material_type_with_alpha(MaterialType type) +{ + switch (type) { + case TILE_MATERIAL_OPAQUE: + return TILE_MATERIAL_BASIC; + case TILE_MATERIAL_WAVING_LIQUID_OPAQUE: + return TILE_MATERIAL_WAVING_LIQUID_BASIC; + default: + return type; + } +} + // Material flags // Should backface culling be enabled? #define MATERIAL_FLAG_BACKFACE_CULLING 0x01 @@ -39,6 +54,7 @@ enum MaterialType : u8 { This fully defines the looks of a tile. The SMaterial of a tile is constructed according to this. */ + struct FrameSpec { FrameSpec() = default; @@ -60,18 +76,18 @@ struct TileLayer TileLayer() = default; /*! - * Two layers are equal if they can be merged. + * Two layers are equal if they can be merged (same material). */ bool operator==(const TileLayer &other) const { return texture_id == other.texture_id && - material_type == other.material_type && + shader_id == other.shader_id && material_flags == other.material_flags && has_color == other.has_color && color == other.color && - scale == other.scale && need_polygon_offset == other.need_polygon_offset; + // texture_layer_idx and scale are notably part of the vertex data } /*! @@ -84,7 +100,7 @@ struct TileLayer /** * Set some material parameters accordingly. - * @note does not set `MaterialType` + * @note does not set `MaterialType`! * @param material material to mody * @param layer index of this layer in the `TileSpec` */ @@ -122,12 +138,16 @@ struct TileLayer u16 animation_frame_length_ms = 0; u16 animation_frame_count = 1; + /// Layer index to use, if the texture is an array texture + u16 texture_layer_idx = 0; + MaterialType material_type = TILE_MATERIAL_BASIC; u8 material_flags = MATERIAL_FLAG_BACKFACE_CULLING | MATERIAL_FLAG_TILEABLE_HORIZONTAL| MATERIAL_FLAG_TILEABLE_VERTICAL; + /// Texture scale in both directions (used for world-align) u8 scale = 1; /// does this tile need to have a positive polygon offset set? diff --git a/src/client/wieldmesh.cpp b/src/client/wieldmesh.cpp index 32ce81cda4..c27144adf4 100644 --- a/src/client/wieldmesh.cpp +++ b/src/client/wieldmesh.cpp @@ -125,55 +125,32 @@ static video::ITexture *extractTexture(const TileDef &def, const TileLayer &laye { // If animated take first frame from tile layer (so we don't have to handle // that manually), otherwise look up by name. - if (!layer.empty() && (layer.material_flags & MATERIAL_FLAG_ANIMATION)) - return (*layer.frames)[0].texture; + if (!layer.empty() && (layer.material_flags & MATERIAL_FLAG_ANIMATION)) { + auto *ret = (*layer.frames)[0].texture; + assert(ret->getType() == video::ETT_2D); + return ret; + } if (!def.name.empty()) return tsrc->getTextureForMesh(def.name); return nullptr; } -// (the function name represents the amount of time wasted on all of this) - -static void setAlphaBullshit(video::SMaterial &mat, - AlphaMode mode, bool overlay) +void getAdHocNodeShader(video::SMaterial &mat, IShaderSource *shdsrc, + const char *shader, AlphaMode mode, int layer) { - switch (mode) { - case ALPHAMODE_BLEND: - mat.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL; - mat.MaterialTypeParam = 0; - return; - case ALPHAMODE_OPAQUE: - if (!overlay) { - mat.MaterialType = video::EMT_SOLID; - return; - } - [[fallthrough]]; - case ALPHAMODE_CLIP: - default: - mat.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF; - mat.MaterialTypeParam = 0.5f; - return; - } -} + assert(shdsrc); + MaterialType type = alpha_mode_to_material_type(mode); + if (layer == 1) + type = material_type_with_alpha(type); -static void setAlphaBullshit(video::SMaterial &mat, IShaderSource *shdsrc, - AlphaMode mode, bool overlay) -{ - MaterialType mt; - switch (mode) { - case ALPHAMODE_BLEND: - mt = TILE_MATERIAL_ALPHA; - break; - case ALPHAMODE_OPAQUE: - mt = overlay ? TILE_MATERIAL_BASIC : TILE_MATERIAL_OPAQUE; - break; - case ALPHAMODE_CLIP: - default: - mt = TILE_MATERIAL_BASIC; - break; - } + // Note: logic wise this duplicates what `ContentFeatures::updateTextures` + // and related functions do. - u32 shader_id = shdsrc->getShader("object_shader", mt, NDT_NORMAL); + bool array_texture = false; + if (mat.getTexture(0)) + array_texture = mat.getTexture(0)->getType() == video::ETT_2D_ARRAY; + + u32 shader_id = shdsrc->getShader(shader, type, NDT_NORMAL, array_texture); mat.MaterialType = shdsrc->getShaderInfo(shader_id).material; } @@ -387,10 +364,6 @@ static scene::SMesh *createGenericNodeMesh(Client *client, MapNode n, for (int layer = 0; layer < MAX_TILE_LAYERS; layer++) { auto &prebuffers = collector.prebuffers[layer]; for (PreMeshBuffer &p : prebuffers) { - if (p.layer.material_flags & MATERIAL_FLAG_ANIMATION) { - const FrameSpec &frame = (*p.layer.frames)[0]; - p.layer.texture = frame.texture; - } for (video::S3DVertex &v : p.vertices) v.Color.setAlpha(255); @@ -425,20 +398,19 @@ std::vector createAnimationFrames(ITextureSource *tsrc, return {{id, texture}}; } - video::ITexture *orginal_texture = tsrc->getTexture(image_name); - if (!orginal_texture) + auto texture_size = tsrc->getTextureDimensions(image_name); + if (!texture_size.Width || !texture_size.Height) return {}; int frame_count = 1; - auto orginal_size = orginal_texture->getOriginalSize(); - animation.determineParams(orginal_size, &frame_count, &result_frame_length_ms, nullptr); + animation.determineParams(texture_size, &frame_count, &result_frame_length_ms, nullptr); std::vector frames(frame_count); std::ostringstream os(std::ios::binary); for (int i = 0; i < frame_count; i++) { os.str(""); os << image_name; - animation.getTextureModifer(os, orginal_size, i); + animation.getTextureModifer(os, texture_size, i); u32 id; frames[i].texture = tsrc->getTextureForMesh(os.str(), &id); @@ -559,7 +531,7 @@ void WieldMeshSceneNode::setItem(const ItemStack &item, Client *client, bool che for (u32 i = 0; i < material_count; ++i) { video::SMaterial &material = m_meshnode->getMaterial(i); // apply node's alpha mode - setAlphaBullshit(material, shdsrc, f.alpha, + getAdHocNodeShader(material, shdsrc, "object_shader", f.alpha, m_buffer_info[i].layer == 1); material.forEachTexture([this] (auto &tex) { setMaterialFilters(tex, m_bilinear_filter, m_trilinear_filter, @@ -668,6 +640,7 @@ void createItemMesh(Client *client, const ItemDefinition &def, ItemMesh *result) { ITextureSource *tsrc = client->getTextureSource(); + IShaderSource *shdsrc = client->getShaderSource(); const NodeDefManager *ndef = client->getNodeDefManager(); const ContentFeatures &f = ndef->get(def.name); assert(result); @@ -676,8 +649,8 @@ void createItemMesh(Client *client, const ItemDefinition &def, scene::SMesh *mesh = nullptr; - // Shading is on by default - result->needs_shading = true; + // Shading is off by default + result->needs_shading = false; video::ITexture *inventory_texture = animation_normal.getTexture(0.0f), *inventory_overlay_texture = animation_overlay.getTexture(0.0f); @@ -691,8 +664,6 @@ void createItemMesh(Client *client, const ItemDefinition &def, // overlay is white, if present result->buffer_info.emplace_back(1, &animation_overlay, true, video::SColor(0xFFFFFFFF)); - // Items with inventory images do not need shading - result->needs_shading = false; } else if (def.type == ITEM_NODE && f.drawtype == NDT_AIRLIKE) { // Fallback image for airlike node mesh = getExtrudedMesh(tsrc->getTexture("no_texture_airlike.png"), @@ -701,7 +672,6 @@ void createItemMesh(Client *client, const ItemDefinition &def, // overlay is white, if present result->buffer_info.emplace_back(1, true, video::SColor(0xFFFFFFFF)); - result->needs_shading = false; } else if (def.type == ITEM_NODE) { switch (f.drawtype) { case NDT_PLANTLIKE: { @@ -732,6 +702,7 @@ void createItemMesh(Client *client, const ItemDefinition &def, mesh = createGenericNodeMesh(client, n, &result->buffer_info, f); scaleMesh(mesh, v3f(0.12f)); + result->needs_shading = true; break; } } @@ -740,7 +711,7 @@ void createItemMesh(Client *client, const ItemDefinition &def, scene::IMeshBuffer *buf = mesh->getMeshBuffer(i); video::SMaterial &material = buf->getMaterial(); // apply node's alpha mode - setAlphaBullshit(material, f.alpha, + getAdHocNodeShader(material, shdsrc, "inventory_shader", f.alpha, result->buffer_info[i].layer == 1); material.forEachTexture([] (auto &tex) { tex.MinFilter = video::ETMINF_NEAREST_MIPMAP_NEAREST; diff --git a/src/client/wieldmesh.h b/src/client/wieldmesh.h index 964d859219..3dd4a5e642 100644 --- a/src/client/wieldmesh.h +++ b/src/client/wieldmesh.h @@ -13,6 +13,7 @@ #include #include #include "tile.h" +#include "nodedef.h" namespace scene { @@ -28,6 +29,7 @@ class Client; class ITextureSource; struct ItemDefinition; struct TileAnimationParams; +class IShaderSource; class ShadowRenderer; /* @@ -180,6 +182,19 @@ std::vector createAnimationFrames(ITextureSource *tsrc, scene::SMesh *getExtrudedMesh(video::ITexture *texture, video::ITexture *overlay_texture = nullptr); +/** + * Replace the material's shader with a custom one while respecting the usual + * things expected of node rendering (texture type, alpha mode, overlay). + * Call this after `TileLayer::applyMaterialOptions`. + * @param mat material to modify + * @param shdsrc shader source + * @param shader name of shader + * @param mode alpha mode from nodedef + * @param layer index of this layer + */ +void getAdHocNodeShader(video::SMaterial &mat, IShaderSource *shdsrc, + const char *shader, AlphaMode mode, int layer); + /** * NOTE: The item mesh is only suitable for inventory rendering (due to its * material types). In-world rendering of items must go through WieldMeshSceneNode. diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 4008d2af6f..9a95f75808 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -260,6 +260,7 @@ void set_default_settings() settings->setDefault("connected_glass", "false"); settings->setDefault("smooth_lighting", "true"); settings->setDefault("performance_tradeoffs", "false"); + settings->setDefault("array_texture_max", "65535"); settings->setDefault("lighting_alpha", "0.0"); settings->setDefault("lighting_beta", "1.5"); settings->setDefault("display_gamma", "1.0"); diff --git a/src/nodedef.cpp b/src/nodedef.cpp index 77a50ea79f..9f7afad652 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -269,6 +269,10 @@ void TileDef::deSerialize(std::istream &is, NodeDrawType drawtype, u16 protocol_ } } +/* + TextureSettings +*/ + void TextureSettings::readSettings() { connected_glass = g_settings->getBool("connected_glass"); @@ -305,6 +309,56 @@ void TextureSettings::readSettings() autoscale_mode = AUTOSCALE_DISABLE; } +/* + Texture pool and related +*/ + +#if CHECK_CLIENT_BUILD() +struct PreLoadedTexture { + video::ITexture *texture = nullptr; + u32 texture_id = 0; + u16 texture_layer_idx = 0; + bool used = false; // For debugging +}; + +struct PreLoadedTextures { + std::unordered_map pool; + std::unordered_set missed; // For debugging + + PreLoadedTexture find(const std::string &name); + void add(const std::string &name, const PreLoadedTexture &t); + + void printStats(std::ostream &to) const; +}; + +PreLoadedTexture PreLoadedTextures::find(const std::string &name) +{ + auto it = pool.find(name); + if (it == pool.end()) { + missed.emplace(name); + return {}; + } + it->second.used = true; + return it->second; +} + +void PreLoadedTextures::add(const std::string &name, const PreLoadedTexture &t) +{ + assert(pool.find(name) == pool.end()); + pool[name] = t; +} + +void PreLoadedTextures::printStats(std::ostream &to) const +{ + size_t unused = 0; + for (auto &it : pool) + unused += it.second.used ? 0 : 1; + to << "PreLoadedTextures: " << pool.size() << "\n wasted: " << unused + << " missed: " << missed.size() << std::endl; +} +#endif + + /* ContentFeatures */ @@ -321,8 +375,10 @@ ContentFeatures::~ContentFeatures() delete tiles[j].layers[0].frames; delete tiles[j].layers[1].frames; } - for (u16 j = 0; j < CF_SPECIAL_COUNT; j++) + for (u16 j = 0; j < CF_SPECIAL_COUNT; j++) { delete special_tiles[j].layers[0].frames; + delete special_tiles[j].layers[1].frames; + } #endif } @@ -670,20 +726,47 @@ void ContentFeatures::deSerialize(std::istream &is, u16 protocol_version) } #if CHECK_CLIENT_BUILD() -static void fillTileAttribs(ITextureSource *tsrc, TileLayer *layer, - const TileSpec &tile, const TileDef &tiledef, video::SColor color, - MaterialType material_type, u32 shader_id, bool backface_culling, - const TextureSettings &tsettings) -{ - layer->shader_id = shader_id; - layer->texture = tsrc->getTextureForMesh(tiledef.name, &layer->texture_id); - layer->material_type = material_type; +struct TileAttribContext { + ITextureSource *tsrc; + PreLoadedTextures *texture_pool; + video::SColor base_color; + const TextureSettings &tsettings; +}; +struct ShaderIds { + u32 normal; + // Shader that will handle an array texture and texture_layer_idx + u32 with_layers; +}; + +static void fillTileAttribs(TileLayer *layer, TileAttribContext context, + const TileSpec &tile, const TileDef &tiledef, + MaterialType material_type, ShaderIds shader) +{ + auto *tsrc = context.tsrc; + const auto &tsettings = context.tsettings; + + std::string texture_image; + if (!tiledef.name.empty()) { + texture_image = tiledef.name; + if (tsrc->needFilterForMesh()) + texture_image += tsrc->FILTER_FOR_MESH; + } else { + // Tile is empty, nothing to do. + return; + } + + core::dimension2du texture_size; + if (!texture_image.empty()) + texture_size = tsrc->getTextureDimensions(texture_image); + if (!texture_size.Width || !texture_size.Height) + texture_size = {1, 1}; // dummy if there's an error + + // Scale bool has_scale = tiledef.scale > 0; bool use_autoscale = tsettings.autoscale_mode == AUTOSCALE_FORCE || (tsettings.autoscale_mode == AUTOSCALE_ENABLE && !has_scale); - if (use_autoscale && layer->texture) { - auto texture_size = layer->texture->getOriginalSize(); + if (use_autoscale) { float base_size = tsettings.node_texture_size; float size = std::fmin(texture_size.Width, texture_size.Height); layer->scale = std::fmax(base_size, size) / base_size; @@ -695,9 +778,10 @@ static void fillTileAttribs(ITextureSource *tsrc, TileLayer *layer, if (!tile.world_aligned) layer->scale = 1; - // Material flags + // Material + layer->material_type = material_type; layer->material_flags = 0; - if (backface_culling) + if (tiledef.backface_culling) layer->material_flags |= MATERIAL_FLAG_BACKFACE_CULLING; if (tiledef.animation.type != TAT_NONE) layer->material_flags |= MATERIAL_FLAG_ANIMATION; @@ -711,7 +795,7 @@ static void fillTileAttribs(ITextureSource *tsrc, TileLayer *layer, if (tiledef.has_color) layer->color = tiledef.color; else - layer->color = color; + layer->color = context.base_color; // Animation if (layer->material_flags & MATERIAL_FLAG_ANIMATION) { @@ -722,10 +806,74 @@ static void fillTileAttribs(ITextureSource *tsrc, TileLayer *layer, layer->frames = new std::vector(frames); layer->animation_frame_count = layer->frames->size(); layer->animation_frame_length_ms = frame_length_ms; + + // Set default texture to first frame (not used practice) + layer->texture = (*layer->frames)[0].texture; + layer->texture_id = (*layer->frames)[0].texture_id; } else { layer->material_flags &= ~MATERIAL_FLAG_ANIMATION; } } + + if (!(layer->material_flags & MATERIAL_FLAG_ANIMATION)) { + // Grab texture + auto tex = context.texture_pool->find(texture_image); + if (!tex.texture) { + // wasn't pre-loaded: create standard texture on the fly + layer->texture = tsrc->getTexture(texture_image, &layer->texture_id); + } else { + layer->texture = tex.texture; + layer->texture_id = tex.texture_id; + layer->texture_layer_idx = tex.texture_layer_idx; + } + } + + // Decide on shader to use + if (layer->texture) { + layer->shader_id = (layer->texture->getType() == video::ETT_2D_ARRAY) ? + shader.with_layers : shader.normal; + } +} + +void ContentFeatures::preUpdateTextures(ITextureSource *tsrc, + std::unordered_set &pool, const TextureSettings &tsettings) +{ + // Find out the exact texture strings this node might use, and put them into the pool + // (this should match updateTextures, but it's not the end of the world if + // a mismatch occurs) + std::string append; + if (tsrc->needFilterForMesh()) + append = ITextureSource::FILTER_FOR_MESH; + std::string append_overlay = append, append_special = append; + bool use = true, use_overlay = true, use_special = true; + + if (drawtype == NDT_ALLFACES_OPTIONAL) { + use_special = (tsettings.leaves_style == LEAVES_SIMPLE); + use = !use_special; + if (tsettings.leaves_style == LEAVES_OPAQUE) + append.insert(0, "^[noalpha"); + } + + const auto &consider_tile = [&] (const TileDef &def, const std::string &append) { + // Animations are chopped into frames later, so we won't actually need + // the source texture + if (!def.name.empty() && def.animation.type == TAT_NONE) { + pool.insert(def.name + append); + } + }; + + for (u32 j = 0; j < 6; j++) { + if (use) + consider_tile(tiledef[j], append); + } + for (u32 j = 0; j < 6; j++) { + if (use_overlay) + consider_tile(tiledef_overlay[j], append_overlay); + } + for (u32 j = 0; j < CF_SPECIAL_COUNT; j++) { + if (use_special) + consider_tile(tiledef_special[j], append_special); + } } static bool isWorldAligned(AlignStyle style, WorldAlignMode mode, NodeDrawType drawtype) @@ -743,8 +891,32 @@ static bool isWorldAligned(AlignStyle style, WorldAlignMode mode, NodeDrawType d return false; } +/// @return maximum number of layers in array textures we can use (0 if unsupported) +static size_t getArrayTextureMax(video::IVideoDriver *driver) +{ + // needs to actually support array textures + if (!driver->queryFeature(video::EVDF_TEXTURE_2D_ARRAY)) + return 0; + // must not be the legacy driver, due to custom vertex format + if (driver->getDriverType() == video::EDT_OPENGL) + return 0; + // doesn't work on GLES yet (TODO) + if (driver->getDriverType() == video::EDT_OGLES2) + return 0; + // shadow shaders can't handle array textures yet (TODO) + if (g_settings->getBool("enable_dynamic_shadows")) + return 0; + + u32 n = driver->getLimits().MaxArrayTextureImages; + constexpr u32 type_max = std::numeric_limits::max(); + n = std::min(n, type_max); + n = std::min(n, g_settings->getU32("array_texture_max")); + return n; +} + void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc, - scene::IMeshManipulator *meshmanip, Client *client, const TextureSettings &tsettings) + Client *client, PreLoadedTextures *texture_pool, + const TextureSettings &tsettings) { // Figure out the actual tiles to use TileDef tdef[6]; @@ -767,9 +939,7 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc bool is_liquid = false; - MaterialType material_type = alpha == ALPHAMODE_OPAQUE ? - TILE_MATERIAL_OPAQUE : (alpha == ALPHAMODE_CLIP ? TILE_MATERIAL_BASIC : - TILE_MATERIAL_ALPHA); + MaterialType material_type = alpha_mode_to_material_type(alpha); switch (drawtype) { default: @@ -880,15 +1050,21 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc } } - u32 tile_shader = shdsrc->getShader("nodes_shader", material_type, drawtype); + const bool texture_2d_array = getArrayTextureMax(RenderingEngine::get_video_driver()) > 1; + const auto &getNodeShader = [&] (MaterialType my_material, NodeDrawType my_drawtype) { + ShaderIds ret; + ret.normal = shdsrc->getShader("nodes_shader", my_material, my_drawtype); + // need to avoid generating the shader if unsupported + if (texture_2d_array) + ret.with_layers = shdsrc->getShader("nodes_shader", my_material, my_drawtype, true); + return ret; + }; - MaterialType overlay_material = material_type; - if (overlay_material == TILE_MATERIAL_OPAQUE) - overlay_material = TILE_MATERIAL_BASIC; - else if (overlay_material == TILE_MATERIAL_LIQUID_OPAQUE) - overlay_material = TILE_MATERIAL_LIQUID_TRANSPARENT; + ShaderIds tile_shader = getNodeShader(material_type, drawtype); - u32 overlay_shader = shdsrc->getShader("nodes_shader", overlay_material, drawtype); + MaterialType overlay_material = material_type_with_alpha(material_type); + + ShaderIds overlay_shader = getNodeShader(overlay_material, drawtype); // minimap pixel color = average color of top tile if (tsettings.enable_minimap && drawtype != NDT_AIRLIKE && !tdef[0].name.empty()) @@ -904,16 +1080,18 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc // Tiles (fill in f->tiles[]) bool any_polygon_offset = false; + TileAttribContext tac{tsrc, texture_pool, color, tsettings}; + for (u16 j = 0; j < 6; j++) { tiles[j].world_aligned = isWorldAligned(tdef[j].align_style, tsettings.world_aligned_mode, drawtype); - fillTileAttribs(tsrc, &tiles[j].layers[0], tiles[j], tdef[j], - color, material_type, tile_shader, - tdef[j].backface_culling, tsettings); - if (!tdef_overlay[j].name.empty()) - fillTileAttribs(tsrc, &tiles[j].layers[1], tiles[j], tdef_overlay[j], - color, overlay_material, overlay_shader, - tdef[j].backface_culling, tsettings); + fillTileAttribs(&tiles[j].layers[0], tac, tiles[j], tdef[j], + material_type, tile_shader); + if (!tdef_overlay[j].name.empty()) { + tdef_overlay[j].backface_culling = tdef[j].backface_culling; + fillTileAttribs(&tiles[j].layers[1], tac, tiles[j], tdef_overlay[j], + overlay_material, overlay_shader); + } tiles[j].layers[0].need_polygon_offset = !tiles[j].layers[1].empty(); any_polygon_offset |= tiles[j].layers[0].need_polygon_offset; @@ -936,13 +1114,14 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc else if (waving == 2) special_material = TILE_MATERIAL_WAVING_LEAVES; } - u32 special_shader = shdsrc->getShader("nodes_shader", special_material, drawtype); + + ShaderIds special_shader = getNodeShader(special_material, drawtype); // Special tiles (fill in f->special_tiles[]) - for (u16 j = 0; j < CF_SPECIAL_COUNT; j++) - fillTileAttribs(tsrc, &special_tiles[j].layers[0], special_tiles[j], tdef_spec[j], - color, special_material, special_shader, - tdef_spec[j].backface_culling, tsettings); + for (u16 j = 0; j < CF_SPECIAL_COUNT; j++) { + fillTileAttribs(&special_tiles[j].layers[0], tac, + special_tiles[j], tdef_spec[j], special_material, special_shader); + } if (param_type_2 == CPT2_COLOR || param_type_2 == CPT2_COLORED_FACEDIR || @@ -951,6 +1130,13 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc param_type_2 == CPT2_COLORED_DEGROTATE) palette = tsrc->getPalette(palette_name); +} + +void ContentFeatures::updateMesh(Client *client, const TextureSettings &tsettings) +{ + auto *manip = client->getSceneManager()->getMeshManipulator(); + (void)tsettings; + if (drawtype == NDT_MESH && !mesh.empty()) { // Note: By freshly reading, we get an unencumbered mesh. if (scene::IMesh *src_mesh = client->getMesh(mesh)) { @@ -979,13 +1165,27 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc // TODO this should be done consistently when the mesh is loaded infostream << "ContentFeatures: recalculating normals for mesh " << mesh << std::endl; - meshmanip->recalculateNormals(mesh_ptr, true, false); + manip->recalculateNormals(mesh_ptr, true, false); } } else { mesh_ptr = nullptr; } } } + +void ContentFeatures::collectMaterials(std::vector &leaves_materials) +{ + if (drawtype == NDT_AIRLIKE) + return; + + for (u16 j = 0; j < 6; j++) { + auto &l = tiles[j].layers; + if (!l[0].empty() && l[0].material_type == TILE_MATERIAL_WAVING_LEAVES) + leaves_materials.push_back(l[0].shader_id); + if (!l[1].empty() && l[1].material_type == TILE_MATERIAL_WAVING_LEAVES) + leaves_materials.push_back(l[1].shader_id); + } +} #endif /* @@ -1021,13 +1221,13 @@ void NodeDefManager::clear() m_next_id = 0; m_selection_box_union.reset(0,0,0); m_selection_box_int_union.reset(0,0,0); +#if CHECK_CLIENT_BUILD() + m_leaves_materials.clear(); +#endif resetNodeResolveState(); - u32 initial_length = 0; - initial_length = MYMAX(initial_length, CONTENT_UNKNOWN + 1); - initial_length = MYMAX(initial_length, CONTENT_AIR + 1); - initial_length = MYMAX(initial_length, CONTENT_IGNORE + 1); + constexpr u32 initial_length = std::max({CONTENT_UNKNOWN, CONTENT_AIR, CONTENT_IGNORE}) + 1; m_content_features.resize(initial_length); // Set CONTENT_UNKNOWN @@ -1164,7 +1364,7 @@ content_t NodeDefManager::allocateId() * @param[in] boxes the vector containing the boxes * @param[in, out] box_union the union of the arguments */ -void boxVectorUnion(const std::vector &boxes, aabb3f *box_union) +static void boxVectorUnion(const std::vector &boxes, aabb3f *box_union) { for (const aabb3f &box : boxes) { box_union->addInternalBox(box); @@ -1180,7 +1380,7 @@ void boxVectorUnion(const std::vector &boxes, aabb3f *box_union) * can be rotated * @param[in, out] box_union the union of the arguments */ -void getNodeBoxUnion(const NodeBox &nodebox, const ContentFeatures &features, +static void getNodeBoxUnion(const NodeBox &nodebox, const ContentFeatures &features, aabb3f *box_union) { switch(nodebox.type) { @@ -1450,32 +1650,101 @@ void NodeDefManager::applyTextureOverrides(const std::vector &o } } +#if CHECK_CLIENT_BUILD() void NodeDefManager::updateTextures(IGameDef *gamedef, void *progress_callback_args) { -#if CHECK_CLIENT_BUILD() infostream << "NodeDefManager::updateTextures(): Updating " "textures in node definitions" << std::endl; Client *client = (Client *)gamedef; ITextureSource *tsrc = client->tsrc(); IShaderSource *shdsrc = client->getShaderSource(); - auto smgr = client->getSceneManager(); - scene::IMeshManipulator *meshmanip = smgr->getMeshManipulator(); TextureSettings tsettings; tsettings.readSettings(); tsrc->setImageCaching(true); + const u32 size = m_content_features.size(); - u32 size = m_content_features.size(); + /* collect all textures we might use */ + std::unordered_set pool; for (u32 i = 0; i < size; i++) { ContentFeatures *f = &(m_content_features[i]); - f->updateTextures(tsrc, shdsrc, meshmanip, client, tsettings); - client->showUpdateProgressTexture(progress_callback_args, i, size); + f->preUpdateTextures(tsrc, pool, tsettings); } + /* texture pre-loading stage */ + const size_t arraymax = getArrayTextureMax(RenderingEngine::get_video_driver()); + // Group by size + std::unordered_map> sizes; + if (arraymax > 1) { + infostream << "Using array textures with " << arraymax << " layers" << std::endl; + size_t i = 0; + for (auto &image : pool) { + core::dimension2du dim = tsrc->getTextureDimensions(image); + client->showUpdateProgressTexture(progress_callback_args, + 0.33333f * ++i / pool.size()); + if (!dim.Width || !dim.Height) // error + continue; + sizes[v2u32(dim)].emplace_back(image); + } + } + + // create array textures as far as possible + size_t num_preloadable = 0, preload_progress = 0; + for (auto &it : sizes) { + if (it.second.size() < 2) + continue; + num_preloadable += it.second.size(); + } + PreLoadedTextures plt; + const auto &doBunch = [&] (const std::vector &bunch) { + PreLoadedTexture t; + t.texture = tsrc->addArrayTexture(bunch, &t.texture_id); + preload_progress += bunch.size(); + client->showUpdateProgressTexture(progress_callback_args, + 0.33333f + 0.33333f * preload_progress / num_preloadable); + if (t.texture) { + // Success: all of the images in this bunch can now refer to this texture + for (size_t idx = 0; idx < bunch.size(); idx++) { + t.texture_layer_idx = idx; + plt.add(bunch[idx], t); + } + } + }; + for (auto &it : sizes) { + if (it.second.size() < 2) + continue; + std::vector bunch; + for (auto &image : it.second) { + bunch.emplace_back(image); + if (bunch.size() == arraymax) { + doBunch(bunch); + bunch.clear(); + } + } + if (!bunch.empty()) + doBunch(bunch); + } + // note that standard textures aren't preloaded + + /* final step */ + for (u32 i = 0; i < size; i++) { + ContentFeatures *f = &(m_content_features[i]); + f->updateTextures(tsrc, shdsrc, client, &plt, tsettings); + f->updateMesh(client, tsettings); + f->collectMaterials(m_leaves_materials); + + client->showUpdateProgressTexture(progress_callback_args, + 0.66666f + 0.33333f * i / size); + } + SORT_AND_UNIQUE(m_leaves_materials); + verbosestream << "m_leaves_materials.size() = " << m_leaves_materials.size() + << std::endl; + + plt.printStats(infostream); tsrc->setImageCaching(false); -#endif } +#endif void NodeDefManager::serialize(std::ostream &os, u16 protocol_version) const { diff --git a/src/nodedef.h b/src/nodedef.h index 3477cb7db4..bb2852d402 100644 --- a/src/nodedef.h +++ b/src/nodedef.h @@ -14,7 +14,9 @@ #if CHECK_CLIENT_BUILD() #include "client/tile.h" #include +#include class Client; +struct PreLoadedTextures; #endif #include "itemgroup.h" #include "sound.h" // SoundSpec @@ -259,6 +261,24 @@ enum AlphaMode : u8 { AlphaMode_END // Dummy for validity check }; +#if CHECK_CLIENT_BUILD() +/** + * @brief get fitting material type for an alpha mode + */ +static inline MaterialType alpha_mode_to_material_type(AlphaMode mode) +{ + switch (mode) { + case ALPHAMODE_BLEND: + return TILE_MATERIAL_ALPHA; + case ALPHAMODE_OPAQUE: + return TILE_MATERIAL_OPAQUE; + case ALPHAMODE_CLIP: + default: + return TILE_MATERIAL_BASIC; + } +} +#endif + /* Stand-alone definition of a TileSpec (basically a server-side TileSpec) @@ -505,8 +525,13 @@ struct ContentFeatures } #if CHECK_CLIENT_BUILD() + void preUpdateTextures(ITextureSource *tsrc, + std::unordered_set &pool, const TextureSettings &tsettings); void updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc, - scene::IMeshManipulator *meshmanip, Client *client, const TextureSettings &tsettings); + Client *client, PreLoadedTextures *texture_pool, + const TextureSettings &tsettings); + void updateMesh(Client *client, const TextureSettings &tsettings); + void collectMaterials(std::vector &leaves_materials); #endif private: @@ -675,8 +700,9 @@ public: */ void applyTextureOverrides(const std::vector &overrides); +#if CHECK_CLIENT_BUILD() /*! - * Only the client uses this. Loads textures and shaders required for + * Loads textures and shaders required for * rendering the nodes. * @param gamedef must be a Client. * @param progress_cbk called each time a node is loaded. Arguments: @@ -685,6 +711,7 @@ public: * @param progress_cbk_args passed to the callback function */ void updateTextures(IGameDef *gamedef, void *progress_cbk_args); +#endif /*! * Writes the content of this manager to the given output stream. @@ -728,6 +755,12 @@ public: */ void resolveCrossrefs(); +#if CHECK_CLIENT_BUILD() + // Set of all shader IDs used by leaves-like nodes + // (kind of a hack but is needed for dynamic shadows) + std::vector m_leaves_materials; +#endif + private: /*! * Resets the manager to its initial state.