/** * @file lllocalrendermaterials.cpp * @brief Local GLTF materials source * * $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$ */ /* precompiled headers */ #include "llviewerprecompiledheaders.h" /* own header */ #include "lllocalgltfmaterials.h" /* boost: will not compile unless equivalent is undef'd, beware. */ #include "fix_macros.h" #include /* time headers */ #include #include /* misc headers */ #include "llgltfmateriallist.h" #include "llimage.h" #include "llinventoryicon.h" #include "llmaterialmgr.h" #include "llnotificationsutil.h" #include "llscrolllistctrl.h" #include "lltextureentry.h" #include "lltinygltfhelper.h" #include "llviewertexture.h" /*=======================================*/ /* Formal declarations, constants, etc. */ /*=======================================*/ static const F32 LL_LOCAL_TIMER_HEARTBEAT = 3.0; static const S32 LL_LOCAL_UPDATE_RETRIES = 5; /*=======================================*/ /* LLLocalGLTFMaterial: unit class */ /*=======================================*/ LLLocalGLTFMaterial::LLLocalGLTFMaterial(std::string filename, S32 index) : mFilename(filename) , mShortName(gDirUtilp->getBaseFileName(filename, true)) , mLastModified() , mLinkStatus(LS_ON) , mUpdateRetries(LL_LOCAL_UPDATE_RETRIES) , mMaterialIndex(index) { mTrackingID.generate(); /* extension */ std::string temp_exten = gDirUtilp->getExtension(mFilename); if (temp_exten == "gltf") { mExtension = ET_MATERIAL_GLTF; } else if (temp_exten == "glb") { mExtension = ET_MATERIAL_GLB; } else { LL_WARNS("GLTF") << "File of no valid extension given, local material creation aborted." << "\n" << "Filename: " << mFilename << LL_ENDL; return; // no valid extension. } } LLLocalGLTFMaterial::~LLLocalGLTFMaterial() { // gGLTFMaterialList will clean itself } /* accessors */ std::string LLLocalGLTFMaterial::getFilename() const { return mFilename; } std::string LLLocalGLTFMaterial::getShortName() const { return mShortName; } LLUUID LLLocalGLTFMaterial::getTrackingID() const { return mTrackingID; } LLUUID LLLocalGLTFMaterial::getWorldID() const { return mWorldID; } S32 LLLocalGLTFMaterial::getIndexInFile() const { return mMaterialIndex; } /* update functions */ bool LLLocalGLTFMaterial::updateSelf() { bool updated = false; if (mLinkStatus == LS_ON) { // verifying that the file exists if (gDirUtilp->fileExists(mFilename)) { // verifying that the file has indeed been modified #ifndef LL_WINDOWS const std::time_t temp_time = boost::filesystem::last_write_time(boost::filesystem::path(mFilename)); #else const std::time_t temp_time = boost::filesystem::last_write_time(boost::filesystem::path(utf8str_to_utf16str(mFilename))); #endif LLSD new_last_modified = asctime(localtime(&temp_time)); if (mLastModified.asString() != new_last_modified.asString()) { if (loadMaterial()) { // decode is successful, we can safely proceed. if (mWorldID.isNull()) { mWorldID.generate(); } mLastModified = new_last_modified; // addMaterial will replace material witha a new // pointer if value already exists but we are // reusing existing pointer, so it should add only. gGLTFMaterialList.addMaterial(mWorldID, this); mUpdateRetries = LL_LOCAL_UPDATE_RETRIES; for (LLTextureEntry* entry : mTextureEntires) { // Normally a change in applied material id is supposed to // drop overrides thus reset material, but local materials // currently reuse their existing asset id, and purpose is // to preview how material will work in-world, overrides // included, so do an override to render update instead. LLGLTFMaterial* override_mat = entry->getGLTFMaterialOverride(); if (override_mat) { // do not create a new material, reuse existing pointer LLFetchedGLTFMaterial* render_mat = (LLFetchedGLTFMaterial*)entry->getGLTFRenderMaterial(); if (render_mat) { llassert(dynamic_cast(entry->getGLTFRenderMaterial()) != nullptr); *render_mat = *this; render_mat->applyOverride(*override_mat); } } } materialBegin(); materialComplete(true); updated = true; } // if decoding failed, we get here and it will attempt to decode it in the next cycles // until mUpdateRetries runs out. this is done because some software lock the material while writing to it else { if (mUpdateRetries) { mUpdateRetries--; } else { LL_WARNS("GLTF") << "During the update process the following file was found" << "\n" << "but could not be opened or decoded for " << LL_LOCAL_UPDATE_RETRIES << " attempts." << "\n" << "Filename: " << mFilename << "\n" << "Disabling further update attempts for this file." << LL_ENDL; LLSD notif_args; notif_args["FNAME"] = mFilename; notif_args["NRETRIES"] = LL_LOCAL_UPDATE_RETRIES; LLNotificationsUtil::add("LocalBitmapsUpdateFailedFinal", notif_args); mLinkStatus = LS_BROKEN; materialBegin(); materialComplete(false); } } } } // end if file exists else { LL_WARNS("GLTF") << "During the update process, the following file was not found." << "\n" << "Filename: " << mFilename << "\n" << "Disabling further update attempts for this file." << LL_ENDL; LLSD notif_args; notif_args["FNAME"] = mFilename; LLNotificationsUtil::add("LocalBitmapsUpdateFileNotFound", notif_args); mLinkStatus = LS_BROKEN; materialBegin(); materialComplete(false); } } return updated; } bool LLLocalGLTFMaterial::loadMaterial() { bool decode_successful = false; switch (mExtension) { case ET_MATERIAL_GLTF: case ET_MATERIAL_GLB: { std::string filename_lc = mFilename; LLStringUtil::toLower(filename_lc); std::string material_name; tinygltf::Model model; decode_successful = LLTinyGLTFHelper::loadModel(mFilename, model); if (decode_successful) { // Might be a good idea to make these textures into local textures decode_successful = LLTinyGLTFHelper::getMaterialFromModel( mFilename, model, mMaterialIndex, this, material_name); } if (!material_name.empty()) { mShortName = gDirUtilp->getBaseFileName(filename_lc, true) + " (" + material_name + ")"; } break; } default: { // separating this into -several- LL_WARNS() calls because in the extremely unlikely case that this happens // accessing mFilename and any other object properties might very well crash the viewer. // getting here should be impossible, or there's been a pretty serious bug. LL_WARNS("GLTF") << "During a decode attempt, the following local material had no properly assigned extension." << LL_ENDL; LL_WARNS("GLTF") << "Filename: " << mFilename << LL_ENDL; LL_WARNS("GLTF") << "Disabling further update attempts for this file." << LL_ENDL; mLinkStatus = LS_BROKEN; } } return decode_successful; } /*=======================================*/ /* LLLocalGLTFMaterialTimer: timer class */ /*=======================================*/ LLLocalGLTFMaterialTimer::LLLocalGLTFMaterialTimer() : LLEventTimer(LL_LOCAL_TIMER_HEARTBEAT) { } LLLocalGLTFMaterialTimer::~LLLocalGLTFMaterialTimer() { } void LLLocalGLTFMaterialTimer::startTimer() { mEventTimer.start(); } void LLLocalGLTFMaterialTimer::stopTimer() { mEventTimer.stop(); } bool LLLocalGLTFMaterialTimer::isRunning() { return mEventTimer.getStarted(); } bool LLLocalGLTFMaterialTimer::tick() { // todo: do on idle? No point in timer LLLocalGLTFMaterialMgr::getInstance()->doUpdates(); return false; } /*=======================================*/ /* LLLocalGLTFMaterialMgr: manager class */ /*=======================================*/ LLLocalGLTFMaterialMgr::LLLocalGLTFMaterialMgr() { } LLLocalGLTFMaterialMgr::~LLLocalGLTFMaterialMgr() { mMaterialList.clear(); } S32 LLLocalGLTFMaterialMgr::addUnit(const std::vector& filenames) { S32 add_count = 0; std::vector::const_iterator iter = filenames.begin(); while (iter != filenames.end()) { if (!iter->empty()) { add_count += addUnit(*iter); } iter++; } return add_count; } S32 LLLocalGLTFMaterialMgr::addUnit(const std::string& filename) { tinygltf::Model model; LLTinyGLTFHelper::loadModel(filename, model); auto materials_in_file = model.materials.size(); if (materials_in_file <= 0) { return 0; } S32 loaded_materials = 0; for (size_t i = 0; i < materials_in_file; i++) { // Todo: this is rather inefficient, files will be spammed with // separate loads and date checks, find a way to improve this. // May be doUpdates() should be checking individual files. LLPointer unit = new LLLocalGLTFMaterial(filename, static_cast(i)); // load material from file if (unit->updateSelf()) { mMaterialList.emplace_back(unit); loaded_materials++; } else { LL_WARNS("GLTF") << "Attempted to add invalid or unreadable image file, attempt cancelled.\n" << "Filename: " << filename << LL_ENDL; LLSD notif_args; notif_args["FNAME"] = filename; LLNotificationsUtil::add("LocalGLTFVerifyFail", notif_args); unit = NULL; } } return loaded_materials; } void LLLocalGLTFMaterialMgr::delUnit(LLUUID tracking_id) { if (!mMaterialList.empty()) { std::vector to_delete; for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { /* finding which ones we want deleted and making a separate list */ LLLocalGLTFMaterial* unit = *iter; if (unit->getTrackingID() == tracking_id) { to_delete.push_back(unit); } } for (std::vector::iterator del_iter = to_delete.begin(); del_iter != to_delete.end(); del_iter++) { /* iterating over a temporary list, hence preserving the iterator validity while deleting. */ LLLocalGLTFMaterial* unit = *del_iter; mMaterialList.remove(unit); unit = NULL; } } } LLUUID LLLocalGLTFMaterialMgr::getWorldID(LLUUID tracking_id) { LLUUID world_id = LLUUID::null; for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { LLLocalGLTFMaterial* unit = *iter; if (unit->getTrackingID() == tracking_id) { world_id = unit->getWorldID(); } } return world_id; } bool LLLocalGLTFMaterialMgr::isLocal(const LLUUID world_id) { for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { LLLocalGLTFMaterial* unit = *iter; if (unit->getWorldID() == world_id) { return true; } } return false; } void LLLocalGLTFMaterialMgr::getFilenameAndIndex(LLUUID tracking_id, std::string &filename, S32 &index) { filename = ""; index = 0; for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { LLLocalGLTFMaterial* unit = *iter; if (unit->getTrackingID() == tracking_id) { filename = unit->getFilename(); index = unit->getIndexInFile(); } } } // probably shouldn't be here, but at the moment this mirrors lllocalbitmaps void LLLocalGLTFMaterialMgr::feedScrollList(LLScrollListCtrl* ctrl) { if (ctrl) { if (!mMaterialList.empty()) { std::string icon_name = LLInventoryIcon::getIconName( LLAssetType::AT_MATERIAL, LLInventoryType::IT_NONE); for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { LLSD element; element["columns"][0]["column"] = "icon"; element["columns"][0]["type"] = "icon"; element["columns"][0]["value"] = icon_name; element["columns"][1]["column"] = "unit_name"; element["columns"][1]["type"] = "text"; element["columns"][1]["value"] = (*iter)->getShortName(); LLSD data; data["id"] = (*iter)->getTrackingID(); data["type"] = (S32)LLAssetType::AT_MATERIAL; element["value"] = data; ctrl->addElement(element); } } } } void LLLocalGLTFMaterialMgr::doUpdates() { // preventing theoretical overlap in cases with huge number of loaded images. mTimer.stopTimer(); for (local_list_iter iter = mMaterialList.begin(); iter != mMaterialList.end(); iter++) { (*iter)->updateSelf(); } mTimer.startTimer(); }