diff --git a/Content/EditorHooks/BP_IKRigFunctions.uasset b/Content/EditorHooks/BP_IKRigFunctions.uasset index 6a248bd..0affbcf 100644 --- a/Content/EditorHooks/BP_IKRigFunctions.uasset +++ b/Content/EditorHooks/BP_IKRigFunctions.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7464e38919bd9d6e7e91267b7e7feede35ca1df821e23dd50d5b132c798fc4d1 -size 155940 +oid sha256:13c62820faaae47d786d89786c78ee195a87c88495bd098b2a0bba505b4231ac +size 162317 diff --git a/Content/EditorHooks/BP_SkeletonBoneFunctions.uasset b/Content/EditorHooks/BP_SkeletonBoneFunctions.uasset index abf752d..6756bf4 100644 --- a/Content/EditorHooks/BP_SkeletonBoneFunctions.uasset +++ b/Content/EditorHooks/BP_SkeletonBoneFunctions.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbebc41380fbb0e43d79978f15e939bd81484099be4f6cac65e024f5b2a4ab42 -size 80889 +oid sha256:82d61cbf44d8c94e8de3d5716372dc45542cd7d532b7f1add6947ddc39935cbe +size 114991 diff --git a/Source/TTToolbox/Private/IKRig_ConstraintBones.cpp b/Source/TTToolbox/Private/IKRig_ConstraintBones.cpp index 0fd7ab5..a9c9e15 100644 --- a/Source/TTToolbox/Private/IKRig_ConstraintBones.cpp +++ b/Source/TTToolbox/Private/IKRig_ConstraintBones.cpp @@ -34,8 +34,8 @@ void UIKRig_ConstraintBones::Initialize(const FIKRigSkeleton& IKRigSkeleton) //! @todo @ffs sort bone indices for (auto& constraint : ConstraintBones) { - int32 constraintBone = IKRigSkeleton.GetBoneIndexFromName(constraint.ConstraintBone);//INDEX_NONE - int32 modifiedBone = IKRigSkeleton.GetBoneIndexFromName(constraint.ModifiedBone);// INDEX_NONE; + int32 constraintBone = IKRigSkeleton.GetBoneIndexFromName(constraint.ConstraintBone); + int32 modifiedBone = IKRigSkeleton.GetBoneIndexFromName(constraint.ModifiedBone); if (constraintBone == INDEX_NONE) { @@ -60,7 +60,7 @@ void UIKRig_ConstraintBones::Initialize(const FIKRigSkeleton& IKRigSkeleton) if (errorsOccurred) { - UE_LOG(LogTemp, Error, TEXT("Some constaint bones could not be set up, no constraining will be done. Please check the error messages above.")); + UE_LOG(LogTemp, Error, TEXT("Some constraint bones could not be set up, no constraining will be done. Please check the error messages above.")); m_constraintBones.Empty(); } } diff --git a/Source/TTToolbox/Private/TTToolboxBlueprintLibrary.cpp b/Source/TTToolbox/Private/TTToolboxBlueprintLibrary.cpp index a1094bc..ccc5305 100644 --- a/Source/TTToolbox/Private/TTToolboxBlueprintLibrary.cpp +++ b/Source/TTToolbox/Private/TTToolboxBlueprintLibrary.cpp @@ -22,8 +22,12 @@ // Unreal Engine includes #include "Engine/SkeletalMeshSocket.h" +#include "Engine/AssetManager.h" + #include "Rendering/SkeletalMeshModel.h" +#include "Commandlets/CompressAnimationsCommandlet.h" + #include "IKRigDefinition.h" #include "RigEditor/IKRigController.h" @@ -36,6 +40,10 @@ // function prototypes static FString FVectorToString(const FVector& Vector); +static TArray getAllSkeletalMeshes(USkeleton* Skeleton); + +// helper variables +static const FName gs_rootBoneName("root"); bool UTTToolboxBlueprintLibrary::DumpVirtualBones(USkeleton* Skeleton) @@ -458,42 +466,16 @@ bool UTTToolboxBlueprintLibrary::CheckForMissingCurveNames(const TArray& return hasNoMissingCurveNames; } -static TArray getAllSkeletalMeshes(USkeleton* Skeleton) -{ - check(IsValid(Skeleton)); - - TArray skeletalMeshes; - - FARFilter filter; - filter.ClassNames.Add(USkeletalMesh::StaticClass()->GetFName()); - filter.bRecursiveClasses = true; - const FString skeletonString = FAssetData(Skeleton).GetExportTextName(); - filter.TagsAndValues.Add(USkeletalMesh::GetSkeletonMemberName(), skeletonString); - FAssetRegistryModule& assetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); - - TArray assets; - assetRegistryModule.Get().GetAssets(filter, assets); - - for (auto& asset : assets) - { - if (auto skeletalMesh = Cast(asset.GetAsset())) - { - skeletalMeshes.Add(skeletalMesh); - } - } - - return skeletalMeshes; -} - +//! @todo @ffs check if the engine class could be used here struct CSkeletonReferencePose { CSkeletonReferencePose(const FReferenceSkeleton& ReferenceSkeleton) : m_referenceSkeleton(ReferenceSkeleton) { - LocalSpacePoses.SetNumZeroed(m_referenceSkeleton.GetNum()); + m_localSpacePoses.SetNumZeroed(m_referenceSkeleton.GetNum()); for (int32 ii = 0; ii < m_referenceSkeleton.GetNum(); ++ii) { - LocalSpacePoses[ii] = m_referenceSkeleton.GetRefBonePose()[ii]; + m_localSpacePoses[ii] = m_referenceSkeleton.GetRefBonePose()[ii]; } calculateWorldSpaceTransforms(); @@ -512,19 +494,19 @@ struct CSkeletonReferencePose int32 boneIndex = m_referenceSkeleton.FindBoneIndex(BoneName); if (boneIndex == INDEX_NONE) { - //! @todo error message + UE_LOG(LogTemp, Error, TEXT("The bone name \"%s\" is not present to calculate the local and world transforms. Please create an issue here https://github.com/tuatec/TTToolbox/issues."), *BoneName.ToString()); return; } if (Space == EBonePoseSpaces::Local) { - LocalSpacePoses[boneIndex] = Transform; + m_localSpacePoses[boneIndex] = Transform; } else { const int32 parentIndex = m_referenceSkeleton.GetParentIndex(boneIndex); - const FTransform ParentTransformWS = parentIndex != INDEX_NONE ? WorldSpacePoses[parentIndex] : FTransform::Identity; - LocalSpacePoses[boneIndex] = Transform.GetRelativeTransform(ParentTransformWS); + const FTransform ParentTransformWS = parentIndex != INDEX_NONE ? m_worldSpacePoses[parentIndex] : FTransform::Identity; + m_localSpacePoses[boneIndex] = Transform.GetRelativeTransform(ParentTransformWS); } calculateWorldSpaceTransforms(); @@ -538,36 +520,36 @@ struct CSkeletonReferencePose return FTransform::Identity; } - return Space == EBonePoseSpaces::Local ? LocalSpacePoses[boneIndex] : WorldSpacePoses[boneIndex]; + return Space == EBonePoseSpaces::Local ? m_localSpacePoses[boneIndex] : m_worldSpacePoses[boneIndex]; } private: void calculateWorldSpaceTransforms() { - TArray Processed; - Processed.SetNumZeroed(LocalSpacePoses.Num()); - WorldSpacePoses.SetNum(LocalSpacePoses.Num()); + TArray processed; + processed.SetNumZeroed(m_localSpacePoses.Num()); + m_worldSpacePoses.SetNum(m_localSpacePoses.Num()); for (int32 ii = 0; ii < m_referenceSkeleton.GetNum(); ++ii) { const int32 ParentIndex = m_referenceSkeleton.GetParentIndex(ii); if (ParentIndex != INDEX_NONE) { //ensure(Processed[ii]); - WorldSpacePoses[ii] = LocalSpacePoses[ii] * WorldSpacePoses[ParentIndex]; + m_worldSpacePoses[ii] = m_localSpacePoses[ii] * m_worldSpacePoses[ParentIndex]; } else { - WorldSpacePoses[ii] = LocalSpacePoses[ii]; + m_worldSpacePoses[ii] = m_localSpacePoses[ii]; } - Processed[ii] = true; + processed[ii] = true; } } const FReferenceSkeleton& m_referenceSkeleton; - TArray LocalSpacePoses; + TArray m_localSpacePoses; - TArray WorldSpacePoses; + TArray m_worldSpacePoses; }; bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& NewBones, USkeleton* Skeleton) @@ -590,16 +572,14 @@ bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& { if (Skeleton->GetReferenceSkeleton().FindBoneIndex(newBone.NewBoneName) != INDEX_NONE) { - //! @todo error message - UE_LOG(LogTemp, Error, TEXT("The unweighted bone \"%s\" already exists in the skeleton."), *newBone.NewBoneName.ToString()); + UE_LOG(LogTemp, Error, TEXT("The unweighted bone \"%s\" already exists in the skeleton \"%s\"."), *newBone.NewBoneName.ToString(), *Skeleton->GetPathName()); errorsOccured = true; } if (Skeleton->GetReferenceSkeleton().FindBoneIndex(newBone.ParentBone) != INDEX_NONE) { foundParent = true; - //! @todo error message - UE_LOG(LogTemp, Display, TEXT("ParentBone found \"%s\""), *newBone.ParentBone.ToString()); + UE_LOG(LogTemp, Display, TEXT("The following bone seems to be a parent bone \"%s\" for the new unweighted bone chain."), *newBone.ParentBone.ToString()); } else { @@ -669,8 +649,6 @@ bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& for (auto skeletalMesh : skeletalMeshes) { - UE_LOG(LogTemp, Warning, TEXT("skeletal mesh found %s"), *skeletalMesh->GetFullName()); - if (Skeleton == skeletalMesh->GetSkeleton()) { //! @todo @ffs release renderer ressources @@ -696,8 +674,7 @@ bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& if (parentBoneIndex == INDEX_NONE) { - //! @todo error messge - UE_LOG(LogTemp, Warning, TEXT("parent bone %s not found in reference skeleton skipping..."), *newBone.ParentBone.ToString()); + UE_LOG(LogTemp, Error, TEXT("parent bone \"%s\" of the new bone \"%s\" not found in reference skeleton skipping..."), *newBone.ParentBone.ToString(), *newBone.NewBoneName.ToString()); continue; } @@ -709,12 +686,19 @@ bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& int32 LODIdx = 0; for (FSkeletalMeshLODModel& skeletalMeshLODModel : skeletalMesh->GetImportedModel()->LODModels) { - int32 newBoneIndex = skeletalMesh->GetRefSkeleton().FindBoneIndex(newBone.NewBoneName); int32 parentBoneIndex = skeletalMesh->GetRefSkeleton().FindBoneIndex(newBone.ParentBone); - if (newBoneIndex == INDEX_NONE || parentBoneIndex == INDEX_NONE) + if (parentBoneIndex == INDEX_NONE) { - //! @todo error messge, parent bone message - UE_LOG(LogTemp, Warning, TEXT("bone index not found for %s mesh LODs"), *newBone.NewBoneName.ToString()); + UE_LOG(LogTemp, Warning, TEXT("During LOD adaption the parent bone \"%s\" was not present in the skeletal mesh \"%s\""), + *newBone.ParentBone.ToString(), *skeletalMesh->GetPathName()); + continue; + } + + int32 newBoneIndex = skeletalMesh->GetRefSkeleton().FindBoneIndex(newBone.NewBoneName); + if (newBoneIndex == INDEX_NONE) + { + UE_LOG(LogTemp, Warning, TEXT("During LOD adaption the new bone \"%s\" was not present in the skeletal mesh \"%s\""), + *newBone.NewBoneName.ToString(), *skeletalMesh->GetPathName()); continue; } @@ -749,8 +733,7 @@ bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& // the mesh got new bones and now it is necessary to merge those bones into the USkeleton asset as well if (!(Skeleton->MergeAllBonesToBoneTree(skeletalMesh))) { - //! @todo error message - UE_LOG(LogTemp, Error, TEXT("MergeAllBonesToBoneTree failed")); + UE_LOG(LogTemp, Error, TEXT("The final step of merging all bones for the skeletal mesh \"%s\"into the bone failed. Please create an issue here https://github.com/tuatec/TTToolbox/issues."), *(skeletalMesh->GetPathName())); } for (auto& newBone : NewBones) @@ -759,20 +742,18 @@ bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& int32 constrainBoneIndex = skeletalMesh->GetRefSkeleton().FindBoneIndex(newBone.ConstraintBone); if (constrainBoneIndex == INDEX_NONE) { - //! @todo error messge - UE_LOG(LogTemp, Warning, TEXT("constraint bone %s not found in reference skeleton applying identity transform."), *newBone.ConstraintBone.ToString()); + UE_LOG(LogTemp, Warning, TEXT("constraint bone \"%s\" was not found in the reference skeleton of skeleton asset \"%s\" applying identity transform."), *newBone.ConstraintBone.ToString(), *(Skeleton->GetPathName())); } else if (constrainBoneIndex >= skeletalMesh->GetRefSkeleton().GetRefBonePose().Num()) { - //! @todo error messge - UE_LOG(LogTemp, Warning, TEXT("constraint bone %s index is not valid."), *newBone.ConstraintBone.ToString()); + UE_LOG(LogTemp, Warning, TEXT("constraint bone \"%s\" index is not valid."), *newBone.ConstraintBone.ToString()); } else { CSkeletonReferencePose skeletonReferencePose(skeletalMesh->GetRefSkeleton()); const FTransform worldTransform = skeletonReferencePose.GetRefBonePose(newBone.ConstraintBone, CSkeletonReferencePose::EBonePoseSpaces::World); skeletonReferencePose.SetBonePose(newBone.NewBoneName, worldTransform, CSkeletonReferencePose::EBonePoseSpaces::World); - FTransform newBoneTransform = skeletonReferencePose.GetRefBonePose(newBone.NewBoneName); + const FTransform newBoneTransform = skeletonReferencePose.GetRefBonePose(newBone.NewBoneName); FReferenceSkeletonModifier referenceSkeletonModifier(skeletalMesh->GetRefSkeleton(), Skeleton); referenceSkeletonModifier.UpdateRefPoseTransform(skeletalMesh->GetRefSkeleton().FindBoneIndex(newBone.NewBoneName), newBoneTransform); @@ -822,13 +803,18 @@ bool UTTToolboxBlueprintLibrary::AddUnweightedBone(const TArray& return true; } -#include "Commandlets/CompressAnimationsCommandlet.h" -#include "Engine/AssetManager.h" void UTTToolboxBlueprintLibrary::RequestAnimationRecompress(USkeleton* Skeleton) { + // check input arguments + if (!IsValid(Skeleton)) + { + UE_LOG(LogTemp, Error, TEXT("Called \"RequestAnimationRecompress\" with invalid skeleton.")); + return; + } + TArray assets; - IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); - AssetRegistry.GetAssetsByClass(UAnimSequence::StaticClass()->GetFName(), assets); + IAssetRegistry& assetRegistry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); + assetRegistry.GetAssetsByClass(UAnimSequence::StaticClass()->GetFName(), assets); for (auto& asset : assets) { @@ -848,8 +834,6 @@ bool UTTToolboxBlueprintLibrary::ConstraintBonesForSkeletonPose(const TArrayGetReferenceSkeleton().FindBoneIndex("root") != INDEX_NONE) { - UE_LOG(LogTemp, Error, TEXT("root bone already exists in %s"), *(Skeleton->GetPathName())); + UE_LOG(LogTemp, Error, TEXT("root bone already exists in \"%s\""), *(Skeleton->GetPathName())); return false; } @@ -870,178 +854,208 @@ bool UTTToolboxBlueprintLibrary::AddRootBone(USkeleton* Skeleton) TArray skeletalMeshes = getAllSkeletalMeshes(Skeleton); if (skeletalMeshes.IsEmpty()) { - //! @todo error message - UE_LOG(LogTemp, Error, TEXT("no skeletal meshes found")); + UE_LOG(LogTemp, Error, TEXT("During the call of \"AddRootBone\" no skeletal meshes found that are connected to the skeleton \"s\""), *(Skeleton->GetPathName())); return false; } + // Sadly, the implementation does have some issues with wrong bone indices, + // see https://github.com/tuatec/TTToolbox/issues/5#issuecomment-1184052765 for the details. + // That's why all virtual bones get removed (same state if a skeletal mesh is imported through an fbx file) + // and later added again. This step needs to be done anyways as the bone tree needs to be regenerated, + // sadly again there is no public API that can trigger this. BUT! It is possible to trigger the regeneration process + // through adding a virtual bone. + // Long story short, adding virtual bones makes it possible to introduce unweighted bones in a save way. ;-) + const auto savedVirtualBones = Skeleton->GetVirtualBones(); + { + TArray virtualBoneNamesToDelete; + for (auto& virtualBone : savedVirtualBones) { + virtualBoneNamesToDelete.Add(virtualBone.VirtualBoneName); + } + if (virtualBoneNamesToDelete.Num() > 0) { + Skeleton->RemoveVirtualBones(virtualBoneNamesToDelete); + } + } + uint32 modifiedSkeletalMeshes = 0; for (auto skeletalMesh : skeletalMeshes) { - { // add root bone - FReferenceSkeleton referenceSkeleton; - { - FReferenceSkeletonModifier referenceSkeletonModifier(referenceSkeleton, skeletalMesh->GetSkeleton()); - - const FMeshBoneInfo meshRootBoneInfo(gs_rootBoneName, gs_rootBoneName.ToString(), INDEX_NONE); - referenceSkeletonModifier.Add(meshRootBoneInfo, FTransform::Identity); - - // increase parent bone indices to sucessfully register the root bone - for (int32 ii = 0; ii < skeletalMesh->GetSkeleton()->GetReferenceSkeleton().GetRawBoneNum(); ii++) + if (Skeleton == skeletalMesh->GetSkeleton()) + { + { // add root bone + FReferenceSkeleton referenceSkeleton; { - FMeshBoneInfo meshBoneInfo = skeletalMesh->GetSkeleton()->GetReferenceSkeleton().GetRawRefBoneInfo()[ii]; - meshBoneInfo.ParentIndex++; - const auto boneRefPoseTransform = skeletalMesh->GetSkeleton()->GetReferenceSkeleton().GetRawRefBonePose()[ii]; - referenceSkeletonModifier.Add(meshBoneInfo, boneRefPoseTransform); - } - } + FReferenceSkeletonModifier referenceSkeletonModifier(referenceSkeleton, skeletalMesh->GetSkeleton()); - skeletalMesh->SetRefSkeleton(referenceSkeleton); - } + const FMeshBoneInfo meshRootBoneInfo(gs_rootBoneName, gs_rootBoneName.ToString(), INDEX_NONE); + referenceSkeletonModifier.Add(meshRootBoneInfo, FTransform::Identity); - // reset all bone transforms and reset retarget pose - skeletalMesh->GetRetargetBasePose().Empty(); - skeletalMesh->CalculateInvRefMatrices(); + // increase parent bone indices to sucessfully register the root bone + for (int32 ii = 0; ii < skeletalMesh->GetSkeleton()->GetReferenceSkeleton().GetRawBoneNum(); ii++) + { + FMeshBoneInfo meshBoneInfo = skeletalMesh->GetSkeleton()->GetReferenceSkeleton().GetRawRefBoneInfo()[ii]; + meshBoneInfo.ParentIndex++; + const auto boneRefPoseTransform = skeletalMesh->GetSkeleton()->GetReferenceSkeleton().GetRawRefBonePose()[ii]; + referenceSkeletonModifier.Add(meshBoneInfo, boneRefPoseTransform); + } + } - uint32 LODIndex = 0; - for (FSkeletalMeshLODModel& skeletalMeshLODModel : skeletalMesh->GetImportedModel()->LODModels) - { - // increase active bone indices to sucessfully register the new root bone - for (auto& activeBoneIndex : skeletalMeshLODModel.ActiveBoneIndices) - { - activeBoneIndex++; + skeletalMesh->SetRefSkeleton(referenceSkeleton); } - // insert root bone - skeletalMeshLODModel.ActiveBoneIndices.Insert(0, 0); - // increase required bone (unweighted bones) indices to sucessfully register the new root bone - for (auto& requiredBoneIndex : skeletalMeshLODModel.RequiredBones) - { - requiredBoneIndex++; - } - // insert root bone - skeletalMeshLODModel.RequiredBones.Insert(0, 0); + // reset all bone transforms and reset retarget pose + skeletalMesh->GetRetargetBasePose().Empty(); + skeletalMesh->CalculateInvRefMatrices(); - // update bone references used by the skin weights - for (auto& skinWeightsProfile : skeletalMeshLODModel.SkinWeightProfiles) + uint32 LODIndex = 0; + for (FSkeletalMeshLODModel& skeletalMeshLODModel : skeletalMesh->GetImportedModel()->LODModels) { - FImportedSkinWeightProfileData& importedSkinWeightProfileData = skeletalMeshLODModel.SkinWeightProfiles.FindChecked(skinWeightsProfile.Key); + // increase active bone indices to sucessfully register the new root bone + for (auto& activeBoneIndex : skeletalMeshLODModel.ActiveBoneIndices) + { + activeBoneIndex++; + } + // insert root bone + skeletalMeshLODModel.ActiveBoneIndices.Insert(0, 0); - // increase bone skin weight indices to sucessfully register the root bone - for (auto& skinWeight : importedSkinWeightProfileData.SkinWeights) + // increase required bone (unweighted bones) indices to sucessfully register the new root bone + for (auto& requiredBoneIndex : skeletalMeshLODModel.RequiredBones) { - for (int32 ii = 0; ii < MAX_TOTAL_INFLUENCES; ii++) + requiredBoneIndex++; + } + // insert root bone + skeletalMeshLODModel.RequiredBones.Insert(0, 0); + + // update bone references used by the skin weights + for (auto& skinWeightsProfile : skeletalMeshLODModel.SkinWeightProfiles) + { + FImportedSkinWeightProfileData& importedSkinWeightProfileData = skeletalMeshLODModel.SkinWeightProfiles.FindChecked(skinWeightsProfile.Key); + + // increase bone skin weight indices to sucessfully register the root bone + for (auto& skinWeight : importedSkinWeightProfileData.SkinWeights) { - if (skinWeight.InfluenceWeights[ii] > 0) + for (int32 ii = 0; ii < MAX_TOTAL_INFLUENCES; ii++) { - skinWeight.InfluenceBones[ii]++; + if (skinWeight.InfluenceWeights[ii] > 0) + { + skinWeight.InfluenceBones[ii]++; + } + } + } + + // increase source model influence bone indices to sucessfully register the root bone + for (auto& sourceModelInfluence : importedSkinWeightProfileData.SourceModelInfluences) + { + if (sourceModelInfluence.Weight > 0) + { + sourceModelInfluence.BoneIndex++; } } } - // increase source model influence bone indices to sucessfully register the root bone - for (auto& sourceModelInfluence : importedSkinWeightProfileData.SourceModelInfluences) + // adapt LOD sections + if (skeletalMesh->IsLODImportedDataBuildAvailable(LODIndex) && !skeletalMesh->IsLODImportedDataEmpty(LODIndex)) { - if (sourceModelInfluence.Weight > 0) + FSkeletalMeshImportData skeletalMeshImportData; + skeletalMesh->LoadLODImportedData(LODIndex, skeletalMeshImportData); + + // increase parent indices to sucessfully add the new root bone + int32 numRootBoneChilds = 0; + for (auto& referenceBoneBinary : skeletalMeshImportData.RefBonesBinary) { - sourceModelInfluence.BoneIndex++; + if (referenceBoneBinary.ParentIndex == INDEX_NONE) + { + numRootBoneChilds += referenceBoneBinary.NumChildren; + } + referenceBoneBinary.ParentIndex++; } - } - } - // adapt LOD sections - if (skeletalMesh->IsLODImportedDataBuildAvailable(LODIndex) && !skeletalMesh->IsLODImportedDataEmpty(LODIndex)) - { - FSkeletalMeshImportData skeletalMeshImportData; - skeletalMesh->LoadLODImportedData(LODIndex, skeletalMeshImportData); + const SkeletalMeshImportData::FJointPos rootBonePosition = { FTransform3f::Identity, 1.f, 100.f, 100.f, 100.f }; + const SkeletalMeshImportData::FBone rootBone = { gs_rootBoneName.ToString(), 0, numRootBoneChilds, INDEX_NONE, rootBonePosition }; + skeletalMeshImportData.RefBonesBinary.Insert(rootBone, 0); - // increase parent indices to sucessfully add the new root bone - int32 numRootBoneChilds = 0; - for (auto& referenceBoneBinary : skeletalMeshImportData.RefBonesBinary) - { - if (referenceBoneBinary.ParentIndex == INDEX_NONE) + // increase bone influences to sucessfully add the new root bone + for (auto& influence : skeletalMeshImportData.Influences) { - numRootBoneChilds += referenceBoneBinary.NumChildren; + influence.BoneIndex++; } - referenceBoneBinary.ParentIndex++; - } - const SkeletalMeshImportData::FJointPos rootBonePosition = { FTransform3f::Identity, 1.f, 100.f, 100.f, 100.f }; - const SkeletalMeshImportData::FBone rootBone = { gs_rootBoneName.ToString(), 0, numRootBoneChilds, INDEX_NONE, rootBonePosition }; - skeletalMeshImportData.RefBonesBinary.Insert(rootBone, 0); + if (skeletalMeshImportData.MorphTargets.Num() > 0) + { + //! @todo @ffs is it possible to support morph targets? + UE_LOG(LogTemp, Warning, TEXT("MorphTargets are currently not supported.")); + } - // increase bone influences to sucessfully add the new root bone - for (auto& influence : skeletalMeshImportData.Influences) - { - influence.BoneIndex++; - } + if (skeletalMeshImportData.AlternateInfluences.Num() > 0) + { + //! @todo @ffs is it possible to support alternate influences? + UE_LOG(LogTemp, Warning, TEXT("AlternateInfluences are currently not supported.")); + } - if (skeletalMeshImportData.MorphTargets.Num() > 0) + skeletalMesh->SaveLODImportedData(LODIndex, skeletalMeshImportData); + } + else { - //! @todo @ffs is it possible to support morph targets? - UE_LOG(LogTemp, Warning, TEXT("MorphTargets are currently not supported.")); + for (auto& LODSection : skeletalMeshLODModel.Sections) + { + // increase bone indices to sucessfully register the new root bone + for (auto& boneIndex : LODSection.BoneMap) + { + boneIndex++; + } + } } - if (skeletalMeshImportData.AlternateInfluences.Num() > 0) + ++LODIndex; + } + + if (modifiedSkeletalMeshes == 0) + { + if (!(Skeleton->RecreateBoneTree(skeletalMesh))) { - //! @todo @ffs is it possible to support alternate influences? - UE_LOG(LogTemp, Warning, TEXT("AlternateInfluences are currently not supported.")); + UE_LOG(LogTemp, Error, TEXT("Final step of recreating the bone tree failed for skeleton asset \"%s\". Please raise a issue here: https://github.com/tuatec/TTToolbox/issues."), *(skeletalMesh->GetPathName())); } - - skeletalMesh->SaveLODImportedData(LODIndex, skeletalMeshImportData); } else { - for (auto& LODSection : skeletalMeshLODModel.Sections) + // the mesh got new bones and now it is necessary to merge those bones into the USkeleton asset as well + if (!(Skeleton->MergeAllBonesToBoneTree(skeletalMesh))) { - // increase bone indices to sucessfully register the new root bone - for (auto& boneIndex : LODSection.BoneMap) - { - boneIndex++; - } + UE_LOG(LogTemp, Error, TEXT("The final step of merging all bones for the skeletal mesh \"%s\"into the bone failed. Please create an issue here https://github.com/tuatec/TTToolbox/issues."), *(skeletalMesh->GetPathName())); } } - ++LODIndex; - } - - if (modifiedSkeletalMeshes == 0) - { - if (!(Skeleton->RecreateBoneTree(skeletalMesh))) + // through caching reasons the USkeleton has internally a mapping table between skeletal meshes and the skeleton, + // as new bones were added this table is not valid anymore ==> force rebuilding of that table! + // Sadly none of these methods is exposed for plugin developers :( + // - USkeleton::HandleVirtualBoneChanges + // - USkeleton::RebuildLinkup + // - USkeleton::RemoveLinkup + // + // But happily adding and removing virtual bones call internall USkeleton::HandleVirtualBoneChanges, + // which should rebuild the mapping table ;-) + FName virtualBoneName = *(gs_rootBoneName.ToString() + "_delete_me"); + if (!Skeleton->AddNewVirtualBone(gs_rootBoneName, gs_rootBoneName, virtualBoneName)) { - //! @todo error message - UE_LOG(LogTemp, Error, TEXT("RecreateBoneTree failed")); + UE_LOG(LogTemp, Error, TEXT("failed to add dirty virtual bone hack to force the rebuild of the bone mapping table of skeleton \"%s\""), *Skeleton->GetPathName()); } + Skeleton->RemoveVirtualBones({ virtualBoneName }); + + skeletalMesh->PostEditChange(); + skeletalMesh->Modify(); + modifiedSkeletalMeshes++; } - else + } + + // finally readd the virtual bones again to savely store everything + if (savedVirtualBones.Num() > 0) + { + for (auto& virtualBone : savedVirtualBones) { - // the mesh got new bones and now it is necessary to merge those bones into the USkeleton asset as well - if (!(Skeleton->MergeAllBonesToBoneTree(skeletalMesh))) + if (!UTTToolboxBlueprintLibrary::AddVirtualBone(virtualBone.VirtualBoneName, virtualBone.SourceBoneName, virtualBone.TargetBoneName, Skeleton)) { - //! @todo error message - UE_LOG(LogTemp, Error, TEXT("MergeAllBonesToBoneTree failed")); + UE_LOG(LogTemp, Error, TEXT("Internal error! Failed to add virtual bone \"%s\" again please raise a issue here: https://github.com/tuatec/TTToolbox/issues."), *virtualBone.VirtualBoneName.ToString()); } } - - // through caching reasons the USkeleton has internally a mapping table between skeletal meshes and the skeleton, - // as new bones were added this table is not valid anymore ==> force rebuilding of that table! - // Sadly none of these methods is exposed for plugin developers :( - // - USkeleton::HandleVirtualBoneChanges - // - USkeleton::RebuildLinkup - // - USkeleton::RemoveLinkup - // - // But happily adding and removing virtual bones call internall USkeleton::HandleVirtualBoneChanges, - // which should rebuild the mapping table ;-) - FName virtualBoneName = *(gs_rootBoneName.ToString() + "_delete_me"); - if (!Skeleton->AddNewVirtualBone(gs_rootBoneName, gs_rootBoneName, virtualBoneName)) - { - UE_LOG(LogTemp, Error, TEXT("failed to add dirty virtual bone hack to force the rebuild of the bone mapping table of skeleton \"%s\""), *Skeleton->GetPathName()); - } - Skeleton->RemoveVirtualBones({ virtualBoneName }); - - skeletalMesh->PostEditChange(); - skeletalMesh->Modify(); - modifiedSkeletalMeshes++; } if (modifiedSkeletalMeshes > 0) @@ -1255,3 +1269,30 @@ FString FVectorToString(const FVector& Vector) return str; } + +static TArray getAllSkeletalMeshes(USkeleton* Skeleton) +{ + check(IsValid(Skeleton)); + + TArray skeletalMeshes; + + FARFilter filter; + filter.ClassNames.Add(USkeletalMesh::StaticClass()->GetFName()); + filter.bRecursiveClasses = true; + const FString skeletonString = FAssetData(Skeleton).GetExportTextName(); + filter.TagsAndValues.Add(USkeletalMesh::GetSkeletonMemberName(), skeletonString); + FAssetRegistryModule& assetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); + + TArray assets; + assetRegistryModule.Get().GetAssets(filter, assets); + + for (auto& asset : assets) + { + if (auto skeletalMesh = Cast(asset.GetAsset())) + { + skeletalMeshes.Add(skeletalMesh); + } + } + + return skeletalMeshes; +} diff --git a/Source/TTToolbox/Public/TTToolboxBlueprintLibrary.h b/Source/TTToolbox/Public/TTToolboxBlueprintLibrary.h index de810f8..01ed445 100644 --- a/Source/TTToolbox/Public/TTToolboxBlueprintLibrary.h +++ b/Source/TTToolbox/Public/TTToolboxBlueprintLibrary.h @@ -30,6 +30,8 @@ class USkeleton; class UIKRigDefinition; +// Helper stucture that is exposed to Blueprints to be independent from the ik rig implementation. +// Additionally, this can be reused in data tables to store the bone chains. USTRUCT(Blueprintable) struct TTTOOLBOX_API FBoneChain_BP { @@ -44,15 +46,19 @@ struct TTTOOLBOX_API FBoneChain_BP , IKGoalName(BoneChain.IKGoalName) {} + // stores the bone chain name that will be shown in the ik rig asset editor window UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneChain) FName ChainName = NAME_None; + // stores the beginning bone name of the bone chain UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneChain) FName StartBone = NAME_None; + // stores the ending bone name of the bone chain UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = BoneChain) FName EndBone = NAME_None; + // stores the ik goal name that is visible in the ik rig asset editor window UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = IK) FName IKGoalName = NAME_None; }; @@ -113,18 +119,27 @@ class TTTOOLBOX_API UTTToolboxBlueprintLibrary : public UBlueprintFunctionLibrar // skeleton functions + // dumps all available skeleton curve names to the console and makes them available in the clipboard as well UFUNCTION(BlueprintCallable, Category = "TTToolbox") static bool DumpSkeletonCurveNames(USkeleton* Skeleton); + // checks if the given 'CurveNamesToCheck' are available in the given 'Skeleton' and prints the missing curves to the console UFUNCTION(BlueprintCallable, Category = "TTToolbox") static bool CheckForMissingCurveNames(const TArray& CurveNamesToCheck, USkeleton* Skeleton); + // adds the fiven 'NewBones' to the given 'Skeleton' and it's connected skeletal meshes. + // NOTE: Sadly Unreal Engine does come with lot's of assertions and it is very hard to implement this feature in a save way, + // the function removes all virtual bones and adds them after again after the unweighted bones are added to the skeletal meshes. UFUNCTION(BlueprintCallable, Category = "TTToolbox") static bool AddUnweightedBone(const TArray& NewBones, USkeleton* Skeleton); + //! @todo @ffs implement this feature UFUNCTION(BlueprintCallable, Category = "TTToolbox") static bool ConstraintBonesForSkeletonPose(const TArray& ConstraintBones, USkeleton* Skeleton); + // adds the root bone to the given 'Skeleton' and it's connected skeletal meshes (needed for Mixamo based characters) + // NOTE: Sadly Unreal Engine does come with lot's of assertions and it is very hard to implement this feature in a save way, + // the function removes all virtual bones and adds them after again after the root bone was added to the skeletal meshes. UFUNCTION(BlueprintCallable, Category = "TTToolbox") static bool AddRootBone(USkeleton* Skeleton); @@ -135,6 +150,7 @@ class TTTOOLBOX_API UTTToolboxBlueprintLibrary : public UBlueprintFunctionLibrar // AnimSequence functions + // forces animation sequence recompression, which will also reconstraint the virtual bones UFUNCTION(BlueprintCallable, Category = "TTToolbox") static void RequestAnimationRecompress(USkeleton* Skeleton);