diff options
Diffstat (limited to 'indra/newview/gltf')
| -rw-r--r-- | indra/newview/gltf/accessor.cpp | 2 | ||||
| -rw-r--r-- | indra/newview/gltf/asset.cpp | 208 | ||||
| -rw-r--r-- | indra/newview/gltf/asset.h | 13 | ||||
| -rw-r--r-- | indra/newview/gltf/buffer_util.h | 23 | ||||
| -rw-r--r-- | indra/newview/gltf/common.h | 2 | ||||
| -rw-r--r-- | indra/newview/gltf/llgltfloader.cpp | 1822 | ||||
| -rw-r--r-- | indra/newview/gltf/llgltfloader.h | 216 | ||||
| -rw-r--r-- | indra/newview/gltf/primitive.cpp | 2 | 
8 files changed, 2189 insertions, 99 deletions
diff --git a/indra/newview/gltf/accessor.cpp b/indra/newview/gltf/accessor.cpp index d1845605d4..03f7331893 100644 --- a/indra/newview/gltf/accessor.cpp +++ b/indra/newview/gltf/accessor.cpp @@ -159,7 +159,7 @@ bool Buffer::prep(Asset& asset)          std::string dir = gDirUtilp->getDirName(asset.mFilename);          std::string bin_file = dir + gDirUtilp->getDirDelimiter() + mUri; -        std::ifstream file(bin_file, std::ios::binary); +        llifstream file(bin_file.c_str(), std::ios::binary);          if (!file.is_open())          {              LL_WARNS("GLTF") << "Failed to open file: " << bin_file << LL_ENDL; diff --git a/indra/newview/gltf/asset.cpp b/indra/newview/gltf/asset.cpp index c210b9c61d..f7a5a20872 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,110 @@ 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 primitive -                    for (auto& primitive : mesh.mPrimitives) -                    { -                        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(); +                // for each shader variant +                U32 vertex_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 }; +                U32 index_count[LLGLSLShader::NUM_GLTF_VARIANTS] = { 0 }; -                            // 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; -                        } -                    } +                S32 ds_mat = mat_id == -1 ? 0 : mMaterials[mat_id].mDoubleSided; +                if (ds_mat != double_sided) +                { +                    continue;                  } -                // allocate vertex buffer and pack it -                if (vertex_count[variant] > 0) +                for (U32 variant = 0; variant < LLGLSLShader::NUM_GLTF_VARIANTS; ++variant)                  { -                    U32 mat_idx = mat_id + 1; -                    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(); - +#ifdef SHOW_ASSERT +                    U32 attribute_mask = 0; +#endif +                    // for each mesh                      for (auto& mesh : mMeshes)                      { +                        // for each primitive                          for (auto& primitive : mesh.mPrimitives)                          {                              if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant)                              { -                                primitive.upload(vb); +                                // 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(); + +                                // all primitives of a given variant and material should all have the same attribute mask +                                llassert(attribute_mask == 0 || primitive.mAttributeMask == attribute_mask); +#ifdef SHOW_ASSERT +                                attribute_mask |= primitive.mAttributeMask; +#endif                              }                          }                      } -                    vb->unmapBuffer(); +                    // 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(); + +                        for (auto& mesh : mMeshes) +                        { +                            for (auto& primitive : mesh.mPrimitives) +                            { +                                if (primitive.mMaterial == mat_id && primitive.mShaderVariant == variant) +                                { +                                    primitive.upload(vb); +                                } +                            } +                        } + +                        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 +684,7 @@ bool Asset::prep()              }          }      } +    #endif      return true;  } @@ -672,13 +693,14 @@ 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); -    std::ifstream file(filename.data(), std::ios::binary); +    llifstream file(filename.data(), std::ios::binary);      if (file.is_open())      {          std::string str((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>()); @@ -692,7 +714,7 @@ bool Asset::load(std::string_view filename)          }          else if (ext == "glb")          { -            return loadBinary(str); +            return loadBinary(str, mLoadIntoVRAM);          }          else          { @@ -709,8 +731,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(); @@ -904,7 +927,7 @@ bool Asset::save(const std::string& filename)      // save .gltf      object obj;      serialize(obj); -    std::string buffer = boost::json::serialize(obj, {}); +    std::string buffer = boost::json::serialize(obj);      std::ofstream file(filename, std::ios::binary);      file.write(buffer.c_str(), buffer.size()); @@ -935,8 +958,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 +975,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 +995,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 +1015,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..c231443a9e 100644 --- a/indra/newview/gltf/buffer_util.h +++ b/indra/newview/gltf/buffer_util.h @@ -147,6 +147,12 @@ namespace LL          }          template<> +        inline void copyVec3<F32, LLVector2>(F32* src, LLVector2& dst) +        { +            dst.set(src[0], src[1]); +        } + +        template<>          inline void copyVec3<F32, vec3>(F32* src, vec3& dst)          {              dst = vec3(src[0], src[1], src[2]); @@ -159,6 +165,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); @@ -369,7 +381,18 @@ namespace LL          template<class T>          inline void copy(Asset& asset, Accessor& accessor, LLStrider<T>& dst)          { +            if (accessor.mBufferView == INVALID_INDEX +                || accessor.mBufferView >= asset.mBufferViews.size()) +            { +                LL_WARNS("GLTF") << "Invalid buffer" << LL_ENDL; +                return; +            }              const BufferView& bufferView = asset.mBufferViews[accessor.mBufferView]; +            if (bufferView.mBuffer >= asset.mBuffers.size()) +            { +                LL_WARNS("GLTF") << "Invalid buffer view" << LL_ENDL; +                return; +            }              const Buffer& buffer = asset.mBuffers[bufferView.mBuffer];              const U8* src = buffer.mData.data() + bufferView.mByteOffset + accessor.mByteOffset; diff --git a/indra/newview/gltf/common.h b/indra/newview/gltf/common.h index 742daff715..8cf3f1dff7 100644 --- a/indra/newview/gltf/common.h +++ b/indra/newview/gltf/common.h @@ -26,8 +26,6 @@   * $/LicenseInfo$   */ -#define GLM_ENABLE_EXPERIMENTAL 1 -  #include "glm/vec2.hpp"  #include "glm/vec3.hpp"  #include "glm/vec4.hpp" diff --git a/indra/newview/gltf/llgltfloader.cpp b/indra/newview/gltf/llgltfloader.cpp new file mode 100644 index 0000000000..dd1d327683 --- /dev/null +++ b/indra/newview/gltf/llgltfloader.cpp @@ -0,0 +1,1822 @@ +/** + * @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 "llbase64.h" +#include "lldir.h" + +#include "llmatrix4a.h" + +#include <boost/regex.hpp> +#include <boost/algorithm/string/replace.hpp> +#include <boost/exception/diagnostic_information.hpp> +#include <fstream> + +static const std::string lod_suffix[LLModel::NUM_LODS] = +{ +    "_LOD0", +    "_LOD1", +    "_LOD2", +    "", +    "_PHYS", +}; + +// Premade rotation matrix, GLTF is Y-up while SL is Z-up +static const glm::mat4 coord_system_rotation( +    1.f, 0.f, 0.f, 0.f, +    0.f, 0.f, 1.f, 0.f, +    0.f, -1.f, 0.f, 0.f, +    0.f, 0.f, 0.f, 1.f +); + + +static const glm::mat4 coord_system_rotationxy( +    0.f, 1.f, 0.f, 0.f, +    -1.f, 0.f, 0.f, 0.f, +    0.f, 0.f, 1.f, 0.f, +    0.f, 0.f, 0.f, 1.f +); + +static const S32 VERTEX_SPLIT_SAFETY_MARGIN = 3 * 3 + 1; // 10 vertices: 3 complete triangles plus remapping overhead +static const S32 VERTEX_LIMIT = USHRT_MAX - VERTEX_SPLIT_SAFETY_MARGIN; + +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, std::less<>> & jointAliasMap, +    U32                                               maxJointsPerMesh, +    U32                                               modelLimit, +    U32                                               debugMode, +    std::vector<LLJointData>                          viewer_skeleton) //, +    //bool                                            preprocess) +    : LLModelLoader( filename, +                     lod, +                     load_cb, +                     joint_lookup_func, +                     texture_load_func, +                     state_cb, +                     opaque_userdata, +                     jointTransformMap, +                     jointsFromNodes, +                     jointAliasMap, +                     maxJointsPerMesh, +                     modelLimit, +                     debugMode) +    , mViewerJointData(viewer_skeleton) +    , mGltfLoaded(false) +    , mApplyXYRotation(false) +{ +} + +LLGLTFLoader::~LLGLTFLoader() {} + +bool LLGLTFLoader::OpenFile(const std::string &filename) +{ +    // Clear the material cache for new file +    mMaterialCache.clear(); + +    tinygltf::TinyGLTF loader; +    std::string filename_lc(filename); +    LLStringUtil::toLower(filename_lc); + +    try +    { +        mGltfLoaded = mGLTFAsset.load(filename, false); +    } +    catch (const std::exception& e) +    { +        LL_WARNS() << "Exception in LLModelLoader::run: " << e.what() << LL_ENDL; +        LLSD args; +        args["Message"] = "ParsingErrorException"; +        args["FILENAME"] = filename; +        args["EXCEPTION"] = e.what(); +        mWarningsArray.append(args); +        setLoadState(ERROR_PARSING); +        return false; +    } +    catch (...) +    { +        LOG_UNHANDLED_EXCEPTION("LLGLTFLoader"); +        LLSD args; +        args["Message"] = "ParsingErrorException"; +        args["FILENAME"] = filename; +        args["EXCEPTION"] = boost::current_exception_diagnostic_information(); +        mWarningsArray.append(args); +        setLoadState(ERROR_PARSING); +        return false; +    } + +    if (!mGltfLoaded) +    { +        notifyUnsupportedExtension(true); + +        for (const auto& buffer : mGLTFAsset.mBuffers) +        { +            if (buffer.mByteLength > 0 && buffer.mData.empty()) +            { +                bool bin_file = buffer.mUri.ends_with(".bin"); +                LLSD args; +                args["Message"] = bin_file ? "ParsingErrorMissingBufferBin" : "ParsingErrorMissingBuffer"; +                args["BUFFER_NAME"] = buffer.mName; +                args["BUFFER_URI"] = buffer.mUri; +                mWarningsArray.append(args); +            } +        } +        setLoadState(ERROR_PARSING); +        return false; +    } + +    notifyUnsupportedExtension(false); + +    bool meshesLoaded = parseMeshes(); + +    setLoadState(DONE); + +    return meshesLoaded; +} + +void LLGLTFLoader::addModelToScene( +    LLModel* pModel, +    const std::string& model_name, +    U32 submodel_limit, +    const LLMatrix4& transformation, +    const LLVolumeParams& volume_params, +    const material_map& mats) +{ +    U32 volume_faces = pModel->getNumVolumeFaces(); + +    // Side-steps all manner of issues when splitting models +    // and matching lower LOD materials to base models +    // +    pModel->sortVolumeFacesByMaterialName(); + +    int submodelID = 0; + +    // remove all faces that definitely won't fit into one model and submodel limit +    U32 face_limit = (submodel_limit + 1) * LL_SCULPT_MESH_MAX_FACES; +    if (face_limit < volume_faces) +    { +        LL_WARNS("GLTF_IMPORT") << "Model contains " << volume_faces +            << " faces, exceeding the limit of " << face_limit << LL_ENDL; + +        LLSD args; +        args["Message"] = "ModelTooManySubmodels"; +        args["MODEL_NAME"] = pModel->mLabel; +        args["SUBMODEL_COUNT"] = static_cast<S32>(llfloor((F32)volume_faces / LL_SCULPT_MESH_MAX_FACES)); +        args["SUBMODEL_LIMIT"] = static_cast<S32>(submodel_limit); +        mWarningsArray.append(args); + +        pModel->setNumVolumeFaces(face_limit); +    } + +    LLVolume::face_list_t remainder; +    std::vector<LLModel*> ready_models; +    LLModel* current_model = pModel; + +    do +    { +        current_model->trimVolumeFacesToSize(LL_SCULPT_MESH_MAX_FACES, &remainder); + +        volume_faces = static_cast<U32>(remainder.size()); + +        // Don't add to scene yet because weights and materials aren't ready. +        // Just save it +        ready_models.push_back(current_model); + +        // If we have left-over volume faces, create another model +        // to absorb them. +        if (volume_faces) +        { +            LLModel* next = new LLModel(volume_params, 0.f); +            next->ClearFacesAndMaterials(); +            next->mSubmodelID = ++submodelID; + +            std::string instance_name = model_name; +            if (next->mSubmodelID > 0) +            { +                instance_name += (char)((int)'a' + next->mSubmodelID); +            } +            // Check for duplicates and add copy suffix if needed +            int duplicate_count = 0; +            for (const auto& inst : mScene[transformation]) +            { +                if (inst.mLabel == instance_name) +                { +                    ++duplicate_count; +                } +            } +            if (duplicate_count > 0) { +                instance_name += "_copy_" + std::to_string(duplicate_count); +            } +            next->mLabel = instance_name; + +            next->getVolumeFaces() = remainder; +            next->mNormalizedScale = current_model->mNormalizedScale; +            next->mNormalizedTranslation = current_model->mNormalizedTranslation; +            next->mSkinWeights = current_model->mSkinWeights; +            next->mPosition = current_model->mPosition; + +            const LLMeshSkinInfo& current_skin_info = current_model->mSkinInfo; +            LLMeshSkinInfo& next_skin_info = next->mSkinInfo; +            next_skin_info.mJointNames = current_skin_info.mJointNames; +            next_skin_info.mJointNums = current_skin_info.mJointNums; +            next_skin_info.mBindShapeMatrix = current_skin_info.mBindShapeMatrix; +            next_skin_info.mInvBindMatrix = current_skin_info.mInvBindMatrix; +            next_skin_info.mAlternateBindMatrix = current_skin_info.mAlternateBindMatrix; +            next_skin_info.mPelvisOffset = current_skin_info.mPelvisOffset; + + +            if (current_model->mMaterialList.size() > LL_SCULPT_MESH_MAX_FACES) +            { +                next->mMaterialList.assign(current_model->mMaterialList.begin() + LL_SCULPT_MESH_MAX_FACES, current_model->mMaterialList.end()); +                current_model->mMaterialList.resize(LL_SCULPT_MESH_MAX_FACES); +            } + +            current_model = next; +        } + +        remainder.clear(); + +    } while (volume_faces); + +    for (auto model : ready_models) +    { +        // remove unused/redundant vertices +        model->remapVolumeFaces(); + +        mModelList.push_back(model); + +        std::map<std::string, LLImportMaterial> materials; +        for (U32 i = 0; i < (U32)model->mMaterialList.size(); ++i) +        { +            material_map::const_iterator found = mats.find(model->mMaterialList[i]); +            if (found != mats.end()) +            { +                materials[model->mMaterialList[i]] = found->second; +            } +            else +            { +                materials[model->mMaterialList[i]] = LLImportMaterial(); +            } +        } +        // Keep base name for scene instance. +        std::string instance_name = model->mLabel; +        // Add suffix. Suffix is nessesary for model matching logic +        // because sometimes higher lod can be used as a lower one, so models +        // need unique names not just in scope of one lod, but across lods. +        model->mLabel += lod_suffix[mLod]; +        mScene[transformation].push_back(LLModelInstance(model, instance_name, transformation, materials)); +        stretch_extents(model, transformation); +    } +} + +bool LLGLTFLoader::parseMeshes() +{ +    if (!mGltfLoaded) return false; + +    // 2022-04 DJH Volume params from dae example. TODO understand PCODE +    LLVolumeParams volume_params; +    volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE); + +    mTransform.setIdentity(); + +    for (auto& node : mGLTFAsset.mNodes) +    { +        // Make node matrix valid for correct transformation +        node.makeMatrixValid(); +    } + +    if (mGLTFAsset.mSkins.size() > 0) +    { +        checkForXYrotation(mGLTFAsset.mSkins[0]); +        populateJointGroups(); +    } + +    // Populate the joints from skins first. +    // Multiple meshes can share the same skin, so preparing skins beforehand. +    for (S32 i = 0; i < mGLTFAsset.mSkins.size(); i++) +    { +        populateJointsFromSkin(i); +    } + +    // Track how many times each mesh name has been used +    std::map<std::string, S32> mesh_name_counts; + +    // For now use mesh count, but might be better to do 'mNodes.size() - joints count'. +    U32 submodel_limit = mGLTFAsset.mMeshes.size() > 0 ? mGeneratedModelLimit / (U32)mGLTFAsset.mMeshes.size() : 0; + +    // Check if we have scenes defined +    if (!mGLTFAsset.mScenes.empty()) +    { +        // Process the default scene (or first scene if no default) +        S32 scene_idx = mGLTFAsset.mScene >= 0 ? mGLTFAsset.mScene : 0; + +        if (scene_idx < mGLTFAsset.mScenes.size()) +        { +            const LL::GLTF::Scene& scene = mGLTFAsset.mScenes[scene_idx]; + +            LL_INFOS("GLTF_IMPORT") << "Processing scene " << scene_idx << " with " << scene.mNodes.size() << " root nodes" << LL_ENDL; + +            // Process all root nodes defined in the scene +            for (S32 root_idx : scene.mNodes) +            { +                if (root_idx >= 0 && root_idx < static_cast<S32>(mGLTFAsset.mNodes.size())) +                { +                    processNodeHierarchy(root_idx, mesh_name_counts, submodel_limit, volume_params); +                } +            } +        } +    } +    else +    { +        LL_WARNS("GLTF_IMPORT") << "No scenes defined in GLTF file" << LL_ENDL; + +        LLSD args; +        args["Message"] = "NoScenesFound"; +        mWarningsArray.append(args); +        return false; +    } + +    checkGlobalJointUsage(); + +    return true; +} + +void LLGLTFLoader::processNodeHierarchy(S32 node_idx, std::map<std::string, S32>& mesh_name_counts, U32 submodel_limit, const LLVolumeParams& volume_params) +{ +    if (node_idx < 0 || node_idx >= static_cast<S32>(mGLTFAsset.mNodes.size())) +        return; + +    const LL::GLTF::Node& node = mGLTFAsset.mNodes[node_idx]; + +    LL_DEBUGS("GLTF_IMPORT") << "Processing node " << node_idx << " (" << node.mName << ")" +                            << " - has mesh: " << (node.mMesh >= 0 ? "yes" : "no") +                            << " - children: " << node.mChildren.size() << LL_ENDL; + +    // Process this node's mesh if it has one +    if (node.mMesh >= 0 && node.mMesh < mGLTFAsset.mMeshes.size()) +    { +        LLMatrix4    transformation; +        material_map mats; + +        LLModel* pModel = new LLModel(volume_params, 0.f); +        const LL::GLTF::Mesh& mesh = mGLTFAsset.mMeshes[node.mMesh]; + +        // Get base mesh name and track usage +        std::string base_name = getLodlessLabel(mesh); +        if (base_name.empty()) +        { +            base_name = "mesh_" + std::to_string(node.mMesh); +        } + +        S32 instance_count = mesh_name_counts[base_name]++; + +        // make name unique +        if (instance_count > 0) +        { +            base_name = base_name + "_copy_" + std::to_string(instance_count); +        } + +        if (populateModelFromMesh(pModel, base_name, mesh, node, mats) && +            (LLModel::NO_ERRORS == pModel->getStatus()) && +            validate_model(pModel)) +        { +            mTransform.setIdentity(); +            transformation = mTransform; + +            // adjust the transformation to compensate for mesh normalization +            LLVector3 mesh_scale_vector; +            LLVector3 mesh_translation_vector; +            pModel->getNormalizedScaleTranslation(mesh_scale_vector, mesh_translation_vector); + +            LLMatrix4 mesh_translation; +            mesh_translation.setTranslation(mesh_translation_vector); +            mesh_translation *= transformation; +            transformation = mesh_translation; + +            LLMatrix4 mesh_scale; +            mesh_scale.initScale(mesh_scale_vector); +            mesh_scale *= transformation; +            transformation = mesh_scale; + +            if (node.mSkin >= 0) +            { +                // "Bind Shape Matrix" is supposed to transform the geometry of the skinned mesh +                // into the coordinate space of the joints. +                // In GLTF, this matrix is omitted, and it is assumed that this transform is either +                // premultiplied with the mesh data, or postmultiplied to the inverse bind matrices. +                // +                // TODO: There appears to be missing rotation when joints rotate the model +                // or inverted bind matrices are missing inherited rotation +                // (based of values the 'bento shoes' mesh might be missing 90 degrees horizontaly +                // prior to skinning) + +                pModel->mSkinInfo.mBindShapeMatrix.loadu(mesh_scale); +                LL_INFOS("GLTF_DEBUG") << "Model: " << pModel->mLabel << " mBindShapeMatrix: " << pModel->mSkinInfo.mBindShapeMatrix << LL_ENDL; +            } + +            if (transformation.determinant() < 0) +            { // negative scales are not supported +                LL_INFOS("GLTF_IMPORT") << "Negative scale detected, unsupported post-normalization transform.  domInstance_geometry: " +                           << pModel->mLabel << LL_ENDL; +                LLSD args; +                args["Message"] = "NegativeScaleNormTrans"; +                args["LABEL"]   = pModel->mLabel; +                mWarningsArray.append(args); +            } + +            addModelToScene(pModel, base_name, submodel_limit, transformation, volume_params, mats); +            mats.clear(); +        } +        else +        { +            setLoadState(ERROR_MODEL + pModel->getStatus()); +            delete pModel; +            return; +        } +    } +    else if (node.mMesh >= 0) +    { +        // Log invalid mesh reference +        LL_WARNS("GLTF_IMPORT") << "Node " << node_idx << " (" << node.mName +                                << ") references invalid mesh " << node.mMesh +                                << " (total meshes: " << mGLTFAsset.mMeshes.size() << ")" << LL_ENDL; + +        LLSD args; +        args["Message"] = "InvalidMeshReference"; +        args["NODE_NAME"] = node.mName; +        args["MESH_INDEX"] = node.mMesh; +        args["TOTAL_MESHES"] = static_cast<S32>(mGLTFAsset.mMeshes.size()); +        mWarningsArray.append(args); +    } + +    // Process all children recursively +    for (S32 child_idx : node.mChildren) +    { +        processNodeHierarchy(child_idx, mesh_name_counts, submodel_limit, volume_params); +    } +} + +void LLGLTFLoader::computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S32 node_index, glm::mat4& combined_transform) const +{ +    if (node_index < 0 || node_index >= static_cast<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::addJointToModelSkin(LLMeshSkinInfo& skin_info, S32 gltf_skin_idx, size_t gltf_joint_idx) +{ +    const std::string& legal_name = mJointNames[gltf_skin_idx][gltf_joint_idx]; +    if (legal_name.empty()) +    { +        llassert(false); // should have been stopped by gltf_joint_index_use[i] == -1 +        return false; +    } +    skin_info.mJointNames.push_back(legal_name); +    skin_info.mJointNums.push_back(-1); + +    // In scope of same skin multiple meshes reuse same bind matrices +    skin_info.mInvBindMatrix.push_back(mInverseBindMatrices[gltf_skin_idx][gltf_joint_idx]); +    skin_info.mAlternateBindMatrix.push_back(mAlternateBindMatrices[gltf_skin_idx][gltf_joint_idx]); + +    // Track joint usage for this skin, for the sake of unused joints detection +    mJointUsage[gltf_skin_idx][gltf_joint_idx]++; + +    return true; +} + +LLGLTFLoader::LLGLTFImportMaterial LLGLTFLoader::processMaterial(S32 material_index, S32 fallback_index) +{ +    // Check cache first +    auto cached = mMaterialCache.find(material_index); +    if (cached != mMaterialCache.end()) +    { +        return cached->second; +    } + +    LLImportMaterial impMat; +    impMat.mDiffuseColor = LLColor4::white; // Default color + +    // Generate material name +    std::string materialName = generateMaterialName(material_index, fallback_index); + +    // Process material if available +    if (material_index >= 0 && material_index < mGLTFAsset.mMaterials.size()) +    { +        LL::GLTF::Material* material = &mGLTFAsset.mMaterials[material_index]; + +        // Set diffuse color from base color factor +        impMat.mDiffuseColor = LLColor4( +            material->mPbrMetallicRoughness.mBaseColorFactor[0], +            material->mPbrMetallicRoughness.mBaseColorFactor[1], +            material->mPbrMetallicRoughness.mBaseColorFactor[2], +            material->mPbrMetallicRoughness.mBaseColorFactor[3] +        ); + +        // Process base color texture if it exists +        if (material->mPbrMetallicRoughness.mBaseColorTexture.mIndex >= 0) +        { +            S32 texIndex = material->mPbrMetallicRoughness.mBaseColorTexture.mIndex; +            std::string filename = processTexture(texIndex, "base_color", material->mName); + +            if (!filename.empty()) +            { +                impMat.mDiffuseMapFilename = filename; +                impMat.mDiffuseMapLabel = material->mName.empty() ? filename : material->mName; + +                // Check if the texture is already loaded +                S32 sourceIndex; +                if (validateTextureIndex(texIndex, sourceIndex)) +                { +                    LL::GLTF::Image& image = mGLTFAsset.mImages[sourceIndex]; +                    if (image.mTexture.notNull()) +                    { +                        mTexturesNeedScaling |= image.mHeight > LLViewerTexture::MAX_IMAGE_SIZE_DEFAULT || image.mWidth > LLViewerTexture::MAX_IMAGE_SIZE_DEFAULT; +                        impMat.setDiffuseMap(image.mTexture->getID()); +                        LL_INFOS("GLTF_IMPORT") << "Using existing texture ID: " << image.mTexture->getID().asString() << LL_ENDL; +                    } +                    else +                    { +                        LL_INFOS("GLTF_IMPORT") << "Texture needs loading: " << impMat.mDiffuseMapFilename << LL_ENDL; +                    } +                } +            } +        } +    } + +    // Create cached material with both material and name +    LLGLTFImportMaterial cachedMat(impMat, materialName); + +    // Cache the processed material +    mMaterialCache[material_index] = cachedMat; +    return cachedMat; +} + +std::string LLGLTFLoader::processTexture(S32 texture_index, const std::string& texture_type, const std::string& material_name) +{ +    S32 sourceIndex; +    if (!validateTextureIndex(texture_index, sourceIndex)) +        return ""; + +    LL::GLTF::Image& image = mGLTFAsset.mImages[sourceIndex]; + +    // Process URI-based textures +    if (!image.mUri.empty()) +    { +        std::string filename = image.mUri; +        size_t pos = filename.find_last_of("/\\"); +        if (pos != std::string::npos) +        { +            filename = filename.substr(pos + 1); +        } + +        LL_INFOS("GLTF_IMPORT") << "Found texture: " << filename << " for material: " << material_name << LL_ENDL; + +        LLSD args; +        args["Message"] = "TextureFound"; +        args["TEXTURE_NAME"] = filename; +        args["MATERIAL_NAME"] = material_name; +        mWarningsArray.append(args); + +        return filename; +    } + +    // Process embedded textures +    if (image.mBufferView >= 0) +    { +        return extractTextureToTempFile(texture_index, texture_type); +    } + +    return ""; +} + +bool LLGLTFLoader::validateTextureIndex(S32 texture_index, S32& source_index) +{ +    if (texture_index < 0 || texture_index >= mGLTFAsset.mTextures.size()) +        return false; + +    source_index = mGLTFAsset.mTextures[texture_index].mSource; +    if (source_index < 0 || source_index >= mGLTFAsset.mImages.size()) +        return false; + +    return true; +} + +std::string LLGLTFLoader::generateMaterialName(S32 material_index, S32 fallback_index) +{ +    if (material_index >= 0 && material_index < mGLTFAsset.mMaterials.size()) +    { +        LL::GLTF::Material* material = &mGLTFAsset.mMaterials[material_index]; +        std::string materialName = material->mName; + +        if (materialName.empty()) +        { +            materialName = "mat" + std::to_string(material_index); +        } +        return materialName; +    } +    else +    { +        return fallback_index >= 0 ? "mat_default" + std::to_string(fallback_index) : "mat_default"; +    } +} + +bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const std::string& base_name, const LL::GLTF::Mesh& mesh, const LL::GLTF::Node& nodeno, material_map& mats) +{ +    // Set the requested label for the floater display and uploading +    pModel->mRequestedLabel = gDirUtilp->getBaseFileName(mFilename, true); +    // Set only name, suffix will be added later +    pModel->mLabel = base_name; + +    LL_DEBUGS("GLTF_DEBUG") << "Processing model " << pModel->mLabel << LL_ENDL; + +    pModel->ClearFacesAndMaterials(); + +    S32 skinIdx = nodeno.mSkin; + +    // Compute final combined transform matrix (hierarchy + coordinate rotation) +    S32 node_index = static_cast<S32>(&nodeno - &mGLTFAsset.mNodes[0]); +    glm::mat4 hierarchy_transform; +    computeCombinedNodeTransform(mGLTFAsset, node_index, hierarchy_transform); + +    // Combine transforms: coordinate rotation applied to hierarchy transform +    glm::mat4 final_transform = coord_system_rotation * hierarchy_transform; +    if (mApplyXYRotation) +    { +        final_transform = coord_system_rotationxy * final_transform; +    } + +    // Check if we have a negative scale (flipped coordinate system) +    bool hasNegativeScale = glm::determinant(final_transform) < 0.0f; + +    // Pre-compute normal transform matrix (transpose of inverse of upper-left 3x3) +    const glm::mat3 normal_transform = glm::transpose(glm::inverse(glm::mat3(final_transform))); + +    // Mark unsuported joints with '-1' so that they won't get added into weights +    // GLTF maps all joints onto all meshes. Gather use count per mesh to cut unused ones. +    std::vector<S32> gltf_joint_index_use; +    if (skinIdx >= 0 && mGLTFAsset.mSkins.size() > skinIdx) +    { +        LL::GLTF::Skin& gltf_skin = mGLTFAsset.mSkins[skinIdx]; + +        size_t jointCnt = gltf_skin.mJoints.size(); +        gltf_joint_index_use.resize(jointCnt, 0); + +        for (size_t i = 0; i < jointCnt; ++i) +        { +            if (mJointNames[skinIdx][i].empty()) +            { +                // This might need to hold a substitute index +                gltf_joint_index_use[i] = -1; // mark as unsupported +            } +        } +    } + +    for (size_t prim_idx = 0; prim_idx < mesh.mPrimitives.size(); ++prim_idx) +    { +        const LL::GLTF::Primitive& prim = mesh.mPrimitives[prim_idx]; + +        // So primitives already have all of the data we need for a given face in SL land. +        // Primitives may only ever have a single material assigned to them - as the relation is 1:1 in terms of intended draw call +        // count. Just go ahead and populate faces direct from the GLTF primitives here. -Geenz 2025-04-07 +        LLVolumeFace face; +        std::vector<GLTFVertex> vertices; + +        // Use cached material processing +        LLGLTFImportMaterial cachedMat = processMaterial(prim.mMaterial, pModel->getNumVolumeFaces() - 1); +        LLImportMaterial impMat = cachedMat; +        std::string materialName = cachedMat.name; +        mats[materialName] = impMat; + +        if (prim.getIndexCount() % 3 != 0) +        { +            LL_WARNS("GLTF_IMPORT") << "Mesh '" << mesh.mName << "' primitive " << prim_idx +                << ": Invalid index count " << prim.getIndexCount() +                << " (not divisible by 3). GLTF files must contain triangulated geometry." << LL_ENDL; + +            LLSD args; +            args["Message"] = "InvalidGeometryNonTriangulated"; +            args["MESH_NAME"] = mesh.mName; +            args["PRIMITIVE_INDEX"] = static_cast<S32>(prim_idx); +            args["INDEX_COUNT"] = static_cast<S32>(prim.getIndexCount()); +            mWarningsArray.append(args); +            return false; // Skip this primitive +        } + +        // Apply the global scale and center offset to all vertices +        for (U32 i = 0; i < prim.getVertexCount(); i++) +        { +            // Use pre-computed final_transform +            glm::vec4 pos(prim.mPositions[i][0], prim.mPositions[i][1], prim.mPositions[i][2], 1.0f); +            glm::vec4 transformed_pos = final_transform * pos; + +            GLTFVertex vert; +            vert.position = glm::vec3(transformed_pos); + +            if (!prim.mNormals.empty()) +            { +                // Use pre-computed normal_transform +                glm::vec3 normal_vec(prim.mNormals[i][0], prim.mNormals[i][1], prim.mNormals[i][2]); +                vert.normal = glm::normalize(normal_transform * normal_vec); +            } +            else +            { +                // Use default normal (pointing up in model space) +                vert.normal = glm::normalize(normal_transform * glm::vec3(0.0f, 0.0f, 1.0f)); +                LL_DEBUGS("GLTF_IMPORT") << "No normals found for primitive, using default normal." << LL_ENDL; +            } + +            vert.uv0 = glm::vec2(prim.mTexCoords0[i][0], -prim.mTexCoords0[i][1]); + +            if (skinIdx >= 0) +            { +                vert.weights = glm::vec4(prim.mWeights[i]); + +                auto accessorIdx = prim.mAttributes.at("JOINTS_0"); +                LL::GLTF::Accessor::ComponentType componentType = LL::GLTF::Accessor::ComponentType::UNSIGNED_BYTE; +                if (accessorIdx >= 0) +                { +                    auto accessor = mGLTFAsset.mAccessors[accessorIdx]; +                    componentType = accessor.mComponentType; +                } + +                // The GLTF spec allows for either an unsigned byte for joint indices, or an unsigned short. +                // Detect and unpack accordingly. +                if (componentType == LL::GLTF::Accessor::ComponentType::UNSIGNED_BYTE) +                { +                    auto ujoint = glm::unpackUint4x8((U32)(prim.mJoints[i] & 0xFFFFFFFF)); +                    vert.joints = glm::u16vec4(ujoint.x, ujoint.y, ujoint.z, ujoint.w); +                } +                else if (componentType == LL::GLTF::Accessor::ComponentType::UNSIGNED_SHORT) +                { +                    vert.joints = glm::unpackUint4x16(prim.mJoints[i]); +                } +                else +                { +                    vert.joints = glm::zero<glm::u16vec4>(); +                    vert.weights = glm::zero<glm::vec4>(); +                } +            } +            vertices.push_back(vert); +        } + +        // Check for empty vertex array before processing +        if (vertices.empty()) +        { +            LL_WARNS("GLTF_IMPORT") << "Empty vertex array for primitive " << prim_idx << " in model " << mesh.mName << LL_ENDL; +            LLSD args; +            args["Message"] = "EmptyVertexArray"; +            args["MESH_NAME"] = mesh.mName; +            args["PRIMITIVE_INDEX"] = static_cast<S32>(prim_idx); +            args["INDEX_COUNT"] = static_cast<S32>(prim.getIndexCount()); +            mWarningsArray.append(args); +            return false; // 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[vertices[i].joints.x] >= 0 +                    && vertices[i].weights.x > 0.f) +                { +                    weight_list.push_back(LLModel::JointWeight(vertices[i].joints.x, vertices[i].weights.x)); +                    gltf_joint_index_use[vertices[i].joints.x]++; +                } +                if (gltf_joint_index_use[vertices[i].joints.y] >= 0 +                    && vertices[i].weights.y > 0.f) +                { +                    weight_list.push_back(LLModel::JointWeight(vertices[i].joints.y, vertices[i].weights.y)); +                    gltf_joint_index_use[vertices[i].joints.y]++; +                } +                if (gltf_joint_index_use[vertices[i].joints.z] >= 0 +                    && vertices[i].weights.z > 0.f) +                { +                    weight_list.push_back(LLModel::JointWeight(vertices[i].joints.z, vertices[i].weights.z)); +                    gltf_joint_index_use[vertices[i].joints.z]++; +                } +                if (gltf_joint_index_use[vertices[i].joints.w] >= 0 +                    && vertices[i].weights.w > 0.f) +                { +                    weight_list.push_back(LLModel::JointWeight(vertices[i].joints.w, vertices[i].weights.w)); +                    gltf_joint_index_use[vertices[i].joints.w]++; +                } + +                std::sort(weight_list.begin(), weight_list.end(), LLModel::CompareWeightGreater()); + +                std::vector<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; +                } +            } +        } + +        // Indices handling +        if (faceVertices.size() >= VERTEX_LIMIT) +        { +            // Will have to remap 32 bit indices into 16 bit indices +            // For the sake of simplicity build vector of 32 bit indices first +            std::vector<U32> indices_32; +            for (U32 i = 0; i < prim.getIndexCount(); i += 3) +            { +                // When processing indices, flip winding order if needed +                if (hasNegativeScale) +                { +                    // Flip winding order for negative scale +                    indices_32.push_back(prim.mIndexArray[i]); +                    indices_32.push_back(prim.mIndexArray[i + 2]); // Swap these two +                    indices_32.push_back(prim.mIndexArray[i + 1]); +                } +                else +                { +                    indices_32.push_back(prim.mIndexArray[i]); +                    indices_32.push_back(prim.mIndexArray[i + 1]); +                    indices_32.push_back(prim.mIndexArray[i + 2]); +                } +            } + +            // Generates a vertex remap table with no gaps in the resulting sequence +            std::vector<U32> remap(faceVertices.size()); +            size_t vertex_count = meshopt_generateVertexRemap(&remap[0], &indices_32[0], indices_32.size(), &faceVertices[0], faceVertices.size(), sizeof(LLVolumeFace::VertexData)); + +            // Manually remap vertices +            std::vector<LLVolumeFace::VertexData> optimized_vertices(vertex_count); +            for (size_t i = 0; i < vertex_count; ++i) +            { +                optimized_vertices[i] = faceVertices[remap[i]]; +            } + +            std::vector<U32> optimized_indices(indices_32.size()); +            meshopt_remapIndexBuffer(&optimized_indices[0], &indices_32[0], indices_32.size(), &remap[0]); + +            // Sort indices to improve mesh splits (reducing amount of duplicated indices) +            meshopt_optimizeVertexCache(&optimized_indices[0], &optimized_indices[0], indices_32.size(), vertex_count); + +            std::vector<U16> indices_16; +            std::vector<S64> vertices_remap; +            vertices_remap.resize(vertex_count, -1); +            S32 created_faces = 0; +            std::vector<LLVolumeFace::VertexData> face_verts; +            min = glm::vec3(FLT_MAX); +            max = glm::vec3(-FLT_MAX); + +            for (size_t idx = 0; idx < optimized_indices.size(); idx++) +            { +                size_t vert_index = optimized_indices[idx]; +                if (vertices_remap[vert_index] == -1) +                { +                    // First encounter, add it +                    size_t new_vert_idx = face_verts.size(); +                    vertices_remap[vert_index] = (S64)new_vert_idx; +                    face_verts.push_back(optimized_vertices[vert_index]); +                    vert_index = new_vert_idx; + +                    // Update min/max bounds +                    const LLVector4a& vec = face_verts[new_vert_idx].getPosition(); +                    if (new_vert_idx == 0) +                    { +                        min.x = vec[0]; +                        min.y = vec[1]; +                        min.z = vec[2]; +                        max = min; +                    } +                    else +                    { +                        min.x = std::min(min.x, vec[0]); +                        min.y = std::min(min.y, vec[1]); +                        min.z = std::min(min.z, vec[2]); +                        max.x = std::max(max.x, vec[0]); +                        max.y = std::max(max.y, vec[1]); +                        max.z = std::max(max.z, vec[2]); +                    } +                } +                else +                { +                    // already in vector, get position +                    vert_index = (size_t)vertices_remap[vert_index]; +                } +                indices_16.push_back((U16)vert_index); + +                if (indices_16.size() % 3 == 0 && face_verts.size() >= VERTEX_LIMIT) +                { +                    LLVolumeFace face; +                    face.fillFromLegacyData(face_verts, indices_16); +                    face.mExtents[0] = LLVector4a(min.x, min.y, min.z, 0); +                    face.mExtents[1] = LLVector4a(max.x, max.y, max.z, 0); +                    pModel->getVolumeFaces().push_back(face); +                    pModel->getMaterialList().push_back(materialName); +                    created_faces++; + +                    std::fill(vertices_remap.begin(), vertices_remap.end(), -1); +                    indices_16.clear(); +                    face_verts.clear(); + +                    min = glm::vec3(FLT_MAX); +                    max = glm::vec3(-FLT_MAX); +                } +            } +            if (indices_16.size() > 0 && face_verts.size() > 0) +            { +                LLVolumeFace face; +                face.fillFromLegacyData(face_verts, indices_16); +                face.mExtents[0] = LLVector4a(min.x, min.y, min.z, 0); +                face.mExtents[1] = LLVector4a(max.x, max.y, max.z, 0); +                pModel->getVolumeFaces().push_back(face); +                pModel->getMaterialList().push_back(materialName); +                created_faces++; +            } + +            LL_INFOS("GLTF_IMPORT") << "Primitive " << (S32)prim_idx << " from model " << pModel->mLabel +                << " is over vertices limit, it was split into " << created_faces +                << " faces" << LL_ENDL; +            LLSD args; +            args["Message"] = "ModelSplitPrimitive"; +            args["MODEL_NAME"] = pModel->mLabel; +            args["FACE_COUNT"] = created_faces; +            mWarningsArray.append(args); +        } +        else +        { +            // can use indices directly +            std::vector<U16> indices; +            for (U32 i = 0; i < prim.getIndexCount(); i += 3) +            { +                // When processing indices, flip winding order if needed +                if (hasNegativeScale) +                { +                    // Flip winding order for negative scale +                    indices.push_back(prim.mIndexArray[i]); +                    indices.push_back(prim.mIndexArray[i + 2]); // Swap these two +                    indices.push_back(prim.mIndexArray[i + 1]); +                } +                else +                { +                    indices.push_back(prim.mIndexArray[i]); +                    indices.push_back(prim.mIndexArray[i + 1]); +                    indices.push_back(prim.mIndexArray[i + 2]); +                } +            } + +            face.fillFromLegacyData(faceVertices, indices); +            face.mExtents[0] = LLVector4a(min.x, min.y, min.z, 0); +            face.mExtents[1] = LLVector4a(max.x, max.y, max.z, 0); + +            pModel->getVolumeFaces().push_back(face); +            pModel->getMaterialList().push_back(materialName); +        } +    } + +    // Call normalizeVolumeFacesAndWeights to compute proper extents +    pModel->normalizeVolumeFacesAndWeights(); + +    // Fill joint names, bind matrices and remap weight indices +    if (skinIdx >= 0) +    { +        LL::GLTF::Skin& gltf_skin = mGLTFAsset.mSkins[skinIdx]; +        LLMeshSkinInfo& skin_info = pModel->mSkinInfo; +        S32 valid_joints_count = mValidJointsCount[skinIdx]; + +        S32 replacement_index = 0; +        std::vector<S32> gltfindex_to_joitindex_map; +        size_t jointCnt = gltf_skin.mJoints.size(); +        gltfindex_to_joitindex_map.resize(jointCnt, -1); + +        if (valid_joints_count > (S32)mMaxJointsPerMesh) +        { +            std::map<std::string, S32> goup_use_count; + +            for (const auto& elem : mJointGroups) +            { +                goup_use_count[elem.second.mGroup] = 0; +                goup_use_count[elem.second.mParentGroup] = 0; +            } + +            // Assume that 'Torso' group is always in use since that's what everything else is attached to +            goup_use_count["Torso"] = 1; +            // Note that Collisions and Extra groups are all over the place, might want to include them from the start +            // or add individual when parents are added + +            // Check which groups are in use +            for (size_t i = 0; i < jointCnt; ++i) +            { +                std::string& joint_name = mJointNames[skinIdx][i]; +                if (!joint_name.empty()) +                { +                    if (gltf_joint_index_use[i] > 0) +                    { +                        const JointGroups &group = mJointGroups[joint_name]; +                        // Joint in use, increment it's groups +                        goup_use_count[group.mGroup]++; +                        goup_use_count[group.mParentGroup]++; +                    } +                } +            } + +            // 1. add joints that are in use directly +            for (size_t i = 0; i < jointCnt; ++i) +            { +                // Process joint name and idnex +                S32 joint = gltf_skin.mJoints[i]; +                if (gltf_joint_index_use[i] <= 0) +                { +                    // unsupported (-1) joint, drop it +                    // unused (0) joint, drop it +                    continue; +                } + +                if (addJointToModelSkin(skin_info, skinIdx, i)) +                { +                    gltfindex_to_joitindex_map[i] = replacement_index++; +                } +            } + +            // 2. add joints from groups that this model's joints belong to +            // It's perfectly valid to have more joints than is in use +            // Ex: sandals that make your legs digitigrade despite not skining to +            // knees or the like. +            // Todo: sort and add by usecount +            for (size_t i = 0; i < jointCnt; ++i) +            { +                S32 joint = gltf_skin.mJoints[i]; +                if (gltf_joint_index_use[i] != 0) +                { +                    // this step needs only joints that have zero uses +                    continue; +                } +                if (skin_info.mInvBindMatrix.size() > mMaxJointsPerMesh) +                { +                    break; +                } +                const std::string& legal_name = mJointNames[skinIdx][i]; +                std::string group_name = mJointGroups[legal_name].mGroup; +                if (goup_use_count[group_name] > 0) +                { +                    if (addJointToModelSkin(skin_info, skinIdx, i)) +                    { +                        gltfindex_to_joitindex_map[i] = replacement_index++; +                    } +                } +            } +        } +        else +        { +            // Less than 110, just add every valid joint +            for (size_t i = 0; i < jointCnt; ++i) +            { +                // Process joint name and idnex +                S32 joint = gltf_skin.mJoints[i]; +                if (gltf_joint_index_use[i] < 0) +                { +                    // unsupported (-1) joint, drop it +                    continue; +                } + +                if (addJointToModelSkin(skin_info, skinIdx, i)) +                { +                    gltfindex_to_joitindex_map[i] = replacement_index++; +                } +            } +        } + +        if (skin_info.mInvBindMatrix.size() > mMaxJointsPerMesh) +        { +            // mMaxJointsPerMesh ususlly is equal to LL_MAX_JOINTS_PER_MESH_OBJECT +            // and is 110. +            LL_WARNS("GLTF_IMPORT") << "Too many jonts in " << pModel->mLabel +                << " Count: " << (S32)skin_info.mInvBindMatrix.size() +                << " Limit:" << (S32)mMaxJointsPerMesh << LL_ENDL; +            LLSD args; +            args["Message"] = "ModelTooManyJoints"; +            args["MODEL_NAME"] = pModel->mLabel; +            args["JOINT_COUNT"] = (S32)skin_info.mInvBindMatrix.size(); +            args["MAX"] = (S32)mMaxJointsPerMesh; +            mWarningsArray.append(args); +        } + +        // Remap indices for pModel->mSkinWeights +        for (auto& weights : pModel->mSkinWeights) +        { +            for (auto& weight : weights.second) +            { +                weight.mJointIdx = gltfindex_to_joitindex_map[weight.mJointIdx]; +            } +        } +    } + +    return true; +} + +void LLGLTFLoader::populateJointsFromSkin(S32 skin_idx) +{ +    const LL::GLTF::Skin& skin = mGLTFAsset.mSkins[skin_idx]; + +    LL_INFOS("GLTF_DEBUG") << "populateJointFromSkin: Processing skin " << skin_idx << " with " << skin.mJoints.size() << " joints" << LL_ENDL; + +    if (skin.mInverseBindMatrices > 0 && skin.mJoints.size() != skin.mInverseBindMatricesData.size()) +    { +        LL_INFOS("GLTF_IMPORT") << "Bind matrices count mismatch joints count" << LL_ENDL; +        LLSD args; +        args["Message"] = "InvBindCountMismatch"; +        mWarningsArray.append(args); +    } + +    S32 joint_count = (S32)skin.mJoints.size(); +    S32 inverse_count = (S32)skin.mInverseBindMatricesData.size(); +    if (mInverseBindMatrices.size() <= skin_idx) +    { +        mInverseBindMatrices.resize(skin_idx + 1); +        mAlternateBindMatrices.resize(skin_idx + 1); +        mJointNames.resize(skin_idx + 1); +        mJointUsage.resize(skin_idx + 1); +        mValidJointsCount.resize(skin_idx + 1, 0); +    } + +    // fill up joints related data +    joints_data_map_t joints_data; +    joints_name_to_node_map_t names_to_nodes; +    for (S32 i = 0; i < joint_count; i++) +    { +        S32 joint = skin.mJoints[i]; +        const LL::GLTF::Node &jointNode = mGLTFAsset.mNodes[joint]; +        JointNodeData& data = joints_data[joint]; +        data.mNodeIdx = joint; +        data.mJointListIdx = i; +        data.mGltfRestMatrix = buildGltfRestMatrix(joint, skin); +        data.mGltfMatrix = jointNode.mMatrix; +        data.mOverrideMatrix = glm::mat4(1.f); + +        if (mJointMap.find(jointNode.mName) != mJointMap.end()) +        { +            data.mName = mJointMap[jointNode.mName]; +            data.mIsValidViewerJoint = true; +            mValidJointsCount[skin_idx]++; +        } +        else +        { +            data.mName = jointNode.mName; +            data.mIsValidViewerJoint = false; +        } +        names_to_nodes[data.mName] = joint; + +        for (S32 child : jointNode.mChildren) +        { +            JointNodeData& child_data = joints_data[child]; +            child_data.mParentNodeIdx = joint; +            child_data.mIsParentValidViewerJoint = data.mIsValidViewerJoint; +        } +    } + +    // Go over viewer joints and build overrides +    // This is needed because gltf skeleton doesn't necessarily match viewer's skeleton. +    glm::mat4 ident(1.0); +    for (auto &viewer_data : mViewerJointData) +    { +        buildOverrideMatrix(viewer_data, joints_data, names_to_nodes, ident, ident); +    } + +    for (S32 i = 0; i < joint_count; i++) +    { +        S32 joint = skin.mJoints[i]; +        const LL::GLTF::Node &jointNode = mGLTFAsset.mNodes[joint]; +        std::string legal_name(jointNode.mName); + +        // Viewer supports a limited set of joints, mark them as legal +        bool legal_joint = false; +        if (mJointMap.find(legal_name) != mJointMap.end()) +        { +            legal_name = mJointMap[legal_name]; +            legal_joint = true; +            mJointNames[skin_idx].push_back(legal_name); +        } +        else +        { +            mJointNames[skin_idx].emplace_back(); +        } +        mJointUsage[skin_idx].push_back(0); + +        // Compute bind matrices + +        if (!legal_joint) +        { +            // Add placeholder to not break index. +            // Not going to be used by viewer, will be stripped from skin_info. +            LLMatrix4 gltf_transform; +            gltf_transform.setIdentity(); +            mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); +        } +        else if (inverse_count > i) +        { +            // Transalte existing bind matrix to viewer's overriden skeleton +            glm::mat4 original_bind_matrix = glm::inverse(skin.mInverseBindMatricesData[i]); +            glm::mat4 rotated_original = coord_system_rotation * original_bind_matrix; +            glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(joints_data, joint, legal_name); +            glm::mat4 tranlated_original = skeleton_transform * rotated_original; +            glm::mat4 final_inverse_bind_matrix = glm::inverse(tranlated_original); + +            LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(final_inverse_bind_matrix)); +            LL_DEBUGS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " Translated val: " << gltf_transform << LL_ENDL; +            mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); +        } +        else +        { +            // If bind matrices aren't present (they are optional in gltf), +            // assume an identy matrix +            // todo: find a model with this, might need to use YZ rotated matrix +            glm::mat4 inv_bind(1.0f); +            glm::mat4 skeleton_transform = computeGltfToViewerSkeletonTransform(joints_data, joint, legal_name); +            inv_bind = glm::inverse(skeleton_transform * inv_bind); + +            LLMatrix4 gltf_transform = LLMatrix4(glm::value_ptr(inv_bind)); +            LL_DEBUGS("GLTF_DEBUG") << "mInvBindMatrix name: " << legal_name << " Generated val: " << gltf_transform << LL_ENDL; +            mInverseBindMatrices[skin_idx].push_back(LLMatrix4a(gltf_transform)); +        } + +        // Compute Alternative matrices also known as overrides +        LLMatrix4 original_joint_transform(glm::value_ptr(joints_data[joint].mOverrideMatrix)); + +        // Viewer seems to care only about translation part, +        // but for parity with collada taking original value +        LLMatrix4 newInverse = LLMatrix4(mInverseBindMatrices[skin_idx].back().getF32ptr()); +        newInverse.setTranslation(original_joint_transform.getTranslation()); + +        LL_DEBUGS("GLTF_DEBUG") << "mAlternateBindMatrix name: " << legal_name << " val: " << newInverse << LL_ENDL; +        mAlternateBindMatrices[skin_idx].push_back(LLMatrix4a(newInverse)); + +        if (legal_joint) +        { +            // Might be needed for uploader UI to correctly identify overriden joints +            // but going to be incorrect if multiple skins are present +            mJointList[legal_name] = newInverse; +            mJointsFromNode.push_front(legal_name); +        } +    } + +    S32 valid_joints = mValidJointsCount[skin_idx]; +    if (valid_joints < joint_count) +    { +        LL_INFOS("GLTF_IMPORT") << "Skin " << skin_idx +            << " defines " << joint_count +            << " joints, but only " << valid_joints +            << " were recognized and are compatible." << LL_ENDL; +        LLSD args; +        args["Message"] = "SkinUsupportedJoints"; +        args["SKIN_INDEX"] = skin_idx; +        args["JOINT_COUNT"] = joint_count; +        args["LEGAL_COUNT"] = valid_joints; +        mWarningsArray.append(args); +    } +} + +void LLGLTFLoader::populateJointGroups() +{ +    std::string parent; +    for (auto& viewer_data : mViewerJointData) +    { +        buildJointGroup(viewer_data, parent); +    } +} + +void LLGLTFLoader::buildJointGroup(LLJointData& viewer_data, const std::string &parent_group) +{ +    JointGroups& jount_group_data = mJointGroups[viewer_data.mName]; +    jount_group_data.mGroup = viewer_data.mGroup; +    jount_group_data.mParentGroup = parent_group; + +    for (LLJointData& child_data : viewer_data.mChildren) +    { +        buildJointGroup(child_data, viewer_data.mGroup); +    } +} + +void LLGLTFLoader::buildOverrideMatrix(LLJointData& viewer_data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& parent_support_rest) const +{ +    glm::mat4 rest(1.f); +    joints_name_to_node_map_t::iterator found_node = names_to_nodes.find(viewer_data.mName); +    if (found_node != names_to_nodes.end()) +    { +        S32 gltf_node_idx = found_node->second; +        JointNodeData& node = gltf_nodes[gltf_node_idx]; +        node.mIsOverrideValid = true; +        node.mViewerRestMatrix = viewer_data.mRestMatrix; + +        glm::mat4 gltf_joint_rest_pose = coord_system_rotation * node.mGltfRestMatrix; +        if (mApplyXYRotation) +        { +            gltf_joint_rest_pose = coord_system_rotationxy * gltf_joint_rest_pose; +        } + +        glm::mat4 translated_joint; +        // Example: +        // Viewer has pelvis->spine1->spine2->torso. +        // gltf example model has pelvis->torso +        // By doing glm::inverse(transalted_rest_spine2) * gltf_rest_torso +        // We get what torso would have looked like if gltf had a spine2 +        if (viewer_data.mIsJoint) +        { +            translated_joint = glm::inverse(parent_rest) * gltf_joint_rest_pose; +        } +        else +        { +            translated_joint = glm::inverse(parent_support_rest) * gltf_joint_rest_pose; +        } + +        glm::vec3 translation_override; +        glm::vec3 skew; +        glm::vec3 scale; +        glm::vec4 perspective; +        glm::quat rotation; +        glm::decompose(translated_joint, scale, rotation, translation_override, skew, perspective); + +        // Viewer allows overrides, which are base joint with applied translation override. +        // fortunately normal bones use only translation, without rotation or scale +        node.mOverrideMatrix = glm::recompose(glm::vec3(1, 1, 1), glm::identity<glm::quat>(), translation_override, glm::vec3(0, 0, 0), glm::vec4(0, 0, 0, 1)); + +        glm::mat4 overriden_joint = node.mOverrideMatrix; + +        // todo: if gltf bone had rotation or scale, they probably should be saved here +        // then applied to bind matrix +        rest = parent_rest * overriden_joint; +        if (viewer_data.mIsJoint) +        { +            node.mOverrideRestMatrix = rest; +        } +        else +        { +            // This is likely incomplete or even wrong. +            // Viewer Collision bones specify rotation and scale. +            // Importer should apply rotation and scale to this matrix and save as needed +            // then subsctruct them from bind matrix +            // Todo: get models that use collision bones, made by different programs + +            overriden_joint = glm::scale(overriden_joint, viewer_data.mScale); +            node.mOverrideRestMatrix = parent_support_rest * overriden_joint; +        } +    } +    else +    { +        // No override for this joint +        rest = parent_rest * viewer_data.mJointMatrix; +    } + +    glm::mat4 support_rest(1.f); +    if (viewer_data.mSupport == LLJointData::SUPPORT_BASE) +    { +        support_rest = rest; +    } +    else +    { +        support_rest = parent_support_rest; +    } + +    for (LLJointData& child_data : viewer_data.mChildren) +    { +        buildOverrideMatrix(child_data, gltf_nodes, names_to_nodes, rest, support_rest); +    } +} + +glm::mat4 LLGLTFLoader::buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const +{ +    // This is inefficient since we are recalculating some joints multiple times over +    // Todo: cache it? + +    if (joint_node_index < 0 || joint_node_index >= static_cast<S32>(mGLTFAsset.mNodes.size())) +    { +        return glm::mat4(1.0f); +    } + +    const auto& node = mGLTFAsset.mNodes[joint_node_index]; + +    // Find and apply parent transform if it exists +    for (size_t i = 0; i < mGLTFAsset.mNodes.size(); ++i) +    { +        const auto& potential_parent = mGLTFAsset.mNodes[i]; +        auto it = std::find(potential_parent.mChildren.begin(), potential_parent.mChildren.end(), joint_node_index); + +        if (it != potential_parent.mChildren.end()) +        { +            // Found parent +            if (std::find(gltf_skin.mJoints.begin(), gltf_skin.mJoints.end(), joint_node_index) != gltf_skin.mJoints.end()) +            { +                // parent is a joint - recursively combine transform +                // assumes that matrix is already valid +                return buildGltfRestMatrix(static_cast<S32>(i), gltf_skin) * node.mMatrix; +            } +        } +    } +    // Should we return armature or stop earlier? +    return node.mMatrix; +} + +glm::mat4 LLGLTFLoader::buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const +{ +    // This is inefficient since we are recalculating some joints multiple times over +    // Todo: cache it? + +    if (joint_node_index < 0 || joint_node_index >= static_cast<S32>(mGLTFAsset.mNodes.size())) +    { +        return glm::mat4(1.0f); +    } + +    auto& data = joint_data.at(joint_node_index); + +    if (data.mParentNodeIdx >=0) +    { +        return buildGltfRestMatrix(data.mParentNodeIdx, joint_data) * data.mGltfMatrix; +    } +    // Should we return armature or stop earlier? +    return data.mGltfMatrix; +} + +// This function computes the transformation matrix needed to convert from GLTF skeleton space +// to viewer skeleton space for a specific joint + +glm::mat4 LLGLTFLoader::computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const +{ +    const JointNodeData& node_data = joints_data_map.at(gltf_node_index); +    if (!node_data.mIsOverrideValid) +    { +        // For now assume they are identical and return an identity (for ease of debuging) +        return glm::mat4(1.0f); +    } + +    // Get the GLTF joint's rest pose (in GLTF coordinate system) +    const glm::mat4 &gltf_joint_rest_pose = node_data.mGltfRestMatrix; +    glm::mat4 rest_pose = coord_system_rotation * gltf_joint_rest_pose; + +    LL_INFOS("GLTF_DEBUG") << "rest matrix for joint " << joint_name << ": "; + +    LLMatrix4 transform(glm::value_ptr(rest_pose)); + +    LL_CONT << transform << LL_ENDL; + +    // Compute transformation from GLTF space to viewer space +    // This assumes both skeletons are in rest pose initially +    return node_data.mOverrideRestMatrix * glm::inverse(rest_pose); +} + +bool LLGLTFLoader::checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx) +{ +    glm::mat4 gltf_joint_rest = buildGltfRestMatrix(joint_idx, gltf_skin); +    glm::mat4 test_mat = glm::inverse(gltf_joint_rest) * gltf_skin.mInverseBindMatricesData[bind_indx]; +    // Normally for shoulders it should be something close to +    // {1,0,0,0;0,-1,0,0;0,0,-1,0;0,0,0,1} +    // rotated one will look like +    // {0,0,0,-1;1,0,0,0;0,-1,0,0;0,0,0,1} +    // Todo: This is a cheap hack, +    // figure out how rotation is supposed to work +    return abs(test_mat[0][0]) < 0.5 && abs(test_mat[1][1]) < 0.5 && abs(test_mat[2][2]) < 0.5; +} + +void LLGLTFLoader::checkForXYrotation(const LL::GLTF::Skin& gltf_skin) +{ +    // HACK: figure out model's rotation from shoulders' matrix. +    // This is wrong on many levels: +    // Too limited (only models that have shoulders), +    // Will not work well with things that emulate 3 hands in some manner +    // Only supports xy 90 degree rotation +    // Todo: figure out how to find skeleton's orientation Correctly +    // when model is rotated at a triangle level +    constexpr char right_shoulder_str[] = "mShoulderRight"; +    constexpr char left_shoulder_str[] = "mShoulderLeft"; + +    S32 size = (S32)gltf_skin.mJoints.size(); +    S32 joints_found = 0; +    for (S32 i= 0; i < size; i++) +    { +        S32 joint = gltf_skin.mJoints[i]; +        const LL::GLTF::Node &joint_node = mGLTFAsset.mNodes[joint]; + +        // todo: we are doing this search thing everywhere, +        // just pre-translate every joint +        JointMap::iterator found = mJointMap.find(joint_node.mName); +        if (found == mJointMap.end()) +        { +            // unsupported joint +            continue; +        } +        if (found->second == right_shoulder_str || found->second == left_shoulder_str) +        { +            if (checkForXYrotation(gltf_skin, joint, i)) +            { +                joints_found++; +            } +            else +            { +                return; +            } +        } +    } + +    if (joints_found == 2) +    { +        // Both joints in a weird position/rotation, assume rotated model +        mApplyXYRotation = true; +    } +} + +void LLGLTFLoader::checkGlobalJointUsage() +{ +    // Check if some joints remained unused +    for (S32 skin_idx = 0; skin_idx < (S32)mGLTFAsset.mSkins.size(); ++skin_idx) +    { +        const LL::GLTF::Skin& gltf_skin = mGLTFAsset.mSkins[skin_idx]; +        S32 joint_count = (S32)gltf_skin.mJoints.size(); +        S32 used_joints = 0; +        for (S32 i = 0; i < joint_count; ++i) +        { +            S32 joint = gltf_skin.mJoints[i]; +            if (mJointUsage[skin_idx][i] == 0) +            { +                // Joint is unused, log it +                LL_INFOS("GLTF_DEBUG") << "Joint " << mJointNames[skin_idx][i] +                    << " in skin " << skin_idx << " is unused." << LL_ENDL; +            } +            else +            { +                used_joints++; +            } +        } + +        S32 valid_joints = mValidJointsCount[skin_idx]; +        if (valid_joints > used_joints) +        { +            S32 unsed_joints = valid_joints - used_joints; +            LL_INFOS("GLTF_IMPORT") << "Skin " << skin_idx +                << " declares " << valid_joints +                << " valid joints, of them " << unsed_joints +                << " remained unused" << LL_ENDL; +            LLSD args; +            args["Message"] = "SkinUnusedJoints"; +            args["SKIN_INDEX"] = (S32)skin_idx; +            args["JOINT_COUNT"] = valid_joints; +            args["USED_COUNT"] = used_joints; +            mWarningsArray.append(args); +        } +    } +} + +std::string LLGLTFLoader::extractTextureToTempFile(S32 textureIndex, const std::string& texture_type) +{ +    if (textureIndex < 0 || textureIndex >= mGLTFAsset.mTextures.size()) +        return ""; + +    S32 sourceIndex = mGLTFAsset.mTextures[textureIndex].mSource; +    if (sourceIndex < 0 || sourceIndex >= mGLTFAsset.mImages.size()) +        return ""; + +    LL::GLTF::Image& image = mGLTFAsset.mImages[sourceIndex]; + +    // Handle URI-based textures +    if (!image.mUri.empty()) +    { +        return image.mUri; // Return URI directly +    } + +    // Handle embedded textures +    if (image.mBufferView >= 0) +    { +        if (image.mBufferView < mGLTFAsset.mBufferViews.size()) +        { +            const LL::GLTF::BufferView& buffer_view = mGLTFAsset.mBufferViews[image.mBufferView]; +            if (buffer_view.mBuffer < mGLTFAsset.mBuffers.size()) +            { +                const LL::GLTF::Buffer& buffer = mGLTFAsset.mBuffers[buffer_view.mBuffer]; + +                if (buffer_view.mByteOffset + buffer_view.mByteLength <= buffer.mData.size()) +                { +                    // Extract image data +                    const U8* data_ptr = &buffer.mData[buffer_view.mByteOffset]; +                    U32 data_size = buffer_view.mByteLength; + +                    // Determine the file extension +                    std::string extension = ".png"; // Default +                    if (!image.mMimeType.empty()) +                    { +                        if (image.mMimeType == "image/jpeg") +                            extension = ".jpg"; +                        else if (image.mMimeType == "image/png") +                            extension = ".png"; +                    } +                    else if (data_size >= 4) +                    { +                        if (data_ptr[0] == 0xFF && data_ptr[1] == 0xD8) +                            extension = ".jpg"; // JPEG magic bytes +                        else if (data_ptr[0] == 0x89 && data_ptr[1] == 0x50 && data_ptr[2] == 0x4E && data_ptr[3] == 0x47) +                            extension = ".png"; // PNG magic bytes +                    } + +                    // Create a temporary file +                    std::string temp_dir = gDirUtilp->getTempDir(); +                    std::string temp_filename = temp_dir + gDirUtilp->getDirDelimiter() + +                                               "gltf_embedded_" + texture_type + "_" + std::to_string(sourceIndex) + extension; + +                    // Write the image data to the temporary file +                    std::ofstream temp_file(temp_filename, std::ios::binary); +                    if (temp_file.is_open()) +                    { +                        temp_file.write(reinterpret_cast<const char*>(data_ptr), data_size); +                        temp_file.close(); + +                        LL_INFOS("GLTF_IMPORT") << "Extracted embedded " << texture_type << " texture to: " << temp_filename << LL_ENDL; +                        return temp_filename; +                    } +                    else +                    { +                        LL_WARNS("GLTF_IMPORT") << "Failed to create temporary file for " << texture_type << " texture: " << temp_filename << LL_ENDL; + +                        LLSD args; +                        args["Message"] = "FailedToCreateTempFile"; +                        args["TEXTURE_INDEX"] = sourceIndex; +                        args["TEXTURE_TYPE"]  = texture_type; +                        args["TEMP_FILE"] = temp_filename; +                        mWarningsArray.append(args); +                    } +                } +            } +        } +    } + +    return ""; +} + +void LLGLTFLoader::notifyUnsupportedExtension(bool unsupported) +{ +    std::vector<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); + +        LL_WARNS("GLTF_IMPORT") << "Model uses unsupported extension: " << ext << LL_ENDL; +    } +} + +size_t LLGLTFLoader::getSuffixPosition(const std::string &label) +{ +    if ((label.find("_LOD") != -1) || (label.find("_PHYS") != -1)) +    { +        return label.rfind('_'); +    } +    return -1; +} + +std::string LLGLTFLoader::getLodlessLabel(const LL::GLTF::Mesh& mesh) +{ +    size_t ext_pos = getSuffixPosition(mesh.mName); +    if (ext_pos != -1) +    { +        return mesh.mName.substr(0, ext_pos); +    } +    return mesh.mName; +} + diff --git a/indra/newview/gltf/llgltfloader.h b/indra/newview/gltf/llgltfloader.h new file mode 100644 index 0000000000..e8b91996c7 --- /dev/null +++ b/indra/newview/gltf/llgltfloader.h @@ -0,0 +1,216 @@ +/** + * @file LLGLTFLoader.h + * @brief LLGLTFLoader class definition + * + * $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$ + */ + +#ifndef LL_LLGLTFLoader_H +#define LL_LLGLTFLoader_H + +#include "tinygltf/tiny_gltf.h" + +#include "asset.h" + +#include "llglheaders.h" +#include "lljointdata.h" +#include "llmodelloader.h" + +class LLGLTFLoader : public LLModelLoader +{ +  public: +    typedef std::map<std::string, LLImportMaterial> material_map; +    typedef std::map<std::string, std::string> joint_viewer_parent_map_t; +    typedef std::map<std::string, glm::mat4> joint_viewer_rest_map_t; +    typedef std::map<S32, glm::mat4> joint_node_mat4_map_t; + +    struct JointNodeData +    { +        JointNodeData() +            : mJointListIdx(-1) +            , mNodeIdx(-1) +            , mParentNodeIdx(-1) +            , mIsValidViewerJoint(false) +            , mIsParentValidViewerJoint(false) +            , mIsOverrideValid(false) +        { + +        } +        S32 mJointListIdx; +        S32 mNodeIdx; +        S32 mParentNodeIdx; +        glm::mat4 mGltfRestMatrix; +        glm::mat4 mViewerRestMatrix; +        glm::mat4 mOverrideRestMatrix; +        glm::mat4 mGltfMatrix; +        glm::mat4 mOverrideMatrix; +        std::string mName; +        bool mIsValidViewerJoint; +        bool mIsParentValidViewerJoint; +        bool mIsOverrideValid; +    }; +    typedef std::map <S32, JointNodeData> joints_data_map_t; +    typedef std::map <std::string, S32> joints_name_to_node_map_t; + +    class LLGLTFImportMaterial : public LLImportMaterial +    { +    public: +        std::string name; +        LLGLTFImportMaterial() = default; +        LLGLTFImportMaterial(const LLImportMaterial& mat, const std::string& n) : LLImportMaterial(mat), name(n) {} +    }; + +    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, std::less<>> & jointAliasMap, +                    U32                                               maxJointsPerMesh, +                    U32                                               modelLimit, +                    U32                                               debugMode, +                    std::vector<LLJointData>                          viewer_skeleton); //, +                    //bool                                            preprocess ); +    virtual ~LLGLTFLoader(); + +    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 = false; +    bool            mApplyXYRotation = false; + +    // GLTF isn't aware of viewer's skeleton and uses it's own, +    // so need to take viewer's joints and use them to +    // recalculate iverse bind matrices +    std::vector<LLJointData>             mViewerJointData; + +    // vector of vectors because of a posibility of having more than one skin +    typedef std::vector<LLMeshSkinInfo::matrix_list_t> bind_matrices_t; +    typedef std::vector<std::vector<std::string> > joint_names_t; +    bind_matrices_t                     mInverseBindMatrices; +    bind_matrices_t                     mAlternateBindMatrices; +    joint_names_t                       mJointNames; // empty string when no legal name for a given idx +    std::vector<std::vector<S32>>       mJointUsage; // detect and warn about unsed joints + +    // what group a joint belongs to. +    // For purpose of stripping unused groups when joints are over limit. +    struct JointGroups +    { +        std::string mGroup; +        std::string mParentGroup; +    }; +    typedef std::map<std::string, JointGroups, std::less<> > joint_to_group_map_t; +    joint_to_group_map_t mJointGroups; + +    // per skin joint count, needs to be tracked for the sake of limits check. +    std::vector<S32>                    mValidJointsCount; + +    // Cached material information +    typedef std::map<S32, LLGLTFImportMaterial> MaterialCache; +    MaterialCache mMaterialCache; + +private: +    bool parseMeshes(); +    void computeCombinedNodeTransform(const LL::GLTF::Asset& asset, S32 node_index, glm::mat4& combined_transform) const; +    void processNodeHierarchy(S32 node_idx, std::map<std::string, S32>& mesh_name_counts, U32 submodel_limit, const LLVolumeParams& volume_params); +    bool addJointToModelSkin(LLMeshSkinInfo& skin_info, S32 gltf_skin_idx, size_t gltf_joint_idx); +    LLGLTFImportMaterial processMaterial(S32 material_index, S32 fallback_index); +    std::string processTexture(S32 texture_index, const std::string& texture_type, const std::string& material_name); +    bool validateTextureIndex(S32 texture_index, S32& source_index); +    std::string generateMaterialName(S32 material_index, S32 fallback_index = -1); +    bool populateModelFromMesh(LLModel* pModel, const std::string& base_name, const LL::GLTF::Mesh &mesh, const LL::GLTF::Node &node, material_map& mats); +    void populateJointsFromSkin(S32 skin_idx); +    void populateJointGroups(); +    void addModelToScene(LLModel* pModel, const std::string& model_name, U32 submodel_limit, const LLMatrix4& transformation, const LLVolumeParams& volume_params, const material_map& mats); +    void buildJointGroup(LLJointData& viewer_data, const std::string& parent_group); +    void buildOverrideMatrix(LLJointData& data, joints_data_map_t &gltf_nodes, joints_name_to_node_map_t &names_to_nodes, glm::mat4& parent_rest, glm::mat4& support_rest) const; +    glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const LL::GLTF::Skin& gltf_skin) const; +    glm::mat4 buildGltfRestMatrix(S32 joint_node_index, const joints_data_map_t& joint_data) const; +    glm::mat4 computeGltfToViewerSkeletonTransform(const joints_data_map_t& joints_data_map, S32 gltf_node_index, const std::string& joint_name) const; +    bool checkForXYrotation(const LL::GLTF::Skin& gltf_skin, S32 joint_idx, S32 bind_indx); +    void checkForXYrotation(const LL::GLTF::Skin& gltf_skin); +    void checkGlobalJointUsage(); + +    std::string extractTextureToTempFile(S32 textureIndex, const std::string& texture_type); + +    void notifyUnsupportedExtension(bool unsupported); + +    static size_t getSuffixPosition(const std::string& label); +    static std::string getLodlessLabel(const LL::GLTF::Mesh& mesh); + +    //    bool mPreprocessGLTF; + +    /*  Below inherited from dae loader - unknown if/how useful here + +    void processElement(gltfElement *element, bool &badElement, GLTF *gltf); +    void processGltfModel(LLModel *model, GLTF *gltf, gltfElement *pRoot, gltfMesh *mesh, gltfSkin *skin); + +    material_map     getMaterials(LLModel *model, gltfInstance_geometry *instance_geo, GLTF *gltf); +    LLImportMaterial profileToMaterial(gltfProfile_COMMON *material, GLTF *gltf); +    LLColor4         getGltfColor(gltfElement *element); + +    gltfElement *getChildFromElement(gltfElement *pElement, std::string const &name); + +    bool isNodeAJoint(gltfNode *pNode); +    void processJointNode(gltfNode *pNode, std::map<std::string, LLMatrix4> &jointTransforms); +    void extractTranslation(gltfTranslate *pTranslate, LLMatrix4 &transform); +    void extractTranslationViaElement(gltfElement *pTranslateElement, LLMatrix4 &transform); +    void extractTranslationViaSID(gltfElement *pElement, LLMatrix4 &transform); +    void buildJointToNodeMappingFromScene(gltfElement *pRoot); +    void processJointToNodeMapping(gltfNode *pNode); +    void processChildJoints(gltfNode *pParentNode); + +    bool verifyCount(int expected, int result); + +    // Verify that a controller matches vertex counts +    bool verifyController(gltfController *pController); + +    static bool addVolumeFacesFromGltfMesh(LLModel *model, gltfMesh *mesh, LLSD &log_msg); +    static bool createVolumeFacesFromGltfMesh(LLModel *model, gltfMesh *mesh); + +    static LLModel *loadModelFromGltfMesh(gltfMesh *mesh); + +    // Loads a mesh breaking it into one or more models as necessary +    // to get around volume face limitations while retaining >8 materials +    // +    bool loadModelsFromGltfMesh(gltfMesh *mesh, std::vector<LLModel *> &models_out, U32 submodel_limit); + +    static std::string preprocessGLTF(std::string filename); +    */ + +}; +#endif  // LL_LLGLTFLLOADER_H diff --git a/indra/newview/gltf/primitive.cpp b/indra/newview/gltf/primitive.cpp index 81caff8ab2..388a6eee01 100644 --- a/indra/newview/gltf/primitive.cpp +++ b/indra/newview/gltf/primitive.cpp @@ -32,7 +32,7 @@  #include "mikktspace/mikktspace.hh" -#include "meshoptimizer/meshoptimizer.h" +#include <meshoptimizer.h>  using namespace LL::GLTF;  | 
