// Copyright Epic Games, Inc. All Rights Reserved. #include "RelativeBodyAnimModifier.h" #include "AnimPose.h" #include "MeshDescription.h" #include "RelativeBodyAnimNotifies.h" #include "RelativeBodyUtils.h" #include "SkeletalMeshAttributes.h" #include "Animation/AnimSequence.h" #include "Components/SkeletalMeshComponent.h" #include "Engine/SkeletalMesh.h" #include "Misc/ScopedSlowTask.h" #include "PhysicsEngine/PhysicsAsset.h" #include "PhysicsEngine/SkeletalBodySetup.h" #include "Rendering/SkeletalMeshRenderData.h" #include "Spatial/PointHashGrid3.h" #define LOCTEXT_NAMESPACE "RelativeBodyAnimModifier" void URelativeBodyAnimModifier::OnApply_Implementation(UAnimSequence* InAnimation) { // TODO: Make log category for relative body utils? if (InAnimation == nullptr) { UE_LOG(LogAnimation, Error, TEXT("Cannot create RelativeBodyAnimNotify. Reason: Invalid Animation")); return; } if (!SkeletalMeshAsset) { UE_LOG(LogAnimation, Error, TEXT("Cannot create RelativeBodyAnimNotify. Reason: Invalid SkeletalMeshComponent")); return; } if (!SkeletalMeshAsset->CloneMeshDescription(LODIndex, MeshDescription)) { UE_LOG(LogAnimation, Error, TEXT("Cannot create RelativeBodyAnimNotify. Reason: Could not clone mesh description")); return; } if (!NotifyClass || NotifyClass == URelativeBodyAnimNotifyBase::StaticClass()) { UE_LOG(LogAnimation, Error, TEXT("Cannot create RelativeBodyAnimNotify. Reason: Must specify valid relative body notify subclass to create")); return; } if (!PhysicsAssetOverride) { UE_LOG(LogAnimation, Warning, TEXT("RelativeBodyAnimModifier: No physics asset override specified, using skeletal mesh physics asset!")) } //Cache UPhysicsAsset* PhysicsAsset = GetPhysicsAsset(); FReferenceSkeleton& RefSkeleton = SkeletalMeshAsset->GetRefSkeleton(); // TODO: Move cached data out of modifier (or check if it's outdated and only rebuild in that case) CacheBodyDataForSourceMesh(CachedBodySourceData); const TArray& BodyIndicesParentBodyIndices = CachedBodySourceData.BodyIndicesParentBodyIndices; const TArray& BodyIndicesToIgnore = CachedBodySourceData.BodyIndicesToIgnore; const TArray& IsDomainBodyIndices = CachedBodySourceData.IsDomainBody; const TArray& BodyIndicesToBoneIndices = CachedBodySourceData.BodyIndicesToSourceBoneIndices; const TArray& BoneIndicesToBodyIndices = CachedBodySourceData.SourceBoneIndicesToBodyIndices; const TArray>& VertexIndicesInfluencedByBodyIndices = CachedBodySourceData.SourceVertexIndicesInfluencedByBodyIndices; const int32 NumBodies = BodyIndicesToBoneIndices.Num(); const float HashGridCellSize = std::min(1.5f*ContactThreshold, 50.f); // the length of the cell size in the point hash gri // Process animation asset { const FAnimPoseEvaluationOptions AnimPoseEvalOptions{ EAnimDataEvalType::Raw, true, false, false, nullptr, true, false }; const float SequenceLength = InAnimation->GetPlayLength(); const float SampleStep = 1.0f / static_cast(SampleRate); const int SampleNum = FMath::TruncToInt(SequenceLength / SampleStep); URelativeBodyBakeAnimNotify* BakedAnimNotifyInfo = nullptr; bool bUseDenseNotifyInfo = NotifyClass->IsChildOf(URelativeBodyBakeAnimNotify::StaticClass()); if (bUseDenseNotifyInfo) { FName TrackName("RelBody-<" + SkeletalMeshAsset->GetName() + ">"); UE_LOG(LogAnimation, Verbose, TEXT("TrackName %d: '%s'."), GeneratedNotifyTracks.Num()-1, *TrackName.ToString()); const bool bDoesTrackNameAlreadyExist = UAnimationBlueprintLibrary::IsValidAnimNotifyTrackName(InAnimation, TrackName); if (!bDoesTrackNameAlreadyExist) { UAnimationBlueprintLibrary::AddAnimationNotifyTrack(InAnimation, TrackName, FLinearColor::MakeRandomColor()); GeneratedNotifyTracks.Add(TrackName); } BakedAnimNotifyInfo = Cast(UAnimationBlueprintLibrary::AddAnimationNotifyEvent(InAnimation, TrackName, 0., NotifyClass)); if (!BakedAnimNotifyInfo) { return; } for (int32 BodyIndex1 = 0; BodyIndex1 < NumBodies; ++BodyIndex1) { if (BodyIndicesToIgnore[BodyIndex1]) { continue; } for (int32 BodyIndex2 = BodyIndex1 + 1; BodyIndex2 < NumBodies; ++BodyIndex2) { if (BodyIndicesToIgnore[BodyIndex2]) continue; if (BodyIndicesParentBodyIndices[BodyIndex1] == BodyIndex2 || BodyIndicesParentBodyIndices[BodyIndex2] == BodyIndex1) continue; if (IsDomainBodyIndices[BodyIndex1]&&IsDomainBodyIndices[BodyIndex2]) continue; int32 QueryBodyIndex = VertexIndicesInfluencedByBodyIndices[BodyIndex1].Num() < VertexIndicesInfluencedByBodyIndices[BodyIndex2].Num() ? BodyIndex1 : BodyIndex2; int32 HashBodyIndex = VertexIndicesInfluencedByBodyIndices[BodyIndex1].Num() < VertexIndicesInfluencedByBodyIndices[BodyIndex2].Num() ? BodyIndex2 : BodyIndex1; bool IsParentDominates = false; if (IsDomainBodyIndices[BodyIndex1]) { QueryBodyIndex = BodyIndex2; HashBodyIndex = BodyIndex1; IsParentDominates = true; } else if (IsDomainBodyIndices[BodyIndex2]) { QueryBodyIndex = BodyIndex1; HashBodyIndex = BodyIndex2; IsParentDominates = true; } else if (VertexIndicesInfluencedByBodyIndices[BodyIndex1].Num() < VertexIndicesInfluencedByBodyIndices[BodyIndex2].Num()) { QueryBodyIndex = BodyIndex1; HashBodyIndex = BodyIndex2; } else { QueryBodyIndex = BodyIndex2; HashBodyIndex = BodyIndex1; } FName BoneName1 = RefSkeleton.GetBoneName(BodyIndicesToBoneIndices[HashBodyIndex]); FName BoneName2 = RefSkeleton.GetBoneName(BodyIndicesToBoneIndices[QueryBodyIndex]); // FName BodyPairName("<" + SkeletalMeshAsset->GetName() + ">" + BoneName1.ToString() + "-" + BoneName2.ToString()); BakedAnimNotifyInfo->BodyPairs.Add(BoneName1); BakedAnimNotifyInfo->BodyPairs.Add(BoneName2); BakedAnimNotifyInfo->bBodyPairsIsParentDominates.Add(IsParentDominates); } } BakedAnimNotifyInfo->BodyPairsLocalReference.SetNumZeroed(BakedAnimNotifyInfo->BodyPairs.Num()*SampleNum); BakedAnimNotifyInfo->BodyPairsSampleTime.SetNumZeroed(SampleNum); BakedAnimNotifyInfo->NumSamples = SampleNum; } FScopedSlowTask BakePairVertsTask((float)SampleNum, LOCTEXT("BakeVertsTaskText", "Baking Body Pair Verts...")); BakePairVertsTask.MakeDialog(); // Get ground levels and max speed values. TArray VLocations; for (int SampleIndex = 0; SampleIndex < SampleNum; ++SampleIndex) { BakePairVertsTask.EnterProgressFrame(1.0f); const float SampleTime = FMath::Clamp(static_cast(SampleIndex) * SampleStep, 0.0f, SequenceLength); const float FutureSampleTime = FMath::Clamp((static_cast(SampleIndex) + 1.0f) * SampleStep, 0.0f, SequenceLength); FAnimPose AnimPose; FAnimPose FutureAnimPose; UAnimPoseExtensions::GetAnimPoseAtTime(InAnimation, SampleTime, AnimPoseEvalOptions, AnimPose); UAnimPoseExtensions::GetAnimPoseAtTime(InAnimation, FutureSampleTime, AnimPoseEvalOptions, FutureAnimPose); TArray CacheToLocals; GetRefToAnimPoseMatrices(CacheToLocals, AnimPose); GetSkinnedVertices(VLocations, CacheToLocals); // build a spatial hash grid TArray*> BodiesVertHash; BodiesVertHash.SetNum(NumBodies); size_t count = 0; for (int32 BodyIndex = 0; BodyIndex < NumBodies; ++BodyIndex) { BodiesVertHash[BodyIndex] = new UE::Geometry::TPointHashGrid3f(HashGridCellSize, INDEX_NONE); BodiesVertHash[BodyIndex]->Reserve(VertexIndicesInfluencedByBodyIndices[BodyIndex].Num()); for (int32 VertexIndex : VertexIndicesInfluencedByBodyIndices[BodyIndex]) { BodiesVertHash[BodyIndex]->InsertPointUnsafe(VertexIndex, static_cast(VLocations[VertexIndex])); } } TArray ContactedBodyPairs; TArray ContactPoints; ContactPoints.Empty(); if (BakedAnimNotifyInfo) { BakedAnimNotifyInfo->BodyPairsSampleTime[SampleIndex] = SampleTime; } for (int32 BodyIndex1 = 0; BodyIndex1 < NumBodies; ++BodyIndex1) { if (BodyIndicesToIgnore[BodyIndex1]) continue; for (int32 BodyIndex2 = BodyIndex1 + 1; BodyIndex2 < NumBodies; ++BodyIndex2) { if (BodyIndicesToIgnore[BodyIndex2]) continue; if (BodyIndicesParentBodyIndices[BodyIndex1] == BodyIndex2 || BodyIndicesParentBodyIndices[BodyIndex2] == BodyIndex1) continue; if (IsDomainBodyIndices[BodyIndex1]&&IsDomainBodyIndices[BodyIndex2]) continue; int32 QueryBodyIndex = VertexIndicesInfluencedByBodyIndices[BodyIndex1].Num() < VertexIndicesInfluencedByBodyIndices[BodyIndex2].Num() ? BodyIndex1 : BodyIndex2; int32 HashBodyIndex = VertexIndicesInfluencedByBodyIndices[BodyIndex1].Num() < VertexIndicesInfluencedByBodyIndices[BodyIndex2].Num() ? BodyIndex2 : BodyIndex1; bool IsParentDominates = false; if (IsDomainBodyIndices[BodyIndex1]) { QueryBodyIndex = BodyIndex2; HashBodyIndex = BodyIndex1; IsParentDominates = true; } else if (IsDomainBodyIndices[BodyIndex2]) { QueryBodyIndex = BodyIndex1; HashBodyIndex = BodyIndex2; IsParentDominates = true; } else if (VertexIndicesInfluencedByBodyIndices[BodyIndex1].Num() < VertexIndicesInfluencedByBodyIndices[BodyIndex2].Num()) { QueryBodyIndex = BodyIndex1; HashBodyIndex = BodyIndex2; } else { QueryBodyIndex = BodyIndex2; HashBodyIndex = BodyIndex1; } float MinDist = FLT_MAX; int32 NearestHashVertexIndex = INDEX_NONE; int32 NearestQueryVertexIndex = INDEX_NONE; for (int32 QueryVertexIndex : VertexIndicesInfluencedByBodyIndices[QueryBodyIndex]) { FVector3f QueryPoint = VLocations[QueryVertexIndex]; TPair AnyClosePoint = { INDEX_NONE, TNumericLimits::Max() }; AnyClosePoint = BodiesVertHash[HashBodyIndex]->FindNearestInRadius( QueryPoint, ContactThreshold, [&VLocations, QueryPoint](int32 VID) { return FVector3f::DistSquared(FVector3f(VLocations[VID]), QueryPoint); }); if (AnyClosePoint.Value < MinDist) { NearestQueryVertexIndex = QueryVertexIndex; NearestHashVertexIndex = AnyClosePoint.Key; MinDist = AnyClosePoint.Value; } } if (NearestQueryVertexIndex != INDEX_NONE) { FName BoneName1 = RefSkeleton.GetBoneName(BodyIndicesToBoneIndices[HashBodyIndex]); FName BoneName2 = RefSkeleton.GetBoneName(BodyIndicesToBoneIndices[QueryBodyIndex]); FVector3f ContactPoint1 = VLocations[NearestHashVertexIndex]; FVector3f ContactPoint2 = VLocations[NearestQueryVertexIndex]; ContactedBodyPairs.Add(BoneName1); ContactedBodyPairs.Add(BoneName2); FVector3f Loc1 = FRelativeBodyAnimUtils::CalcVertLocationInUnitBody(ContactPoint1, BoneName1, RefSkeleton, CacheToLocals, CachedBodySourceData.SourceRetargetGlobalPose, PhysicsAsset); FVector3f Loc2 = FRelativeBodyAnimUtils::CalcVertLocationInUnitBody(ContactPoint2, BoneName2, RefSkeleton, CacheToLocals, CachedBodySourceData.SourceRetargetGlobalPose, PhysicsAsset); ContactPoints.Add(Loc1); ContactPoints.Add(Loc2); // Use your custom notify class if (!bUseDenseNotifyInfo) { FName TrackName("<" + SkeletalMeshAsset->GetName() + ">" + BoneName1.ToString() + "-" + BoneName2.ToString()); UE_LOG(LogAnimation, Verbose, TEXT("TrackName %d: '%s'."), GeneratedNotifyTracks.Num()-1, *TrackName.ToString()); const bool bDoesTrackNameAlreadyExist = UAnimationBlueprintLibrary::IsValidAnimNotifyTrackName(InAnimation, TrackName); if (!bDoesTrackNameAlreadyExist) { UAnimationBlueprintLibrary::AddAnimationNotifyTrack(InAnimation, TrackName, FLinearColor::MakeRandomColor()); GeneratedNotifyTracks.Add(TrackName); } URelativeBodyPerFrameAnimNotify* NewNotify = Cast(UAnimationBlueprintLibrary::AddAnimationNotifyEvent(InAnimation, TrackName, SampleTime, NotifyClass)); if (NewNotify) { NewNotify->SetInfo(SkeletalMeshAsset,BoneName1,BoneName2,Loc1,Loc2,IsParentDominates); } } else if (BakedAnimNotifyInfo) { size_t offset = SampleIndex*BakedAnimNotifyInfo->BodyPairs.Num(); BakedAnimNotifyInfo->BodyPairsLocalReference[offset+count*2+0] = Loc1; BakedAnimNotifyInfo->BodyPairsLocalReference[offset+count*2+1] = Loc2; } } count++; } } } } } void URelativeBodyAnimModifier::OnRevert_Implementation(UAnimSequence* InAnimation) { // Delete any generate tracks. for (const FName GeneratedNotifyTrack : GeneratedNotifyTracks) { UAnimationBlueprintLibrary::RemoveAnimationNotifyTrack(InAnimation, GeneratedNotifyTrack); } GeneratedNotifyTracks.Reset(); } UPhysicsAsset* URelativeBodyAnimModifier::GetPhysicsAsset() const { if (PhysicsAssetOverride) { return PhysicsAssetOverride; } if (SkeletalMeshAsset) { return SkeletalMeshAsset->GetPhysicsAsset(); } return nullptr; } void URelativeBodyAnimModifier::CacheBodyDataForSourceMesh(FRelativeBodySourceData& OutSourceData) { const int32 NumVertices = MeshDescription.Vertices().Num(); //Extract skin weights UPhysicsAsset* PhysicsAsset = GetPhysicsAsset(); const FReferenceSkeleton& RefSkeleton = SkeletalMeshAsset->GetRefSkeleton(); FSkeletalMeshAttributes MeshAttribs(MeshDescription); FSkinWeightsVertexAttributesRef VertexSkinWeights = MeshAttribs.GetVertexSkinWeights(); //CPU skinninged vertices const int32 NumBones = MeshAttribs.GetNumBones(); OutSourceData.SourceVLocations.Empty(); OutSourceData.SourceVLocations.AddUninitialized(NumVertices); OutSourceData.BodyIndicesToSourceBoneIndices.Empty(); OutSourceData.SourceBoneIndicesToBodyIndices.Init(INDEX_NONE, NumBones); OutSourceData.SourceRetargetGlobalPose.Empty(); FRelativeBodyAnimUtils::GetRefGlobalPos(OutSourceData.SourceRetargetGlobalPose, RefSkeleton); TArray SourceCacheToLocals; FRelativeBodyAnimUtils::GetRefPoseToLocalMatrices(SourceCacheToLocals, SkeletalMeshAsset, LODIndex, OutSourceData.SourceRetargetGlobalPose); OutSourceData.SourceVLocations.Empty(); OutSourceData.SourceVLocations.AddUninitialized(NumVertices); for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex) { FVertexBoneWeights BoneWeights = VertexSkinWeights.Get(FVertexID(VertexIndex)); const int32 InfluenceCount = BoneWeights.Num(); OutSourceData.SourceVLocations[VertexIndex] = FVector3f(0, 0, 0); int32 VertexMajorBoneIndex = INDEX_NONE; float VertexMajorBoneWeight = 0.; FName VertexMajorBoneName; for (int32 InfluenceIndex = 0; InfluenceIndex < InfluenceCount; ++InfluenceIndex) { int32 InfluenceBoneIndex = BoneWeights[InfluenceIndex].GetBoneIndex(); float InfluenceBoneWeight = BoneWeights[InfluenceIndex].GetWeight(); FName InfluenceBoneName = RefSkeleton.GetBoneName(InfluenceBoneIndex); UE_LOG(LogAnimation, Log, TEXT("Source %d: Bone '%s'[%f]."), VertexIndex, *InfluenceBoneName.ToString(), InfluenceBoneWeight); if (InfluenceBoneWeight > VertexMajorBoneWeight) { VertexMajorBoneIndex = InfluenceBoneIndex; VertexMajorBoneWeight = InfluenceBoneWeight; VertexMajorBoneName = InfluenceBoneName; } // TODO: Is this necessary here? The ref/inv-ref should be identity transform const FMatrix44f RefToLocal = SourceCacheToLocals[InfluenceBoneIndex]; OutSourceData.SourceVLocations[VertexIndex] += RefToLocal.TransformPosition(MeshDescription.GetVertexPosition(VertexIndex)) * InfluenceBoneWeight; } const int32 ParentBodyIndex = PhysicsAsset->FindBodyIndex(VertexMajorBoneName); FName ParentBoneName; if (ParentBodyIndex == INDEX_NONE) ParentBoneName = FRelativeBodyAnimUtils::FindParentBodyBoneName(VertexMajorBoneName, RefSkeleton, PhysicsAsset); else ParentBoneName = PhysicsAsset->SkeletalBodySetups[ParentBodyIndex]->BoneName; const int32 ParentBoneIndex = RefSkeleton.FindBoneIndex(ParentBoneName); if (ParentBoneIndex == INDEX_NONE) continue; if (OutSourceData.SourceBoneIndicesToBodyIndices[ParentBoneIndex] == INDEX_NONE) { OutSourceData.SourceBoneIndicesToBodyIndices[ParentBoneIndex] = OutSourceData.BodyIndicesToSourceBoneIndices.Num(); OutSourceData.BodyIndicesToSourceBoneIndices.Add(ParentBoneIndex); TArray& VertexIndicesInfluenced = OutSourceData.SourceVertexIndicesInfluencedByBodyIndices.AddDefaulted_GetRef(); VertexIndicesInfluenced.Add(VertexIndex); } else { OutSourceData.SourceVertexIndicesInfluencedByBodyIndices[OutSourceData.SourceBoneIndicesToBodyIndices[ParentBoneIndex]].Add(VertexIndex); } } int32 NumBodies = OutSourceData.BodyIndicesToSourceBoneIndices.Num(); OutSourceData.BodyIndicesParentBodyIndices.Empty(); OutSourceData.BodyIndicesParentBodyIndices.SetNum(NumBodies); OutSourceData.BodyIndicesToIgnore.Empty(); OutSourceData.IsDomainBody.Empty(); OutSourceData.BodyIndicesToIgnore.SetNum(NumBodies); OutSourceData.IsDomainBody.SetNum(NumBodies); for (int32 idx = 0; idx < NumBodies; ++idx) { FName BoneName = RefSkeleton.GetBoneName(OutSourceData.BodyIndicesToSourceBoneIndices[idx]); FName ParentBoneName = FRelativeBodyAnimUtils::FindParentBodyBoneName(BoneName, RefSkeleton, PhysicsAsset); int32 BodyIndicesParentBoneIndex = RefSkeleton.FindBoneIndex(ParentBoneName); if (BodyIndicesParentBoneIndex != INDEX_NONE) { OutSourceData.BodyIndicesParentBodyIndices[idx] = OutSourceData.SourceBoneIndicesToBodyIndices[BodyIndicesParentBoneIndex]; } else { OutSourceData.BodyIndicesParentBodyIndices[idx] = INDEX_NONE; } OutSourceData.BodyIndicesToIgnore[idx] = true; OutSourceData.IsDomainBody[idx] = false; UE_LOG(LogAnimation, Log, TEXT("%d: Bone '%s' [%d verts]."), idx, *BoneName.ToString(), OutSourceData.SourceVertexIndicesInfluencedByBodyIndices[idx].Num()); } for (FName BodyName : DomainBodyNames) { int32 BodyIndex = PhysicsAsset->FindBodyIndex(BodyName); if (BodyIndex != INDEX_NONE) { FName BoneName = PhysicsAsset->SkeletalBodySetups[BodyIndex]->BoneName; int32 BoneIndex = RefSkeleton.FindBoneIndex(BoneName); OutSourceData.BodyIndicesToIgnore[OutSourceData.SourceBoneIndicesToBodyIndices[BoneIndex]] = false; OutSourceData.IsDomainBody[OutSourceData.SourceBoneIndicesToBodyIndices[BoneIndex]] = true; } } for (FName BodyName : ContactBodyNames) { int32 BodyIndex = PhysicsAsset->FindBodyIndex(BodyName); if (BodyIndex != INDEX_NONE) { FName BoneName = PhysicsAsset->SkeletalBodySetups[BodyIndex]->BoneName; int32 BoneIndex = RefSkeleton.FindBoneIndex(BoneName); OutSourceData.BodyIndicesToIgnore[OutSourceData.SourceBoneIndicesToBodyIndices[BoneIndex]] = false; } } } void URelativeBodyAnimModifier::GetRefToAnimPoseMatrices(TArray& OutRefToPose, const FAnimPose& AnimPose) const { const FReferenceSkeleton& RefSkeleton = SkeletalMeshAsset->GetRefSkeleton(); const TArray& RefBasesInvMatrix = SkeletalMeshAsset->GetRefBasesInvMatrix(); OutRefToPose.Init(FMatrix44f::Identity, RefBasesInvMatrix.Num()); const FSkeletalMeshLODRenderData& LOD = SkeletalMeshAsset->GetResourceForRendering()->LODRenderData[LODIndex]; const TArray* RequiredBoneSets[3] = { &LOD.ActiveBoneIndices, nullptr, NULL }; for (int32 RequiredBoneSetIndex = 0; RequiredBoneSets[RequiredBoneSetIndex] != NULL; RequiredBoneSetIndex++) { const TArray& RequiredBoneIndices = *RequiredBoneSets[RequiredBoneSetIndex]; for (int32 BoneIndex = 0; BoneIndex < RequiredBoneIndices.Num(); BoneIndex++) { const int32 ThisBoneIndex = RequiredBoneIndices[BoneIndex]; if (RefBasesInvMatrix.IsValidIndex(ThisBoneIndex)) { FName ThisBoneName = RefSkeleton.GetBoneName(ThisBoneIndex); const FTransform ThisBoneTransform = UAnimPoseExtensions::GetBonePose(AnimPose, ThisBoneName, EAnimPoseSpaces::World); OutRefToPose[ThisBoneIndex] = static_cast(ThisBoneTransform.ToMatrixWithScale()); } } } for (int32 ThisBoneIndex = 0; ThisBoneIndex < OutRefToPose.Num(); ++ThisBoneIndex) { OutRefToPose[ThisBoneIndex] = RefBasesInvMatrix[ThisBoneIndex] * OutRefToPose[ThisBoneIndex]; } } void URelativeBodyAnimModifier::GetSkinnedVertices(TArray& VLocations, const TArray& RefToPoseMatrices) { const int32 NumVertices = MeshDescription.Vertices().Num(); VLocations.Empty(); VLocations.AddUninitialized(NumVertices); //Extract skin weights FReferenceSkeleton& RefSkeleton = SkeletalMeshAsset->GetRefSkeleton(); FSkeletalMeshAttributes MeshAttribs(MeshDescription); FSkinWeightsVertexAttributesRef VertexSkinWeights = MeshAttribs.GetVertexSkinWeights(); //CPU skinned vertices const int32 NumBones = MeshAttribs.GetNumBones(); for (int32 VertexIndex = 0; VertexIndex < NumVertices; ++VertexIndex) { FVertexBoneWeights BoneWeights = VertexSkinWeights.Get(FVertexID(VertexIndex)); const int32 InfluenceCount = BoneWeights.Num(); VLocations[VertexIndex] = FVector3f(0, 0, 0); for (int32 InfluenceIndex = 0; InfluenceIndex < InfluenceCount; ++InfluenceIndex) { int32 InfluenceBoneIndex = BoneWeights[InfluenceIndex].GetBoneIndex(); float InfluenceBoneWeight = BoneWeights[InfluenceIndex].GetWeight(); const FMatrix44f& RefToPose = RefToPoseMatrices[InfluenceBoneIndex]; VLocations[VertexIndex] += RefToPose.TransformPosition(MeshDescription.GetVertexPosition(VertexIndex)) * InfluenceBoneWeight; } } } #undef LOCTEXT_NAMESPACE