diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 9fea885622..a378c9f799 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -135,6 +135,7 @@ if(AL_BUILD_VIEWER) alassetblocklist.cpp alavataractions.cpp alavatargroups.cpp + albakeproxies.cpp alchatautocomplete.cpp alchatbar.cpp alchatcommand.cpp @@ -1011,6 +1012,7 @@ if(AL_BUILD_VIEWER) alassetblocklist.h alavataractions.h alavatargroups.h + albakeproxies.h alchatautocomplete.h alchatbar.h alchatcommand.h @@ -2535,6 +2537,7 @@ if(AL_BUILD_TESTS) PROJECT ${test_project} SOURCES tests/alagentlocationformat_test.cpp + tests/albakeproxies_test.cpp tests/alchattimestamp_test.cpp tests/alcolorwheelmodel_test.cpp tests/alcrashreporter_test.cpp @@ -2609,6 +2612,7 @@ if(AL_BUILD_TESTS) tests/noise_test.cpp UNIT alagentlocationformat.cpp + albakeproxies.cpp alchattimestamp.cpp alcolorwheelmodel.cpp alcrashreporter.cpp diff --git a/indra/newview/albakeproxies.cpp b/indra/newview/albakeproxies.cpp new file mode 100644 index 0000000000..53f9bcd2c9 --- /dev/null +++ b/indra/newview/albakeproxies.cpp @@ -0,0 +1,69 @@ +/** + * @file albakeproxies.cpp + * @brief The bakes a face names through bake proxies (IMG_USE_BAKED_*) + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "linden_common.h" + +#include "albakeproxies.h" + +#include "llgltfmaterial.h" +#include "lltextureentry.h" + +using namespace LLAvatarAppearanceDefines; + +namespace +{ + // The bake a texture ID names, where it is a bake proxy + ALBakeProxies::bake_set_t named_by(const LLUUID& id) + { + ALBakeProxies::bake_set_t bakes; + const EBakedTextureIndex index = LLAvatarAppearanceDictionary::assetIdToBakedTextureIndex(id); + if (index != BAKED_NUM_INDICES) + { + bakes.set(index); + } + return bakes; + } +} + +namespace ALBakeProxies +{ + +bool anyNamedBy(const LLGLTFMaterial& material) +{ + for (const LLUUID& id : material.mTextureId) + { + if (LLAvatarAppearanceDictionary::isBakedImageId(id)) + { + return true; + } + } + return false; +} + +bake_set_t drawnBy(const LLGLTFMaterial* material) +{ + return material ? named_by(material->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]) : bake_set_t(); +} + +bake_set_t drawnBy(const LLTextureEntry& te, bool hud) +{ + bake_set_t bakes = named_by(te.getID()); + if (!hud) + { + bakes |= drawnBy(te.getGLTFRenderMaterial()); + } + return bakes; +} + +} diff --git a/indra/newview/albakeproxies.h b/indra/newview/albakeproxies.h new file mode 100644 index 0000000000..18c54b5871 --- /dev/null +++ b/indra/newview/albakeproxies.h @@ -0,0 +1,45 @@ +/** + * @file albakeproxies.h + * @brief The bakes a face names through bake proxies (IMG_USE_BAKED_*): those an avatar wearing it hides + * the system body parts of, and whether its PBR material resolves one for its own avatar. + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_BAKEPROXIES_H +#define AL_BAKEPROXIES_H + +#include "llavatarappearancedefines.h" + +#include + +class LLGLTFMaterial; +class LLTextureEntry; + +namespace ALBakeProxies +{ + // A set of bakes, by EBakedTextureIndex + typedef std::bitset bake_set_t; + + // Whether any slot of a PBR material names a bake proxy, which the face drawing it resolves for its own + // object's avatar, and so draws from a copy of its own (LLViewerObject::initRenderMaterial) + bool anyNamedBy(const LLGLTFMaterial& material); + + // The bake a PBR material draws a body with: the one its base colour names, the slot that stands for a + // face's image. A bake in its other slots shades the surface without drawing a body + bake_set_t drawnBy(const LLGLTFMaterial* material); + + // The bakes a face draws a body with, whose system body parts an avatar wearing it hides + // (LLVOAvatar::updateMeshVisibility): its image's and its PBR material's. A HUD's face resolves no bake + // (LLViewerObject::getBakedTextureForMagicId): its image counts, as Linden's viewer counts it, and its + // material does not + bake_set_t drawnBy(const LLTextureEntry& te, bool hud); +} + +#endif // AL_BAKEPROXIES_H diff --git a/indra/newview/alselectionoutline.cpp b/indra/newview/alselectionoutline.cpp index 6ac7cc51ee..d4f237df87 100644 --- a/indra/newview/alselectionoutline.cpp +++ b/indra/newview/alselectionoutline.cpp @@ -885,7 +885,7 @@ ALSelectionOutline::FaceBinding ALSelectionOutline::bindFace(LLGLSLShader& progr // LLFetchedGLTFMaterial::bind in its place. if (!face->hasMedia() || !texture || texture->getType() != LLViewerTexture::MEDIA_TEXTURE) { - texture = gltf->mBaseColorTexture.get(); + texture = gltf->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR); } if (texture && texture_has_alpha(texture)) { diff --git a/indra/newview/alviewermenu.cpp b/indra/newview/alviewermenu.cpp index a1b013cff7..bb69ca79e9 100644 --- a/indra/newview/alviewermenu.cpp +++ b/indra/newview/alviewermenu.cpp @@ -38,6 +38,7 @@ #include "llagentcamera.h" #include "llappviewer.h" #include "llavatarpropertiesprocessor.h" +#include "llfetchedgltfmaterial.h" #include "llhudobject.h" #include "llnotifications.h" #include "llnotificationsutil.h" @@ -48,6 +49,7 @@ #include "llviewerobject.h" #include "llviewerobjectlist.h" #include "llviewerregion.h" +#include "llviewertexturelist.h" #include "llvoavatar.h" #include "llvoavatarself.h" @@ -381,7 +383,9 @@ namespace if (id.isNull() || id == IMG_DEFAULT || id == IMG_TRANSPARENT|| id == "8dcd4a48-2d37-4909-9f78-f7a9eb4ef903") return; - LLViewerFetchedTexture* texture = LLViewerTextureManager::getFetchedTexture(id); + // Looked up, not created: an ID no fetched texture has, such as a dynamic texture's, has + // nothing to clear, and creating one would start a download nothing draws + LLViewerFetchedTexture* texture = gTextureList.findImage(id, TEX_LIST_STANDARD); if (texture) texture->clearFetchedResults(); LLAppViewer::getTextureCache()->removeFromCache(id); @@ -412,12 +416,16 @@ namespace faces_per_tex[spec_img->getID()].push_back(i); } - LLPointer mat = node->getObject()->getTE(i)->getGLTFRenderMaterial(); - if (mat.notNull()) + // What each slot draws, as getTEImage is for the image: a bake proxy's bake, not the + // proxy's own ID. A copy of the material not yet built since it was made + // (initRenderMaterial) draws nothing yet, and its slots' own IDs are what it will draw + const auto* mat = static_cast(node->getObject()->getTE(i)->getGLTFRenderMaterial()); + if (mat) { for (U32 j = 0; j < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++j) { - faces_per_tex[mat->mTextureId[j]].push_back(i); + const LLViewerTexture* tex = mat->getRenderTexture((LLGLTFMaterial::TextureInfo)j); + faces_per_tex[tex ? tex->getID() : mat->mTextureId[j]].push_back(i); } } } diff --git a/indra/newview/lldrawpool.cpp b/indra/newview/lldrawpool.cpp index bec7df7d8d..b66fb5f6bd 100644 --- a/indra/newview/lldrawpool.cpp +++ b/indra/newview/lldrawpool.cpp @@ -1220,7 +1220,8 @@ void LLRenderPass::pushGLTFBatchIndexed(LLDrawInfo& params, eGLTFIndexedMaps map // vs data split LLFetchedGLTFMaterial::bind makes, and it has to match: a batched and // an unbatched copy of the same material must sample identically, or they shade // differently for no reason the content author can see. - LLViewerTexture* base = mat->mBaseColorTexture.notNull() ? mat->mBaseColorTexture.get() : LLViewerFetchedTexture::sWhiteImagep.get(); + LLViewerTexture* base = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR); + base = base ? base : LLViewerFetchedTexture::sWhiteImagep.get(); gGL.getTextureSlot(s)->bindFast(base, ALSamplers::AnisoWrapSRGB); min_alpha[s] = (mat->mAlphaMode == LLGLTFMaterial::ALPHA_MODE_MASK) ? mat->mAlphaCutoff : -1.f; @@ -1236,7 +1237,8 @@ void LLRenderPass::pushGLTFBatchIndexed(LLDrawInfo& params, eGLTFIndexedMaps map } // emissive map/color/transform -- needed by both the glow and GBuffer passes - LLViewerTexture* em = mat->mEmissiveTexture.notNull() ? mat->mEmissiveTexture.get() : LLViewerFetchedTexture::sWhiteImagep.get(); + LLViewerTexture* em = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE); + em = em ? em : LLViewerFetchedTexture::sWhiteImagep.get(); gGL.getTextureSlot(3 * N + s)->bindFast(em, ALSamplers::AnisoWrapSRGB); emissive[3 * s + 0] = mat->mEmissiveColor.mV[0]; @@ -1254,8 +1256,10 @@ void LLRenderPass::pushGLTFBatchIndexed(LLDrawInfo& params, eGLTFIndexedMaps map continue; } - LLViewerTexture* norm = (mat->mNormalTexture.notNull() && mat->mNormalTexture->getDiscardLevel() <= 4) ? mat->mNormalTexture.get() : LLViewerFetchedTexture::sFlatNormalImagep.get(); - LLViewerTexture* orm = mat->mMetallicRoughnessTexture.notNull() ? mat->mMetallicRoughnessTexture.get() : LLViewerFetchedTexture::sWhiteImagep.get(); + LLViewerTexture* norm = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL); + norm = (norm && norm->getDiscardLevel() <= 4) ? norm : LLViewerFetchedTexture::sFlatNormalImagep.get(); + LLViewerTexture* orm = mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS); + orm = orm ? orm : LLViewerFetchedTexture::sWhiteImagep.get(); gGL.getTextureSlot(N + s)->bindFast(norm, ALSamplers::AnisoWrap); gGL.getTextureSlot(2 * N + s)->bindFast(orm, ALSamplers::AnisoWrap); diff --git a/indra/newview/llfetchedgltfmaterial.cpp b/indra/newview/llfetchedgltfmaterial.cpp index bd04cfbce4..74bd423533 100644 --- a/indra/newview/llfetchedgltfmaterial.cpp +++ b/indra/newview/llfetchedgltfmaterial.cpp @@ -57,6 +57,7 @@ LLFetchedGLTFMaterial& LLFetchedGLTFMaterial::operator=(const LLFetchedGLTFMater mNormalTexture = rhs.mNormalTexture; mMetallicRoughnessTexture = rhs.mMetallicRoughnessTexture; mEmissiveTexture = rhs.mEmissiveTexture; + std::copy(std::begin(rhs.mBakedTextures), std::end(rhs.mBakedTextures), std::begin(mBakedTextures)); return *this; } @@ -95,8 +96,8 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex) LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr; // override emissive and base color textures with media tex if present - LLViewerTexture* baseColorTex = media_tex ? media_tex : mBaseColorTexture; - LLViewerTexture* emissiveTex = media_tex ? media_tex : mEmissiveTexture; + LLViewerTexture* baseColorTex = media_tex ? media_tex : getRenderTexture(GLTF_TEXTURE_INFO_BASE_COLOR); + LLViewerTexture* emissiveTex = media_tex ? media_tex : getRenderTexture(GLTF_TEXTURE_INFO_EMISSIVE); // sImpostorRender is checked alongside sShadowRender throughout this function because // generateImpostor sets BOTH, and only one of them means "depth-only". An impostor bake @@ -136,18 +137,20 @@ void LLFetchedGLTFMaterial::bind(LLViewerTexture* media_tex) if (!depth_only_pass) { - if (mNormalTexture.notNull() && mNormalTexture->getDiscardLevel() <= 4) + LLViewerTexture* normalTex = getRenderTexture(GLTF_TEXTURE_INFO_NORMAL); + if (normalTex != nullptr && normalTex->getDiscardLevel() <= 4) { - shader->bindTexture(LLShaderMgr::BUMP_MAP, mNormalTexture, GLTF_DATA_SAMPLER); + shader->bindTexture(LLShaderMgr::BUMP_MAP, normalTex, GLTF_DATA_SAMPLER); } else { shader->bindTexture(LLShaderMgr::BUMP_MAP, LLViewerFetchedTexture::sFlatNormalImagep, GLTF_DATA_SAMPLER); } - if (mMetallicRoughnessTexture.notNull()) + LLViewerTexture* metallicRoughnessTex = getRenderTexture(GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS); + if (metallicRoughnessTex != nullptr) { - shader->bindTexture(LLShaderMgr::SPECULAR_MAP, mMetallicRoughnessTexture, GLTF_DATA_SAMPLER); // PBR linear packed Occlusion, Roughness, Metal. + shader->bindTexture(LLShaderMgr::SPECULAR_MAP, metallicRoughnessTex, GLTF_DATA_SAMPLER); // PBR linear packed Occlusion, Roughness, Metal. } else { @@ -269,6 +272,7 @@ void LLFetchedGLTFMaterial::clearFetchedTextures() mNormalTexture = nullptr; mMetallicRoughnessTexture = nullptr; mEmissiveTexture = nullptr; + std::fill(std::begin(mBakedTextures), std::end(mBakedTextures), nullptr); } void LLFetchedGLTFMaterial::materialBegin() diff --git a/indra/newview/llfetchedgltfmaterial.h b/indra/newview/llfetchedgltfmaterial.h index 3530660603..b3a13b6d99 100644 --- a/indra/newview/llfetchedgltfmaterial.h +++ b/indra/newview/llfetchedgltfmaterial.h @@ -71,6 +71,30 @@ class LLFetchedGLTFMaterial: public LLGLTFMaterial LLPointer mNormalTexture; LLPointer mMetallicRoughnessTexture; LLPointer mEmissiveTexture; + + // What a slot naming a bake proxy (IMG_USE_BAKED_*) resolved to on the face this render + // material is drawn on (LLViewerObject::updateTEMaterialTextures, which fetches nothing for + // it): the server's bake, or the local composite while the avatar edits its appearance, which + // is no fetched texture and so has no place in the slots above. Null for any other slot. + LLPointer mBakedTextures[GLTF_TEXTURE_INFO_COUNT]; + + // The texture a pass draws a slot with: its bake, else its fetched texture + LLViewerTexture* getRenderTexture(TextureInfo slot) const + { + if (mBakedTextures[slot].notNull()) + { + return mBakedTextures[slot]; + } + switch (slot) + { + case GLTF_TEXTURE_INFO_BASE_COLOR: return mBaseColorTexture; + case GLTF_TEXTURE_INFO_NORMAL: return mNormalTexture; + case GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS: return mMetallicRoughnessTexture; + case GLTF_TEXTURE_INFO_EMISSIVE: return mEmissiveTexture; + default: return nullptr; + } + } + void clearFetchedTextures(); std::set mTextureEntires; diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index 4b527212aa..7c86bd66ef 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -54,6 +54,7 @@ #include "object_flags.h" #include "alassetblocklist.h" +#include "albakeproxies.h" #include "llaudiosourcevo.h" #include "llagent.h" #include "llagentcamera.h" @@ -5413,7 +5414,24 @@ void LLViewerObject::updateAvatarMeshVisibility(const LLUUID& id, const LLUUID& LLVOAvatar* avatar = getAvatar(); if (avatar) { - avatar->updateMeshVisibility(); + avatar->dirtyMeshVisibility(); + } +} + +void LLViewerObject::updateAvatarMeshVisibility(U8 te, const LLGLTFMaterial* old_material) +{ + const LLTextureEntry* tep = getTE(te); + const LLGLTFMaterial* material = tep ? tep->getGLTFRenderMaterial() : nullptr; + // a HUD's material draws no bake (ALBakeProxies::drawnBy) + if (material == old_material || isHUDAttachment()) + { + return; + } + + LLVOAvatar* avatar = getAvatar(); + if (avatar && ALBakeProxies::drawnBy(material) != ALBakeProxies::drawnBy(old_material)) + { + avatar->dirtyMeshVisibility(); } } @@ -5457,17 +5475,21 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) llassert(mat == nullptr || dynamic_cast(gGLTFMaterialList.getMaterial(mat_id)) != nullptr); if (mat->isFetching()) { // material is not loaded yet, rebuild draw info when the object finishes loading - mat->onMaterialComplete([id=getID()] + mat->onMaterialComplete([id=getID(), te] { LLViewerObject* obj = gObjectList.findObject(id); if (obj) { + obj->onRenderMaterialFetched(te); obj->markForUpdate(); } }); } getTE(te)->setGLTFMaterial(mat); initRenderMaterial(te); + // initRenderMaterial compared the face's copy with the material just set on it, which names the + // same bakes: the face had no material before + updateAvatarMeshVisibility(te, nullptr); mat = (LLFetchedGLTFMaterial*) getTE(te)->getGLTFRenderMaterial(); llassert(mat == nullptr || dynamic_cast(getTE(te)->getGLTFRenderMaterial()) != nullptr); } @@ -5477,22 +5499,28 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) getTE(te)->setGLTFMaterial(nullptr); } - auto fetch_texture = [this](const LLUUID& id) + // The bakes a material names are resolved for this object's avatar, into the face's own copy of the + // material (initRenderMaterial): resolved into the material itself, they would be drawn on every face it + // is on, whoever wears it. A face still drawing the material itself, as one whose texture entry was + // copied (a copied LLTextureEntry keeps no render material), gets its copy here + if (mat != nullptr && mat == getTE(te)->getGLTFMaterial() && ALBakeProxies::anyNamedBy(*mat)) + { + initRenderMaterial(te); + mat = (LLFetchedGLTFMaterial*) getTE(te)->getGLTFRenderMaterial(); + } + + // A slot naming a bake proxy draws what it resolves to on this object's avatar + // (LLFetchedGLTFMaterial::mBakedTextures), and fetches nothing; any other fetches its asset + auto fetch_texture = [this, mat](LLGLTFMaterial::TextureInfo slot) { + const LLUUID& id = mat->mTextureId[slot]; + mat->mBakedTextures[slot] = getBakedTextureForMagicId(id); + LLViewerFetchedTexture* img = nullptr; - if (id.notNull()) + if (id.notNull() && mat->mBakedTextures[slot].isNull()) { - if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(id)) - { - // TODO -- fall back to LLTextureEntry::mGLTFRenderMaterial when overriding with baked texture - LLViewerTexture* viewerTexture = getBakedTextureForMagicId(id); - img = viewerTexture ? dynamic_cast(viewerTexture) : nullptr; - } - else - { - img = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE); - img->addTextureStats(64.f * 64.f, true); - } + img = LLViewerTextureManager::getFetchedTexture(id, FTT_DEFAULT, true, LLGLTexture::BOOST_NONE, LLViewerTexture::LOD_TEXTURE); + img->addTextureStats(64.f * 64.f, true); } return img; @@ -5500,10 +5528,10 @@ void LLViewerObject::updateTEMaterialTextures(U8 te) if (mat != nullptr) { - mat->mBaseColorTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR]); - mat->mNormalTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL]); - mat->mMetallicRoughnessTexture = fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS]); - mat->mEmissiveTexture= fetch_texture(mat->mTextureId[LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE]); + mat->mBaseColorTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR); + mat->mNormalTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL); + mat->mMetallicRoughnessTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS); + mat->mEmissiveTexture = fetch_texture(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE); } } @@ -5512,12 +5540,33 @@ void LLViewerObject::refreshBakeTexture() for (int face_index = 0; face_index < getNumTEs(); face_index++) { LLTextureEntry* tex_entry = getTE(face_index); - if (tex_entry && LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(tex_entry->getID())) + if (!tex_entry) + { + continue; + } + + if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(tex_entry->getID())) { const LLUUID& image_id = tex_entry->getID(); LLViewerTexture* bakedTexture = getBakedTextureForMagicId(image_id); changeTEImage(face_index, bakedTexture); } + + // A PBR face draws the bake proxies its render material names from what the material holds + // for them, resolved when the face was last built: resolve them again where one now resolves + // to another texture, whatever the face's own image names + const LLFetchedGLTFMaterial* mat = static_cast(tex_entry->getGLTFRenderMaterial()); + if (mat != nullptr) + { + for (U32 slot = 0; slot < LLGLTFMaterial::GLTF_TEXTURE_INFO_COUNT; ++slot) + { + if (getBakedTextureForMagicId(mat->mTextureId[slot]) != mat->mBakedTextures[slot].get()) + { + changeTEMaterialTextures(face_index); + break; + } + } + } } } @@ -5617,6 +5666,16 @@ void LLViewerObject::changeTEImage(S32 index, LLViewerTexture* new_image) mTEImages[index] = new_image ; } +//virtual +void LLViewerObject::changeTEMaterialTextures(S32 index) +{ + if (index < 0 || index >= getNumTEs()) + { + return; + } + updateTEMaterialTextures((U8)index); +} + void LLViewerObject::changeTENormalMap(S32 index, LLViewerTexture* new_image) { if(index < 0 || index >= getNumTEs()) @@ -5874,25 +5933,33 @@ S32 LLViewerObject::initRenderMaterial(U8 te) if (!base_material) { return 0; } const LLGLTFMaterial* override_material = tep->getGLTFMaterialOverride(); LLFetchedGLTFMaterial* render_material = nullptr; - bool need_render_material = override_material; - if (!need_render_material) - { - for (const LLUUID& texture_id : base_material->mTextureId) - { - if (LLAvatarAppearanceDefines::LLAvatarAppearanceDictionary::isBakedImageId(texture_id)) - { - need_render_material = true; - break; - } - } - } + const bool need_render_material = override_material || ALBakeProxies::anyNamedBy(*base_material); if (need_render_material) { render_material = new LLFetchedGLTFMaterial(*base_material); if (override_material) { render_material->applyOverride(*override_material); } render_material->clearFetchedTextures(); } - return tep->setGLTFRenderMaterial(render_material); + + // held, as setting the new one may release it + const LLPointer old_material = tep->getGLTFRenderMaterial(); + const S32 changed = tep->setGLTFRenderMaterial(render_material); + updateAvatarMeshVisibility(te, old_material); + return changed; +} + +void LLViewerObject::onRenderMaterialFetched(U8 te) +{ + const LLTextureEntry* tep = getTE(te); + if (!tep || !tep->getGLTFMaterial()) + { // the face lost its material while it fetched + return; + } + + initRenderMaterial(te); + // The material named no bake when the face took it and has filled in its textures since, so + // initRenderMaterial saw none of them change: look from nothing + updateAvatarMeshVisibility(te, nullptr); } S32 LLViewerObject::setTEGLTFMaterialOverride(U8 te, LLGLTFMaterial* override_mat) @@ -7793,6 +7860,8 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat for (S32 te = start_idx; te < end_idx; ++te) { LLTextureEntry* tep = getTE(te); + // held, as clearing the render material may release it + const LLPointer old_material = tep->getGLTFRenderMaterial(); // If local_origin=false (i.e. it's from the server), we know the // material has updated or been created, because extra params are @@ -7822,6 +7891,8 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat tep->setGLTFMaterial(new_material, !update_server); } + updateAvatarMeshVisibility((U8)te, old_material); + if (material_changed && new_material) { // Sometimes, the material may change out from underneath the overrides. @@ -7832,11 +7903,18 @@ void LLViewerObject::setRenderMaterialID(S32 te_in, const LLUUID& id, bool updat // Also, sometimes, the material has baked textures, which requires // a copy unique to this object. // Currently, we do not deduplicate render materials. - new_material->onMaterialComplete([obj_id = getID(), te]() + new_material->onMaterialComplete([obj_id = getID(), te, fetching = new_material->isFetching()]() { LLViewerObject* obj = gObjectList.findObject(obj_id); if (!obj) { return; } - obj->initRenderMaterial(te); + if (fetching) + { + obj->onRenderMaterialFetched((U8)te); + } + else + { + obj->initRenderMaterial(te); + } }); } } diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h index f51049f826..ef3d865323 100644 --- a/indra/newview/llviewerobject.h +++ b/indra/newview/llviewerobject.h @@ -387,6 +387,10 @@ class LLViewerObject /*virtual*/ S32 setTEMaterialID(const U8 te, const LLMaterialID& pMaterialID); /*virtual*/ S32 setTEMaterialParams(const U8 te, const LLMaterialPtr pMaterialParams); S32 initRenderMaterial(const U8 te); + // The material face te took while it was still fetching has completed: give the face its own + // copy where it needs one (initRenderMaterial), so the bakes it names are resolved for this + // object's avatar and not into the material every object using it shares + void onRenderMaterialFetched(const U8 te); virtual S32 setTEGLTFMaterialOverride(U8 te, LLGLTFMaterial* mat); // Used by Materials update functions to properly kick off rebuilds @@ -399,6 +403,9 @@ class LLViewerObject virtual void changeTEImage(S32 index, LLViewerTexture* new_image) ; virtual void changeTENormalMap(S32 index, LLViewerTexture* new_image) ; virtual void changeTESpecularMap(S32 index, LLViewerTexture* new_image) ; + // Resolve a face's render material textures again (updateTEMaterialTextures), as when a bake + // proxy it names resolves to another texture + virtual void changeTEMaterialTextures(S32 index); LLViewerTexture *getTEImage(const U8 te) const; LLViewerTexture *getTENormalMap(const U8 te) const; LLViewerTexture *getTESpecularMap(const U8 te) const; @@ -692,6 +699,8 @@ class LLViewerObject public: LLViewerTexture* getBakedTextureForMagicId(const LLUUID& id); void updateAvatarMeshVisibility(const LLUUID& id, const LLUUID& old_id); + // As above, for a face's PBR material: old_material is the render material it had before + void updateAvatarMeshVisibility(U8 te, const LLGLTFMaterial* old_material); void refreshBakeTexture(); public: static void unpackVector3(LLDataPackerBinaryBuffer* dp, LLVector3& value, std::string name); diff --git a/indra/newview/llvoavatar.cpp b/indra/newview/llvoavatar.cpp index 52e325ffbc..731c2c9bee 100644 --- a/indra/newview/llvoavatar.cpp +++ b/indra/newview/llvoavatar.cpp @@ -38,6 +38,7 @@ #include "raytrace.h" #include "alavatargroups.h" +#include "albakeproxies.h" #include "alrenderisolation.h" #include "aoengine.h" #include "llagent.h" // Get state values from here @@ -2854,6 +2855,11 @@ void LLVOAvatar::idleUpdate(LLAgent &agent, const F64 &time) return; } + if (mMeshVisibilityDirty) + { + updateMeshVisibility(); + } + readProfileQuery(); static LLCachedControl friends_only(gSavedSettings, "RenderAvatarFriendsOnly", false); @@ -9492,15 +9498,15 @@ void LLVOAvatar::debugColorizeSubMeshes(U32 i, const LLColor4& color) } } - //----------------------------------------------------------------------------- // updateMeshVisibility() // Hide the mesh joints if attachments are using baked textures //----------------------------------------------------------------------------- void LLVOAvatar::updateMeshVisibility() { - bool bake_flag[BAKED_NUM_INDICES]; - memset(bake_flag, 0, BAKED_NUM_INDICES*sizeof(bool)); + mMeshVisibilityDirty = false; + + ALBakeProxies::bake_set_t bake_flag; if (getOverallAppearance() == AOA_NORMAL) { @@ -9509,6 +9515,7 @@ void LLVOAvatar::updateMeshVisibility() if (!attachment) continue; + const bool hud = attachment->getIsHUDAttachment(); for (const auto& objectp : attachment->mAttachedObjects) { if (objectp.isNull()) @@ -9516,19 +9523,7 @@ void LLVOAvatar::updateMeshVisibility() for (int face_index = 0; face_index < objectp->getNumTEs(); face_index++) { - LLTextureEntry* tex_entry = objectp->getTE(face_index); - const auto& tex_id = tex_entry->getID(); - bake_flag[BAKED_HEAD] |= (tex_id == IMG_USE_BAKED_HEAD); - bake_flag[BAKED_EYES] |= (tex_id == IMG_USE_BAKED_EYES); - bake_flag[BAKED_HAIR] |= (tex_id == IMG_USE_BAKED_HAIR); - bake_flag[BAKED_LOWER] |= (tex_id == IMG_USE_BAKED_LOWER); - bake_flag[BAKED_UPPER] |= (tex_id == IMG_USE_BAKED_UPPER); - bake_flag[BAKED_SKIRT] |= (tex_id == IMG_USE_BAKED_SKIRT); - bake_flag[BAKED_LEFT_ARM] |= (tex_id == IMG_USE_BAKED_LEFTARM); - bake_flag[BAKED_LEFT_LEG] |= (tex_id == IMG_USE_BAKED_LEFTLEG); - bake_flag[BAKED_AUX1] |= (tex_id == IMG_USE_BAKED_AUX1); - bake_flag[BAKED_AUX2] |= (tex_id == IMG_USE_BAKED_AUX2); - bake_flag[BAKED_AUX3] |= (tex_id == IMG_USE_BAKED_AUX3); + bake_flag |= ALBakeProxies::drawnBy(*objectp->getTE(face_index), hud); } for (const auto& objectchild : objectp->getChildren()) @@ -9538,19 +9533,7 @@ void LLVOAvatar::updateMeshVisibility() for (int face_index = 0; face_index < objectchild->getNumTEs(); face_index++) { - LLTextureEntry* tex_entry = objectchild->getTE(face_index); - const auto& tex_id = tex_entry->getID(); - bake_flag[BAKED_HEAD] |= (tex_id == IMG_USE_BAKED_HEAD); - bake_flag[BAKED_EYES] |= (tex_id == IMG_USE_BAKED_EYES); - bake_flag[BAKED_HAIR] |= (tex_id == IMG_USE_BAKED_HAIR); - bake_flag[BAKED_LOWER] |= (tex_id == IMG_USE_BAKED_LOWER); - bake_flag[BAKED_UPPER] |= (tex_id == IMG_USE_BAKED_UPPER); - bake_flag[BAKED_SKIRT] |= (tex_id == IMG_USE_BAKED_SKIRT); - bake_flag[BAKED_LEFT_ARM] |= (tex_id == IMG_USE_BAKED_LEFTARM); - bake_flag[BAKED_LEFT_LEG] |= (tex_id == IMG_USE_BAKED_LEFTLEG); - bake_flag[BAKED_AUX1] |= (tex_id == IMG_USE_BAKED_AUX1); - bake_flag[BAKED_AUX2] |= (tex_id == IMG_USE_BAKED_AUX2); - bake_flag[BAKED_AUX3] |= (tex_id == IMG_USE_BAKED_AUX3); + bake_flag |= ALBakeProxies::drawnBy(*objectchild->getTE(face_index), hud); } } } diff --git a/indra/newview/llvoavatar.h b/indra/newview/llvoavatar.h index 1688b23036..bca3f74a93 100644 --- a/indra/newview/llvoavatar.h +++ b/indra/newview/llvoavatar.h @@ -940,6 +940,9 @@ class LLVOAvatar : virtual void dirtyMesh(); // Dirty the avatar mesh void updateMeshData(); void updateMeshVisibility(); + // updateMeshVisibility at the avatar's next idle update, once however many of its attachments' faces + // ask before then: a material its body's faces share completes for all of them at once + void dirtyMeshVisibility() { mMeshVisibilityDirty = true; } LLViewerTexture* getBakedTexture(const U8 te); // Matrix palette cache entry @@ -989,6 +992,7 @@ class LLVOAvatar : LLViewerJoint* getViewerJoint(S32 idx); S32 mDirtyMesh; // 0 -- not dirty, 1 -- morphed, 2 -- LOD bool mMeshTexturesDirty; + bool mMeshVisibilityDirty = false; //-------------------------------------------------------------------- // Destroy invisible mesh diff --git a/indra/newview/llvovolume.cpp b/indra/newview/llvovolume.cpp index 71fa8c240d..a7fd2366d7 100644 --- a/indra/newview/llvovolume.cpp +++ b/indra/newview/llvovolume.cpp @@ -2529,6 +2529,15 @@ void LLVOVolume::changeTEImage(S32 index, LLViewerTexture* imagep) } } +//virtual +void LLVOVolume::changeTEMaterialTextures(S32 index) +{ + LLViewerObject::changeTEMaterialTextures(index); + // the rebuild hands the faces' texture channels what the material now draws + gPipeline.markTextured(mDrawable); + mFaceMappingChanged = true; +} + void LLVOVolume::setTEImage(const U8 te, LLViewerTexture *imagep) { bool changed = (mTEImages[te] != imagep); @@ -6348,10 +6357,10 @@ void LLVolumeGeometryManager::rebuildGeom(LLSpatialGroup* group) facep->setTexture(LLRender::SPECULAR_MAP, nullptr); // let texture streaming system know about PBR textures - facep->setTexture(LLRender::BASECOLOR_MAP, gltf_mat->mBaseColorTexture); - facep->setTexture(LLRender::GLTF_NORMAL_MAP, gltf_mat->mNormalTexture); - facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, gltf_mat->mMetallicRoughnessTexture); - facep->setTexture(LLRender::EMISSIVE_MAP, gltf_mat->mEmissiveTexture); + facep->setTexture(LLRender::BASECOLOR_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR)); + facep->setTexture(LLRender::GLTF_NORMAL_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL)); + facep->setTexture(LLRender::METALLIC_ROUGHNESS_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS)); + facep->setTexture(LLRender::EMISSIVE_MAP, gltf_mat->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE)); } //ALWAYS null out vertex buffer on rebuild -- if the face lands in a render diff --git a/indra/newview/llvovolume.h b/indra/newview/llvovolume.h index da1bbd6e21..d1da564e41 100644 --- a/indra/newview/llvovolume.h +++ b/indra/newview/llvovolume.h @@ -214,6 +214,7 @@ class LLVOVolume : public LLViewerObject /*virtual*/ void setScale(const LLVector3 &scale, bool damped) override; /*virtual*/ void changeTEImage(S32 index, LLViewerTexture* new_image) override; + /*virtual*/ void changeTEMaterialTextures(S32 index) override; /*virtual*/ void setNumTEs(const U8 num_tes) override; /*virtual*/ void setTEImage(const U8 te, LLViewerTexture *imagep) override; /*virtual*/ S32 setTETexture(const U8 te, const LLUUID &uuid) override; diff --git a/indra/newview/pipeline.cpp b/indra/newview/pipeline.cpp index 29471d814b..9099505c6c 100644 --- a/indra/newview/pipeline.cpp +++ b/indra/newview/pipeline.cpp @@ -13190,7 +13190,7 @@ namespace draw.add((U64)material->mDoubleSided); if (textured) { - add_shadow_texture(casters, draw, material->mBaseColorTexture.get()); + add_shadow_texture(casters, draw, material->getRenderTexture(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR)); draw.add((U64)material->mAlphaMode).add(material->mAlphaCutoff).add(material->mBaseColor.mV[VALPHA]); LLGLTFMaterial::TextureTransform::Pack packed; material->mTextureTransform[LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR].getPacked(packed); diff --git a/indra/newview/tests/albakeproxies_test.cpp b/indra/newview/tests/albakeproxies_test.cpp new file mode 100644 index 0000000000..b76edad4db --- /dev/null +++ b/indra/newview/tests/albakeproxies_test.cpp @@ -0,0 +1,113 @@ +/** + * @file albakeproxies_test.cpp + * @brief Unit tests for the bakes a face names through bake proxies + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "linden_common.h" + +#include + +#include "../albakeproxies.h" + +#include "indra_constants.h" +#include "llgltfmaterial.h" +#include "lltextureentry.h" + +#include +#include + +namespace +{ + using namespace ALBakeProxies; + using namespace LLAvatarAppearanceDefines; + + // The set of the given bakes, as a number Catch prints + unsigned long bakes(std::initializer_list indices) + { + bake_set_t set; + for (EBakedTextureIndex index : indices) + { + set.set(index); + } + return set.to_ulong(); + } + + // A face and the PBR material it is drawn with + struct face_data + { + LLPointer mMaterial = new LLGLTFMaterial(); + LLTextureEntry mTE; + + face_data() : mTE(IMG_DEFAULT) { mTE.setGLTFMaterial(mMaterial); } + + LLUUID& slot(LLGLTFMaterial::TextureInfo info) { return mMaterial->mTextureId[info]; } + }; + + TEST_CASE("ALBakeProxies: every bake proxy names its own bake", "[ALBakeProxies]") + { + const std::pair proxies[] = { + { &IMG_USE_BAKED_HEAD, BAKED_HEAD }, { &IMG_USE_BAKED_UPPER, BAKED_UPPER }, + { &IMG_USE_BAKED_LOWER, BAKED_LOWER }, { &IMG_USE_BAKED_EYES, BAKED_EYES }, + { &IMG_USE_BAKED_SKIRT, BAKED_SKIRT }, { &IMG_USE_BAKED_HAIR, BAKED_HAIR }, + { &IMG_USE_BAKED_LEFTARM, BAKED_LEFT_ARM }, { &IMG_USE_BAKED_LEFTLEG, BAKED_LEFT_LEG }, + { &IMG_USE_BAKED_AUX1, BAKED_AUX1 }, { &IMG_USE_BAKED_AUX2, BAKED_AUX2 }, + { &IMG_USE_BAKED_AUX3, BAKED_AUX3 }, + }; + for (const auto& [id, index] : proxies) + { + LLTextureEntry te(*id); + CHECK(drawnBy(te, false).to_ulong() == bakes({ index })); + } + } + + TEST_CASE("ALBakeProxies: a face with no proxy draws no bake", "[ALBakeProxies]") + { + LLTextureEntry te(IMG_DEFAULT); + CHECK(drawnBy(te, false).none()); + CHECK(drawnBy(nullptr).none()); + + LLPointer material = new LLGLTFMaterial(); + CHECK_FALSE(anyNamedBy(*material)); + } + + TEST_CASE_METHOD(face_data, "ALBakeProxies: a PBR material draws the bake its base colour names", "[ALBakeProxies]") + { + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR) = IMG_USE_BAKED_UPPER; + CHECK(drawnBy(mMaterial).to_ulong() == bakes({ BAKED_UPPER })); + CHECK(drawnBy(mTE, false).to_ulong() == bakes({ BAKED_UPPER })); + + // the image's bake and the material's together + mTE.setID(IMG_USE_BAKED_HEAD); + CHECK(drawnBy(mTE, false).to_ulong() == bakes({ BAKED_HEAD, BAKED_UPPER })); + } + + TEST_CASE_METHOD(face_data, "ALBakeProxies: a bake in a material's other slots draws no body", "[ALBakeProxies]") + { + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_NORMAL) = IMG_USE_BAKED_UPPER; + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_METALLIC_ROUGHNESS) = IMG_USE_BAKED_HEAD; + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_EMISSIVE) = IMG_USE_BAKED_HAIR; + + CHECK(drawnBy(mMaterial).none()); + CHECK(drawnBy(mTE, false).none()); + // but each is resolved for the face's own avatar + CHECK(anyNamedBy(*mMaterial)); + } + + TEST_CASE_METHOD(face_data, "ALBakeProxies: a HUD's face counts its image and not its material", "[ALBakeProxies]") + { + mTE.setID(IMG_USE_BAKED_HEAD); + slot(LLGLTFMaterial::GLTF_TEXTURE_INFO_BASE_COLOR) = IMG_USE_BAKED_UPPER; + + CHECK(drawnBy(mTE, true).to_ulong() == bakes({ BAKED_HEAD })); + CHECK(drawnBy(mTE, false).to_ulong() == bakes({ BAKED_HEAD, BAKED_UPPER })); + } +}