/** * @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 "llmatrix4a.h" #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 ); 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) //, //bool preprocess) : LLModelLoader( filename, lod, load_cb, joint_lookup_func, texture_load_func, state_cb, opaque_userdata, jointTransformMap, jointsFromNodes, jointAliasMap, maxJointsPerMesh ), //mPreprocessGLTF(preprocess), mMeshesLoaded(false), mMaterialsLoaded(false), mGeneratedModelLimit(modelLimit) { } LLGLTFLoader::~LLGLTFLoader() {} bool LLGLTFLoader::OpenFile(const std::string &filename) { tinygltf::TinyGLTF loader; std::string filename_lc(filename); LLStringUtil::toLower(filename_lc); mGltfLoaded = mGLTFAsset.load(filename, false); if (!mGltfLoaded) { notifyUnsupportedExtension(true); return false; } notifyUnsupportedExtension(false); mMeshesLoaded = parseMeshes(); if (mMeshesLoaded) uploadMeshes(); mMaterialsLoaded = parseMaterials(); if (mMaterialsLoaded) uploadMaterials(); setLoadState(DONE); return (mMeshesLoaded); } void LLGLTFLoader::addModelToScene( LLModel* pModel, 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) { 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; next->mLabel = pModel->mLabel + (char)((int)'a' + next->mSubmodelID) + lod_suffix[mLod]; 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(); // remove unused/redundant weights and joints model->remapSkinWeightsAndJoints(); 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(); } } mScene[transformation].push_back(LLModelInstance(model, model->mLabel, 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(); } // 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); } // Track how many times each mesh name has been used std::map mesh_name_counts; U32 submodel_limit = mGLTFAsset.mNodes.size() > 0 ? mGeneratedModelLimit / (U32)mGLTFAsset.mNodes.size() : 0; // Build parent mapping for efficient traversal std::vector node_parents(mGLTFAsset.mNodes.size(), -1); std::vector is_root(mGLTFAsset.mNodes.size(), true); // Build parent relationships for (size_t parent_idx = 0; parent_idx < mGLTFAsset.mNodes.size(); parent_idx++) { const auto& parent_node = mGLTFAsset.mNodes[parent_idx]; for (S32 child_idx : parent_node.mChildren) { if (child_idx >= 0 && child_idx < static_cast(mGLTFAsset.mNodes.size())) { node_parents[child_idx] = static_cast(parent_idx); is_root[child_idx] = false; } } } // Process all root nodes and their hierarchies for (size_t node_idx = 0; node_idx < mGLTFAsset.mNodes.size(); node_idx++) { if (is_root[node_idx]) { processNodeHierarchy(static_cast(node_idx), mesh_name_counts, submodel_limit, volume_params); } } 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; auto& node = mGLTFAsset.mNodes[node_idx]; // 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); auto& mesh = mGLTFAsset.mMeshes[node.mMesh]; // Get base mesh name and track usage std::string base_name = mesh.mName; if (base_name.empty()) { base_name = "mesh_" + std::to_string(node.mMesh); } S32 instance_count = mesh_name_counts[base_name]++; if (populateModelFromMesh(pModel, mesh, node, mats, instance_count) && (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, submodel_limit, transformation, volume_params, mats); mats.clear(); } else { setLoadState(ERROR_MODEL + pModel->getStatus()); delete pModel; return; } } // 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::populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh& mesh, const LL::GLTF::Node& nodeno, material_map& mats, S32 instance_count) { // Create unique model name std::string base_name = mesh.mName; if (base_name.empty()) { S32 mesh_index = static_cast(&mesh - &mGLTFAsset.mMeshes[0]); base_name = "mesh_" + std::to_string(mesh_index); } LL_INFOS("GLTF_DEBUG") << "Processing model " << base_name << LL_ENDL; if (instance_count > 0) { pModel->mLabel = base_name + "_copy_" + std::to_string(instance_count); } else { pModel->mLabel = base_name; } 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 const glm::mat4 final_transform = coord_system_rotation * hierarchy_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_count; 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_count.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()) { gltf_joint_index_use_count[i] = -1; // mark as unsupported } } } for (const LL::GLTF::Primitive& prim : mesh.mPrimitives) { // Unfortunately, SLM does not support 32 bit indices. Filter out anything that goes beyond 16 bit. if (prim.getVertexCount() < USHRT_MAX) { // 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; std::vector indices; LLImportMaterial impMat; impMat.mDiffuseColor = LLColor4::white; // Default color // Process material if available if (prim.mMaterial >= 0 && prim.mMaterial < mGLTFAsset.mMaterials.size()) { LL::GLTF::Material* material = &mGLTFAsset.mMaterials[prim.mMaterial]; // 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; if (texIndex < mGLTFAsset.mTextures.size()) { S32 sourceIndex = mGLTFAsset.mTextures[texIndex].mSource; if (sourceIndex >= 0 && sourceIndex < mGLTFAsset.mImages.size()) { LL::GLTF::Image& image = mGLTFAsset.mImages[sourceIndex]; // Use URI as texture file name if (!image.mUri.empty()) { // URI might be a remote URL or a local path std::string filename = image.mUri; // Extract just the filename from the URI size_t pos = filename.find_last_of("/\\"); if (pos != std::string::npos) { filename = filename.substr(pos + 1); } // Store the texture filename impMat.mDiffuseMapFilename = filename; impMat.mDiffuseMapLabel = material->mName.empty() ? filename : material->mName; LL_INFOS("GLTF_IMPORT") << "Found texture: " << impMat.mDiffuseMapFilename << LL_ENDL; // If the image has a texture loaded already, use it if (image.mTexture.notNull()) { impMat.setDiffuseMap(image.mTexture->getID()); LL_INFOS("GLTF_IMPORT") << "Using existing texture ID: " << image.mTexture->getID().asString() << LL_ENDL; } else { // Let the model preview know we need to load this texture mNumOfFetchingTextures++; LL_INFOS("GLTF_IMPORT") << "Adding texture to load queue: " << impMat.mDiffuseMapFilename << LL_ENDL; } } else if (image.mTexture.notNull()) { // No URI but we have a texture, use it directly impMat.setDiffuseMap(image.mTexture->getID()); LL_INFOS("GLTF_IMPORT") << "Using existing texture ID without URI: " << image.mTexture->getID().asString() << LL_ENDL; } else if (image.mBufferView >= 0) { // For embedded textures (no URI but has buffer data) // Create a pseudo filename for the embedded texture std::string pseudo_filename = "gltf_embedded_texture_" + std::to_string(sourceIndex) + ".png"; impMat.mDiffuseMapFilename = pseudo_filename; impMat.mDiffuseMapLabel = material->mName.empty() ? pseudo_filename : material->mName; // Mark for loading mNumOfFetchingTextures++; LL_INFOS("GLTF_IMPORT") << "Adding embedded texture to load queue: " << pseudo_filename << LL_ENDL; } } } } } // 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); } if (prim.getIndexCount() % 3 != 0) { LL_WARNS("GLTF_IMPORT") << "Invalid primitive: index count " << prim.getIndexCount() << " is not divisible by 3. GLTF files must contain triangulated geometry." << LL_ENDL; LLSD args; args["Message"] = "InvalidGeometryNonTriangulated"; mWarningsArray.append(args); continue; // Skip this primitive } // When processing indices, flip winding order if needed for (U32 i = 0; i < prim.getIndexCount(); i += 3) { 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]); } } // Check for empty vertex array before processing if (vertices.empty()) { LL_WARNS("GLTF_IMPORT") << "Empty vertex array for primitive" << LL_ENDL; continue; // 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_count[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_count[vertices[i].joints.x]++; } if (gltf_joint_index_use_count[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_count[vertices[i].joints.y]++; } if (gltf_joint_index_use_count[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_count[vertices[i].joints.z]++; } if (gltf_joint_index_use_count[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_count[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; } } } 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); // Create a unique material name for this primitive std::string materialName; if (prim.mMaterial >= 0 && prim.mMaterial < mGLTFAsset.mMaterials.size()) { LL::GLTF::Material* material = &mGLTFAsset.mMaterials[prim.mMaterial]; materialName = material->mName; if (materialName.empty()) { materialName = "mat" + std::to_string(prim.mMaterial); } } else { materialName = "mat_default" + std::to_string(pModel->getNumVolumeFaces() - 1); } pModel->getMaterialList().push_back(materialName); mats[materialName] = impMat; } else { LL_INFOS("GLTF_IMPORT") << "Unable to process mesh due to 16-bit index limits" << LL_ENDL; LLSD args; args["Message"] = "ErrorIndexLimit"; mWarningsArray.append(args); return false; } } // Call normalizeVolumeFacesAndWeights to compute proper extents pModel->normalizeVolumeFacesAndWeights(); // Fill joint names, bind matrices and prepare to remap weight indices if (skinIdx >= 0) { LL::GLTF::Skin& gltf_skin = mGLTFAsset.mSkins[skinIdx]; LLMeshSkinInfo& skin_info = pModel->mSkinInfo; std::vector gltfindex_to_joitindex_map; size_t jointCnt = gltf_skin.mJoints.size(); gltfindex_to_joitindex_map.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]; if (gltf_joint_index_use_count[i] <= 0) { // Unused (0) or unsupported (-1) joint, drop it continue; } LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; std::string legal_name(jointNode.mName); if (mJointMap.find(legal_name) != mJointMap.end()) { legal_name = mJointMap[legal_name]; } else { llassert(false); // should have been stopped by gltf_joint_index_use_count[i] == -1 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]); // For alternate bind matrix, use the ORIGINAL joint transform (before rotation) // Get the original joint node and use its matrix directly // Todo: this seems blatantly wrong, it should have been rotated glm::mat4 joint_mat = jointNode.mMatrix; S32 root_joint = findValidRootJointNode(joint, gltf_skin); // skeleton can have multiple real roots if (root_joint == joint) { // This is very likely incomplete in some way. // Root shouldn't be the only one to need full coordinate fix joint_mat = coord_system_rotation * joint_mat; } LLMatrix4 original_joint_transform(glm::value_ptr(joint_mat)); LL_INFOS("GLTF_DEBUG") << "mAlternateBindMatrix name: " << legal_name << " val: " << original_joint_transform << LL_ENDL; skin_info.mAlternateBindMatrix.push_back(LLMatrix4a(original_joint_transform)); } // Remap indices for pModel->mSkinWeights // Todo: this is now partially redundant due to // remapSkinWeightsAndJoints being called later. // Consider storing all joints now as is and let it // ramap later due to missing weights. for (auto& weights : pModel->mSkinWeights) { for (auto& weight : weights.second) { weight.mJointIdx = gltfindex_to_joitindex_map[weight.mJointIdx]; } } } return true; } void LLGLTFLoader::populateJointFromSkin(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); } for (S32 i = 0; i < joint_count; i++) { S32 joint = skin.mJoints[i]; LL::GLTF::Node jointNode = mGLTFAsset.mNodes[joint]; std::string legal_name(jointNode.mName); bool legal_joint = false; if (mJointMap.find(legal_name) != mJointMap.end()) { legal_name = mJointMap[legal_name]; legal_joint = true; } if (!legal_joint) { // add placeholder to not break index LLMatrix4 gltf_transform; gltf_transform.setIdentity(); mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); } else if (inverse_count > i) { LL::GLTF::mat4 gltf_mat = skin.mInverseBindMatricesData[i]; // Todo: there should be a simpler way glm::mat4 original_bind_matrix = glm::inverse(gltf_mat); glm::mat4 rotated_original = coord_system_rotation * original_bind_matrix; glm::mat4 rotated_inverse_bind_matrix = glm::inverse(rotated_original); LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(rotated_inverse_bind_matrix)); LL_INFOS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " val: " << gltf_transform << LL_ENDL; mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); } else { LLMatrix4 gltf_transform; gltf_transform.setIdentity(); LL_INFOS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " val: " << gltf_transform << LL_ENDL; mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); } // todo: prepare mAlternateBindMatrix here 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++) { 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; } 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; } } S32 LLGLTFLoader::findClosestValidJoint(S32 source_joint, const LL::GLTF::Skin& gltf_skin) const { S32 source_joint_node = gltf_skin.mJoints[source_joint]; S32 root_node = source_joint_node; S32 found_node = source_joint_node; S32 size = (S32)gltf_skin.mJoints.size(); do { root_node = found_node; for (S32 i = 0; i < size; i++) { S32 joint = gltf_skin.mJoints[i]; const LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; std::vector::const_iterator it = std::find(jointNode.mChildren.begin(), jointNode.mChildren.end(), root_node); if (it != jointNode.mChildren.end()) { // Found node's parent found_node = joint; if (mJointMap.find(jointNode.mName) != mJointMap.end()) { return i; } break; } } } while (root_node != found_node); return -1; } S32 LLGLTFLoader::findValidRootJointNode(S32 source_joint_node, const LL::GLTF::Skin& gltf_skin) const { S32 root_node = 0; S32 found_node = source_joint_node; S32 size = (S32)gltf_skin.mJoints.size(); do { root_node = found_node; for (S32 i = 0; i < size; i++) { S32 joint = gltf_skin.mJoints[i]; const LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; if (mJointMap.find(jointNode.mName) != mJointMap.end()) { std::vector::const_iterator it = std::find(jointNode.mChildren.begin(), jointNode.mChildren.end(), root_node); if (it != jointNode.mChildren.end()) { // Found node's parent found_node = joint; break; } } } } while (root_node != found_node); return root_node; } S32 LLGLTFLoader::findGLTFRootJointNode(const LL::GLTF::Skin& gltf_skin) const { S32 root_node = 0; S32 found_node = 0; S32 size = (S32)gltf_skin.mJoints.size(); do { root_node = found_node; for (S32 i = 0; i < size; i++) { S32 joint = gltf_skin.mJoints[i]; const LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; std::vector::const_iterator it = std::find(jointNode.mChildren.begin(), jointNode.mChildren.end(), root_node); if (it != jointNode.mChildren.end()) { // Found node's parent found_node = joint; break; } } } while (root_node != found_node); LL_INFOS("GLTF_DEBUG") << "mJointList name: "; const LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[root_node]; LL_CONT << jointNode.mName << " index: " << root_node << LL_ENDL; return root_node; } S32 LLGLTFLoader::findParentNode(S32 node) const { S32 size = (S32)mGLTFAsset.mNodes.size(); for (S32 i = 0; i < size; i++) { const LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[i]; std::vector::const_iterator it = std::find(jointNode.mChildren.begin(), jointNode.mChildren.end(), node); if (it != jointNode.mChildren.end()) { return i; } } return -1; } bool LLGLTFLoader::parseMaterials() { if (!mGltfLoaded) return false; // fill local texture data structures mSamplers.clear(); for (auto& in_sampler : mGLTFAsset.mSamplers) { gltf_sampler sampler; sampler.magFilter = in_sampler.mMagFilter > 0 ? in_sampler.mMagFilter : GL_LINEAR; sampler.minFilter = in_sampler.mMinFilter > 0 ? in_sampler.mMinFilter : GL_LINEAR; sampler.wrapS = in_sampler.mWrapS; sampler.wrapT = in_sampler.mWrapT; sampler.name = in_sampler.mName; mSamplers.push_back(sampler); } mImages.clear(); for (auto& in_image : mGLTFAsset.mImages) { gltf_image image; image.numChannels = in_image.mComponent; image.bytesPerChannel = in_image.mBits >> 3; // Convert bits to bytes image.pixelType = in_image.mPixelType; image.size = 0; // We'll calculate this below if we have valid dimensions // Get dimensions from the texture if available if (in_image.mTexture && in_image.mTexture->getDiscardLevel() >= 0) { image.height = in_image.mTexture->getHeight(); image.width = in_image.mTexture->getWidth(); // Since we don't have direct access to the raw data, we'll use the dimensions to calculate size if (image.height > 0 && image.width > 0 && image.numChannels > 0 && image.bytesPerChannel > 0) { image.size = static_cast(image.height * image.width * image.numChannels * image.bytesPerChannel); } } else { // Fallback to provided dimensions image.height = in_image.mHeight; image.width = in_image.mWidth; if (image.height > 0 && image.width > 0 && image.numChannels > 0 && image.bytesPerChannel > 0) { image.size = static_cast(image.height * image.width * image.numChannels * image.bytesPerChannel); } } // If we couldn't determine the size, skip this image if (image.size == 0) { LL_WARNS("GLTF_IMPORT") << "Image size could not be determined" << LL_ENDL; continue; } // We don't have direct access to the image data, so data pointer remains nullptr image.data = nullptr; mImages.push_back(image); } mTextures.clear(); for (auto& in_tex : mGLTFAsset.mTextures) { gltf_texture tex; tex.imageIdx = in_tex.mSource; tex.samplerIdx = in_tex.mSampler; tex.imageUuid.setNull(); if (tex.imageIdx >= mImages.size() || tex.samplerIdx >= mSamplers.size()) { LL_WARNS("GLTF_IMPORT") << "Texture sampler/image index error" << LL_ENDL; return false; } mTextures.push_back(tex); } // parse each material mMaterials.clear(); for (const auto& gltf_material : mGLTFAsset.mMaterials) { gltf_render_material mat; mat.name = gltf_material.mName; // PBR Metallic Roughness properties mat.hasPBR = true; // Base color factor mat.baseColor = LLColor4( gltf_material.mPbrMetallicRoughness.mBaseColorFactor[0], gltf_material.mPbrMetallicRoughness.mBaseColorFactor[1], gltf_material.mPbrMetallicRoughness.mBaseColorFactor[2], gltf_material.mPbrMetallicRoughness.mBaseColorFactor[3] ); // Base color texture mat.hasBaseTex = gltf_material.mPbrMetallicRoughness.mBaseColorTexture.mIndex >= 0; mat.baseColorTexIdx = gltf_material.mPbrMetallicRoughness.mBaseColorTexture.mIndex; mat.baseColorTexCoords = gltf_material.mPbrMetallicRoughness.mBaseColorTexture.mTexCoord; // Metalness and roughness mat.metalness = gltf_material.mPbrMetallicRoughness.mMetallicFactor; mat.roughness = gltf_material.mPbrMetallicRoughness.mRoughnessFactor; // Metallic-roughness texture mat.hasMRTex = gltf_material.mPbrMetallicRoughness.mMetallicRoughnessTexture.mIndex >= 0; mat.metalRoughTexIdx = gltf_material.mPbrMetallicRoughness.mMetallicRoughnessTexture.mIndex; mat.metalRoughTexCoords = gltf_material.mPbrMetallicRoughness.mMetallicRoughnessTexture.mTexCoord; // Normal texture mat.normalScale = gltf_material.mNormalTexture.mScale; mat.hasNormalTex = gltf_material.mNormalTexture.mIndex >= 0; mat.normalTexIdx = gltf_material.mNormalTexture.mIndex; mat.normalTexCoords = gltf_material.mNormalTexture.mTexCoord; // Occlusion texture mat.occlusionScale = gltf_material.mOcclusionTexture.mStrength; mat.hasOcclusionTex = gltf_material.mOcclusionTexture.mIndex >= 0; mat.occlusionTexIdx = gltf_material.mOcclusionTexture.mIndex; mat.occlusionTexCoords = gltf_material.mOcclusionTexture.mTexCoord; // Emissive texture and color mat.emissiveColor = LLColor4( gltf_material.mEmissiveFactor[0], gltf_material.mEmissiveFactor[1], gltf_material.mEmissiveFactor[2], 1.0f ); mat.hasEmissiveTex = gltf_material.mEmissiveTexture.mIndex >= 0; mat.emissiveTexIdx = gltf_material.mEmissiveTexture.mIndex; mat.emissiveTexCoords = gltf_material.mEmissiveTexture.mTexCoord; // Convert AlphaMode enum to string switch (gltf_material.mAlphaMode) { case LL::GLTF::Material::AlphaMode::OPAQUE: mat.alphaMode = "OPAQUE"; break; case LL::GLTF::Material::AlphaMode::MASK: mat.alphaMode = "MASK"; break; case LL::GLTF::Material::AlphaMode::BLEND: mat.alphaMode = "BLEND"; break; default: mat.alphaMode = "OPAQUE"; break; } mat.alphaMask = gltf_material.mAlphaCutoff; // Verify that all referenced textures are valid if ((mat.hasNormalTex && (mat.normalTexIdx >= mTextures.size())) || (mat.hasOcclusionTex && (mat.occlusionTexIdx >= mTextures.size())) || (mat.hasEmissiveTex && (mat.emissiveTexIdx >= mTextures.size())) || (mat.hasBaseTex && (mat.baseColorTexIdx >= mTextures.size())) || (mat.hasMRTex && (mat.metalRoughTexIdx >= mTextures.size()))) { LL_WARNS("GLTF_IMPORT") << "Texture resource index error" << LL_ENDL; return false; } // Verify texture coordinate sets are valid (mesh can have up to 3 sets of UV) if ((mat.hasNormalTex && (mat.normalTexCoords > 2)) || (mat.hasOcclusionTex && (mat.occlusionTexCoords > 2)) || (mat.hasEmissiveTex && (mat.emissiveTexCoords > 2)) || (mat.hasBaseTex && (mat.baseColorTexCoords > 2)) || (mat.hasMRTex && (mat.metalRoughTexCoords > 2))) { LL_WARNS("GLTF_IMPORT") << "Image texcoord index error" << LL_ENDL; return false; } mMaterials.push_back(mat); } return true; } // TODO: convert raw vertex buffers to UUIDs void LLGLTFLoader::uploadMeshes() { //llassert(0); } // convert raw image buffers to texture UUIDs & assemble into a render material void LLGLTFLoader::uploadMaterials() { LL_INFOS("GLTF_IMPORT") << "Uploading materials, count: " << mMaterials.size() << LL_ENDL; for (gltf_render_material& mat : mMaterials) { LL_INFOS("GLTF_IMPORT") << "Processing material: " << mat.name << LL_ENDL; // Process base color texture if (mat.hasBaseTex && mat.baseColorTexIdx < mTextures.size()) { gltf_texture& gtex = mTextures[mat.baseColorTexIdx]; if (gtex.imageUuid.isNull()) { LL_INFOS("GLTF_IMPORT") << "Loading base color texture for material " << mat.name << LL_ENDL; gtex.imageUuid = imageBufferToTextureUUID(gtex); if (gtex.imageUuid.notNull()) { LL_INFOS("GLTF_IMPORT") << "Base color texture loaded, ID: " << gtex.imageUuid.asString() << LL_ENDL; } else { LL_WARNS("GLTF_IMPORT") << "Failed to load base color texture for material " << mat.name << LL_ENDL; } } } // Process other textures similarly if (mat.hasMRTex && mat.metalRoughTexIdx < mTextures.size()) { gltf_texture& gtex = mTextures[mat.metalRoughTexIdx]; if (gtex.imageUuid.isNull()) { gtex.imageUuid = imageBufferToTextureUUID(gtex); } } if (mat.hasNormalTex && mat.normalTexIdx < mTextures.size()) { gltf_texture& gtex = mTextures[mat.normalTexIdx]; if (gtex.imageUuid.isNull()) { gtex.imageUuid = imageBufferToTextureUUID(gtex); } } if (mat.hasOcclusionTex && mat.occlusionTexIdx < mTextures.size()) { gltf_texture& gtex = mTextures[mat.occlusionTexIdx]; if (gtex.imageUuid.isNull()) { gtex.imageUuid = imageBufferToTextureUUID(gtex); } } if (mat.hasEmissiveTex && mat.emissiveTexIdx < mTextures.size()) { gltf_texture& gtex = mTextures[mat.emissiveTexIdx]; if (gtex.imageUuid.isNull()) { gtex.imageUuid = imageBufferToTextureUUID(gtex); } } } // Update material map for all model instances to ensure textures are properly associated // mScene is a std::map, not an array, so we need to iterate through it correctly for (auto& scene_entry : mScene) { for (LLModelInstance& instance : scene_entry.second) { LLModel* model = instance.mModel; if (model) { for (size_t i = 0; i < model->getMaterialList().size(); ++i) { const std::string& matName = model->getMaterialList()[i]; if (!matName.empty()) { // Ensure this material exists in the instance's material map if (instance.mMaterial.find(matName) == instance.mMaterial.end()) { // Find material in our render materials for (const auto& renderMat : mMaterials) { if (renderMat.name == matName) { // Create an import material from the render material LLImportMaterial impMat; impMat.mDiffuseColor = renderMat.baseColor; // Set diffuse texture if available if (renderMat.hasBaseTex && renderMat.baseColorTexIdx < mTextures.size()) { const gltf_texture& gtex = mTextures[renderMat.baseColorTexIdx]; if (!gtex.imageUuid.isNull()) { impMat.setDiffuseMap(gtex.imageUuid); LL_INFOS("GLTF_IMPORT") << "Setting texture " << gtex.imageUuid.asString() << " for material " << matName << LL_ENDL; } } // Add material to instance's material map instance.mMaterial[matName] = impMat; LL_INFOS("GLTF_IMPORT") << "Added material " << matName << " to instance" << LL_ENDL; break; } } } } } } } } } LLUUID LLGLTFLoader::imageBufferToTextureUUID(const gltf_texture& tex) { if (tex.imageIdx >= mImages.size() || tex.samplerIdx >= mSamplers.size()) { LL_WARNS("GLTF_IMPORT") << "Invalid texture indices in imageBufferToTextureUUID" << LL_ENDL; return LLUUID::null; } gltf_image& image = mImages[tex.imageIdx]; gltf_sampler& sampler = mSamplers[tex.samplerIdx]; S32 sourceIndex = tex.imageIdx; if (sourceIndex < 0 || sourceIndex >= mGLTFAsset.mImages.size()) { LL_WARNS("GLTF_IMPORT") << "Invalid image index: " << sourceIndex << LL_ENDL; return LLUUID::null; } LL::GLTF::Image& source_image = mGLTFAsset.mImages[sourceIndex]; // If the image already has a texture loaded, use it if (source_image.mTexture.notNull()) { LL_INFOS("GLTF_IMPORT") << "Using already loaded texture ID: " << source_image.mTexture->getID().asString() << LL_ENDL; return source_image.mTexture->getID(); } // Create an import material to pass to the texture load function LLImportMaterial material; // Try to get the texture filename from the URI if (!source_image.mUri.empty()) { std::string filename = source_image.mUri; // Extract just the filename from the URI size_t pos = filename.find_last_of("/\\"); if (pos != std::string::npos) { filename = filename.substr(pos + 1); } material.mDiffuseMapFilename = filename; material.mDiffuseMapLabel = filename; } else if (source_image.mBufferView >= 0) { // For embedded textures, create a pseudo-filename std::string pseudo_filename = "gltf_embedded_texture_" + std::to_string(sourceIndex) + ".png"; material.mDiffuseMapFilename = pseudo_filename; material.mDiffuseMapLabel = pseudo_filename; } else { LL_WARNS("GLTF_IMPORT") << "No URI or buffer data for image" << LL_ENDL; return LLUUID::null; } // Create LLSD container with image and sampler data for texture upload LLSD texture_data = LLSD::emptyMap(); // Image data texture_data["width"] = LLSD::Integer(image.width); texture_data["height"] = LLSD::Integer(image.height); texture_data["components"] = LLSD::Integer(image.numChannels); texture_data["bytes_per_component"] = LLSD::Integer(image.bytesPerChannel); texture_data["pixel_type"] = LLSD::Integer(image.pixelType); // Sampler data texture_data["min_filter"] = LLSD::Integer(sampler.minFilter); texture_data["mag_filter"] = LLSD::Integer(sampler.magFilter); texture_data["wrap_s"] = LLSD::Integer(sampler.wrapS); texture_data["wrap_t"] = LLSD::Integer(sampler.wrapT); // Add URI for reference if (!source_image.mUri.empty()) { texture_data["uri"] = source_image.mUri; } // Check if we have a buffer view for embedded data if (source_image.mBufferView >= 0) { texture_data["has_embedded_data"] = LLSD::Boolean(true); texture_data["buffer_view"] = LLSD::Integer(source_image.mBufferView); // Extract embedded data for texture loading if (source_image.mBufferView < mGLTFAsset.mBufferViews.size()) { const LL::GLTF::BufferView& buffer_view = mGLTFAsset.mBufferViews[source_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()) { // Add embedded data reference to texture_data texture_data["buffer_index"] = LLSD::Integer(buffer_view.mBuffer); texture_data["byte_offset"] = LLSD::Integer(buffer_view.mByteOffset); texture_data["byte_length"] = LLSD::Integer(buffer_view.mByteLength); LL_INFOS("GLTF_IMPORT") << "Found embedded texture data: offset=" << buffer_view.mByteOffset << " length=" << buffer_view.mByteLength << LL_ENDL; } } } } // Store the texture metadata in the binding field std::ostringstream ostr; LLSDSerialize::toXML(texture_data, ostr); material.mBinding = ostr.str(); LL_INFOS("GLTF_IMPORT") << "Loading texture: " << material.mDiffuseMapFilename << LL_ENDL; // Flag to track if texture was loaded immediately bool texture_loaded = false; // Call texture loading function with our import material if (mTextureLoadFunc) { // Increment textures to fetch counter BEFORE calling load function mNumOfFetchingTextures++; U32 result = mTextureLoadFunc(material, mOpaqueData); // If result is 0, texture is being loaded asynchronously // If result is >0, texture was loaded immediately if (result > 0) { // Texture was loaded immediately, so decrement counter mNumOfFetchingTextures--; texture_loaded = true; if (material.getDiffuseMap().notNull()) { LL_INFOS("GLTF_IMPORT") << "Texture loaded successfully, ID: " << material.getDiffuseMap().asString() << LL_ENDL; // Store the texture in the source image for future reference if (source_image.mTexture.isNull()) { // Create and store a texture object using the UUID source_image.mTexture = LLViewerTextureManager::getFetchedTexture(material.getDiffuseMap()); } return material.getDiffuseMap(); } } else if (result == 0) { LL_INFOS("GLTF_IMPORT") << "Texture loading queued asynchronously for " << material.mDiffuseMapFilename << LL_ENDL; } else // result < 0, indicating error { // Texture loading failed, decrement counter mNumOfFetchingTextures--; LL_WARNS("GLTF_IMPORT") << "Texture loading failed for " << material.mDiffuseMapFilename << LL_ENDL; } } else { LL_WARNS("GLTF_IMPORT") << "No texture loading function available" << LL_ENDL; } return LLUUID::null; } 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); } }