diff options
Diffstat (limited to 'indra/newview/gltf/llgltfloader.cpp')
-rw-r--r-- | indra/newview/gltf/llgltfloader.cpp | 1350 |
1 files changed, 1350 insertions, 0 deletions
diff --git a/indra/newview/gltf/llgltfloader.cpp b/indra/newview/gltf/llgltfloader.cpp new file mode 100644 index 0000000000..5cdd7f09e0 --- /dev/null +++ b/indra/newview/gltf/llgltfloader.cpp @@ -0,0 +1,1350 @@ +/** + * @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 <glm/gtc/packing.hpp> + +// 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 <boost/regex.hpp> +#include <boost/algorithm/string/replace.hpp> + +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<std::string, std::string> &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) +{ +} + +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); +} + +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 (auto& skin : mGLTFAsset.mSkins) + { + populateJointFromSkin(skin); + } + + // Track how many times each mesh name has been used + std::map<std::string, S32> mesh_name_counts; + + // Process each node + for (auto& node : mGLTFAsset.mNodes) + { + LLMatrix4 transformation; + material_map mats; + auto meshidx = node.mMesh; + + if (meshidx >= 0) + { + if (mGLTFAsset.mMeshes.size() > meshidx) + { + LLModel* pModel = new LLModel(volume_params, 0.f); + auto mesh = mGLTFAsset.mMeshes[meshidx]; + + // Get base mesh name and track usage + std::string base_name = mesh.mName; + if (base_name.empty()) + { + base_name = "mesh_" + std::to_string(meshidx); + } + + S32 instance_count = mesh_name_counts[base_name]++; + + if (populateModelFromMesh(pModel, mesh, node, mats, instance_count) && + (LLModel::NO_ERRORS == pModel->getStatus()) && + validate_model(pModel)) + { + mModelList.push_back(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); + } + + mScene[transformation].push_back(LLModelInstance(pModel, pModel->mLabel, transformation, mats)); + stretch_extents(pModel, transformation); + } + else + { + setLoadState(ERROR_MODEL + pModel->getStatus()); + delete pModel; + return false; + } + } + } + } + + return true; +} + +void LLGLTFLoader::computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S32 node_index, glm::mat4& combined_transform) const +{ + if (node_index < 0 || node_index >= static_cast<S32>(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<LL::GLTF::Node&>(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<S32>(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<S32>(&mesh - &mGLTFAsset.mMeshes[0]); + base_name = "mesh_" + std::to_string(mesh_index); + } + + 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<S32>(&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<S32> 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 + } + } + } + + auto prims = mesh.mPrimitives; + for (auto prim : prims) + { + // 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<GLTFVertex> vertices; + std::vector<U16> 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); + + // 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); + + 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["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<glm::u16vec4>(); + vert.weights = glm::zero<glm::vec4>(); + } + } + 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<LLVolumeFace::VertexData> 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<LLModel::JointWeight> 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"] = "ParsingErrorBadElement"; + 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; + + size_t jointCnt = gltf_skin.mJoints.size(); + if (gltf_skin.mInverseBindMatrices >= 0 && jointCnt != gltf_skin.mInverseBindMatricesData.size()) + { + LL_INFOS("GLTF_IMPORT") << "Bind matrices count mismatch joints count" << LL_ENDL; + LLSD args; + args["Message"] = "InvBindCountMismatch"; + mWarningsArray.append(args); + } + + std::vector<S32> gltfindex_to_joitindex_map; + 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); + + if (i < gltf_skin.mInverseBindMatricesData.size()) + { + // Use pre-computed coord_system_rotation instead of recreating it + LL::GLTF::mat4 gltf_mat = gltf_skin.mInverseBindMatricesData[i]; + + 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)); + skin_info.mInvBindMatrix.push_back(LLMatrix4a(gltf_transform)); + + LL_INFOS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " val: " << gltf_transform << LL_ENDL; + + // For alternate bind matrix, use the ORIGINAL joint transform (before rotation) + // Get the original joint node and use its matrix directly + S32 joint = gltf_skin.mJoints[i]; + LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; + glm::mat4 joint_mat = jointNode.mMatrix; + S32 root_joint = findValidRootJoint(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)); + } + else + { + // For gltf mInverseBindMatrices are optional, but not for viewer + // todo: get a model that triggers this + skin_info.mInvBindMatrix.push_back(LLMatrix4a(mJointList[legal_name])); // might need to be an 'identity' + skin_info.mAlternateBindMatrix.push_back(LLMatrix4a(mJointList[legal_name])); + } + } + + // 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::populateJointFromSkin(const LL::GLTF::Skin& skin) +{ + LL_INFOS("GLTF_DEBUG") << "populateJointFromSkin: Processing " << skin.mJoints.size() << " joints" << LL_ENDL; + + for (auto joint : skin.mJoints) + { + auto jointNode = mGLTFAsset.mNodes[joint]; + + std::string legal_name(jointNode.mName); + if (mJointMap.find(legal_name) != mJointMap.end()) + { + legal_name = mJointMap[legal_name]; + } + else + { + // ignore unrecognized joint + LL_DEBUGS("GLTF") << "Ignoring 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::findValidRootJoint(S32 source_joint, const LL::GLTF::Skin& gltf_skin) const +{ + S32 root_joint = 0; + S32 found_joint = source_joint; + S32 size = (S32)gltf_skin.mJoints.size(); + do + { + root_joint = found_joint; + 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<S32>::const_iterator it = std::find(jointNode.mChildren.begin(), jointNode.mChildren.end(), root_joint); + if (it != jointNode.mChildren.end()) + { + found_joint = joint; + break; + } + } + } + } while (root_joint != found_joint); + + return root_joint; +} + +S32 LLGLTFLoader::findGLTFRootJoint(const LL::GLTF::Skin& gltf_skin) const +{ + S32 root_joint = 0; + S32 found_joint = 0; + S32 size = (S32)gltf_skin.mJoints.size(); + do + { + root_joint = found_joint; + for (S32 i = 0; i < size; i++) + { + S32 joint = gltf_skin.mJoints[i]; + const LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[joint]; + std::vector<S32>::const_iterator it = std::find(jointNode.mChildren.begin(), jointNode.mChildren.end(), root_joint); + if (it != jointNode.mChildren.end()) + { + found_joint = joint; + break; + } + } + } while (root_joint != found_joint); + + LL_INFOS("GLTF_DEBUG") << "mJointList name: "; + const LL::GLTF::Node& jointNode = mGLTFAsset.mNodes[root_joint]; + LL_CONT << jointNode.mName << " index: " << root_joint << LL_ENDL; + return root_joint; +} + +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<U32>(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<U32>(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<LLMatrix4, model_instance_list>, 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<std::string> 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); + } +} + |