diff options
author | Erik Kundiman <erik@megapahit.org> | 2025-05-30 17:55:17 +0800 |
---|---|---|
committer | Erik Kundiman <erik@megapahit.org> | 2025-05-30 17:55:17 +0800 |
commit | d1353441f91d5776e1f4e72666f7c9de96eecca5 (patch) | |
tree | cd22d6ac18a1b64cb5c2a94f793e7d7cf27a8b4b | |
parent | 2c8521de6d357d74f5b256cc2f94e9951e654d83 (diff) | |
parent | 136149d1a196d2c0c15b9977937e64ccd26c1a49 (diff) |
Merge remote-tracking branch 'secondlife/project/gltf-mesh-import' into gltf-mesh-import
21 files changed, 1726 insertions, 514 deletions
diff --git a/indra/llprimitive/CMakeLists.txt b/indra/llprimitive/CMakeLists.txt index 00d821c470..21d412baeb 100644 --- a/indra/llprimitive/CMakeLists.txt +++ b/indra/llprimitive/CMakeLists.txt @@ -34,7 +34,6 @@ endif(LINUX OR CMAKE_SYSTEM_NAME MATCHES FreeBSD ) set(llprimitive_SOURCE_FILES lldaeloader.cpp - llgltfloader.cpp llgltfmaterial.cpp llmaterialid.cpp llmaterial.cpp @@ -54,7 +53,6 @@ set(llprimitive_SOURCE_FILES set(llprimitive_HEADER_FILES CMakeLists.txt lldaeloader.h - llgltfloader.h llgltfmaterial.h llgltfmaterial_templates.h legacy_object_types.h diff --git a/indra/llprimitive/llgltfloader.cpp b/indra/llprimitive/llgltfloader.cpp deleted file mode 100644 index 480012699a..0000000000 --- a/indra/llprimitive/llgltfloader.cpp +++ /dev/null @@ -1,404 +0,0 @@ -/** - * @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" - -// 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", -}; - - -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 error_msg; - std::string warn_msg; - std::string filename_lc(filename); - LLStringUtil::toLower(filename_lc); - - // Load a tinygltf model fom a file. Assumes that the input filename has already been - // been sanitized to one of (.gltf , .glb) extensions, so does a simple find to distinguish. - if (std::string::npos == filename_lc.rfind(".gltf")) - { // file is binary - mGltfLoaded = loader.LoadBinaryFromFile(&mGltfModel, &error_msg, &warn_msg, filename); - } - else - { // file is ascii - mGltfLoaded = loader.LoadASCIIFromFile(&mGltfModel, &error_msg, &warn_msg, filename); - } - - if (!mGltfLoaded) - { - if (!warn_msg.empty()) - LL_WARNS("GLTF_IMPORT") << "gltf load warning: " << warn_msg.c_str() << LL_ENDL; - if (!error_msg.empty()) - LL_WARNS("GLTF_IMPORT") << "gltf load error: " << error_msg.c_str() << LL_ENDL; - return false; - } - - mMeshesLoaded = parseMeshes(); - if (mMeshesLoaded) uploadMeshes(); - - mMaterialsLoaded = parseMaterials(); - if (mMaterialsLoaded) uploadMaterials(); - - return (mMeshesLoaded || mMaterialsLoaded); -} - -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); - - for (tinygltf::Mesh mesh : mGltfModel.meshes) - { - LLModel *pModel = new LLModel(volume_params, 0.f); - - if (populateModelFromMesh(pModel, mesh) && - (LLModel::NO_ERRORS == pModel->getStatus()) && - validate_model(pModel)) - { - mModelList.push_back(pModel); - } - else - { - setLoadState(ERROR_MODEL + pModel->getStatus()); - delete(pModel); - return false; - } - } - return true; -} - -bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const tinygltf::Mesh &mesh) -{ - pModel->mLabel = mesh.name; - int pos_idx; - tinygltf::Accessor indices_a, positions_a, normals_a, uv0_a, color0_a; - - auto prims = mesh.primitives; - for (auto prim : prims) - { - if (prim.indices >= 0) indices_a = mGltfModel.accessors[prim.indices]; - - pos_idx = (prim.attributes.count("POSITION") > 0) ? prim.attributes.at("POSITION") : -1; - if (pos_idx >= 0) - { - positions_a = mGltfModel.accessors[pos_idx]; - if (TINYGLTF_COMPONENT_TYPE_FLOAT != positions_a.componentType) - continue; - auto positions_bv = mGltfModel.bufferViews[positions_a.bufferView]; - auto positions_buf = mGltfModel.buffers[positions_bv.buffer]; - //auto type = positions_vb. - //if (positions_buf.name - } - -#if 0 - int norm_idx, tan_idx, uv0_idx, uv1_idx, color0_idx, color1_idx; - norm_idx = (prim.attributes.count("NORMAL") > 0) ? prim.attributes.at("NORMAL") : -1; - tan_idx = (prim.attributes.count("TANGENT") > 0) ? prim.attributes.at("TANGENT") : -1; - uv0_idx = (prim.attributes.count("TEXCOORDS_0") > 0) ? prim.attributes.at("TEXCOORDS_0") : -1; - uv1_idx = (prim.attributes.count("TEXCOORDS_1") > 0) ? prim.attributes.at("TEXCOORDS_1") : -1; - color0_idx = (prim.attributes.count("COLOR_0") > 0) ? prim.attributes.at("COLOR_0") : -1; - color1_idx = (prim.attributes.count("COLOR_1") > 0) ? prim.attributes.at("COLOR_1") : -1; -#endif - - if (prim.mode == TINYGLTF_MODE_TRIANGLES) - { - //auto pos = mesh. TODO resume here DJH 2022-04 - } - } - - //pModel->addFace() - return false; -} - -bool LLGLTFLoader::parseMaterials() -{ - if (!mGltfLoaded) return false; - - // fill local texture data structures - mSamplers.clear(); - for (auto in_sampler : mGltfModel.samplers) - { - gltf_sampler sampler; - sampler.magFilter = in_sampler.magFilter > 0 ? in_sampler.magFilter : GL_LINEAR; - sampler.minFilter = in_sampler.minFilter > 0 ? in_sampler.minFilter : GL_LINEAR;; - sampler.wrapS = in_sampler.wrapS; - sampler.wrapT = in_sampler.wrapT; - sampler.name = in_sampler.name; // unused - mSamplers.push_back(sampler); - } - - mImages.clear(); - for (auto in_image : mGltfModel.images) - { - gltf_image image; - image.numChannels = in_image.component; - image.bytesPerChannel = in_image.bits >> 3; // Convert bits to bytes - image.pixelType = in_image.pixel_type; // Maps exactly, i.e. TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE == GL_UNSIGNED_BYTE, etc - image.size = static_cast<U32>(in_image.image.size()); - image.height = in_image.height; - image.width = in_image.width; - image.data = in_image.image.data(); - - if (in_image.as_is) - { - LL_WARNS("GLTF_IMPORT") << "Unsupported image encoding" << LL_ENDL; - return false; - } - - if (image.size != image.height * image.width * image.numChannels * image.bytesPerChannel) - { - LL_WARNS("GLTF_IMPORT") << "Image size error" << LL_ENDL; - return false; - } - - mImages.push_back(image); - } - - mTextures.clear(); - for (auto in_tex : mGltfModel.textures) - { - gltf_texture tex; - tex.imageIdx = in_tex.source; - tex.samplerIdx = in_tex.sampler; - 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 - for (tinygltf::Material gltf_material : mGltfModel.materials) - { - gltf_render_material mat; - mat.name = gltf_material.name; - - tinygltf::PbrMetallicRoughness& pbr = gltf_material.pbrMetallicRoughness; - mat.hasPBR = true; // Always true, for now - - mat.baseColor.set(pbr.baseColorFactor.data()); - mat.hasBaseTex = pbr.baseColorTexture.index >= 0; - mat.baseColorTexIdx = pbr.baseColorTexture.index; - mat.baseColorTexCoords = pbr.baseColorTexture.texCoord; - - mat.metalness = pbr.metallicFactor; - mat.roughness = pbr.roughnessFactor; - mat.hasMRTex = pbr.metallicRoughnessTexture.index >= 0; - mat.metalRoughTexIdx = pbr.metallicRoughnessTexture.index; - mat.metalRoughTexCoords = pbr.metallicRoughnessTexture.texCoord; - - mat.normalScale = gltf_material.normalTexture.scale; - mat.hasNormalTex = gltf_material.normalTexture.index >= 0; - mat.normalTexIdx = gltf_material.normalTexture.index; - mat.normalTexCoords = gltf_material.normalTexture.texCoord; - - mat.occlusionScale = gltf_material.occlusionTexture.strength; - mat.hasOcclusionTex = gltf_material.occlusionTexture.index >= 0; - mat.occlusionTexIdx = gltf_material.occlusionTexture.index; - mat.occlusionTexCoords = gltf_material.occlusionTexture.texCoord; - - mat.emissiveColor.set(gltf_material.emissiveFactor.data()); - mat.hasEmissiveTex = gltf_material.emissiveTexture.index >= 0; - mat.emissiveTexIdx = gltf_material.emissiveTexture.index; - mat.emissiveTexCoords = gltf_material.emissiveTexture.texCoord; - - mat.alphaMode = gltf_material.alphaMode; - mat.alphaMask = gltf_material.alphaCutoff; - - 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; - } - - if ((mat.hasNormalTex && (mat.normalTexCoords > 2)) || // mesh can have up to 3 sets of UV - (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() -{ - for (gltf_render_material mat : mMaterials) // Initially 1 material per gltf file, but design for multiple - { - if (mat.hasBaseTex) - { - gltf_texture& gtex = mTextures[mat.baseColorTexIdx]; - if (gtex.imageUuid.isNull()) - { - gtex.imageUuid = imageBufferToTextureUUID(gtex); - } - } - - if (mat.hasMRTex) - { - gltf_texture& gtex = mTextures[mat.metalRoughTexIdx]; - if (gtex.imageUuid.isNull()) - { - gtex.imageUuid = imageBufferToTextureUUID(gtex); - } - } - - if (mat.hasNormalTex) - { - gltf_texture& gtex = mTextures[mat.normalTexIdx]; - if (gtex.imageUuid.isNull()) - { - gtex.imageUuid = imageBufferToTextureUUID(gtex); - } - } - - if (mat.hasOcclusionTex) - { - gltf_texture& gtex = mTextures[mat.occlusionTexIdx]; - if (gtex.imageUuid.isNull()) - { - gtex.imageUuid = imageBufferToTextureUUID(gtex); - } - } - - if (mat.hasEmissiveTex) - { - gltf_texture& gtex = mTextures[mat.emissiveTexIdx]; - if (gtex.imageUuid.isNull()) - { - gtex.imageUuid = imageBufferToTextureUUID(gtex); - } - } - } -} - -LLUUID LLGLTFLoader::imageBufferToTextureUUID(const gltf_texture& tex) -{ - //gltf_image& image = mImages[tex.imageIdx]; - //gltf_sampler& sampler = mSamplers[tex.samplerIdx]; - - // fill an LLSD container with image+sampler data - - // upload texture - - // retrieve UUID - - return LLUUID::null; -} diff --git a/indra/llprimitive/llmodel.cpp b/indra/llprimitive/llmodel.cpp index 6b4bb3a8b0..0bc7846023 100644 --- a/indra/llprimitive/llmodel.cpp +++ b/indra/llprimitive/llmodel.cpp @@ -334,6 +334,162 @@ void LLModel::normalizeVolumeFaces() } } +void LLModel::normalizeVolumeFacesAndWeights() +{ + if (!mVolumeFaces.empty()) + { + LLVector4a min, max; + + // For all of the volume faces + // in the model, loop over + // them and see what the extents + // of the volume along each axis. + min = mVolumeFaces[0].mExtents[0]; + max = mVolumeFaces[0].mExtents[1]; + + for (U32 i = 1; i < mVolumeFaces.size(); ++i) + { + LLVolumeFace& face = mVolumeFaces[i]; + + update_min_max(min, max, face.mExtents[0]); + update_min_max(min, max, face.mExtents[1]); + + if (face.mTexCoords) + { + LLVector2& min_tc = face.mTexCoordExtents[0]; + LLVector2& max_tc = face.mTexCoordExtents[1]; + + min_tc = face.mTexCoords[0]; + max_tc = face.mTexCoords[0]; + + for (S32 j = 1; j < face.mNumVertices; ++j) + { + update_min_max(min_tc, max_tc, face.mTexCoords[j]); + } + } + else + { + face.mTexCoordExtents[0].set(0, 0); + face.mTexCoordExtents[1].set(1, 1); + } + } + + // Now that we have the extents of the model + // we can compute the offset needed to center + // the model at the origin. + + // Compute center of the model + // and make it negative to get translation + // needed to center at origin. + LLVector4a trans; + trans.setAdd(min, max); + trans.mul(-0.5f); + + // Compute the total size along all + // axes of the model. + LLVector4a size; + size.setSub(max, min); + + // Prevent division by zero. + F32 x = size[0]; + F32 y = size[1]; + F32 z = size[2]; + F32 w = size[3]; + if (fabs(x) < F_APPROXIMATELY_ZERO) + { + x = 1.0; + } + if (fabs(y) < F_APPROXIMATELY_ZERO) + { + y = 1.0; + } + if (fabs(z) < F_APPROXIMATELY_ZERO) + { + z = 1.0; + } + size.set(x, y, z, w); + + // Compute scale as reciprocal of size + LLVector4a scale; + scale.splat(1.f); + scale.div(size); + + LLVector4a inv_scale(1.f); + inv_scale.div(scale); + + for (U32 i = 0; i < mVolumeFaces.size(); ++i) + { + LLVolumeFace& face = mVolumeFaces[i]; + + // We shrink the extents so + // that they fall within + // the unit cube. + // VFExtents change + face.mExtents[0].add(trans); + face.mExtents[0].mul(scale); + + face.mExtents[1].add(trans); + face.mExtents[1].mul(scale); + + // For all the positions, we scale + // the positions to fit within the unit cube. + LLVector4a* pos = (LLVector4a*)face.mPositions; + LLVector4a* norm = (LLVector4a*)face.mNormals; + LLVector4a* t = (LLVector4a*)face.mTangents; + + for (S32 j = 0; j < face.mNumVertices; ++j) + { + pos[j].add(trans); + pos[j].mul(scale); + if (norm && !norm[j].equals3(LLVector4a::getZero())) + { + norm[j].mul(inv_scale); + norm[j].normalize3(); + } + + if (t) + { + F32 w = t[j].getF32ptr()[3]; + t[j].mul(inv_scale); + t[j].normalize3(); + t[j].getF32ptr()[3] = w; + } + } + } + + weight_map old_weights = mSkinWeights; + mSkinWeights.clear(); + mPosition.clear(); + + for (auto& weights : old_weights) + { + LLVector4a pos(weights.first.mV[VX], weights.first.mV[VY], weights.first.mV[VZ]); + pos.add(trans); + pos.mul(scale); + LLVector3 scaled_pos(pos.getF32ptr()); + mPosition.push_back(scaled_pos); + mSkinWeights[scaled_pos] = weights.second; + } + + // mNormalizedScale is the scale at which + // we would need to multiply the model + // by to get the original size of the + // model instead of the normalized size. + LLVector4a normalized_scale; + normalized_scale.splat(1.f); + normalized_scale.div(scale); + mNormalizedScale.set(normalized_scale.getF32ptr()); + mNormalizedTranslation.set(trans.getF32ptr()); + mNormalizedTranslation *= -1.f; + + // remember normalized scale so original dimensions can be recovered for mesh processing (i.e. tangent generation) + for (auto& face : mVolumeFaces) + { + face.mNormalizedScale = mNormalizedScale; + } + } +} + void LLModel::getNormalizedScaleTranslation(LLVector3& scale_out, LLVector3& translation_out) const { scale_out = mNormalizedScale; diff --git a/indra/llprimitive/llmodel.h b/indra/llprimitive/llmodel.h index 4b5d079b48..521bd54fd1 100644 --- a/indra/llprimitive/llmodel.h +++ b/indra/llprimitive/llmodel.h @@ -204,6 +204,7 @@ public: void sortVolumeFacesByMaterialName(); void normalizeVolumeFaces(); + void normalizeVolumeFacesAndWeights(); void trimVolumeFacesToSize(U32 new_count = LL_SCULPT_MESH_MAX_FACES, LLVolume::face_list_t* remainder = NULL); void remapVolumeFaces(); void optimizeVolumeFaces(); diff --git a/indra/llprimitive/llmodelloader.cpp b/indra/llprimitive/llmodelloader.cpp index 84adec4da5..dee39175f8 100644 --- a/indra/llprimitive/llmodelloader.cpp +++ b/indra/llprimitive/llmodelloader.cpp @@ -150,6 +150,8 @@ void LLModelLoader::run() { mWarningsArray.clear(); doLoadModel(); + // todo: we are inside of a thread, push this into main thread worker, + // not into doOnIdleOneTime that laks tread safety doOnIdleOneTime(boost::bind(&LLModelLoader::loadModelCallback,this)); } diff --git a/indra/llprimitive/llmodelloader.h b/indra/llprimitive/llmodelloader.h index 530e61e2b8..73ec0ed1f4 100644 --- a/indra/llprimitive/llmodelloader.h +++ b/indra/llprimitive/llmodelloader.h @@ -111,6 +111,7 @@ public: bool mCacheOnlyHitIfRigged; // ignore cached SLM if it does not contain rig info (and we want rig info) model_list mModelList; + // The scene is pretty much what ends up getting loaded for upload. Basically assign things to this guy if you want something uploaded. scene mScene; typedef std::queue<LLPointer<LLModel> > model_queue; @@ -119,9 +120,14 @@ public: model_queue mPhysicsQ; //map of avatar joints as named in COLLADA assets to internal joint names + // Do not use this for anything other than looking up the name of a joint. This is populated elsewhere. JointMap mJointMap; + + // The joint list is what you want to use to actually setup the specific joint transformations. JointTransformMap& mJointList; JointNameSet& mJointsFromNode; + + U32 mMaxJointsPerMesh; LLModelLoader( diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 80f0b60f98..764f1f6b21 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -81,6 +81,7 @@ set(viewer_SOURCE_FILES gltf/accessor.cpp gltf/primitive.cpp gltf/animation.cpp + gltf/llgltfloader.cpp groupchatlistener.cpp llaccountingcostmanager.cpp llaisapi.cpp @@ -762,6 +763,7 @@ set(viewer_HEADER_FILES gltf/buffer_util.h gltf/primitive.h gltf/animation.h + gltf/llgltfloader.h llaccountingcost.h llaccountingcostmanager.h llaisapi.h diff --git a/indra/newview/gltf/asset.cpp b/indra/newview/gltf/asset.cpp index a399a59f40..f40cc58440 100644 --- a/indra/newview/gltf/asset.cpp +++ b/indra/newview/gltf/asset.cpp @@ -50,6 +50,10 @@ namespace LL "KHR_texture_transform" }; + static std::unordered_set<std::string> ExtensionsIgnored = { + "KHR_materials_pbrSpecularGlossiness" + }; + Material::AlphaMode gltf_alpha_mode_to_enum(const std::string& alpha_mode) { if (alpha_mode == "OPAQUE") @@ -472,11 +476,14 @@ void Asset::update() for (auto& image : mImages) { - if (image.mTexture.notNull()) - { // HACK - force texture to be loaded full rez - // TODO: calculate actual vsize - image.mTexture->addTextureStats(2048.f * 2048.f); - image.mTexture->setBoostLevel(LLViewerTexture::BOOST_HIGH); + if (image.mLoadIntoTexturePipe) + { + if (image.mTexture.notNull()) + { // HACK - force texture to be loaded full rez + // TODO: calculate actual vsize + image.mTexture->addTextureStats(2048.f * 2048.f); + image.mTexture->setBoostLevel(LLViewerTexture::BOOST_HIGH); + } } } } @@ -486,18 +493,23 @@ void Asset::update() bool Asset::prep() { LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF; - // check required extensions and fail if not supported - bool unsupported = false; + // check required extensions for (auto& extension : mExtensionsRequired) { if (ExtensionsSupported.find(extension) == ExtensionsSupported.end()) { - LL_WARNS() << "Unsupported extension: " << extension << LL_ENDL; - unsupported = true; + if (ExtensionsIgnored.find(extension) == ExtensionsIgnored.end()) + { + LL_WARNS() << "Unsupported extension: " << extension << LL_ENDL; + mUnsupportedExtensions.push_back(extension); + } + else + { + mIgnoredExtensions.push_back(extension); + } } } - - if (unsupported) + if (mUnsupportedExtensions.size() > 0) { return false; } @@ -513,7 +525,7 @@ bool Asset::prep() for (auto& image : mImages) { - if (!image.prep(*this)) + if (!image.prep(*this, mLoadIntoVRAM)) { return false; } @@ -542,102 +554,106 @@ bool Asset::prep() return false; } } + if (mLoadIntoVRAM) + { + // prepare vertex buffers - // prepare vertex buffers - - // material count is number of materials + 1 for default material - U32 mat_count = (U32) mMaterials.size() + 1; - - if (LLGLSLShader::sCurBoundShaderPtr == nullptr) - { // make sure a shader is bound to satisfy mVertexBuffer->setBuffer - gDebugProgram.bind(); - } + // material count is number of materials + 1 for default material + U32 mat_count = (U32) mMaterials.size() + 1; - for (S32 double_sided = 0; double_sided < 2; ++double_sided) - { - RenderData& rd = mRenderData[double_sided]; - for (U32 i = 0; i < LLGLSLShader::NUM_GLTF_VARIANTS; ++i) - { - rd.mBatches[i].resize(mat_count); + if (LLGLSLShader::sCurBoundShaderPtr == nullptr) + { // make sure a shader is bound to satisfy mVertexBuffer->setBuffer + gDebugProgram.bind(); } - // for each material - for (S32 mat_id = -1; mat_id < (S32)mMaterials.size(); ++mat_id) + for (S32 double_sided = 0; double_sided < 2; ++double_sided) { - // for each shader variant - U32 vertex_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 }; - U32 index_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 }; - - S32 ds_mat = mat_id == -1 ? 0 : mMaterials[mat_id].mDoubleSided; - if (ds_mat != double_sided) + RenderData& rd = mRenderData[double_sided]; + for (U32 i = 0; i < LLGLSLShader::NUM_GLTF_VARIANTS; ++i) { - continue; + rd.mBatches[i].resize(mat_count); } - for (U32 variant = 0; variant < LLGLSLShader::NUM_GLTF_VARIANTS; ++variant) + // for each material + for (S32 mat_id = -1; mat_id < (S32)mMaterials.size(); ++mat_id) { - U32 attribute_mask = 0; - // for each mesh - for (auto& mesh : mMeshes) + // for each shader variant + U32 vertex_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 }; + U32 index_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 }; + + S32 ds_mat = mat_id == -1 ? 0 : mMaterials[mat_id].mDoubleSided; + if (ds_mat != double_sided) { - // for each primitive - for (auto& primitive : mesh.mPrimitives) + continue; + } + + for (U32 variant = 0; variant < LLGLSLShader::NUM_GLTF_VARIANTS; ++variant) + { + U32 attribute_mask = 0; + // for each mesh + for (auto& mesh : mMeshes) { - if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant) + // for each primitive + for (auto& primitive : mesh.mPrimitives) { - // accumulate vertex and index counts - primitive.mVertexOffset = vertex_count[variant]; - primitive.mIndexOffset = index_count[variant]; + if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant) + { + // accumulate vertex and index counts + primitive.mVertexOffset = vertex_count[variant]; + primitive.mIndexOffset = index_count[variant]; - vertex_count[variant] += primitive.getVertexCount(); - index_count[variant] += primitive.getIndexCount(); + vertex_count[variant] += primitive.getVertexCount(); + index_count[variant] += primitive.getIndexCount(); - // all primitives of a given variant and material should all have the same attribute mask - llassert(attribute_mask == 0 || primitive.mAttributeMask == attribute_mask); - attribute_mask |= primitive.mAttributeMask; + // all primitives of a given variant and material should all have the same attribute mask + llassert(attribute_mask == 0 || primitive.mAttributeMask == attribute_mask); + attribute_mask |= primitive.mAttributeMask; + } } } - } - // allocate vertex buffer and pack it - if (vertex_count[variant] > 0) - { - U32 mat_idx = mat_id + 1; - LLVertexBuffer* vb = new LLVertexBuffer(attribute_mask); + // allocate vertex buffer and pack it + if (vertex_count[variant] > 0) + { + U32 mat_idx = mat_id + 1; + #if 0 + LLVertexBuffer* vb = new LLVertexBuffer(attribute_mask); - rd.mBatches[variant][mat_idx].mVertexBuffer = vb; - vb->allocateBuffer(vertex_count[variant], - index_count[variant] * 2); // hack double index count... TODO: find a better way to indicate 32-bit indices will be used - vb->setBuffer(); + rd.mBatches[variant][mat_idx].mVertexBuffer = vb; + vb->allocateBuffer(vertex_count[variant], + index_count[variant] * 2); // hack double index count... TODO: find a better way to indicate 32-bit indices will be used + vb->setBuffer(); - for (auto& mesh : mMeshes) - { - for (auto& primitive : mesh.mPrimitives) + for (auto& mesh : mMeshes) { - if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant) + for (auto& primitive : mesh.mPrimitives) { - primitive.upload(vb); + if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant) + { + primitive.upload(vb); + } } } - } - vb->unmapBuffer(); + vb->unmapBuffer(); - vb->unbind(); + vb->unbind(); + #endif + } } } } - } - // sanity check that all primitives have a vertex buffer - for (auto& mesh : mMeshes) - { - for (auto& primitive : mesh.mPrimitives) + // sanity check that all primitives have a vertex buffer + for (auto& mesh : mMeshes) { - llassert(primitive.mVertexBuffer.notNull()); + for (auto& primitive : mesh.mPrimitives) + { + //llassert(primitive.mVertexBuffer.notNull()); + } } } - + #if 0 // build render batches for (S32 node_id = 0; node_id < mNodes.size(); ++node_id) { @@ -664,6 +680,7 @@ bool Asset::prep() } } } + #endif return true; } @@ -672,9 +689,10 @@ Asset::Asset(const Value& src) *this = src; } -bool Asset::load(std::string_view filename) +bool Asset::load(std::string_view filename, bool loadIntoVRAM) { LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF; + mLoadIntoVRAM = loadIntoVRAM; mFilename = filename; std::string ext = gDirUtilp->getExtension(mFilename); @@ -692,7 +710,7 @@ bool Asset::load(std::string_view filename) } else if (ext == "glb") { - return loadBinary(str); + return loadBinary(str, mLoadIntoVRAM); } else { @@ -709,8 +727,9 @@ bool Asset::load(std::string_view filename) return false; } -bool Asset::loadBinary(const std::string& data) +bool Asset::loadBinary(const std::string& data, bool loadIntoVRAM) { + mLoadIntoVRAM = loadIntoVRAM; // load from binary gltf const U8* ptr = (const U8*)data.data(); const U8* end = ptr + data.size(); @@ -935,8 +954,9 @@ void Asset::eraseBufferView(S32 bufferView) LLViewerFetchedTexture* fetch_texture(const LLUUID& id); -bool Image::prep(Asset& asset) +bool Image::prep(Asset& asset, bool loadIntoVRAM) { + mLoadIntoTexturePipe = loadIntoVRAM; LLUUID id; if (mUri.size() == UUID_STR_SIZE && LLUUID::parseUUID(mUri, &id) && id.notNull()) { // loaded from an asset, fetch the texture from the asset system @@ -951,12 +971,12 @@ bool Image::prep(Asset& asset) { // embedded in a buffer, load the texture from the buffer BufferView& bufferView = asset.mBufferViews[mBufferView]; Buffer& buffer = asset.mBuffers[bufferView.mBuffer]; - - U8* data = buffer.mData.data() + bufferView.mByteOffset; - - mTexture = LLViewerTextureManager::getFetchedTextureFromMemory(data, bufferView.mByteLength, mMimeType); - - if (mTexture.isNull()) + if (mLoadIntoTexturePipe) + { + U8* data = buffer.mData.data() + bufferView.mByteOffset; + mTexture = LLViewerTextureManager::getFetchedTextureFromMemory(data, bufferView.mByteLength, mMimeType); + } + else if (mTexture.isNull() && mLoadIntoTexturePipe) { LL_WARNS("GLTF") << "Failed to load image from buffer:" << LL_ENDL; LL_WARNS("GLTF") << " image: " << mName << LL_ENDL; @@ -971,12 +991,12 @@ bool Image::prep(Asset& asset) std::string img_file = dir + gDirUtilp->getDirDelimiter() + mUri; LLUUID tracking_id = LLLocalBitmapMgr::getInstance()->addUnit(img_file); - if (tracking_id.notNull()) + if (tracking_id.notNull() && mLoadIntoTexturePipe) { LLUUID world_id = LLLocalBitmapMgr::getInstance()->getWorldID(tracking_id); mTexture = LLViewerTextureManager::getFetchedTexture(world_id); } - else + else if (mLoadIntoTexturePipe) { LL_WARNS("GLTF") << "Failed to load image from file:" << LL_ENDL; LL_WARNS("GLTF") << " image: " << mName << LL_ENDL; @@ -991,7 +1011,7 @@ bool Image::prep(Asset& asset) return false; } - if (!asset.mFilename.empty()) + if (!asset.mFilename.empty() && mLoadIntoTexturePipe) { // local preview, boost image so it doesn't discard and force to save raw image in case we save out or upload mTexture->setBoostLevel(LLViewerTexture::BOOST_PREVIEW); mTexture->forceToSaveRawImage(0, F32_MAX); diff --git a/indra/newview/gltf/asset.h b/indra/newview/gltf/asset.h index 27821659db..b9554d753c 100644 --- a/indra/newview/gltf/asset.h +++ b/indra/newview/gltf/asset.h @@ -286,6 +286,7 @@ namespace LL void serialize(boost::json::object& dst) const; }; + // Image is for images that we want to load for the given asset. This acts as an interface into the viewer's texture pipe. class Image { public: @@ -301,6 +302,8 @@ namespace LL S32 mBits = -1; S32 mPixelType = -1; + bool mLoadIntoTexturePipe = false; + LLPointer<LLViewerFetchedTexture> mTexture; const Image& operator=(const Value& src); @@ -316,7 +319,7 @@ namespace LL // preserve only uri and name void clearData(Asset& asset); - bool prep(Asset& asset); + bool prep(Asset& asset, bool loadIntoVRAM); }; // Render Batch -- vertex buffer and list of primitives to render using @@ -391,6 +394,10 @@ namespace LL // UBO for storing material data U32 mMaterialsUBO = 0; + bool mLoadIntoVRAM = false; + + std::vector<std::string> mUnsupportedExtensions; + std::vector<std::string> mIgnoredExtensions; // prepare for first time use bool prep(); @@ -428,12 +435,12 @@ namespace LL // accepts .gltf and .glb files // Any existing data will be lost // returns result of prep() on success - bool load(std::string_view filename); + bool load(std::string_view filename, bool loadIntoVRAM); // load .glb contents from memory // data - binary contents of .glb file // returns result of prep() on success - bool loadBinary(const std::string& data); + bool loadBinary(const std::string& data, bool loadIntoVRAM); const Asset& operator=(const Value& src); void serialize(boost::json::object& dst) const; diff --git a/indra/newview/gltf/buffer_util.h b/indra/newview/gltf/buffer_util.h index ef9bba8128..2632a0f263 100644 --- a/indra/newview/gltf/buffer_util.h +++ b/indra/newview/gltf/buffer_util.h @@ -159,6 +159,12 @@ namespace LL } template<> + inline void copyVec3<F32, LLColor4U>(F32* src, LLColor4U& dst) + { + dst.set((U8)(src[0] * 255.f), (U8)(src[1] * 255.f), (U8)(src[2] * 255.f), 255); + } + + template<> inline void copyVec3<U16, LLColor4U>(U16* src, LLColor4U& dst) { dst.set((U8)(src[0]), (U8)(src[1]), (U8)(src[2]), 255); 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); + } +} + diff --git a/indra/llprimitive/llgltfloader.h b/indra/newview/gltf/llgltfloader.h index 66671d1c5a..6e0fe2b32c 100644 --- a/indra/llprimitive/llgltfloader.h +++ b/indra/newview/gltf/llgltfloader.h @@ -29,6 +29,8 @@ #include "tinygltf/tiny_gltf.h" +#include "asset.h" + #include "llglheaders.h" #include "llmodelloader.h" @@ -137,7 +139,17 @@ class LLGLTFLoader : public LLModelLoader virtual bool OpenFile(const std::string &filename); + struct GLTFVertex + { + glm::vec3 position; + glm::vec3 normal; + glm::vec2 uv0; + glm::u16vec4 joints; + glm::vec4 weights; + }; + protected: + LL::GLTF::Asset mGLTFAsset; tinygltf::Model mGltfModel; bool mGltfLoaded; bool mMeshesLoaded; @@ -155,9 +167,15 @@ private: void uploadMeshes(); bool parseMaterials(); void uploadMaterials(); - bool populateModelFromMesh(LLModel* pModel, const tinygltf::Mesh &mesh); + void computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S32 node_index, glm::mat4& combined_transform) const; + bool populateModelFromMesh(LLModel* pModel, const LL::GLTF::Mesh &mesh, const LL::GLTF::Node &node, material_map& mats, S32 instance_count); + void populateJointFromSkin(const LL::GLTF::Skin& skin); + S32 findValidRootJoint(S32 source_joint, const LL::GLTF::Skin& gltf_skin) const; + S32 findGLTFRootJoint(const LL::GLTF::Skin& gltf_skin) const; // if there are multiple roots, gltf stores them under one commor joint LLUUID imageBufferToTextureUUID(const gltf_texture& tex); + void notifyUnsupportedExtension(bool unsupported); + // bool mPreprocessGLTF; /* Below inherited from dae loader - unknown if/how useful here diff --git a/indra/newview/gltfscenemanager.cpp b/indra/newview/gltfscenemanager.cpp index 9a381f9ba6..5dec4dd62f 100644 --- a/indra/newview/gltfscenemanager.cpp +++ b/indra/newview/gltfscenemanager.cpp @@ -317,7 +317,7 @@ void GLTFSceneManager::load(const std::string& filename) { std::shared_ptr<Asset> asset = std::make_shared<Asset>(); - if (asset->load(filename)) + if (asset->load(filename, true)) { gDebugProgram.bind(); // bind a shader to satisfy LLVertexBuffer assertions asset->updateTransforms(); diff --git a/indra/newview/llfilepicker.cpp b/indra/newview/llfilepicker.cpp index 2516bece8d..66e19d551e 100644 --- a/indra/newview/llfilepicker.cpp +++ b/indra/newview/llfilepicker.cpp @@ -64,7 +64,7 @@ LLFilePicker LLFilePicker::sInstance; #define XML_FILTER L"XML files (*.xml)\0*.xml\0" #define SLOBJECT_FILTER L"Objects (*.slobject)\0*.slobject\0" #define RAW_FILTER L"RAW files (*.raw)\0*.raw\0" -#define MODEL_FILTER L"Model files (*.dae)\0*.dae\0" +#define MODEL_FILTER L"Model files (*.dae, *.gltf, *.glb)\0*.dae;*.gltf;*.glb\0" #define MATERIAL_FILTER L"GLTF Files (*.gltf; *.glb)\0*.gltf;*.glb\0" #define HDRI_FILTER L"HDRI Files (*.exr)\0*.exr\0" #define MATERIAL_TEXTURES_FILTER L"GLTF Import (*.gltf; *.glb; *.tga; *.bmp; *.jpg; *.jpeg; *.png)\0*.gltf;*.glb;*.tga;*.bmp;*.jpg;*.jpeg;*.png\0" @@ -676,6 +676,8 @@ std::unique_ptr<std::vector<std::string>> LLFilePicker::navOpenFilterProc(ELoadF case FFLOAD_HDRI: allowedv->push_back("exr"); case FFLOAD_MODEL: + allowedv->push_back("gltf"); + allowedv->push_back("glb"); case FFLOAD_COLLADA: allowedv->push_back("dae"); break; diff --git a/indra/newview/llfloatermodelpreview.cpp b/indra/newview/llfloatermodelpreview.cpp index 8332a430e6..96f9ddbf8a 100644 --- a/indra/newview/llfloatermodelpreview.cpp +++ b/indra/newview/llfloatermodelpreview.cpp @@ -64,6 +64,7 @@ #include "llcallbacklist.h" #include "llviewertexteditor.h" #include "llviewernetwork.h" +#include "llmaterialeditor.h" //static @@ -113,6 +114,11 @@ void LLMeshFilePicker::notify(const std::vector<std::string>& filenames) if (filenames.size() > 0) { mMP->loadModel(filenames[0], mLOD); + + if (filenames[0].substr(filenames[0].length() - 4) == ".glb" || filenames[0].substr(filenames[0].length() - 5) == ".gltf") + { + LLMaterialEditor::loadMaterialFromFile(filenames[0], -1); + } } else { diff --git a/indra/newview/llmaterialeditor.cpp b/indra/newview/llmaterialeditor.cpp index 28160177f6..378f5fdf91 100644 --- a/indra/newview/llmaterialeditor.cpp +++ b/indra/newview/llmaterialeditor.cpp @@ -137,7 +137,8 @@ LLFloaterComboOptions* LLFloaterComboOptions::showUI( { combo_picker->mComboOptions->addSimpleElement(*iter); } - combo_picker->mComboOptions->selectFirstItem(); + // select 'Bulk Upload All' option + combo_picker->mComboOptions->selectNthItem((S32)options.size() - 1); combo_picker->openFloater(LLSD(title)); combo_picker->setFocus(true); diff --git a/indra/newview/llmodelpreview.cpp b/indra/newview/llmodelpreview.cpp index c73282dad3..c7cb2b68e0 100644 --- a/indra/newview/llmodelpreview.cpp +++ b/indra/newview/llmodelpreview.cpp @@ -30,7 +30,7 @@ #include "llmodelloader.h" #include "lldaeloader.h" -#include "llgltfloader.h" +#include "gltf/llgltfloader.h" #include "llfloatermodelpreview.h" #include "llagent.h" @@ -3090,25 +3090,48 @@ void LLModelPreview::lookupLODModelFiles(S32 lod) S32 next_lod = (lod - 1 >= LLModel::LOD_IMPOSTOR) ? lod - 1 : LLModel::LOD_PHYSICS; std::string lod_filename = mLODFile[LLModel::LOD_HIGH]; - std::string ext = ".dae"; std::string lod_filename_lower(lod_filename); LLStringUtil::toLower(lod_filename_lower); - std::string::size_type i = lod_filename_lower.rfind(ext); - if (i != std::string::npos) + + // Check for each supported file extension + std::vector<std::string> supported_exts = { ".dae", ".gltf", ".glb" }; + std::string found_ext; + std::string::size_type ext_pos = std::string::npos; + + for (const auto& ext : supported_exts) { - lod_filename.replace(i, lod_filename.size() - ext.size(), getLodSuffix(next_lod) + ext); + std::string::size_type i = lod_filename_lower.rfind(ext); + if (i != std::string::npos) + { + ext_pos = i; + found_ext = ext; + break; + } } - if (gDirUtilp->fileExists(lod_filename)) + + if (ext_pos != std::string::npos) { - LLFloaterModelPreview* fmp = LLFloaterModelPreview::sInstance; - if (fmp) + // Replace extension with LOD suffix + original extension + std::string lod_file_to_check = lod_filename; + lod_file_to_check.replace(ext_pos, found_ext.size(), getLodSuffix(next_lod) + found_ext); + + if (gDirUtilp->fileExists(lod_file_to_check)) + { + LLFloaterModelPreview* fmp = LLFloaterModelPreview::sInstance; + if (fmp) + { + fmp->setCtrlLoadFromFile(next_lod); + } + loadModel(lod_file_to_check, next_lod); + } + else { - fmp->setCtrlLoadFromFile(next_lod); + lookupLODModelFiles(next_lod); } - loadModel(lod_filename, next_lod); } else { + // No recognized extension found, continue with next LOD lookupLODModelFiles(next_lod); } } diff --git a/indra/newview/llskinningutil.cpp b/indra/newview/llskinningutil.cpp index cee43f3cff..43836a420b 100644 --- a/indra/newview/llskinningutil.cpp +++ b/indra/newview/llskinningutil.cpp @@ -135,6 +135,12 @@ void LLSkinningUtil::initSkinningMatrixPalette( initJointNums(const_cast<LLMeshSkinInfo*>(skin), avatar); + if (skin->mInvBindMatrix.size() < count ) + { + // faulty model? mInvBindMatrix.size() should have matched mJointNames.size() + return; + } + LLMatrix4a world[LL_CHARACTER_MAX_ANIMATED_JOINTS]; for (S32 j = 0; j < count; ++j) diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 6b3dccf89c..d0bdcf132f 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -3783,7 +3783,12 @@ bool LLVOVolume::canBeAnimatedObject() const bool LLVOVolume::isAnimatedObject() const { - LLVOVolume *root_vol = (LLVOVolume*)getRootEdit(); + LLViewerObject *root_obj = getRootEdit(); + if (root_obj->getPCode() != LL_PCODE_VOLUME) + { + return false; // at the moment only volumes can be animated + } + LLVOVolume* root_vol = (LLVOVolume*)root_obj; mIsAnimatedObject = root_vol->getExtendedMeshFlags() & LLExtendedMeshParams::ANIMATED_MESH_ENABLED_FLAG; return mIsAnimatedObject; } diff --git a/indra/newview/skins/default/xui/en/floater_model_preview.xml b/indra/newview/skins/default/xui/en/floater_model_preview.xml index 90223fcda8..6231abf9a5 100644 --- a/indra/newview/skins/default/xui/en/floater_model_preview.xml +++ b/indra/newview/skins/default/xui/en/floater_model_preview.xml @@ -45,6 +45,7 @@ <string name="UnrecognizedJoint">Rigged to unrecognized joint name [NAME]</string> <string name="UnknownJoints">Skinning disabled due to [COUNT] unknown joints</string> <string name="ModelLoaded">Model [MODEL_NAME] loaded</string> + <string name="InvBindCountMismatch">Bind matrices count mismatch joints count</string> <string name="IncompleteTC">Texture coordinates data is not complete.</string> <string name="PositionNaN">Found NaN while loading position data from DAE-Model, invalid model.</string> @@ -60,6 +61,9 @@ <string name="ParsingErrorNoRoot">Document has no root</string> <string name="ParsingErrorNoScene">Document has no visual_scene</string> <string name="ParsingErrorPositionInvalidModel">Unable to process mesh without position data. Invalid model.</string> + <string name="InvalidGeometryNonTriangulated">Invalid geometry: GLTF files must contain triangulated meshes only.</string> + <string name="IgnoredExtension">Model uses unsupported extension: [EXT], related material properties are ignored.</string> + <string name="UnsupportedExtension">Unable to load a model, unsupported extension: [EXT]</string> <panel follows="top|left" diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index 8a41405e4c..6bb9f7b1dc 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -9453,8 +9453,11 @@ Unable to upload texture: '[NAME]' icon="alertmodal.tga" name="CannotUploadMaterial" type="alertmodal"> -There was a problem uploading the file +Unable to upload material file. The file may be corrupted, in an unsupported format, or contain invalid data. Please check that you're using a valid GLTF/GLB file with proper material definitions. <tag>fail</tag> + <usetemplate + name="okbutton" + yestext="OK"/> </notification> <notification |