diff --git a/res/xrc/OutfitStudio.xrc b/res/xrc/OutfitStudio.xrc index e9843de6..67352575 100644 --- a/res/xrc/OutfitStudio.xrc +++ b/res/xrc/OutfitStudio.xrc @@ -2182,6 +2182,10 @@ Delete bone(s) from only the selected shapes. + + + Edit a custom bone or view a standard bone. + @@ -2196,6 +2200,10 @@ Add a custom bone to the project. + + + Edit a custom bone or view a standard bone. + diff --git a/res/xrc/Skeleton.xrc b/res/xrc/Skeleton.xrc index 0e978c70..a5af4e08 100644 --- a/res/xrc/Skeleton.xrc +++ b/res/xrc/Skeleton.xrc @@ -87,19 +87,20 @@ wxHORIZONTAL - wxLEFT|wxRIGHT|wxEXPAND + wxALL|wxEXPAND 5 - - + + -1 - wxLEFT|wxRIGHT|wxEXPAND + wxALL|wxEXPAND 5 - - 0.00000 + + 0 + @@ -108,8 +109,65 @@ wxALL|wxEXPAND 5 - - wxHORIZONTAL + + 0 + 3 + 0 + 0 + 1 + + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + + -1 + + + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + + -1 + + + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + + -1 + + + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + + -1 + + + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + 0.00000 + + + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + 0.00000 + + wxLEFT|wxRIGHT|wxEXPAND @@ -127,14 +185,14 @@ 0.00000 - - - - - wxALL|wxEXPAND - 5 - - wxHORIZONTAL + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + 0.00000 + + wxLEFT|wxRIGHT|wxEXPAND @@ -152,6 +210,14 @@ 0.00000 + + + wxLEFT|wxRIGHT|wxEXPAND + 5 + + 0.00000 + + diff --git a/src/components/Anim.cpp b/src/components/Anim.cpp index 41bd3715..607e8d3a 100644 --- a/src/components/Anim.cpp +++ b/src/components/Anim.cpp @@ -17,14 +17,7 @@ bool AnimInfo::AddShapeBone(const std::string& shape, const std::string& boneNam shapeSkinning[shape].boneNames[boneName] = shapeBones[shape].size(); shapeBones[shape].push_back(boneName); AnimSkeleton::getInstance().RefBone(boneName); - // Calculate a good default value for xformSkinToBone by: - // Composing: bone -> global -> skin - // then inverting - MatTransform xformGlobalToSkin = shapeSkinning[shape].xformGlobalToSkin; - MatTransform xformBoneToGlobal; - AnimSkeleton::getInstance().GetBoneTransformToGlobal(boneName, xformBoneToGlobal); - MatTransform xformBoneToSkin = xformGlobalToSkin.ComposeTransforms(xformBoneToGlobal); - SetXFormSkinToBone(shape, boneName, xformBoneToSkin.InverseTransform()); + RecalcXFormSkinToBone(shape, boneName); return true; } @@ -242,6 +235,23 @@ void AnimInfo::SetXFormSkinToBone(const std::string& shape, const std::string& b shapeSkinning[shape].boneWeights[b].xformSkinToBone = stransform; } +void AnimInfo::RecalcXFormSkinToBone(const std::string& shape, const std::string& boneName) { + // Calculate a good default value for xformSkinToBone by: + // Composing: bone -> global -> skin + // then inverting + MatTransform xformGlobalToSkin = shapeSkinning[shape].xformGlobalToSkin; + MatTransform xformBoneToGlobal; + AnimSkeleton::getInstance().GetBoneTransformToGlobal(boneName, xformBoneToGlobal); + MatTransform xformBoneToSkin = xformGlobalToSkin.ComposeTransforms(xformBoneToGlobal); + SetXFormSkinToBone(shape, boneName, xformBoneToSkin.InverseTransform()); +} + +void AnimInfo::RecursiveRecalcXFormSkinToBone(const std::string& shape, AnimBone *bPtr) { + RecalcXFormSkinToBone(shape, bPtr->boneName); + for (AnimBone *cptr : bPtr->children) + RecursiveRecalcXFormSkinToBone(shape, cptr); +} + bool AnimInfo::CalcShapeSkinBounds(const std::string& shapeName, const int& boneIndex) { if (!refNif || !refNif->IsValid()) // Check for existence of reference nif return false; @@ -480,9 +490,8 @@ AnimBone *AnimSkeleton::LoadCustomBoneFromNif(NifFile *nif, const std::string &b parentBone = LoadCustomBoneFromNif(nif, parentNode->GetName()); } AnimBone& cstm = AnimSkeleton::getInstance().AddCustomBone(boneName); - cstm.parent = parentBone; - parentBone->children.push_back(&cstm); cstm.SetTransformBoneToParent(node->GetTransformToParent()); + cstm.SetParentBone(parentBone); return &cstm; } @@ -594,3 +603,18 @@ void AnimBone::SetTransformBoneToParent(const MatTransform &ttp) { UpdateTransformToGlobal(); UpdatePoseTransform(); } + +void AnimBone::SetParentBone(AnimBone* newParent) { + if (parent == newParent) + return; + if (parent) { + //std::erase(parent->children, this); + auto it = std::remove(parent->children.begin(), parent->children.end(), this); + parent->children.erase(it, parent->children.end()); + } + parent = newParent; + if (parent) + parent->children.push_back(this); + UpdateTransformToGlobal(); + UpdatePoseTransform(); +} diff --git a/src/components/Anim.h b/src/components/Anim.h index d5d7cfc5..c602bee8 100644 --- a/src/components/Anim.h +++ b/src/components/Anim.h @@ -56,13 +56,17 @@ class AnimBone { // and xformPoseToGlobal, for this and for descendants. void SetTransformBoneToParent(const MatTransform &ttp); // UpdateTransformToGlobal updates xformToGlobal for this and for - // descendants. This should only be called from itself and - // SetTransformBoneToParent. + // descendants. This should only be called from itself, + // SetTransformBoneToParent, and SetParentBone. void UpdateTransformToGlobal(); // UpdatePoseTransform updates xformPoseToGlobal for this and all // descendants. Call it after poseRotVec, poseTranVec, or // xformToGlobal is changed. void UpdatePoseTransform(); + // SetParentBone updates "parent" of this and "children" of the old + // and new parents. It also calls UpdateTransformToGlobal and + // UpdatePoseTranform. + void SetParentBone(AnimBone* newParent); }; // Vertex to weight value association. Also keeps track of skin-to-bone transform and bounding sphere. @@ -146,6 +150,12 @@ class AnimInfo { void SetWeights(const std::string& shape, const std::string& boneName, std::unordered_map& inVertWeights); bool GetXFormSkinToBone(const std::string& shape, const std::string& boneName, MatTransform& stransform); void SetXFormSkinToBone(const std::string& shape, const std::string& boneName, const MatTransform& stransform); + // RecalcXFormSkinToBone recalculates a shape bone's xformSkinToBone + // from other transforms. + void RecalcXFormSkinToBone(const std::string& shape, const std::string& boneName); + // RecursiveRecalcXFormSkinToBone calls RecalcXFormSkinToBone for the + // given bone and all its descendants. + void RecursiveRecalcXFormSkinToBone(const std::string& shape, AnimBone *bPtr); bool CalcShapeSkinBounds(const std::string& shapeName, const int& boneIndex); void CleanupBones(); void WriteToNif(NifFile* nif, const std::string& shapeException = ""); diff --git a/src/program/OutfitProject.cpp b/src/program/OutfitProject.cpp index b78d1bb9..8387a10a 100644 --- a/src/program/OutfitProject.cpp +++ b/src/program/OutfitProject.cpp @@ -1755,16 +1755,21 @@ void OutfitProject::AddBoneRef(const std::string& boneName) { workAnim.AddShapeBone(s, boneName); } -void OutfitProject::AddCustomBoneRef(const std::string& boneName, const Vector3& translation) { +void OutfitProject::AddCustomBoneRef(const std::string& boneName, const std::string& parentBone, const MatTransform &xformToParent) { AnimBone& customBone = AnimSkeleton::getInstance().AddCustomBone(boneName); + customBone.SetTransformBoneToParent(xformToParent); + customBone.SetParentBone(AnimSkeleton::getInstance().GetBonePtr(parentBone)); - MatTransform xformBoneToGlobal; - xformBoneToGlobal.translation = translation; + for (auto &s : workNif.GetShapeNames()) + workAnim.AddShapeBone(s, boneName); +} - customBone.SetTransformBoneToParent(xformBoneToGlobal); +void OutfitProject::ModifyCustomBone(AnimBone *bPtr, const std::string& parentBone, const MatTransform &xformToParent) { + bPtr->SetTransformBoneToParent(xformToParent); + bPtr->SetParentBone(AnimSkeleton::getInstance().GetBonePtr(parentBone)); for (auto &s : workNif.GetShapeNames()) - workAnim.AddShapeBone(s, boneName); + workAnim.RecursiveRecalcXFormSkinToBone(s, bPtr); } void OutfitProject::ClearWorkSliders() { diff --git a/src/program/OutfitProject.h b/src/program/OutfitProject.h index 65f6e22a..d40ebe54 100644 --- a/src/program/OutfitProject.h +++ b/src/program/OutfitProject.h @@ -174,7 +174,8 @@ class OutfitProject { void ClearBoneScale(bool clear = true); void AddBoneRef(const std::string& boneName); - void AddCustomBoneRef(const std::string& boneName, const Vector3& translation); + void AddCustomBoneRef(const std::string& boneName, const std::string& parentBone, const MatTransform &xformToParent); + void ModifyCustomBone(AnimBone *bPtr, const std::string& parentBone, const MatTransform &xformToParent); void ClearWorkSliders(); void ClearReference(); diff --git a/src/program/OutfitStudio.cpp b/src/program/OutfitStudio.cpp index 44241ce9..f1016da2 100644 --- a/src/program/OutfitStudio.cpp +++ b/src/program/OutfitStudio.cpp @@ -189,6 +189,7 @@ wxBEGIN_EVENT_TABLE(OutfitStudioFrame, wxFrame) EVT_MENU(XRCID("addCustomBone"), OutfitStudioFrame::OnAddCustomBone) EVT_MENU(XRCID("deleteBone"), OutfitStudioFrame::OnDeleteBone) EVT_MENU(XRCID("deleteBoneSelected"), OutfitStudioFrame::OnDeleteBoneFromSelected) + EVT_MENU(XRCID("editBone"), OutfitStudioFrame::OnEditBone) EVT_MENU(XRCID("copyBoneWeight"), OutfitStudioFrame::OnCopyBoneWeight) EVT_MENU(XRCID("copySelectedWeight"), OutfitStudioFrame::OnCopySelectedWeight) EVT_MENU(XRCID("transferSelectedWeight"), OutfitStudioFrame::OnTransferSelectedWeight) @@ -4215,7 +4216,11 @@ void OutfitStudioFrame::OnShapeDrop(wxTreeEvent& event) { outfitShapes->SelectItem(movedItem); } -void OutfitStudioFrame::OnBoneContext(wxTreeEvent& WXUNUSED(event)) { +void OutfitStudioFrame::OnBoneContext(wxTreeEvent& event) { + contextBone.clear(); + wxTreeItemId itemId = event.GetItem(); + if (itemId.IsOk()) + contextBone = outfitBones->GetItemText(itemId); wxMenu* menu = wxXmlResource::Get()->LoadMenu("menuBoneContext"); if (menu) { PopupMenu(menu); @@ -4224,6 +4229,7 @@ void OutfitStudioFrame::OnBoneContext(wxTreeEvent& WXUNUSED(event)) { } void OutfitStudioFrame::OnBoneTreeContext(wxCommandEvent& WXUNUSED(event)) { + contextBone.clear(); wxMenu* menu = wxXmlResource::Get()->LoadMenu("menuBoneTreeContext"); if (menu) { PopupMenu(menu); @@ -7839,6 +7845,8 @@ void OutfitStudioFrame::OnAddBone(wxCommandEvent& WXUNUSED(event)) { if (!cb->boneName.empty()) { auto newItem = boneTree->AppendItem(treeParent, cb->boneName); fAddBoneChildren(newItem, cb); + if (cb->boneName == contextBone) + boneTree->SelectItem(newItem); } else fAddBoneChildren(treeParent, cb); @@ -7865,41 +7873,145 @@ void OutfitStudioFrame::OnAddBone(wxCommandEvent& WXUNUSED(event)) { } } +void OutfitStudioFrame::FillParentBoneChoice(wxDialog &dlg, const std::string &selBone) { + wxChoice *cParentBone = XRCCTRL(dlg, "cParentBone", wxChoice); + cParentBone->AppendString("(none)"); + + std::set boneSet; + for (auto selItem : selectedItems) { + const std::vector &bones = project->GetWorkAnim()->shapeBones[selItem->GetShape()->GetName()]; + for (const std::string &b : bones) + boneSet.insert(b); + } + + for (auto &bone : boneSet) { + cParentBone->AppendString(bone); + if (bone == selBone) + cParentBone->SetSelection(cParentBone->GetCount() - 1); + } + + if (cParentBone->GetSelection() == wxNOT_FOUND) { + if (selBone.empty()) { + cParentBone->SetSelection(0); + } + else { + cParentBone->AppendString(selBone); + cParentBone->SetSelection(cParentBone->GetCount() - 1); + } + } +} + +void OutfitStudioFrame::GetBoneDlgData(wxDialog &dlg, MatTransform &xform, std::string &parentBone) { + xform.translation.x = atof(XRCCTRL(dlg, "textX", wxTextCtrl)->GetValue().c_str()); + xform.translation.y = atof(XRCCTRL(dlg, "textY", wxTextCtrl)->GetValue().c_str()); + xform.translation.z = atof(XRCCTRL(dlg, "textZ", wxTextCtrl)->GetValue().c_str()); + + Vector3 rotvec; + rotvec.x = atof(XRCCTRL(dlg, "textRX", wxTextCtrl)->GetValue().c_str()); + rotvec.y = atof(XRCCTRL(dlg, "textRY", wxTextCtrl)->GetValue().c_str()); + rotvec.z = atof(XRCCTRL(dlg, "textRZ", wxTextCtrl)->GetValue().c_str()); + xform.rotation = RotVecToMat(rotvec); + + wxChoice *cParentBone = XRCCTRL(dlg, "cParentBone", wxChoice); + int pBChoice = cParentBone->GetSelection(); + if (pBChoice != wxNOT_FOUND) + parentBone = cParentBone->GetString(pBChoice).ToStdString(); + + if (parentBone == "(none)") + parentBone = std::string(); +} + void OutfitStudioFrame::OnAddCustomBone(wxCommandEvent& WXUNUSED(event)) { wxDialog dlg; - if (wxXmlResource::Get()->LoadDialog(&dlg, this, "dlgCustomBone")) { - dlg.Bind(wxEVT_CHAR_HOOK, &OutfitStudioFrame::OnEnterClose, this); + if (!wxXmlResource::Get()->LoadDialog(&dlg, this, "dlgCustomBone")) + return; - if (dlg.ShowModal() == wxID_OK) { - wxString bone = XRCCTRL(dlg, "boneName", wxTextCtrl)->GetValue(); - if (bone.empty()) { - wxMessageBox(_("No bone name was entered!"), _("Error"), wxICON_INFORMATION, this); - return; - } + dlg.Bind(wxEVT_CHAR_HOOK, &OutfitStudioFrame::OnEnterClose, this); + FillParentBoneChoice(dlg, contextBone); - wxTreeItemIdValue cookie; - wxTreeItemId item = outfitBones->GetFirstChild(bonesRoot, cookie); - while (item.IsOk()) { - if (outfitBones->GetItemText(item) == bone) { - wxMessageBox(wxString::Format(_("Bone '%s' already exists in the project!"), bone), _("Error"), wxICON_INFORMATION, this); - return; - } - item = outfitBones->GetNextChild(bonesRoot, cookie); - } + if (dlg.ShowModal() != wxID_OK) + return; - Vector3 translation; - translation.x = atof(XRCCTRL(dlg, "textX", wxTextCtrl)->GetValue().c_str()); - translation.y = atof(XRCCTRL(dlg, "textY", wxTextCtrl)->GetValue().c_str()); - translation.z = atof(XRCCTRL(dlg, "textZ", wxTextCtrl)->GetValue().c_str()); + wxString bone = XRCCTRL(dlg, "boneName", wxTextCtrl)->GetValue(); + if (bone.empty()) { + wxMessageBox(_("No bone name was entered!"), _("Error"), wxICON_INFORMATION, this); + return; + } - wxLogMessage("Adding custom bone '%s' to project.", bone); - project->AddCustomBoneRef(bone.ToStdString(), translation); - wxTreeItemId newItem = outfitBones->AppendItem(bonesRoot, bone); - outfitBones->SetItemState(newItem, 0); - cXMirrorBone->AppendString(bone); - cPoseBone->AppendString(bone); + wxTreeItemIdValue cookie; + wxTreeItemId item = outfitBones->GetFirstChild(bonesRoot, cookie); + while (item.IsOk()) { + if (outfitBones->GetItemText(item) == bone) { + wxMessageBox(wxString::Format(_("Bone '%s' already exists in the project!"), bone), _("Error"), wxICON_INFORMATION, this); + return; } + item = outfitBones->GetNextChild(bonesRoot, cookie); } + + MatTransform xform; + std::string parentBone; + GetBoneDlgData(dlg, xform, parentBone); + + wxLogMessage("Adding custom bone '%s' to project.", bone); + project->AddCustomBoneRef(bone.ToStdString(), parentBone, xform); + wxTreeItemId newItem = outfitBones->AppendItem(bonesRoot, bone); + outfitBones->SetItemState(newItem, 0); + cXMirrorBone->AppendString(bone); + cPoseBone->AppendString(bone); +} + +void OutfitStudioFrame::OnEditBone(wxCommandEvent& WXUNUSED(event)) { + AnimBone *bPtr = AnimSkeleton::getInstance().GetBonePtr(contextBone); + if (!bPtr) + return; + + wxDialog dlg; + if (!wxXmlResource::Get()->LoadDialog(&dlg, this, "dlgCustomBone")) + return; + + dlg.Bind(wxEVT_CHAR_HOOK, &OutfitStudioFrame::OnEnterClose, this); + + if (bPtr->parent) + FillParentBoneChoice(dlg, bPtr->parent->boneName); + else + FillParentBoneChoice(dlg); + + wxTextCtrl *boneNameTC = XRCCTRL(dlg, "boneName", wxTextCtrl); + boneNameTC->SetValue(bPtr->boneName); + boneNameTC->Disable(); + + Vector3 rotvec = RotMatToVec(bPtr->xformToParent.rotation); + XRCCTRL(dlg, "textX", wxTextCtrl)->SetValue(wxString() << bPtr->xformToParent.translation.x); + XRCCTRL(dlg, "textY", wxTextCtrl)->SetValue(wxString() << bPtr->xformToParent.translation.y); + XRCCTRL(dlg, "textZ", wxTextCtrl)->SetValue(wxString() << bPtr->xformToParent.translation.z); + XRCCTRL(dlg, "textRX", wxTextCtrl)->SetValue(wxString() << rotvec.x); + XRCCTRL(dlg, "textRY", wxTextCtrl)->SetValue(wxString() << rotvec.y); + XRCCTRL(dlg, "textRZ", wxTextCtrl)->SetValue(wxString() << rotvec.z); + + if (bPtr->isStandardBone) { + dlg.SetLabel("View Standard Bone"); + XRCCTRL(dlg, "textX", wxTextCtrl)->Disable(); + XRCCTRL(dlg, "textY", wxTextCtrl)->Disable(); + XRCCTRL(dlg, "textZ", wxTextCtrl)->Disable(); + XRCCTRL(dlg, "textRX", wxTextCtrl)->Disable(); + XRCCTRL(dlg, "textRY", wxTextCtrl)->Disable(); + XRCCTRL(dlg, "textRZ", wxTextCtrl)->Disable(); + XRCCTRL(dlg, "cParentBone", wxChoice)->Disable(); + XRCCTRL(dlg, "wxID_OK", wxButton)->Disable(); + } + else { + dlg.SetLabel("Edit Custom Bone"); + } + + if (dlg.ShowModal() != wxID_OK) + return; + + MatTransform xform; + std::string parentBone; + GetBoneDlgData(dlg, xform, parentBone); + + project->ModifyCustomBone(bPtr, parentBone, xform); + ApplyPose(); } void OutfitStudioFrame::OnDeleteBone(wxCommandEvent& WXUNUSED(event)) { diff --git a/src/program/OutfitStudio.h b/src/program/OutfitStudio.h index bab8f92d..ead213d0 100644 --- a/src/program/OutfitStudio.h +++ b/src/program/OutfitStudio.h @@ -731,6 +731,7 @@ class OutfitStudioFrame : public wxFrame { ShapeItemData* activeItem = nullptr; std::string activeSlider; bool bEditSlider; + std::string contextBone; wxTreeCtrl* outfitShapes; wxTreeCtrl* outfitBones; @@ -1195,6 +1196,9 @@ class OutfitStudioFrame : public wxFrame { void OnAddCustomBone(wxCommandEvent& event); void OnDeleteBone(wxCommandEvent& event); void OnDeleteBoneFromSelected(wxCommandEvent& event); + void FillParentBoneChoice(wxDialog &dlg, const std::string &selBone = ""); + void GetBoneDlgData(wxDialog &dlg, MatTransform &xform, std::string &parentBone); + void OnEditBone(wxCommandEvent& event); void OnCopyBoneWeight(wxCommandEvent& event); void OnCopySelectedWeight(wxCommandEvent& event); void OnTransferSelectedWeight(wxCommandEvent& event);