diff --git a/doc/lua_api.md b/doc/lua_api.md index f028a14d8..b604b317c 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -10172,9 +10172,12 @@ Used by `core.register_node`. mesh = "", -- File name of mesh when using "mesh" drawtype -- The center of the node is the model origin. - -- For legacy reasons, models in OBJ format use a scale of 1 node = 1 unit; - -- all other model file formats use a scale of 1 node = 10 units, - -- consistent with the scale used for entities. + -- For legacy reasons, this uses a different scale depending on the mesh: + -- 1. For glTF models: 10 units = 1 node (consistent with the scale for entities). + -- 2. For obj models: 1 unit = 1 node. + -- 3. For b3d and x models: 1 unit = 1 node if static, otherwise 10 units = 1 node. + -- Using static glTF or obj models is recommended. + -- You can use the `visual_scale` multiplier to achieve the expected scale. selection_box = { -- see [Node boxes] for possibilities diff --git a/irr/include/SkinnedMesh.h b/irr/include/SkinnedMesh.h index c9ea99365..a527db76e 100644 --- a/irr/include/SkinnedMesh.h +++ b/irr/include/SkinnedMesh.h @@ -26,12 +26,21 @@ class ISceneManager; class SkinnedMesh : public IAnimatedMesh { public: + + enum class SourceFormat { + B3D, + X, + GLTF, + OTHER, + }; + //! constructor - SkinnedMesh() : + SkinnedMesh(SourceFormat src_format) : EndFrame(0.f), FramesPerSecond(25.f), LastAnimatedFrame(-1), SkinnedLastFrame(false), HasAnimation(false), PreparedForSkinning(false), - AnimateNormals(true), HardwareSkinning(false) + AnimateNormals(true), HardwareSkinning(false), + SrcFormat(src_format) { SkinningBuffers = &LocalBuffers; } @@ -39,6 +48,10 @@ public: //! destructor virtual ~SkinnedMesh(); + //! The source (file) format the mesh was loaded from. + //! Important for legacy reasons pertaining to different mesh loader behavior. + SourceFormat getSourceFormat() const { return SrcFormat; } + //! If the duration is 0, it is a static (=non animated) mesh. f32 getMaxFrameNumber() const override; @@ -382,12 +395,14 @@ protected: bool PreparedForSkinning; bool AnimateNormals; bool HardwareSkinning; + + SourceFormat SrcFormat; }; // Interface for mesh loaders class SkinnedMeshBuilder : public SkinnedMesh { public: - SkinnedMeshBuilder() : SkinnedMesh() {} + SkinnedMeshBuilder(SourceFormat src_format) : SkinnedMesh(src_format) {} //! loaders should call this after populating the mesh // returns *this, so do not try to drop the mesh builder instance diff --git a/irr/src/CB3DMeshFileLoader.cpp b/irr/src/CB3DMeshFileLoader.cpp index e99bd2eed..51342f451 100644 --- a/irr/src/CB3DMeshFileLoader.cpp +++ b/irr/src/CB3DMeshFileLoader.cpp @@ -48,7 +48,7 @@ IAnimatedMesh *CB3DMeshFileLoader::createMesh(io::IReadFile *file) return 0; B3DFile = file; - AnimatedMesh = new scene::SkinnedMeshBuilder(); + AnimatedMesh = new scene::SkinnedMeshBuilder(SkinnedMesh::SourceFormat::B3D); ShowWarning = true; // If true a warning is issued if too many textures are used VerticesStart = 0; diff --git a/irr/src/CGLTFMeshFileLoader.cpp b/irr/src/CGLTFMeshFileLoader.cpp index f70f6692b..3f2096f40 100644 --- a/irr/src/CGLTFMeshFileLoader.cpp +++ b/irr/src/CGLTFMeshFileLoader.cpp @@ -347,7 +347,8 @@ IAnimatedMesh* SelfType::createMesh(io::IReadFile* file) const char *filename = file->getFileName().c_str(); try { tiniergltf::GlTF model = parseGLTF(file); - irr_ptr mesh(new SkinnedMeshBuilder()); + irr_ptr mesh(new SkinnedMeshBuilder( + SkinnedMesh::SourceFormat::GLTF)); MeshExtractor extractor(std::move(model), mesh.get()); try { extractor.load(); diff --git a/irr/src/CSceneManager.cpp b/irr/src/CSceneManager.cpp index b5a310287..05f8de71a 100644 --- a/irr/src/CSceneManager.cpp +++ b/irr/src/CSceneManager.cpp @@ -762,7 +762,7 @@ ISceneManager *CSceneManager::createNewSceneManager(bool cloneContent) //! Get a skinned mesh, which is not available as header-only code SkinnedMesh *CSceneManager::createSkinnedMesh() { - return new SkinnedMesh(); + return new SkinnedMesh(SkinnedMesh::SourceFormat::OTHER); } // creates a scenemanager diff --git a/irr/src/CXMeshFileLoader.cpp b/irr/src/CXMeshFileLoader.cpp index ed8c18350..d93502d6b 100644 --- a/irr/src/CXMeshFileLoader.cpp +++ b/irr/src/CXMeshFileLoader.cpp @@ -54,7 +54,7 @@ IAnimatedMesh *CXMeshFileLoader::createMesh(io::IReadFile *file) u32 time = os::Timer::getRealTime(); #endif - AnimatedMesh = new SkinnedMeshBuilder(); + AnimatedMesh = new SkinnedMeshBuilder(SkinnedMesh::SourceFormat::X); SkinnedMesh *res = nullptr; if (load(file)) { diff --git a/src/client/content_mapblock.cpp b/src/client/content_mapblock.cpp index f5b528287..3edba95e3 100644 --- a/src/client/content_mapblock.cpp +++ b/src/client/content_mapblock.cpp @@ -1676,7 +1676,7 @@ void MapblockMeshGenerator::drawMeshNode() if (cur_node.f->mesh_ptr) { // clone and rotate mesh - mesh = cloneMesh(cur_node.f->mesh_ptr); + mesh = cloneStaticMesh(cur_node.f->mesh_ptr); bool modified = true; if (facedir) rotateMeshBy6dFacedir(mesh, facedir); diff --git a/src/client/mesh.cpp b/src/client/mesh.cpp index a2c0ae327..808dcdd18 100644 --- a/src/client/mesh.cpp +++ b/src/client/mesh.cpp @@ -3,6 +3,8 @@ // Copyright (C) 2010-2013 celeron55, Perttu Ahola #include "mesh.h" +#include "IMeshBuffer.h" +#include "SSkinMeshBuffer.h" #include "debug.h" #include "log.h" #include @@ -102,6 +104,21 @@ scene::IAnimatedMesh* createCubeMesh(v3f scale) return anim_mesh; } +template +inline static void transformMeshBuffer(scene::IMeshBuffer *buf, + const F &transform_vertex) +{ + const u32 stride = getVertexPitchFromType(buf->getVertexType()); + u32 vertex_count = buf->getVertexCount(); + u8 *vertices = (u8 *)buf->getVertices(); + for (u32 i = 0; i < vertex_count; i++) { + auto *vertex = (video::S3DVertex *)(vertices + i * stride); + transform_vertex(vertex); + } + buf->setDirty(scene::EBT_VERTEX); + buf->recalculateBoundingBox(); +} + void scaleMesh(scene::IMesh *mesh, v3f scale) { if (mesh == NULL) @@ -112,14 +129,9 @@ void scaleMesh(scene::IMesh *mesh, v3f scale) u32 mc = mesh->getMeshBufferCount(); for (u32 j = 0; j < mc; j++) { scene::IMeshBuffer *buf = mesh->getMeshBuffer(j); - const u32 stride = getVertexPitchFromType(buf->getVertexType()); - u32 vertex_count = buf->getVertexCount(); - u8 *vertices = (u8 *)buf->getVertices(); - for (u32 i = 0; i < vertex_count; i++) - ((video::S3DVertex *)(vertices + i * stride))->Pos *= scale; - - buf->setDirty(scene::EBT_VERTEX); - buf->recalculateBoundingBox(); + transformMeshBuffer(buf, [scale](video::S3DVertex *vertex) { + vertex->Pos *= scale; + }); // calculate total bounding box if (j == 0) @@ -140,14 +152,9 @@ void translateMesh(scene::IMesh *mesh, v3f vec) u32 mc = mesh->getMeshBufferCount(); for (u32 j = 0; j < mc; j++) { scene::IMeshBuffer *buf = mesh->getMeshBuffer(j); - const u32 stride = getVertexPitchFromType(buf->getVertexType()); - u32 vertex_count = buf->getVertexCount(); - u8 *vertices = (u8 *)buf->getVertices(); - for (u32 i = 0; i < vertex_count; i++) - ((video::S3DVertex *)(vertices + i * stride))->Pos += vec; - - buf->setDirty(scene::EBT_VERTEX); - buf->recalculateBoundingBox(); + transformMeshBuffer(buf, [vec](video::S3DVertex *vertex) { + vertex->Pos += vec; + }); // calculate total bounding box if (j == 0) @@ -330,44 +337,40 @@ bool checkMeshNormals(scene::IMesh *mesh) return true; } +template +static scene::IMeshBuffer *cloneMeshBuffer(scene::IMeshBuffer *mesh_buffer) +{ + auto *v = static_cast(mesh_buffer->getVertices()); + u16 *indices = mesh_buffer->getIndices(); + auto *cloned_buffer = new SMeshBufferType(); + cloned_buffer->append(v, mesh_buffer->getVertexCount(), indices, + mesh_buffer->getIndexCount()); + // Rigidly animated meshes may have transformation matrices that need to be applied + if (auto *sbuf = dynamic_cast(mesh_buffer)) { + transformMeshBuffer(cloned_buffer, [sbuf](video::S3DVertex *vertex) { + sbuf->Transformation.transformVect(vertex->Pos); + vertex->Normal = sbuf->Transformation.rotateAndScaleVect(vertex->Normal); + vertex->Normal.normalize(); + }); + } + return cloned_buffer; +} + scene::IMeshBuffer* cloneMeshBuffer(scene::IMeshBuffer *mesh_buffer) { switch (mesh_buffer->getVertexType()) { - case video::EVT_STANDARD: { - video::S3DVertex *v = (video::S3DVertex *) mesh_buffer->getVertices(); - u16 *indices = mesh_buffer->getIndices(); - scene::SMeshBuffer *cloned_buffer = new scene::SMeshBuffer(); - cloned_buffer->append(v, mesh_buffer->getVertexCount(), indices, - mesh_buffer->getIndexCount()); - return cloned_buffer; + case video::EVT_STANDARD: + return cloneMeshBuffer(mesh_buffer); + case video::EVT_2TCOORDS: + return cloneMeshBuffer(mesh_buffer); + case video::EVT_TANGENTS: + return cloneMeshBuffer(mesh_buffer); } - case video::EVT_2TCOORDS: { - video::S3DVertex2TCoords *v = - (video::S3DVertex2TCoords *) mesh_buffer->getVertices(); - u16 *indices = mesh_buffer->getIndices(); - scene::SMeshBufferLightMap *cloned_buffer = - new scene::SMeshBufferLightMap(); - cloned_buffer->append(v, mesh_buffer->getVertexCount(), indices, - mesh_buffer->getIndexCount()); - return cloned_buffer; - } - case video::EVT_TANGENTS: { - video::S3DVertexTangents *v = - (video::S3DVertexTangents *) mesh_buffer->getVertices(); - u16 *indices = mesh_buffer->getIndices(); - scene::SMeshBufferTangents *cloned_buffer = - new scene::SMeshBufferTangents(); - cloned_buffer->append(v, mesh_buffer->getVertexCount(), indices, - mesh_buffer->getIndexCount()); - return cloned_buffer; - } - } - // This should not happen. sanity_check(false); return NULL; } -scene::SMesh* cloneMesh(scene::IMesh *src_mesh) +scene::SMesh* cloneStaticMesh(scene::IMesh *src_mesh) { scene::SMesh* dst_mesh = new scene::SMesh(); for (u16 j = 0; j < src_mesh->getMeshBufferCount(); j++) { diff --git a/src/client/mesh.h b/src/client/mesh.h index d8eb6080e..53c54fc51 100644 --- a/src/client/mesh.h +++ b/src/client/mesh.h @@ -93,10 +93,8 @@ void rotateMeshYZby (scene::IMesh *mesh, f64 degrees); */ scene::IMeshBuffer* cloneMeshBuffer(scene::IMeshBuffer *mesh_buffer); -/* - Clone the mesh. -*/ -scene::SMesh* cloneMesh(scene::IMesh *src_mesh); +/// Clone a mesh. For an animated mesh, this will clone the static pose. +scene::SMesh* cloneStaticMesh(scene::IMesh *src_mesh); /* Convert nodeboxes to mesh. Each tile goes into a different buffer. diff --git a/src/client/wieldmesh.cpp b/src/client/wieldmesh.cpp index baa72f1d9..bdd24a727 100644 --- a/src/client/wieldmesh.cpp +++ b/src/client/wieldmesh.cpp @@ -255,7 +255,7 @@ void WieldMeshSceneNode::setExtruded(const std::string &imagename, dim = core::dimension2d(dim.Width, frame_height); } scene::IMesh *original = g_extrusion_mesh_cache->create(dim); - scene::SMesh *mesh = cloneMesh(original); + scene::SMesh *mesh = cloneStaticMesh(original); original->drop(); //set texture mesh->getMeshBuffer(0)->getMaterial().setTexture(0, @@ -639,7 +639,7 @@ scene::SMesh *getExtrudedMesh(ITextureSource *tsrc, // get mesh core::dimension2d dim = texture->getSize(); scene::IMesh *original = g_extrusion_mesh_cache->create(dim); - scene::SMesh *mesh = cloneMesh(original); + scene::SMesh *mesh = cloneStaticMesh(original); original->drop(); //set texture diff --git a/src/nodedef.cpp b/src/nodedef.cpp index d4dc16a61..04f1959c1 100644 --- a/src/nodedef.cpp +++ b/src/nodedef.cpp @@ -4,6 +4,7 @@ #include "nodedef.h" +#include "SAnimatedMesh.h" #include "itemdef.h" #if CHECK_CLIENT_BUILD() #include "client/mesh.h" @@ -13,6 +14,7 @@ #include "client/texturesource.h" #include "client/tile.h" #include +#include #include #endif #include "log.h" @@ -959,23 +961,44 @@ void ContentFeatures::updateTextures(ITextureSource *tsrc, IShaderSource *shdsrc palette = tsrc->getPalette(palette_name); if (drawtype == NDT_MESH && !mesh.empty()) { - // Read the mesh and apply scale - mesh_ptr = client->getMesh(mesh); - if (mesh_ptr) { - v3f scale = v3f(BS) * visual_scale; - scaleMesh(mesh_ptr, scale); + // Note: By freshly reading, we get an unencumbered mesh. + if (scene::IMesh *src_mesh = client->getMesh(mesh)) { + bool apply_bs = false; + // For frame-animated meshes, always get the first frame, + // which holds a model for which we can eventually get the static pose. + while (auto *src_meshes = dynamic_cast(src_mesh)) { + src_mesh = src_meshes->getMesh(0.0f); + src_mesh->grab(); + src_meshes->drop(); + } + if (auto *skinned_mesh = dynamic_cast(src_mesh)) { + // Compatibility: Animated meshes, as well as static gltf meshes, are not scaled by BS. + // See https://github.com/luanti-org/luanti/pull/16112#issuecomment-2881860329 + bool is_gltf = skinned_mesh->getSourceFormat() == + scene::SkinnedMesh::SourceFormat::GLTF; + apply_bs = skinned_mesh->isStatic() && !is_gltf; + // Nodes do not support mesh animation, so we clone the static pose. + // This simplifies working with the mesh: We can just scale the vertices + // as transformations have already been applied. + mesh_ptr = cloneStaticMesh(src_mesh); + src_mesh->drop(); + } else { + auto *static_mesh = dynamic_cast(src_mesh); + assert(static_mesh); + mesh_ptr = static_mesh; + // Compatibility: Apply BS scaling to static meshes (.obj). See #15811. + apply_bs = true; + } + scaleMesh(mesh_ptr, v3f((apply_bs ? BS : 1.0f) * visual_scale)); recalculateBoundingBox(mesh_ptr); if (!checkMeshNormals(mesh_ptr)) { + // 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); - } else { - // Animation is not supported, but we need to reset it to - // default state if it is animated. - // Note: recalculateNormals() also does this hence the else-block - if (mesh_ptr->getMeshType() == scene::EAMT_SKINNED) - ((scene::SkinnedMesh*) mesh_ptr)->resetAnimation(); } + } else { + mesh_ptr = nullptr; } } } diff --git a/src/nodedef.h b/src/nodedef.h index 71a61896b..967e3fcd4 100644 --- a/src/nodedef.h +++ b/src/nodedef.h @@ -342,7 +342,7 @@ struct ContentFeatures enum NodeDrawType drawtype; std::string mesh; #if CHECK_CLIENT_BUILD() - scene::IMesh *mesh_ptr; // mesh in case of mesh node + scene::SMesh *mesh_ptr; // mesh in case of mesh node video::SColor minimap_color; #endif float visual_scale; // Misc. scale parameter