diff --git a/src/TruePBR.cpp b/src/TruePBR.cpp index 60a6dc038c..9ac42daae1 100644 --- a/src/TruePBR.cpp +++ b/src/TruePBR.cpp @@ -1340,22 +1340,79 @@ struct TESBoundObject_Clone3D auto* result = func(object, ref, arg3); if (result != nullptr && ref != nullptr && ref->data.objectReference != nullptr && ref->data.objectReference->formType == RE::FormType::Static) { auto* stat = static_cast(ref->data.objectReference); - if (stat->data.materialObj != nullptr && stat->data.materialObj->directionalData.singlePass) { - if (auto* pbrData = truePBR->GetPBRMaterialObjectData(stat->data.materialObj)) { - RE::BSVisit::TraverseScenegraphGeometries(result, [pbrData](RE::BSGeometry* geometry) { - if (auto* shaderProperty = static_cast(geometry->GetGeometryRuntimeData().shaderProperty.get())) { - if (shaderProperty->GetMaterialType() == RE::BSShaderMaterial::Type::kLighting && - shaderProperty->flags.any(RE::BSShaderProperty::EShaderPropertyFlag::kVertexLighting)) { - if (auto* material = static_cast(shaderProperty->material)) { - material->ApplyMaterialObjectData(*pbrData); - BSLightingShaderMaterialPBR::All[material].materialObjectData = pbrData; + RE::BGSMaterialObject* currentMato = stat->data.materialObj; + + // Resolve PBR MATO data for the three-way condition as a single pointer: + // non-null -> apply MATO to geometries + // null -> clear any previously applied MATO from geometries + // This covers all negative cases (currentMato == nullptr, singlePass == + // false, or no matching PBR entry) with one branch so the clear path is + // never silently skipped. + auto* pbrData = (currentMato != nullptr && currentMato->directionalData.singlePass) ? truePBR->GetPBRMaterialObjectData(currentMato) : nullptr; + + if (pbrData != nullptr) { + RE::BSVisit::TraverseScenegraphGeometries(result, [pbrData, ref](RE::BSGeometry* geometry) { + if (auto* shaderProperty = static_cast(geometry->GetGeometryRuntimeData().shaderProperty.get())) { + if (shaderProperty->GetMaterialType() == RE::BSShaderMaterial::Type::kLighting && + shaderProperty->flags.any(RE::BSShaderProperty::EShaderPropertyFlag::kVertexLighting)) { + if (auto* material = static_cast(shaderProperty->material)) { + auto& ext = BSLightingShaderMaterialPBR::All[material]; + const auto prevOwnerRefID = ext.lastOwnerRefFormID; + + // Fork-before-write: if this material instance is already owned + // by a different ref whose MATO payload differs from the incoming + // one, clone it so we don't contaminate the previous owner's + // geometry. Use pointer identity: GetPBRMaterialObjectData + // returns stable addresses into pbrMaterialObjects, so two + // different MATOs always produce different pointers regardless of + // whether their individual fields (baseColorScale, roughness, + // specularLevel, glint) happen to match. + const bool wouldContaminate = + (prevOwnerRefID != 0) && + (prevOwnerRefID != ref->GetFormID()) && + (ext.materialObjectData != pbrData); + + BSLightingShaderMaterialPBR* targetMat = material; + + if (wouldContaminate) { + auto* freshMat = BSLightingShaderMaterialPBR::Make(); + if (freshMat) { + freshMat->CopyMembers(material); + shaderProperty->material = freshMat; + targetMat = freshMat; + } else { + logger::warn("[TruePBR] failed to clone PBR material for ref {:08X}; skipping to avoid contamination", ref->GetFormID()); + return RE::BSVisit::BSVisitControl::kContinue; + } } + + targetMat->ApplyMaterialObjectData(*pbrData); + auto& targetExt = BSLightingShaderMaterialPBR::All[targetMat]; + targetExt.materialObjectData = pbrData; + targetExt.lastOwnerRefFormID = ref->GetFormID(); } } + } - return RE::BSVisit::BSVisitControl::kContinue; - }); - } + return RE::BSVisit::BSVisitControl::kContinue; + }); + } else { + RE::BSVisit::TraverseScenegraphGeometries(result, [](RE::BSGeometry* geometry) { + if (auto* shaderProperty = static_cast(geometry->GetGeometryRuntimeData().shaderProperty.get())) { + if (shaderProperty->GetMaterialType() == RE::BSShaderMaterial::Type::kLighting && + shaderProperty->flags.any(RE::BSShaderProperty::EShaderPropertyFlag::kVertexLighting)) { + if (auto* material = static_cast(shaderProperty->material)) { + auto& ext = BSLightingShaderMaterialPBR::All[material]; + if (ext.materialObjectData != nullptr) { + material->ClearMaterialObjectData(); + ext.materialObjectData = nullptr; + ext.lastOwnerRefFormID = 0; + } + } + } + } + return RE::BSVisit::BSVisitControl::kContinue; + }); } } return result; diff --git a/src/TruePBR/BSLightingShaderMaterialPBR.cpp b/src/TruePBR/BSLightingShaderMaterialPBR.cpp index 897bdc27ad..6ef70c0c8c 100644 --- a/src/TruePBR/BSLightingShaderMaterialPBR.cpp +++ b/src/TruePBR/BSLightingShaderMaterialPBR.cpp @@ -151,6 +151,14 @@ void BSLightingShaderMaterialPBR::ApplyMaterialObjectData(const TruePBR::PBRMate projectedMaterialGlintParameters = materialObjectData.glintParameters; } +void BSLightingShaderMaterialPBR::ClearMaterialObjectData() +{ + projectedMaterialBaseColorScale = { 1.f, 1.f, 1.f }; + projectedMaterialRoughness = 1.f; + projectedMaterialSpecularLevel = 0.04f; + projectedMaterialGlintParameters = GlintParameters{}; +} + void BSLightingShaderMaterialPBR::OnLoadTextureSet(std::uint64_t arg1, RE::BSTextureSet* inTextureSet) { const auto& stateData = globals::game::graphicsState->GetRuntimeData(); diff --git a/src/TruePBR/BSLightingShaderMaterialPBR.h b/src/TruePBR/BSLightingShaderMaterialPBR.h index 597d258034..c98e338a16 100644 --- a/src/TruePBR/BSLightingShaderMaterialPBR.h +++ b/src/TruePBR/BSLightingShaderMaterialPBR.h @@ -37,6 +37,10 @@ class BSLightingShaderMaterialPBR : public RE::BSLightingShaderMaterialBase { TruePBR::PBRTextureSetData* textureSetData = nullptr; TruePBR::PBRMaterialObjectData* materialObjectData = nullptr; + /// FormID of the TESObjectREFR whose Clone3D call last wrote MATO data to this + /// material. Used by the fork-before-write check to detect when a pooled material + /// instance would be overwritten by a different ref, triggering a clone instead. + RE::FormID lastOwnerRefFormID = 0; }; inline static constexpr auto FEATURE = static_cast(32); @@ -64,6 +68,10 @@ class BSLightingShaderMaterialPBR : public RE::BSLightingShaderMaterialBase void ApplyTextureSetData(const TruePBR::PBRTextureSetData& textureSetData); void ApplyMaterialObjectData(const TruePBR::PBRMaterialObjectData& materialObjectData); + /// Resets all projected-material fields to their default values. + /// Called on references that carry no MATO (or no PBR config for their MATO) to + /// prevent stale data copied in by CopyMembers from persisting on the material. + void ClearMaterialObjectData(); float GetRoughnessScale() const; float GetSpecularLevel() const;