/** * @file LLGLTFLoader.cpp * @brief LLGLTFLoader class implementation * * $LicenseInfo:firstyear=2022&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2022, 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$ */ #include "llgltfloader.h" #include "meshoptimizer.h" #include // Import & define single-header gltf import/export lib #define TINYGLTF_IMPLEMENTATION #define TINYGLTF_USE_CPP14 // default is C++ 11 // tinygltf by default loads image files using STB #define STB_IMAGE_IMPLEMENTATION // to use our own image loading: // 1. replace this definition with TINYGLTF_NO_STB_IMAGE // 2. provide image loader callback with TinyGLTF::SetImageLoader(LoadimageDataFunction LoadImageData, void *user_data) // tinygltf saves image files using STB #define STB_IMAGE_WRITE_IMPLEMENTATION // similarly, can override with TINYGLTF_NO_STB_IMAGE_WRITE and TinyGLTF::SetImageWriter(fxn, data) // Additionally, disable inclusion of STB header files entirely with // TINYGLTF_NO_INCLUDE_STB_IMAGE // TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE #include "tinygltf/tiny_gltf.h" // TODO: includes inherited from dae loader. Validate / prune #include "llsdserialize.h" #include "lljoint.h" #include "llbase64.h" #include "lldir.h" #include "llmatrix4a.h" #include #include #include static const std::string lod_suffix[LLModel::NUM_LODS] = { "_LOD0", "_LOD1", "_LOD2", "", "_PHYS", }; // Premade rotation matrix, GLTF is Y-up while SL is Z-up static const glm::mat4 coord_system_rotation( 1.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, -1.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f ); static const glm::mat4 coord_system_rotationxy( 0.f, 1.f, 0.f, 0.f, -1.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 0.f, 1.f ); static const S32 VERTICIES_LIMIT = USHRT_MAX - 2; LLGLTFLoader::LLGLTFLoader(std::string filename, S32 lod, LLModelLoader::load_callback_t load_cb, LLModelLoader::joint_lookup_func_t joint_lookup_func, LLModelLoader::texture_load_func_t texture_load_func, LLModelLoader::state_callback_t state_cb, void * opaque_userdata, JointTransformMap & jointTransformMap, JointNameSet & jointsFromNodes, std::map> & jointAliasMap, U32 maxJointsPerMesh, U32 modelLimit, U32 debugMode, std::vector viewer_skeleton) //, //bool preprocess) : LLModelLoader( filename, lod, load_cb, joint_lookup_func, texture_load_func, state_cb, opaque_userdata, jointTransformMap, jointsFromNodes, jointAliasMap, maxJointsPerMesh, modelLimit, debugMode) , mViewerJointData(viewer_skeleton) , mGltfLoaded(false) , mApplyXYRotation(false) { } LLGLTFLoader::~LLGLTFLoader() {} bool LLGLTFLoader::OpenFile(const std::string &filename) { // Clear the material cache for new file mMaterialCache.clear(); tinygltf::TinyGLTF loader; std::string filename_lc(filename); LLStringUtil::toLower(filename_lc); try { mGltfLoaded = mGLTFAsset.load(filename, false); } catch (const std::exception& e) { LL_WARNS() << "Exception in LLModelLoader::run: " << e.what() << LL_ENDL; LLSD args; args["Message"] = "ParsingErrorException"; args["FILENAME"] = filename; args["EXCEPTION"] = e.what(); mWarningsArray.append(args); setLoadState(ERROR_PARSING); return false; } catch (...) { LOG_UNHANDLED_EXCEPTION("LLGLTFLoader"); LLSD args; args["Message"] = "ParsingErrorException"; args["FILENAME"] = filename; args["EXCEPTION"] = "Unknown exception"; mWarningsArray.append(args); setLoadState(ERROR_PARSING); return false; } if (!mGltfLoaded) { notifyUnsupportedExtension(true); for (const auto& buffer : mGLTFAsset.mBuffers) { if (buffer.mByteLength > 0 && buffer.mData.empty()) { bool bin_file = buffer.mUri.ends_with(".bin"); LLSD args; args["Message"] = bin_file ? "ParsingErrorMissingBufferBin" : "ParsingErrorMissingBuffer"; args["BUFFER_NAME"] = buffer.mName; args["BUFFER_URI"] = buffer.mUri; mWarningsArray.append(args); } } setLoadState(ERROR_PARSING); return false; } notifyUnsupportedExtension(false); bool meshesLoaded = parseMeshes(); setLoadState(DONE); return meshesLoaded; } void LLGLTFLoader::addModelToScene( LLModel* pModel, const std::string& model_name, U32 submodel_limit, const LLMatrix4& transformation, const LLVolumeParams& volume_params, const material_map& mats) { U32 volume_faces = pModel->getNumVolumeFaces(); // Side-steps all manner of issues when splitting models // and matching lower LOD materials to base models // pModel->sortVolumeFacesByMaterialName(); int submodelID = 0; // remove all faces that definitely won't fit into one model and submodel limit U32 face_limit = (submodel_limit + 1) * LL_SCULPT_MESH_MAX_FACES; if (face_limit < volume_faces) { LL_WARNS("GLTF_IMPORT") << "Model contains " << volume_faces << " faces, exceeding the limit of " << face_limit << LL_ENDL; LLSD args; args["Message"] = "ModelTooManySubmodels"; args["MODEL_NAME"] = pModel->mLabel; args["SUBMODEL_COUNT"] = static_cast(llfloor((F32)volume_faces / LL_SCULPT_MESH_MAX_FACES)); args["SUBMODEL_LIMIT"] = static_cast(submodel_limit); mWarningsArray.append(args); pModel->setNumVolumeFaces(face_limit); } LLVolume::face_list_t remainder; std::vector ready_models; LLModel* current_model = pModel; do { current_model->trimVolumeFacesToSize(LL_SCULPT_MESH_MAX_FACES, &remainder); volume_faces = static_cast(remainder.size()); // Don't add to scene yet because weights and materials aren't ready. // Just save it ready_models.push_back(current_model); // If we have left-over volume faces, create another model // to absorb them. if (volume_faces) { LLModel* next = new LLModel(volume_params, 0.f); next->ClearFacesAndMaterials(); next->mSubmodelID = ++submodelID; std::string instance_name = model_name; if (next->mSubmodelID > 0) { instance_name += (char)((int)'a' + next->mSubmodelID); } // Check for duplicates and add copy suffix if needed int duplicate_count = 0; for (const auto& inst : mScene[transformation]) { if (inst.mLabel == instance_name) { ++duplicate_count; } } if (duplicate_count > 0) { instance_name += "_copy_" + std::to_string(duplicate_count); } next->mLabel = instance_name; next->getVolumeFaces() = remainder; next->mNormalizedScale = current_model->mNormalizedScale; next->mNormalizedTranslation = current_model->mNormalizedTranslation; next->mSkinWeights = current_model->mSkinWeights; next->mPosition = current_model->mPosition; const LLMeshSkinInfo& current_skin_info = current_model->mSkinInfo; LLMeshSkinInfo& next_skin_info = next->mSkinInfo; next_skin_info.mJointNames = current_skin_info.mJointNames; next_skin_info.mJointNums = current_skin_info.mJointNums; next_skin_info.mBindShapeMatrix = current_skin_info.mBindShapeMatrix; next_skin_info.mInvBindMatrix = current_skin_info.mInvBindMatrix; next_skin_info.mAlternateBindMatrix = current_skin_info.mAlternateBindMatrix; next_skin_info.mPelvisOffset = current_skin_info.mPelvisOffset; if (current_model->mMaterialList.size() > LL_SCULPT_MESH_MAX_FACES) { next->mMaterialList.assign(current_model->mMaterialList.begin() + LL_SCULPT_MESH_MAX_FACES, current_model->mMaterialList.end()); current_model->mMaterialList.resize(LL_SCULPT_MESH_MAX_FACES); } current_model = next; } remainder.clear(); } while (volume_faces); for (auto model : ready_models) { // remove unused/redundant vertices model->remapVolumeFaces(); mModelList.push_back(model); std::map materials; for (U32 i = 0; i < (U32)model->mMaterialList.size(); ++i) { material_map::const_iterator found = mats.find(model->mMaterialList[i]); if (found != mats.end()) { materials[model->mMaterialList[i]] = found->second; } else { materials[model->mMaterialList[i]] = LLImportMaterial(); } } // Keep base name for scene instance. std::string instance_name = model->mLabel; // Add suffix. Suffix is nessesary for model matching logic // because sometimes higher lod can be used as a lower one, so models // need unique names not just in scope of one lod, but across lods. model->mLabel += lod_suffix[mLod]; mScene[transformation].push_back(LLModelInstance(model, instance_name, transformation, materials)); stretch_extents(model, transformation); } } bool LLGLTFLoader::parseMeshes() { if (!mGltfLoaded) return false; // 2022-04 DJH Volume params from dae example. TODO understand PCODE LLVolumeParams volume_params; volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); mTransform.setIdentity(); for (auto& node : mGLTFAsset.mNodes) { // Make node matrix valid for correct transformation node.makeMatrixValid(); } if (mGLTFAsset.mSkins.size() > 0) { checkForXYrotation(mGLTFAsset.mSkins[0]); populateJointGroups(); } // Populate the joints from skins first. // Multiple meshes can share the same skin, so preparing skins beforehand. for (S32 i = 0; i < mGLTFAsset.mSkins.size(); i++) { populateJointsFromSkin(i); } // Track how many times each mesh name has been used std::map mesh_name_counts; // For now use mesh count, but might be better to do 'mNodes.size() - joints count'. U32 submodel_limit = mGLTFAsset.mMeshes.size() > 0 ? mGeneratedModelLimit / (U32)mGLTFAsset.mMeshes.size() : 0; // Check if we have scenes defined if (!mGLTFAsset.mScenes.empty()) { // Process the default scene (or first scene if no default) S32 scene_idx = mGLTFAsset.mScene >= 0 ? mGLTFAsset.mScene : 0; if (scene_idx < mGLTFAsset.mScenes.size()) { const LL::GLTF::Scene& scene = mGLTFAsset.mScenes[scene_idx]; LL_INFOS("GLTF_IMPORT") << "Processing scene " << scene_idx << " with " << scene.mNodes.size() << " root nodes" << LL_ENDL; // Process all root nodes defined in the scene for (S32 root_idx : scene.mNodes) { if (root_idx >= 0 && root_idx < static_cast(mGLTFAsset.mNodes.size())) { processNodeHierarchy(root_idx, mesh_name_counts, submodel_limit, volume_params); } } } } else { LL_WARNS("GLTF_IMPORT") << "No scenes defined in GLTF file" << LL_ENDL; LLSD args; args["Message"] = "NoScenesFound"; mWarningsArray.append(args); return false; } checkGlobalJointUsage(); return true; } void LLGLTFLoader::processNodeHierarchy(S32 node_idx, std::map& mesh_name_counts, U32 submodel_limit, const LLVolumeParams& volume_params) { if (node_idx < 0 || node_idx >= static_cast(mGLTFAsset.mNodes.size())) return; const LL::GLTF::Node& node = mGLTFAsset.mNodes[node_idx]; LL_DEBUGS("GLTF_IMPORT") << "Processing node " << node_idx << " (" << node.mName << ")" << " - has mesh: " << (node.mMesh >= 0 ? "yes" : "no") << " - children: " << node.mChildren.size() << LL_ENDL; // Process this node's mesh if it has one if (node.mMesh >= 0 && node.mMesh < mGLTFAsset.mMeshes.size()) { LLMatrix4 transformation; material_map mats; LLModel* pModel = new LLModel(volume_params, 0.f); const LL::GLTF::Mesh& mesh = mGLTFAsset.mMeshes[node.mMesh]; // Get base mesh name and track usage std::string base_name = getLodlessLabel(mesh); if (base_name.empty()) { base_name = "mesh_" + std::to_string(node.mMesh); } S32 instance_count = mesh_name_counts[base_name]++; // make name unique if (instance_count > 0) { base_name = base_name + "_copy_" + std::to_string(instance_count); } if (populateModelFromMesh(pModel, base_name, mesh, node, mats) && (LLModel::NO_ERRORS == pModel->getStatus()) && validate_model(pModel)) { mTransform.setIdentity(); transformation = mTransform; // adjust the transformation to compensate for mesh normalization LLVector3 mesh_scale_vector; LLVector3 mesh_translation_vector; pModel->getNormalizedScaleTranslation(mesh_scale_vector, mesh_translation_vector); LLMatrix4 mesh_translation; mesh_translation.setTranslation(mesh_translation_vector); mesh_translation *= transformation; transformation = mesh_translation; LLMatrix4 mesh_scale; mesh_scale.initScale(mesh_scale_vector); mesh_scale *= transformation; transformation = mesh_scale; if (node.mSkin >= 0) { // "Bind Shape Matrix" is supposed to transform the geometry of the skinned mesh // into the coordinate space of the joints. // In GLTF, this matrix is omitted, and it is assumed that this transform is either // premultiplied with the mesh data, or postmultiplied to the inverse bind matrices. // // TODO: There appears to be missing rotation when joints rotate the model // or inverted bind matrices are missing inherited rotation // (based of values the 'bento shoes' mesh might be missing 90 degrees horizontaly // prior to skinning) pModel->mSkinInfo.mBindShapeMatrix.loadu(mesh_scale); LL_INFOS("GLTF_DEBUG") << "Model: " << pModel->mLabel << " mBindShapeMatrix: " << pModel->mSkinInfo.mBindShapeMatrix << LL_ENDL; } if (transformation.determinant() < 0) { // negative scales are not supported LL_INFOS("GLTF_IMPORT") << "Negative scale detected, unsupported post-normalization transform. domInstance_geometry: " << pModel->mLabel << LL_ENDL; LLSD args; args["Message"] = "NegativeScaleNormTrans"; args["LABEL"] = pModel->mLabel; mWarningsArray.append(args); } addModelToScene(pModel, base_name, submodel_limit, transformation, volume_params, mats); mats.clear(); } else { setLoadState(ERROR_MODEL + pModel->getStatus()); delete pModel; return; } } else if (node.mMesh >= 0) { // Log invalid mesh reference LL_WARNS("GLTF_IMPORT") << "Node " << node_idx << " (" << node.mName << ") references invalid mesh " << node.mMesh << " (total meshes: " << mGLTFAsset.mMeshes.size() << ")" << LL_ENDL; LLSD args; args["Message"] = "InvalidMeshReference"; args["NODE_NAME"] = node.mName; args["MESH_INDEX"] = node.mMesh; args["TOTAL_MESHES"] = static_cast(mGLTFAsset.mMeshes.size()); mWarningsArray.append(args); } // Process all children recursively for (S32 child_idx : node.mChildren) { processNodeHierarchy(child_idx, mesh_name_counts, submodel_limit, volume_params); } } void LLGLTFLoader::computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S32 node_index, glm::mat4& combined_transform) const { if (node_index < 0 || node_index >= static_cast(asset.mNodes.size())) { combined_transform = glm::mat4(1.0f); return; } const auto& node = asset.mNodes[node_index]; // Ensure the node's matrix is valid const_cast(node).makeMatrixValid(); // Start with this node's transform combined_transform = node.mMatrix; // Find and apply parent transform if it exists for (size_t i = 0; i < asset.mNodes.size(); ++i) { const auto& potential_parent = asset.mNodes[i]; auto it = std::find(potential_parent.mChildren.begin(), potential_parent.mChildren.end(), node_index); if (it != potential_parent.mChildren.end()) { // Found parent - recursively get its combined transform and apply it glm::mat4 parent_transform; computeCombinedNodeTransform(asset, static_cast(i), parent_transform); combined_transform = parent_transform * combined_transform; return; // Early exit - a node can only have one parent } } } bool LLGLTFLoader::addJointToModelSkin(LLMeshSkinInfo& skin_info, S32 gltf_skin_idx, size_t gltf_joint_idx) { 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]); // Track joint usage for this skin, for the sake of unused joints detection mJointUsage[gltf_skin_idx][gltf_joint_idx]++; return true; } LLGLTFLoader::LLGLTFImportMaterial LLGLTFLoader::processMaterial(S32 material_index, S32 fallback_index) { // Check cache first auto cached = mMaterialCache.find(material_index); if (cached != mMaterialCache.end()) { return cached->second; } LLImportMaterial impMat; impMat.mDiffuseColor = LLColor4::white; // Default color // Generate material name std::string materialName = generateMaterialName(material_index, fallback_index); // Process material if available if (material_index >= 0 && material_index < mGLTFAsset.mMaterials.size()) { LL::GLTF::Material* material = &mGLTFAsset.mMaterials[material_index]; // Set diffuse color from base color factor impMat.mDiffuseColor = LLColor4( material->mPbrMetallicRoughness.mBaseColorFactor[0], material->mPbrMetallicRoughness.mBaseColorFactor[1], material->mPbrMetallicRoughness.mBaseColorFactor[2], material->mPbrMetallicRoughness.mBaseColorFactor[3] ); // Process base color texture if it exists if (material->mPbrMetallicRoughness.mBaseColorTexture.mIndex >= 0) { S32 texIndex = material->mPbrMetallicRoughness.mBaseColorTexture.mIndex; std::string filename = processTexture(texIndex, "base_color", material->mName); if (!filename.empty()) { impMat.mDiffuseMapFilename = filename; impMat.mDiffuseMapLabel = material->mName.empty() ? filename : material->mName; // Check if the texture is already loaded S32 sourceIndex; if (validateTextureIndex(texIndex, sourceIndex)) { LL::GLTF::Image& image = mGLTFAsset.mImages[sourceIndex]; if (image.mTexture.notNull()) { impMat.setDiffuseMap(image.mTexture->getID()); LL_INFOS("GLTF_IMPORT") << "Using existing texture ID: " << image.mTexture->getID().asString() << LL_ENDL; } else { LL_INFOS("GLTF_IMPORT") << "Texture needs loading: " << impMat.mDiffuseMapFilename << LL_ENDL; } } } } } // Create cached material with both material and name LLGLTFImportMaterial cachedMat(impMat, materialName); // Cache the processed material mMaterialCache[material_index] = cachedMat; return cachedMat; } std::string LLGLTFLoader::processTexture(S32 texture_index, const std::string& texture_type, const std::string& material_name) { S32 sourceIndex; if (!validateTextureIndex(texture_index, sourceIndex)) return ""; LL::GLTF::Image& image = mGLTFAsset.mImages[sourceIndex]; // Process URI-based textures if (!image.mUri.empty()) { std::string filename = image.mUri; size_t pos = filename.find_last_of("/\\"); if (pos != std::string::npos) { filename = filename.substr(pos + 1); } LL_INFOS("GLTF_IMPORT") << "Found texture: " << filename << " for material: " << material_name << LL_ENDL; LLSD args; args["Message"] = "TextureFound"; args["TEXTURE_NAME"] = filename; args["MATERIAL_NAME"] = material_name; mWarningsArray.append(args); return filename; } // Process embedded textures if (image.mBufferView >= 0) { return extractTextureToTempFile(texture_index, texture_type); } return ""; } bool LLGLTFLoader::validateTextureIndex(S32 texture_index, S32& source_index) { if (texture_index < 0 || texture_index >= mGLTFAsset.mTextures.size()) return false; source_index = mGLTFAsset.mTextures[texture_index].mSource; if (source_index < 0 || source_index >= mGLTFAsset.mImages.size()) return false; return true; } std::string LLGLTFLoader::generateMaterialName(S32 material_index, S32 fallback_index) { if (material_index >= 0 && material_index < mGLTFAsset.mMaterials.size()) { LL::GLTF::Material* material = &mGLTFAsset.mMaterials[material_index]; std::string materialName = material->mName; if (materialName.empty()) { materialName = "mat" + std::to_string(material_index); } return materialName; } else { return fallback_index >= 0 ? "mat_default" + std::to_string(fallback_index) : "mat_default"; } } bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const std::string& base_name, const LL::GLTF::Mesh& mesh, const LL::GLTF::Node& nodeno, material_map& mats) { // Set the requested label for the floater display and uploading pModel->mRequestedLabel = gDirUtilp->getBaseFileName(mFilename, true); // Set only name, suffix will be added later pModel->mLabel = base_name; LL_DEBUGS("GLTF_DEBUG") << "Processing model " << pModel->mLabel << LL_ENDL; pModel->ClearFacesAndMaterials(); S32 skinIdx = nodeno.mSkin; // Compute final combined transform matrix (hierarchy + coordinate rotation) S32 node_index = static_cast(&nodeno - &mGLTFAsset.mNodes[0]); glm::mat4 hierarchy_transform; computeCombinedNodeTransform(mGLTFAsset, node_index, hierarchy_transform); // Combine transforms: coordinate rotation applied to hierarchy transform glm::mat4 final_transform = coord_system_rotation * hierarchy_transform; if (mApplyXYRotation) { final_transform = coord_system_rotationxy * final_transform; } // Check if we have a negative scale (flipped coordinate system) bool hasNegativeScale = glm::determinant(final_transform) < 0.0f; // Pre-compute normal transform matrix (transpose of inverse of upper-left 3x3) const glm::mat3 normal_transform = glm::transpose(glm::inverse(glm::mat3(final_transform))); // Mark unsuported joints with '-1' so that they won't get added into weights // GLTF maps all joints onto all meshes. Gather use count per mesh to cut unused ones. std::vector gltf_joint_index_use; if (skinIdx >= 0 && mGLTFAsset.mSkins.size() > skinIdx) { LL::GLTF::Skin& gltf_skin = mGLTFAsset.mSkins[skinIdx]; size_t jointCnt = gltf_skin.mJoints.size(); gltf_joint_index_use.resize(jointCnt, 0); for (size_t i = 0; i < jointCnt; ++i) { if (mJointNames[skinIdx][i].empty()) { // This might need to hold a substitute index gltf_joint_index_use[i] = -1; // mark as unsupported } } } for (size_t prim_idx = 0; prim_idx < mesh.mPrimitives.size(); ++prim_idx) { const LL::GLTF::Primitive& prim = mesh.mPrimitives[prim_idx]; // So primitives already have all of the data we need for a given face in SL land. // Primitives may only ever have a single material assigned to them - as the relation is 1:1 in terms of intended draw call // count. Just go ahead and populate faces direct from the GLTF primitives here. -Geenz 2025-04-07 LLVolumeFace face; std::vector vertices; // Use cached material processing LLGLTFImportMaterial cachedMat = processMaterial(prim.mMaterial, pModel->getNumVolumeFaces() - 1); LLImportMaterial impMat = cachedMat; std::string materialName = cachedMat.name; mats[materialName] = impMat; if (prim.getIndexCount() % 3 != 0) { LL_WARNS("GLTF_IMPORT") << "Mesh '" << mesh.mName << "' primitive " << prim_idx << ": Invalid index count " << prim.getIndexCount() << " (not divisible by 3). GLTF files must contain triangulated geometry." << LL_ENDL; LLSD args; args["Message"] = "InvalidGeometryNonTriangulated"; args["MESH_NAME"] = mesh.mName; args["PRIMITIVE_INDEX"] = static_cast(prim_idx); args["INDEX_COUNT"] = static_cast(prim.getIndexCount()); mWarningsArray.append(args); return false; // Skip this primitive } // Apply the global scale and center offset to all vertices for (U32 i = 0; i < prim.getVertexCount(); i++) { // Use pre-computed final_transform glm::vec4 pos(prim.mPositions[i][0], prim.mPositions[i][1], prim.mPositions[i][2], 1.0f); glm::vec4 transformed_pos = final_transform * pos; GLTFVertex vert; vert.position = glm::vec3(transformed_pos); if (!prim.mNormals.empty()) { // Use pre-computed normal_transform glm::vec3 normal_vec(prim.mNormals[i][0], prim.mNormals[i][1], prim.mNormals[i][2]); vert.normal = glm::normalize(normal_transform * normal_vec); } else { // Use default normal (pointing up in model space) vert.normal = glm::normalize(normal_transform * glm::vec3(0.0f, 0.0f, 1.0f)); LL_DEBUGS("GLTF_IMPORT") << "No normals found for primitive, using default normal." << LL_ENDL; } vert.uv0 = glm::vec2(prim.mTexCoords0[i][0], -prim.mTexCoords0[i][1]); if (skinIdx >= 0) { vert.weights = glm::vec4(prim.mWeights[i]); auto accessorIdx = prim.mAttributes.at("JOINTS_0"); LL::GLTF::Accessor::ComponentType componentType = LL::GLTF::Accessor::ComponentType::UNSIGNED_BYTE; if (accessorIdx >= 0) { auto accessor = mGLTFAsset.mAccessors[accessorIdx]; componentType = accessor.mComponentType; } // The GLTF spec allows for either an unsigned byte for joint indices, or an unsigned short. // Detect and unpack accordingly. if (componentType == LL::GLTF::Accessor::ComponentType::UNSIGNED_BYTE) { auto ujoint = glm::unpackUint4x8((U32)(prim.mJoints[i] & 0xFFFFFFFF)); vert.joints = glm::u16vec4(ujoint.x, ujoint.y, ujoint.z, ujoint.w); } else if (componentType == LL::GLTF::Accessor::ComponentType::UNSIGNED_SHORT) { vert.joints = glm::unpackUint4x16(prim.mJoints[i]); } else { vert.joints = glm::zero(); vert.weights = glm::zero(); } } vertices.push_back(vert); } // Check for empty vertex array before processing if (vertices.empty()) { LL_WARNS("GLTF_IMPORT") << "Empty vertex array for primitive " << prim_idx << " in model " << mesh.mName << LL_ENDL; LLSD args; args["Message"] = "EmptyVertexArray"; args["MESH_NAME"] = mesh.mName; args["PRIMITIVE_INDEX"] = static_cast(prim_idx); args["INDEX_COUNT"] = static_cast(prim.getIndexCount()); mWarningsArray.append(args); return false; // Skip this primitive } std::vector faceVertices; glm::vec3 min = glm::vec3(FLT_MAX); glm::vec3 max = glm::vec3(-FLT_MAX); for (U32 i = 0; i < vertices.size(); i++) { LLVolumeFace::VertexData vert; // Update min/max bounds if (i == 0) { min = max = vertices[i].position; } else { min.x = std::min(min.x, vertices[i].position.x); min.y = std::min(min.y, vertices[i].position.y); min.z = std::min(min.z, vertices[i].position.z); max.x = std::max(max.x, vertices[i].position.x); max.y = std::max(max.y, vertices[i].position.y); max.z = std::max(max.z, vertices[i].position.z); } LLVector4a position = LLVector4a(vertices[i].position.x, vertices[i].position.y, vertices[i].position.z); LLVector4a normal = LLVector4a(vertices[i].normal.x, vertices[i].normal.y, vertices[i].normal.z); vert.setPosition(position); vert.setNormal(normal); vert.mTexCoord = LLVector2(vertices[i].uv0.x, vertices[i].uv0.y); faceVertices.push_back(vert); if (skinIdx >= 0) { // create list of weights that influence this vertex LLModel::weight_list weight_list; // Drop joints that viewer doesn't support (negative in gltf_joint_index_use_count) // don't reindex them yet, more indexes will be removed // Also drop joints that have no weight. GLTF stores 4 per vertex, so there might be // 'empty' ones if (gltf_joint_index_use[vertices[i].joints.x] >= 0 && vertices[i].weights.x > 0.f) { weight_list.push_back(LLModel::JointWeight(vertices[i].joints.x, vertices[i].weights.x)); gltf_joint_index_use[vertices[i].joints.x]++; } if (gltf_joint_index_use[vertices[i].joints.y] >= 0 && vertices[i].weights.y > 0.f) { weight_list.push_back(LLModel::JointWeight(vertices[i].joints.y, vertices[i].weights.y)); gltf_joint_index_use[vertices[i].joints.y]++; } if (gltf_joint_index_use[vertices[i].joints.z] >= 0 && vertices[i].weights.z > 0.f) { weight_list.push_back(LLModel::JointWeight(vertices[i].joints.z, vertices[i].weights.z)); gltf_joint_index_use[vertices[i].joints.z]++; } if (gltf_joint_index_use[vertices[i].joints.w] >= 0 && vertices[i].weights.w > 0.f) { weight_list.push_back(LLModel::JointWeight(vertices[i].joints.w, vertices[i].weights.w)); gltf_joint_index_use[vertices[i].joints.w]++; } std::sort(weight_list.begin(), weight_list.end(), LLModel::CompareWeightGreater()); std::vector wght; F32 total = 0.f; for (U32 j = 0; j < llmin((U32)4, (U32)weight_list.size()); ++j) { // take up to 4 most significant weights // Ported from the DAE loader - however, GLTF right now only supports up to four weights per vertex. wght.push_back(weight_list[j]); total += weight_list[j].mWeight; } if (total != 0.f) { F32 scale = 1.f / total; if (scale != 1.f) { // normalize weights for (U32 j = 0; j < wght.size(); ++j) { wght[j].mWeight *= scale; } } } if (wght.size() > 0) { pModel->mSkinWeights[LLVector3(vertices[i].position)] = wght; } } } // Indices handling if (faceVertices.size() >= VERTICIES_LIMIT) { // Will have to remap 32 bit indices into 16 bit indices // For the sake of simplicity build vector of 32 bit indices first std::vector indices_32; for (U32 i = 0; i < prim.getIndexCount(); i += 3) { // When processing indices, flip winding order if needed if (hasNegativeScale) { // Flip winding order for negative scale indices_32.push_back(prim.mIndexArray[i]); indices_32.push_back(prim.mIndexArray[i + 2]); // Swap these two indices_32.push_back(prim.mIndexArray[i + 1]); } else { indices_32.push_back(prim.mIndexArray[i]); indices_32.push_back(prim.mIndexArray[i + 1]); indices_32.push_back(prim.mIndexArray[i + 2]); } } // Generates a vertex remap table with no gaps in the resulting sequence std::vector remap(faceVertices.size()); size_t vertex_count = meshopt_generateVertexRemap(&remap[0], &indices_32[0], indices_32.size(), &faceVertices[0], faceVertices.size(), sizeof(LLVolumeFace::VertexData)); // Manually remap vertices std::vector optimized_vertices(vertex_count); for (size_t i = 0; i < vertex_count; ++i) { optimized_vertices[i] = faceVertices[remap[i]]; } std::vector optimized_indices(indices_32.size()); meshopt_remapIndexBuffer(&optimized_indices[0], &indices_32[0], indices_32.size(), &remap[0]); // Sort indices to improve mesh splits (reducing amount of duplicated indices) meshopt_optimizeVertexCache(&optimized_indices[0], &optimized_indices[0], indices_32.size(), vertex_count); std::vector indices_16; std::vector vertices_remap; vertices_remap.resize(vertex_count, -1); S32 created_faces = 0; std::vector face_verts; min = glm::vec3(FLT_MAX); max = glm::vec3(-FLT_MAX); for (size_t idx = 0; idx < optimized_indices.size(); idx++) { size_t vert_index = optimized_indices[idx]; if (vertices_remap[vert_index] == -1) { // First encounter, add it size_t new_vert_idx = face_verts.size(); vertices_remap[vert_index] = (S64)new_vert_idx; face_verts.push_back(optimized_vertices[vert_index]); vert_index = new_vert_idx; // Update min/max bounds const LLVector4a& vec = face_verts[new_vert_idx].getPosition(); if (new_vert_idx == 0) { min.x = vec[0]; min.y = vec[1]; min.z = vec[2]; max = min; } else { min.x = std::min(min.x, vec[0]); min.y = std::min(min.y, vec[1]); min.z = std::min(min.z, vec[2]); max.x = std::max(max.x, vec[0]); max.y = std::max(max.y, vec[1]); max.z = std::max(max.z, vec[2]); } } else { // already in vector, get position vert_index = (size_t)vertices_remap[vert_index]; } indices_16.push_back((U16)vert_index); if (indices_16.size() % 3 == 0 && face_verts.size() >= VERTICIES_LIMIT - 1) { LLVolumeFace face; face.fillFromLegacyData(face_verts, indices_16); face.mExtents[0] = LLVector4a(min.x, min.y, min.z, 0); face.mExtents[1] = LLVector4a(max.x, max.y, max.z, 0); pModel->getVolumeFaces().push_back(face); pModel->getMaterialList().push_back(materialName); created_faces++; std::fill(vertices_remap.begin(), vertices_remap.end(), -1); indices_16.clear(); face_verts.clear(); min = glm::vec3(FLT_MAX); max = glm::vec3(-FLT_MAX); } } if (indices_16.size() > 0 && face_verts.size() > 0) { LLVolumeFace face; face.fillFromLegacyData(face_verts, indices_16); face.mExtents[0] = LLVector4a(min.x, min.y, min.z, 0); face.mExtents[1] = LLVector4a(max.x, max.y, max.z, 0); pModel->getVolumeFaces().push_back(face); pModel->getMaterialList().push_back(materialName); created_faces++; } LL_INFOS("GLTF_IMPORT") << "Primitive " << (S32)prim_idx << " from model " << pModel->mLabel << " is over vertices limit, it was split into " << created_faces << " faces" << LL_ENDL; LLSD args; args["Message"] = "ModelSplitPrimitive"; args["MODEL_NAME"] = pModel->mLabel; args["FACE_COUNT"] = created_faces; mWarningsArray.append(args); } else { // can use indices directly std::vector indices; for (U32 i = 0; i < prim.getIndexCount(); i += 3) { // When processing indices, flip winding order if needed if (hasNegativeScale) { // Flip winding order for negative scale indices.push_back(prim.mIndexArray[i]); indices.push_back(prim.mIndexArray[i + 2]); // Swap these two indices.push_back(prim.mIndexArray[i + 1]); } else { indices.push_back(prim.mIndexArray[i]); indices.push_back(prim.mIndexArray[i + 1]); indices.push_back(prim.mIndexArray[i + 2]); } } face.fillFromLegacyData(faceVertices, indices); face.mExtents[0] = LLVector4a(min.x, min.y, min.z, 0); face.mExtents[1] = LLVector4a(max.x, max.y, max.z, 0); pModel->getVolumeFaces().push_back(face); pModel->getMaterialList().push_back(materialName); } } // Call normalizeVolumeFacesAndWeights to compute proper extents pModel->normalizeVolumeFacesAndWeights(); // 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, -1); if (valid_joints_count > (S32)mMaxJointsPerMesh) { std::map goup_use_count; for (const auto& elem : mJointGroups) { goup_use_count[elem.second.mGroup] = 0; goup_use_count[elem.second.mParentGroup] = 0; } // 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) { 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]++; } } } // 1. add joints that are in use directly 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 // unused (0) joint, drop it continue; } if (addJointToModelSkin(skin_info, skinIdx, i)) { gltfindex_to_joitindex_map[i] = replacement_index++; } } // 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) { 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() > mMaxJointsPerMesh) { 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; } if (addJointToModelSkin(skin_info, skinIdx, i)) { gltfindex_to_joitindex_map[i] = replacement_index++; } } } if (skin_info.mInvBindMatrix.size() > mMaxJointsPerMesh) { // mMaxJointsPerMesh ususlly is equal to LL_MAX_JOINTS_PER_MESH_OBJECT // and is 110. LL_WARNS("GLTF_IMPORT") << "Too many jonts in " << pModel->mLabel << " Count: " << (S32)skin_info.mInvBindMatrix.size() << " Limit:" << (S32)mMaxJointsPerMesh << LL_ENDL; LLSD args; args["Message"] = "ModelTooManyJoints"; args["MODEL_NAME"] = pModel->mLabel; args["JOINT_COUNT"] = (S32)skin_info.mInvBindMatrix.size(); args["MAX"] = (S32)mMaxJointsPerMesh; mWarningsArray.append(args); } // Remap indices for pModel->mSkinWeights for (auto& weights : pModel->mSkinWeights) { for (auto& weight : weights.second) { weight.mJointIdx = gltfindex_to_joitindex_map[weight.mJointIdx]; } } } return true; } void LLGLTFLoader::populateJointsFromSkin(S32 skin_idx) { const LL::GLTF::Skin& skin = mGLTFAsset.mSkins[skin_idx]; LL_INFOS("GLTF_DEBUG") << "populateJointFromSkin: Processing skin " << skin_idx << " with " << skin.mJoints.size() << " joints" << LL_ENDL; if (skin.mInverseBindMatrices > 0 && skin.mJoints.size() != skin.mInverseBindMatricesData.size()) { LL_INFOS("GLTF_IMPORT") << "Bind matrices count mismatch joints count" << LL_ENDL; LLSD args; args["Message"] = "InvBindCountMismatch"; mWarningsArray.append(args); } S32 joint_count = (S32)skin.mJoints.size(); S32 inverse_count = (S32)skin.mInverseBindMatricesData.size(); if (mInverseBindMatrices.size() <= skin_idx) { mInverseBindMatrices.resize(skin_idx + 1); mAlternateBindMatrices.resize(skin_idx + 1); mJointNames.resize(skin_idx + 1); mJointUsage.resize(skin_idx + 1); mValidJointsCount.resize(skin_idx + 1, 0); } // 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]; const 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; mValidJointsCount[skin_idx]++; } 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 // This is needed because gltf skeleton doesn't necessarily match viewer's skeleton. 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++) { S32 joint = skin.mJoints[i]; const LL::GLTF::Node &jointNode = mGLTFAsset.mNodes[joint]; std::string legal_name(jointNode.mName); // Viewer supports a limited set of joints, mark them as legal bool legal_joint = false; if (mJointMap.find(legal_name) != mJointMap.end()) { legal_name = mJointMap[legal_name]; legal_joint = true; mJointNames[skin_idx].push_back(legal_name); } else { mJointNames[skin_idx].emplace_back(); } mJointUsage[skin_idx].push_back(0); // Compute bind matrices if (!legal_joint) { // 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 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(joints_data, 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_DEBUGS("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 YZ rotated matrix glm::mat4 inv_bind(1.0f); 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)); LL_DEBUGS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " Generated val: " << gltf_transform << LL_ENDL; mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); } // Compute Alternative matrices also known as overrides 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_DEBUGS("GLTF_DEBUG") << "mAlternateBindMatrix name: " << legal_name << " val: " << newInverse << LL_ENDL; mAlternateBindMatrices[skin_idx].push_back(LLMatrix4a(newInverse)); if (legal_joint) { // 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); } } S32 valid_joints = mValidJointsCount[skin_idx]; if (valid_joints < joint_count) { LL_INFOS("GLTF_IMPORT") << "Skin " << skin_idx << " defines " << joint_count << " joints, but only " << valid_joints << " were recognized and are compatible." << LL_ENDL; LLSD args; args["Message"] = "SkinUsupportedJoints"; args["SKIN_INDEX"] = skin_idx; args["JOINT_COUNT"] = joint_count; args["LEGAL_COUNT"] = valid_joints; mWarningsArray.append(args); } } void LLGLTFLoader::populateJointGroups() { std::string parent; for (auto& viewer_data : mViewerJointData) { buildJointGroup(viewer_data, parent); } } 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 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; 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; } 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(translated_joint, scale, rotation, translation_override, skew, perspective); // Viewer allows overrides, which are base joint with applied translation override. // fortunately normal bones use only translation, without rotation or scale 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 overriden_joint = node.mOverrideMatrix; // todo: if gltf bone had rotation or scale, they probably should be saved here // then applied to bind matrix rest = parent_rest * overriden_joint; if (viewer_data.mIsJoint) { node.mOverrideRestMatrix = rest; } else { // This is likely incomplete or even wrong. // Viewer Collision bones specify rotation and scale. // Importer should apply rotation and scale to this matrix and save as needed // then subsctruct them from bind matrix // Todo: get models that use collision bones, made by different programs overriden_joint = glm::scale(overriden_joint, viewer_data.mScale); node.mOverrideRestMatrix = parent_support_rest * overriden_joint; } } else { // 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, support_rest); } } 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 // Todo: cache it? if (joint_node_index < 0 || joint_node_index >= static_cast(mGLTFAsset.mNodes.size())) { return glm::mat4(1.0f); } const auto& node = mGLTFAsset.mNodes[joint_node_index]; // Find and apply parent transform if it exists for (size_t i = 0; i < mGLTFAsset.mNodes.size(); ++i) { const auto& potential_parent = mGLTFAsset.mNodes[i]; auto it = std::find(potential_parent.mChildren.begin(), potential_parent.mChildren.end(), joint_node_index); if (it != potential_parent.mChildren.end()) { // Found parent if (std::find(gltf_skin.mJoints.begin(), gltf_skin.mJoints.end(), joint_node_index) != gltf_skin.mJoints.end()) { // parent is a joint - recursively combine transform // assumes that matrix is already valid return buildGltfRestMatrix(static_cast(i), gltf_skin) * node.mMatrix; } } } // Should we return armature or stop earlier? 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 joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const { 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) return glm::mat4(1.0f); } // Get the GLTF joint's rest pose (in GLTF coordinate system) 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 << ": "; 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 node_data.mOverrideRestMatrix * glm::inverse(rest_pose); } bool LLGLTFLoader::checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx) { glm::mat4 gltf_joint_rest = buildGltfRestMatrix(joint_idx, gltf_skin); glm::mat4 test_mat = glm::inverse(gltf_joint_rest) * gltf_skin.mInverseBindMatricesData[bind_indx]; // Normally for shoulders it should be something close to // {1,0,0,0;0,-1,0,0;0,0,-1,0;0,0,0,1} // rotated one will look like // {0,0,0,-1;1,0,0,0;0,-1,0,0;0,0,0,1} // Todo: This is a cheap hack, // figure out how rotation is supposed to work return abs(test_mat[0][0]) < 0.5 && abs(test_mat[1][1]) < 0.5 && abs(test_mat[2][2]) < 0.5; } void LLGLTFLoader::checkForXYrotation(const LL::GLTF::Skin& gltf_skin) { // HACK: figure out model's rotation from shoulders' matrix. // This is wrong on many levels: // Too limited (only models that have shoulders), // Will not work well with things that emulate 3 hands in some manner // Only supports xy 90 degree rotation // Todo: figure out how to find skeleton's orientation Correctly // when model is rotated at a triangle level constexpr char right_shoulder_str[] = "mShoulderRight"; constexpr char left_shoulder_str[] = "mShoulderLeft"; S32 size = (S32)gltf_skin.mJoints.size(); S32 joints_found = 0; for (S32 i= 0; i < size; i++) { S32 joint = gltf_skin.mJoints[i]; const LL::GLTF::Node &joint_node = mGLTFAsset.mNodes[joint]; // todo: we are doing this search thing everywhere, // just pre-translate every joint JointMap::iterator found = mJointMap.find(joint_node.mName); if (found == mJointMap.end()) { // unsupported joint continue; } if (found->second == right_shoulder_str || found->second == left_shoulder_str) { if (checkForXYrotation(gltf_skin, joint, i)) { joints_found++; } else { return; } } } if (joints_found == 2) { // Both joints in a weird position/rotation, assume rotated model mApplyXYRotation = true; } } void LLGLTFLoader::checkGlobalJointUsage() { // Check if some joints remained unused for (S32 skin_idx = 0; skin_idx < (S32)mGLTFAsset.mSkins.size(); ++skin_idx) { const LL::GLTF::Skin& gltf_skin = mGLTFAsset.mSkins[skin_idx]; S32 joint_count = (S32)gltf_skin.mJoints.size(); S32 used_joints = 0; for (S32 i = 0; i < joint_count; ++i) { S32 joint = gltf_skin.mJoints[i]; if (mJointUsage[skin_idx][i] == 0) { // Joint is unused, log it LL_INFOS("GLTF_DEBUG") << "Joint " << mJointNames[skin_idx][i] << " in skin " << skin_idx << " is unused." << LL_ENDL; } else { used_joints++; } } S32 valid_joints = mValidJointsCount[skin_idx]; if (valid_joints > used_joints) { S32 unsed_joints = valid_joints - used_joints; LL_INFOS("GLTF_IMPORT") << "Skin " << skin_idx << " declares " << valid_joints << " valid joints, of them " << unsed_joints << " remained unused" << LL_ENDL; LLSD args; args["Message"] = "SkinUnusedJoints"; args["SKIN_INDEX"] = (S32)skin_idx; args["JOINT_COUNT"] = valid_joints; args["USED_COUNT"] = used_joints; mWarningsArray.append(args); } } } std::string LLGLTFLoader::extractTextureToTempFile(S32 textureIndex, const std::string& texture_type) { if (textureIndex < 0 || textureIndex >= mGLTFAsset.mTextures.size()) return ""; S32 sourceIndex = mGLTFAsset.mTextures[textureIndex].mSource; if (sourceIndex < 0 || sourceIndex >= mGLTFAsset.mImages.size()) return ""; LL::GLTF::Image& image = mGLTFAsset.mImages[sourceIndex]; // Handle URI-based textures if (!image.mUri.empty()) { return image.mUri; // Return URI directly } // Handle embedded textures if (image.mBufferView >= 0) { if (image.mBufferView < mGLTFAsset.mBufferViews.size()) { const LL::GLTF::BufferView& buffer_view = mGLTFAsset.mBufferViews[image.mBufferView]; if (buffer_view.mBuffer < mGLTFAsset.mBuffers.size()) { const LL::GLTF::Buffer& buffer = mGLTFAsset.mBuffers[buffer_view.mBuffer]; if (buffer_view.mByteOffset + buffer_view.mByteLength <= buffer.mData.size()) { // Extract image data const U8* data_ptr = &buffer.mData[buffer_view.mByteOffset]; U32 data_size = buffer_view.mByteLength; // Determine the file extension std::string extension = ".png"; // Default if (!image.mMimeType.empty()) { if (image.mMimeType == "image/jpeg") extension = ".jpg"; else if (image.mMimeType == "image/png") extension = ".png"; } else if (data_size >= 4) { if (data_ptr[0] == 0xFF && data_ptr[1] == 0xD8) extension = ".jpg"; // JPEG magic bytes else if (data_ptr[0] == 0x89 && data_ptr[1] == 0x50 && data_ptr[2] == 0x4E && data_ptr[3] == 0x47) extension = ".png"; // PNG magic bytes } // Create a temporary file std::string temp_dir = gDirUtilp->getTempDir(); std::string temp_filename = temp_dir + gDirUtilp->getDirDelimiter() + "gltf_embedded_" + texture_type + "_" + std::to_string(sourceIndex) + extension; // Write the image data to the temporary file std::ofstream temp_file(temp_filename, std::ios::binary); if (temp_file.is_open()) { temp_file.write(reinterpret_cast(data_ptr), data_size); temp_file.close(); LL_INFOS("GLTF_IMPORT") << "Extracted embedded " << texture_type << " texture to: " << temp_filename << LL_ENDL; return temp_filename; } else { LL_WARNS("GLTF_IMPORT") << "Failed to create temporary file for " << texture_type << " texture: " << temp_filename << LL_ENDL; LLSD args; args["Message"] = "FailedToCreateTempFile"; args["TEXTURE_INDEX"] = sourceIndex; args["TEXTURE_TYPE"] = texture_type; args["TEMP_FILE"] = temp_filename; mWarningsArray.append(args); } } } } } return ""; } void LLGLTFLoader::notifyUnsupportedExtension(bool unsupported) { std::vector extensions = unsupported ? mGLTFAsset.mUnsupportedExtensions : mGLTFAsset.mIgnoredExtensions; if (extensions.size() > 0) { LLSD args; args["Message"] = unsupported ? "UnsupportedExtension" : "IgnoredExtension"; std::string del; std::string ext; for (auto& extension : extensions) { ext += del; ext += extension; del = ","; } args["EXT"] = ext; mWarningsArray.append(args); LL_WARNS("GLTF_IMPORT") << "Model uses unsupported extension: " << ext << LL_ENDL; } } size_t LLGLTFLoader::getSuffixPosition(const std::string &label) { if ((label.find("_LOD") != -1) || (label.find("_PHYS") != -1)) { return label.rfind('_'); } return -1; } std::string LLGLTFLoader::getLodlessLabel(const LL::GLTF::Mesh& mesh) { size_t ext_pos = getSuffixPosition(mesh.mName); if (ext_pos != -1) { return mesh.mName.substr(0, ext_pos); } return mesh.mName; }