yeahhowaboutnooo Posted December 9, 2020 Posted December 9, 2020 Hi @dagobaking, (hope it's OK to make a bug report here?) the copyMorphs function has the following bug: if multiple mods change a morphvalue, then the last mod in the keywords[] array will overwrite the value of all other mods (including the values set in looksmenu). Obviously there are multiple solutions to this issue, but (because i don't know how bodygen blends multiple morph values: could be a simple addition, could be more complex) here's a solution which just copies every single morph+keyword from one actor to another (usually player to doppelganger): 1049 Function copyMorphs(Actor a, Actor b) 1 2 | If looksMenu_installed == 1 && disable_morph_copying == false 3 4 | | Bool isFemale = a.GetLeveledActorBase().GetSex() == 1 5 6 | | String[] morphs = BodyGen.GetMorphs(a, isFemale) 7 8 | | BodyGen.RemoveAllMorphs(b, isFemale) 9 10 | | int i = 0 11 | | While i < morphs.Length 12 | | | Keyword[] keywords = BodyGen.GetKeywords(a, isFemale, morphs[i]) 13 | | | int t = 0 14 | | | While t < keywords.Length 15 | | | | float morphValue = BodyGen.GetMorph(a, isFemale, morphs[i], keywords[t]) 16 | | | | BodyGen.SetMorph(b, isFemale, morphs[i], keywords[t], morphValue) 17 | | | | t = t + 1 18 | | | EndWhile 19 | | i = i + 1 20 | | EndWhile 21 22 | | BodyGen.UpdateMorphs(b) 23 | EndIf 24 EndFunction This obviously has the drawback that the preexisting morphs on actor b are being deleted. Let me know if this is an issue and i'll change the code s.t. every morph from every mod is being added together if you prefer that.
dagobaking Posted December 10, 2020 Posted December 10, 2020 16 hours ago, yeahhowaboutnooo said: This obviously has the drawback that the preexisting morphs on actor b are being deleted. Let me know if this is an issue and i'll change the code s.t. every morph from every mod is being added together if you prefer that. Thank you for pointing this out. Deleting morphs on actor b shouldn't be an issue because this is only used to re-create the morphs on the PC onto a temporary body-double that gets created on-the-fly as needed anyway. This might resolve something related to keywords. But, in terms of which morph gets applied, I think it will still result in only the last one referenced applying? Have you tried compiling and testing in game to see what happens?
yeahhowaboutnooo Posted December 10, 2020 Author Posted December 10, 2020 41 minutes ago, dagobaking said: Have you tried compiling and testing in game to see what happens? I've been using the code i posted for the past 2 days or so and all morphs (Looksmenu + BDH + FPE) have been applied to the doppelganger. There was one AAF scene once, where no morphs were applied at all to the doppelganger, not sure if this had to do with when the freecam gets activated. 45 minutes ago, dagobaking said: But, in terms of which morph gets applied, I think it will still result in only the last one referenced applying? No, because afaik morphs in bodygen are always a morph-keyword pair: the original code always sets all morphs to the "AAF_MorphKeyword", which obviously gets overwritten if there are multiple keywords on actor a setting the same morph. the code i posted circumvents this by copying the keyword as well -> it's now up to bodygen to blend these morphs (currently it seems to be a simple addition, but iirc in skyrim's version it's configurable. maybe that'll come later as well for FO4?) -> in any case: this way the blending of morphs is the same for the actual player character and the doppelganger FYI: the updateMorphs function is the only other usage of the "AAF_MorphKeyword", but that function seems to be never used: Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False) AAF Beta 154-31304-154b-1603335756/Scripts/Source$ grep -rn . -ie updateMorphs ./User/AAF/AAF_MainQuestScript.psc:1071: BodyGen.UpdateMorphs(b) ./User/AAF/AAF_MainQuestScript.psc:2522:Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False) ./User/AAF/AAF_MainQuestScript.psc:2535: BodyGen.UpdateMorphs(targetActor[i])
requiredname65 Posted December 10, 2020 Posted December 10, 2020 3 hours ago, yeahhowaboutnooo said: it's now up to bodygen to blend these morphs (currently it seems to be a simple addition Doesn't seem to be true based on F4EE sources from expired6978's github [ https://github.com/expired6978/F4SEPlugins ]. To be honest, making it sum over all values seems trivial, if there's no need for configuration. There might be more to this under the surface as Expired hasn't done that.. ... MorphApplicator morpher(geometry, newBlock, newBlock, [&](std::vector<Morpher::Vector3> & verts) { SimpleLocker locker(&m_morphLock); for(auto & actorMorph : *actorMorphs) { float effectiveValue = actorMorph.second->GetEffectiveValue(); if(effectiveValue == 0.0f) continue; auto morph = morphMap->GetVertexData(*actorMorph.first); if(!morph) continue; bool outOfBounds = morph->ApplyMorph(geometry->numVertices, (NiPoint3*)&verts.at(0), effectiveValue); if(outOfBounds) { _WARNING("%s - Shape: %s Morph: %s contained out of bounds vertices\t[%s]", __FUNCTION__, morphableShape->shapeName.c_str(), actorMorph.first->c_str(), morphableShape->morphPath.c_str()); } } }); ... float UserValues::GetEffectiveValue() { SimpleLocker locker(&m_morphLock); auto maxIt = std::max_element(begin(), end()); if(maxIt != end()) { return maxIt->second; } return 0.0f; }
dagobaking Posted December 11, 2020 Posted December 11, 2020 19 hours ago, yeahhowaboutnooo said: I've been using the code i posted for the past 2 days or so and all morphs (Looksmenu + BDH + FPE) have been applied to the doppelganger. There was one AAF scene once, where no morphs were applied at all to the doppelganger, not sure if this had to do with when the freecam gets activated. No, because afaik morphs in bodygen are always a morph-keyword pair: the original code always sets all morphs to the "AAF_MorphKeyword", which obviously gets overwritten if there are multiple keywords on actor a setting the same morph. the code i posted circumvents this by copying the keyword as well -> it's now up to bodygen to blend these morphs (currently it seems to be a simple addition, but iirc in skyrim's version it's configurable. maybe that'll come later as well for FO4?) -> in any case: this way the blending of morphs is the same for the actual player character and the doppelganger FYI: the updateMorphs function is the only other usage of the "AAF_MorphKeyword", but that function seems to be never used: Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False) AAF Beta 154-31304-154b-1603335756/Scripts/Source$ grep -rn . -ie updateMorphs ./User/AAF/AAF_MainQuestScript.psc:1071: BodyGen.UpdateMorphs(b) ./User/AAF/AAF_MainQuestScript.psc:2522:Function updateMorphs(Var[] actorFormID, Var[] isFemale, Var[] morph, Var[] value, Bool updateArmor = False) ./User/AAF/AAF_MainQuestScript.psc:2535: BodyGen.UpdateMorphs(targetActor[i]) I would be interested in seeing a series of tests with screenshots verifying all of this. My understanding of the reason for these keywords is to disable the morphs if/when the mod that created them gets uninstalled. Also, I could be wrong. But, I don't think that morph commands on the same morph "blend". They just over-write the last one anyway. Otherwise, the schlong angle morphs that were used for a long time never would have worked as implemented.
dagobaking Posted December 11, 2020 Posted December 11, 2020 From the SetMorph source: ; Sets the specified actors morph and key to value (LooksMenu internally uses None for the keyword) ; This keyword should be a Keyword in a plugin so that when the plugin is uninstalled the morph is removed automatically Function SetMorph(Actor akActor, bool isFemale, string morph, keyword akKeyword, float value) global native This seems to suggest that the keyword is for morph removal after the mod is uninstalled. "Setting to a value", to me, suggests no blending. Though it isn't 100% clear.
yeahhowaboutnooo Posted December 11, 2020 Author Posted December 11, 2020 Spoiler ingame PC: aaf vanilla-doppelganger: aaf patched-doppelganger: I wouldn't be surprised if the behavior of F4EE has changed in the past 3 years since the last github-commit. In any case: The code i posted leaves it up to F4EE how to display different values for the same morph from different keywords. Let me know if you have more questions and/or want more tests One last thing (which i'm not sure is a problem but wanted to mention nevertheless? ; Removes all morphs on the specified actor (Actor becomes applicable for BodyGen when they are no active morphs, use a dummy morph to prevent) Function RemoveAllMorphs(Actor akActor, bool isFemale) global native i interpret this as: if you call removeallmorphs, immediately follow it with a setmorph call s.t. bodygen does not run ^ i don't think the above is an issue because: a) setmorph is being called (almost) immediately (for copying the morphs from actor a) b) in case actor a does not have any morphs applied: it's safe to assume the user doesn't use the bodygen feature (because if the user were using bodygen, there would be morphs applied to the PC) i use bodygen myself and haven't seen any random morphs being applied (yet)
requiredname65 Posted December 12, 2020 Posted December 12, 2020 Quote I wouldn't be surprised if the behavior of F4EE has changed in the past 3 years since the last github-commit. Sure but I don't see any mention of such in nexus changelog, a setting determing used reduce function in f4ee.ini or strings exported from f4ee.dll. If there's something to control this, I'd be very intrested to know. For reference this is from nioverride.ini for Skyrim LE ; Determines scaling mode ; 0 - Multiplicative ; 1 - Averaged ; 2 - Additive ; 3 - Largest iScaleMode=3 ; Determines combination mode for BodyMorph ; 0 - Additive ; 1 - Averaged ; 2 - Largest iBodyMorphMode=0 and this is from f4ee.ini 1.6.20 [BodyMorph] ; Enables loading of tri files and morphing meshes bEnable=1 ; Enables loading bodygen files to randomize actor sliders ; Disabled by default due to bugs with invis bodies on actors starting dead bEnableBodyGen=0 uMaxCache=2147483648 bParallelShapes=1 I'll do a quick test to see how it behaves when multiple mos set same morhp under different keywords.
requiredname65 Posted December 12, 2020 Posted December 12, 2020 Test setup Function Preg1() Global Actor a = Game.GetPlayer() ; AAF_MorphKeyword [KYWD:09000F9E] AAF.esm Keyword m = Game.GetFormFromFile(0x09000F9E, "AAF.esm") as Keyword BodyGen.SetMorph(a, true, "Belly Pregnant", m, 0.5) BodyGen.UpdateMorphs(a) EndFunction Function Preg2() Global Actor a = Game.GetPlayer() ; FPFP_Keyword [KYWD:36001743] FP_FamilyPlanningEnhanced.esp Keyword m = Game.GetFormFromFile(0x36001743, "FP_FamilyPlanningEnhanced.esp") as Keyword BodyGen.SetMorph(a, true, "Belly Pregnant", m, 0.5) BodyGen.UpdateMorphs(a) EndFunction They are called from console in order > Preg1 > Preg2 with CallGlobalFunction "QA.PregN". Keyword hosting mods are loaded during these tests. Test 1: After calling both functions from console, behavior matches either taking max from morph values or latest value. There's no addition or multiplication or averaging. Test 2: Change Preg2 to apply morph at strength 0.25. Hypothesis: After calling Preg1 then Preg2, morph matches that of Preg1. Results: hypothesis holds, morphed at values equal to 0.5. Test 3: Same setup as in test 2 but swap function call order. Result: morphs on actor are changed if new morph has larger value then current max. Adding morphs in order 0.25 -> 0.5 results in morphs applied that matches visually value of max([0.25, 0.5]). So it looks like looksmenu doesn't blend but applies morph with largest value found for each morph key. Therefore, it should be fine to let looksmenu handle this. However this might be a slight problem if user wants to apply morphs with negative values.
dagobaking Posted December 13, 2020 Posted December 13, 2020 Thank you both. I asked the author directly. He said that the purpose of the keywords is to automatically disable the morphs when the mod is uninstalled. But, he did also say that there is an averaging mechanism that can be changed in an ini (and he didn't remember what the default was). I've asked some follow up questions because I'm still not sure how it gets applied. For example, if you use the same keyword on the same morph does it replace? Or, is every single call averaged regardless of keyword? I don't see how it could be the latter given that we've had morphSets that go up and down already that worked like a replacement. requiredname65's test appears to show that the default "averaging" method is to use the largest value. If that is the case, the only explanations for yeahhowaboutnoo's visual test is that: A) Some other plugin we don't know about in that build impacts the PC. There are many unique player type mods that can cause issues. B) Some smaller value (from where though?) replaces a larger value because it uses the same keyword and is the last setmorph applied. Ultimately, I think I will make this adjustment even though I'm not 100% sure about it. Mainly because I can't see any harm. If a morph change originated in some other mod, AAF doesn't really need the morph to be disabled if someone strangely happens to uninstall AAF on a save that occurred during a PC animation (which I believe is blocked anyway).
requiredname65 Posted December 13, 2020 Posted December 13, 2020 3 hours ago, dagobaking said: But, he did also say that there is an averaging mechanism that can be changed in an ini (and he didn't remember what the default was). Any chance he was talking about Skyrim's Racemenu instead? 3 hours ago, dagobaking said: .. the only explanations for yeahhowaboutnoo's .. Some smaller value (from where though?) replaces a larger value because it uses the same keyword and is the last setmorph applied. Based on old sources, for every actor there is a map of "morphnames" to UserValues, where UserValues is map of UInt32 to floats. UInt32 is keyword's formId. When setMorph is called and passed in keyword is in UserValues map, it's assoc float is replaced. Otherwise a new one is added. In reverse when keyword is missing, this entry is removed. When UpdateMorphs is called, at one point for each morphs, UserValues is reduced to single value and then applied on mesh. What seems to happen here is that UserValues map contains two or more elements in such order (native implementation seems to use unordered_map, 'ordering' is based on hash value of key) where larger value is retrieved first then smaller etc when map is iterated over. BodyGen.GetKeywords returns those UserValues keys in that order. So when you do this While i < morphs.Length Keyword[] keywords = BodyGen.GetKeywords(a, isFemale, morphs[i]) int t = 0 While t < keywords.Length float morphValue = BodyGen.GetMorph(a, isFemale, morphs[i], keywords[t]) BodyGen.SetMorph(b, isFemale, morphs[i], AAF_MorphKeyword, morphValue) t = t + 1 EndWhile i = i + 1 EndWhile it' looks to be same as BodyGen.SetMorph(b, isFemale, morphs, AAF_MorphKeyword, BodyGen.GetMorph(..., keywords[keywords.length - 1])). So +1 for the change. But anything having to do with native implementation relies on older sources staying accurate, so let reader beware and do their own tests. But so far ingame behavior is aligned with assumed implementation. Such a bummer there is no papyrus vm binding for this void BodyMorphInterface::CloneMorphs(Actor * source, Actor * target) { if(!source || !target) return; SimpleLocker locker(&m_morphLock); bool isFemale = false; TESNPC * npc = DYNAMIC_CAST(source->baseForm, TESForm, TESNPC); if(npc) isFemale = CALL_MEMBER_FN(npc, GetSex)() == 1 ? true : false; auto it = m_morphMap[isFemale ? 1 : 0].find(source->formID); if(it != m_morphMap[isFemale ? 1 : 0].end()) { m_morphMap[isFemale ? 1 : 0][target->formID] = it->second; } }
dagobaking Posted December 16, 2020 Posted December 16, 2020 On 12/13/2020 at 3:20 AM, requiredname65 said: Any chance he was talking about Skyrim's Racemenu instead? It's possible. Not sure. On 12/13/2020 at 3:20 AM, requiredname65 said: Such a bummer there is no papyrus vm binding for this void BodyMorphInterface::CloneMorphs(Actor * source, Actor * target) { if(!source || !target) return; SimpleLocker locker(&m_morphLock); bool isFemale = false; TESNPC * npc = DYNAMIC_CAST(source->baseForm, TESForm, TESNPC); if(npc) isFemale = CALL_MEMBER_FN(npc, GetSex)() == 1 ? true : false; auto it = m_morphMap[isFemale ? 1 : 0].find(source->formID); if(it != m_morphMap[isFemale ? 1 : 0].end()) { m_morphMap[isFemale ? 1 : 0][target->formID] = it->second; } } I think he mentioned that he would work on a function like this a very long time ago. I guess never completed?
Recommended Posts
Archived
This topic is now archived and is closed to further replies.