// Copyright Epic Games, Inc. All Rights Reserved. #include "CompressedAnimationDataNodeBuilder.h" #include "DetailLayoutBuilder.h" #include "DetailWidgetRow.h" #include "Editor.h" #include "IDetailChildrenBuilder.h" #include "IDetailGroup.h" #include "PlatformInfo.h" #include "Animation/AnimSequence.h" #include "Animation/Skeleton.h" #include "Interfaces/ITargetPlatform.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "Styling/AppStyle.h" #include "Widgets/SNullWidget.h" #include "Widgets/SBoxPanel.h" #include "Widgets/Images/SImage.h" #include "Widgets/Images/SThrobber.h" #include "Widgets/Input/SButton.h" #include "Widgets/Input/SComboBox.h" #include "Widgets/Text/STextBlock.h" #define LOCTEXT_NAMESPACE "CompressedAnimationDataNodeBuilder" FCompressedAnimationDataNodeBuilder::FCompressedAnimationDataNodeBuilder(UAnimSequence* InAnimSequence): WeakAnimSequence(InAnimSequence) { CurrentTargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); SelectedPlatformName = CurrentTargetPlatform->CookingDeviceProfileName(); for (const FName& PlatformName : PlatformInfo::GetAllVanillaPlatformNames()) { PlatformsList.Add(MakeShared(PlatformName.ToString())); } for (const PlatformInfo::FTargetPlatformInfo* PlatformInfo : PlatformInfo::GetPlatformInfoArray()) { if(PlatformInfo->PlatformType == EBuildTargetType::Game || PlatformInfo->PlatformType == EBuildTargetType::Program) { continue; } PlatformsList.Add(MakeShared(PlatformInfo->Name.ToString())); } PlatformsList.Sort([](TSharedPtr A, TSharedPtr B) { return *A < *B; }); ensure(PlatformsList.ContainsByPredicate([this](TSharedPtr SharedPlatform) { return *SharedPlatform == CurrentTargetPlatform->IniPlatformName(); })); } void FCompressedAnimationDataNodeBuilder::Tick(float DeltaTime) { if (const UAnimSequence* Sequence = WeakAnimSequence.Get()) { const bool bHasCompressionData = Sequence->HasCompressedDataForPlatform(CurrentTargetPlatform); if (bCachedHasCompressionData != bHasCompressionData ) { bCachedHasCompressionData = bHasCompressionData; OnRegenerateChildren.Execute(); } } } const FSlateBrush* FCompressedAnimationDataNodeBuilder::GetSelectedPlatformBrush() const { const PlatformInfo::FTargetPlatformInfo* PlatformInfo = PlatformInfo::FindPlatformInfo(FName(SelectedPlatformName)); if (PlatformInfo != nullptr) { return FAppStyle::GetBrush(PlatformInfo->GetIconStyleName(EPlatformIconSize::Normal)); } return FStyleDefaults::GetNoBrush(); } TSharedRef FCompressedAnimationDataNodeBuilder::OnGeneratePlatformListWidget(TSharedPtr Platform) { const PlatformInfo::FTargetPlatformInfo* PlatformInfo = PlatformInfo::FindPlatformInfo(FName(*Platform)); if (PlatformInfo != nullptr) { const float Indent = PlatformInfo->IsVanilla() ? 0.f : 16.f; return SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4+Indent) [ SNew(STextBlock) .Text(PlatformInfo->DisplayName) .Font(IDetailLayoutBuilder::GetDetailFont()) ]; } return SNullWidget::NullWidget; } void FCompressedAnimationDataNodeBuilder::OnPlatformSelectionChanged(TSharedPtr Platform, ESelectInfo::Type InSelectInfo) { if (Platform.IsValid()) { SelectedPlatformName = *Platform; } const FName PlatformName(*Platform); const PlatformInfo::FTargetPlatformInfo* PlatformInfo = PlatformInfo::FindPlatformInfo(PlatformName); CurrentTargetPlatform = GetTargetPlatformManagerRef().FindTargetPlatform(PlatformName); check(CurrentTargetPlatform); // Request compressed data if (UAnimSequence* Sequence = WeakAnimSequence.Get()) { Sequence->BeginCacheDerivedData(CurrentTargetPlatform); bCachedHasCompressionData = false; OnRegenerateChildren.Execute(); } } FText FCompressedAnimationDataNodeBuilder::GetSelectedPlatformName() const { return FText::FromString(SelectedPlatformName); } void FCompressedAnimationDataNodeBuilder::GenerateHeaderRowContent(FDetailWidgetRow& NodeRow) { NodeRow.NameContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() [ SNew( SButton ) .ButtonStyle( FAppStyle::Get(), "NoBorder" ) .ContentPadding(FMargin(0,2,0,2)) .OnClicked_Lambda( [this]() { bExpanded = !bExpanded; OnToggleExpansion.ExecuteIfBound(bExpanded); return FReply::Handled(); } ) .ForegroundColor( FSlateColor::UseForeground() ) .Content() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this]() { return LOCTEXT("CompressedAnimationDataLabel", "Compressed Animation Data"); }) ] ] ]; NodeRow.ValueContent() [ SNew(SHorizontalBox) +SHorizontalBox::Slot() [ SNew(SComboBox>) .OptionsSource(&PlatformsList) .InitiallySelectedItem(*PlatformsList.FindByPredicate([this](TSharedPtr SharedPlatform) { return *SharedPlatform == CurrentTargetPlatform->CookingDeviceProfileName();})) .OnGenerateWidget_Static(&FCompressedAnimationDataNodeBuilder::OnGeneratePlatformListWidget) .OnSelectionChanged_Raw(this, &FCompressedAnimationDataNodeBuilder::OnPlatformSelectionChanged) [ SNew(SHorizontalBox) +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4,0) [ SNew(SImage) .DesiredSizeOverride(FVector2D(16,16)) .Image_Raw(this, &FCompressedAnimationDataNodeBuilder::GetSelectedPlatformBrush) ] +SHorizontalBox::Slot() .AutoWidth() .VAlign(VAlign_Center) .Padding(4,0) [ SNew(STextBlock) .Text_Raw(this, &FCompressedAnimationDataNodeBuilder::GetSelectedPlatformName) .Font(IDetailLayoutBuilder::GetDetailFont()) ] ] ] +SHorizontalBox::Slot() .VAlign(VAlign_Center) .Padding(2.0f, 0.0f) .AutoWidth() [ SNew(SThrobber) .Visibility_Raw(this, &FCompressedAnimationDataNodeBuilder::GetCompressionIndicatorVisibility) .ToolTipText(LOCTEXT("CompressionTooltip", "Waiting for compressed data...")) ] ]; } EVisibility FCompressedAnimationDataNodeBuilder::GetCompressionIndicatorVisibility() const { if (const UAnimSequence* Sequence = WeakAnimSequence.Get()) { return Sequence->HasCompressedDataForPlatform(CurrentTargetPlatform) ? EVisibility::Collapsed : EVisibility::Visible; } return EVisibility::Collapsed; } void FCompressedAnimationDataNodeBuilder::GenerateChildContent(IDetailChildrenBuilder& ChildrenBuilder) { auto GetCompressedData = [this]() -> UAnimSequence::FScopedCompressedAnimSequence { const UAnimSequence* Sequence = WeakAnimSequence.Get(); check(Sequence); return Sequence->GetCompressedData(CurrentTargetPlatform); }; TAttribute VisibilityAttribute = TAttribute::Create([this]() -> EVisibility { const UAnimSequence* Sequence = WeakAnimSequence.Get(); check(Sequence); return Sequence->GetCompressedData(CurrentTargetPlatform).Get().IsValid(Sequence) ? EVisibility::Visible : EVisibility::Collapsed; }); FNumberFormattingOptions Options; Options.MaximumFractionalDigits = 3; const FText InvalidText = LOCTEXT("InvalidData", "Invalid"); { ChildrenBuilder.AddCustomRow(LOCTEXT("CompressedDataHash", "Compressed Data Hash")) .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("CompressedDataHash", "Compressed Data Hash")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText]() -> FText { if (const UAnimSequence* AnimSequence = WeakAnimSequence.Get()) { const FIoHash Hash = AnimSequence->GetDerivedDataKeyHash(CurrentTargetPlatform); const FString HashString = LexToString(Hash); return FText::FromString(HashString); } return InvalidText; }) ] .Visibility(VisibilityAttribute); } { ChildrenBuilder.AddCustomRow(LOCTEXT("UncompressedSize", "Uncompressed (source) Data Size")) .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("UncompressedSize", "Uncompressed (source) Data Size")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get())) { return FText::AsMemory(Data.Get().CompressedRawDataSize, &Options); } return InvalidText; }) ] .Visibility(VisibilityAttribute); } { ChildrenBuilder.AddCustomRow(LOCTEXT("CompressedDataSize", "Compressed Data Size")) .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("CompressedSize", "Compressed Data Size")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get())) { return FText::AsMemory((Data.Get().CompressedDataStructure ? Data.Get().CompressedDataStructure->GetApproxCompressedSize() : 0) + Data.Get().CompressedCurveByteStream.Num(), &Options); } return InvalidText; }) ] .Visibility(VisibilityAttribute); } const FName BoneGroupName("BoneGroup"); IDetailGroup& BoneGroup = ChildrenBuilder.AddGroup(BoneGroupName, LOCTEXT("BoneGroupLabel", "Bone Data"), true); { { BoneGroup.AddWidgetRow() .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("CompressedBoneSize", "Compressed Bone Data Size")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get())) { return FText::AsMemory(Data.Get().CompressedDataStructure ? Data.Get().CompressedDataStructure->GetApproxCompressedSize() : 0, &Options); } return InvalidText; }) ] .Visibility(VisibilityAttribute); } { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); const TArray& Tracks = Data.Get().CompressedTrackToSkeletonMapTable; const int32 NumTracks = Tracks.Num(); const UAnimSequence* Sequence = WeakAnimSequence.Get(); const USkeleton* Skeleton = Sequence ? Sequence->GetSkeleton() : nullptr; if (NumTracks && Skeleton) { const FName CompressedTrackNamesGroup("CompressedTrackNamesGroup"); IDetailGroup& CompressedTrackNameGroup = BoneGroup.AddGroup(CompressedTrackNamesGroup, LOCTEXT("CompressedTrackLabel", "Compressed Track Names")); CompressedTrackNameGroup.HeaderRow() .NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("CompressedTrackLabel", "Compressed Track Names")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(FText::Format(LOCTEXT("NumCompressedBonesFormat", "Number of Tracks: {0}"), FText::AsNumber(NumTracks))) ]; for (int32 Index = 0; Index < NumTracks; ++Index) { const int32 SkeletonIndex = Data.Get().GetSkeletonIndexFromTrackIndex(Index); CompressedTrackNameGroup.AddWidgetRow() .WholeRowWidget [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Fill) .Padding(5, 0, 0, 0) .AutoWidth() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(FText::FromName(Skeleton->GetReferenceSkeleton().GetBoneName(SkeletonIndex))) ] ] .Visibility(VisibilityAttribute); } } } { const FName CompressedBoneErrorStats("CompressedBoneErrorStats"); IDetailGroup& CompressedBoneErrorStatGroup = BoneGroup.AddGroup(CompressedBoneErrorStats, LOCTEXT("BoneErrorStatGroupLabel", "Bone Compression Statistics")); CompressedBoneErrorStatGroup.AddWidgetRow() .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("AverageErrorLabel", "Average Error")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get()) && Data.Get().CompressedDataStructure.IsValid()) { return FText::AsNumber(Data.Get().CompressedDataStructure->BoneCompressionErrorStats.AverageError); } return InvalidText; }) ] .Visibility(VisibilityAttribute); CompressedBoneErrorStatGroup.AddWidgetRow() .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("MaximumErrorLabel", "Maximum Error")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get()) && Data.Get().CompressedDataStructure.IsValid()) { return FText::AsNumber(Data.Get().CompressedDataStructure->BoneCompressionErrorStats.MaxError, &Options); } return InvalidText; }) ] .Visibility(VisibilityAttribute); CompressedBoneErrorStatGroup.AddWidgetRow() .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("MaxErrorTimeLabel", "Maximum Error Time-Interval")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get()) && Data.Get().CompressedDataStructure.IsValid()) { return FText::AsNumber(Data.Get().CompressedDataStructure->BoneCompressionErrorStats.MaxErrorTime, &Options); } return InvalidText; }) ] .Visibility(VisibilityAttribute); CompressedBoneErrorStatGroup.AddWidgetRow() .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("MaxErrorBoneLabel", "Maximum Error Bone Name")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get()) && Data.Get().CompressedDataStructure.IsValid()) { FName BoneName = NAME_None; const int32 ErrorBoneIndex = Data.Get().CompressedDataStructure->BoneCompressionErrorStats.MaxErrorBone; if (const UAnimSequence* AnimSequence = WeakAnimSequence.Get()) { if (const USkeleton* Skeleton = AnimSequence->GetSkeleton()) { BoneName = Skeleton->GetReferenceSkeleton().GetBoneName(ErrorBoneIndex); } } return FText::Format(LOCTEXT("MaxErrorBoneNameFormat", "{0}"), FText::FromName(BoneName)); } return InvalidText; }) ] .Visibility(VisibilityAttribute); } } const FName CurveGroupName("CurveGroup"); IDetailGroup& CurveGroup = ChildrenBuilder.AddGroup(BoneGroupName, LOCTEXT("CurveGroupLabel", "Curve Data"), true); { { CurveGroup.AddWidgetRow() .NameWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("CompressedCurveSize", "Compressed Curve Data Size")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text_Lambda([this, GetCompressedData, InvalidText, Options]() -> FText { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); if (Data.Get().IsValid(WeakAnimSequence.Get())) { return FText::AsMemory(Data.Get().CompressedCurveByteStream.Num(), &Options); } return InvalidText; }) ] .Visibility(VisibilityAttribute); } { UAnimSequence::FScopedCompressedAnimSequence Data = GetCompressedData(); const TArray& IndexedNames = Data.Get().IndexedCurveNames; if (IndexedNames.Num()) { const FName CompressedCurveNamesGroup("CompressedCurveNamesGroup"); IDetailGroup& CompressedCurveNameGroup = CurveGroup.AddGroup(CompressedCurveNamesGroup, LOCTEXT("CompressedCurvesLabel", "Compressed Curve Names")); CompressedCurveNameGroup.HeaderRow() .NameContent() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(LOCTEXT("CompressedCurvesLabel", "Compressed Curve Names")) ] .ValueWidget [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(FText::Format(LOCTEXT("NumCompressedCurvesFormat", "Number of Curves: {0}"), FText::AsNumber(IndexedNames.Num()))) ]; const int32 NumCurves = IndexedNames.Num(); for (int32 Index = 0; Index < NumCurves; ++Index) { CompressedCurveNameGroup.AddWidgetRow() .WholeRowWidget [ SNew(SHorizontalBox) + SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Fill) .Padding(5, 0, 0, 0) .AutoWidth() [ SNew(STextBlock) .Font(IDetailLayoutBuilder::GetDetailFont()) .Text(FText::FromName(IndexedNames[Index].CurveName)) ] ] .Visibility(VisibilityAttribute); } } } } } #undef LOCTEXT_NAMESPACE // "CompressedAnimationDataNodeBuilder"