phoenix-firestorm/indra/newview/llmaterialeditor.cpp

3130 lines
102 KiB
C++

/**
* @file llmaterialeditor.cpp
* @brief Implementation of the gltf material editor
*
* $LicenseInfo:firstyear=2022&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2010, 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 "llviewerprecompiledheaders.h"
#include "llmaterialeditor.h"
#include "llagent.h"
#include "llagentbenefits.h"
#include "llappviewer.h"
#include "llcolorswatch.h"
#include "llcombobox.h"
#include "llfloaterreg.h"
#include "llfilesystem.h"
#include "llgltfmateriallist.h"
#include "llinventorymodel.h"
#include "lllocalgltfmaterials.h"
#include "llnotificationsutil.h"
#include "lltexturectrl.h"
#include "lltrans.h"
#include "llviewermenufile.h"
#include "llviewertexture.h"
#include "llsdutil.h"
#include "llselectmgr.h"
#include "llstatusbar.h" // can_afford_transaction()
#include "lltoolpie.h"
#include "llviewerinventory.h"
#include "llinventory.h"
#include "llviewerregion.h"
#include "llvovolume.h"
#include "roles_constants.h"
#include "llviewerobjectlist.h"
#include "llsdserialize.h"
#include "llimagej2c.h"
#include "llviewertexturelist.h"
#include "llfloaterperms.h"
#include "tinygltf/tiny_gltf.h"
#include "lltinygltfhelper.h"
#include <strstream>
const std::string MATERIAL_BASE_COLOR_DEFAULT_NAME = "Base Color";
const std::string MATERIAL_NORMAL_DEFAULT_NAME = "Normal";
const std::string MATERIAL_METALLIC_DEFAULT_NAME = "Metallic Roughness";
const std::string MATERIAL_EMISSIVE_DEFAULT_NAME = "Emissive";
// Dirty flags
static const U32 MATERIAL_BASE_COLOR_DIRTY = 0x1 << 0;
static const U32 MATERIAL_BASE_COLOR_TEX_DIRTY = 0x1 << 1;
static const U32 MATERIAL_NORMAL_TEX_DIRTY = 0x1 << 2;
static const U32 MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY = 0x1 << 3;
static const U32 MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY = 0x1 << 4;
static const U32 MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY = 0x1 << 5;
static const U32 MATERIAL_EMISIVE_COLOR_DIRTY = 0x1 << 6;
static const U32 MATERIAL_EMISIVE_TEX_DIRTY = 0x1 << 7;
static const U32 MATERIAL_DOUBLE_SIDED_DIRTY = 0x1 << 8;
static const U32 MATERIAL_ALPHA_MODE_DIRTY = 0x1 << 9;
static const U32 MATERIAL_ALPHA_CUTOFF_DIRTY = 0x1 << 10;
LLUUID LLMaterialEditor::mOverrideObjectId;
S32 LLMaterialEditor::mOverrideObjectTE = -1;
bool LLMaterialEditor::mOverrideInProgress = false;
bool LLMaterialEditor::mSelectionNeedsUpdate = true;
LLFloaterComboOptions::LLFloaterComboOptions()
: LLFloater(LLSD())
{
buildFromFile("floater_combobox_ok_cancel.xml");
}
LLFloaterComboOptions::~LLFloaterComboOptions()
{
}
BOOL LLFloaterComboOptions::postBuild()
{
mConfirmButton = getChild<LLButton>("combo_ok", TRUE);
mCancelButton = getChild<LLButton>("combo_cancel", TRUE);
mComboOptions = getChild<LLComboBox>("combo_options", TRUE);
mComboText = getChild<LLTextBox>("combo_text", TRUE);
mConfirmButton->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) {onConfirm(); });
mCancelButton->setCommitCallback([this](LLUICtrl* ctrl, const LLSD& param) {onCancel(); });
return TRUE;
}
LLFloaterComboOptions* LLFloaterComboOptions::showUI(
combo_callback callback,
const std::string &title,
const std::string &description,
const std::list<std::string> &options)
{
LLFloaterComboOptions* combo_picker = new LLFloaterComboOptions();
if (combo_picker)
{
combo_picker->mCallback = callback;
combo_picker->setTitle(title);
combo_picker->mComboText->setText(description);
std::list<std::string>::const_iterator iter = options.begin();
std::list<std::string>::const_iterator end = options.end();
for (; iter != end; iter++)
{
combo_picker->mComboOptions->addSimpleElement(*iter);
}
combo_picker->mComboOptions->selectFirstItem();
combo_picker->openFloater(LLSD(title));
combo_picker->setFocus(TRUE);
combo_picker->center();
}
return combo_picker;
}
LLFloaterComboOptions* LLFloaterComboOptions::showUI(
combo_callback callback,
const std::string &title,
const std::string &description,
const std::string &ok_text,
const std::string &cancel_text,
const std::list<std::string> &options)
{
LLFloaterComboOptions* combo_picker = showUI(callback, title, description, options);
if (combo_picker)
{
combo_picker->mConfirmButton->setLabel(ok_text);
combo_picker->mCancelButton->setLabel(cancel_text);
}
return combo_picker;
}
void LLFloaterComboOptions::onConfirm()
{
mCallback(mComboOptions->getSimple(), mComboOptions->getCurrentIndex());
closeFloater();
}
void LLFloaterComboOptions::onCancel()
{
mCallback(std::string(), -1);
closeFloater();
}
class LLMaterialEditorCopiedCallback : public LLInventoryCallback
{
public:
LLMaterialEditorCopiedCallback(
const std::string &buffer,
const LLSD &old_key,
bool has_unsaved_changes)
: mBuffer(buffer),
mOldKey(old_key),
mHasUnsavedChanges(has_unsaved_changes)
{}
LLMaterialEditorCopiedCallback(
const LLSD &old_key,
const std::string &new_name)
: mOldKey(old_key),
mNewName(new_name),
mHasUnsavedChanges(false)
{}
virtual void fire(const LLUUID& inv_item_id)
{
if (!mNewName.empty())
{
// making a copy from a notecard doesn't change name, do it now
LLViewerInventoryItem* item = gInventory.getItem(inv_item_id);
if (item->getName() != mNewName)
{
LLSD updates;
updates["name"] = mNewName;
update_inventory_item(inv_item_id, updates, NULL);
}
}
LLMaterialEditor::finishSaveAs(mOldKey, inv_item_id, mBuffer, mHasUnsavedChanges);
}
private:
std::string mBuffer;
LLSD mOldKey;
std::string mNewName;
bool mHasUnsavedChanges;
};
///----------------------------------------------------------------------------
/// Class LLSelectedTEGetMatData
/// For finding selected applicable inworld material
///----------------------------------------------------------------------------
struct LLSelectedTEGetMatData : public LLSelectedTEFunctor
{
LLSelectedTEGetMatData(bool for_override);
bool apply(LLViewerObject* objectp, S32 te_index);
bool mIsOverride;
bool mIdenticalTexColor;
bool mIdenticalTexMetal;
bool mIdenticalTexEmissive;
bool mIdenticalTexNormal;
bool mFirst;
LLUUID mTexColorId;
LLUUID mTexMetalId;
LLUUID mTexEmissiveId;
LLUUID mTexNormalId;
LLUUID mObjectId;
S32 mObjectTE;
LLPointer<LLGLTFMaterial> mMaterial;
LLPointer<LLLocalGLTFMaterial> mLocalMaterial;
};
LLSelectedTEGetMatData::LLSelectedTEGetMatData(bool for_override)
: mIsOverride(for_override)
, mIdenticalTexColor(true)
, mIdenticalTexMetal(true)
, mIdenticalTexEmissive(true)
, mIdenticalTexNormal(true)
, mObjectTE(-1)
, mFirst(true)
{}
bool LLSelectedTEGetMatData::apply(LLViewerObject* objectp, S32 te_index)
{
if (!objectp)
{
return false;
}
LLUUID mat_id = objectp->getRenderMaterialID(te_index);
bool can_use = mIsOverride ? objectp->permModify() : objectp->permCopy();
LLTextureEntry *tep = objectp->getTE(te_index);
// We might want to disable this entirely if at least
// something in selection is no-copy or no modify
// or has no base material
if (can_use && tep && mat_id.notNull())
{
if (mIsOverride)
{
LLPointer<LLGLTFMaterial> mat = tep->getGLTFRenderMaterial();
LLUUID tex_color_id;
LLUUID tex_metal_id;
LLUUID tex_emissive_id;
LLUUID tex_normal_id;
llassert(mat.notNull()); // by this point shouldn't be null
if (mat.notNull())
{
tex_color_id = mat->mBaseColorId;
tex_metal_id = mat->mMetallicRoughnessId;
tex_emissive_id = mat->mEmissiveId;
tex_normal_id = mat->mNormalId;
}
if (mFirst)
{
mMaterial = mat;
mTexColorId = tex_color_id;
mTexMetalId = tex_metal_id;
mTexEmissiveId = tex_emissive_id;
mTexNormalId = tex_normal_id;
mObjectTE = te_index;
mObjectId = objectp->getID();
mFirst = false;
}
else
{
if (mTexColorId != tex_color_id)
{
mIdenticalTexColor = false;
}
if (mTexMetalId != tex_metal_id)
{
mIdenticalTexMetal = false;
}
if (mTexEmissiveId != tex_emissive_id)
{
mIdenticalTexEmissive = false;
}
if (mTexNormalId != tex_normal_id)
{
mIdenticalTexNormal = false;
}
}
}
else
{
LLGLTFMaterial *mat = tep->getGLTFMaterial();
LLLocalGLTFMaterial *local_mat = dynamic_cast<LLLocalGLTFMaterial*>(mat);
if (local_mat)
{
mLocalMaterial = local_mat;
}
mMaterial = tep->getGLTFRenderMaterial();
if (mMaterial.isNull())
{
// Shouldn't be possible?
LL_WARNS("MaterialEditor") << "Object has material id, but no material" << LL_ENDL;
mMaterial = gGLTFMaterialList.getMaterial(mat_id);
}
}
return true;
}
return false;
}
///----------------------------------------------------------------------------
/// Class LLMaterialEditor
///----------------------------------------------------------------------------
// Default constructor
LLMaterialEditor::LLMaterialEditor(const LLSD& key)
: LLPreview(key)
, mUnsavedChanges(0)
, mRevertedChanges(0)
, mExpectedUploadCost(0)
, mUploadingTexturesCount(0)
{
const LLInventoryItem* item = getItem();
if (item)
{
mAssetID = item->getAssetUUID();
}
}
void LLMaterialEditor::setObjectID(const LLUUID& object_id)
{
LLPreview::setObjectID(object_id);
const LLInventoryItem* item = getItem();
if (item)
{
mAssetID = item->getAssetUUID();
}
}
void LLMaterialEditor::setAuxItem(const LLInventoryItem* item)
{
LLPreview::setAuxItem(item);
if (item)
{
mAssetID = item->getAssetUUID();
}
}
BOOL LLMaterialEditor::postBuild()
{
// if this is a 'live editor' instance, it is also
// single instance and uses live overrides
mIsOverride = getIsSingleInstance();
mBaseColorTextureCtrl = getChild<LLTextureCtrl>("base_color_texture");
mMetallicTextureCtrl = getChild<LLTextureCtrl>("metallic_roughness_texture");
mEmissiveTextureCtrl = getChild<LLTextureCtrl>("emissive_texture");
mNormalTextureCtrl = getChild<LLTextureCtrl>("normal_texture");
mBaseColorCtrl = getChild<LLColorSwatchCtrl>("base color");
mEmissiveColorCtrl = getChild<LLColorSwatchCtrl>("emissive color");
if (!gAgent.isGodlike())
{
// Only allow fully permissive textures
mBaseColorTextureCtrl->setImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
mBaseColorTextureCtrl->setNonImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
mMetallicTextureCtrl->setImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
mMetallicTextureCtrl->setNonImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
mEmissiveTextureCtrl->setImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
mEmissiveTextureCtrl->setNonImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
mNormalTextureCtrl->setImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
mNormalTextureCtrl->setNonImmediateFilterPermMask(PERM_ITEM_UNRESTRICTED);
}
// Texture callback
mBaseColorTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_BASE_COLOR_TEX_DIRTY));
mMetallicTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY));
mEmissiveTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_EMISIVE_TEX_DIRTY));
mNormalTextureCtrl->setCommitCallback(boost::bind(&LLMaterialEditor::onCommitTexture, this, _1, _2, MATERIAL_NORMAL_TEX_DIRTY));
if (mIsOverride)
{
// Live editing needs a recovery mechanism on cancel
mBaseColorTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_BASE_COLOR_TEX_DIRTY));
mMetallicTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY));
mEmissiveTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_EMISIVE_TEX_DIRTY));
mNormalTextureCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_NORMAL_TEX_DIRTY));
// Save applied changes on 'OK' to our recovery mechanism.
mBaseColorTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_BASE_COLOR_TEX_DIRTY));
mMetallicTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY));
mEmissiveTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_EMISIVE_TEX_DIRTY));
mNormalTextureCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_NORMAL_TEX_DIRTY));
}
if (!mIsOverride)
{
childSetAction("save", boost::bind(&LLMaterialEditor::onClickSave, this));
childSetAction("save_as", boost::bind(&LLMaterialEditor::onClickSaveAs, this));
childSetAction("cancel", boost::bind(&LLMaterialEditor::onClickCancel, this));
}
if (mIsOverride)
{
childSetVisible("base_color_upload_fee", FALSE);
childSetVisible("metallic_upload_fee", FALSE);
childSetVisible("emissive_upload_fee", FALSE);
childSetVisible("normal_upload_fee", FALSE);
}
else
{
S32 upload_cost = LLAgentBenefitsMgr::current().getTextureUploadCost();
getChild<LLUICtrl>("base_color_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
getChild<LLUICtrl>("metallic_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
getChild<LLUICtrl>("emissive_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
getChild<LLUICtrl>("normal_upload_fee")->setTextArg("[FEE]", llformat("%d", upload_cost));
}
boost::function<void(LLUICtrl*, void*)> changes_callback = [this](LLUICtrl * ctrl, void* userData)
{
const U32 *flag = (const U32*)userData;
markChangesUnsaved(*flag);
// Apply changes to object live
applyToSelection();
};
childSetCommitCallback("double sided", changes_callback, (void*)&MATERIAL_DOUBLE_SIDED_DIRTY);
// BaseColor
mBaseColorCtrl->setCommitCallback(changes_callback, (void*)&MATERIAL_BASE_COLOR_DIRTY);
if (mIsOverride)
{
mBaseColorCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_BASE_COLOR_DIRTY));
mBaseColorCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_BASE_COLOR_DIRTY));
}
// transparency is a part of base color
childSetCommitCallback("transparency", changes_callback, (void*)&MATERIAL_BASE_COLOR_DIRTY);
childSetCommitCallback("alpha mode", changes_callback, (void*)&MATERIAL_ALPHA_MODE_DIRTY);
childSetCommitCallback("alpha cutoff", changes_callback, (void*)&MATERIAL_ALPHA_CUTOFF_DIRTY);
// Metallic-Roughness
childSetCommitCallback("metalness factor", changes_callback, (void*)&MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY);
childSetCommitCallback("roughness factor", changes_callback, (void*)&MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY);
// Emissive
mEmissiveColorCtrl->setCommitCallback(changes_callback, (void*)&MATERIAL_EMISIVE_COLOR_DIRTY);
if (mIsOverride)
{
mEmissiveColorCtrl->setOnCancelCallback(boost::bind(&LLMaterialEditor::onCancelCtrl, this, _1, _2, MATERIAL_EMISIVE_COLOR_DIRTY));
mEmissiveColorCtrl->setOnSelectCallback(boost::bind(&LLMaterialEditor::onSelectCtrl, this, _1, _2, MATERIAL_EMISIVE_COLOR_DIRTY));
}
if (!mIsOverride)
{
// "unsaved_changes" doesn't exist in live editor
childSetVisible("unsaved_changes", mUnsavedChanges);
// Doesn't exist in live editor
getChild<LLUICtrl>("total_upload_fee")->setTextArg("[FEE]", llformat("%d", 0));
}
// Todo:
// Disable/enable setCanApplyImmediately() based on
// working from inventory, upload or editing inworld
return LLPreview::postBuild();
}
void LLMaterialEditor::onClickCloseBtn(bool app_quitting)
{
if (app_quitting || mIsOverride)
{
closeFloater(app_quitting);
}
else
{
onClickCancel();
}
}
void LLMaterialEditor::onClose(bool app_quitting)
{
if (mSelectionUpdateSlot.connected())
{
mSelectionUpdateSlot.disconnect();
}
LLPreview::onClose(app_quitting);
}
void LLMaterialEditor::draw()
{
if (mIsOverride)
{
bool selection_empty = LLSelectMgr::getInstance()->getSelection()->isEmpty();
if (selection_empty && mHasSelection)
{
mSelectionNeedsUpdate = true;
}
if (mSelectionNeedsUpdate)
{
mSelectionNeedsUpdate = false;
clearTextures();
setFromSelection();
}
}
LLPreview::draw();
}
void LLMaterialEditor::handleReshape(const LLRect& new_rect, bool by_user)
{
if (by_user)
{
const LLRect old_rect = getRect();
LLRect clamp_rect(new_rect);
clamp_rect.mRight = clamp_rect.mLeft + old_rect.getWidth();
LLPreview::handleReshape(clamp_rect, by_user);
}
else
{
LLPreview::handleReshape(new_rect, by_user);
}
}
LLUUID LLMaterialEditor::getBaseColorId()
{
return mBaseColorTextureCtrl->getValue().asUUID();
}
void LLMaterialEditor::setBaseColorId(const LLUUID& id)
{
mBaseColorTextureCtrl->setValue(id);
mBaseColorTextureCtrl->setDefaultImageAssetID(id);
mBaseColorTextureCtrl->setTentative(FALSE);
}
void LLMaterialEditor::setBaseColorUploadId(const LLUUID& id)
{
// Might be better to use local textures and
// assign a fee in case of a local texture
if (id.notNull())
{
// todo: this does not account for posibility of texture
// being from inventory, need to check that
childSetValue("base_color_upload_fee", getString("upload_fee_string"));
// Only set if we will need to upload this texture
mBaseColorTextureUploadId = id;
}
markChangesUnsaved(MATERIAL_BASE_COLOR_TEX_DIRTY);
}
LLColor4 LLMaterialEditor::getBaseColor()
{
LLColor4 ret = linearColor4(LLColor4(mBaseColorCtrl->getValue()));
ret.mV[3] = getTransparency();
return ret;
}
void LLMaterialEditor::setBaseColor(const LLColor4& color)
{
mBaseColorCtrl->setValue(srgbColor4(color).getValue());
setTransparency(color.mV[3]);
}
F32 LLMaterialEditor::getTransparency()
{
return childGetValue("transparency").asReal();
}
void LLMaterialEditor::setTransparency(F32 transparency)
{
childSetValue("transparency", transparency);
}
std::string LLMaterialEditor::getAlphaMode()
{
return childGetValue("alpha mode").asString();
}
void LLMaterialEditor::setAlphaMode(const std::string& alpha_mode)
{
childSetValue("alpha mode", alpha_mode);
}
F32 LLMaterialEditor::getAlphaCutoff()
{
return childGetValue("alpha cutoff").asReal();
}
void LLMaterialEditor::setAlphaCutoff(F32 alpha_cutoff)
{
childSetValue("alpha cutoff", alpha_cutoff);
}
void LLMaterialEditor::setMaterialName(const std::string &name)
{
setTitle(name);
mMaterialName = name;
}
LLUUID LLMaterialEditor::getMetallicRoughnessId()
{
return mMetallicTextureCtrl->getValue().asUUID();
}
void LLMaterialEditor::setMetallicRoughnessId(const LLUUID& id)
{
mMetallicTextureCtrl->setValue(id);
mMetallicTextureCtrl->setDefaultImageAssetID(id);
mMetallicTextureCtrl->setTentative(FALSE);
}
void LLMaterialEditor::setMetallicRoughnessUploadId(const LLUUID& id)
{
if (id.notNull())
{
// todo: this does not account for posibility of texture
// being from inventory, need to check that
childSetValue("metallic_upload_fee", getString("upload_fee_string"));
mMetallicTextureUploadId = id;
}
markChangesUnsaved(MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY);
}
F32 LLMaterialEditor::getMetalnessFactor()
{
return childGetValue("metalness factor").asReal();
}
void LLMaterialEditor::setMetalnessFactor(F32 factor)
{
childSetValue("metalness factor", factor);
}
F32 LLMaterialEditor::getRoughnessFactor()
{
return childGetValue("roughness factor").asReal();
}
void LLMaterialEditor::setRoughnessFactor(F32 factor)
{
childSetValue("roughness factor", factor);
}
LLUUID LLMaterialEditor::getEmissiveId()
{
return mEmissiveTextureCtrl->getValue().asUUID();
}
void LLMaterialEditor::setEmissiveId(const LLUUID& id)
{
mEmissiveTextureCtrl->setValue(id);
mEmissiveTextureCtrl->setDefaultImageAssetID(id);
mEmissiveTextureCtrl->setTentative(FALSE);
}
void LLMaterialEditor::setEmissiveUploadId(const LLUUID& id)
{
if (id.notNull())
{
// todo: this does not account for posibility of texture
// being from inventory, need to check that
childSetValue("emissive_upload_fee", getString("upload_fee_string"));
mEmissiveTextureUploadId = id;
}
markChangesUnsaved(MATERIAL_EMISIVE_TEX_DIRTY);
}
LLColor4 LLMaterialEditor::getEmissiveColor()
{
return linearColor4(LLColor4(mEmissiveColorCtrl->getValue()));
}
void LLMaterialEditor::setEmissiveColor(const LLColor4& color)
{
mEmissiveColorCtrl->setValue(srgbColor4(color).getValue());
}
LLUUID LLMaterialEditor::getNormalId()
{
return mNormalTextureCtrl->getValue().asUUID();
}
void LLMaterialEditor::setNormalId(const LLUUID& id)
{
mNormalTextureCtrl->setValue(id);
mNormalTextureCtrl->setDefaultImageAssetID(id);
mNormalTextureCtrl->setTentative(FALSE);
}
void LLMaterialEditor::setNormalUploadId(const LLUUID& id)
{
if (id.notNull())
{
// todo: this does not account for posibility of texture
// being from inventory, need to check that
childSetValue("normal_upload_fee", getString("upload_fee_string"));
mNormalTextureUploadId = id;
}
markChangesUnsaved(MATERIAL_NORMAL_TEX_DIRTY);
}
bool LLMaterialEditor::getDoubleSided()
{
return childGetValue("double sided").asBoolean();
}
void LLMaterialEditor::setDoubleSided(bool double_sided)
{
childSetValue("double sided", double_sided);
}
void LLMaterialEditor::resetUnsavedChanges()
{
mUnsavedChanges = 0;
mRevertedChanges = 0;
if (!mIsOverride)
{
childSetVisible("unsaved_changes", false);
setCanSave(false);
mExpectedUploadCost = 0;
getChild<LLUICtrl>("total_upload_fee")->setTextArg("[FEE]", llformat("%d", mExpectedUploadCost));
}
}
void LLMaterialEditor::markChangesUnsaved(U32 dirty_flag)
{
mUnsavedChanges |= dirty_flag;
if (mIsOverride)
{
// at the moment live editing (mIsOverride) applies everything 'live'
// and "unsaved_changes", save/cancel buttons don't exist there
return;
}
childSetVisible("unsaved_changes", mUnsavedChanges);
if (mUnsavedChanges)
{
const LLInventoryItem* item = getItem();
if (item)
{
LLPermissions perm(item->getPermissions());
bool allow_modify = canModify(mObjectUUID, item);
bool source_library = mObjectUUID.isNull() && gInventory.isObjectDescendentOf(mItemUUID, gInventory.getLibraryRootFolderID());
bool source_notecard = mNotecardInventoryID.notNull();
setCanSave(allow_modify && !source_library && !source_notecard);
}
}
else
{
setCanSave(false);
}
S32 upload_texture_count = 0;
if (mBaseColorTextureUploadId.notNull() && mBaseColorTextureUploadId == getBaseColorId())
{
upload_texture_count++;
}
if (mMetallicTextureUploadId.notNull() && mMetallicTextureUploadId == getMetallicRoughnessId())
{
upload_texture_count++;
}
if (mEmissiveTextureUploadId.notNull() && mEmissiveTextureUploadId == getEmissiveId())
{
upload_texture_count++;
}
if (mNormalTextureUploadId.notNull() && mNormalTextureUploadId == getNormalId())
{
upload_texture_count++;
}
mExpectedUploadCost = upload_texture_count * LLAgentBenefitsMgr::current().getTextureUploadCost();
getChild<LLUICtrl>("total_upload_fee")->setTextArg("[FEE]", llformat("%d", mExpectedUploadCost));
}
void LLMaterialEditor::setCanSaveAs(bool value)
{
if (!mIsOverride)
{
childSetEnabled("save_as", value);
}
}
void LLMaterialEditor::setCanSave(bool value)
{
if (!mIsOverride)
{
childSetEnabled("save", value);
}
}
void LLMaterialEditor::setEnableEditing(bool can_modify)
{
childSetEnabled("double sided", can_modify);
// BaseColor
childSetEnabled("base color", can_modify);
childSetEnabled("transparency", can_modify);
childSetEnabled("alpha mode", can_modify);
childSetEnabled("alpha cutoff", can_modify);
// Metallic-Roughness
childSetEnabled("metalness factor", can_modify);
childSetEnabled("roughness factor", can_modify);
// Metallic-Roughness
childSetEnabled("metalness factor", can_modify);
childSetEnabled("roughness factor", can_modify);
// Emissive
childSetEnabled("emissive color", can_modify);
mBaseColorTextureCtrl->setEnabled(can_modify);
mMetallicTextureCtrl->setEnabled(can_modify);
mEmissiveTextureCtrl->setEnabled(can_modify);
mNormalTextureCtrl->setEnabled(can_modify);
}
void LLMaterialEditor::onCommitTexture(LLUICtrl* ctrl, const LLSD& data, S32 dirty_flag)
{
if (!mIsOverride)
{
std::string upload_fee_ctrl_name;
LLUUID old_uuid;
switch (dirty_flag)
{
case MATERIAL_BASE_COLOR_TEX_DIRTY:
{
upload_fee_ctrl_name = "base_color_upload_fee";
old_uuid = mBaseColorTextureUploadId;
break;
}
case MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY:
{
upload_fee_ctrl_name = "metallic_upload_fee";
old_uuid = mMetallicTextureUploadId;
break;
}
case MATERIAL_EMISIVE_TEX_DIRTY:
{
upload_fee_ctrl_name = "emissive_upload_fee";
old_uuid = mEmissiveTextureUploadId;
break;
}
case MATERIAL_NORMAL_TEX_DIRTY:
{
upload_fee_ctrl_name = "normal_upload_fee";
old_uuid = mNormalTextureUploadId;
break;
}
default:
break;
}
LLUUID new_val = ctrl->getValue().asUUID();
if (new_val == old_uuid && old_uuid.notNull())
{
childSetValue(upload_fee_ctrl_name, getString("upload_fee_string"));
}
else
{
// Texture picker has 'apply now' with 'cancel' support.
// Don't clean mBaseColorJ2C and mBaseColorFetched, it's our
// storage in case user decides to cancel changes.
// Without mBaseColorFetched, viewer will eventually cleanup
// the texture that is not in use
childSetValue(upload_fee_ctrl_name, getString("no_upload_fee_string"));
}
}
markChangesUnsaved(dirty_flag);
applyToSelection();
}
void LLMaterialEditor::onCancelCtrl(LLUICtrl* ctrl, const LLSD& data, S32 dirty_flag)
{
mRevertedChanges |= dirty_flag;
applyToSelection();
}
void LLMaterialEditor::onSelectCtrl(LLUICtrl* ctrl, const LLSD& data, S32 dirty_flag)
{
mUnsavedChanges |= dirty_flag;
applyToSelection();
struct f : public LLSelectedNodeFunctor
{
f(LLUICtrl* ctrl, S32 dirty_flag) : mCtrl(ctrl), mDirtyFlag(dirty_flag)
{
}
virtual bool apply(LLSelectNode* nodep)
{
LLViewerObject* objectp = nodep->getObject();
if (!objectp)
{
return false;
}
S32 num_tes = llmin((S32)objectp->getNumTEs(), (S32)objectp->getNumFaces()); // avatars have TEs but no faces
for (S32 te = 0; te < num_tes; ++te)
{
if (nodep->isTESelected(te) && nodep->mSavedGLTFOverrideMaterials.size() > te)
{
if (nodep->mSavedGLTFOverrideMaterials[te].isNull())
{
// populate with default values, default values basically mean 'not in use'
nodep->mSavedGLTFOverrideMaterials[te] = new LLGLTFMaterial();
}
switch (mDirtyFlag)
{
//Textures
case MATERIAL_BASE_COLOR_TEX_DIRTY:
{
nodep->mSavedGLTFOverrideMaterials[te]->setBaseColorId(mCtrl->getValue().asUUID(), true);
break;
}
case MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY:
{
nodep->mSavedGLTFOverrideMaterials[te]->setMetallicRoughnessId(mCtrl->getValue().asUUID(), true);
break;
}
case MATERIAL_EMISIVE_TEX_DIRTY:
{
nodep->mSavedGLTFOverrideMaterials[te]->setEmissiveId(mCtrl->getValue().asUUID(), true);
break;
}
case MATERIAL_NORMAL_TEX_DIRTY:
{
nodep->mSavedGLTFOverrideMaterials[te]->setNormalId(mCtrl->getValue().asUUID(), true);
break;
}
// Colors
case MATERIAL_BASE_COLOR_DIRTY:
{
LLColor4 ret = linearColor4(LLColor4(mCtrl->getValue()));
// except transparency
ret.mV[3] = nodep->mSavedGLTFOverrideMaterials[te]->mBaseColor.mV[3];
nodep->mSavedGLTFOverrideMaterials[te]->setBaseColorFactor(ret, true);
break;
}
case MATERIAL_EMISIVE_COLOR_DIRTY:
{
nodep->mSavedGLTFOverrideMaterials[te]->setEmissiveColorFactor(LLColor3(mCtrl->getValue()), true);
break;
}
default:
break;
}
}
}
return true;
}
LLUICtrl* mCtrl;
S32 mDirtyFlag;
} func(ctrl, dirty_flag);
LLSelectMgr::getInstance()->getSelection()->applyToNodes(&func);
}
static void write_color(const LLColor4& color, std::vector<double>& c)
{
for (int i = 0; i < c.size(); ++i) // NOTE -- use c.size because some gltf colors are 3-component
{
c[i] = color.mV[i];
}
}
static U32 write_texture(const LLUUID& id, tinygltf::Model& model)
{
tinygltf::Image image;
image.uri = id.asString();
model.images.push_back(image);
U32 image_idx = model.images.size() - 1;
tinygltf::Texture texture;
texture.source = image_idx;
model.textures.push_back(texture);
U32 texture_idx = model.textures.size() - 1;
return texture_idx;
}
void LLMaterialEditor::onClickSave()
{
if (!capabilitiesAvailable())
{
LLNotificationsUtil::add("MissingMaterialCaps");
return;
}
if (!can_afford_transaction(mExpectedUploadCost))
{
LLSD args;
args["COST"] = llformat("%d", mExpectedUploadCost);
LLNotificationsUtil::add("ErrorCannotAffordUpload", args);
return;
}
applyToSelection();
saveIfNeeded();
}
std::string LLMaterialEditor::getGLTFJson(bool prettyprint)
{
tinygltf::Model model;
getGLTFModel(model);
std::ostringstream str;
tinygltf::TinyGLTF gltf;
gltf.WriteGltfSceneToStream(&model, str, prettyprint, false);
std::string dump = str.str();
return dump;
}
void LLMaterialEditor::getGLBData(std::vector<U8>& data)
{
tinygltf::Model model;
getGLTFModel(model);
std::ostringstream str;
tinygltf::TinyGLTF gltf;
gltf.WriteGltfSceneToStream(&model, str, false, true);
std::string dump = str.str();
data.resize(dump.length());
memcpy(&data[0], dump.c_str(), dump.length());
}
void LLMaterialEditor::getGLTFModel(tinygltf::Model& model)
{
model.materials.resize(1);
tinygltf::PbrMetallicRoughness& pbrMaterial = model.materials[0].pbrMetallicRoughness;
// write base color
LLColor4 base_color = getBaseColor();
base_color.mV[3] = getTransparency();
write_color(base_color, pbrMaterial.baseColorFactor);
model.materials[0].alphaCutoff = getAlphaCutoff();
model.materials[0].alphaMode = getAlphaMode();
LLUUID base_color_id = getBaseColorId();
if (base_color_id.notNull())
{
U32 texture_idx = write_texture(base_color_id, model);
pbrMaterial.baseColorTexture.index = texture_idx;
}
// write metallic/roughness
F32 metalness = getMetalnessFactor();
F32 roughness = getRoughnessFactor();
pbrMaterial.metallicFactor = metalness;
pbrMaterial.roughnessFactor = roughness;
LLUUID mr_id = getMetallicRoughnessId();
if (mr_id.notNull())
{
U32 texture_idx = write_texture(mr_id, model);
pbrMaterial.metallicRoughnessTexture.index = texture_idx;
}
//write emissive
LLColor4 emissive_color = getEmissiveColor();
model.materials[0].emissiveFactor.resize(3);
write_color(emissive_color, model.materials[0].emissiveFactor);
LLUUID emissive_id = getEmissiveId();
if (emissive_id.notNull())
{
U32 idx = write_texture(emissive_id, model);
model.materials[0].emissiveTexture.index = idx;
}
//write normal
LLUUID normal_id = getNormalId();
if (normal_id.notNull())
{
U32 idx = write_texture(normal_id, model);
model.materials[0].normalTexture.index = idx;
}
//write doublesided
model.materials[0].doubleSided = getDoubleSided();
model.asset.version = "2.0";
}
std::string LLMaterialEditor::getEncodedAsset()
{
LLSD asset;
asset["version"] = "1.0";
asset["type"] = "GLTF 2.0";
asset["data"] = getGLTFJson(false);
std::ostringstream str;
LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);
return str.str();
}
bool LLMaterialEditor::decodeAsset(const std::vector<char>& buffer)
{
LLSD asset;
std::istrstream str(&buffer[0], buffer.size());
if (LLSDSerialize::deserialize(asset, str, buffer.size()))
{
if (asset.has("version") && asset["version"] == "1.0")
{
if (asset.has("type") && asset["type"] == "GLTF 2.0")
{
if (asset.has("data") && asset["data"].isString())
{
std::string data = asset["data"];
tinygltf::TinyGLTF gltf;
tinygltf::TinyGLTF loader;
std::string error_msg;
std::string warn_msg;
tinygltf::Model model_in;
if (loader.LoadASCIIFromString(&model_in, &error_msg, &warn_msg, data.c_str(), data.length(), ""))
{
// assets are only supposed to have one item
return setFromGltfModel(model_in, 0, true);
}
else
{
LL_WARNS("MaterialEditor") << "Floater " << getKey() << " Failed to decode material asset: " << LL_NEWLINE
<< warn_msg << LL_NEWLINE
<< error_msg << LL_ENDL;
}
}
}
}
else
{
LL_WARNS("MaterialEditor") << "Invalid LLSD content "<< asset << " for flaoter " << getKey() << LL_ENDL;
}
}
else
{
LL_WARNS("MaterialEditor") << "Failed to deserialize material LLSD for flaoter " << getKey() << LL_ENDL;
}
return false;
}
/**
* Build a description of the material we just imported.
* Currently this means a list of the textures present but we
* may eventually want to make it more complete - will be guided
* by what the content creators say they need.
*/
const std::string LLMaterialEditor::buildMaterialDescription()
{
std::ostringstream desc;
desc << LLTrans::getString("Material Texture Name Header");
// add the texture names for each just so long as the material
// we loaded has an entry for it (i think testing the texture
// control UUI for NULL is a valid metric for if it was loaded
// or not but I suspect this code will change a lot so may need
// to revisit
if (!mBaseColorTextureCtrl->getValue().asUUID().isNull())
{
desc << mBaseColorName;
desc << ", ";
}
if (!mMetallicTextureCtrl->getValue().asUUID().isNull())
{
desc << mMetallicRoughnessName;
desc << ", ";
}
if (!mEmissiveTextureCtrl->getValue().asUUID().isNull())
{
desc << mEmissiveName;
desc << ", ";
}
if (!mNormalTextureCtrl->getValue().asUUID().isNull())
{
desc << mNormalName;
}
// trim last char if it's a ',' in case there is no normal texture
// present and the code above inserts one
// (no need to check for string length - always has initial string)
std::string::iterator iter = desc.str().end() - 1;
if (*iter == ',')
{
desc.str().erase(iter);
}
// sanitize the material description so that it's compatible with the inventory
// note: split this up because clang doesn't like operating directly on the
// str() - error: lvalue reference to type 'basic_string<...>' cannot bind to a
// temporary of type 'basic_string<...>'
std::string inv_desc = desc.str();
LLInventoryObject::correctInventoryName(inv_desc);
return inv_desc;
}
bool LLMaterialEditor::saveIfNeeded()
{
if (mUploadingTexturesCount > 0)
{
// upload already in progress
// wait until textures upload
// will retry saving on callback
return true;
}
if (saveTextures() > 0)
{
// started texture upload
setEnabled(false);
return true;
}
std::string buffer = getEncodedAsset();
const LLInventoryItem* item = getItem();
// save it out to database
if (item)
{
if (!updateInventoryItem(buffer, mItemUUID, mObjectUUID))
{
return false;
}
if (mCloseAfterSave)
{
closeFloater();
}
else
{
mAssetStatus = PREVIEW_ASSET_LOADING;
setEnabled(false);
}
}
else
{
//make a new inventory item
std::string res_desc = buildMaterialDescription();
createInventoryItem(buffer, mMaterialName, res_desc);
// We do not update floater with uploaded asset yet, so just close it.
closeFloater();
}
return true;
}
// static
bool LLMaterialEditor::updateInventoryItem(const std::string &buffer, const LLUUID &item_id, const LLUUID &task_id)
{
const LLViewerRegion* region = gAgent.getRegion();
if (!region)
{
LL_WARNS("MaterialEditor") << "Not connected to a region, cannot save material." << LL_ENDL;
return false;
}
std::string agent_url = region->getCapability("UpdateMaterialAgentInventory");
std::string task_url = region->getCapability("UpdateMaterialTaskInventory");
if (!agent_url.empty() && !task_url.empty())
{
std::string url;
LLResourceUploadInfo::ptr_t uploadInfo;
if (task_id.isNull() && !agent_url.empty())
{
uploadInfo = std::make_shared<LLBufferedAssetUploadInfo>(item_id, LLAssetType::AT_MATERIAL, buffer,
[](LLUUID itemId, LLUUID newAssetId, LLUUID newItemId, LLSD) {
LLMaterialEditor::finishInventoryUpload(itemId, newAssetId, newItemId);
});
url = agent_url;
}
else if (!task_id.isNull() && !task_url.empty())
{
LLUUID object_uuid(task_id);
uploadInfo = std::make_shared<LLBufferedAssetUploadInfo>(task_id, item_id, LLAssetType::AT_MATERIAL, buffer,
[object_uuid](LLUUID itemId, LLUUID, LLUUID newAssetId, LLSD) {
LLMaterialEditor::finishTaskUpload(itemId, newAssetId, object_uuid);
});
url = task_url;
}
if (!url.empty() && uploadInfo)
{
LLViewerAssetUpload::EnqueueInventoryUpload(url, uploadInfo);
}
else
{
return false;
}
}
else // !gAssetStorage
{
LL_WARNS("MaterialEditor") << "Not connected to an materials capable region." << LL_ENDL;
return false;
}
// todo: apply permissions from textures here if server doesn't
// if any texture is 'no transfer', material should be 'no transfer' as well
return true;
}
void LLMaterialEditor::createInventoryItem(const std::string &buffer, const std::string &name, const std::string &desc)
{
// gen a new uuid for this asset
LLTransactionID tid;
tid.generate(); // timestamp-based randomization + uniquification
LLAssetID new_asset_id = tid.makeAssetID(gAgent.getSecureSessionID());
U32 next_owner_perm = LLFloaterPerms::getNextOwnerPerms("Materials");
LLUUID parent = gInventory.findUserDefinedCategoryUUIDForType(LLFolderType::FT_MATERIAL);
const U8 subtype = NO_INV_SUBTYPE; // TODO maybe use AT_SETTINGS and LLSettingsType::ST_MATERIAL ?
create_inventory_item(gAgent.getID(), gAgent.getSessionID(), parent, tid, name, desc,
LLAssetType::AT_MATERIAL, LLInventoryType::IT_MATERIAL, subtype, next_owner_perm,
new LLBoostFuncInventoryCallback([output = buffer](LLUUID const& inv_item_id)
{
LLViewerInventoryItem* item = gInventory.getItem(inv_item_id);
if (item)
{
// create_inventory_item doesn't allow presetting some permissions, fix it now
LLPermissions perm = item->getPermissions();
if (perm.getMaskEveryone() != LLFloaterPerms::getEveryonePerms("Materials")
|| perm.getMaskGroup() != LLFloaterPerms::getGroupPerms("Materials"))
{
perm.setMaskEveryone(LLFloaterPerms::getEveryonePerms("Materials"));
perm.setMaskGroup(LLFloaterPerms::getGroupPerms("Materials"));
item->setPermissions(perm);
item->updateServer(FALSE);
gInventory.updateItem(item);
gInventory.notifyObservers();
}
}
// from reference in LLSettingsVOBase::createInventoryItem()/updateInventoryItem()
LLResourceUploadInfo::ptr_t uploadInfo =
std::make_shared<LLBufferedAssetUploadInfo>(
inv_item_id,
LLAssetType::AT_MATERIAL,
output,
[](LLUUID item_id, LLUUID new_asset_id, LLUUID new_item_id, LLSD response) {
LL_INFOS("Material") << "inventory item uploaded. item: " << item_id << " asset: " << new_asset_id << " new_item_id: " << new_item_id << " response: " << response << LL_ENDL;
LLSD params = llsd::map("ASSET_ID", new_asset_id);
LLNotificationsUtil::add("MaterialCreated", params);
});
// todo: apply permissions from textures here if server doesn't
// if any texture is 'no transfer', material should be 'no transfer' as well
const LLViewerRegion* region = gAgent.getRegion();
if (region)
{
std::string agent_url(region->getCapability("UpdateMaterialAgentInventory"));
if (agent_url.empty())
{
LL_ERRS("MaterialEditor") << "missing required agent inventory cap url" << LL_ENDL;
}
LLViewerAssetUpload::EnqueueInventoryUpload(agent_url, uploadInfo);
}
})
);
}
void LLMaterialEditor::finishInventoryUpload(LLUUID itemId, LLUUID newAssetId, LLUUID newItemId)
{
// Update the UI with the new asset.
LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", LLSD(itemId));
if (me)
{
if (newItemId.isNull())
{
me->setAssetId(newAssetId);
me->refreshFromInventory();
}
else if (newItemId.notNull())
{
// Not supposed to happen?
me->refreshFromInventory(newItemId);
}
else
{
me->refreshFromInventory(itemId);
}
}
}
void LLMaterialEditor::finishTaskUpload(LLUUID itemId, LLUUID newAssetId, LLUUID taskId)
{
LLSD floater_key;
floater_key["taskid"] = taskId;
floater_key["itemid"] = itemId;
LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", floater_key);
if (me)
{
me->setAssetId(newAssetId);
me->refreshFromInventory();
me->setEnabled(true);
}
}
void LLMaterialEditor::finishSaveAs(
const LLSD &oldKey,
const LLUUID &newItemId,
const std::string &buffer,
bool has_unsaved_changes)
{
LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", oldKey);
LLViewerInventoryItem* item = gInventory.getItem(newItemId);
if (item)
{
if (me)
{
me->mItemUUID = newItemId;
me->mObjectUUID = LLUUID::null;
me->mNotecardInventoryID = LLUUID::null;
me->mNotecardObjectID = LLUUID::null;
me->mAuxItem = nullptr;
me->setKey(LLSD(newItemId)); // for findTypedInstance
me->setMaterialName(item->getName());
if (has_unsaved_changes)
{
if (!updateInventoryItem(buffer, newItemId, LLUUID::null))
{
me->setEnabled(true);
}
}
else
{
me->loadAsset();
me->setEnabled(true);
}
}
else if(has_unsaved_changes)
{
updateInventoryItem(buffer, newItemId, LLUUID::null);
}
}
else if (me)
{
me->setEnabled(true);
LL_WARNS("MaterialEditor") << "Item does not exist, floater " << me->getKey() << LL_ENDL;
}
}
void LLMaterialEditor::refreshFromInventory(const LLUUID& new_item_id)
{
if (mIsOverride)
{
// refreshFromInventory shouldn't be called for overrides,
// but just in case.
LL_WARNS("MaterialEditor") << "Tried to refresh from inventory for live editor" << LL_ENDL;
return;
}
LLSD old_key = getKey();
if (new_item_id.notNull())
{
mItemUUID = new_item_id;
if (mNotecardInventoryID.notNull())
{
LLSD floater_key;
floater_key["objectid"] = mNotecardObjectID;
floater_key["notecardid"] = mNotecardInventoryID;
setKey(floater_key);
}
else if (mObjectUUID.notNull())
{
LLSD floater_key;
floater_key["taskid"] = new_item_id;
floater_key["itemid"] = mObjectUUID;
setKey(floater_key);
}
else
{
setKey(LLSD(new_item_id));
}
}
LL_DEBUGS("MaterialEditor") << "New floater key: " << getKey() << " Old key: " << old_key << LL_ENDL;
loadAsset();
}
void LLMaterialEditor::onClickSaveAs()
{
if (!can_afford_transaction(mExpectedUploadCost))
{
LLSD args;
args["COST"] = llformat("%d", mExpectedUploadCost);
LLNotificationsUtil::add("ErrorCannotAffordUpload", args);
return;
}
LLSD args;
args["DESC"] = mMaterialName;
LLNotificationsUtil::add("SaveMaterialAs", args, LLSD(), boost::bind(&LLMaterialEditor::onSaveAsMsgCallback, this, _1, _2));
}
void LLMaterialEditor::onSaveAsMsgCallback(const LLSD& notification, const LLSD& response)
{
S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
if (0 == option)
{
std::string new_name = response["message"].asString();
LLInventoryObject::correctInventoryName(new_name);
if (!new_name.empty())
{
const LLInventoryItem* item;
if (mNotecardInventoryID.notNull())
{
item = mAuxItem.get();
}
else
{
item = getItem();
}
if (item)
{
const LLUUID &marketplacelistings_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MARKETPLACE_LISTINGS, false);
LLUUID parent_id = item->getParentUUID();
if (mObjectUUID.notNull() || marketplacelistings_id == parent_id || gInventory.isObjectDescendentOf(item->getUUID(), gInventory.getLibraryRootFolderID()))
{
parent_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_MATERIAL);
}
// A two step process, first copy an existing item, then create new asset
if (mNotecardInventoryID.notNull())
{
LLPointer<LLInventoryCallback> cb = new LLMaterialEditorCopiedCallback(getKey(), new_name);
copy_inventory_from_notecard(parent_id,
mNotecardObjectID,
mNotecardInventoryID,
mAuxItem.get(),
gInventoryCallbacks.registerCB(cb));
}
else
{
std::string buffer = getEncodedAsset();
LLPointer<LLInventoryCallback> cb = new LLMaterialEditorCopiedCallback(buffer, getKey(), mUnsavedChanges);
copy_inventory_item(
gAgent.getID(),
item->getPermissions().getOwner(),
item->getUUID(),
parent_id,
new_name,
cb);
}
mAssetStatus = PREVIEW_ASSET_LOADING;
setEnabled(false);
}
else
{
setMaterialName(new_name);
onClickSave();
}
}
else
{
LLNotificationsUtil::add("InvalidMaterialName");
}
}
}
void LLMaterialEditor::onClickCancel()
{
if (mUnsavedChanges)
{
LLNotificationsUtil::add("UsavedMaterialChanges", LLSD(), LLSD(), boost::bind(&LLMaterialEditor::onCancelMsgCallback, this, _1, _2));
}
else
{
closeFloater();
}
}
void LLMaterialEditor::onCancelMsgCallback(const LLSD& notification, const LLSD& response)
{
S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
if (0 == option)
{
closeFloater();
}
}
static void pack_textures(
LLPointer<LLImageRaw>& base_color_img,
LLPointer<LLImageRaw>& normal_img,
LLPointer<LLImageRaw>& mr_img,
LLPointer<LLImageRaw>& emissive_img,
LLPointer<LLImageRaw>& occlusion_img,
LLPointer<LLImageJ2C>& base_color_j2c,
LLPointer<LLImageJ2C>& normal_j2c,
LLPointer<LLImageJ2C>& mr_j2c,
LLPointer<LLImageJ2C>& emissive_j2c)
{
// NOTE : remove log spam and lossless vs lossy comparisons when the logs are no longer useful
if (base_color_img)
{
base_color_j2c = LLViewerTextureList::convertToUploadFile(base_color_img);
LL_DEBUGS("MaterialEditor") << "BaseColor: " << base_color_j2c->getDataSize() << LL_ENDL;
}
if (normal_img)
{
normal_j2c = LLViewerTextureList::convertToUploadFile(normal_img);
LLPointer<LLImageJ2C> test;
test = LLViewerTextureList::convertToUploadFile(normal_img, 1024, true);
S32 lossy_bytes = normal_j2c->getDataSize();
S32 lossless_bytes = test->getDataSize();
LL_DEBUGS("MaterialEditor") << llformat("Lossless vs Lossy: (%d/%d) = %.2f", lossless_bytes, lossy_bytes, (F32)lossless_bytes / lossy_bytes) << LL_ENDL;
normal_j2c = test;
}
if (mr_img)
{
mr_j2c = LLViewerTextureList::convertToUploadFile(mr_img);
LL_DEBUGS("MaterialEditor") << "Metallic/Roughness: " << mr_j2c->getDataSize() << LL_ENDL;
}
if (emissive_img)
{
emissive_j2c = LLViewerTextureList::convertToUploadFile(emissive_img);
LL_DEBUGS("MaterialEditor") << "Emissive: " << emissive_j2c->getDataSize() << LL_ENDL;
}
}
void LLMaterialEditor::loadMaterialFromFile(const std::string& filename, S32 index)
{
tinygltf::TinyGLTF loader;
std::string error_msg;
std::string warn_msg;
bool loaded = false;
tinygltf::Model model_in;
std::string filename_lc = filename;
LLStringUtil::toLower(filename_lc);
// Load a tinygltf model fom a file. Assumes that the input filename has already been
// been sanitized to one of (.gltf , .glb) extensions, so does a simple find to distinguish.
if (std::string::npos == filename_lc.rfind(".gltf"))
{ // file is binary
loaded = loader.LoadBinaryFromFile(&model_in, &error_msg, &warn_msg, filename);
}
else
{ // file is ascii
loaded = loader.LoadASCIIFromFile(&model_in, &error_msg, &warn_msg, filename);
}
if (!loaded)
{
LLNotificationsUtil::add("CannotUploadMaterial");
return;
}
if (model_in.materials.empty())
{
// materials are missing
LLNotificationsUtil::add("CannotUploadMaterial");
return;
}
if (index >= 0 && model_in.materials.size() <= index)
{
// material is missing
LLNotificationsUtil::add("CannotUploadMaterial");
return;
}
LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("material_editor");
if (index >= 0)
{
// Prespecified material
me->loadMaterial(model_in, filename_lc, index);
}
else if (model_in.materials.size() == 1)
{
// Only one, just load it
me->loadMaterial(model_in, filename_lc, 0);
}
else
{
// Promt user to select material
std::list<std::string> material_list;
std::vector<tinygltf::Material>::const_iterator mat_iter = model_in.materials.begin();
std::vector<tinygltf::Material>::const_iterator mat_end = model_in.materials.end();
for (; mat_iter != mat_end; mat_iter++)
{
std::string mat_name = mat_iter->name;
if (mat_name.empty())
{
material_list.push_back("Material " + std::to_string(material_list.size()));
}
else
{
material_list.push_back(mat_name);
}
}
LLFloaterComboOptions::showUI(
[me, model_in, filename_lc](const std::string& option, S32 index)
{
me->loadMaterial(model_in, filename_lc, index);
},
me->getString("material_selection_title"),
me->getString("material_selection_text"),
material_list
);
}
}
void LLMaterialEditor::onSelectionChanged()
{
// Drop selection updates if we are waiting for
// overrides to finish applying to not reset values
// (might need a timeout)
if (!mOverrideInProgress)
{
// mUpdateSignal triggers a lot per frame, breakwater
mSelectionNeedsUpdate = true;
}
}
void LLMaterialEditor::updateLive()
{
mSelectionNeedsUpdate = true;
mOverrideInProgress = false;
}
void LLMaterialEditor::updateLive(const LLUUID &object_id, S32 te)
{
if (mOverrideObjectId != object_id
|| mOverrideObjectTE != te)
{
// Ignore if waiting for override,
// if not waiting, mark selection dirty
mSelectionNeedsUpdate |= !mOverrideInProgress;
return;
}
// update for currently displayed object and face
mSelectionNeedsUpdate = true;
mOverrideInProgress = false;
}
void LLMaterialEditor::loadLive()
{
LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("live_material_editor");
if (me)
{
me->mOverrideInProgress = false;
me->setFromSelection();
// Set up for selection changes updates
if (!me->mSelectionUpdateSlot.connected())
{
me->mSelectionUpdateSlot = LLSelectMgr::instance().mUpdateSignal.connect(boost::bind(&LLMaterialEditor::onSelectionChanged, me));
}
me->openFloater();
me->setFocus(TRUE);
}
}
void LLMaterialEditor::saveObjectsMaterialAs()
{
LLSelectedTEGetMatData func(false);
LLSelectMgr::getInstance()->getSelection()->applyToTEs(&func, true /*first applicable*/);
saveMaterialAs(func.mMaterial, func.mLocalMaterial);
}
void LLMaterialEditor::savePickedMaterialAs()
{
LLPickInfo pick = LLToolPie::getInstance()->getPick();
if (pick.mPickType != LLPickInfo::PICK_OBJECT || !pick.getObject())
{
return;
}
LLPointer<LLGLTFMaterial> render_material;
LLPointer<LLLocalGLTFMaterial> local_material;
LLViewerObject *objectp = pick.getObject();
LLUUID mat_id = objectp->getRenderMaterialID(pick.mObjectFace);
if (mat_id.notNull() && objectp->permCopy())
{
// Try a face user picked first
// (likely the only method we need, but in such case
// enable_object_save_gltf_material will need to check this)
LLTextureEntry *tep = objectp->getTE(pick.mObjectFace);
LLGLTFMaterial *mat = tep->getGLTFMaterial();
LLLocalGLTFMaterial *local_mat = dynamic_cast<LLLocalGLTFMaterial*>(mat);
if (local_mat)
{
local_material = local_mat;
}
render_material = tep->getGLTFRenderMaterial();
}
else
{
// Find an applicable material.
// Do this before showing message, because
// message is going to drop selection.
LLSelectedTEGetMatData func(false);
LLSelectMgr::getInstance()->getSelection()->applyToTEs(&func, true /*first applicable*/);
local_material = func.mLocalMaterial;
render_material = func.mMaterial;
}
saveMaterialAs(render_material, local_material);
}
void LLMaterialEditor::saveMaterialAs(const LLGLTFMaterial* render_material, const LLLocalGLTFMaterial *local_material)
{
if (local_material)
{
// This is a local material, reload it from file
// so that user won't end up with grey textures
// on next login.
LLMaterialEditor::loadMaterialFromFile(local_material->getFilename(), local_material->getIndexInFile());
LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("material_editor");
if (me)
{
// don't use override material here, it has 'hacked ids'
// and values, use end result, apply it on top of local.
me->setBaseColor(render_material->mBaseColor);
me->setMetalnessFactor(render_material->mMetallicFactor);
me->setRoughnessFactor(render_material->mRoughnessFactor);
me->setEmissiveColor(render_material->mEmissiveColor);
me->setDoubleSided(render_material->mDoubleSided);
me->setAlphaMode(render_material->getAlphaMode());
me->setAlphaCutoff(render_material->mAlphaCutoff);
// most things like colors we can apply without verifying
// but texture ids are going to be different from both, base and override
// so only apply override id if there is actually a difference
if (local_material->mBaseColorId != render_material->mBaseColorId)
{
me->setBaseColorId(render_material->mBaseColorId);
me->childSetValue("base_color_upload_fee", me->getString("no_upload_fee_string"));
}
if (local_material->mNormalId != render_material->mNormalId)
{
me->setNormalId(render_material->mNormalId);
me->childSetValue("normal_upload_fee", me->getString("no_upload_fee_string"));
}
if (local_material->mMetallicRoughnessId != render_material->mMetallicRoughnessId)
{
me->setMetallicRoughnessId(render_material->mMetallicRoughnessId);
me->childSetValue("metallic_upload_fee", me->getString("no_upload_fee_string"));
}
if (local_material->mEmissiveId != render_material->mEmissiveId)
{
me->setEmissiveId(render_material->mEmissiveId);
me->childSetValue("emissive_upload_fee", me->getString("no_upload_fee_string"));
}
// recalculate upload prices
me->markChangesUnsaved(0);
}
return;
}
LLSD payload;
if (render_material)
{
payload["data"] = render_material->asJSON();
}
else
{
// Menu shouldn't allow this, but as a fallback
// pick defaults from a blank material
LLGLTFMaterial blank_mat;
payload["data"] = blank_mat.asJSON();
LL_WARNS() << "Got no material when trying to save material" << LL_ENDL;
}
LLSD args;
args["DESC"] = LLTrans::getString("New Material");
LLNotificationsUtil::add("SaveMaterialAs", args, payload, boost::bind(&LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback, _1, _2));
}
void LLMaterialEditor::onSaveObjectsMaterialAsMsgCallback(const LLSD& notification, const LLSD& response)
{
S32 option = LLNotificationsUtil::getSelectedOption(notification, response);
if (0 == option)
{
LLSD asset;
asset["version"] = "1.0";
asset["type"] = "GLTF 2.0";
asset["data"] = notification["payload"]["data"];
std::ostringstream str;
LLSDSerialize::serialize(asset, str, LLSDSerialize::LLSD_BINARY);
std::string new_name = response["message"].asString();
createInventoryItem(str.str(), new_name, std::string());
}
}
void LLMaterialEditor::loadFromGLTFMaterial(LLUUID &asset_id)
{
if (asset_id.isNull())
{
LL_WARNS("MaterialEditor") << "Trying to open material with null id" << LL_ENDL;
return;
}
LLMaterialEditor* me = (LLMaterialEditor*)LLFloaterReg::getInstance("material_editor");
me->mMaterialName = LLTrans::getString("New Material");
me->setTitle(me->mMaterialName);
me->setFromGLTFMaterial(gGLTFMaterialList.getMaterial(asset_id));
me->openFloater();
me->setFocus(TRUE);
}
void LLMaterialEditor::loadMaterial(const tinygltf::Model &model_in, const std::string &filename_lc, S32 index)
{
if (model_in.materials.size() <= index)
{
return;
}
std::string folder = gDirUtilp->getDirName(filename_lc);
tinygltf::Material material_in = model_in.materials[index];
tinygltf::Model model_out;
model_out.asset.version = "2.0";
model_out.materials.resize(1);
// get base color texture
LLPointer<LLImageRaw> base_color_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.pbrMetallicRoughness.baseColorTexture.index, mBaseColorName);
// get normal map
LLPointer<LLImageRaw> normal_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.normalTexture.index, mNormalName);
// get metallic-roughness texture
LLPointer<LLImageRaw> mr_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.pbrMetallicRoughness.metallicRoughnessTexture.index, mMetallicRoughnessName);
// get emissive texture
LLPointer<LLImageRaw> emissive_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.emissiveTexture.index, mEmissiveName);
// get occlusion map if needed
LLPointer<LLImageRaw> occlusion_img;
if (material_in.occlusionTexture.index != material_in.pbrMetallicRoughness.metallicRoughnessTexture.index)
{
std::string tmp;
occlusion_img = LLTinyGLTFHelper::getTexture(folder, model_in, material_in.occlusionTexture.index, tmp);
}
LLTinyGLTFHelper::initFetchedTextures(material_in, base_color_img, normal_img, mr_img, emissive_img, occlusion_img,
mBaseColorFetched, mNormalFetched, mMetallicRoughnessFetched, mEmissiveFetched);
pack_textures(base_color_img, normal_img, mr_img, emissive_img, occlusion_img,
mBaseColorJ2C, mNormalJ2C, mMetallicRoughnessJ2C, mEmissiveJ2C);
LLUUID base_color_id;
if (mBaseColorFetched.notNull())
{
mBaseColorFetched->forceToSaveRawImage(0, F32_MAX);
base_color_id = mBaseColorFetched->getID();
if (mBaseColorName.empty())
{
mBaseColorName = MATERIAL_BASE_COLOR_DEFAULT_NAME;
}
}
LLUUID normal_id;
if (mNormalFetched.notNull())
{
mNormalFetched->forceToSaveRawImage(0, F32_MAX);
normal_id = mNormalFetched->getID();
if (mNormalName.empty())
{
mNormalName = MATERIAL_NORMAL_DEFAULT_NAME;
}
}
LLUUID mr_id;
if (mMetallicRoughnessFetched.notNull())
{
mMetallicRoughnessFetched->forceToSaveRawImage(0, F32_MAX);
mr_id = mMetallicRoughnessFetched->getID();
if (mMetallicRoughnessName.empty())
{
mMetallicRoughnessName = MATERIAL_METALLIC_DEFAULT_NAME;
}
}
LLUUID emissive_id;
if (mEmissiveFetched.notNull())
{
mEmissiveFetched->forceToSaveRawImage(0, F32_MAX);
emissive_id = mEmissiveFetched->getID();
if (mEmissiveName.empty())
{
mEmissiveName = MATERIAL_EMISSIVE_DEFAULT_NAME;
}
}
setBaseColorId(base_color_id);
setBaseColorUploadId(base_color_id);
setMetallicRoughnessId(mr_id);
setMetallicRoughnessUploadId(mr_id);
setEmissiveId(emissive_id);
setEmissiveUploadId(emissive_id);
setNormalId(normal_id);
setNormalUploadId(normal_id);
setFromGltfModel(model_in, index);
setFromGltfMetaData(filename_lc, model_in, index);
markChangesUnsaved(U32_MAX);
openFloater();
setFocus(TRUE);
applyToSelection();
}
bool LLMaterialEditor::setFromGltfModel(const tinygltf::Model& model, S32 index, bool set_textures)
{
if (model.materials.size() > index)
{
const tinygltf::Material& material_in = model.materials[index];
if (set_textures)
{
S32 index;
LLUUID id;
// get base color texture
index = material_in.pbrMetallicRoughness.baseColorTexture.index;
if (index >= 0)
{
id.set(model.images[index].uri);
setBaseColorId(id);
}
else
{
setBaseColorId(LLUUID::null);
}
// get normal map
index = material_in.normalTexture.index;
if (index >= 0)
{
id.set(model.images[index].uri);
setNormalId(id);
}
else
{
setNormalId(LLUUID::null);
}
// get metallic-roughness texture
index = material_in.pbrMetallicRoughness.metallicRoughnessTexture.index;
if (index >= 0)
{
id.set(model.images[index].uri);
setMetallicRoughnessId(id);
}
else
{
setMetallicRoughnessId(LLUUID::null);
}
// get emissive texture
index = material_in.emissiveTexture.index;
if (index >= 0)
{
id.set(model.images[index].uri);
setEmissiveId(id);
}
else
{
setEmissiveId(LLUUID::null);
}
}
setAlphaMode(material_in.alphaMode);
setAlphaCutoff(material_in.alphaCutoff);
setBaseColor(LLTinyGLTFHelper::getColor(material_in.pbrMetallicRoughness.baseColorFactor));
setEmissiveColor(LLTinyGLTFHelper::getColor(material_in.emissiveFactor));
setMetalnessFactor(material_in.pbrMetallicRoughness.metallicFactor);
setRoughnessFactor(material_in.pbrMetallicRoughness.roughnessFactor);
setDoubleSided(material_in.doubleSided);
}
return true;
}
/**
* Build a texture name from the contents of the (in tinyGLFT parlance)
* Image URI. This often is filepath to the original image on the users'
* local file system.
*/
const std::string LLMaterialEditor::getImageNameFromUri(std::string image_uri, const std::string texture_type)
{
// getBaseFileName() works differently on each platform and file patchs
// can contain both types of delimiter so unify them then extract the
// base name (no path or extension)
std::replace(image_uri.begin(), image_uri.end(), '\\', gDirUtilp->getDirDelimiter()[0]);
std::replace(image_uri.begin(), image_uri.end(), '/', gDirUtilp->getDirDelimiter()[0]);
const bool strip_extension = true;
std::string stripped_uri = gDirUtilp->getBaseFileName(image_uri, strip_extension);
// sometimes they can be really long and unwieldy - 64 chars is enough for anyone :)
const int max_texture_name_length = 64;
if (stripped_uri.length() > max_texture_name_length)
{
stripped_uri = stripped_uri.substr(0, max_texture_name_length - 1);
}
// We intend to append the type of texture (base color, emissive etc.) to the
// name of the texture but sometimes the creator already did that. To try
// to avoid repeats (not perfect), we look for the texture type in the name
// and if we find it, do not append the type, later on. One way this fails
// (and it's fine for now) is I see some texture/image uris have a name like
// "metallic roughness" and of course, that doesn't match our predefined
// name "metallicroughness" - consider fix later..
bool name_includes_type = false;
std::string stripped_uri_lower = stripped_uri;
LLStringUtil::toLower(stripped_uri_lower);
stripped_uri_lower.erase(std::remove_if(stripped_uri_lower.begin(), stripped_uri_lower.end(), isspace), stripped_uri_lower.end());
std::string texture_type_lower = texture_type;
LLStringUtil::toLower(texture_type_lower);
texture_type_lower.erase(std::remove_if(texture_type_lower.begin(), texture_type_lower.end(), isspace), texture_type_lower.end());
if (stripped_uri_lower.find(texture_type_lower) != std::string::npos)
{
name_includes_type = true;
}
// uri doesn't include the type at all
if (name_includes_type == false)
{
// uri doesn't include the type and the uri is not empty
// so we can include everything
if (stripped_uri.length() > 0)
{
// example "DamagedHelmet: base layer"
return STRINGIZE(
mMaterialNameShort <<
": " <<
stripped_uri <<
" (" <<
texture_type <<
")"
);
}
else
// uri doesn't include the type (because the uri is empty)
// so we must reorganize the string a bit to include the name
// and an explicit name type
{
// example "DamagedHelmet: (Emissive)"
return STRINGIZE(
mMaterialNameShort <<
" (" <<
texture_type <<
")"
);
}
}
else
// uri includes the type so just use it directly with the
// name of the material
{
return STRINGIZE(
// example: AlienBust: normal_layer
mMaterialNameShort <<
": " <<
stripped_uri
);
}
}
/**
* Update the metadata for the material based on what we find in the loaded
* file (along with some assumptions and interpretations...). Fields include
* the name of the material, a material description and the names of the
* composite textures.
*/
void LLMaterialEditor::setFromGltfMetaData(const std::string& filename, const tinygltf::Model& model, S32 index)
{
// Use the name (without any path/extension) of the file that was
// uploaded as the base of the material name. Then if the name of the
// scene is present and not blank, append that and use the result as
// the name of the material. This is a first pass at creating a
// naming scheme that is useful to real content creators and hopefully
// avoid 500 materials in your inventory called "scene" or "Default"
const bool strip_extension = true;
std::string base_filename = gDirUtilp->getBaseFileName(filename, strip_extension);
// Extract the name of the scene. Note it is often blank or some very
// generic name like "Scene" or "Default" so using this in the name
// is less useful than you might imagine.
std::string material_name;
if (model.materials.size() > index && !model.materials[index].name.empty())
{
material_name = model.materials[index].name;
}
else if (model.scenes.size() > 0)
{
const tinygltf::Scene& scene_in = model.scenes[0];
if (scene_in.name.length())
{
material_name = scene_in.name;
}
else
{
// scene name is empty so no point using it
}
}
else
{
// scene name isn't present so no point using it
}
// If we have a valid material or scene name, use it to build the short and
// long versions of the material name. The long version is used
// as you might expect, for the material name. The short version is
// used as part of the image/texture name - the theory is that will
// allow content creators to track the material and the corresponding
// textures
if (material_name.length())
{
mMaterialNameShort = base_filename;
mMaterialName = STRINGIZE(
base_filename <<
" " <<
"(" <<
material_name <<
")"
);
}
else
// otherwise, just use the trimmed filename as is
{
mMaterialNameShort = base_filename;
mMaterialName = base_filename;
}
// sanitize the material name so that it's compatible with the inventory
LLInventoryObject::correctInventoryName(mMaterialName);
LLInventoryObject::correctInventoryName(mMaterialNameShort);
// We also set the title of the floater to match the
// name of the material
setTitle(mMaterialName);
/**
* Extract / derive the names of each composite texture. For each, the
* index is used to to determine which of the "Images" is used. If the index
* is -1 then that texture type is not present in the material (Seems to be
* quite common that a material is missing 1 or more types of texture)
*/
if (model.materials.size() > index)
{
const tinygltf::Material& first_material = model.materials[index];
mBaseColorName = MATERIAL_BASE_COLOR_DEFAULT_NAME;
// note: unlike the other textures, base color doesn't have its own entry
// in the tinyGLTF Material struct. Rather, it is taken from a
// sub-texture in the pbrMetallicRoughness member
int index = first_material.pbrMetallicRoughness.baseColorTexture.index;
if (index > -1 && index < model.images.size())
{
// sanitize the name we decide to use for each texture
std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_BASE_COLOR_DEFAULT_NAME);
LLInventoryObject::correctInventoryName(texture_name);
mBaseColorName = texture_name;
}
mEmissiveName = MATERIAL_EMISSIVE_DEFAULT_NAME;
index = first_material.emissiveTexture.index;
if (index > -1 && index < model.images.size())
{
std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_EMISSIVE_DEFAULT_NAME);
LLInventoryObject::correctInventoryName(texture_name);
mEmissiveName = texture_name;
}
mMetallicRoughnessName = MATERIAL_METALLIC_DEFAULT_NAME;
index = first_material.pbrMetallicRoughness.metallicRoughnessTexture.index;
if (index > -1 && index < model.images.size())
{
std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_METALLIC_DEFAULT_NAME);
LLInventoryObject::correctInventoryName(texture_name);
mMetallicRoughnessName = texture_name;
}
mNormalName = MATERIAL_NORMAL_DEFAULT_NAME;
index = first_material.normalTexture.index;
if (index > -1 && index < model.images.size())
{
std::string texture_name = getImageNameFromUri(model.images[index].uri, MATERIAL_NORMAL_DEFAULT_NAME);
LLInventoryObject::correctInventoryName(texture_name);
mNormalName = texture_name;
}
}
}
void LLMaterialEditor::importMaterial()
{
LLFilePickerReplyThread::startPicker(
[](const std::vector<std::string>& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter)
{
if (LLAppViewer::instance()->quitRequested())
{
return;
}
if (filenames.size() > 0)
{
LLMaterialEditor::loadMaterialFromFile(filenames[0], -1);
}
},
LLFilePicker::FFLOAD_MATERIAL,
true);
}
class LLRenderMaterialFunctor : public LLSelectedTEFunctor
{
public:
LLRenderMaterialFunctor(const LLUUID &id)
: mMatId(id)
{
}
bool apply(LLViewerObject* objectp, S32 te) override
{
if (objectp && objectp->permModify() && objectp->getVolume())
{
LLVOVolume* vobjp = (LLVOVolume*)objectp;
vobjp->setRenderMaterialID(te, mMatId, false /*preview only*/);
vobjp->updateTEMaterialTextures(te);
}
return true;
}
private:
LLUUID mMatId;
};
class LLRenderMaterialOverrideFunctor : public LLSelectedNodeFunctor
{
public:
LLRenderMaterialOverrideFunctor(
LLMaterialEditor * me,
const LLUUID &report_on_object_id,
S32 report_on_te)
: mEditor(me)
, mSuccess(false)
, mObjectId(report_on_object_id)
, mObjectTE(report_on_te)
{
}
virtual bool apply(LLSelectNode* nodep) override
{
LLViewerObject* objectp = nodep->getObject();
if (!objectp || !objectp->permModify() || !objectp->getVolume())
{
return false;
}
S32 num_tes = llmin((S32)objectp->getNumTEs(), (S32)objectp->getNumFaces()); // avatars have TEs but no faces
// post override from given object and te to the simulator
// requestData should have:
// object_id - UUID of LLViewerObject
// side - S32 index of texture entry
// gltf_json - String of GLTF json for override data
for (S32 te = 0; te < num_tes; ++te)
{
if (!nodep->isTESelected(te))
{
continue;
}
// Get material from object
// Selection can cover multiple objects, and live editor is
// supposed to overwrite changed values only
LLTextureEntry* tep = objectp->getTE(te);
if (tep->getGLTFMaterial() == nullptr)
{
// overrides are not supposed to work or apply if
// there is no base material to work from
return false;
}
LLPointer<LLGLTFMaterial> material = tep->getGLTFMaterialOverride();
// make a copy to not invalidate existing
// material for multiple objects
if (material.isNull())
{
// Start with a material override which does not make any changes
material = new LLGLTFMaterial();
}
else
{
material = new LLGLTFMaterial(*material);
}
U32 changed_flags = mEditor->getUnsavedChangesFlags();
U32 reverted_flags = mEditor->getRevertedChangesFlags();
LLPointer<LLGLTFMaterial> revert_mat;
if (nodep->mSavedGLTFOverrideMaterials.size() > te)
{
if (nodep->mSavedGLTFOverrideMaterials[te].notNull())
{
revert_mat = nodep->mSavedGLTFOverrideMaterials[te];
}
else
{
// mSavedGLTFOverrideMaterials[te] being present but null
// means we need to use a default value
revert_mat = new LLGLTFMaterial();
}
}
// else can not revert at all
// Override object's values with values from editor where appropriate
if (changed_flags & MATERIAL_BASE_COLOR_DIRTY)
{
material->setBaseColorFactor(mEditor->getBaseColor(), true);
}
else if ((reverted_flags & MATERIAL_BASE_COLOR_DIRTY) && revert_mat.notNull())
{
material->setBaseColorFactor(revert_mat->mBaseColor, false);
}
if (changed_flags & MATERIAL_BASE_COLOR_TEX_DIRTY)
{
material->setBaseColorId(mEditor->getBaseColorId(), true);
}
else if ((reverted_flags & MATERIAL_BASE_COLOR_TEX_DIRTY) && revert_mat.notNull())
{
material->setBaseColorId(revert_mat->mBaseColorId, false);
}
if (changed_flags & MATERIAL_NORMAL_TEX_DIRTY)
{
material->setNormalId(mEditor->getNormalId(), true);
}
else if ((reverted_flags & MATERIAL_NORMAL_TEX_DIRTY) && revert_mat.notNull())
{
material->setNormalId(revert_mat->mNormalId, false);
}
if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY)
{
material->setMetallicRoughnessId(mEditor->getMetallicRoughnessId(), true);
}
else if ((reverted_flags & MATERIAL_METALLIC_ROUGHTNESS_TEX_DIRTY) && revert_mat.notNull())
{
material->setMetallicRoughnessId(revert_mat->mMetallicRoughnessId, false);
}
if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY)
{
material->setMetallicFactor(mEditor->getMetalnessFactor(), true);
}
else if ((reverted_flags & MATERIAL_METALLIC_ROUGHTNESS_METALNESS_DIRTY) && revert_mat.notNull())
{
material->setMetallicFactor(revert_mat->mMetallicFactor, false);
}
if (changed_flags & MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY)
{
material->setRoughnessFactor(mEditor->getRoughnessFactor(), true);
}
else if ((reverted_flags & MATERIAL_METALLIC_ROUGHTNESS_ROUGHNESS_DIRTY) && revert_mat.notNull())
{
material->setRoughnessFactor(revert_mat->mRoughnessFactor, false);
}
if (changed_flags & MATERIAL_EMISIVE_COLOR_DIRTY)
{
material->setEmissiveColorFactor(LLColor3(mEditor->getEmissiveColor()), true);
}
else if ((reverted_flags & MATERIAL_EMISIVE_COLOR_DIRTY) && revert_mat.notNull())
{
material->setEmissiveColorFactor(revert_mat->mEmissiveColor, false);
}
if (changed_flags & MATERIAL_EMISIVE_TEX_DIRTY)
{
material->setEmissiveId(mEditor->getEmissiveId(), true);
}
else if ((reverted_flags & MATERIAL_EMISIVE_TEX_DIRTY) && revert_mat.notNull())
{
material->setEmissiveId(revert_mat->mEmissiveId, false);
}
if (changed_flags & MATERIAL_DOUBLE_SIDED_DIRTY)
{
material->setDoubleSided(mEditor->getDoubleSided(), true);
}
else if ((reverted_flags & MATERIAL_DOUBLE_SIDED_DIRTY) && revert_mat.notNull())
{
material->setDoubleSided(revert_mat->mDoubleSided, false);
}
if (changed_flags & MATERIAL_ALPHA_MODE_DIRTY)
{
material->setAlphaMode(mEditor->getAlphaMode(), true);
}
else if ((reverted_flags & MATERIAL_ALPHA_MODE_DIRTY) && revert_mat.notNull())
{
material->setAlphaMode(revert_mat->mAlphaMode, false);
}
if (changed_flags & MATERIAL_ALPHA_CUTOFF_DIRTY)
{
material->setAlphaCutoff(mEditor->getAlphaCutoff(), true);
}
else if ((reverted_flags & MATERIAL_ALPHA_CUTOFF_DIRTY) && revert_mat.notNull())
{
material->setAlphaCutoff(revert_mat->mAlphaCutoff, false);
}
if (mObjectTE == te
&& mObjectId == objectp->getID())
{
mSuccess = true;
}
LLGLTFMaterialList::queueModify(objectp->getID(), te, material);
}
return true;
}
static void modifyCallback(bool success)
{
if (!success)
{
// something went wrong update selection
LLMaterialEditor::updateLive();
}
// else we will get updateLive(obj, id) from applied overrides
}
bool getResult() { return mSuccess; }
private:
LLMaterialEditor * mEditor;
LLUUID mObjectId;
S32 mObjectTE;
bool mSuccess;
};
void LLMaterialEditor::applyToSelection()
{
if (!mIsOverride)
{
// Only apply if working with 'live' materials
// Might need a better way to distinguish 'live' mode.
// But only one live edit is supposed to work at a time
// as a pair to tools floater.
return;
}
std::string url = gAgent.getRegionCapability("ModifyMaterialParams");
if (!url.empty())
{
// Don't send data if there is nothing to send.
// Some UI elements will cause multiple commits,
// like spin ctrls on click and on down
if (mUnsavedChanges != 0 || mRevertedChanges != 0)
{
mOverrideInProgress = true;
LLObjectSelectionHandle selected_objects = LLSelectMgr::getInstance()->getSelection();
LLRenderMaterialOverrideFunctor override_func(this, mOverrideObjectId, mOverrideObjectTE);
selected_objects->applyToNodes(&override_func);
void(*done_callback)(bool) = LLRenderMaterialOverrideFunctor::modifyCallback;
LLGLTFMaterialList::flushUpdates(done_callback);
if (!override_func.getResult())
{
// OverrideFunctor didn't find expected object or face
mOverrideInProgress = false;
}
// we posted all changes
mUnsavedChanges = 0;
mRevertedChanges = 0;
}
}
else
{
LL_WARNS("MaterialEditor") << "Not connected to materials capable region, missing ModifyMaterialParams cap" << LL_ENDL;
// Fallback local preview. Will be removed once override systems is finished and new cap is deployed everywhere.
LLPointer<LLFetchedGLTFMaterial> mat = new LLFetchedGLTFMaterial();
getGLTFMaterial(mat);
static const LLUUID placeholder("984e183e-7811-4b05-a502-d79c6f978a98");
gGLTFMaterialList.addMaterial(placeholder, mat);
LLRenderMaterialFunctor mat_func(placeholder);
LLObjectSelectionHandle selected_objects = LLSelectMgr::getInstance()->getSelection();
selected_objects->applyToTEs(&mat_func);
}
}
void LLMaterialEditor::getGLTFMaterial(LLGLTFMaterial* mat)
{
mat->mBaseColor = getBaseColor();
mat->mBaseColor.mV[3] = getTransparency();
mat->mBaseColorId = getBaseColorId();
mat->mNormalId = getNormalId();
mat->mMetallicRoughnessId = getMetallicRoughnessId();
mat->mMetallicFactor = getMetalnessFactor();
mat->mRoughnessFactor = getRoughnessFactor();
mat->mEmissiveColor = getEmissiveColor();
mat->mEmissiveId = getEmissiveId();
mat->mDoubleSided = getDoubleSided();
mat->setAlphaMode(getAlphaMode());
mat->mAlphaCutoff = getAlphaCutoff();
}
void LLMaterialEditor::setFromGLTFMaterial(LLGLTFMaterial* mat)
{
setBaseColor(mat->mBaseColor);
setBaseColorId(mat->mBaseColorId);
setNormalId(mat->mNormalId);
setMetallicRoughnessId(mat->mMetallicRoughnessId);
setMetalnessFactor(mat->mMetallicFactor);
setRoughnessFactor(mat->mRoughnessFactor);
setEmissiveColor(mat->mEmissiveColor);
setEmissiveId(mat->mEmissiveId);
setDoubleSided(mat->mDoubleSided);
setAlphaMode(mat->getAlphaMode());
setAlphaCutoff(mat->mAlphaCutoff);
}
bool LLMaterialEditor::setFromSelection()
{
LLObjectSelectionHandle selected_objects = LLSelectMgr::getInstance()->getSelection();
LLSelectedTEGetMatData func(mIsOverride);
selected_objects->applyToTEs(&func);
mHasSelection = !selected_objects->isEmpty();
mSelectionNeedsUpdate = false;
if (func.mMaterial.notNull())
{
setFromGLTFMaterial(func.mMaterial);
setEnableEditing(true);
}
else
{
// pick defaults from a blank material;
LLGLTFMaterial blank_mat;
setFromGLTFMaterial(&blank_mat);
if (mIsOverride)
{
setEnableEditing(false);
}
}
if (mIsOverride)
{
mBaseColorTextureCtrl->setTentative(!func.mIdenticalTexColor);
mMetallicTextureCtrl->setTentative(!func.mIdenticalTexMetal);
mEmissiveTextureCtrl->setTentative(!func.mIdenticalTexEmissive);
mNormalTextureCtrl->setTentative(!func.mIdenticalTexNormal);
// Memorize selection data for filtering further updates
mOverrideObjectId = func.mObjectId;
mOverrideObjectTE = func.mObjectTE;
}
return func.mMaterial.notNull();
}
void LLMaterialEditor::loadAsset()
{
const LLInventoryItem* item;
if (mNotecardInventoryID.notNull())
{
item = mAuxItem.get();
}
else
{
item = getItem();
}
bool fail = false;
if (item)
{
LLPermissions perm(item->getPermissions());
bool allow_copy = gAgent.allowOperation(PERM_COPY, perm, GP_OBJECT_MANIPULATE);
bool allow_modify = canModify(mObjectUUID, item);
bool source_library = mObjectUUID.isNull() && gInventory.isObjectDescendentOf(mItemUUID, gInventory.getLibraryRootFolderID());
setCanSaveAs(allow_copy);
setMaterialName(item->getName());
{
mAssetID = item->getAssetUUID();
if (mAssetID.isNull())
{
mAssetStatus = PREVIEW_ASSET_LOADED;
loadDefaults();
resetUnsavedChanges();
setEnableEditing(allow_modify && !source_library);
}
else
{
LLHost source_sim = LLHost();
LLSD* user_data = new LLSD();
if (mNotecardInventoryID.notNull())
{
user_data->with("objectid", mNotecardObjectID).with("notecardid", mNotecardInventoryID);
}
else if (mObjectUUID.notNull())
{
LLViewerObject* objectp = gObjectList.findObject(mObjectUUID);
if (objectp && objectp->getRegion())
{
source_sim = objectp->getRegion()->getHost();
}
else
{
// The object that we're trying to look at disappeared, bail.
LL_WARNS("MaterialEditor") << "Can't find object " << mObjectUUID << " associated with material." << LL_ENDL;
mAssetID.setNull();
mAssetStatus = PREVIEW_ASSET_LOADED;
resetUnsavedChanges();
setEnableEditing(allow_modify && !source_library);
return;
}
user_data->with("taskid", mObjectUUID).with("itemid", mItemUUID);
}
else
{
user_data = new LLSD(mItemUUID);
}
setEnableEditing(false); // wait for it to load
// request the asset.
gAssetStorage->getInvItemAsset(source_sim,
gAgent.getID(),
gAgent.getSessionID(),
item->getPermissions().getOwner(),
mObjectUUID,
item->getUUID(),
item->getAssetUUID(),
item->getType(),
&onLoadComplete,
(void*)user_data,
TRUE);
mAssetStatus = PREVIEW_ASSET_LOADING;
}
}
}
else if (mObjectUUID.notNull() && mItemUUID.notNull())
{
LLViewerObject* objectp = gObjectList.findObject(mObjectUUID);
if (objectp && (objectp->isInventoryPending() || objectp->isInventoryDirty()))
{
// It's a material in object's inventory and we failed to get it because inventory is not up to date.
// Subscribe for callback and retry at inventoryChanged()
registerVOInventoryListener(objectp, NULL); //removes previous listener
if (objectp->isInventoryDirty())
{
objectp->requestInventory();
}
}
else
{
fail = true;
}
}
else
{
fail = true;
}
if (fail)
{
/*editor->setText(LLStringUtil::null);
editor->makePristine();
editor->setEnabled(TRUE);*/
// Don't set asset status here; we may not have set the item id yet
// (e.g. when this gets called initially)
//mAssetStatus = PREVIEW_ASSET_LOADED;
}
}
// static
void LLMaterialEditor::onLoadComplete(const LLUUID& asset_uuid,
LLAssetType::EType type,
void* user_data, S32 status, LLExtStat ext_status)
{
LLSD* floater_key = (LLSD*)user_data;
LL_DEBUGS("MaterialEditor") << "loading " << asset_uuid << " for " << *floater_key << LL_ENDL;
LLMaterialEditor* editor = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", *floater_key);
if (editor)
{
if (asset_uuid != editor->mAssetID)
{
LL_WARNS() << "Asset id mismatch, expected: " << editor->mAssetID << " got: " << asset_uuid << LL_ENDL;
}
if (0 == status)
{
LLFileSystem file(asset_uuid, type, LLFileSystem::READ);
S32 file_length = file.getSize();
std::vector<char> buffer(file_length + 1);
file.read((U8*)&buffer[0], file_length);
editor->decodeAsset(buffer);
BOOL allow_modify = editor->canModify(editor->mObjectUUID, editor->getItem());
BOOL source_library = editor->mObjectUUID.isNull() && gInventory.isObjectDescendentOf(editor->mItemUUID, gInventory.getLibraryRootFolderID());
editor->setEnableEditing(allow_modify && !source_library);
editor->resetUnsavedChanges();
editor->mAssetStatus = PREVIEW_ASSET_LOADED;
editor->setEnabled(true); // ready for use
}
else
{
if (LL_ERR_ASSET_REQUEST_NOT_IN_DATABASE == status ||
LL_ERR_FILE_EMPTY == status)
{
LLNotificationsUtil::add("MaterialMissing");
}
else if (LL_ERR_INSUFFICIENT_PERMISSIONS == status)
{
LLNotificationsUtil::add("MaterialNoPermissions");
}
else
{
LLNotificationsUtil::add("UnableToLoadMaterial");
}
editor->setEnableEditing(false);
LL_WARNS() << "Problem loading material: " << status << LL_ENDL;
editor->mAssetStatus = PREVIEW_ASSET_ERROR;
}
}
else
{
LL_DEBUGS("MaterialEditor") << "Floater " << *floater_key << " does not exist." << LL_ENDL;
}
delete floater_key;
}
void LLMaterialEditor::inventoryChanged(LLViewerObject* object,
LLInventoryObject::object_list_t* inventory,
S32 serial_num,
void* user_data)
{
removeVOInventoryListener();
loadAsset();
}
void LLMaterialEditor::saveTexture(LLImageJ2C* img, const std::string& name, const LLUUID& asset_id, upload_callback_f cb)
{
if (asset_id.isNull()
|| img == nullptr
|| img->getDataSize() == 0)
{
return;
}
// copy image bytes into string
std::string buffer;
buffer.assign((const char*) img->getData(), img->getDataSize());
U32 expected_upload_cost = LLAgentBenefitsMgr::current().getTextureUploadCost();
LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLNewBufferedResourceUploadInfo>(
buffer,
asset_id,
name,
name,
0,
LLFolderType::FT_TEXTURE,
LLInventoryType::IT_TEXTURE,
LLAssetType::AT_TEXTURE,
LLFloaterPerms::getNextOwnerPerms("Uploads"),
LLFloaterPerms::getGroupPerms("Uploads"),
LLFloaterPerms::getEveryonePerms("Uploads"),
expected_upload_cost,
false,
cb));
upload_new_resource(uploadInfo);
}
S32 LLMaterialEditor::saveTextures()
{
S32 work_count = 0;
LLSD key = getKey(); // must be locally declared for lambda's capture to work
if (mBaseColorTextureUploadId == getBaseColorId() && mBaseColorTextureUploadId.notNull())
{
mUploadingTexturesCount++;
work_count++;
saveTexture(mBaseColorJ2C, mBaseColorName, mBaseColorTextureUploadId, [key](LLUUID newAssetId, LLSD response)
{
LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", key);
if (me)
{
if (response["success"].asBoolean())
{
me->setBaseColorId(newAssetId);
}
else
{
// To make sure that we won't retry (some failures can cb immediately)
me->setBaseColorId(LLUUID::null);
}
me->mUploadingTexturesCount--;
// try saving
me->saveIfNeeded();
}
});
}
if (mNormalTextureUploadId == getNormalId() && mNormalTextureUploadId.notNull())
{
mUploadingTexturesCount++;
work_count++;
saveTexture(mNormalJ2C, mNormalName, mNormalTextureUploadId, [key](LLUUID newAssetId, LLSD response)
{
LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", key);
if (me)
{
if (response["success"].asBoolean())
{
me->setNormalId(newAssetId);
}
else
{
me->setNormalId(LLUUID::null);
}
me->setNormalId(newAssetId);
me->mUploadingTexturesCount--;
// try saving
me->saveIfNeeded();
}
});
}
if (mMetallicTextureUploadId == getMetallicRoughnessId() && mMetallicTextureUploadId.notNull())
{
mUploadingTexturesCount++;
work_count++;
saveTexture(mMetallicRoughnessJ2C, mMetallicRoughnessName, mMetallicTextureUploadId, [key](LLUUID newAssetId, LLSD response)
{
LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", key);
if (me)
{
if (response["success"].asBoolean())
{
me->setMetallicRoughnessId(newAssetId);
}
else
{
me->setMetallicRoughnessId(LLUUID::null);
}
me->mUploadingTexturesCount--;
// try saving
me->saveIfNeeded();
}
});
}
if (mEmissiveTextureUploadId == getEmissiveId() && mEmissiveTextureUploadId.notNull())
{
mUploadingTexturesCount++;
work_count++;
saveTexture(mEmissiveJ2C, mEmissiveName, mEmissiveTextureUploadId, [key](LLUUID newAssetId, LLSD response)
{
LLMaterialEditor* me = LLFloaterReg::findTypedInstance<LLMaterialEditor>("material_editor", LLSD(key));
if (me)
{
if (response["success"].asBoolean())
{
me->setEmissiveId(newAssetId);
}
else
{
me->setEmissiveId(LLUUID::null);
}
me->mUploadingTexturesCount--;
// try saving
me->saveIfNeeded();
}
});
}
// discard upload buffers once textures have been saved
clearTextures();
// asset storage can callback immediately, causing a decrease
// of mUploadingTexturesCount, report amount of work scheduled
// not amount of work remaining
return work_count;
}
void LLMaterialEditor::clearTextures()
{
mBaseColorJ2C = nullptr;
mNormalJ2C = nullptr;
mEmissiveJ2C = nullptr;
mMetallicRoughnessJ2C = nullptr;
mBaseColorFetched = nullptr;
mNormalFetched = nullptr;
mMetallicRoughnessFetched = nullptr;
mEmissiveFetched = nullptr;
mBaseColorTextureUploadId.setNull();
mNormalTextureUploadId.setNull();
mMetallicTextureUploadId.setNull();
mEmissiveTextureUploadId.setNull();
}
void LLMaterialEditor::loadDefaults()
{
tinygltf::Model model_in;
model_in.materials.resize(1);
setFromGltfModel(model_in, 0, true);
}
bool LLMaterialEditor::capabilitiesAvailable()
{
const LLViewerRegion* region = gAgent.getRegion();
if (!region)
{
LL_WARNS("MaterialEditor") << "Not connected to a region, cannot save material." << LL_ENDL;
return false;
}
std::string agent_url = region->getCapability("UpdateMaterialAgentInventory");
std::string task_url = region->getCapability("UpdateMaterialTaskInventory");
return (!agent_url.empty() && !task_url.empty());
}