Files
UnrealEngine/Engine/Plugins/Animation/ACLPlugin/Source/ACLPluginEditor/Private/ACLStatsDumpCommandlet.cpp
2025-05-18 13:04:45 +08:00

1532 lines
55 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
// Copyright 2018 Nicholas Frechette. All Rights Reserved.
#include "ACLStatsDumpCommandlet.h"
#include "HAL/FileManagerGeneric.h"
#include "HAL/PlatformTime.h"
#include "HAL/UnrealMemory.h"
#include "Runtime/CoreUObject/Public/UObject/UObjectIterator.h"
#include "Runtime/Engine/Classes/Animation/AnimBoneCompressionSettings.h"
#include "Runtime/Engine/Classes/Animation/AnimCompress.h"
#include "Runtime/Engine/Classes/Animation/AnimCompress_RemoveLinearKeys.h"
#include "Runtime/Engine/Classes/Animation/Skeleton.h"
#include "Runtime/Engine/Public/AnimationCompression.h"
#include "Runtime/Launch/Resources/Version.h"
#include "Editor/UnrealEd/Public/PackageHelperFunctions.h"
#include "Misc/CoreMisc.h"
#include "Interfaces/ITargetPlatform.h"
#include "Interfaces/ITargetPlatformManagerModule.h"
#include "AnimDataController.h"
#include "AnimBoneCompressionCodec_ACL.h"
#include "ACLImpl.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(ACLStatsDumpCommandlet)
THIRD_PARTY_INCLUDES_START
#include <sjson/parser.h>
#include <sjson/writer.h>
#include <acl/compression/impl/track_list_context.h> // For create_output_track_mapping(..)
#include <acl/compression/convert.h>
#include <acl/compression/track_array.h>
#include <acl/compression/transform_error_metrics.h>
#include <acl/compression/track_error.h>
#include <acl/decompression/decompress.h>
#include <acl/io/clip_reader.h>
#include <acl/io/clip_writer.h>
#include <rtm/quatf.h>
#include <rtm/qvvf.h>
#include <rtm/vector4f.h>
THIRD_PARTY_INCLUDES_END
//////////////////////////////////////////////////////////////////////////
// Commandlet example inspired by: https://github.com/ue4plugins/CommandletPlugin
// To run the commandlet, add to the commandline: "$(SolutionDir)$(ProjectName).uproject" -run=/Script/ACLPluginEditor.ACLStatsDump "-input=<path/to/raw/acl/sjson/files/directory>" "-output=<path/to/output/stats/directory>" -compress
//
// Usage:
// -input=<directory>: If present all *acl.sjson files will be used as the input for the commandlet otherwise the current project is used
// -output=<directory>: The commandlet output will be written at the given path (stats or dumped clips)
// -compress: Commandlet will compress the input clips and output stats
// -extract: Commandlet will extract the input clips into output *acl.sjson clips
// -error: Enables the exhaustive error dumping
// -resume: If present, clip extraction or compression will continue where it left off
//
// Codec specific:
// -auto: Uses automatic compression
// -ErrorTolerance=<tolerance>: The error threshold used by automatic compression
//
// -acl: Uses ACL compression
//
// -keyreduction: Use linear key reduction
// -keyreductionrt: Use linear key reduction with retargetting (error compensation)
//////////////////////////////////////////////////////////////////////////
class UESJSONStreamWriter final : public sjson::StreamWriter
{
public:
UESJSONStreamWriter(FArchive* File_)
: File(File_)
{}
virtual void write(const void* Buffer, size_t BufferSize) override
{
File->Serialize(const_cast<void*>(Buffer), BufferSize);
}
private:
FArchive* File;
};
static const TCHAR* ReadACLClip(FFileManagerGeneric& FileManager, const FString& ACLClipPath, acl::iallocator& Allocator, acl::track_array_qvvf& OutTracks)
{
FArchive* Reader = FileManager.CreateFileReader(*ACLClipPath);
const int64 Size = Reader->TotalSize();
// Allocate directly without a TArray to automatically manage the memory because some
// clips are larger than 2 GB
char* RawData = static_cast<char*>(FMemory::Malloc(Size));
Reader->Serialize(RawData, Size);
Reader->Close();
if (ACLClipPath.EndsWith(TEXT(".acl")))
{
acl::compressed_tracks* CompressedTracks = reinterpret_cast<acl::compressed_tracks*>(RawData);
if (Size != CompressedTracks->get_size() || CompressedTracks->is_valid(true).any())
{
FMemory::Free(RawData);
return TEXT("Invalid binary ACL file provided");
}
const acl::error_result Result = acl::convert_track_list(Allocator, *CompressedTracks, OutTracks);
if (Result.any())
{
FMemory::Free(RawData);
return TEXT("Failed to convert input binary track list");
}
}
else
{
acl::clip_reader ClipReader(Allocator, RawData, Size);
if (ClipReader.get_file_type() != acl::sjson_file_type::raw_clip)
{
FMemory::Free(RawData);
return TEXT("SJSON file isn't a raw clip");
}
acl::sjson_raw_clip RawClip;
if (!ClipReader.read_raw_clip(RawClip))
{
FMemory::Free(RawData);
return TEXT("Failed to read ACL raw clip from file");
}
OutTracks = MoveTemp(RawClip.track_list);
}
FMemory::Free(RawData);
return nullptr;
}
static FString GetBoneName(const acl::track_qvvf& Track)
{
// We add a prefix to ensure the name is safe for ControlRig in 5.x
return FString::Printf(TEXT("ACL_%s"), ANSI_TO_TCHAR(Track.get_name().c_str()));
}
static void ConvertSkeleton(const acl::track_array_qvvf& Tracks, USkeleton* UESkeleton)
{
// Not terribly clean, we cast away the 'const' to modify the skeleton
FReferenceSkeleton& RefSkeleton = const_cast<FReferenceSkeleton&>(UESkeleton->GetReferenceSkeleton());
FReferenceSkeletonModifier SkeletonModifier(RefSkeleton, UESkeleton);
for (const acl::track_qvvf& Track : Tracks)
{
const acl::track_desc_transformf& Desc = Track.get_description();
const FString BoneName = GetBoneName(Track);
FMeshBoneInfo UEBone;
UEBone.Name = FName(*BoneName);
UEBone.ParentIndex = Desc.parent_index == acl::k_invalid_track_index ? INDEX_NONE : Desc.parent_index;
UEBone.ExportName = BoneName;
const FTransform BindPose = ACLTransformToUE(Desc.default_value);
SkeletonModifier.Add(UEBone, BindPose);
}
// When our modifier is destroyed here, it will rebuild the skeleton
}
static void ConvertClip(const acl::track_array_qvvf& Tracks, UAnimSequence* UEClip, USkeleton* UESkeleton)
{
UEClip->SetSkeleton(UESkeleton);
const int32 NumSamples = Tracks.get_num_samples_per_track(); // int32 for 5.2 FFrameNumber constructor
const float SequenceLength = FGenericPlatformMath::Max<float>(Tracks.get_finite_duration(), MINIMUM_ANIMATION_LENGTH);
const float SampleRate = Tracks.get_sample_rate();
// This is incorrect because the true sample rate can be fractional but UE doesn't support it
const uint32 FrameRate = FGenericPlatformMath::RoundToInt(SampleRate);
IAnimationDataController& UEClipController = UEClip->GetController();
UEClipController.InitializeModel();
UEClipController.ResetModel(false);
UEClipController.OpenBracket(FText::FromString("Generating Animation Data"));
UEClipController.SetFrameRate(FFrameRate(FrameRate, 1));
const int32 NumFrames = NumSamples - 1;
UEClipController.SetNumberOfFrames(FFrameNumber(NumFrames));
// Ensure our frame rate update propagates first to avoid re-sampling below
UEClipController.NotifyPopulated();
if (NumSamples != 0)
{
const uint32 NumBones = Tracks.get_num_tracks();
for (uint32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
const acl::track_qvvf& Track = Tracks[BoneIndex];
FRawAnimSequenceTrack RawTrack;
RawTrack.PosKeys.Empty();
RawTrack.RotKeys.Empty();
RawTrack.ScaleKeys.Empty();
for (int32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex)
{
const FQuat4f Rotation = ACLQuatToUE(rtm::quat_normalize(Track[SampleIndex].rotation));
RawTrack.RotKeys.Add(Rotation);
}
for (int32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex)
{
const FVector3f Translation = ACLVector3ToUE(Track[SampleIndex].translation);
RawTrack.PosKeys.Add(Translation);
}
for (int32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex)
{
const FVector3f Scale = ACLVector3ToUE(Track[SampleIndex].scale);
RawTrack.ScaleKeys.Add(Scale);
}
const FName BoneName(*GetBoneName(Track));
UEClipController.AddBoneCurve(BoneName);
UEClipController.SetBoneTrackKeys(BoneName, RawTrack.PosKeys, RawTrack.RotKeys, RawTrack.ScaleKeys);
}
}
UEClipController.NotifyPopulated();
UEClipController.CloseBracket();
}
static int32 GetAnimationTrackIndex(const int32 BoneIndex, const UAnimSequence* AnimSeq)
{
if (BoneIndex == INDEX_NONE)
{
return INDEX_NONE;
}
UAnimSequence::FScopedCompressedAnimSequence CompressedAnimSequence = AnimSeq->GetCompressedData();
const TArray<FTrackToSkeletonMap>& TrackToSkelMap = CompressedAnimSequence.Get().CompressedTrackToSkeletonMapTable;
for (int32 TrackIndex = 0; TrackIndex < TrackToSkelMap.Num(); ++TrackIndex)
{
const FTrackToSkeletonMap& TrackToSkeleton = TrackToSkelMap[TrackIndex];
if (TrackToSkeleton.BoneTreeIndex == BoneIndex)
{
return TrackIndex;
}
}
return INDEX_NONE;
}
static void SampleUEClip(const acl::track_array_qvvf& Tracks, USkeleton* UESkeleton, const UAnimSequence* UEClip, float SampleTime, rtm::qvvf* LossyPoseTransforms)
{
const FReferenceSkeleton& RefSkeleton = UESkeleton->GetReferenceSkeleton();
const TArray<FTransform>& RefSkeletonPose = UESkeleton->GetRefLocalPoses();
const FAnimExtractContext Context(static_cast<double>(SampleTime));
const uint32 NumBones = Tracks.get_num_tracks();
for (uint32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
const acl::track_qvvf& Track = Tracks[BoneIndex];
const FName BoneName(*GetBoneName(Track));
const int32 BoneTreeIndex = RefSkeleton.FindBoneIndex(BoneName);
FTransform BoneTransform;
if (BoneTreeIndex != INDEX_NONE)
{
BoneTransform = RefSkeletonPose[BoneTreeIndex];
if (UEClip->GetDataModel()->IsValidBoneTrackName(BoneName))
{
UEClip->GetBoneTransform(BoneTransform, FSkeletonPoseBoneIndex(BoneTreeIndex), Context, false);
}
}
const rtm::quatf Rotation = UEQuatToACL(BoneTransform.GetRotation());
const rtm::vector4f Translation = UEVector3ToACL(BoneTransform.GetTranslation());
const rtm::vector4f Scale = UEVector3ToACL(BoneTransform.GetScale3D());
LossyPoseTransforms[BoneIndex] = rtm::qvv_set(Rotation, Translation, Scale);
}
}
static bool UEClipHasScale(const UAnimSequence* UEClip)
{
TArray<FName> TrackNames;
UEClip->GetDataModel()->GetBoneTrackNames(TrackNames);
bool bHasScaleKeys = false;
for (const FName& TrackName : TrackNames)
{
UEClip->GetDataModel()->IterateBoneKeys(
TrackName,
[&bHasScaleKeys](const FVector3f& Position, const FQuat4f& Rotation, const FVector3f& Scale, const FFrameNumber& FrameNumber)
{
if (!Scale.IsUnit())
{
bHasScaleKeys = true;
return false;
}
return true;
});
if (bHasScaleKeys)
{
break;
}
}
return bHasScaleKeys;
}
struct SimpleTransformWriter final : public acl::track_writer
{
//////////////////////////////////////////////////////////////////////////
// For performance reasons, this writer skips all default sub-tracks.
// It is the responsibility of the caller to pre-populate them by calling initialize_with_defaults().
static constexpr acl::default_sub_track_mode get_default_rotation_mode() { return acl::default_sub_track_mode::skipped; }
static constexpr acl::default_sub_track_mode get_default_translation_mode() { return acl::default_sub_track_mode::skipped; }
static constexpr acl::default_sub_track_mode get_default_scale_mode() { return acl::default_sub_track_mode::skipped; }
explicit SimpleTransformWriter(TArray<rtm::qvvf>& Transforms_) : Transforms(Transforms_) {}
TArray<rtm::qvvf>& Transforms;
//////////////////////////////////////////////////////////////////////////
// Called by the decoder to write out a quaternion rotation value for a specified bone index.
void RTM_SIMD_CALL write_rotation(uint32_t TrackIndex, rtm::quatf_arg0 Rotation)
{
Transforms[TrackIndex].rotation = Rotation;
}
//////////////////////////////////////////////////////////////////////////
// Called by the decoder to write out a translation value for a specified bone index.
void RTM_SIMD_CALL write_translation(uint32_t TrackIndex, rtm::vector4f_arg0 Translation)
{
Transforms[TrackIndex].translation = Translation;
}
//////////////////////////////////////////////////////////////////////////
// Called by the decoder to write out a scale value for a specified bone index.
void RTM_SIMD_CALL write_scale(uint32_t TrackIndex, rtm::vector4f_arg0 Scale)
{
Transforms[TrackIndex].scale = Scale;
}
};
static void CalculateClipError(const acl::track_array_qvvf& Tracks, const UAnimSequence* UEClip, USkeleton* UESkeleton, uint32& OutWorstBone, float& OutMaxError, float& OutWorstSampleTime)
{
// Use the ACL code if we can to calculate the error instead of approximating it with UE.
UAnimSequence::FScopedCompressedAnimSequence CompressedAnimSequence = UEClip->GetCompressedData();
UAnimBoneCompressionCodec_ACLBase* ACLCodec = Cast<UAnimBoneCompressionCodec_ACLBase>(CompressedAnimSequence.Get().BoneCompressionCodec);
if (ACLCodec != nullptr)
{
const acl::compressed_tracks* CompressedClipData = acl::make_compressed_tracks(CompressedAnimSequence.Get().CompressedByteStream.GetData());
const acl::qvvf_transform_error_metric ErrorMetric;
// Use debug settings since we don't know the specific codec used
acl::decompression_context<UEDebugDecompressionSettings> Context;
Context.initialize(*CompressedClipData);
const acl::track_error TrackError = acl::calculate_compression_error(ACLAllocatorImpl, Tracks, Context, ErrorMetric);
OutWorstBone = TrackError.index;
OutMaxError = TrackError.error;
OutWorstSampleTime = TrackError.sample_time;
return;
}
const uint32 NumBones = Tracks.get_num_tracks();
const float ClipDuration = Tracks.get_duration();
const float SampleRate = Tracks.get_sample_rate();
const uint32 NumSamples = Tracks.get_num_samples_per_track();
const bool HasScale = UEClipHasScale(UEClip);
TArray<rtm::qvvf> RawLocalPoseTransforms;
TArray<rtm::qvvf> RawObjectPoseTransforms;
TArray<rtm::qvvf> LossyLocalPoseTransforms;
TArray<rtm::qvvf> LossyObjectPoseTransforms;
RawLocalPoseTransforms.AddUninitialized(NumBones);
RawObjectPoseTransforms.AddUninitialized(NumBones);
LossyLocalPoseTransforms.AddUninitialized(NumBones);
LossyObjectPoseTransforms.AddUninitialized(NumBones);
uint32 WorstBone = acl::k_invalid_track_index;
float MaxError = 0.0f;
float WorstSampleTime = 0.0f;
const acl::qvvf_transform_error_metric ErrorMetric;
SimpleTransformWriter RawWriter(RawLocalPoseTransforms);
TArray<uint32> ParentTransformIndices;
TArray<uint32> SelfTransformIndices;
ParentTransformIndices.AddUninitialized(NumBones);
SelfTransformIndices.AddUninitialized(NumBones);
for (uint32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
const acl::track_qvvf& Track = Tracks[BoneIndex];
const acl::track_desc_transformf& Desc = Track.get_description();
ParentTransformIndices[BoneIndex] = Desc.parent_index;
SelfTransformIndices[BoneIndex] = BoneIndex;
}
acl::itransform_error_metric::local_to_object_space_args local_to_object_space_args_raw;
local_to_object_space_args_raw.dirty_transform_indices = SelfTransformIndices.GetData();
local_to_object_space_args_raw.num_dirty_transforms = NumBones;
local_to_object_space_args_raw.parent_transform_indices = ParentTransformIndices.GetData();
local_to_object_space_args_raw.local_transforms = RawLocalPoseTransforms.GetData();
local_to_object_space_args_raw.num_transforms = NumBones;
acl::itransform_error_metric::local_to_object_space_args local_to_object_space_args_lossy = local_to_object_space_args_raw;
local_to_object_space_args_lossy.local_transforms = LossyLocalPoseTransforms.GetData();
for (uint32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex)
{
// Sample our streams and calculate the error
const float SampleTime = rtm::scalar_min(float(SampleIndex) / SampleRate, ClipDuration);
Tracks.sample_tracks(SampleTime, acl::sample_rounding_policy::none, RawWriter);
SampleUEClip(Tracks, UESkeleton, UEClip, SampleTime, LossyLocalPoseTransforms.GetData());
if (HasScale)
{
ErrorMetric.local_to_object_space(local_to_object_space_args_raw, RawObjectPoseTransforms.GetData());
ErrorMetric.local_to_object_space(local_to_object_space_args_lossy, LossyObjectPoseTransforms.GetData());
}
else
{
ErrorMetric.local_to_object_space_no_scale(local_to_object_space_args_raw, RawObjectPoseTransforms.GetData());
ErrorMetric.local_to_object_space_no_scale(local_to_object_space_args_lossy, LossyObjectPoseTransforms.GetData());
}
for (uint32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
const acl::track_qvvf& Track = Tracks[BoneIndex];
const acl::track_desc_transformf& Desc = Track.get_description();
acl::itransform_error_metric::calculate_error_args calculate_error_args;
calculate_error_args.transform0 = &RawObjectPoseTransforms[BoneIndex];
calculate_error_args.transform1 = &LossyObjectPoseTransforms[BoneIndex];
calculate_error_args.construct_sphere_shell(Desc.shell_distance);
float Error;
if (HasScale)
{
Error = rtm::scalar_cast(ErrorMetric.calculate_error(calculate_error_args));
}
else
{
Error = rtm::scalar_cast(ErrorMetric.calculate_error_no_scale(calculate_error_args));
}
if (Error > MaxError)
{
MaxError = Error;
WorstBone = BoneIndex;
WorstSampleTime = SampleTime;
}
}
}
OutWorstBone = WorstBone;
OutMaxError = MaxError;
OutWorstSampleTime = WorstSampleTime;
}
static void DumpClipDetailedError(const acl::track_array_qvvf& Tracks, const UAnimSequence* UEClip, USkeleton* UESkeleton, sjson::ObjectWriter& Writer)
{
const uint32 NumBones = Tracks.get_num_tracks();
const float ClipDuration = Tracks.get_duration();
const float SampleRate = Tracks.get_sample_rate();
const uint32 NumSamples = Tracks.get_num_samples_per_track();
const bool HasScale = UEClipHasScale(UEClip);
TArray<rtm::qvvf> RawLocalPoseTransforms;
TArray<rtm::qvvf> RawObjectPoseTransforms;
TArray<rtm::qvvf> LossyLocalPoseTransforms;
TArray<rtm::qvvf> LossyObjectPoseTransforms;
RawLocalPoseTransforms.AddUninitialized(NumBones);
RawObjectPoseTransforms.AddUninitialized(NumBones);
LossyLocalPoseTransforms.AddUninitialized(NumBones);
LossyObjectPoseTransforms.AddUninitialized(NumBones);
const acl::qvvf_transform_error_metric ErrorMetric;
SimpleTransformWriter RawWriter(RawLocalPoseTransforms);
TArray<uint32> ParentTransformIndices;
TArray<uint32> SelfTransformIndices;
ParentTransformIndices.AddUninitialized(NumBones);
SelfTransformIndices.AddUninitialized(NumBones);
for (uint32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
const acl::track_qvvf& Track = Tracks[BoneIndex];
const acl::track_desc_transformf& Desc = Track.get_description();
ParentTransformIndices[BoneIndex] = Desc.parent_index;
SelfTransformIndices[BoneIndex] = BoneIndex;
}
acl::itransform_error_metric::local_to_object_space_args local_to_object_space_args_raw;
local_to_object_space_args_raw.dirty_transform_indices = SelfTransformIndices.GetData();
local_to_object_space_args_raw.num_dirty_transforms = NumBones;
local_to_object_space_args_raw.parent_transform_indices = ParentTransformIndices.GetData();
local_to_object_space_args_raw.local_transforms = RawLocalPoseTransforms.GetData();
local_to_object_space_args_raw.num_transforms = NumBones;
acl::itransform_error_metric::local_to_object_space_args local_to_object_space_args_lossy = local_to_object_space_args_raw;
local_to_object_space_args_lossy.local_transforms = LossyLocalPoseTransforms.GetData();
UAnimSequence::FScopedCompressedAnimSequence CompressedAnimSequence = UEClip->GetCompressedData();
// Use the ACL code if we can to calculate the error instead of approximating it with UE.
const UAnimBoneCompressionCodec_ACLBase* ACLCodec = Cast<const UAnimBoneCompressionCodec_ACLBase>(CompressedAnimSequence.Get().BoneCompressionCodec);
if (ACLCodec != nullptr)
{
uint32 NumOutputBones = 0;
uint32* OutputBoneMapping = acl::acl_impl::create_output_track_mapping(ACLAllocatorImpl, Tracks, NumOutputBones);
TArray<rtm::qvvf> LossyRemappedLocalPoseTransforms;
LossyRemappedLocalPoseTransforms.AddUninitialized(NumBones);
local_to_object_space_args_lossy.local_transforms = LossyRemappedLocalPoseTransforms.GetData();
const acl::compressed_tracks* CompressedClipData = acl::make_compressed_tracks(CompressedAnimSequence.Get().CompressedByteStream.GetData());
acl::decompression_context<acl::debug_transform_decompression_settings> Context;
Context.initialize(*CompressedClipData);
SimpleTransformWriter PoseWriter(LossyLocalPoseTransforms);
// Initialize the output pose with our default values (possibly bind pose) since default sub-tracks will be skipped
// to handle stripping
for (const acl::track_qvvf& track : Tracks)
{
const acl::track_desc_transformf& desc = track.get_description();
if (desc.output_index == acl::k_invalid_track_index)
continue; // Stripped, skip it
LossyLocalPoseTransforms[desc.output_index] = desc.default_value;
}
Writer["error_per_frame_and_bone"] = [&](sjson::ArrayWriter& Writer) //-V1047
{
for (uint32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex)
{
// Sample our streams and calculate the error
const float SampleTime = rtm::scalar_min(float(SampleIndex) / SampleRate, ClipDuration);
Tracks.sample_tracks(SampleTime, acl::sample_rounding_policy::none, RawWriter);
Context.seek(SampleTime, acl::sample_rounding_policy::none);
Context.decompress_tracks(PoseWriter);
// Perform remapping by copying the raw pose first and we overwrite with the decompressed pose if
// the data is available
LossyRemappedLocalPoseTransforms = RawLocalPoseTransforms;
for (uint32 OutputIndex = 0; OutputIndex < NumOutputBones; ++OutputIndex)
{
const uint32 BoneIndex = OutputBoneMapping[OutputIndex];
LossyRemappedLocalPoseTransforms[BoneIndex] = LossyLocalPoseTransforms[OutputIndex];
}
if (HasScale)
{
ErrorMetric.local_to_object_space(local_to_object_space_args_raw, RawObjectPoseTransforms.GetData());
ErrorMetric.local_to_object_space(local_to_object_space_args_lossy, LossyObjectPoseTransforms.GetData());
}
else
{
ErrorMetric.local_to_object_space_no_scale(local_to_object_space_args_raw, RawObjectPoseTransforms.GetData());
ErrorMetric.local_to_object_space_no_scale(local_to_object_space_args_lossy, LossyObjectPoseTransforms.GetData());
}
Writer.push_newline();
Writer.push([&](sjson::ArrayWriter& Writer)
{
for (uint32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
const acl::track_qvvf& Track = Tracks[BoneIndex];
const acl::track_desc_transformf& Desc = Track.get_description();
acl::itransform_error_metric::calculate_error_args calculate_error_args;
calculate_error_args.transform0 = &RawObjectPoseTransforms[BoneIndex];
calculate_error_args.transform1 = &LossyObjectPoseTransforms[BoneIndex];
calculate_error_args.construct_sphere_shell(Desc.shell_distance);
float Error;
if (HasScale)
Error = rtm::scalar_cast(ErrorMetric.calculate_error(calculate_error_args));
else
Error = rtm::scalar_cast(ErrorMetric.calculate_error_no_scale(calculate_error_args));
Writer.push(Error);
}
});
}
};
acl::deallocate_type_array(ACLAllocatorImpl, OutputBoneMapping, NumOutputBones);
return;
}
Writer["error_per_frame_and_bone"] = [&](sjson::ArrayWriter& Writer) //-V1047
{
for (uint32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex)
{
// Sample our streams and calculate the error
const float SampleTime = rtm::scalar_min(float(SampleIndex) / SampleRate, ClipDuration);
Tracks.sample_tracks(SampleTime, acl::sample_rounding_policy::none, RawWriter);
SampleUEClip(Tracks, UESkeleton, UEClip, SampleTime, LossyLocalPoseTransforms.GetData());
if (HasScale)
{
ErrorMetric.local_to_object_space(local_to_object_space_args_raw, RawObjectPoseTransforms.GetData());
ErrorMetric.local_to_object_space(local_to_object_space_args_lossy, LossyObjectPoseTransforms.GetData());
}
else
{
ErrorMetric.local_to_object_space_no_scale(local_to_object_space_args_raw, RawObjectPoseTransforms.GetData());
ErrorMetric.local_to_object_space_no_scale(local_to_object_space_args_lossy, LossyObjectPoseTransforms.GetData());
}
Writer.push_newline();
Writer.push([&](sjson::ArrayWriter& Writer)
{
for (uint32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
{
const acl::track_qvvf& Track = Tracks[BoneIndex];
const acl::track_desc_transformf& Desc = Track.get_description();
acl::itransform_error_metric::calculate_error_args calculate_error_args;
calculate_error_args.transform0 = &RawObjectPoseTransforms[BoneIndex];
calculate_error_args.transform1 = &LossyObjectPoseTransforms[BoneIndex];
calculate_error_args.construct_sphere_shell(Desc.shell_distance);
float Error;
if (HasScale)
Error = rtm::scalar_cast(ErrorMetric.calculate_error(calculate_error_args));
else
Error = rtm::scalar_cast(ErrorMetric.calculate_error_no_scale(calculate_error_args));
Writer.push(Error);
}
});
}
};
}
struct FCompressionContext
{
UAnimBoneCompressionSettings* AutoCompressor;
UAnimBoneCompressionSettings* ACLCompressor;
UAnimBoneCompressionSettings* KeyReductionCompressor;
UAnimSequence* UEClip;
USkeleton* UESkeleton;
acl::track_array_qvvf ACLTracks;
uint32 ACLRawSize;
int32 UERawSize;
};
static FString GetCodecName(UAnimBoneCompressionCodec* Codec)
{
if (Codec == nullptr)
{
return TEXT("<null>");
}
if (Codec->Description.Len() > 0 && Codec->Description != TEXT("None"))
{
return Codec->Description;
}
return Codec->GetClass()->GetName();
}
static void CompressWithUEAuto(FCompressionContext& Context, bool PerformExhaustiveDump, sjson::Writer& Writer)
{
// Force recompression and avoid the DDC
TGuardValue<int32> CompressGuard(Context.UEClip->CompressCommandletVersion, INDEX_NONE);
const uint64 UEStartTimeCycles = FPlatformTime::Cycles64();
Context.UEClip->BoneCompressionSettings = Context.AutoCompressor;
Context.UEClip->CacheDerivedDataForCurrentPlatform();
const uint64 UEEndTimeCycles = FPlatformTime::Cycles64();
const uint64 UEElapsedCycles = UEEndTimeCycles - UEStartTimeCycles;
const double UEElapsedTimeSec = FPlatformTime::ToSeconds64(UEElapsedCycles);
if (Context.UEClip->IsBoneCompressedDataValid())
{
const UAnimSequence* ConstClip = Context.UEClip;
UAnimSequence::FScopedCompressedAnimSequence CompressedAnimSequence = ConstClip->GetCompressedData();
const bool bHasClipData = CompressedAnimSequence.Get().CompressedDataStructure != nullptr;
AnimationErrorStats UEErrorStats;
uint32 WorstBone = INDEX_NONE;
float MaxError = 0.0f;
float WorstSampleTime = 0.0f;
if (bHasClipData)
{
UEErrorStats = CompressedAnimSequence.Get().CompressedDataStructure->BoneCompressionErrorStats;
CalculateClipError(Context.ACLTracks, Context.UEClip, Context.UESkeleton, WorstBone, MaxError, WorstSampleTime);
}
const int32 CompressedSize = Context.UEClip->GetApproxCompressedSize();
const double UECompressionRatio = double(Context.UERawSize) / double(CompressedSize);
const double ACLCompressionRatio = double(Context.ACLRawSize) / double(CompressedSize);
Writer["ue4_auto"] = [&](sjson::ObjectWriter& Writer) //-V1047
{
Writer["algorithm_name"] = TCHAR_TO_ANSI(*Context.UEClip->BoneCompressionSettings->GetClass()->GetName());
Writer["codec_name"] = TCHAR_TO_ANSI(*GetCodecName(CompressedAnimSequence.Get().BoneCompressionCodec));
Writer["compressed_size"] = CompressedSize;
Writer["ue4_compression_ratio"] = UECompressionRatio;
Writer["acl_compression_ratio"] = ACLCompressionRatio;
Writer["compression_time"] = UEElapsedTimeSec;
Writer["ue4_max_error"] = UEErrorStats.MaxError;
Writer["ue4_avg_error"] = UEErrorStats.AverageError;
Writer["ue4_worst_bone"] = UEErrorStats.MaxErrorBone;
Writer["ue4_worst_time"] = UEErrorStats.MaxErrorTime;
Writer["acl_max_error"] = MaxError;
Writer["acl_worst_bone"] = WorstBone;
Writer["acl_worst_time"] = WorstSampleTime;
if (CompressedAnimSequence.Get().BoneCompressionCodec != nullptr
&& CompressedAnimSequence.Get().BoneCompressionCodec->IsA<UAnimCompress>()
&& bHasClipData)
{
const FUECompressedAnimData& AnimData = static_cast<FUECompressedAnimData&>(*CompressedAnimSequence.Get().CompressedDataStructure);
Writer["rotation_format"] = TCHAR_TO_ANSI(*FAnimationUtils::GetAnimationCompressionFormatString(AnimData.RotationCompressionFormat));
Writer["translation_format"] = TCHAR_TO_ANSI(*FAnimationUtils::GetAnimationCompressionFormatString(AnimData.TranslationCompressionFormat));
Writer["scale_format"] = TCHAR_TO_ANSI(*FAnimationUtils::GetAnimationCompressionFormatString(AnimData.ScaleCompressionFormat));
}
if (PerformExhaustiveDump && bHasClipData)
{
DumpClipDetailedError(Context.ACLTracks, Context.UEClip, Context.UESkeleton, Writer);
}
};
}
else
{
Writer["error"] = "failed to compress UE clip";
}
}
static void CompressWithACL(FCompressionContext& Context, bool PerformExhaustiveDump, sjson::Writer& Writer)
{
// Force recompression and avoid the DDC
TGuardValue<int32> CompressGuard(Context.UEClip->CompressCommandletVersion, INDEX_NONE);
const uint64 ACLStartTimeCycles = FPlatformTime::Cycles64();
Context.UEClip->BoneCompressionSettings = Context.ACLCompressor;
Context.UEClip->CacheDerivedDataForCurrentPlatform();
const uint64 ACLEndTimeCycles = FPlatformTime::Cycles64();
const uint64 ACLElapsedCycles = ACLEndTimeCycles - ACLStartTimeCycles;
const double ACLElapsedTimeSec = FPlatformTime::ToSeconds64(ACLElapsedCycles);
if (Context.UEClip->IsBoneCompressedDataValid())
{
const UAnimSequence* ConstClip = Context.UEClip;
UAnimSequence::FScopedCompressedAnimSequence CompressedAnimSequence = ConstClip->GetCompressedData();
const bool bHasClipData = CompressedAnimSequence.Get().CompressedDataStructure != nullptr;
AnimationErrorStats UEErrorStats;
uint32 WorstBone = INDEX_NONE;
float MaxError = 0.0f;
float WorstSampleTime = 0.0f;
if (bHasClipData)
{
UEErrorStats = CompressedAnimSequence.Get().CompressedDataStructure->BoneCompressionErrorStats;
CalculateClipError(Context.ACLTracks, Context.UEClip, Context.UESkeleton, WorstBone, MaxError, WorstSampleTime);
}
const int32 CompressedSize = Context.UEClip->GetApproxCompressedSize();
const double UECompressionRatio = double(Context.UERawSize) / double(CompressedSize);
const double ACLCompressionRatio = double(Context.ACLRawSize) / double(CompressedSize);
Writer["ue4_acl"] = [&](sjson::ObjectWriter& Writer) //-V1047
{
Writer["algorithm_name"] = TCHAR_TO_ANSI(*Context.UEClip->BoneCompressionSettings->GetClass()->GetName());
Writer["codec_name"] = TCHAR_TO_ANSI(*GetCodecName(CompressedAnimSequence.Get().BoneCompressionCodec));
Writer["compressed_size"] = CompressedSize;
Writer["ue4_compression_ratio"] = UECompressionRatio;
Writer["acl_compression_ratio"] = ACLCompressionRatio;
Writer["compression_time"] = ACLElapsedTimeSec;
Writer["ue4_max_error"] = UEErrorStats.MaxError;
Writer["ue4_avg_error"] = UEErrorStats.AverageError;
Writer["ue4_worst_bone"] = UEErrorStats.MaxErrorBone;
Writer["ue4_worst_time"] = UEErrorStats.MaxErrorTime;
Writer["acl_max_error"] = MaxError;
Writer["acl_worst_bone"] = WorstBone;
Writer["acl_worst_time"] = WorstSampleTime;
if (PerformExhaustiveDump && bHasClipData)
{
DumpClipDetailedError(Context.ACLTracks, Context.UEClip, Context.UESkeleton, Writer);
}
};
}
else
{
Writer["error"] = "failed to compress UE clip";
}
}
static bool IsKeyDropped(int32 NumFrames, const uint8* FrameTable, int32 NumKeys, float FrameRate, float SampleTime)
{
if (NumFrames > 0xFF)
{
const uint16* Frames = (const uint16*)FrameTable;
for (int32 KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex)
{
const float FrameTime = Frames[KeyIndex] / FrameRate;
if (FMath::IsNearlyEqual(FrameTime, SampleTime, 0.001f))
{
return false;
}
}
return true;
}
else
{
const uint8* Frames = (const uint8*)FrameTable;
for (int32 KeyIndex = 0; KeyIndex < NumKeys; ++KeyIndex)
{
const float FrameTime = Frames[KeyIndex] / FrameRate;
if (FMath::IsNearlyEqual(FrameTime, SampleTime, 0.001f))
{
return false;
}
}
return true;
}
}
static int32 GetCompressedNumberOfKeys(const FUECompressedAnimData& AnimData)
{
return AnimData.CompressedNumberOfKeys;
}
static void CompressWithUEKeyReduction(FCompressionContext& Context, bool PerformExhaustiveDump, sjson::Writer& Writer)
{
using AnimDataModelType = IAnimationDataModel;
// Force recompression and avoid the DDC
TGuardValue<int32> CompressGuard(Context.UEClip->CompressCommandletVersion, INDEX_NONE);
const uint64 UEStartTimeCycles = FPlatformTime::Cycles64();
Context.UEClip->BoneCompressionSettings = Context.KeyReductionCompressor;
Context.UEClip->CacheDerivedDataForCurrentPlatform();
const uint64 UEEndTimeCycles = FPlatformTime::Cycles64();
const uint64 UEElapsedCycles = UEEndTimeCycles - UEStartTimeCycles;
const double UEElapsedTimeSec = FPlatformTime::ToSeconds64(UEElapsedCycles);
if (Context.UEClip->IsBoneCompressedDataValid())
{
const UAnimSequence* ConstClip = Context.UEClip;
UAnimSequence::FScopedCompressedAnimSequence CompressedAnimSequence = ConstClip->GetCompressedData();
const bool bHasClipData = CompressedAnimSequence.Get().CompressedDataStructure != nullptr;
AnimationErrorStats UEErrorStats;
uint32 WorstBone = INDEX_NONE;
float MaxError = 0.0f;
float WorstSampleTime = 0.0f;
if (bHasClipData)
{
UEErrorStats = CompressedAnimSequence.Get().CompressedDataStructure->BoneCompressionErrorStats;
CalculateClipError(Context.ACLTracks, Context.UEClip, Context.UESkeleton, WorstBone, MaxError, WorstSampleTime);
}
const int32 CompressedSize = Context.UEClip->GetApproxCompressedSize();
const double UECompressionRatio = double(Context.UERawSize) / double(CompressedSize);
const double ACLCompressionRatio = double(Context.ACLRawSize) / double(CompressedSize);
Writer["ue4_keyreduction"] = [&](sjson::ObjectWriter& Writer) //-V1047
{
Writer["algorithm_name"] = TCHAR_TO_ANSI(*Context.UEClip->BoneCompressionSettings->GetClass()->GetName());
Writer["codec_name"] = TCHAR_TO_ANSI(*GetCodecName(CompressedAnimSequence.Get().BoneCompressionCodec));
Writer["compressed_size"] = CompressedSize;
Writer["ue4_compression_ratio"] = UECompressionRatio;
Writer["acl_compression_ratio"] = ACLCompressionRatio;
Writer["compression_time"] = UEElapsedTimeSec;
Writer["ue4_max_error"] = UEErrorStats.MaxError;
Writer["ue4_avg_error"] = UEErrorStats.AverageError;
Writer["ue4_worst_bone"] = UEErrorStats.MaxErrorBone;
Writer["ue4_worst_time"] = UEErrorStats.MaxErrorTime;
Writer["acl_max_error"] = MaxError;
Writer["acl_worst_bone"] = WorstBone;
Writer["acl_worst_time"] = WorstSampleTime;
if (PerformExhaustiveDump && bHasClipData)
{
DumpClipDetailedError(Context.ACLTracks, Context.UEClip, Context.UESkeleton, Writer);
}
// Number of animated keys before any key reduction for animated tracks (without constant/default tracks)
int32 TotalNumAnimatedKeys = 0;
// Number of animated keys dropped after key reduction for animated tracks (without constant/default tracks)
int32 TotalNumDroppedAnimatedKeys = 0;
// Number of animated tracks (not constant/default)
int32 NumAnimatedTracks = 0;
Writer["dropped_track_keys"] = [&](sjson::ArrayWriter& Writer) //-V1047
{
if (!bHasClipData)
{
return; // No data, nothing to append
}
const FUECompressedAnimData& AnimData = static_cast<FUECompressedAnimData&>(*CompressedAnimSequence.Get().CompressedDataStructure);
const AnimDataModelType* ClipData = Context.UEClip->GetDataModel();
const int32 NumTracks = ClipData->GetNumBoneTracks();
const int32 NumSamples = ClipData->GetNumberOfFrames();
const int32* TrackOffsets = AnimData.CompressedTrackOffsets.GetData();
const auto& ScaleOffsets = AnimData.CompressedScaleOffsets;
const AnimationCompressionFormat RotationFormat = AnimData.RotationCompressionFormat;
const AnimationCompressionFormat TranslationFormat = AnimData.TranslationCompressionFormat;
const AnimationCompressionFormat ScaleFormat = AnimData.ScaleCompressionFormat;
// offset past Min and Range data
const int32 RotationStreamOffset = (RotationFormat == ACF_IntervalFixed32NoW) ? (sizeof(float) * 6) : 0;
const int32 TranslationStreamOffset = (TranslationFormat == ACF_IntervalFixed32NoW) ? (sizeof(float) * 6) : 0;
const int32 ScaleStreamOffset = (ScaleFormat == ACF_IntervalFixed32NoW) ? (sizeof(float) * 6) : 0;
for (int32 TrackIndex = 0; TrackIndex < NumTracks; ++TrackIndex)
{
const int32* TrackData = TrackOffsets + (TrackIndex * 4);
const int32 NumTransKeys = TrackData[1];
// Skip constant/default tracks
if (NumTransKeys > 1)
{
const int32 DroppedTransCount = NumSamples - NumTransKeys;
const float DroppedRatio = float(DroppedTransCount) / float(NumSamples);
Writer.push(DroppedRatio);
TotalNumAnimatedKeys += NumSamples;
TotalNumDroppedAnimatedKeys += DroppedTransCount;
NumAnimatedTracks++;
}
const int32 NumRotKeys = TrackData[3];
// Skip constant/default tracks
if (NumRotKeys > 1)
{
const int32 DroppedRotCount = NumSamples - NumRotKeys;
const float DroppedRatio = float(DroppedRotCount) / float(NumSamples);
Writer.push(DroppedRatio);
TotalNumAnimatedKeys += NumSamples;
TotalNumDroppedAnimatedKeys += DroppedRotCount;
NumAnimatedTracks++;
}
if (ScaleOffsets.IsValid())
{
const int32 NumScaleKeys = ScaleOffsets.GetOffsetData(TrackIndex, 1);
// Skip constant/default tracks
if (NumScaleKeys > 1)
{
const int32 DroppedScaleCount = NumSamples - NumScaleKeys;
const float DroppedRatio = float(DroppedScaleCount) / float(NumSamples);
Writer.push(DroppedRatio);
TotalNumAnimatedKeys += NumSamples;
TotalNumDroppedAnimatedKeys += DroppedScaleCount;
NumAnimatedTracks++;
}
}
}
};
Writer["total_num_animated_keys"] = TotalNumAnimatedKeys;
Writer["total_num_dropped_animated_keys"] = TotalNumDroppedAnimatedKeys;
Writer["dropped_pose_keys"] = [&](sjson::ArrayWriter& Writer) //-V1047
{
if (!bHasClipData)
{
return; // No data, nothing to append
}
const FUECompressedAnimData& AnimData = static_cast<FUECompressedAnimData&>(*CompressedAnimSequence.Get().CompressedDataStructure);
const AnimDataModelType* ClipData = Context.UEClip->GetDataModel();
const int32 NumTracks = ClipData->GetNumBoneTracks();
const int32 NumSamples = ClipData->GetNumberOfFrames();
const float SequenceLength = GetSequenceLength(*Context.UEClip);
const int32 NumCompressedKeys = GetCompressedNumberOfKeys(AnimData);
const float FrameRate = (NumSamples - 1) / SequenceLength;
const uint8* ByteStream = AnimData.CompressedByteStream.GetData();
const int32* TrackOffsets = AnimData.CompressedTrackOffsets.GetData();
const auto& ScaleOffsets = AnimData.CompressedScaleOffsets;
const AnimationCompressionFormat RotationFormat = AnimData.RotationCompressionFormat;
const AnimationCompressionFormat TranslationFormat = AnimData.TranslationCompressionFormat;
const AnimationCompressionFormat ScaleFormat = AnimData.ScaleCompressionFormat;
// offset past Min and Range data
const int32 RotationStreamOffset = (RotationFormat == ACF_IntervalFixed32NoW) ? (sizeof(float) * 6) : 0;
const int32 TranslationStreamOffset = (TranslationFormat == ACF_IntervalFixed32NoW) ? (sizeof(float) * 6) : 0;
const int32 ScaleStreamOffset = (ScaleFormat == ACF_IntervalFixed32NoW) ? (sizeof(float) * 6) : 0;
for (int32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex)
{
const float SampleTime = float(SampleIndex) / FrameRate;
int32 DroppedRotCount = 0;
int32 DroppedTransCount = 0;
int32 DroppedScaleCount = 0;
for (int32 TrackIndex = 0; TrackIndex < NumTracks; ++TrackIndex)
{
const int32* TrackData = TrackOffsets + (TrackIndex * 4);
const int32 TransKeysOffset = TrackData[0];
const int32 NumTransKeys = TrackData[1];
const uint8* TransStream = ByteStream + TransKeysOffset;
const uint8* TransFrameTable = TransStream + TranslationStreamOffset + (NumTransKeys * CompressedTranslationStrides[TranslationFormat] * CompressedTranslationNum[TranslationFormat]);
TransFrameTable = Align(TransFrameTable, 4);
// Skip constant/default tracks
if (NumTransKeys > 1 && IsKeyDropped(NumCompressedKeys, TransFrameTable, NumTransKeys, FrameRate, SampleTime))
{
DroppedTransCount++;
}
const int32 RotKeysOffset = TrackData[2];
const int32 NumRotKeys = TrackData[3];
const uint8* RotStream = ByteStream + RotKeysOffset;
const uint8* RotFrameTable = RotStream + RotationStreamOffset + (NumRotKeys * CompressedRotationStrides[RotationFormat] * CompressedRotationNum[RotationFormat]);
RotFrameTable = Align(RotFrameTable, 4);
// Skip constant/default tracks
if (NumRotKeys > 1 && IsKeyDropped(NumCompressedKeys, RotFrameTable, NumRotKeys, FrameRate, SampleTime))
{
DroppedRotCount++;
}
if (ScaleOffsets.IsValid())
{
const int32 ScaleKeysOffset = ScaleOffsets.GetOffsetData(TrackIndex, 0);
const int32 NumScaleKeys = ScaleOffsets.GetOffsetData(TrackIndex, 1);
const uint8* ScaleStream = ByteStream + ScaleKeysOffset;
const uint8* ScaleFrameTable = ScaleStream + ScaleStreamOffset + (NumScaleKeys * CompressedScaleStrides[ScaleFormat] * CompressedScaleNum[ScaleFormat]);
ScaleFrameTable = Align(ScaleFrameTable, 4);
// Skip constant/default tracks
if (NumScaleKeys > 1 && IsKeyDropped(NumCompressedKeys, ScaleFrameTable, NumScaleKeys, FrameRate, SampleTime))
{
DroppedScaleCount++;
}
}
}
const int32 TotalDroppedCount = DroppedRotCount + DroppedTransCount + DroppedScaleCount;
const float DropRatio = NumAnimatedTracks != 0 ? (float(TotalDroppedCount) / float(NumAnimatedTracks)) : 1.0f;
Writer.push(DropRatio);
}
};
#if DO_CHECK && 0
{
// Double check our count
const int32 NumSamples = Context.UEClip->GetRawNumberOfFrames();
const TArray<FRawAnimSequenceTrack>& RawTracks = Context.UEClip->GetRawAnimationData();
const int32 NumTracks = RawTracks.Num();
const int32* TrackOffsets = Context.UEClip->CompressedTrackOffsets.GetData();
const FCompressedOffsetData& ScaleOffsets = Context.UEClip->CompressedScaleOffsets;
int32 DroppedRotCount = 0;
int32 DroppedTransCount = 0;
int32 DroppedScaleCount = 0;
for (int32 TrackIndex = 0; TrackIndex < NumTracks; ++TrackIndex)
{
const int32* TrackData = TrackOffsets + (TrackIndex * 4);
const int32 NumTransKeys = TrackData[1];
// Skip constant/default tracks
if (NumTransKeys > 1)
{
DroppedTransCount += NumSamples - NumTransKeys;
}
const int32 NumRotKeys = TrackData[3];
// Skip constant/default tracks
if (NumRotKeys > 1)
{
DroppedRotCount += NumSamples - NumRotKeys;
}
if (ScaleOffsets.IsValid())
{
const int32 NumScaleKeys = ScaleOffsets.GetOffsetData(TrackIndex, 1);
// Skip constant/default tracks
if (NumScaleKeys > 1)
{
DroppedScaleCount += NumSamples - NumScaleKeys;
}
}
}
check(TotalNumDroppedKeys == (DroppedRotCount + DroppedTransCount + DroppedScaleCount));
}
#endif
};
}
else
{
Writer["error"] = "failed to compress UE clip";
}
}
static void ClearClip(UAnimSequence* UEClip)
{
UEClip->ResetAnimation();
}
struct CompressAnimationsFunctor
{
template<typename ObjectType>
void DoIt(UCommandlet* Commandlet, UPackage* Package, const TArray<FString>& Tokens, const TArray<FString>& Switches)
{
TArray<UAnimSequence*> AnimSequences;
for (TObjectIterator<UAnimSequence> It; It; ++It)
{
UAnimSequence* AnimSeq = *It;
if (AnimSeq->IsIn(Package))
{
AnimSequences.Add(AnimSeq);
}
}
// Skip packages that contain no Animations.
const int32 NumAnimSequences = AnimSequences.Num();
if (NumAnimSequences == 0)
{
return;
}
UACLStatsDumpCommandlet* StatsCommandlet = Cast<UACLStatsDumpCommandlet>(Commandlet);
FFileManagerGeneric FileManager;
for (int32 SequenceIndex = 0; SequenceIndex < NumAnimSequences; ++SequenceIndex)
{
UAnimSequence* UEClip = AnimSequences[SequenceIndex];
// Make sure all our required dependencies are loaded
FAnimationUtils::EnsureAnimSequenceLoaded(*UEClip);
USkeleton* UESkeleton = UEClip->GetSkeleton();
if (UESkeleton == nullptr)
{
continue;
}
FString Filename = UEClip->GetPathName();
if (StatsCommandlet->PerformCompression)
{
Filename = FString::Printf(TEXT("%X_stats.sjson"), GetTypeHash(Filename));
}
else if (StatsCommandlet->PerformClipExtraction)
{
Filename = FString::Printf(TEXT("%X.acl.sjson"), GetTypeHash(Filename));
}
FString UEOutputPath = FPaths::Combine(*StatsCommandlet->OutputDir, *Filename).Replace(TEXT("/"), TEXT("\\"));
if (StatsCommandlet->ResumeTask && FileManager.FileExists(*UEOutputPath))
{
continue;
}
const bool bIsAdditive = UEClip->IsValidAdditive();
if (bIsAdditive && StatsCommandlet->SkipAdditiveClips)
{
continue;
}
FCompressionContext Context;
Context.AutoCompressor = StatsCommandlet->AutoCompressionSettings;
Context.ACLCompressor = StatsCommandlet->ACLCompressionSettings;
Context.UEClip = UEClip;
Context.UESkeleton = UESkeleton;
FCompressibleAnimData CompressibleData(UEClip, false, GetTargetPlatformManagerRef().GetRunningTargetPlatform());
acl::track_array_qvvf ACLTracks = BuildACLTransformTrackArray(ACLAllocatorImpl, CompressibleData,
StatsCommandlet->ACLCodec->DefaultVirtualVertexDistance, StatsCommandlet->ACLCodec->SafeVirtualVertexDistance,
false, ACLPhantomTrackMode::Ignore);
// TODO: Add support for additive clips
//acl::track_array_qvvf ACLBaseTracks;
//if (CompressibleData.bIsValidAdditive)
//ACLBaseTracks = BuildACLTransformTrackArray(Allocator, CompressibleData, StatsCommandlet->ACLCodec->DefaultVirtualVertexDistance, StatsCommandlet->ACLCodec->SafeVirtualVertexDistance, true);
Context.ACLTracks = MoveTemp(ACLTracks);
Context.ACLRawSize = Context.ACLTracks.get_raw_size();
Context.UERawSize = UEClip->GetApproxRawSize();
if (StatsCommandlet->PerformCompression)
{
UE_LOG(LogAnimationCompression, Verbose, TEXT("Compressing: %s (%d / %d)"), *UEClip->GetPathName(), SequenceIndex, NumAnimSequences);
FArchive* OutputWriter = FileManager.CreateFileWriter(*UEOutputPath);
if (OutputWriter == nullptr)
{
// Opening the file handle can fail if the file path is too long on Windows. UE does not properly handle long paths
// and adding the \\?\ prefix manually doesn't work, UE mangles it when it normalizes the path.
ClearClip(UEClip);
continue;
}
// Make sure any pending async compression that might have started during load or construction is done
UEClip->WaitOnExistingCompression();
UESJSONStreamWriter StreamWriter(OutputWriter);
sjson::Writer Writer(StreamWriter);
Writer["duration"] = GetSequenceLength(*UEClip);
Writer["num_samples"] = GetNumSamples(CompressibleData);
Writer["ue4_raw_size"] = Context.UERawSize;
Writer["acl_raw_size"] = Context.ACLRawSize;
if (StatsCommandlet->TryAutomaticCompression)
{
CompressWithUEAuto(Context, StatsCommandlet->PerformExhaustiveDump, Writer);
}
if (StatsCommandlet->TryACLCompression)
{
CompressWithACL(Context, StatsCommandlet->PerformExhaustiveDump, Writer);
}
if (StatsCommandlet->TryKeyReduction)
{
CompressWithUEKeyReduction(Context, StatsCommandlet->PerformExhaustiveDump, Writer);
}
OutputWriter->Close();
}
else if (StatsCommandlet->PerformClipExtraction)
{
UE_LOG(LogAnimationCompression, Verbose, TEXT("Extracting: %s (%d / %d)"), *UEClip->GetPathName(), SequenceIndex, NumAnimSequences);
const ITargetPlatform* TargetPlatform = GetTargetPlatformManager()->GetRunningTargetPlatform();
acl::compression_settings Settings;
StatsCommandlet->ACLCodec->GetCompressionSettings(TargetPlatform, Settings);
const acl::error_result Error = acl::write_track_list(Context.ACLTracks, Settings, TCHAR_TO_ANSI(*UEOutputPath));
if (Error.any())
{
UE_LOG(LogAnimationCompression, Warning, TEXT("Failed to write ACL clip file: %s"), ANSI_TO_TCHAR(Error.c_str()));
}
}
ClearClip(UEClip);
}
}
};
UACLStatsDumpCommandlet::UACLStatsDumpCommandlet(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
ShowErrorCount = true;
}
static void ClearCompressedData(UAnimSequence* UEClip)
{
UEClip->ClearAllCachedCookedPlatformData();
}
int32 UACLStatsDumpCommandlet::Main(const FString& Params)
{
TArray<FString> Tokens;
TArray<FString> Switches;
TMap<FString, FString> ParamsMap;
UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
if (!ParamsMap.Contains(TEXT("output")))
{
UE_LOG(LogAnimationCompression, Error, TEXT("Missing commandlet argument: -output=<path/to/output/directory>"));
return 0;
}
OutputDir = ParamsMap[TEXT("output")];
PerformExhaustiveDump = Switches.Contains(TEXT("error"));
PerformCompression = Switches.Contains(TEXT("compress"));
PerformClipExtraction = Switches.Contains(TEXT("extract"));
TryAutomaticCompression = Switches.Contains(TEXT("auto"));
TryACLCompression = Switches.Contains(TEXT("acl"));
TryKeyReductionRetarget = Switches.Contains(TEXT("keyreductionrt"));
TryKeyReduction = TryKeyReductionRetarget || Switches.Contains(TEXT("keyreduction"));
ResumeTask = Switches.Contains(TEXT("resume"));
SkipAdditiveClips = Switches.Contains(TEXT("noadditive")) || true; // Disabled for now, TODO add support for it
const bool HasInput = ParamsMap.Contains(TEXT("input"));
if (PerformClipExtraction)
{
// We don't support extracting additive clips
SkipAdditiveClips = true;
}
if (PerformCompression && PerformClipExtraction)
{
UE_LOG(LogAnimationCompression, Error, TEXT("Cannot compress and extract clips at the same time"));
return 0;
}
if (!PerformCompression && !PerformClipExtraction)
{
UE_LOG(LogAnimationCompression, Error, TEXT("Must compress or extract clips"));
return 0;
}
if (PerformClipExtraction && ParamsMap.Contains(TEXT("input")))
{
UE_LOG(LogAnimationCompression, Error, TEXT("Cannot use an input directory when extracting clips"));
return 0;
}
// Make sure to log everything
LogAnimationCompression.SetVerbosity(ELogVerbosity::All);
if (TryAutomaticCompression)
{
AutoCompressionSettings = FAnimationUtils::GetDefaultAnimationBoneCompressionSettings();
AutoCompressionSettings->bForceBelowThreshold = true;
if (ParamsMap.Contains(TEXT("ErrorTolerance")))
{
AutoCompressionSettings->ErrorThreshold = FCString::Atof(*ParamsMap[TEXT("ErrorTolerance")]);
}
}
if (TryACLCompression || !HasInput)
{
ACLCompressionSettings = NewObject<UAnimBoneCompressionSettings>(this, UAnimBoneCompressionSettings::StaticClass());
ACLCodec = NewObject<UAnimBoneCompressionCodec_ACL>(this, UAnimBoneCompressionCodec_ACL::StaticClass());
ACLCompressionSettings->Codecs.Add(ACLCodec);
ACLCompressionSettings->AddToRoot();
}
if (TryKeyReduction)
{
KeyReductionCompressionSettings = NewObject<UAnimBoneCompressionSettings>(this, UAnimBoneCompressionSettings::StaticClass());
KeyReductionCodec = NewObject<UAnimCompress_RemoveLinearKeys>(this, UAnimCompress_RemoveLinearKeys::StaticClass());
KeyReductionCodec->RotationCompressionFormat = ACF_Float96NoW;
KeyReductionCodec->TranslationCompressionFormat = ACF_None;
KeyReductionCodec->ScaleCompressionFormat = ACF_None;
KeyReductionCodec->bActuallyFilterLinearKeys = true;
KeyReductionCodec->bRetarget = TryKeyReductionRetarget;
KeyReductionCompressionSettings->Codecs.Add(KeyReductionCodec);
KeyReductionCompressionSettings->AddToRoot();
}
FFileManagerGeneric FileManager;
FileManager.MakeDirectory(*OutputDir, true);
if (!HasInput)
{
// No source directory, use the current project instead
ACLRawDir = TEXT("");
DoActionToAllPackages<UAnimSequence, CompressAnimationsFunctor>(this, Params.ToUpper());
return 0;
}
else
{
check(PerformCompression);
// Use source directory
ACLRawDir = ParamsMap[TEXT("input")];
#if (ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION >= 26) || ENGINE_MAJOR_VERSION >= 5
UPackage* TempPackage = CreatePackage(TEXT("/Temp/ACL"));
#else
UPackage* TempPackage = CreatePackage(nullptr, TEXT("/Temp/ACL"));
#endif
TArray<FString> FilesLegacy;
FileManager.FindFiles(FilesLegacy, *ACLRawDir, TEXT(".acl.sjson")); // Legacy ASCII file format
TArray<FString> FilesBinary;
FileManager.FindFiles(FilesBinary, *ACLRawDir, TEXT(".acl")); // ACL 2.0+ binary format
TArray<FString> Files;
Files.Append(FilesLegacy);
Files.Append(FilesBinary);
for (const FString& Filename : Files)
{
const FString ACLClipPath = FPaths::Combine(*ACLRawDir, *Filename);
FString UEStatFilename = Filename.Replace(TEXT(".acl.sjson"), TEXT("_stats.sjson"), ESearchCase::CaseSensitive);
UEStatFilename = UEStatFilename.Replace(TEXT(".acl"), TEXT("_stats.sjson"), ESearchCase::CaseSensitive);
const FString UEStatPath = FPaths::Combine(*OutputDir, *UEStatFilename);
if (ResumeTask && FileManager.FileExists(*UEStatPath))
{
continue;
}
UE_LOG(LogAnimationCompression, Verbose, TEXT("Compressing: %s"), *Filename);
FArchive* StatWriter = FileManager.CreateFileWriter(*UEStatPath);
if (StatWriter == nullptr)
{
// Opening the file handle can fail if the file path is too long on Windows. UE does not properly handle long paths
// and adding the \\?\ prefix manually doesn't work, UE mangles it when it normalizes the path.
continue;
}
UESJSONStreamWriter StreamWriter(StatWriter);
sjson::Writer Writer(StreamWriter);
acl::track_array_qvvf ACLTracks;
const TCHAR* ErrorMsg = ReadACLClip(FileManager, ACLClipPath, ACLAllocatorImpl, ACLTracks);
if (ErrorMsg == nullptr)
{
USkeleton* UESkeleton = NewObject<USkeleton>(TempPackage, USkeleton::StaticClass());
ConvertSkeleton(ACLTracks, UESkeleton);
UAnimSequence* UEClip = NewObject<UAnimSequence>(TempPackage, UAnimSequence::StaticClass());
ConvertClip(ACLTracks, UEClip, UESkeleton);
// Make sure any pending async compression that might have started during load or construction is done
UEClip->WaitOnExistingCompression();
FCompressionContext Context;
Context.AutoCompressor = AutoCompressionSettings;
Context.ACLCompressor = ACLCompressionSettings;
Context.KeyReductionCompressor = KeyReductionCompressionSettings;
Context.UEClip = UEClip;
Context.UESkeleton = UESkeleton;
Context.ACLTracks = MoveTemp(ACLTracks);
Context.ACLRawSize = Context.ACLTracks.get_raw_size();
Context.UERawSize = UEClip->GetApproxRawSize();
Writer["duration"] = GetSequenceLength(*UEClip);
Writer["num_samples"] = Context.ACLTracks.get_num_samples_per_track();
Writer["ue4_raw_size"] = Context.UERawSize;
Writer["acl_raw_size"] = Context.ACLRawSize;
if (TryAutomaticCompression)
{
CompressWithUEAuto(Context, PerformExhaustiveDump, Writer);
ClearCompressedData(UEClip);
}
if (TryACLCompression)
{
CompressWithACL(Context, PerformExhaustiveDump, Writer);
ClearCompressedData(UEClip);
}
if (TryKeyReduction)
{
CompressWithUEKeyReduction(Context, PerformExhaustiveDump, Writer);
ClearCompressedData(UEClip);
}
ClearClip(UEClip);
}
else
{
Writer["error"] = TCHAR_TO_ANSI(ErrorMsg);
}
StatWriter->Close();
}
}
return 0;
}