From b20d10c0cc96cfcd93468b8e31e47ab1977a9555 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Wed, 4 Jun 2025 16:39:51 +0300 Subject: #4148 Skeleton Translation --- indra/llappearance/llavatarappearance.cpp | 47 +++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) (limited to 'indra/llappearance/llavatarappearance.cpp') diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index 3d66809ed6..34d6b4db83 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -34,11 +34,11 @@ #include "llpolymorph.h" #include "llpolymesh.h" #include "llpolyskeletaldistortion.h" -#include "llstl.h" #include "lltexglobalcolor.h" #include "llwearabledata.h" #include "boost/bind.hpp" #include "boost/tokenizer.hpp" +#include "v4math.h" using namespace LLAvatarAppearanceDefines; @@ -71,6 +71,7 @@ public: mChildren.clear(); } bool parseXml(LLXmlTreeNode* node); + glm::mat4 getJointMatrix(); private: std::string mName; @@ -105,11 +106,14 @@ public: S32 getNumBones() const { return mNumBones; } S32 getNumCollisionVolumes() const { return mNumCollisionVolumes; } +private: + typedef std::vector bone_info_list_t; + static void getJointRestMatrices(const bone_info_list_t& bone_list, LLAvatarAppearance::joint_rest_map_t& result, const glm::mat4 &parent_mat); + private: S32 mNumBones; S32 mNumCollisionVolumes; LLAvatarAppearance::joint_alias_map_t mJointAliasMap; - typedef std::vector bone_info_list_t; bone_info_list_t mBoneInfoList; }; @@ -1623,6 +1627,21 @@ bool LLAvatarBoneInfo::parseXml(LLXmlTreeNode* node) return true; } + +glm::mat4 LLAvatarBoneInfo::getJointMatrix() +{ + glm::mat4 mat(1.0f); + // 1. Scaling + mat = glm::scale(mat, glm::vec3(mScale[0], mScale[1], mScale[2])); + // 2. Rotation (Euler angles rad) + mat = glm::rotate(mat, mRot[0], glm::vec3(1, 0, 0)); + mat = glm::rotate(mat, mRot[1], glm::vec3(0, 1, 0)); + mat = glm::rotate(mat, mRot[2], glm::vec3(0, 0, 1)); + // 3. Position + mat = glm::translate(mat, glm::vec3(mPos[0], mPos[1], mPos[2])); + return mat; +} + //----------------------------------------------------------------------------- // LLAvatarSkeletonInfo::parseXml() //----------------------------------------------------------------------------- @@ -1653,6 +1672,22 @@ bool LLAvatarSkeletonInfo::parseXml(LLXmlTreeNode* node) return true; } +void LLAvatarSkeletonInfo::getJointRestMatrices( + const bone_info_list_t& bone_list, + LLAvatarAppearance::joint_rest_map_t& result, + const glm::mat4& parent_mat) +{ + for (LLAvatarBoneInfo* bone_info : bone_list) + { + if (bone_info->mIsJoint) + { + glm::mat4 rest_mat = parent_mat * bone_info->getJointMatrix(); + result[bone_info->mName] = rest_mat; + getJointRestMatrices(bone_info->mChildren, result, rest_mat); + } + } +} + //Make aliases for joint and push to map. void LLAvatarAppearance::makeJointAliases(LLAvatarBoneInfo *bone_info) { @@ -1714,6 +1749,14 @@ const LLAvatarAppearance::joint_alias_map_t& LLAvatarAppearance::getJointAliases return mJointAliasMap; } +LLAvatarAppearance::joint_rest_map_t LLAvatarAppearance:: getJointRestMatrices() const +{ + LLAvatarAppearance::joint_rest_map_t result; + glm::mat4 identity(1.f); + LLAvatarSkeletonInfo::getJointRestMatrices(sAvatarSkeletonInfo->mBoneInfoList, result, identity); + return result; +} + //----------------------------------------------------------------------------- // parseXmlSkeletonNode(): parses nodes from XML tree -- cgit v1.3 From db957313918d1716aa5087f283fd51d7fe49a357 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Tue, 10 Jun 2025 23:49:52 +0300 Subject: #4148 Skeleton Translation #3 'non joints' also need adjustments --- indra/llappearance/llavatarappearance.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'indra/llappearance/llavatarappearance.cpp') diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index 34d6b4db83..a3032325c9 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -1679,12 +1679,9 @@ void LLAvatarSkeletonInfo::getJointRestMatrices( { for (LLAvatarBoneInfo* bone_info : bone_list) { - if (bone_info->mIsJoint) - { - glm::mat4 rest_mat = parent_mat * bone_info->getJointMatrix(); - result[bone_info->mName] = rest_mat; - getJointRestMatrices(bone_info->mChildren, result, rest_mat); - } + glm::mat4 rest_mat = parent_mat * bone_info->getJointMatrix(); + result[bone_info->mName] = rest_mat; + getJointRestMatrices(bone_info->mChildren, result, rest_mat); } } -- cgit v1.3 From 8322a9a61e951275278fbf80b0a46880f5318107 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Thu, 12 Jun 2025 14:21:40 +0300 Subject: #4147 Joint Overrides #2 --- indra/llappearance/llavatarappearance.cpp | 22 ++++-- indra/llappearance/llavatarappearance.h | 3 +- indra/newview/gltf/llgltfloader.cpp | 126 ++++++++++++++++-------------- indra/newview/gltf/llgltfloader.h | 12 ++- indra/newview/llmodelpreview.cpp | 7 +- 5 files changed, 97 insertions(+), 73 deletions(-) (limited to 'indra/llappearance/llavatarappearance.cpp') diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index a3032325c9..f3e474dba7 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -108,7 +108,12 @@ public: private: typedef std::vector bone_info_list_t; - static void getJointRestMatrices(const bone_info_list_t& bone_list, LLAvatarAppearance::joint_rest_map_t& result, const glm::mat4 &parent_mat); + static void getJointRestMatrices( + const bone_info_list_t& bone_list, + const std::string &parent_name, + LLAvatarAppearance::joint_rest_map_t& rest, + LLAvatarAppearance::joint_parent_map_t& parent_map, + const glm::mat4& parent_mat); private: S32 mNumBones; @@ -1674,14 +1679,17 @@ bool LLAvatarSkeletonInfo::parseXml(LLXmlTreeNode* node) void LLAvatarSkeletonInfo::getJointRestMatrices( const bone_info_list_t& bone_list, - LLAvatarAppearance::joint_rest_map_t& result, + const std::string& parent_name, + LLAvatarAppearance::joint_rest_map_t& rest, + LLAvatarAppearance::joint_parent_map_t& parent_map, const glm::mat4& parent_mat) { for (LLAvatarBoneInfo* bone_info : bone_list) { glm::mat4 rest_mat = parent_mat * bone_info->getJointMatrix(); - result[bone_info->mName] = rest_mat; - getJointRestMatrices(bone_info->mChildren, result, rest_mat); + rest[bone_info->mName] = rest_mat; + parent_map[bone_info->mName] = parent_name; + getJointRestMatrices(bone_info->mChildren, bone_info->mName, rest, parent_map, rest_mat); } } @@ -1746,12 +1754,10 @@ const LLAvatarAppearance::joint_alias_map_t& LLAvatarAppearance::getJointAliases return mJointAliasMap; } -LLAvatarAppearance::joint_rest_map_t LLAvatarAppearance:: getJointRestMatrices() const +void LLAvatarAppearance:: getJointRestMatrices(LLAvatarAppearance::joint_rest_map_t& rest_map, LLAvatarAppearance::joint_parent_map_t& parent_map) const { - LLAvatarAppearance::joint_rest_map_t result; glm::mat4 identity(1.f); - LLAvatarSkeletonInfo::getJointRestMatrices(sAvatarSkeletonInfo->mBoneInfoList, result, identity); - return result; + LLAvatarSkeletonInfo::getJointRestMatrices(sAvatarSkeletonInfo->mBoneInfoList, std::string(), rest_map, parent_map, identity); } diff --git a/indra/llappearance/llavatarappearance.h b/indra/llappearance/llavatarappearance.h index cea2a837cd..e48d80b8ce 100644 --- a/indra/llappearance/llavatarappearance.h +++ b/indra/llappearance/llavatarappearance.h @@ -154,8 +154,9 @@ public: const avatar_joint_list_t& getSkeleton() { return mSkeleton; } typedef std::map joint_alias_map_t; const joint_alias_map_t& getJointAliases(); + typedef std::map joint_parent_map_t; // matrix plus parent typedef std::map joint_rest_map_t; - joint_rest_map_t getJointRestMatrices() const; + void getJointRestMatrices(joint_rest_map_t& rest_map, joint_parent_map_t& parent_map) const; protected: static bool parseSkeletonFile(const std::string& filename, LLXmlTree& skeleton_xml_tree); diff --git a/indra/newview/gltf/llgltfloader.cpp b/indra/newview/gltf/llgltfloader.cpp index 40b9334e63..292fd09035 100644 --- a/indra/newview/gltf/llgltfloader.cpp +++ b/indra/newview/gltf/llgltfloader.cpp @@ -95,7 +95,8 @@ LLGLTFLoader::LLGLTFLoader(std::string filename, std::map &jointAliasMap, U32 maxJointsPerMesh, U32 modelLimit, - joint_rest_map_t jointRestMatrices) //, + joint_viewer_rest_map_t jointRestMatrices, + joint_viewer_parent_map_t jointParentMap) //, //bool preprocess) : LLModelLoader( filename, lod, @@ -107,12 +108,13 @@ LLGLTFLoader::LLGLTFLoader(std::string filename, jointTransformMap, jointsFromNodes, jointAliasMap, - maxJointsPerMesh ), + maxJointsPerMesh ) //mPreprocessGLTF(preprocess), - mMeshesLoaded(false), - mMaterialsLoaded(false), - mGeneratedModelLimit(modelLimit), - mJointRestMatrices(jointRestMatrices) + , mMeshesLoaded(false) + , mMaterialsLoaded(false) + , mGeneratedModelLimit(modelLimit) + , mJointViewerRestMatrices(jointRestMatrices) + , mJointViewerParentMap(jointParentMap) { } @@ -929,6 +931,16 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) if (mInverseBindMatrices.size() <= skin_idx) { mInverseBindMatrices.resize(skin_idx + 1); + mAlternateBindMatrices.resize(skin_idx + 1); + } + + // Build gltf rest matrices + // This is very inefficienct, todo: run from root to children, not from children to root + joint_node_mat4_map_t gltf_rest_map; + for (S32 i = 0; i < joint_count; i++) + { + S32 joint = skin.mJoints[i]; + gltf_rest_map[joint] = buildGltfRestMatrix(joint, skin); } for (S32 i = 0; i < joint_count; i++) @@ -943,86 +955,84 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) legal_joint = true; } + // Compute bind matrices + if (!legal_joint) { - // add placeholder to not break index + // Add placeholder to not break index. + // Not going to be used by viewer, will be stripped from skin_info. LLMatrix4 gltf_transform; gltf_transform.setIdentity(); mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); } else if (inverse_count > i) { + // Transalte existing bind matrix to viewer's skeleton + // todo: probably should be 'to viewer's overriden skeleton' glm::mat4 original_bind_matrix = glm::inverse(skin.mInverseBindMatricesData[i]); glm::mat4 rotated_original = coord_system_rotation * original_bind_matrix; - glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(skin, i, legal_name); + glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(gltf_rest_map, joint, legal_name); glm::mat4 tranlated_original = skeleton_transform * rotated_original; glm::mat4 final_inverse_bind_matrix = glm::inverse(tranlated_original); LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(final_inverse_bind_matrix)); - - LL_INFOS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " val: " << gltf_transform << LL_ENDL; + LL_INFOS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " Translated val: " << gltf_transform << LL_ENDL; mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); } else { + // If bind matrices aren't present (they are optional in gltf), + // assume an identy matrix + // todo: find a model with this, might need to use rotated matrix glm::mat4 inv_bind(1.0f); - glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(skin, i, legal_name); + glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(gltf_rest_map, joint, legal_name); inv_bind = glm::inverse(skeleton_transform * inv_bind); LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(inv_bind)); - gltf_transform.setIdentity(); - LL_INFOS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " val: " << gltf_transform << LL_ENDL; + LL_INFOS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " Generated val: " << gltf_transform << LL_ENDL; mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); } - // Todo: this seems blatantly wrong - glm::mat4 joint_mat = jointNode.mMatrix; - S32 root_joint = findValidRootJointNode(joint, skin); // skeleton can have multiple real roots and one gltf root to group them - if (root_joint == joint) + // Compute Alternative matrices also known as overrides + + glm::mat4 joint_mat = glm::mat4(1.f); + if (legal_joint) { - // This is very likely incomplete in some way. - joint_mat = coord_system_rotation; - if (mApplyXYRotation) + joint_viewer_parent_map_t::const_iterator found = mJointViewerParentMap.find(legal_name); + if (found != mJointViewerParentMap.end() && !found->second.empty()) { - joint_mat = coord_system_rotationxy * joint_mat; + glm::mat4 gltf_joint_rest_pose = coord_system_rotation * buildGltfRestMatrix(joint, skin); + if (mApplyXYRotation) + { + gltf_joint_rest_pose = coord_system_rotationxy * gltf_joint_rest_pose; + } + // Compute viewer's override by moving joint to viewer's base + // This might be overused or somewhat incorect for regular cases + // But this logic should be solid for cases where model lacks + // parent joints that viewer has. + // ex: like boots that have only knees and feet, but no pelvis, + // or anything else, in which case we take viewer's pelvis + glm::mat4 viewer_rest = mJointViewerRestMatrices[found->second]; + joint_mat = glm::inverse(viewer_rest) * gltf_joint_rest_pose; } } LLMatrix4 original_joint_transform(glm::value_ptr(joint_mat)); + // Viewer seems to care only about translation part, + // but for parity with collada taking original value + LLMatrix4 newInverse = LLMatrix4(mInverseBindMatrices[skin_idx].back().getF32ptr()); + newInverse.setTranslation(original_joint_transform.getTranslation()); + LL_INFOS("GLTF_DEBUG") << "mAlternateBindMatrix name: " << legal_name << " val: " << original_joint_transform << LL_ENDL; mAlternateBindMatrices[skin_idx].push_back(LLMatrix4a(original_joint_transform)); - if (!legal_joint) - { - LL_DEBUGS("GLTF") << "Ignoring unrecognized joint: " << legal_name << LL_ENDL; - continue; - } - - // Debug: Log original joint matrix - glm::mat4 gltf_joint_matrix = jointNode.mMatrix; - LL_INFOS("GLTF_DEBUG") << "Joint '" << legal_name << "' original matrix:" << LL_ENDL; - for(int i = 0; i < 4; i++) - { - LL_INFOS("GLTF_DEBUG") << " [" << gltf_joint_matrix[i][0] << ", " << gltf_joint_matrix[i][1] - << ", " << gltf_joint_matrix[i][2] << ", " << gltf_joint_matrix[i][3] << "]" << LL_ENDL; - } - - // Apply coordinate system rotation to joint transform - glm::mat4 rotated_joint_matrix = coord_system_rotation * gltf_joint_matrix; - - // Debug: Log rotated joint matrix - LL_INFOS("GLTF_DEBUG") << "Joint '" << legal_name << "' rotated matrix:" << LL_ENDL; - for(int i = 0; i < 4; i++) + if (legal_joint) { - LL_INFOS("GLTF_DEBUG") << " [" << rotated_joint_matrix[i][0] << ", " << rotated_joint_matrix[i][1] - << ", " << rotated_joint_matrix[i][2] << ", " << rotated_joint_matrix[i][3] << "]" << LL_ENDL; + // Might be needed for uploader UI to correctly identify overriden joints + // but going to be incorrect if multiple skins are present + mJointList[legal_name] = newInverse; + mJointsFromNode.push_front(legal_name); } - - LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(rotated_joint_matrix)); - mJointList[legal_name] = gltf_transform; - mJointsFromNode.push_front(legal_name); - - LL_INFOS("GLTF_DEBUG") << "mJointList name: " << legal_name << " val: " << gltf_transform << LL_ENDL; } } @@ -1164,10 +1174,11 @@ glm::mat4 LLGLTFLoader::buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF // This function computes the transformation matrix needed to convert from GLTF skeleton space // to viewer skeleton space for a specific joint -glm::mat4 LLGLTFLoader::computeGltfToViewerSkeletonTransform(const LL::GLTF::Skin& gltf_skin, S32 gltf_joint_index, const std::string& joint_name) const + +glm::mat4 LLGLTFLoader::computeGltfToViewerSkeletonTransform(const joint_node_mat4_map_t& gltf_rest_map, S32 gltf_node_index, const std::string& joint_name) const { - joint_rest_map_t::const_iterator found = mJointRestMatrices.find(joint_name); - if (found == mJointRestMatrices.end()) + joint_viewer_rest_map_t::const_iterator found = mJointViewerRestMatrices.find(joint_name); + if (found == mJointViewerRestMatrices.end()) { // For now assume they are identical and return an identity (for ease of debuging) // But there should be no joints viewer isn't aware about @@ -1177,19 +1188,18 @@ glm::mat4 LLGLTFLoader::computeGltfToViewerSkeletonTransform(const LL::GLTF::Ski glm::mat4 viewer_joint_rest_pose = found->second; // Get the GLTF joint's rest pose (in GLTF coordinate system) - S32 joint_node_index = gltf_skin.mJoints[gltf_joint_index]; - glm::mat4 gltf_joint_rest_pose = buildGltfRestMatrix(joint_node_index, gltf_skin); - gltf_joint_rest_pose = coord_system_rotation * gltf_joint_rest_pose; + const glm::mat4 &gltf_joint_rest_pose = gltf_rest_map.at(gltf_node_index); + glm::mat4 rest_pose = coord_system_rotation * gltf_joint_rest_pose; LL_INFOS("GLTF_DEBUG") << "rest matrix for joint " << joint_name << ": "; - LLMatrix4 transform(glm::value_ptr(gltf_joint_rest_pose)); + LLMatrix4 transform(glm::value_ptr(rest_pose)); LL_CONT << transform << LL_ENDL; // Compute transformation from GLTF space to viewer space // This assumes both skeletons are in rest pose initially - return viewer_joint_rest_pose * glm::inverse(gltf_joint_rest_pose); + return viewer_joint_rest_pose * glm::inverse(rest_pose); } bool LLGLTFLoader::checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx) diff --git a/indra/newview/gltf/llgltfloader.h b/indra/newview/gltf/llgltfloader.h index 6009a8e0c7..bf5f830d47 100644 --- a/indra/newview/gltf/llgltfloader.h +++ b/indra/newview/gltf/llgltfloader.h @@ -121,7 +121,9 @@ class LLGLTFLoader : public LLModelLoader { public: typedef std::map material_map; - typedef std::map joint_rest_map_t; + typedef std::map joint_viewer_parent_map_t; + typedef std::map joint_viewer_rest_map_t; + typedef std::map joint_node_mat4_map_t; LLGLTFLoader(std::string filename, S32 lod, @@ -135,7 +137,8 @@ class LLGLTFLoader : public LLModelLoader std::map &jointAliasMap, U32 maxJointsPerMesh, U32 modelLimit, - joint_rest_map_t jointRestMatrices); //, + joint_viewer_rest_map_t jointRestMatrices, + joint_viewer_parent_map_t jointParentPap); //, //bool preprocess ); virtual ~LLGLTFLoader(); @@ -169,7 +172,8 @@ protected: // GLTF isn't aware of viewer's skeleton and uses it's own, // so need to take viewer's joints and use them to // recalculate iverse bind matrices - joint_rest_map_t mJointRestMatrices; + joint_viewer_rest_map_t mJointViewerRestMatrices; + joint_viewer_parent_map_t mJointViewerParentMap; // vector of vectors because of a posibility of having more than one skin typedef std::vector bind_matrices_t; @@ -191,7 +195,7 @@ private: S32 findGLTFRootJointNode(const LL::GLTF::Skin& gltf_skin) const; // if there are multiple roots, gltf stores them under one commor joint S32 findParentNode(S32 node) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const; - glm::mat4 computeGltfToViewerSkeletonTransform(const LL::GLTF::Skin& gltf_skin, S32 joint_index, const std::string& joint_name) const; + glm::mat4 computeGltfToViewerSkeletonTransform(const joint_node_mat4_map_t &gltf_rest_map, S32 gltf_node_index, const std::string& joint_name) const; bool checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx); void checkForXYrotation(const LL::GLTF::Skin& gltf_skin); LLUUID imageBufferToTextureUUID(const gltf_texture& tex); diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index 40b6d186b8..d6b438a6e2 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -811,7 +811,9 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable else { LLVOAvatar* av = getPreviewAvatar(); - LLAvatarAppearance::joint_rest_map_t rest_pose = av->getJointRestMatrices(); + LLAvatarAppearance::joint_rest_map_t rest_pose; + LLAvatarAppearance::joint_parent_map_t rest_parent_map; + av->getJointRestMatrices(rest_pose, rest_parent_map); mModelLoader = new LLGLTFLoader( filename, lod, @@ -825,7 +827,8 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable joint_alias_map, LLSkinningUtil::getMaxJointCount(), gSavedSettings.getU32("ImporterModelLimit"), - rest_pose); + rest_pose, + rest_parent_map); } if (force_disable_slm) -- cgit v1.3 From 54660c8931593ceb465605acf872d5227e1d2d63 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Fri, 13 Jun 2025 18:33:49 +0300 Subject: #4147 Joint Overrides #3 Remande skeleton translation from default skeleton to overriden skeleton --- indra/llappearance/CMakeLists.txt | 1 + indra/llappearance/llavatarappearance.cpp | 36 +++---- indra/llappearance/llavatarappearance.h | 3 +- indra/llappearance/lljointdata.h | 44 ++++++++ indra/newview/gltf/llgltfloader.cpp | 162 ++++++++++++++++++++++-------- indra/newview/gltf/llgltfloader.h | 39 ++++++- indra/newview/llmodelpreview.cpp | 9 +- 7 files changed, 223 insertions(+), 71 deletions(-) create mode 100644 indra/llappearance/lljointdata.h (limited to 'indra/llappearance/llavatarappearance.cpp') diff --git a/indra/llappearance/CMakeLists.txt b/indra/llappearance/CMakeLists.txt index c3be8bc78e..6744c8d8a4 100644 --- a/indra/llappearance/CMakeLists.txt +++ b/indra/llappearance/CMakeLists.txt @@ -14,6 +14,7 @@ set(llappearance_SOURCE_FILES llavatarjoint.cpp llavatarjointmesh.cpp lldriverparam.cpp + lljointdata.h lllocaltextureobject.cpp llpolyskeletaldistortion.cpp llpolymesh.cpp diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index f3e474dba7..13bea1e5ea 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -29,6 +29,7 @@ #include "llavatarappearance.h" #include "llavatarappearancedefines.h" #include "llavatarjointmesh.h" +#include "lljointdata.h" #include "llstl.h" #include "lldir.h" #include "llpolymorph.h" @@ -108,11 +109,9 @@ public: private: typedef std::vector bone_info_list_t; - static void getJointRestMatrices( - const bone_info_list_t& bone_list, - const std::string &parent_name, - LLAvatarAppearance::joint_rest_map_t& rest, - LLAvatarAppearance::joint_parent_map_t& parent_map, + static void getJointMatricesAndHierarhy( + LLAvatarBoneInfo* bone_info, + LLJointData& data, const glm::mat4& parent_mat); private: @@ -1677,19 +1676,18 @@ bool LLAvatarSkeletonInfo::parseXml(LLXmlTreeNode* node) return true; } -void LLAvatarSkeletonInfo::getJointRestMatrices( - const bone_info_list_t& bone_list, - const std::string& parent_name, - LLAvatarAppearance::joint_rest_map_t& rest, - LLAvatarAppearance::joint_parent_map_t& parent_map, +void LLAvatarSkeletonInfo::getJointMatricesAndHierarhy( + LLAvatarBoneInfo* bone_info, + LLJointData& data, const glm::mat4& parent_mat) { - for (LLAvatarBoneInfo* bone_info : bone_list) + data.mName = bone_info->mName; + data.mJointMatrix = bone_info->getJointMatrix(); + data.mRestMatrix = parent_mat * data.mJointMatrix; + for (LLAvatarBoneInfo* child_info : bone_info->mChildren) { - glm::mat4 rest_mat = parent_mat * bone_info->getJointMatrix(); - rest[bone_info->mName] = rest_mat; - parent_map[bone_info->mName] = parent_name; - getJointRestMatrices(bone_info->mChildren, bone_info->mName, rest, parent_map, rest_mat); + LLJointData& child_data = data.mChildren.emplace_back(); + getJointMatricesAndHierarhy(child_info, child_data, data.mRestMatrix); } } @@ -1754,10 +1752,14 @@ const LLAvatarAppearance::joint_alias_map_t& LLAvatarAppearance::getJointAliases return mJointAliasMap; } -void LLAvatarAppearance:: getJointRestMatrices(LLAvatarAppearance::joint_rest_map_t& rest_map, LLAvatarAppearance::joint_parent_map_t& parent_map) const +void LLAvatarAppearance::getJointMatricesAndHierarhy(std::vector &data) const { glm::mat4 identity(1.f); - LLAvatarSkeletonInfo::getJointRestMatrices(sAvatarSkeletonInfo->mBoneInfoList, std::string(), rest_map, parent_map, identity); + for (LLAvatarBoneInfo* bone_info : sAvatarSkeletonInfo->mBoneInfoList) + { + LLJointData& child_data = data.emplace_back(); + LLAvatarSkeletonInfo::getJointMatricesAndHierarhy(bone_info, child_data, identity); + } } diff --git a/indra/llappearance/llavatarappearance.h b/indra/llappearance/llavatarappearance.h index e48d80b8ce..2748da9a1d 100644 --- a/indra/llappearance/llavatarappearance.h +++ b/indra/llappearance/llavatarappearance.h @@ -42,6 +42,7 @@ class LLTexGlobalColorInfo; class LLWearableData; class LLAvatarBoneInfo; class LLAvatarSkeletonInfo; +class LLJointData; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // LLAvatarAppearance @@ -156,7 +157,7 @@ public: const joint_alias_map_t& getJointAliases(); typedef std::map joint_parent_map_t; // matrix plus parent typedef std::map joint_rest_map_t; - void getJointRestMatrices(joint_rest_map_t& rest_map, joint_parent_map_t& parent_map) const; + void getJointMatricesAndHierarhy(std::vector &data) const; protected: static bool parseSkeletonFile(const std::string& filename, LLXmlTree& skeleton_xml_tree); diff --git a/indra/llappearance/lljointdata.h b/indra/llappearance/lljointdata.h new file mode 100644 index 0000000000..549f4af041 --- /dev/null +++ b/indra/llappearance/lljointdata.h @@ -0,0 +1,44 @@ +/** + * @file lljointdata.h + * @brief LLJointData class for holding individual joint data and skeleton + * + * $LicenseInfo:firstyear=2025&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLJOINTDATA_H +#define LL_LLJOINTDATA_H + +#include "v4math.h" + +// may be just move LLAvatarBoneInfo +class LLJointData +{ +public: + std::string mName; + glm::mat4 mJointMatrix; + glm::mat4 mRestMatrix; + + typedef std::vector bones_t; + bones_t mChildren; +}; + +#endif //LL_LLJOINTDATA_H diff --git a/indra/newview/gltf/llgltfloader.cpp b/indra/newview/gltf/llgltfloader.cpp index 292fd09035..c6d2bec238 100644 --- a/indra/newview/gltf/llgltfloader.cpp +++ b/indra/newview/gltf/llgltfloader.cpp @@ -95,8 +95,7 @@ LLGLTFLoader::LLGLTFLoader(std::string filename, std::map &jointAliasMap, U32 maxJointsPerMesh, U32 modelLimit, - joint_viewer_rest_map_t jointRestMatrices, - joint_viewer_parent_map_t jointParentMap) //, + std::vector viewer_skeleton) //, //bool preprocess) : LLModelLoader( filename, lod, @@ -113,8 +112,7 @@ LLGLTFLoader::LLGLTFLoader(std::string filename, , mMeshesLoaded(false) , mMaterialsLoaded(false) , mGeneratedModelLimit(modelLimit) - , mJointViewerRestMatrices(jointRestMatrices) - , mJointViewerParentMap(jointParentMap) + , mViewerJointData(viewer_skeleton) { } @@ -934,13 +932,45 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) mAlternateBindMatrices.resize(skin_idx + 1); } - // Build gltf rest matrices - // This is very inefficienct, todo: run from root to children, not from children to root - joint_node_mat4_map_t gltf_rest_map; + // fill up joints related data + joints_data_map_t joints_data; + joints_name_to_node_map_t names_to_nodes; for (S32 i = 0; i < joint_count; i++) { S32 joint = skin.mJoints[i]; - gltf_rest_map[joint] = buildGltfRestMatrix(joint, skin); + LL::GLTF::Node jointNode = mGLTFAsset.mNodes[joint]; + JointNodeData& data = joints_data[joint]; + data.mNodeIdx = joint; + data.mJointListIdx = i; + data.mGltfRestMatrix = buildGltfRestMatrix(joint, skin); + data.mGltfMatrix = jointNode.mMatrix; + data.mOverrideMatrix = glm::mat4(1.f); + + if (mJointMap.find(jointNode.mName) != mJointMap.end()) + { + data.mName = mJointMap[jointNode.mName]; + data.mIsValidViewerJoint = true; + } + else + { + data.mName = jointNode.mName; + data.mIsValidViewerJoint = false; + } + names_to_nodes[data.mName] = joint; + + for (S32 child : jointNode.mChildren) + { + JointNodeData& child_data = joints_data[child]; + child_data.mParentNodeIdx = joint; + child_data.mIsParentValidViewerJoint = data.mIsValidViewerJoint; + } + } + + // Go over viewer joints and build overrides + glm::mat4 ident(1.0); + for (auto &viewer_data : mViewerJointData) + { + buildOverrideMatrix(viewer_data, joints_data, names_to_nodes, ident, ident); } for (S32 i = 0; i < joint_count; i++) @@ -971,7 +1001,7 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) // todo: probably should be 'to viewer's overriden skeleton' glm::mat4 original_bind_matrix = glm::inverse(skin.mInverseBindMatricesData[i]); glm::mat4 rotated_original = coord_system_rotation * original_bind_matrix; - glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(gltf_rest_map, joint, legal_name); + glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(joints_data, joint, legal_name); glm::mat4 tranlated_original = skeleton_transform * rotated_original; glm::mat4 final_inverse_bind_matrix = glm::inverse(tranlated_original); @@ -985,7 +1015,7 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) // assume an identy matrix // todo: find a model with this, might need to use rotated matrix glm::mat4 inv_bind(1.0f); - glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(gltf_rest_map, joint, legal_name); + glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(joints_data, joint, legal_name); inv_bind = glm::inverse(skeleton_transform * inv_bind); LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(inv_bind)); @@ -994,37 +1024,15 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) } // Compute Alternative matrices also known as overrides - - glm::mat4 joint_mat = glm::mat4(1.f); - if (legal_joint) - { - joint_viewer_parent_map_t::const_iterator found = mJointViewerParentMap.find(legal_name); - if (found != mJointViewerParentMap.end() && !found->second.empty()) - { - glm::mat4 gltf_joint_rest_pose = coord_system_rotation * buildGltfRestMatrix(joint, skin); - if (mApplyXYRotation) - { - gltf_joint_rest_pose = coord_system_rotationxy * gltf_joint_rest_pose; - } - // Compute viewer's override by moving joint to viewer's base - // This might be overused or somewhat incorect for regular cases - // But this logic should be solid for cases where model lacks - // parent joints that viewer has. - // ex: like boots that have only knees and feet, but no pelvis, - // or anything else, in which case we take viewer's pelvis - glm::mat4 viewer_rest = mJointViewerRestMatrices[found->second]; - joint_mat = glm::inverse(viewer_rest) * gltf_joint_rest_pose; - } - } - LLMatrix4 original_joint_transform(glm::value_ptr(joint_mat)); + LLMatrix4 original_joint_transform(glm::value_ptr(joints_data[joint].mOverrideMatrix)); // Viewer seems to care only about translation part, // but for parity with collada taking original value LLMatrix4 newInverse = LLMatrix4(mInverseBindMatrices[skin_idx].back().getF32ptr()); newInverse.setTranslation(original_joint_transform.getTranslation()); - LL_INFOS("GLTF_DEBUG") << "mAlternateBindMatrix name: " << legal_name << " val: " << original_joint_transform << LL_ENDL; - mAlternateBindMatrices[skin_idx].push_back(LLMatrix4a(original_joint_transform)); + LL_INFOS("GLTF_DEBUG") << "mAlternateBindMatrix name: " << legal_name << " val: " << newInverse << LL_ENDL; + mAlternateBindMatrices[skin_idx].push_back(LLMatrix4a(newInverse)); if (legal_joint) { @@ -1139,6 +1147,57 @@ S32 LLGLTFLoader::findParentNode(S32 node) const return -1; } +void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& leftover) const +{ + glm::mat4 new_lefover(1.f); + glm::mat4 rest(1.f); + joints_name_to_node_map_t::iterator found_node = names_to_nodes.find(viewer_data.mName); + if (found_node != names_to_nodes.end()) + { + S32 gltf_node_idx = found_node->second; + JointNodeData& node = gltf_nodes[gltf_node_idx]; + node.mIsOverrideValid = true; + + glm::mat4 gltf_joint_rest_pose = coord_system_rotation * node.mGltfRestMatrix; + if (mApplyXYRotation) + { + gltf_joint_rest_pose = coord_system_rotationxy * gltf_joint_rest_pose; + } + node.mOverrideMatrix = glm::inverse(parent_rest) * gltf_joint_rest_pose; + + glm::vec3 override; + glm::vec3 skew; + glm::vec3 scale; + glm::vec4 perspective; + glm::quat rotation; + glm::decompose(node.mOverrideMatrix, scale, rotation, override, skew, perspective); + glm::vec3 translate; + glm::decompose(viewer_data.mJointMatrix, scale, rotation, translate, skew, perspective); + glm::mat4 viewer_joint = glm::recompose(scale, rotation, override, skew, perspective); + + node.mOverrideMatrix = viewer_joint; + rest = parent_rest * viewer_joint; + node.mOverrideRestMatrix = rest; + if (viewer_data.mName == "mPelvis") + { + // Todo: This is wrong, but this is a temporary + // solution for parts staying behind. + // Something is still missing with override mechanics + node.mOverrideMatrix = glm::mat4(1.f); + } + } + else + { + // No override for this joint + new_lefover = leftover * viewer_data.mJointMatrix; + rest = parent_rest * viewer_data.mJointMatrix; + } + for (LLJointData& child_data : viewer_data.mChildren) + { + buildOverrideMatrix(child_data, gltf_nodes, names_to_nodes, rest, new_lefover); + } +} + glm::mat4 LLGLTFLoader::buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const { // This is inefficient since we are recalculating some joints multiple times over @@ -1172,23 +1231,40 @@ glm::mat4 LLGLTFLoader::buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF return node.mMatrix; } +glm::mat4 LLGLTFLoader::buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const +{ + // This is inefficient since we are recalculating some joints multiple times over + // Todo: cache it? + + if (joint_node_index < 0 || joint_node_index >= static_cast(mGLTFAsset.mNodes.size())) + { + return glm::mat4(1.0f); + } + + auto& data = joint_data.at(joint_node_index); + + if (data.mParentNodeIdx >=0) + { + return buildGltfRestMatrix(data.mParentNodeIdx, joint_data) * data.mGltfMatrix; + } + // Should we return armature or stop earlier? + return data.mGltfMatrix; +} + // This function computes the transformation matrix needed to convert from GLTF skeleton space // to viewer skeleton space for a specific joint -glm::mat4 LLGLTFLoader::computeGltfToViewerSkeletonTransform(const joint_node_mat4_map_t& gltf_rest_map, S32 gltf_node_index, const std::string& joint_name) const +glm::mat4 LLGLTFLoader::computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const { - joint_viewer_rest_map_t::const_iterator found = mJointViewerRestMatrices.find(joint_name); - if (found == mJointViewerRestMatrices.end()) + const JointNodeData& node_data = joints_data_map.at(gltf_node_index); + if (!node_data.mIsOverrideValid) { // For now assume they are identical and return an identity (for ease of debuging) - // But there should be no joints viewer isn't aware about - // Warn or assert about missing joints return glm::mat4(1.0f); } - glm::mat4 viewer_joint_rest_pose = found->second; // Get the GLTF joint's rest pose (in GLTF coordinate system) - const glm::mat4 &gltf_joint_rest_pose = gltf_rest_map.at(gltf_node_index); + const glm::mat4 &gltf_joint_rest_pose = node_data.mGltfRestMatrix; glm::mat4 rest_pose = coord_system_rotation * gltf_joint_rest_pose; LL_INFOS("GLTF_DEBUG") << "rest matrix for joint " << joint_name << ": "; @@ -1199,7 +1275,7 @@ glm::mat4 LLGLTFLoader::computeGltfToViewerSkeletonTransform(const joint_node_ma // Compute transformation from GLTF space to viewer space // This assumes both skeletons are in rest pose initially - return viewer_joint_rest_pose * glm::inverse(rest_pose); + return node_data.mOverrideRestMatrix * glm::inverse(rest_pose); } bool LLGLTFLoader::checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx) diff --git a/indra/newview/gltf/llgltfloader.h b/indra/newview/gltf/llgltfloader.h index bf5f830d47..408f9243c7 100644 --- a/indra/newview/gltf/llgltfloader.h +++ b/indra/newview/gltf/llgltfloader.h @@ -32,6 +32,7 @@ #include "asset.h" #include "llglheaders.h" +#include "lljointdata.h" #include "llmodelloader.h" // gltf_* structs are temporary, used to organize the subset of data that eventually goes into the material LLSD @@ -125,6 +126,34 @@ class LLGLTFLoader : public LLModelLoader typedef std::map joint_viewer_rest_map_t; typedef std::map joint_node_mat4_map_t; + struct JointNodeData + { + JointNodeData() + : mJointListIdx(-1) + , mNodeIdx(-1) + , mParentNodeIdx(-1) + , mIsValidViewerJoint(false) + , mIsParentValidViewerJoint(false) + , mIsOverrideValid(false) + { + + } + S32 mJointListIdx; + S32 mNodeIdx; + S32 mParentNodeIdx; + glm::mat4 mGltfRestMatrix; + glm::mat4 mViewerRestMatrix; + glm::mat4 mOverrideRestMatrix; + glm::mat4 mGltfMatrix; + glm::mat4 mOverrideMatrix; + std::string mName; + bool mIsValidViewerJoint; + bool mIsParentValidViewerJoint; + bool mIsOverrideValid; + }; + typedef std::map joints_data_map_t; + typedef std::map joints_name_to_node_map_t; + LLGLTFLoader(std::string filename, S32 lod, LLModelLoader::load_callback_t load_cb, @@ -137,8 +166,7 @@ class LLGLTFLoader : public LLModelLoader std::map &jointAliasMap, U32 maxJointsPerMesh, U32 modelLimit, - joint_viewer_rest_map_t jointRestMatrices, - joint_viewer_parent_map_t jointParentPap); //, + std::vector viewer_skeleton); //, //bool preprocess ); virtual ~LLGLTFLoader(); @@ -172,8 +200,7 @@ protected: // GLTF isn't aware of viewer's skeleton and uses it's own, // so need to take viewer's joints and use them to // recalculate iverse bind matrices - joint_viewer_rest_map_t mJointViewerRestMatrices; - joint_viewer_parent_map_t mJointViewerParentMap; + std::vector mViewerJointData; // vector of vectors because of a posibility of having more than one skin typedef std::vector bind_matrices_t; @@ -194,8 +221,10 @@ private: S32 findValidRootJointNode(S32 source_joint_node, const LL::GLTF::Skin& gltf_skin) const; S32 findGLTFRootJointNode(const LL::GLTF::Skin& gltf_skin) const; // if there are multiple roots, gltf stores them under one commor joint S32 findParentNode(S32 node) const; + void buildOverrideMatrix(LLJointData& data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& leftover) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const; - glm::mat4 computeGltfToViewerSkeletonTransform(const joint_node_mat4_map_t &gltf_rest_map, S32 gltf_node_index, const std::string& joint_name) const; + glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const; + glm::mat4 computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const; bool checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx); void checkForXYrotation(const LL::GLTF::Skin& gltf_skin); LLUUID imageBufferToTextureUUID(const gltf_texture& tex); diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index d6b438a6e2..fc0a3ec58f 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -40,6 +40,7 @@ #include "lldrawable.h" #include "llface.h" #include "lliconctrl.h" +#include "lljointdata.h" #include "llmatrix4a.h" #include "llmeshrepository.h" #include "llmeshoptimizer.h" @@ -811,9 +812,8 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable else { LLVOAvatar* av = getPreviewAvatar(); - LLAvatarAppearance::joint_rest_map_t rest_pose; - LLAvatarAppearance::joint_parent_map_t rest_parent_map; - av->getJointRestMatrices(rest_pose, rest_parent_map); + std::vector viewer_skeleton; + av->getJointMatricesAndHierarhy(viewer_skeleton); mModelLoader = new LLGLTFLoader( filename, lod, @@ -827,8 +827,7 @@ void LLModelPreview::loadModel(std::string filename, S32 lod, bool force_disable joint_alias_map, LLSkinningUtil::getMaxJointCount(), gSavedSettings.getU32("ImporterModelLimit"), - rest_pose, - rest_parent_map); + viewer_skeleton); } if (force_disable_slm) -- cgit v1.3 From c404b9375923e0e4260536329b3da7d566a2d259 Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Tue, 24 Jun 2025 17:07:42 +0300 Subject: #4148 Fix collision bones --- indra/llappearance/llavatarappearance.cpp | 3 ++ indra/llappearance/lljointdata.h | 21 +++++++++++++ indra/llprimitive/llmodelloader.cpp | 52 +++++++++++++++++++++++++++++++ indra/llprimitive/llmodelloader.h | 1 + indra/newview/gltf/llgltfloader.cpp | 49 ++++++++++++++++++++++------- indra/newview/gltf/llgltfloader.h | 2 +- 6 files changed, 116 insertions(+), 12 deletions(-) (limited to 'indra/llappearance/llavatarappearance.cpp') diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index 13bea1e5ea..3c573d7227 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -1683,7 +1683,10 @@ void LLAvatarSkeletonInfo::getJointMatricesAndHierarhy( { data.mName = bone_info->mName; data.mJointMatrix = bone_info->getJointMatrix(); + data.mScale = glm::vec3(bone_info->mScale[0], bone_info->mScale[1], bone_info->mScale[2]); + data.mRotation = bone_info->mRot; data.mRestMatrix = parent_mat * data.mJointMatrix; + data.mIsJoint = bone_info->mIsJoint; for (LLAvatarBoneInfo* child_info : bone_info->mChildren) { LLJointData& child_data = data.mChildren.emplace_back(); diff --git a/indra/llappearance/lljointdata.h b/indra/llappearance/lljointdata.h index 549f4af041..0b5eff2ae7 100644 --- a/indra/llappearance/lljointdata.h +++ b/indra/llappearance/lljointdata.h @@ -36,9 +36,30 @@ public: std::string mName; glm::mat4 mJointMatrix; glm::mat4 mRestMatrix; + glm::vec3 mScale; + LLVector3 mRotation; typedef std::vector bones_t; bones_t mChildren; + + bool mIsJoint; // if not, collision_volume + enum SupportCategory + { + SUPPORT_BASE, + SUPPORT_EXTENDED + }; + SupportCategory mSupport; + void setSupport(const std::string& support) + { + if (support == "extended") + { + mSupport = SUPPORT_EXTENDED; + } + else + { + mSupport = SUPPORT_BASE; + } + } }; #endif //LL_LLJOINTDATA_H diff --git a/indra/llprimitive/llmodelloader.cpp b/indra/llprimitive/llmodelloader.cpp index 8f86b90b69..f97ac16a83 100644 --- a/indra/llprimitive/llmodelloader.cpp +++ b/indra/llprimitive/llmodelloader.cpp @@ -468,6 +468,58 @@ bool LLModelLoader::isRigSuitableForJointPositionUpload( const std::vector inv_bind; + std::map alt_bind; + for (LLPointer& mdl : mModelList) + { + + file << "Model name: " << mdl->mLabel << "\n"; + const LLMeshSkinInfo& skin_info = mdl->mSkinInfo; + file << "Shape Bind matrix: " << skin_info.mBindShapeMatrix << "\n"; + file << "Skin Weights count: " << (S32)mdl->mSkinWeights.size() << "\n"; + + // some objects might have individual bind matrices, + // but for now it isn't accounted for + size_t joint_count = skin_info.mJointNames.size(); + for (size_t i = 0; i< joint_count;i++) + { + const std::string& joint = skin_info.mJointNames[i]; + if (skin_info.mInvBindMatrix.size() > i) + { + inv_bind[joint] = skin_info.mInvBindMatrix[i]; + } + if (skin_info.mAlternateBindMatrix.size() > i) + { + alt_bind[joint] = skin_info.mAlternateBindMatrix[i]; + } + } + } + + file << "Inv Bind matrices.\n"; + for (auto& bind : inv_bind) + { + file << "Joint: " << bind.first << " Matrix: " << bind.second << "\n"; + } + + file << "Alt Bind matrices.\n"; + for (auto& bind : alt_bind) + { + file << "Joint: " << bind.first << " Matrix: " << bind.second << "\n"; + } +} //called in the main thread void LLModelLoader::loadTextures() diff --git a/indra/llprimitive/llmodelloader.h b/indra/llprimitive/llmodelloader.h index 73ec0ed1f4..7c808dcae0 100644 --- a/indra/llprimitive/llmodelloader.h +++ b/indra/llprimitive/llmodelloader.h @@ -198,6 +198,7 @@ public: const LLSD logOut() const { return mWarningsArray; } void clearLog() { mWarningsArray.clear(); } + void dumpDebugData(); protected: diff --git a/indra/newview/gltf/llgltfloader.cpp b/indra/newview/gltf/llgltfloader.cpp index 2631b7fefe..1af53311c6 100644 --- a/indra/newview/gltf/llgltfloader.cpp +++ b/indra/newview/gltf/llgltfloader.cpp @@ -1122,7 +1122,7 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) glm::mat4 ident(1.0); for (auto &viewer_data : mViewerJointData) { - buildOverrideMatrix(viewer_data, joints_data, names_to_nodes, ident); + buildOverrideMatrix(viewer_data, joints_data, names_to_nodes, ident, ident); } for (S32 i = 0; i < joint_count; i++) @@ -1299,7 +1299,7 @@ S32 LLGLTFLoader::findParentNode(S32 node) const return -1; } -void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest) const +void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& parent_support_rest) const { glm::mat4 new_lefover(1.f); glm::mat4 rest(1.f); @@ -1309,26 +1309,42 @@ void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map S32 gltf_node_idx = found_node->second; JointNodeData& node = gltf_nodes[gltf_node_idx]; node.mIsOverrideValid = true; + node.mViewerRestMatrix = viewer_data.mRestMatrix; glm::mat4 gltf_joint_rest_pose = coord_system_rotation * node.mGltfRestMatrix; if (mApplyXYRotation) { gltf_joint_rest_pose = coord_system_rotationxy * gltf_joint_rest_pose; } - node.mOverrideMatrix = glm::inverse(parent_rest) * gltf_joint_rest_pose; - glm::vec3 override; + glm::mat4 translated_joint; + // Example: + // Viewer has pelvis->spine1->spine2->torso. + // gltf example model has pelvis->torso + // By doing glm::inverse(transalted_rest_spine2) * gltf_rest_torso + // We get what torso would have looked like if gltf had a spine2 + if (viewer_data.mIsJoint) + { + translated_joint = glm::inverse(parent_rest) * gltf_joint_rest_pose; + } + else + { + translated_joint = glm::inverse(parent_support_rest) * gltf_joint_rest_pose; + } + + glm::vec3 translation_override; glm::vec3 skew; glm::vec3 scale; glm::vec4 perspective; glm::quat rotation; - glm::decompose(node.mOverrideMatrix, scale, rotation, override, skew, perspective); - glm::vec3 translate; - glm::decompose(viewer_data.mJointMatrix, scale, rotation, translate, skew, perspective); - glm::mat4 viewer_joint = glm::recompose(scale, rotation, override, skew, perspective); + glm::decompose(translated_joint, scale, rotation, translation_override, skew, perspective); + + node.mOverrideMatrix = glm::recompose(glm::vec3(1, 1, 1), glm::identity(), translation_override, glm::vec3(0, 0, 0), glm::vec4(0, 0, 0, 1)); + + glm::mat4 override_joint = node.mOverrideMatrix; + override_joint = glm::scale(override_joint, viewer_data.mScale); - node.mOverrideMatrix = viewer_joint; - rest = parent_rest * node.mOverrideMatrix; + rest = parent_rest * override_joint; node.mOverrideRestMatrix = rest; } else @@ -1336,9 +1352,20 @@ void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map // No override for this joint rest = parent_rest * viewer_data.mJointMatrix; } + + glm::mat4 support_rest(1.f); + if (viewer_data.mSupport == LLJointData::SUPPORT_BASE) + { + support_rest = rest; + } + else + { + support_rest = parent_support_rest; + } + for (LLJointData& child_data : viewer_data.mChildren) { - buildOverrideMatrix(child_data, gltf_nodes, names_to_nodes, rest); + buildOverrideMatrix(child_data, gltf_nodes, names_to_nodes, rest, support_rest); } } diff --git a/indra/newview/gltf/llgltfloader.h b/indra/newview/gltf/llgltfloader.h index 19a029d6d4..f3d5dadbb9 100644 --- a/indra/newview/gltf/llgltfloader.h +++ b/indra/newview/gltf/llgltfloader.h @@ -129,7 +129,7 @@ private: S32 findValidRootJointNode(S32 source_joint_node, const LL::GLTF::Skin& gltf_skin) const; S32 findGLTFRootJointNode(const LL::GLTF::Skin& gltf_skin) const; // if there are multiple roots, gltf stores them under one commor joint S32 findParentNode(S32 node) const; - void buildOverrideMatrix(LLJointData& data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest) const; + void buildOverrideMatrix(LLJointData& data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& support_rest) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const; glm::mat4 computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const; -- cgit v1.3 From 8c9d0c61459ebdfa3d17c9ef7ccfba9793bf4d3a Mon Sep 17 00:00:00 2001 From: Andrey Kleshchev Date: Wed, 25 Jun 2025 20:19:23 +0300 Subject: #4142 Joint grouping and stripping Viewer only supports 110 joints at a time, when model has more bones than that importer has to strip some, use groups for stripping. --- indra/llappearance/llavatarappearance.cpp | 11 ++ indra/llappearance/lljointdata.h | 1 + indra/newview/gltf/llgltfloader.cpp | 177 ++++++++++++++++++++++-------- indra/newview/gltf/llgltfloader.h | 17 ++- 4 files changed, 157 insertions(+), 49 deletions(-) (limited to 'indra/llappearance/llavatarappearance.cpp') diff --git a/indra/llappearance/llavatarappearance.cpp b/indra/llappearance/llavatarappearance.cpp index 3c573d7227..dab18c240d 100644 --- a/indra/llappearance/llavatarappearance.cpp +++ b/indra/llappearance/llavatarappearance.cpp @@ -78,6 +78,7 @@ private: std::string mName; std::string mSupport; std::string mAliases; + std::string mGroup; bool mIsJoint; LLVector3 mPos; LLVector3 mEnd; @@ -1606,6 +1607,15 @@ bool LLAvatarBoneInfo::parseXml(LLXmlTreeNode* node) mSupport = "base"; } + // Skeleton has 133 bones, but shader only allows 110 (LL_MAX_JOINTS_PER_MESH_OBJECT) + // Groups can be used by importer to cut out unused groups of joints + static LLStdStringHandle group_string = LLXmlTree::addAttributeString("group"); + if (!node->getFastAttributeString(group_string, mGroup)) + { + LL_WARNS() << "Bone without group " << mName << LL_ENDL; + mGroup = "global"; + } + if (mIsJoint) { static LLStdStringHandle pivot_string = LLXmlTree::addAttributeString("pivot"); @@ -1687,6 +1697,7 @@ void LLAvatarSkeletonInfo::getJointMatricesAndHierarhy( data.mRotation = bone_info->mRot; data.mRestMatrix = parent_mat * data.mJointMatrix; data.mIsJoint = bone_info->mIsJoint; + data.mGroup = bone_info->mGroup; for (LLAvatarBoneInfo* child_info : bone_info->mChildren) { LLJointData& child_data = data.mChildren.emplace_back(); diff --git a/indra/llappearance/lljointdata.h b/indra/llappearance/lljointdata.h index 0b5eff2ae7..2fc26198ee 100644 --- a/indra/llappearance/lljointdata.h +++ b/indra/llappearance/lljointdata.h @@ -34,6 +34,7 @@ class LLJointData { public: std::string mName; + std::string mGroup; glm::mat4 mJointMatrix; glm::mat4 mRestMatrix; glm::vec3 mScale; diff --git a/indra/newview/gltf/llgltfloader.cpp b/indra/newview/gltf/llgltfloader.cpp index 1af53311c6..039c0b561e 100644 --- a/indra/newview/gltf/llgltfloader.cpp +++ b/indra/newview/gltf/llgltfloader.cpp @@ -258,13 +258,14 @@ bool LLGLTFLoader::parseMeshes() if (mGLTFAsset.mSkins.size() > 0) { checkForXYrotation(mGLTFAsset.mSkins[0]); + populateJointGroups(); } // Populate the joints from skins first. // There's not many skins - and you can pretty easily iterate through the nodes from that. for (S32 i = 0; i < mGLTFAsset.mSkins.size(); i++) { - populateJointFromSkin(i); + populateJointsFromSkin(i); } // Track how many times each mesh name has been used @@ -463,6 +464,24 @@ void LLGLTFLoader::computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S3 } } +bool LLGLTFLoader::addJointToModelSkin(LLMeshSkinInfo& skin_info, S32 gltf_skin_idx, size_t gltf_joint_idx) const +{ + const std::string& legal_name = mJointNames[gltf_skin_idx][gltf_joint_idx]; + if (legal_name.empty()) + { + llassert(false); // should have been stopped by gltf_joint_index_use[i] == -1 + return false; + } + skin_info.mJointNames.push_back(legal_name); + skin_info.mJointNums.push_back(-1); + + // In scope of same skin multiple meshes reuse same bind matrices + skin_info.mInvBindMatrix.push_back(mInverseBindMatrices[gltf_skin_idx][gltf_joint_idx]); + skin_info.mAlternateBindMatrix.push_back(mAlternateBindMatrices[gltf_skin_idx][gltf_joint_idx]); + + return true; +} + bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh& mesh, const LL::GLTF::Node& nodeno, material_map& mats, S32 instance_count) { // Set the requested label for the floater display and uploading @@ -519,15 +538,9 @@ bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh& size_t jointCnt = gltf_skin.mJoints.size(); gltf_joint_index_use.resize(jointCnt); - S32 replacement_index = 0; for (size_t i = 0; i < jointCnt; ++i) { - // Process joint name and idnex - S32 joint = gltf_skin.mJoints[i]; - LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; - - std::string legal_name(jointNode.mName); - if (mJointMap.find(legal_name) == mJointMap.end()) + if (mJointNames[i].empty()) { // This might need to hold a substitute index gltf_joint_index_use[i] = -1; // mark as unsupported @@ -959,65 +972,106 @@ bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh& // Call normalizeVolumeFacesAndWeights to compute proper extents pModel->normalizeVolumeFacesAndWeights(); - // Fill joint names, bind matrices and prepare to remap weight indices + // Fill joint names, bind matrices and remap weight indices if (skinIdx >= 0) { LL::GLTF::Skin& gltf_skin = mGLTFAsset.mSkins[skinIdx]; LLMeshSkinInfo& skin_info = pModel->mSkinInfo; S32 valid_joints_count = mValidJointsCount[skinIdx]; + S32 replacement_index = 0; std::vector gltfindex_to_joitindex_map; size_t jointCnt = gltf_skin.mJoints.size(); gltfindex_to_joitindex_map.resize(jointCnt); - S32 replacement_index = 0; - S32 stripped_valid_joints = 0; - for (size_t i = 0; i < jointCnt; ++i) + if (valid_joints_count > LL_MAX_JOINTS_PER_MESH_OBJECT) { - // Process joint name and idnex - S32 joint = gltf_skin.mJoints[i]; - if (gltf_joint_index_use[i] < 0) + std::map goup_use_count; + // Assume that 'Torso' group is always in use since that's what everything else is attached to + goup_use_count["Torso"] = 1; + // Note that Collisions and Extra groups are all over the place, might want to include them from the start + // or add individual when parents are added + + // Check which groups are in use + for (size_t i = 0; i < jointCnt; ++i) { - // unsupported (-1) joint, drop it - continue; + std::string& joint_name = mJointNames[skinIdx][i]; + if (!joint_name.empty()) + { + if (gltf_joint_index_use[i] > 0) + { + const JointGroups &group = mJointGroups[joint_name]; + // Joint in use, increment it's groups + goup_use_count[group.mGroup]++; + goup_use_count[group.mParentGroup]++; + } + } } - LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; - std::string legal_name(jointNode.mName); - if (mJointMap.find(legal_name) != mJointMap.end()) + // 1. add joints that are in use directly + for (size_t i = 0; i < jointCnt; ++i) { - legal_name = mJointMap[legal_name]; - } - else - { - llassert(false); // should have been stopped by gltf_joint_index_use[i] == -1 - continue; + // Process joint name and idnex + S32 joint = gltf_skin.mJoints[i]; + if (gltf_joint_index_use[i] <= 0) + { + // unsupported (-1) joint, drop it + // unused (0) joint, drop it + continue; + } + + if (addJointToModelSkin(skin_info, skinIdx, i)) + { + gltfindex_to_joitindex_map[i] = replacement_index++; + } } - if (valid_joints_count > LL_MAX_JOINTS_PER_MESH_OBJECT - && gltf_joint_index_use[i] == 0 - && legal_name != "mPelvis") + // 2. add joints from groups that this model's joints belong to + // It's perfectly valid to have more joints than is in use + // Ex: sandals that make your legs digitigrade despite not skining to + // knees or the like. + // Todo: sort and add by usecount + for (size_t i = 0; i < jointCnt; ++i) { - // Unused (0) joint - // It's perfectly valid to have more joints than is in use - // Ex: sandals that make your legs digitigrade despite not skining to - // knees or the like. - // But if model is over limid, drop extras sans pelvis. - // Keeping 'pelvis' is a workaround to keep preview whole. - // Todo: consider improving this, either take as much as possible or - // ensure common parents/roots are included - continue; + S32 joint = gltf_skin.mJoints[i]; + if (gltf_joint_index_use[i] != 0) + { + // this step needs only joints that have zero uses + continue; + } + if (skin_info.mInvBindMatrix.size() > LL_MAX_JOINTS_PER_MESH_OBJECT) + { + break; + } + const std::string& legal_name = mJointNames[skinIdx][i]; + std::string group_name = mJointGroups[legal_name].mGroup; + if (goup_use_count[group_name] > 0) + { + if (addJointToModelSkin(skin_info, skinIdx, i)) + { + gltfindex_to_joitindex_map[i] = replacement_index++; + } + } } + } + else + { + // Less than 110, just add every valid joint + for (size_t i = 0; i < jointCnt; ++i) + { + // Process joint name and idnex + S32 joint = gltf_skin.mJoints[i]; + if (gltf_joint_index_use[i] < 0) + { + // unsupported (-1) joint, drop it + continue; + } - gltfindex_to_joitindex_map[i] = replacement_index++; - - skin_info.mJointNames.push_back(legal_name); - skin_info.mJointNums.push_back(-1); - - // In scope of same skin multiple meshes reuse same bind matrices - skin_info.mInvBindMatrix.push_back(mInverseBindMatrices[skinIdx][i]); - - skin_info.mAlternateBindMatrix.push_back(mAlternateBindMatrices[skinIdx][i]); + if (addJointToModelSkin(skin_info, skinIdx, i)) + { + gltfindex_to_joitindex_map[i] = replacement_index++; + } + } } if (skin_info.mInvBindMatrix.size() > LL_MAX_JOINTS_PER_MESH_OBJECT) @@ -1046,7 +1100,7 @@ bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh& return true; } -void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) +void LLGLTFLoader::populateJointsFromSkin(S32 skin_idx) { const LL::GLTF::Skin& skin = mGLTFAsset.mSkins[skin_idx]; @@ -1066,6 +1120,7 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) { mInverseBindMatrices.resize(skin_idx + 1); mAlternateBindMatrices.resize(skin_idx + 1); + mJointNames.resize(skin_idx + 1); mValidJointsCount.resize(skin_idx + 1, 0); } @@ -1135,6 +1190,11 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) { legal_name = mJointMap[legal_name]; legal_joint = true; + mJointNames[skin_idx].push_back(legal_name); + } + else + { + mJointNames[skin_idx].emplace_back(); } // Compute bind matrices @@ -1196,6 +1256,15 @@ void LLGLTFLoader::populateJointFromSkin(S32 skin_idx) } } +void LLGLTFLoader::populateJointGroups() +{ + std::string parent; + for (auto& viewer_data : mViewerJointData) + { + buildJointGroup(viewer_data, parent); + } +} + S32 LLGLTFLoader::findClosestValidJoint(S32 source_joint, const LL::GLTF::Skin& gltf_skin) const { @@ -1299,6 +1368,18 @@ S32 LLGLTFLoader::findParentNode(S32 node) const return -1; } +void LLGLTFLoader::buildJointGroup(LLJointData& viewer_data, const std::string &parent_group) +{ + JointGroups& jount_group_data = mJointGroups[viewer_data.mName]; + jount_group_data.mGroup = viewer_data.mGroup; + jount_group_data.mParentGroup = parent_group; + + for (LLJointData& child_data : viewer_data.mChildren) + { + buildJointGroup(child_data, viewer_data.mGroup); + } +} + void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& parent_support_rest) const { glm::mat4 new_lefover(1.f); diff --git a/indra/newview/gltf/llgltfloader.h b/indra/newview/gltf/llgltfloader.h index f3d5dadbb9..048f7df7f5 100644 --- a/indra/newview/gltf/llgltfloader.h +++ b/indra/newview/gltf/llgltfloader.h @@ -112,8 +112,20 @@ protected: // vector of vectors because of a posibility of having more than one skin typedef std::vector bind_matrices_t; + typedef std::vector > joint_names_t; bind_matrices_t mInverseBindMatrices; bind_matrices_t mAlternateBindMatrices; + joint_names_t mJointNames; // empty string when no legal name for a given idx + + // what group a joint belongs to. + // For purpose of stripping unused groups when joints are over limit. + struct JointGroups + { + std::string mGroup; + std::string mParentGroup; + }; + typedef std::map joint_to_group_map_t; + joint_to_group_map_t mJointGroups; // per skin joint count, needs to be tracked for the sake of limits check. std::vector mValidJointsCount; @@ -122,13 +134,16 @@ private: bool parseMeshes(); void computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S32 node_index, glm::mat4& combined_transform) const; void processNodeHierarchy(S32 node_idx, std::map& mesh_name_counts, U32 submodel_limit, const LLVolumeParams& volume_params); + bool addJointToModelSkin(LLMeshSkinInfo& skin_info, S32 gltf_skin_idx, size_t gltf_joint_idx) const; bool populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh &mesh, const LL::GLTF::Node &node, material_map& mats, S32 instance_count); - void populateJointFromSkin(S32 skin_idx); + void populateJointsFromSkin(S32 skin_idx); + void populateJointGroups(); void addModelToScene(LLModel* pModel, U32 submodel_limit, const LLMatrix4& transformation, const LLVolumeParams& volume_params, const material_map& mats); S32 findClosestValidJoint(S32 source_joint, const LL::GLTF::Skin& gltf_skin) const; S32 findValidRootJointNode(S32 source_joint_node, const LL::GLTF::Skin& gltf_skin) const; S32 findGLTFRootJointNode(const LL::GLTF::Skin& gltf_skin) const; // if there are multiple roots, gltf stores them under one commor joint S32 findParentNode(S32 node) const; + void buildJointGroup(LLJointData& viewer_data, const std::string& parent_group); void buildOverrideMatrix(LLJointData& data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& support_rest) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const; glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const; -- cgit v1.3