├── .gitignore ├── ACLPlugin ├── Resources │ └── Icon128.png ├── Content │ ├── ACLAnimBoneCompressionSettings.uasset │ └── ACLAnimCurveCompressionSettings.uasset ├── Config │ └── FilterPlugin.ini ├── README.md ├── Source │ ├── ACLPlugin │ │ ├── Private │ │ │ ├── EditorDatabaseMonitor.h │ │ │ ├── UEDatabasePreviewStreamer.h │ │ │ ├── EditorDatabaseMonitor.cpp │ │ │ ├── AnimBoneCompressionCodec_ACLSafe.cpp │ │ │ ├── AnimBoneCompressionCodec_ACL.cpp │ │ │ ├── AnimBoneCompressionCodec_ACLCustom.cpp │ │ │ ├── UEDatabaseStreamer.h │ │ │ ├── ACLImpl.cpp │ │ │ ├── AnimCurveCompressionCodec_ACL.cpp │ │ │ ├── ACLPluginModule.cpp │ │ │ └── AnimBoneCompressionCodec_ACLDatabase.cpp │ │ ├── Public │ │ │ ├── IACLPluginModule.h │ │ │ └── ACLImpl.h │ │ ├── Classes │ │ │ ├── AnimBoneCompressionCodec_ACLSafe.h │ │ │ ├── AnimCurveCompressionCodec_ACL.h │ │ │ ├── AnimBoneCompressionCodec_ACL.h │ │ │ ├── AnimBoneCompressionCodec_ACLCustom.h │ │ │ ├── AnimBoneCompressionCodec_ACLBase.h │ │ │ ├── AnimBoneCompressionCodec_ACLDatabase.h │ │ │ └── AnimationCompressionLibraryDatabase.h │ │ └── ACLPlugin.Build.cs │ └── ACLPluginEditor │ │ ├── Classes │ │ ├── ACLDatabaseBuildCommandlet.h │ │ ├── AnimationCompressionLibraryDatabaseFactory.h │ │ └── ACLStatsDumpCommandlet.h │ │ ├── Public │ │ └── IACLPluginEditorModule.h │ │ ├── Private │ │ ├── AnimationCompressionLibraryDatabaseFactory.cpp │ │ ├── AssetTypeActions_AnimationCompressionLibraryDatabase.h │ │ ├── ACLPluginEditorModule.cpp │ │ ├── AssetTypeActions_AnimationCompressionLibraryDatabase.cpp │ │ └── ACLDatabaseBuildCommandlet.cpp │ │ └── ACLPluginEditor.Build.cs ├── ACLPlugin.uplugin ├── LICENSE └── Extras │ └── tally_anim_csv.py ├── Docs ├── Images │ ├── BlueprintStreaming.png │ ├── CompressionSettings_Default.png │ ├── CompressionSettings_Custom_Clip.png │ ├── CurveCompressionSettings_Default.jpg │ ├── CompressionSettings_Custom_Options.png │ ├── CompressionSettings_DatabaseAsset.png │ ├── CompressionSettings_DatabaseCodec.png │ └── CompressionSettings_Custom_Segmenting.png ├── a_boy_and_his_kite.md ├── pre_4_25_integration.md ├── fight_scene_performance.md ├── cmu_performance.md ├── paragon_performance.md ├── decompression_performance.md └── error_measurements.md ├── .gitmodules ├── .editorconfig ├── .github └── FUNDING.yml ├── CONTRIBUTING.md ├── LICENSE ├── .all-contributorsrc ├── CODE_OF_CONDUCT.md ├── Tools └── prepare_release.py ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__/ 2 | Staging/ 3 | ACLPlugin_*.zip 4 | -------------------------------------------------------------------------------- /ACLPlugin/Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/ACLPlugin/Resources/Icon128.png -------------------------------------------------------------------------------- /Docs/Images/BlueprintStreaming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/BlueprintStreaming.png -------------------------------------------------------------------------------- /Docs/Images/CompressionSettings_Default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/CompressionSettings_Default.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ACLPlugin/Source/ThirdParty/acl"] 2 | path = ACLPlugin/Source/ThirdParty/acl 3 | url = https://github.com/nfrechette/acl.git 4 | -------------------------------------------------------------------------------- /Docs/Images/CompressionSettings_Custom_Clip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/CompressionSettings_Custom_Clip.png -------------------------------------------------------------------------------- /Docs/Images/CurveCompressionSettings_Default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/CurveCompressionSettings_Default.jpg -------------------------------------------------------------------------------- /Docs/Images/CompressionSettings_Custom_Options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/CompressionSettings_Custom_Options.png -------------------------------------------------------------------------------- /Docs/Images/CompressionSettings_DatabaseAsset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/CompressionSettings_DatabaseAsset.png -------------------------------------------------------------------------------- /Docs/Images/CompressionSettings_DatabaseCodec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/CompressionSettings_DatabaseCodec.png -------------------------------------------------------------------------------- /Docs/Images/CompressionSettings_Custom_Segmenting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/Docs/Images/CompressionSettings_Custom_Segmenting.png -------------------------------------------------------------------------------- /ACLPlugin/Content/ACLAnimBoneCompressionSettings.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/ACLPlugin/Content/ACLAnimBoneCompressionSettings.uasset -------------------------------------------------------------------------------- /ACLPlugin/Content/ACLAnimCurveCompressionSettings.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/HEAD/ACLPlugin/Content/ACLAnimCurveCompressionSettings.uasset -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | # Matches multiple files with brace expansion notation 8 | [*.{cpp,h,py,txt}] 9 | charset = utf-8 10 | indent_style = tab 11 | indent_size = 4 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /ACLPlugin/Config/FilterPlugin.ini: -------------------------------------------------------------------------------- 1 | ; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and 2 | ; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. 3 | 4 | [FilterPlugin] 5 | /LICENSE 6 | /README.md 7 | -------------------------------------------------------------------------------- /ACLPlugin/README.md: -------------------------------------------------------------------------------- 1 | # ACL Plugin 2 | 3 | This plugin integrates the [Animation Compression Library](https://github.com/nfrechette/acl) within Unreal Engine 4 and 5. It is suitable for all your animations and features a low memory footprint, high accuracy, and very fast compression and decompression. 4 | 5 | See [here](https://github.com/nfrechette/acl-ue4-plugin) for details! 6 | 7 | Note: the plugin releases on GitHub and on the Unreal Marketplace remove a few development only dependencies not required at runtime or within the editor. 8 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/EditorDatabaseMonitor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2021 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | 7 | #if WITH_EDITORONLY_DATA 8 | 9 | class UAnimationCompressionLibraryDatabase; 10 | 11 | /** A central database monitor that ensures database instances have their mappings up to date. */ 12 | namespace EditorDatabaseMonitor 13 | { 14 | void RegisterMonitor(); 15 | void UnregisterMonitor(); 16 | 17 | void MarkDirty(UAnimationCompressionLibraryDatabase* Database); 18 | } 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Classes/ACLDatabaseBuildCommandlet.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2021 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "Commandlets/Commandlet.h" 6 | #include "ACLDatabaseBuildCommandlet.generated.h" 7 | 8 | /* 9 | * This commandlet is used to update instances of UAnimationCompressionLibraryDatabase to ensure their mapping is up-to-date. 10 | */ 11 | UCLASS() 12 | class UACLDatabaseBuildCommandlet : public UCommandlet 13 | { 14 | GENERATED_UCLASS_BODY() 15 | 16 | public: 17 | virtual int32 Main(const FString& Params) override; 18 | }; 19 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Public/IACLPluginModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleInterface.h" 7 | #include "Modules/ModuleManager.h" 8 | 9 | /** The main ACL plugin module interface. */ 10 | class IACLPlugin : public IModuleInterface 11 | { 12 | public: 13 | static inline IACLPlugin& Get() 14 | { 15 | return FModuleManager::LoadModuleChecked("ACLPlugin"); 16 | } 17 | 18 | static inline bool IsAvailable() 19 | { 20 | return FModuleManager::Get().IsModuleLoaded("ACLPlugin"); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Public/IACLPluginEditorModule.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleInterface.h" 7 | #include "Modules/ModuleManager.h" 8 | 9 | /** The editor ACL plugin module interface. */ 10 | class IACLPluginEditor : public IModuleInterface 11 | { 12 | public: 13 | static inline IACLPluginEditor& Get() 14 | { 15 | return FModuleManager::LoadModuleChecked("ACLPluginEditor"); 16 | } 17 | 18 | static inline bool IsAvailable() 19 | { 20 | return FModuleManager::Get().IsModuleLoaded("ACLPluginEditor"); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Classes/AnimationCompressionLibraryDatabaseFactory.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Factories/Factory.h" 7 | #include "AnimationCompressionLibraryDatabaseFactory.generated.h" 8 | 9 | UCLASS(HideCategories = Object, MinimalAPI) 10 | class UAnimationCompressionLibraryDatabaseFactory : public UFactory 11 | { 12 | GENERATED_UCLASS_BODY() 13 | 14 | //~ Begin UFactory Interface 15 | virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; 16 | //~ Begin UFactory Interface 17 | }; 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | 14 | github: [nfrechette] 15 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Private/AnimationCompressionLibraryDatabaseFactory.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "AnimationCompressionLibraryDatabaseFactory.h" 4 | #include "AnimationCompressionLibraryDatabase.h" 5 | 6 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 7 | #include UE_INLINE_GENERATED_CPP_BY_NAME(AnimationCompressionLibraryDatabaseFactory) 8 | #endif 9 | 10 | UAnimationCompressionLibraryDatabaseFactory::UAnimationCompressionLibraryDatabaseFactory(const FObjectInitializer& ObjectInitializer) 11 | : Super(ObjectInitializer) 12 | { 13 | bCreateNew = true; 14 | SupportedClass = UAnimationCompressionLibraryDatabase::StaticClass(); 15 | } 16 | 17 | UObject* UAnimationCompressionLibraryDatabaseFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) 18 | { 19 | return NewObject(InParent, Class, Name, Flags); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /ACLPlugin/ACLPlugin.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 20100, 4 | "VersionName": "2.1.0", 5 | "FriendlyName": "Animation Compression Library", 6 | "Description": "Use the Animation Compression Library (ACL) to compress AnimSequences.", 7 | "Category": "Animation", 8 | "CreatedBy": "Nicholas Frechette", 9 | "CreatedByURL": "https://nfrechette.github.io/", 10 | "DocsURL": "https://github.com/nfrechette/acl-ue4-plugin", 11 | "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/993b51e29e9a48d0b3fb449645b82cb6", 12 | "SupportURL": "https://github.com/nfrechette/acl-ue4-plugin", 13 | "EnabledByDefault": true, 14 | "CanContainContent": true, 15 | "IsBetaVersion": false, 16 | "EngineVersion": "4.25.0", 17 | "Modules": [ 18 | { 19 | "Name": "ACLPlugin", 20 | "Type": "Runtime", 21 | "LoadingPhase": "PostConfigInit", 22 | "BlacklistPlatforms": [] 23 | }, 24 | { 25 | "Name": "ACLPluginEditor", 26 | "Type" : "Editor", 27 | "LoadingPhase" : "Default", 28 | "BlacklistPlatforms": [] 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Thank you for your interest in the Animation Compression Library Unreal Engine 4 Plugin! All contributions that follow our guidelines below and abide by our [code of conduct](CODE_OF_CONDUCT.md) are welcome. 4 | 5 | In this document you will find relevant reading material, what contributions we are looking for, and what we are *not* looking for. 6 | 7 | Project contact email: zeno490@gmail.com 8 | 9 | # Basics 10 | 11 | The project roadmap is tracked with [GitHub issues](https://github.com/nfrechette/acl-ue4-plugin/issues). [Backlog issues](https://github.com/nfrechette/acl-ue4-plugin/milestone/1) are things I would like to get done eventually but that have not been prioritized yet. 12 | 13 | Whether you create an issue or a pull request, I will do my best to comment or reply within 48 hours. 14 | 15 | # Contributions we are looking for 16 | 17 | All contributions are welcome providing that they fit within the scope of this project. Fixes to [ACL](https://github.com/nfrechette/acl) should be submitted to that depot. 18 | 19 | If you aren't sure, don't be afraid to reach out by email! 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nicholas Frechette 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ACLPlugin/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nicholas Frechette 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Classes/ACLStatsDumpCommandlet.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "Commandlets/Commandlet.h" 6 | #include "ACLStatsDumpCommandlet.generated.h" 7 | 8 | /* 9 | * This commandlet is used to extract and dump animation compression statistics. 10 | * 11 | * See cpp implementation for example usage and supported arguments. 12 | */ 13 | UCLASS() 14 | class UACLStatsDumpCommandlet : public UCommandlet 15 | { 16 | GENERATED_UCLASS_BODY() 17 | 18 | public: 19 | virtual int32 Main(const FString& Params) override; 20 | 21 | FString ACLRawDir; 22 | FString OutputDir; 23 | 24 | bool PerformExhaustiveDump; 25 | bool PerformCompression; 26 | bool PerformClipExtraction; 27 | bool TryAutomaticCompression; 28 | bool TryACLCompression; 29 | bool TryKeyReductionRetarget; 30 | bool TryKeyReduction; 31 | bool ResumeTask; 32 | bool SkipAdditiveClips; 33 | 34 | class UAnimBoneCompressionSettings* AutoCompressionSettings; 35 | class UAnimBoneCompressionSettings* ACLCompressionSettings; 36 | class UAnimBoneCompressionSettings* KeyReductionCompressionSettings; 37 | class UAnimBoneCompressionCodec_ACL* ACLCodec; 38 | class UAnimCompress_RemoveLinearKeys* KeyReductionCodec; 39 | }; 40 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/ACLPluginEditor.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | using System.IO; 4 | 5 | namespace UnrealBuildTool.Rules 6 | { 7 | public class ACLPluginEditor : ModuleRules 8 | { 9 | public ACLPluginEditor(ReadOnlyTargetRules Target) : base(Target) 10 | { 11 | CppStandard = CppStandardVersion.Cpp17; 12 | 13 | // Replace with PCHUsageMode.UseExplicitOrSharedPCHs when this plugin can compile with cpp20 14 | PCHUsage = PCHUsageMode.NoPCHs; 15 | 16 | string ACLSDKDir = Path.GetFullPath(Path.Combine(ModuleDirectory, "../ThirdParty")); 17 | 18 | //OptimizeCode = CodeOptimization.Never; 19 | //bUseUnity = false; 20 | 21 | PublicIncludePaths.Add(Path.Combine(ModuleDirectory, "Public")); 22 | PublicIncludePaths.Add(Path.Combine(ACLSDKDir, "acl/external/sjson-cpp/includes")); 23 | 24 | PublicDependencyModuleNames.Add("ACLPlugin"); 25 | PublicDependencyModuleNames.Add("Core"); 26 | PublicDependencyModuleNames.Add("CoreUObject"); 27 | PublicDependencyModuleNames.Add("Engine"); 28 | 29 | if (Target.Version.MajorVersion >= 5) 30 | { 31 | PublicDependencyModuleNames.Add("AnimationDataController"); 32 | } 33 | 34 | PrivateDependencyModuleNames.Add("EditorStyle"); 35 | PrivateDependencyModuleNames.Add("Slate"); 36 | PrivateDependencyModuleNames.Add("SlateCore"); 37 | PrivateDependencyModuleNames.Add("UnrealEd"); 38 | 39 | PrivateDefinitions.Add("ACL_USE_SJSON"); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Docs/a_boy_and_his_kite.md: -------------------------------------------------------------------------------- 1 | # A Boy and His Kite performance 2 | 3 | | | Compressed Size | Compression Rate | 4 | | ------------------------------------ | --------------- | ---------------- | 5 | | **UE Compressed Rich Curves 0.0** | 3620.66 KB | 1.0x (baseline) | 6 | | **UE Compressed Rich Curves 0.001** | 1458.77 KB | 2.5x smaller | 7 | | **UE Uniform Sampling** | 2052.82 KB | 1.8x smaller | 8 | | **ACL 0.001** | 540.24 KB | 6.7x smaller | 9 | | **ACL with morph 0.01 cm** | 381.10 KB | 9.5x smaller | 10 | 11 | *ACL Plugin v1.0* and *UE 4.25* were used to gather these statistics. 12 | 13 | ## Data and method used 14 | 15 | To compile these statistics, the GDC 2015 demo from *Epic* [A Boy and His Kite](https://www.youtube.com/watch?v=JNgsbNvkNjE) is used. 16 | 17 | * Number of clips: **31** 18 | * Number of animated curves: **811** 19 | * Number of morph target curves: **692** 20 | 21 | To measure and extract the compression statistics, UE was manually instrumented to print out the compressed size of the curve data. 22 | 23 | The *ACL Plugin* uses the default settings with a morph target precision of **0.01cm** and a curve precision of **0.001**. Both values are suitable for production use. Numbers above are shown with and without the morph target precision enabled. 24 | 25 | The *UE Compressed Rich Curve* uses an error threshold of **0.0** or **0.001**. 26 | 27 | The *UE Uniform Sampling* uses default values. 28 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Private/AssetTypeActions_AnimationCompressionLibraryDatabase.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "AssetTypeActions_Base.h" 7 | #include "AnimationCompressionLibraryDatabase.h" 8 | 9 | class FAssetTypeActions_AnimationCompressionLibraryDatabase : public FAssetTypeActions_Base 10 | { 11 | public: 12 | // IAssetTypeActions Implementation 13 | virtual FText GetName() const override { return FText::FromString(TEXT("ACL Database")); } 14 | virtual FColor GetTypeColor() const override { return FColor(255, 255, 0); } 15 | virtual UClass* GetSupportedClass() const override { return UAnimationCompressionLibraryDatabase::StaticClass(); } 16 | virtual bool CanFilter() override { return true; } 17 | virtual uint32 GetCategories() override { return EAssetTypeCategories::Animation; } 18 | virtual const TArray& GetSubMenus() const override; 19 | 20 | virtual void OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor = TSharedPtr()) override; 21 | 22 | virtual bool HasActions(const TArray& InObjects) const override { return true; } 23 | virtual void GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) override; 24 | 25 | private: 26 | void AddToolbarExtension(FToolBarBuilder& Builder, TWeakObjectPtr DatabasePtr); 27 | void ExecuteBuild(TWeakObjectPtr DatabasePtr); 28 | }; 29 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Classes/AnimBoneCompressionCodec_ACLSafe.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/ObjectMacros.h" 7 | #include "AnimBoneCompressionCodec_ACLBase.h" 8 | #include "AnimBoneCompressionCodec_ACLSafe.generated.h" 9 | 10 | /** Uses the open source Animation Compression Library with the safest and least destructive settings suitable when animations must be preserved with near raw fidelity. */ 11 | UCLASS(MinimalAPI, config = Engine, meta = (DisplayName = "Anim Compress ACL Safe")) 12 | class UAnimBoneCompressionCodec_ACLSafe : public UAnimBoneCompressionCodec_ACLBase 13 | { 14 | GENERATED_UCLASS_BODY() 15 | 16 | #if WITH_EDITORONLY_DATA 17 | // UAnimBoneCompressionCodec implementation 18 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 19 | virtual void PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) override; 20 | #else 21 | virtual void PopulateDDCKey(FArchive& Ar) override; 22 | #endif 23 | 24 | // UAnimBoneCompressionCodec_ACLBase implementation 25 | virtual void GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const override; 26 | #endif 27 | 28 | // UAnimBoneCompressionCodec implementation 29 | virtual void DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const override; 30 | virtual void DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const override; 31 | }; 32 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/ACLPlugin.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 2 | 3 | using System.IO; 4 | 5 | namespace UnrealBuildTool.Rules 6 | { 7 | public class ACLPlugin : ModuleRules 8 | { 9 | public ACLPlugin(ReadOnlyTargetRules Target) : base(Target) 10 | { 11 | CppStandard = CppStandardVersion.Cpp17; 12 | 13 | string ACLSDKDir = Path.GetFullPath(Path.Combine(ModuleDirectory, "../ThirdParty")); 14 | 15 | // Replace with PCHUsageMode.UseExplicitOrSharedPCHs when this plugin can compile with cpp20 16 | PCHUsage = PCHUsageMode.NoPCHs; 17 | 18 | //OptimizeCode = CodeOptimization.Never; 19 | //bUseUnity = false; 20 | 21 | PublicIncludePaths.Add(Path.Combine(ACLSDKDir, "acl/includes")); 22 | PublicIncludePaths.Add(Path.Combine(ACLSDKDir, "acl/external/rtm/includes")); 23 | 24 | PublicDependencyModuleNames.Add("Core"); 25 | PublicDependencyModuleNames.Add("CoreUObject"); 26 | PublicDependencyModuleNames.Add("Engine"); 27 | 28 | if (Target.bBuildEditor) 29 | { 30 | PrivateDependencyModuleNames.Add("DesktopPlatform"); 31 | PrivateDependencyModuleNames.Add("UnrealEd"); 32 | } 33 | 34 | if (Target.Platform == UnrealTargetPlatform.Linux) 35 | { 36 | // There appears to be a bug when cross-compiling Linux under Windows where the clang tool-chain used 37 | // isn't fully C++11 compliant. The standard specifies that when the 'cinttypes' header is included 38 | // the format macros are always defined unlike C which requires the following macro to be defined first. 39 | // This fix should be required for UE 4.20 and earlier versions. 40 | PrivateDefinitions.Add("__STDC_FORMAT_MACROS"); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Docs/pre_4_25_integration.md: -------------------------------------------------------------------------------- 1 | ## Engine integration into UE 4.24 and earlier 2 | 3 | In order to use the ACL plugin in *Unreal Engine 4.24* and earlier, you will need to manually integrate a few engine changes. These changes can be found in the following *GitHub* branches: 4 | 5 | * **4.19.x:** [branch](https://github.com/nfrechette/UnrealEngine/tree/4.19-acl) - [patch](https://github.com/nfrechette/UnrealEngine/pull/3.patch) (requires ACL plugin **v0.3 or earlier**) 6 | * 4.20.x: Use 4.19.x for inspiration, few to no engine changes should conflict (requires ACL plugin **v0.3 or earlier**) 7 | * 4.21.x: Use 4.22.x for inspiration, few to no engine changes should conflict (requires ACL plugin **v0.4**) 8 | * **4.22.x:** [branch](https://github.com/nfrechette/UnrealEngine/tree/4.22-acl) - [patch](https://github.com/nfrechette/UnrealEngine/pull/4.patch) (requires ACL plugin **v0.4**) 9 | * **4.23.x:** [branch](https://github.com/nfrechette/UnrealEngine/tree/4.23-acl) - [patch](https://github.com/nfrechette/UnrealEngine/pull/5.patch) (requires ACL plugin **v0.5**) 10 | * **4.24.x:** [branch](https://github.com/nfrechette/UnrealEngine/tree/4.24-acl) - [patch](https://github.com/nfrechette/UnrealEngine/pull/6.patch) (requires ACL plugin **v0.6**) 11 | 12 | Note that in order to see the custom engine branches linked above, you will first need to [request access](https://www.unrealengine.com/en-US/ue4-on-github) to the *Unreal Engine* source code. 13 | 14 | Some engine changes are required for the ACL plugin to work with older Unreal Engine versions. The changes are minimal and consist of a global registry for animation codecs that plugins can hook into as well as exposing a few things needed. The branches in my fork of the Unreal Engine do not contain the ACL plugin. You will have to download a plugin release suitable for your engine version. Simply place the `ACLPlugin` directory under `\Engine\Plugins` or in the plugin directory of your project. 15 | -------------------------------------------------------------------------------- /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "Meradrin", 10 | "name": "Meradrin", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/7066278?v=4", 12 | "profile": "https://github.com/Meradrin", 13 | "contributions": [ 14 | "code", 15 | "platform" 16 | ] 17 | }, 18 | { 19 | "login": "r-lyeh", 20 | "name": "r-lyeh", 21 | "avatar_url": "https://avatars.githubusercontent.com/u/35402248?v=4", 22 | "profile": "https://github.com/r-lyeh/statvs", 23 | "contributions": [ 24 | "doc" 25 | ] 26 | }, 27 | { 28 | "login": "nucleiis", 29 | "name": "nucleiis", 30 | "avatar_url": "https://avatars.githubusercontent.com/u/20119165?v=4", 31 | "profile": "https://github.com/nucleiis", 32 | "contributions": [ 33 | "bug" 34 | ] 35 | }, 36 | { 37 | "login": "fjoanisPhxlabs", 38 | "name": "fjoanisPhxlabs", 39 | "avatar_url": "https://avatars.githubusercontent.com/u/90003066?v=4", 40 | "profile": "https://github.com/fjoanisPhxlabs", 41 | "contributions": [ 42 | "code" 43 | ] 44 | }, 45 | { 46 | "login": "EAirPeter", 47 | "name": "EAirPeter", 48 | "avatar_url": "https://avatars.githubusercontent.com/u/5276153?v=4", 49 | "profile": "https://github.com/EAirPeter", 50 | "contributions": [ 51 | "code" 52 | ] 53 | }, 54 | { 55 | "login": "pmsimardeidos", 56 | "name": "pmsimardeidos", 57 | "avatar_url": "https://avatars.githubusercontent.com/u/58734263?v=4", 58 | "profile": "https://github.com/pmsimardeidos", 59 | "contributions": [ 60 | "code", 61 | "bug" 62 | ] 63 | } 64 | ], 65 | "contributorsPerLine": 7, 66 | "projectName": "acl-ue4-plugin", 67 | "projectOwner": "nfrechette", 68 | "repoType": "github", 69 | "repoHost": "https://github.com", 70 | "skipCi": true, 71 | "commitConvention": "angular" 72 | } 73 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Private/ACLPluginEditorModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "CoreMinimal.h" 4 | #include "IACLPluginEditorModule.h" 5 | #include "Modules/ModuleManager.h" 6 | 7 | #include "AssetToolsModule.h" 8 | 9 | #include "AssetTypeActions_AnimationCompressionLibraryDatabase.h" 10 | 11 | class FACLPluginEditor final : public IACLPluginEditor 12 | { 13 | private: 14 | /** IModuleInterface implementation */ 15 | virtual void StartupModule() override; 16 | virtual void ShutdownModule() override; 17 | 18 | void OnPostEngineInit(); 19 | void RegisterAssetTypeAction(IAssetTools& AssetTools, TSharedRef Action); 20 | 21 | TArray> RegisteredAssetTypeActions; 22 | }; 23 | 24 | IMPLEMENT_MODULE(FACLPluginEditor, ACLPluginEditor) 25 | 26 | ////////////////////////////////////////////////////////////////////////// 27 | 28 | void FACLPluginEditor::StartupModule() 29 | { 30 | FCoreDelegates::OnPostEngineInit.AddRaw(this, &FACLPluginEditor::OnPostEngineInit); 31 | } 32 | 33 | void FACLPluginEditor::ShutdownModule() 34 | { 35 | FCoreDelegates::OnPostEngineInit.RemoveAll(this); 36 | 37 | // Unregister our asset types 38 | if (FModuleManager::Get().IsModuleLoaded("AssetTools")) 39 | { 40 | IAssetTools& AssetTools = FModuleManager::GetModuleChecked("AssetTools").Get(); 41 | for (int32 Index = 0; Index < RegisteredAssetTypeActions.Num(); ++Index) 42 | { 43 | AssetTools.UnregisterAssetTypeActions(RegisteredAssetTypeActions[Index].ToSharedRef()); 44 | } 45 | } 46 | RegisteredAssetTypeActions.Empty(); 47 | } 48 | 49 | void FACLPluginEditor::OnPostEngineInit() 50 | { 51 | // Register our asset types 52 | IAssetTools& AssetTools = FModuleManager::LoadModuleChecked("AssetTools").Get(); 53 | 54 | RegisterAssetTypeAction(AssetTools, MakeShareable(new FAssetTypeActions_AnimationCompressionLibraryDatabase)); 55 | } 56 | 57 | void FACLPluginEditor::RegisterAssetTypeAction(IAssetTools& AssetTools, TSharedRef Action) 58 | { 59 | AssetTools.RegisterAssetTypeActions(Action); 60 | RegisteredAssetTypeActions.Add(Action); 61 | } 62 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Classes/AnimCurveCompressionCodec_ACL.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "ACLImpl.h" 6 | 7 | #include "CoreMinimal.h" 8 | #include "UObject/ObjectMacros.h" 9 | #include "Animation/AnimCurveCompressionCodec.h" 10 | #include "AnimCurveCompressionCodec_ACL.generated.h" 11 | 12 | /** Uses the open source Animation Compression Library with default settings suitable for general purpose animation curves. */ 13 | UCLASS(MinimalAPI, config = Engine, meta = (DisplayName = "ACL Curves")) 14 | class UAnimCurveCompressionCodec_ACL : public UAnimCurveCompressionCodec 15 | { 16 | GENERATED_UCLASS_BODY() 17 | 18 | #if WITH_EDITORONLY_DATA 19 | /** The curve precision to target when compressing the animation curves. */ 20 | UPROPERTY(EditAnywhere, Category = "ACL Options", meta = (ClampMin = "0")) 21 | float CurvePrecision; 22 | 23 | /** The mesh deformation precision to target when compressing morph target animation curves. */ 24 | UPROPERTY(EditAnywhere, Category = "ACL Options", meta = (ClampMin = "0", EditCondition = "MorphTargetSource != nullptr")) 25 | float MorphTargetPositionPrecision; 26 | 27 | /** The skeletal mesh used to estimate the morph target deformation during compression. */ 28 | UPROPERTY(EditAnywhere, Category = "ACL Options") 29 | class USkeletalMesh* MorphTargetSource; 30 | 31 | ////////////////////////////////////////////////////////////////////////// 32 | // UAnimCurveCompressionCodec implementation 33 | virtual void PopulateDDCKey(FArchive& Ar) override; 34 | virtual bool Compress(const FCompressibleAnimData& AnimSeq, FAnimCurveCompressionResult& OutResult) override; 35 | #endif 36 | 37 | // UAnimCurveCompressionCodec implementation 38 | virtual void DecompressCurves(const FCompressedAnimSequence& AnimSeq, FBlendedCurve& Curves, float CurrentTime) const override; 39 | 40 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 41 | virtual float DecompressCurve(const FCompressedAnimSequence& AnimSeq, FName CurveName, float CurrentTime) const override; 42 | #else 43 | virtual float DecompressCurve(const FCompressedAnimSequence& AnimSeq, SmartName::UID_Type CurveUID, float CurrentTime) const override; 44 | #endif 45 | }; 46 | -------------------------------------------------------------------------------- /Docs/fight_scene_performance.md: -------------------------------------------------------------------------------- 1 | # Matinee fight scene performance 2 | 3 | | | ACL Plugin v2.1.0 | ACL Plugin v2.0.0 | UE v5.2.0 | 4 | | ------- | -------- | ------- | ------- | 5 | | **Compressed size** | 8.06 MB | 8.18 MB | 23.68 MB | 6 | | **Compression ratio** | 7.74 :1 | 7.62 : 1 | 2.63 : 1 | 7 | | **Compression time** | 9.08s | 4.80s | 1m 35.19s | 8 | | **Compression speed** | 7036.24 KB/sec | 13295.33 KB/sec | 671.02 KB/sec | 9 | | **Max ACL error** | 0.1215 cm | 0.0635 cm | 0.1186 cm | 10 | | **Max UE error** | 0.0480 cm | 0.0684 cm | 0.0562 cm | 11 | | **ACL Error 99th percentile** | 0.0245 cm | 0.0201 cm | 0.0231 cm | 12 | | **Samples below ACL error threshold** | 94.99 % | 97.83 % | 90.49 % | 13 | 14 | ACL was smaller for **4** clip (**80 %**) 15 | ACL was more accurate for **4** clips (**80 %**) 16 | ACL has faster compression for **5** clips (**100 %**) 17 | ACL was smaller, better, and faster for **3** clip (**60 %**) 18 | 19 | Would the *ACL Plugin* have been included in the *Automatic Compression* permutations tried, it would have won for **5** clips (**100 %**) 20 | 21 | **Note: Numbers for ACL 2.0 were extracted with UE 4.25.** 22 | 23 | ## Data and method used 24 | 25 | To compile these statistics, the [Matinee fight scene](https://nfrechette.github.io/2017/10/05/acl_in_ue4/) is used. 26 | 27 | * Number of clips: **5** 28 | * Sample rate: **30 FPS** 29 | * Cinematic duration: **66 seconds** 30 | * *Troopers* 1-4 have **71** bones and the *Main Trooper* has **551** bones 31 | 32 | To measure and extract the compression statistics, the provided [commandlet](../ACLPlugin/Source/ACLPluginEditor/Classes/ACLStatsDumpCommandlet.h) is used along with a [python script](../ACLPlugin/Extras/stat_parser.py) to parse the results. 33 | 34 | The *ACL Plugin* uses the default settings with an error threshold of **0.01cm** while *UE* uses the *Automatic Compression* with an error threshold (master tolerance) of **0.1 cm**. Both error thresholds used are suitable for production use. The **99th** percentile and the number of samples below the ACL error threshold are calculated by measuring the error with ACL on every bone at every sample. 35 | 36 | *ACL* and *UE* both use separate methods to measure the error and both values are shown for transparency. 37 | 38 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/UEDatabasePreviewStreamer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2021 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "AnimationCompression.h" 6 | #include "CoreMinimal.h" 7 | #include "HAL/UnrealMemory.h" 8 | 9 | #include "ACLImpl.h" 10 | 11 | THIRD_PARTY_INCLUDES_START 12 | #include 13 | #include 14 | THIRD_PARTY_INCLUDES_END 15 | 16 | 17 | /** A simple UE preview streamer. Everything is assumed to be in memory and no real streaming is done. */ 18 | class UEDatabasePreviewStreamer final : public acl::database_streamer 19 | { 20 | public: 21 | UEDatabasePreviewStreamer(const acl::compressed_database& CompressedDatabase, const TArray& BulkData_) 22 | : database_streamer(Requests, acl::k_num_database_tiers) 23 | { 24 | const uint32 BulkDataMediumSize = CompressedDatabase.get_bulk_data_size(acl::quality_tier::medium_importance); 25 | 26 | const uint8* BulkDataMedium = BulkData_.GetData(); 27 | BulkData[0] = BulkDataMedium; 28 | BulkData[1] = BulkDataMedium + BulkDataMediumSize; 29 | } 30 | 31 | virtual bool is_initialized() const override { return true; } 32 | 33 | virtual const uint8_t* get_bulk_data(acl::quality_tier Tier) const override 34 | { 35 | checkf(Tier != acl::quality_tier::highest_importance, TEXT("Unexpected quality tier")); 36 | const uint32 TierIndex = uint32(Tier) - 1; 37 | return BulkData[TierIndex]; 38 | } 39 | 40 | virtual void stream_in(uint32_t Offset, uint32_t Size, bool CanAllocateBulkData, acl::quality_tier Tier, acl::streaming_request_id RequestID) override 41 | { 42 | UE_LOG(LogAnimationCompression, Log, TEXT("ACL database bulk data is streaming in!")); 43 | complete(RequestID); 44 | } 45 | 46 | virtual void stream_out(uint32_t Offset, uint32_t Size, bool CanDeallocateBulkData, acl::quality_tier Tier, acl::streaming_request_id RequestID) override 47 | { 48 | UE_LOG(LogAnimationCompression, Log, TEXT("ACL database bulk data is streaming out!")); 49 | complete(RequestID); 50 | } 51 | 52 | private: 53 | UEDatabasePreviewStreamer(const UEDatabasePreviewStreamer&) = delete; 54 | UEDatabasePreviewStreamer& operator=(const UEDatabasePreviewStreamer&) = delete; 55 | 56 | const uint8* BulkData[acl::k_num_database_tiers]; 57 | 58 | acl::streaming_request Requests[acl::k_num_database_tiers]; // One request per tier is enough 59 | }; 60 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/EditorDatabaseMonitor.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "EditorDatabaseMonitor.h" 4 | 5 | #if WITH_EDITORONLY_DATA 6 | 7 | #include "AnimationCompressionLibraryDatabase.h" 8 | 9 | #include "Containers/Ticker.h" 10 | #include "UObject/WeakObjectPtrTemplates.h" 11 | 12 | namespace EditorDatabaseMonitor 13 | { 14 | // Aliases to work with tickers 15 | #if ENGINE_MAJOR_VERSION >= 5 16 | using FTickerType = FTSTicker; 17 | using FTickerDelegateHandleType = FTSTicker::FDelegateHandle; 18 | #else 19 | using FTickerType = FTicker; 20 | using FTickerDelegateHandleType = FDelegateHandle; 21 | #endif 22 | 23 | static FTickerDelegateHandleType MonitorTickerHandle; 24 | static TArray> DirtyDatabases; 25 | static FCriticalSection DirtyDatabasesCS; 26 | 27 | static bool MonitorTicker(float DeltaTime) 28 | { 29 | // Copy our array with a quick swap to avoid holding the lock too long 30 | TArray> DirtyDatabasesTmp; 31 | { 32 | FScopeLock Lock(&DirtyDatabasesCS); 33 | Swap(DirtyDatabases, DirtyDatabasesTmp); 34 | } 35 | 36 | // Iterate over our dirty databases and refresh any stale mappings we might have 37 | for (TWeakObjectPtr& DatabasePtr : DirtyDatabasesTmp) 38 | { 39 | if (UAnimationCompressionLibraryDatabase* Database = DatabasePtr.Get()) 40 | { 41 | Database->UpdateReferencingAnimSequenceList(); 42 | } 43 | } 44 | 45 | const bool bFireTickerAgainAfterDelay = true; 46 | return bFireTickerAgainAfterDelay; 47 | } 48 | 49 | void RegisterMonitor() 50 | { 51 | if (MonitorTickerHandle.IsValid()) 52 | { 53 | return; // Already registered 54 | } 55 | 56 | // Tick every 300ms 57 | const float TickerDelay = 0.3F; 58 | 59 | MonitorTickerHandle = FTickerType::GetCoreTicker().AddTicker(TEXT("ACLEditorDatabaseMonitor"), TickerDelay, MonitorTicker); 60 | } 61 | 62 | void UnregisterMonitor() 63 | { 64 | if (MonitorTickerHandle.IsValid()) 65 | { 66 | FTickerType::GetCoreTicker().RemoveTicker(MonitorTickerHandle); 67 | MonitorTickerHandle.Reset(); 68 | } 69 | } 70 | 71 | void MarkDirty(UAnimationCompressionLibraryDatabase* Database) 72 | { 73 | if (Database == nullptr) 74 | { 75 | return; // Nothing to do 76 | } 77 | 78 | // Add our database, we'll process it later 79 | FScopeLock Lock(&DirtyDatabasesCS); 80 | DirtyDatabases.AddUnique(Database); 81 | } 82 | } 83 | 84 | #endif 85 | -------------------------------------------------------------------------------- /ACLPlugin/Extras/tally_anim_csv.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import sys 3 | 4 | 5 | if __name__ == "__main__": 6 | if len(sys.argv) != 2: 7 | print('Invalid usage: python {}'.format(' '.join(sys.argv))) 8 | print('Usage: python tally_anim_csv.py ') 9 | sys.exit(1) 10 | 11 | csv_filename = sys.argv[1] 12 | 13 | rows = None 14 | 15 | # Read our CSV file and cache our rows 16 | with open(csv_filename, newline='') as csv_file: 17 | reader = csv.reader(csv_file) 18 | rows = list(reader) 19 | 20 | if not rows or len(rows) == 0: 21 | print('No CSV data found!') 22 | sys.exit(1) 23 | 24 | header_row = rows[-2] 25 | if rows[-1][0] != '[HasHeaderRowAtEnd]': 26 | print('Expected the header row at the end') 27 | sys.exit(1) 28 | 29 | if header_row[-1] == 'Animation/Total/ExtractPoseFromAnimData': 30 | print('This CSV file has already been processed!') 31 | sys.exit(1) 32 | 33 | num_columns = len(header_row) 34 | num_rows = len(rows) 35 | 36 | # The header is written last because new categories are added as they are found 37 | # As such, some rows can have missing columns 38 | # We pad them all with zeroes 39 | for row_idx in range(1, num_rows - 2): 40 | row = rows[row_idx] 41 | 42 | num_missing_columns = num_columns - len(row) 43 | if num_missing_columns > 0: 44 | row.extend(["0"] * num_missing_columns) 45 | 46 | # Process our rows 47 | # First append our new header to the header row as the last column 48 | 49 | num_columns = len(header_row) 50 | num_rows = len(rows) 51 | header_row.append('Animation/Total/ExtractPoseFromAnimData') 52 | print('Found {} columns and {} rows to process ...'.format(num_columns, num_rows)) 53 | 54 | # Process every row and add the sum totals to our last column 55 | # Skip the first row, it contains our header 56 | # Skip the last two rows, it contains other things 57 | for row_idx in range(1, num_rows - 2): 58 | row = rows[row_idx] 59 | 60 | total_decomp_time = 0.0 61 | for column_idx in range(num_columns): 62 | header_name = header_row[column_idx] 63 | if header_name.endswith('ExtractPoseFromAnimData'): 64 | try: 65 | value = float(row[column_idx]) 66 | except: 67 | value = 0.0 68 | total_decomp_time += value 69 | 70 | # By default, everything is in milliseconds 71 | row.append(str(total_decomp_time)) 72 | 73 | # Make sure both header rows match 74 | rows[0] = header_row 75 | 76 | # Write out our modified rows over the same file 77 | with open(csv_filename, 'w', newline='') as csv_file: 78 | writer = csv.writer(csv_file, quoting=csv.QUOTE_MINIMAL) 79 | writer.writerows(rows) 80 | 81 | print('Done!') 82 | -------------------------------------------------------------------------------- /Docs/cmu_performance.md: -------------------------------------------------------------------------------- 1 | # Carnegie-Mellon University database performance 2 | 3 | | | ACL Plugin v2.1.0 | ACL Plugin v2.0.0 | UE v5.2.0 | 4 | | ------- | -------- | -------- | -------- | 5 | | **Compressed size** | 67.92 MB | 75.55 MB | 99.74 MB | 6 | | **Compression ratio** | 21.05 : 1 | 18.92 : 1 | 14.33 : 1 | 7 | | **Compression time** | 2m 48.78s | 2m 19.06s | 5m 54.28s | 8 | | **Compression speed** | 8671.99 KB/sec | 10525.35 KB/sec | 4131.39 KB/sec | 9 | | **Max ACL error** | 0.1299 cm | 0.0833 cm cm | 0.1520 cm | 10 | | **Max UE error** | 0.0662 cm | 0.0816 cm | 0.0995 cm | 11 | | **ACL Error 99th percentile** | 0.0092 cm | 0.0088 cm | 0.0283 cm | 12 | | **Samples below ACL error threshold** | 99.61 % | 99.93 % | 49.12 % | 13 | 14 | ACL was smaller for **2533** clips (**99.96 %**) 15 | ACL was more accurate for **2500** clips (**98.66 %**) 16 | ACL has faster compression for **2518** clips (**99.37 %**) 17 | ACL was smaller, better, and faster for **2483** clips (**97.99 %**) 18 | 19 | Would the *ACL Plugin* have been included in the *Automatic Compression* permutations tried, it would have won for **2534** clips (**100.00 %**) 20 | 21 | **Note: Numbers for ACL 2.0 were extracted with UE 4.25.** 22 | 23 | ## Data and method used 24 | 25 | To compile the statistics, the [animation database from Carnegie-Mellon University](http://mocap.cs.cmu.edu/) is used. 26 | The raw animation clips in FBX form can be found on the Unity asset store [here](https://www.assetstore.unity3d.com/en/#!/content/19991). 27 | They were converted to the [ACL file format](the_acl_file_format.md) using the [fbx2acl](https://github.com/nfrechette/acl/tree/develop/tools/fbx2acl) script. Data available upon request, it is far too large for GitHub. 28 | 29 | * Number of clips: **2534** 30 | * Sample rate: **24 FPS** 31 | * Total duration: **9h 49m 37.58s** 32 | * Raw size: **1429.38 MB** (10x float32 * num bones * num samples) 33 | 34 | To measure and extract the compression statistics, the provided [commandlet](../ACLPlugin/Source/ACLPluginEditor/Classes/ACLStatsDumpCommandlet.h) is used along with a [python script](../ACLPlugin/Extras/stat_parser.py) to parse the results. 35 | 36 | The *ACL Plugin* uses the default settings with an error threshold of **0.01cm** while *UE* uses the *Automatic Compression* with an error threshold (master tolerance) of **0.1 cm**. Both error thresholds used are suitable for production use. The **99th** percentile and the number of samples below the ACL error threshold are calculated by measuring the error with ACL on every bone at every sample. 37 | 38 | *ACL* and *UE* both use separate methods to measure the error and both values are shown for transparency. 39 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Classes/AnimBoneCompressionCodec_ACL.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | #include "PerPlatformProperties.h" 7 | #include "UObject/ObjectMacros.h" 8 | #include "AnimBoneCompressionCodec_ACLBase.h" 9 | #include "AnimBoneCompressionCodec_ACL.generated.h" 10 | 11 | /** Uses the open source Animation Compression Library with default settings suitable for general purpose animations. */ 12 | UCLASS(MinimalAPI, config = Engine, meta = (DisplayName = "Anim Compress ACL")) 13 | class UAnimBoneCompressionCodec_ACL : public UAnimBoneCompressionCodec_ACLBase 14 | { 15 | GENERATED_UCLASS_BODY() 16 | 17 | #if WITH_EDITORONLY_DATA 18 | /** The skeletal meshes used to estimate the skinning deformation during compression. */ 19 | UPROPERTY(EditAnywhere, Category = "ACL Options") 20 | TArray OptimizationTargets; 21 | 22 | /** Whether keyframe stripping is supported or not. Only used in the editor to enable/disable the feature. */ 23 | UPROPERTY(Transient) 24 | bool bIsKeyframeStrippingSupported; 25 | 26 | /** The minimum proportion of keyframes that should be stripped. UE 5.1+ */ 27 | UPROPERTY(EditAnywhere, Category = "ACL Destructive Options", meta = (ClampMin = "0", ClampMax = "1", EditCondition = "bIsKeyframeStrippingSupported", HideEditConditionToggle)) 28 | FPerPlatformFloat KeyframeStrippingProportion; 29 | 30 | /** The error threshold below which to strip keyframes. If a keyframe can be reconstructed with an error below the threshold, it is stripped. UE 5.1+ */ 31 | UPROPERTY(EditAnywhere, Category = "ACL Destructive Options", meta = (ClampMin = "0", EditCondition = "bIsKeyframeStrippingSupported", HideEditConditionToggle)) 32 | FPerPlatformFloat KeyframeStrippingThreshold; 33 | 34 | ////////////////////////////////////////////////////////////////////////// 35 | // UAnimBoneCompressionCodec implementation 36 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 37 | virtual void PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) override; 38 | #else 39 | virtual void PopulateDDCKey(FArchive& Ar) override; 40 | #endif 41 | 42 | // UAnimBoneCompressionCodec_ACLBase implementation 43 | virtual void GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const override; 44 | virtual TArray GetOptimizationTargets() const override { return OptimizationTargets; } 45 | #endif 46 | 47 | // UAnimBoneCompressionCodec implementation 48 | virtual void DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const override; 49 | virtual void DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const override; 50 | }; 51 | -------------------------------------------------------------------------------- /Docs/paragon_performance.md: -------------------------------------------------------------------------------- 1 | # Paragon database performance 2 | 3 | | | ACL Plugin v2.1.0 | ACL Plugin v2.0.0 | UE v5.2.0 | 4 | | ------- | -------- | ------- | ------- | 5 | | **Compressed size** | 183.98 MB | 224.30 MB | 391.66 MB | 6 | | **Compression ratio** | 23.24 : 1 | 19.06 : 1 | 10.92 : 1 | 7 | | **Compression time** | 19m 22.95s | 8m 32.77s | 33m 24.28s | 8 | | **Compression speed** | 3765.21 KB/sec | 8539.37 KB/sec | 2184.70 KB/sec | 9 | | **Max ACL error** | 1.2065 cm | 0.8622 cm | 1.2169 cm | 10 | | **Max UE error** | 0.8434 cm | 0.8602 cm | 0.8491 cm | 11 | | **ACL Error 99th percentile** | 0.0197 cm | 0.0095 cm | 0.0358 cm | 12 | | **Samples below ACL error threshold** | 94.78 % | 99.13 % | 82.81 % | 13 | 14 | ACL was smaller for **6469** clips (**98.64 %**) 15 | ACL was more accurate for **4829** clips (**73.64 %**) 16 | ACL has faster compression for **5302** clips (**80.85 %**) 17 | ACL was smaller, better, and faster for **3896** clips (**59.41 %**) 18 | 19 | Would the *ACL Plugin* have been included in the *Automatic Compression* permutations tried, it would have won for **6479** clips (**98.80 %**) 20 | 21 | **Note: Numbers for ACL 2.0 were extracted with UE 4.25.** 22 | 23 | ## Data and method used 24 | 25 | To compile these statistics, a large number of animations from [Paragon](https://www.epicgames.com/paragon) are used. 26 | In October 2017 the animations were manually extracted and converted to the [ACL file format](https://github.com/nfrechette/acl/blob/develop/docs/the_acl_file_format.md) losslessly. The data is sadly **NOT** available upon request. 27 | Epic has permitted [Nicholas Frechette](https://github.com/nfrechette) to use them for research purposes only under a non-disclosure agreement. 28 | 29 | **Note: Epic has since released Paragon assets publicly in early 2018, once I get around to it, it will be extracted along with updated stats.** 30 | 31 | * Number of clips: **6558** 32 | * Total duration: **7h 0m 45.27s** 33 | * Raw size: **4276.11 MB** (10x float32 * num bones * num samples) 34 | 35 | The data set contains among other things: 36 | 37 | * Lots of characters with varying number of bones 38 | * Animated objects of various shape and form 39 | * Very short and very long clips 40 | * Clips with unusual sample rate (as low as **2** FPS!) 41 | * World space clips 42 | * Lots of 3D scale 43 | * Lots of other exotic clips 44 | 45 | To measure and extract the compression statistics, the provided [commandlet](../ACLPlugin/Source/ACLPluginEditor/Classes/ACLStatsDumpCommandlet.h) is used along with a [python script](../ACLPlugin/Extras/stat_parser.py) to parse the results. 46 | 47 | The *ACL Plugin* uses the default settings with an error threshold of **0.01cm** while *UE* uses the *Automatic Compression* with an error threshold (master tolerance) of **0.1 cm**. Both error thresholds used are suitable for production use. The **99th** percentile and the number of samples below the ACL error threshold are calculated by measuring the error with ACL on every bone at every sample. 48 | 49 | *ACL* and *UE* both use separate methods to measure the error and both values are shown for transparency. 50 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Classes/AnimBoneCompressionCodec_ACLCustom.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/ObjectMacros.h" 7 | #include "AnimBoneCompressionCodec_ACLBase.h" 8 | #include "AnimBoneCompressionCodec_ACLCustom.generated.h" 9 | 10 | /** Uses the open source Animation Compression Library with custom settings suitable for debugging purposes. */ 11 | UCLASS(MinimalAPI, config = Engine, meta = (DisplayName = "Anim Compress ACL Custom")) 12 | class UAnimBoneCompressionCodec_ACLCustom : public UAnimBoneCompressionCodec_ACLBase 13 | { 14 | GENERATED_UCLASS_BODY() 15 | 16 | #if WITH_EDITORONLY_DATA 17 | /** The rotation format to use. */ 18 | UPROPERTY(EditAnywhere, Category = Clip) 19 | TEnumAsByte RotationFormat; 20 | 21 | /** The translation format to use. */ 22 | UPROPERTY(EditAnywhere, Category = Clip) 23 | TEnumAsByte TranslationFormat; 24 | 25 | /** The scale format to use. */ 26 | UPROPERTY(EditAnywhere, Category = Clip) 27 | TEnumAsByte ScaleFormat; 28 | 29 | /** The skeletal meshes used to estimate the skinning deformation during compression. */ 30 | UPROPERTY(EditAnywhere, Category = "ACL Options") 31 | TArray OptimizationTargets; 32 | 33 | /** Whether keyframe stripping is supported or not. Only used in the editor to enable/disable the feature. */ 34 | UPROPERTY(Transient) 35 | bool bIsKeyframeStrippingSupported; 36 | 37 | /** The minimum proportion of keyframes that should be stripped. */ 38 | UPROPERTY(EditAnywhere, Category = "ACL Destructive Options", meta = (ClampMin = "0", ClampMax = "1", EditCondition = "bIsKeyframeStrippingSupported", HideEditConditionToggle)) 39 | FPerPlatformFloat KeyframeStrippingProportion; 40 | 41 | /** The error threshold below which to strip keyframes. If a keyframe can be reconstructed with an error below the threshold, it is stripped. UE 5.1+ */ 42 | UPROPERTY(EditAnywhere, Category = "ACL Destructive Options", meta = (ClampMin = "0", EditCondition = "bIsKeyframeStrippingSupported", HideEditConditionToggle)) 43 | FPerPlatformFloat KeyframeStrippingThreshold; 44 | 45 | ////////////////////////////////////////////////////////////////////////// 46 | 47 | // UAnimBoneCompressionCodec implementation 48 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 49 | virtual void PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) override; 50 | #else 51 | virtual void PopulateDDCKey(FArchive& Ar) override; 52 | #endif 53 | 54 | // UAnimBoneCompressionCodec_ACLBase implementation 55 | virtual void GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const override; 56 | virtual TArray GetOptimizationTargets() const override { return OptimizationTargets; } 57 | #endif 58 | 59 | // UAnimBoneCompressionCodec implementation 60 | virtual void DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const override; 61 | virtual void DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const override; 62 | }; 63 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at zeno490@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/AnimBoneCompressionCodec_ACLSafe.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "AnimBoneCompressionCodec_ACLSafe.h" 4 | 5 | #include "ACLDecompressionImpl.h" 6 | 7 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 8 | #include UE_INLINE_GENERATED_CPP_BY_NAME(AnimBoneCompressionCodec_ACLSafe) 9 | #endif 10 | 11 | #if WITH_EDITORONLY_DATA 12 | THIRD_PARTY_INCLUDES_START 13 | #include 14 | THIRD_PARTY_INCLUDES_END 15 | #endif 16 | 17 | UAnimBoneCompressionCodec_ACLSafe::UAnimBoneCompressionCodec_ACLSafe(const FObjectInitializer& ObjectInitializer) 18 | : Super(ObjectInitializer) 19 | { 20 | } 21 | 22 | #if WITH_EDITORONLY_DATA 23 | void UAnimBoneCompressionCodec_ACLSafe::GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const 24 | { 25 | OutSettings = acl::get_default_compression_settings(); 26 | 27 | // Fallback to full precision rotations 28 | OutSettings.rotation_format = acl::rotation_format8::quatf_full; 29 | } 30 | 31 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 32 | void UAnimBoneCompressionCodec_ACLSafe::PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) 33 | #else 34 | void UAnimBoneCompressionCodec_ACLSafe::PopulateDDCKey(FArchive& Ar) 35 | #endif 36 | { 37 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 38 | Super::PopulateDDCKey(KeyArgs, Ar); 39 | 40 | const class ITargetPlatform* TargetPlatform = KeyArgs.TargetPlatform; 41 | #else 42 | Super::PopulateDDCKey(Ar); 43 | 44 | const class ITargetPlatform* TargetPlatform = nullptr; 45 | #endif 46 | 47 | acl::compression_settings Settings; 48 | GetCompressionSettings(TargetPlatform, Settings); 49 | 50 | uint32 ForceRebuildVersion = 1; 51 | uint32 SettingsHash = Settings.get_hash(); 52 | 53 | Ar << DefaultVirtualVertexDistance << SafeVirtualVertexDistance 54 | << ForceRebuildVersion << SettingsHash; 55 | } 56 | #endif // WITH_EDITORONLY_DATA 57 | 58 | void UAnimBoneCompressionCodec_ACLSafe::DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const 59 | { 60 | const FACLCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 61 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 62 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 63 | 64 | acl::decompression_context ACLContext; 65 | ACLContext.initialize(*CompressedClipData); 66 | 67 | ::DecompressPose(DecompContext, ACLContext, RotationPairs, TranslationPairs, ScalePairs, OutAtoms); 68 | } 69 | 70 | void UAnimBoneCompressionCodec_ACLSafe::DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const 71 | { 72 | const FACLCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 73 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 74 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 75 | 76 | acl::decompression_context ACLContext; 77 | ACLContext.initialize(*CompressedClipData); 78 | 79 | ::DecompressBone(DecompContext, ACLContext, TrackIndex, OutAtom); 80 | } 81 | 82 | -------------------------------------------------------------------------------- /Tools/prepare_release.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shutil 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | if len(sys.argv) != 2: 8 | print('Invalid usage: python {}'.format(' '.join(sys.argv))) 9 | print('Usage: python prepare_release.py (e.g. 4.25)') 10 | sys.exit(1) 11 | 12 | target_ue_version = sys.argv[1] 13 | 14 | tools_dir = os.path.dirname(os.path.realpath(__file__)) 15 | root_dir = os.path.join(tools_dir, '..') 16 | staging_dir = os.path.join(root_dir, 'Staging') 17 | 18 | if os.path.exists(staging_dir): 19 | print('Cleaning previous staging environment ...') 20 | shutil.rmtree(staging_dir) 21 | 22 | if not os.path.exists(staging_dir): 23 | os.makedirs(staging_dir) 24 | 25 | plugin_version = None 26 | with open(os.path.join(root_dir, 'ACLPLugin', 'ACLPlugin.uplugin'), 'r') as f: 27 | data = json.load(f) 28 | if not 'VersionName' in data: 29 | print('UE Plugin version not found in ACLPlugin.uplugin') 30 | sys.exit(1) 31 | plugin_version = data['VersionName'] 32 | 33 | print('Copying plugin content ...') 34 | plugin_src_dir = os.path.join(root_dir, 'ACLPlugin') 35 | plugin_dst_dir = os.path.join(staging_dir, 'ACLPlugin') 36 | shutil.copytree(plugin_src_dir, plugin_dst_dir) 37 | 38 | acl_root_dir = os.path.join(plugin_dst_dir, 'Source', 'ThirdParty', 'acl') 39 | rtm_root_dir = os.path.join(acl_root_dir, 'external', 'rtm') 40 | sjsoncpp_root_dir = os.path.join(acl_root_dir, 'external', 'sjson-cpp') 41 | 42 | # Copy natvis files in the root 43 | shutil.copyfile(os.path.join(acl_root_dir, 'tools', 'vs_visualizers', 'acl.natvis'), os.path.join(plugin_dst_dir, 'acl.natvis')) 44 | shutil.copyfile(os.path.join(rtm_root_dir, 'tools', 'vs_visualizers', 'rtm.natvis'), os.path.join(plugin_dst_dir, 'rtm.natvis')) 45 | shutil.copyfile(os.path.join(sjsoncpp_root_dir, 'tools', 'vs_visualizers', 'sjson-cpp.natvis'), os.path.join(plugin_dst_dir, 'sjson-cpp.natvis')) 46 | 47 | # Remove catch2 and other third party dependencies we do not own or need 48 | print('Removing what we don\'t need ...') 49 | shutil.rmtree(os.path.join(acl_root_dir, 'external', 'catch2')) 50 | shutil.rmtree(os.path.join(rtm_root_dir, 'external', 'catch2')) 51 | shutil.rmtree(os.path.join(sjsoncpp_root_dir, 'external', 'catch2')) 52 | 53 | shutil.rmtree(os.path.join(acl_root_dir, 'external', 'benchmark')) 54 | shutil.rmtree(os.path.join(rtm_root_dir, 'external', 'benchmark')) 55 | 56 | # Remove extra things not needed for UE development 57 | dirs_to_prune = [ acl_root_dir, rtm_root_dir, sjsoncpp_root_dir ] 58 | to_remove_list = [ 59 | '.all-contributorsrc', '.editorconfig', '.git', '.gitignore', '.gitmodules', 60 | 'cmake', 'tests', 'test_data', 'tools', 'make.py', 'sonar-project.properties', 61 | 'CMakeLists.txt', 'appveyor.yml', '.github', 'docs' 62 | ] 63 | 64 | for dir_to_prune in dirs_to_prune: 65 | for to_remove in to_remove_list: 66 | path_to_remove = os.path.join(dir_to_prune, to_remove) 67 | if os.path.exists(path_to_remove): 68 | try: 69 | shutil.rmtree(path_to_remove) 70 | except NotADirectoryError: 71 | os.remove(path_to_remove) 72 | 73 | print('Setting uplugin version to: {} ...'.format(target_ue_version)) 74 | uplugin_file = os.path.join(plugin_dst_dir, 'ACLPlugin.uplugin') 75 | with open(uplugin_file) as f: 76 | uplugin_file_content = f.read() 77 | uplugin_file_content = uplugin_file_content.replace('4.25.0', target_ue_version + '.0') 78 | with open(uplugin_file, 'w') as f: 79 | f.write(uplugin_file_content) 80 | 81 | print('Zipping ACLPlugin ...') 82 | zip_filename = os.path.join(root_dir, 'ACLPlugin_v' + plugin_version + '_' + target_ue_version) 83 | shutil.make_archive(zip_filename, 'zip', staging_dir) 84 | 85 | print('Done!') 86 | sys.exit(0) 87 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Classes/AnimBoneCompressionCodec_ACLBase.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/ObjectMacros.h" 7 | #include "Animation/AnimBoneCompressionCodec.h" 8 | 9 | #include "ACLImpl.h" 10 | 11 | THIRD_PARTY_INCLUDES_START 12 | #if WITH_EDITORONLY_DATA 13 | #include 14 | #include 15 | #include 16 | #include 17 | #endif 18 | 19 | #include 20 | THIRD_PARTY_INCLUDES_END 21 | 22 | #include "AnimBoneCompressionCodec_ACLBase.generated.h" 23 | 24 | struct FACLCompressedAnimData final : public ICompressedAnimData 25 | { 26 | /** Holds the compressed_tracks instance */ 27 | TArrayView CompressedByteStream; 28 | 29 | const acl::compressed_tracks* GetCompressedTracks() const { return acl::make_compressed_tracks(CompressedByteStream.GetData()); } 30 | 31 | // ICompressedAnimData implementation 32 | virtual void Bind(const TArrayView BulkData) override { CompressedByteStream = BulkData; } 33 | virtual int64 GetApproxCompressedSize() const override { return CompressedByteStream.Num(); } 34 | virtual bool IsValid() const override; 35 | }; 36 | 37 | /** The base codec implementation for ACL support. */ 38 | UCLASS(abstract, MinimalAPI) 39 | class UAnimBoneCompressionCodec_ACLBase : public UAnimBoneCompressionCodec 40 | { 41 | GENERATED_UCLASS_BODY() 42 | 43 | #if WITH_EDITORONLY_DATA 44 | /** The compression level to use. Higher levels will be slower to compress but yield a lower memory footprint. */ 45 | UPROPERTY(EditAnywhere, Category = "ACL Options") 46 | TEnumAsByte CompressionLevel; 47 | 48 | /** How to treat phantom tracks. Phantom tracks are not mapped to a skeleton bone. */ 49 | UPROPERTY(EditAnywhere, Category = "ACL Options") 50 | ACLPhantomTrackMode PhantomTrackMode; 51 | 52 | /** The default virtual vertex distance for normal bones. */ 53 | UPROPERTY(EditAnywhere, Category = "ACL Options", meta = (ClampMin = "0")) 54 | float DefaultVirtualVertexDistance; 55 | 56 | /** The virtual vertex distance for bones that requires extra accuracy. */ 57 | UPROPERTY(EditAnywhere, Category = "ACL Options", meta = (ClampMin = "0")) 58 | float SafeVirtualVertexDistance; 59 | 60 | /** The error threshold to use when optimizing and compressing the animation sequence. */ 61 | UPROPERTY(EditAnywhere, Category = "ACL Options", meta = (ClampMin = "0")) 62 | float ErrorThreshold; 63 | 64 | // UAnimBoneCompressionCodec implementation 65 | virtual bool Compress(const FCompressibleAnimData& CompressibleAnimData, FCompressibleAnimDataResult& OutResult) override; 66 | 67 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 68 | virtual void PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) override; 69 | #else 70 | virtual void PopulateDDCKey(FArchive& Ar) override; 71 | #endif 72 | 73 | // Our implementation 74 | virtual void PostCompression(const FCompressibleAnimData& CompressibleAnimData, FCompressibleAnimDataResult& OutResult) const {} 75 | virtual void GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const PURE_VIRTUAL(UAnimBoneCompressionCodec_ACLBase::GetCompressionSettings, ); 76 | virtual TArray GetOptimizationTargets() const { return TArray(); } 77 | #endif 78 | 79 | // UAnimBoneCompressionCodec implementation 80 | virtual TUniquePtr AllocateAnimData() const override; 81 | virtual void ByteSwapIn(ICompressedAnimData& AnimData, TArrayView CompressedData, FMemoryReader& MemoryStream) const override; 82 | virtual void ByteSwapOut(ICompressedAnimData& AnimData, TArrayView CompressedData, FMemoryWriter& MemoryStream) const override; 83 | }; 84 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Private/AssetTypeActions_AnimationCompressionLibraryDatabase.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "AssetTypeActions_AnimationCompressionLibraryDatabase.h" 4 | #include "AnimBoneCompressionCodec_ACLDatabase.h" 5 | 6 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 7 | #include "Styling/AppStyle.h" 8 | #else 9 | #include "EditorStyleSet.h" 10 | #endif 11 | 12 | #include "Framework/MultiBox/MultiBoxBuilder.h" 13 | 14 | void FAssetTypeActions_AnimationCompressionLibraryDatabase::OpenAssetEditor(const TArray& InObjects, TSharedPtr EditWithinLevelEditor) 15 | { 16 | TSharedRef AssetEditor = FSimpleAssetEditor::CreateEditor(EToolkitMode::Standalone, EditWithinLevelEditor, InObjects); 17 | 18 | auto DatabaseAssets = GetTypedWeakObjectPtrs(InObjects); 19 | if (DatabaseAssets.Num() == 1) 20 | { 21 | TSharedPtr PluginCommands = MakeShareable(new FUICommandList); 22 | TSharedPtr ToolbarExtender = MakeShareable(new FExtender); 23 | ToolbarExtender->AddToolBarExtension("Asset", EExtensionHook::After, PluginCommands, FToolBarExtensionDelegate::CreateRaw(this, &FAssetTypeActions_AnimationCompressionLibraryDatabase::AddToolbarExtension, DatabaseAssets[0])); 24 | AssetEditor->AddToolbarExtender(ToolbarExtender); 25 | 26 | AssetEditor->RegenerateMenusAndToolbars(); 27 | } 28 | } 29 | 30 | void FAssetTypeActions_AnimationCompressionLibraryDatabase::AddToolbarExtension(FToolBarBuilder& Builder, TWeakObjectPtr DatabasePtr) 31 | { 32 | Builder.BeginSection("Build"); 33 | Builder.AddToolBarButton( 34 | FUIAction( 35 | FExecuteAction::CreateSP(this, &FAssetTypeActions_AnimationCompressionLibraryDatabase::ExecuteBuild, DatabasePtr) 36 | ), 37 | NAME_None, 38 | FText::FromString(TEXT("Build")), 39 | FText::FromString(TEXT("Builds the database from all the animation sequences that reference this database through their codec.")), 40 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 41 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "Persona.ApplyCompression") 42 | #else 43 | FSlateIcon(FEditorStyle::GetStyleSetName(), "Persona.ApplyCompression") 44 | #endif 45 | ); 46 | Builder.EndSection(); 47 | } 48 | 49 | void FAssetTypeActions_AnimationCompressionLibraryDatabase::GetActions(const TArray& InObjects, FMenuBuilder& MenuBuilder) 50 | { 51 | auto DatabaseAssets = GetTypedWeakObjectPtrs(InObjects); 52 | 53 | if (DatabaseAssets.Num() != 1) 54 | { 55 | return; 56 | } 57 | 58 | MenuBuilder.AddMenuEntry( 59 | FText::FromString(TEXT("Build")), 60 | FText::FromString(TEXT("Builds the database from all the animation sequences that reference this database through their codec.")), 61 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 62 | FSlateIcon(FAppStyle::GetAppStyleSetName(), "Persona.ApplyCompression.Small"), 63 | #else 64 | FSlateIcon(FEditorStyle::GetStyleSetName(), "Persona.ApplyCompression.Small"), 65 | #endif 66 | FUIAction( 67 | FExecuteAction::CreateSP(this, &FAssetTypeActions_AnimationCompressionLibraryDatabase::ExecuteBuild, DatabaseAssets[0]) 68 | ) 69 | ); 70 | } 71 | 72 | void FAssetTypeActions_AnimationCompressionLibraryDatabase::ExecuteBuild(TWeakObjectPtr DatabasePtr) 73 | { 74 | if (!DatabasePtr.IsValid()) 75 | { 76 | return; 77 | } 78 | 79 | UAnimationCompressionLibraryDatabase* Database = DatabasePtr.Get(); 80 | Database->UpdateReferencingAnimSequenceList(); 81 | } 82 | 83 | const TArray& FAssetTypeActions_AnimationCompressionLibraryDatabase::GetSubMenus() const 84 | { 85 | static const TArray SubMenus 86 | { 87 | NSLOCTEXT("AssetTypeActions_AnimationCompressionLibraryDatabase", "AnimAdvancedSubMenu", "Advanced") 88 | }; 89 | return SubMenus; 90 | } 91 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/AnimBoneCompressionCodec_ACL.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "AnimBoneCompressionCodec_ACL.h" 4 | 5 | #if WITH_EDITORONLY_DATA 6 | #include "Engine/SkeletalMesh.h" 7 | #include "Rendering/SkeletalMeshModel.h" 8 | 9 | #include "ACLImpl.h" 10 | 11 | THIRD_PARTY_INCLUDES_START 12 | #include 13 | #include 14 | #include 15 | THIRD_PARTY_INCLUDES_END 16 | 17 | #endif // WITH_EDITORONLY_DATA 18 | 19 | #include "ACLDecompressionImpl.h" 20 | 21 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 22 | #include UE_INLINE_GENERATED_CPP_BY_NAME(AnimBoneCompressionCodec_ACL) 23 | #endif 24 | 25 | UAnimBoneCompressionCodec_ACL::UAnimBoneCompressionCodec_ACL(const FObjectInitializer& ObjectInitializer) 26 | : Super(ObjectInitializer) 27 | #if WITH_EDITORONLY_DATA 28 | , bIsKeyframeStrippingSupported(!!ACL_WITH_KEYFRAME_STRIPPING) 29 | , KeyframeStrippingProportion(0.0f) // Strip nothing by default since it is destructive 30 | , KeyframeStrippingThreshold(0.0f) // Strip nothing by default since it is destructive 31 | #endif 32 | { 33 | } 34 | 35 | #if WITH_EDITORONLY_DATA 36 | void UAnimBoneCompressionCodec_ACL::GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const 37 | { 38 | OutSettings = acl::get_default_compression_settings(); 39 | 40 | OutSettings.level = GetCompressionLevel(CompressionLevel); 41 | 42 | #if ACL_WITH_KEYFRAME_STRIPPING 43 | OutSettings.keyframe_stripping.proportion = ACL::Private::GetPerPlatformFloat(KeyframeStrippingProportion, TargetPlatform); 44 | OutSettings.keyframe_stripping.threshold = ACL::Private::GetPerPlatformFloat(KeyframeStrippingThreshold, TargetPlatform); 45 | #endif 46 | } 47 | 48 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 49 | void UAnimBoneCompressionCodec_ACL::PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) 50 | #else 51 | void UAnimBoneCompressionCodec_ACL::PopulateDDCKey(FArchive& Ar) 52 | #endif 53 | { 54 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 55 | Super::PopulateDDCKey(KeyArgs, Ar); 56 | 57 | const class ITargetPlatform* TargetPlatform = KeyArgs.TargetPlatform; 58 | #else 59 | Super::PopulateDDCKey(Ar); 60 | 61 | const class ITargetPlatform* TargetPlatform = nullptr; 62 | #endif 63 | 64 | acl::compression_settings Settings; 65 | GetCompressionSettings(TargetPlatform, Settings); 66 | 67 | uint32 ForceRebuildVersion = 1; 68 | uint32 SettingsHash = Settings.get_hash(); 69 | 70 | Ar << ForceRebuildVersion << SettingsHash; 71 | 72 | for (USkeletalMesh* SkelMesh : OptimizationTargets) 73 | { 74 | FSkeletalMeshModel* MeshModel = SkelMesh != nullptr ? SkelMesh->GetImportedModel() : nullptr; 75 | if (MeshModel != nullptr) 76 | { 77 | Ar << MeshModel->SkeletalMeshModelGUID; 78 | } 79 | } 80 | } 81 | #endif // WITH_EDITORONLY_DATA 82 | 83 | void UAnimBoneCompressionCodec_ACL::DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const 84 | { 85 | const FACLCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 86 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 87 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 88 | 89 | acl::decompression_context ACLContext; 90 | ACLContext.initialize(*CompressedClipData); 91 | 92 | ::DecompressPose(DecompContext, ACLContext, RotationPairs, TranslationPairs, ScalePairs, OutAtoms); 93 | } 94 | 95 | void UAnimBoneCompressionCodec_ACL::DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const 96 | { 97 | const FACLCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 98 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 99 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 100 | 101 | acl::decompression_context ACLContext; 102 | ACLContext.initialize(*CompressedClipData); 103 | 104 | ::DecompressBone(DecompContext, ACLContext, TrackIndex, OutAtom); 105 | } 106 | 107 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Significant changes per release 2 | 3 | ## 2.1.0 4 | 5 | * Upgrade to ACL 2.1 6 | * Add support for bind pose stripping in UE 5.1+ 7 | * Add support for keyframe stripping in UE 5.1+ 8 | * Remove safety fallback codec, no longer required 9 | * Database support cleanup 10 | * Add support for raw data pre-processing 11 | * Add support for UE 5.3 12 | 13 | ## 2.0.8 14 | 15 | * Add support for UE 5.2 16 | * Fix edge case when skeleton bones are missing (validated) 17 | * Minor improvements 18 | 19 | ## 2.0.7 20 | 21 | * Add support for UE 5.1 22 | * Fix wrong enum usage in ACL Custom 23 | * Fix edge case when skeleton bones are missing (tentative) 24 | * Minor improvements 25 | 26 | ## 2.0.6 27 | 28 | * Add missing include 29 | 30 | ## 2.0.5 31 | 32 | * Add support for UE 5.0 33 | * Upgraded to ACL v2.0.4 34 | * Improve additive animation support 35 | 36 | ## 2.0.4 37 | 38 | * Fix rare potential use after free in database 39 | * Minor fixes to database blueprint handling 40 | * Improved logging 41 | 42 | ## 2.0.3 43 | 44 | * Fix potential crash with AVX2 due to incorrect allocation alignment 45 | * Upgraded to ACL v2.0.1 46 | 47 | ## 2.0.2 48 | 49 | * Fix assert when running memreport 50 | * Fix memreport reported usage for ACL 51 | * Fix crash when streaming in multiple database chunks 52 | 53 | ## 2.0.1 54 | 55 | * Add missing include for marketplace submission 56 | 57 | ## 2.0.0 58 | 59 | * Upgraded to ACL v2.0.0 60 | * Added support for streaming databases (code and blueprints) 61 | * Added ability to specify a skeletal mesh to improve accuracy 62 | * Fixed all MSVC static analysis warnings/issues 63 | * Other minor improvements and clean up 64 | 65 | ## 1.0.6 66 | 67 | * Add support for UE 4.26 68 | 69 | ## 1.0.5 70 | 71 | * Upgraded to ACL v1.3.5 72 | * Fix crash when more than 50000 frames are present by failing gracefully 73 | 74 | ## 1.0.4 75 | 76 | * Upgraded to ACL v1.3.4 77 | * Fix harmless assert when compressing a single frame additive sequence 78 | 79 | ## 1.0.3 80 | 81 | * Upgraded to ACL v1.3.3 82 | * Fix build when logging is disabled in non-shipping builds 83 | 84 | ## 1.0.2 85 | 86 | * Changes required to meet the UE4 marketplace guidelines 87 | * Added console commands to display animation codec usage statistics 88 | * Minor fixes 89 | 90 | ## 1.0.1 91 | 92 | * Fix crash when using the curve compression codec and no curves are present 93 | * Added ACL curve compression settings asset 94 | 95 | ## 1.0.0 96 | 97 | * Upgraded code to support UE 4.25 which can now be published on the Unreal Marketplace 98 | * Other minor changes to meet the Unreal Marketplace guidelines 99 | * Add support for AnimSequence curve compression 100 | * Upgraded to ACL v1.3.1 101 | 102 | ## 0.6.4 103 | 104 | * Upgraded to ACL v1.3.5 105 | * Fix crash when more than 50000 frames are present by failing gracefully 106 | 107 | ## 0.6.3 108 | 109 | * Upgraded to ACL v1.3.4 110 | * Fix harmless assert when compressing a single frame additive sequence 111 | 112 | ## 0.6.2 113 | 114 | * Upgraded to ACL v1.3.3 115 | 116 | ## 0.6.1 117 | 118 | * Fix crash when using the curve compression codec and no curves are present 119 | * Minor cleanup 120 | 121 | ## 0.6.0 122 | 123 | * Upgraded code to support UE 4.24 124 | * Add support for AnimSequence curve compression 125 | * Upgraded to ACL v1.3.1 126 | 127 | ## 0.5.0 128 | 129 | * Upgraded code to support UE 4.23 130 | * Upgraded to ACL v1.3.0 131 | * Added linear key reduction stat reporting 132 | * Other minor fixes and improvements 133 | 134 | ## 0.4.0 135 | 136 | * Upgraded code to support UE 4.22 137 | * Enable usage of popcount intrinsics 138 | * Other minor fixes and improvements 139 | 140 | ## 0.3.0 141 | 142 | * Upgraded to ACL v1.2.0 143 | * Added proper support for floating point sample rate 144 | * Added exposure to the new compression level 145 | * Invalidate the compressed data if the KeyEndEffectors array is changed 146 | * Other minor fixes and improvements 147 | 148 | ## 0.2.2 149 | 150 | * Fix additive animation sequences not being handled properly 151 | * Other minor fixes and changes 152 | 153 | ## 0.2.1 154 | 155 | * Fix linux cross-compilation issue 156 | 157 | ## 0.2.0 158 | 159 | * Upgrading to ACL v1.1.0 which offers very large decompression performance improvements 160 | * More decompression performance optimizations 161 | * The commandlet now allows you to dump stats from any local Unreal project 162 | 163 | ## 0.1.0 164 | 165 | * Initial release! 166 | * ACL v1.0.0 is fully supported as a drop in replacement for stock UE4 algorithms 167 | * Scripts and commandlets are provided for regression testing and metric extraction 168 | 169 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/AnimBoneCompressionCodec_ACLCustom.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "AnimBoneCompressionCodec_ACLCustom.h" 4 | 5 | #include "ACLDecompressionImpl.h" 6 | 7 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 8 | #include UE_INLINE_GENERATED_CPP_BY_NAME(AnimBoneCompressionCodec_ACLCustom) 9 | #endif 10 | 11 | #if WITH_EDITORONLY_DATA 12 | #include "Engine/SkeletalMesh.h" 13 | #include "Rendering/SkeletalMeshModel.h" 14 | 15 | THIRD_PARTY_INCLUDES_START 16 | #include 17 | THIRD_PARTY_INCLUDES_END 18 | #endif 19 | 20 | UAnimBoneCompressionCodec_ACLCustom::UAnimBoneCompressionCodec_ACLCustom(const FObjectInitializer& ObjectInitializer) 21 | : Super(ObjectInitializer) 22 | #if WITH_EDITORONLY_DATA 23 | , RotationFormat(ACLRotationFormat::ACLRF_QuatDropW_Variable) 24 | , TranslationFormat(ACLVectorFormat::ACLVF_Vector3_Variable) 25 | , ScaleFormat(ACLVectorFormat::ACLVF_Vector3_Variable) 26 | , bIsKeyframeStrippingSupported(!!ACL_WITH_KEYFRAME_STRIPPING) 27 | , KeyframeStrippingProportion(0.0f) // Strip nothing by default since it is destructive 28 | , KeyframeStrippingThreshold(0.0f) // Strip nothing by default since it is destructive 29 | #endif 30 | { 31 | } 32 | 33 | #if WITH_EDITORONLY_DATA 34 | void UAnimBoneCompressionCodec_ACLCustom::GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const 35 | { 36 | OutSettings = acl::compression_settings(); 37 | 38 | OutSettings.rotation_format = GetRotationFormat(RotationFormat); 39 | OutSettings.translation_format = GetVectorFormat(TranslationFormat); 40 | OutSettings.scale_format = GetVectorFormat(ScaleFormat); 41 | 42 | OutSettings.level = GetCompressionLevel(CompressionLevel); 43 | 44 | #if ACL_WITH_KEYFRAME_STRIPPING 45 | OutSettings.keyframe_stripping.proportion = ACL::Private::GetPerPlatformFloat(KeyframeStrippingProportion, TargetPlatform); 46 | OutSettings.keyframe_stripping.threshold = ACL::Private::GetPerPlatformFloat(KeyframeStrippingThreshold, TargetPlatform); 47 | #endif 48 | } 49 | 50 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 51 | void UAnimBoneCompressionCodec_ACLCustom::PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) 52 | #else 53 | void UAnimBoneCompressionCodec_ACLCustom::PopulateDDCKey(FArchive& Ar) 54 | #endif 55 | { 56 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 57 | Super::PopulateDDCKey(KeyArgs, Ar); 58 | 59 | const class ITargetPlatform* TargetPlatform = KeyArgs.TargetPlatform; 60 | #else 61 | Super::PopulateDDCKey(Ar); 62 | 63 | const class ITargetPlatform* TargetPlatform = nullptr; 64 | #endif 65 | 66 | acl::compression_settings Settings; 67 | GetCompressionSettings(TargetPlatform, Settings); 68 | 69 | uint32 ForceRebuildVersion = 1; 70 | uint32 SettingsHash = Settings.get_hash(); 71 | 72 | Ar << ForceRebuildVersion << SettingsHash; 73 | 74 | for (USkeletalMesh* SkelMesh : OptimizationTargets) 75 | { 76 | FSkeletalMeshModel* MeshModel = SkelMesh != nullptr ? SkelMesh->GetImportedModel() : nullptr; 77 | if (MeshModel != nullptr) 78 | { 79 | Ar << MeshModel->SkeletalMeshModelGUID; 80 | } 81 | } 82 | } 83 | #endif // WITH_EDITORONLY_DATA 84 | 85 | void UAnimBoneCompressionCodec_ACLCustom::DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const 86 | { 87 | const FACLCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 88 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 89 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 90 | 91 | acl::decompression_context ACLContext; 92 | ACLContext.initialize(*CompressedClipData); 93 | 94 | ::DecompressPose(DecompContext, ACLContext, RotationPairs, TranslationPairs, ScalePairs, OutAtoms); 95 | } 96 | 97 | void UAnimBoneCompressionCodec_ACLCustom::DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const 98 | { 99 | const FACLCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 100 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 101 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 102 | 103 | acl::decompression_context ACLContext; 104 | ACLContext.initialize(*CompressedClipData); 105 | 106 | ::DecompressBone(DecompContext, ACLContext, TrackIndex, OutAtom); 107 | } 108 | 109 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Classes/AnimBoneCompressionCodec_ACLDatabase.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "ACLImpl.h" 6 | 7 | THIRD_PARTY_INCLUDES_START 8 | #include 9 | THIRD_PARTY_INCLUDES_END 10 | 11 | #include "CoreMinimal.h" 12 | #include "UObject/ObjectMacros.h" 13 | #include "AnimationCompressionLibraryDatabase.h" 14 | #include "AnimBoneCompressionCodec_ACLBase.h" 15 | #include "AnimBoneCompressionCodec_ACLDatabase.generated.h" 16 | 17 | struct FACLDatabaseCompressedAnimData final : public ICompressedAnimData 18 | { 19 | /** Maps the compressed_tracks instance. Used in cooked build only. */ 20 | TArrayView CompressedByteStream; 21 | 22 | /** Maps the database context instance. Used in cooked build only. */ 23 | acl::database_context* DatabaseContext = nullptr; 24 | 25 | /** The codec instance that owns us. */ 26 | const class UAnimBoneCompressionCodec_ACLDatabase* Codec = nullptr; 27 | 28 | /** The sequence name hash that owns this data. */ 29 | uint32 SequenceNameHash = 0; 30 | 31 | #if WITH_EDITORONLY_DATA 32 | /** Holds the compressed_tracks instance for the anim sequence */ 33 | TArray CompressedClip; 34 | #endif 35 | 36 | #if WITH_EDITORONLY_DATA 37 | const acl::compressed_tracks* GetCompressedTracks() const { return acl::make_compressed_tracks(CompressedClip.GetData()); } 38 | #else 39 | const acl::compressed_tracks* GetCompressedTracks() const { return acl::make_compressed_tracks(CompressedByteStream.GetData()); } 40 | #endif 41 | 42 | // ICompressedAnimData implementation 43 | #if WITH_EDITORONLY_DATA 44 | virtual ~FACLDatabaseCompressedAnimData() override; 45 | #endif 46 | 47 | virtual void SerializeCompressedData(FArchive& Ar) override; 48 | virtual void Bind(const TArrayView BulkData) override; 49 | virtual int64 GetApproxCompressedSize() const override; 50 | virtual bool IsValid() const override; 51 | }; 52 | 53 | /** 54 | * Uses the open source Animation Compression Library with default settings and database support. 55 | * The referenced database can be used to strip the least important keyframes on a per platform basis 56 | * or they can be streamed in/out on demand through Blueprint or C++. 57 | */ 58 | UCLASS(MinimalAPI, config = Engine, meta = (DisplayName = "Anim Compress ACL Database")) 59 | class UAnimBoneCompressionCodec_ACLDatabase : public UAnimBoneCompressionCodec_ACLBase 60 | { 61 | GENERATED_UCLASS_BODY() 62 | 63 | /** The database asset that will hold the compressed animation data. */ 64 | UPROPERTY(EditAnywhere, Category = "ACL Options") 65 | UAnimationCompressionLibraryDatabase* DatabaseAsset; 66 | 67 | #if WITH_EDITORONLY_DATA 68 | /** The skeletal meshes used to estimate the skinning deformation during compression. */ 69 | UPROPERTY(EditAnywhere, Category = "ACL Options") 70 | TArray OptimizationTargets; 71 | 72 | ////////////////////////////////////////////////////////////////////////// 73 | // UObject 74 | virtual void GetPreloadDependencies(TArray& OutDeps) override; 75 | 76 | #if ENGINE_MAJOR_VERSION >= 5 77 | virtual void PreSave(FObjectPreSaveContext ObjectSaveContext) override; 78 | #else 79 | virtual void PreSave(const class ITargetPlatform* TargetPlatform) override; 80 | #endif 81 | 82 | // UAnimBoneCompressionCodec implementation 83 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 84 | virtual void PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) override; 85 | #else 86 | virtual void PopulateDDCKey(FArchive& Ar) override; 87 | #endif 88 | 89 | // UAnimBoneCompressionCodec_ACLBase implementation 90 | virtual void PostCompression(const FCompressibleAnimData& CompressibleAnimData, FCompressibleAnimDataResult& OutResult) const override; 91 | virtual void GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const override; 92 | virtual TArray GetOptimizationTargets() const override { return OptimizationTargets; } 93 | #endif 94 | 95 | // UAnimBoneCompressionCodec implementation 96 | virtual TUniquePtr AllocateAnimData() const override; 97 | virtual void ByteSwapIn(ICompressedAnimData& AnimData, TArrayView CompressedData, FMemoryReader& MemoryStream) const override; 98 | virtual void ByteSwapOut(ICompressedAnimData& AnimData, TArrayView CompressedData, FMemoryWriter& MemoryStream) const override; 99 | virtual void DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const override; 100 | virtual void DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const override; 101 | }; 102 | -------------------------------------------------------------------------------- /Docs/decompression_performance.md: -------------------------------------------------------------------------------- 1 | # Decompression performance 2 | 3 | ## AMD Ryzen 2950X @ 3.5 GHz (x64) 4 | 5 | ![Matinee Ryzen Median Performance](Images/acl_plugin_decomp_ryzen_matinee.svg) 6 | 7 | ![Playground Ryzen Median Performance](Images/acl_plugin_decomp_ryzen_playground.svg) 8 | 9 | **Note: ACL Plugin 2.0 numbers were gathered in UE 4.25. ACL 2.1 appears slightly slower than 2.0 most likely as a result of UE 5.x using double floating point precision in the output buffer.** 10 | 11 | ## Google Pixel 7 @ 2.85 GHz (arm64) 12 | 13 | ![Matinee P7 Median Performance](Images/acl_plugin_decomp_p7_matinee.svg) 14 | 15 | ![Playground P7 Median Performance](Images/acl_plugin_decomp_p7_playground.svg) 16 | 17 | ## Observations 18 | 19 | A number of interesting things can be seen in the above graphs. ACL is *much* faster. The variance is also much lower as decompression performance remains consistent from frame to frame. As a result, fewer spikes can be seen with ACL. 20 | 21 | The Matinee fight scene also shows how UE searches for keys when linearly interpolating. The keys are split into two and the codec estimates if our desired keys are more likely to be in the first half or the second half. It then starts searching from that end towards the other. In the wild, most sequences are very short and such a heuristic is a reasonable choice over a more traditional binary search. However, the fight scene is very long at 2000 frames! The graphs highlights how decompressing early or late in the sequence is much faster than near the middle. 22 | 23 | ## Data and method used 24 | 25 | Two scenarios are tracked: 26 | 27 | * [The Matinee fight scene](https://github.com/nfrechette/acl/blob/develop/docs/fight_scene_performance.md) 28 | * A [playground](./README.md#acl-plugin-playground) with every animation of the [animation starter pack](https://www.unrealengine.com/marketplace/animation-starter-pack) playing simultaneously (64 in total) 29 | 30 | All the performance metrics were extracted with a **Development** build. This might be a bit slower than the performance you might see under a **Shipping** configuration but not by much. 31 | 32 | Decompression performance is extracted using the UE CSV profiler. Reproducing the above numbers with your own projects should be fairly straight forward if you follow these steps. 33 | 34 | **Note: Matinee doesn't run in UE 5.x and thus data has been extracted using UE 4.25. When present, numbers for ACL Plugin 2.0 are from UE 4.25 as well.** 35 | 36 | ### Run your project and dump the stats 37 | 38 | Launch your cooked project with these specific command line arguments: `ACLPlayground.exe -nosound -execcmds="a.Budget.Enabled 0,a.ParallelAnimEvaluation 0" -csvcaptureframes=600 -deterministic -usefixedtimestep -fps=30 -csvCategories="Animation"`. 39 | 40 | For Windows, this executable is usually located here: `...\ACLPlayground\Saved\StagedBuilds\WindowsNoEditor`. 41 | 42 | The above command does a few things: 43 | 44 | * `-execcmds="a.Budget.Enabled 0,a.ParallelAnimEvaluation 0"` makes sure that the animation budget enforcing is disabled and that everything runs on a single thread to reduce noise 45 | * `-csvcaptureframes=600` captures 600 frames as soon as the scene finishes loading 46 | * `-deterministic -usefixedtimestep -fps=30` ensures our frame rate is stable at 30 FPS to keep profiling captures consistent 47 | * `-csvCategories="Animation"` this enables the *Animation* category for CSV profiling 48 | 49 | Once this is done and you have quit the application, you should see a new CSV file located here: `...\ACLPlayground\Saved\StagedBuilds\WindowsNoEditor\ACLPlayground\Saved\Profiling\CSV`. 50 | 51 | To get the above graphs, this process is ran twice: once with the default compression used by UE and once more with ACL. This will yield two CSV files for the next step. 52 | 53 | ### CSV post-process 54 | 55 | Because the animation decompression code can run on any worker thread (including the main thread), our stats end up split in multiple columns (one per thread). The stats we care about take the form: `Animation/Thread Name/ExtractPoseFromAnimData`. A [small python script](../ACLPlugin/Extras/tally_anim_csv.py) is used to accumulate the sum total of each worker thread into a final column. 56 | 57 | Simply run it like so on all your input CSVs: `python tally_anim_csv.py `. 58 | 59 | ### Generate the graph 60 | 61 | Once the CSV files are clean, it is time to transform them into our final graph. UE conveniently packages a tool that can parse the profiling CSV files and generates SVG graphs: `CSVToSVG`. 62 | 63 | It is located here: `...\Engine\Binaries\DotNET\CsvTools`. 64 | 65 | You can run it with the following arguments: `CSVToSVG.exe -csvdir "" -stats "Animation/Total/ExtractPoseFromAnimData" -o "" -showaverages -nocommandlineEmbed -title "Awesome Stats (us/frame)" -noMetadata -budget 1000 -statMultiplier 1000`. 66 | 67 | Those arguments do a few things: 68 | 69 | * `-csvdir ""` points to the directory that contains all the profiling CSV files 70 | * `-stats "Animation/Total/ExtractPoseFromAnimData"` this instructs the tool to chart the column with that name, the one generated by the python script 71 | * `-o ""` points to our output SVG graph file 72 | * `-showaverages` adds average information next to the legend 73 | * `-nocommandlineEmbed` removes metadata display 74 | * `-title "Awesome Stats (us/frame)"` sets the title for our graph 75 | * `-noMetadata` removes more metadata display 76 | * `-budget 1000` moves the dotted green *budget* line automatically added out of the graph view to hide it 77 | * `-statMultiplier 1000` converts the performance numbers from *milliseconds* into *microseconds* 78 | 79 | Most web browsers are then able to render and display the generated graph. 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CLA assistant](https://cla-assistant.io/readme/badge/nfrechette/acl-ue4-plugin)](https://cla-assistant.io/nfrechette/acl-ue4-plugin) 2 | [![All Contributors](https://img.shields.io/github/all-contributors/nfrechette/acl-ue4-plugin)](#contributors-) 3 | [![GitHub release](https://img.shields.io/github/release/nfrechette/acl-ue4-plugin.svg)](https://github.com/nfrechette/acl-ue4-plugin/releases) 4 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nfrechette/acl-ue4-plugin/master/LICENSE) 5 | ![UE](https://img.shields.io/badge/UE-4.25+-orange) 6 | [![Discord](https://img.shields.io/discord/691048241864769647?label=discord)](https://discord.gg/UERt4bS) 7 | 8 | # IMPORTANT NOTICE 9 | 10 | As of Unreal Engine 5.3, this plugin comes out of the box and is distributed and maintained by Epic in their fork. See [here](https://nfrechette.github.io/2023/09/17/acl_in_ue/) for details. 11 | 12 | This repository will remain mostly read-only following the next minor release and only accept critical fixes. Contributions should instead be directed to the Unreal Engine main development repository where the ACL Plugin fork lives (under **Engine/Plugins/Animation/ACLPlugin**). 13 | 14 | Support questions and requests should be directed to the Unreal Developer Network (UDN). 15 | 16 | ==================================================== 17 | 18 | # Animation Compression Library Unreal Engine Plugin 19 | 20 | This plugin integrates the [Animation Compression Library](https://github.com/nfrechette/acl) version [v2.1.0](https://github.com/nfrechette/acl/releases/tag/v2.1.0) within [Unreal Engine 4 and 5](https://www.unrealengine.com/en-US/blog). It is suitable for every animation clip and platform as it features a low memory footprint, high accuracy, and very fast compression and decompression. 21 | 22 | Compared to **UE 5.2.0**, the ACL plugin compresses up to **2.9x smaller**, is up to **2x more accurate**, up to **2x faster to compress**, and up to **6.3x faster to decompress** (results may vary depending on the platform and data). 23 | 24 | The documentation on how to use it can be found [here](./Docs/README.md) along with performance results. 25 | 26 | You can find the ACL plugin on the Unreal Marketplace [here](https://www.unrealengine.com/marketplace/en-US/product/animation-compression-library). 27 | 28 | ## Getting started 29 | 30 | If you would like to contribute to the ACL UE Plugin, make sure to check out the [contributing guidelines](CONTRIBUTING.md). 31 | 32 | **NOTE: If you clone this repository, you MUST run the script under [.\Tools\prepare_release.py](./Tools/prepare_release.py) to package the plugin. It pre-processes some required files.** 33 | 34 | ## License, copyright, and code of conduct 35 | 36 | This project uses the [MIT license](LICENSE). 37 | 38 | Copyright (c) 2018 Nicholas Frechette 39 | 40 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 41 | 42 | ## Contributors ✨ 43 | 44 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
Meradrin
Meradrin

💻 📦
r-lyeh
r-lyeh

📖
nucleiis
nucleiis

🐛
fjoanisPhxlabs
fjoanisPhxlabs

💻
EAirPeter
EAirPeter

💻
pmsimardeidos
pmsimardeidos

💻 🐛
61 | 62 | 63 | 64 | 65 | 66 | 67 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 68 | -------------------------------------------------------------------------------- /Docs/error_measurements.md: -------------------------------------------------------------------------------- 1 | # Error measurements 2 | 3 | Both UE and ACL measure the animation compression error differently. Although both approaches are very similar and mostly agree for the vast majority of animation sequences, some exotic sequences can end up with a very large disagreement between the two. Subtle differences in both approaches can compound to yield very different measurements. Three differences stand out: how the error is measured, where the error is measured in space, and where the error is measured in time. 4 | 5 | ## How the error is measured 6 | 7 | While both UE and ACL use [virtual vertices](https://nfrechette.github.io/2016/11/01/anim_compression_accuracy/) in order to approximate the visual mesh, how they do so differs slightly: ACL uses **three** virtual vertices on *every* bone and UE uses a **single** virtual vertex on *leaf* bones. Due to the hierarchical nature of skeleton based animation data, testing leaf bones is a pretty good approximation but ACL goes one step further and tests every bone regardless. It does this because each bone might have its virtual vertices at different distances. In this respect, ACL is more conservative. 8 | 9 | ## Where the error is measured in space 10 | 11 | As previously mentioned, ACL uses **three** virtual vertices while UE uses just **one**. UE constructs this vertex with all three vector components equal to one of two values: **5cm** or **50cm**. Which value is used depends on whether the bone has a socket or is considered a *key end effector* (from the list in *UAnimationSettings::KeyEndEffectorsMatchNameArray*): regular bones use the lower value while these special bones use the higher value. Because all three components are equal, the virtual vertex distance is actually **8.66cm** or **86.6cm**. 12 | 13 | If our bone transform is made entirely of translation then that is sufficient. However, most bones will end up with some amount of rotation either directly animated or as a result of a parent bone contribution. As the rotation axis approaches our virtual vertex, the measured error will decrease in accuracy to the point where if they are colinear, the error contribution from the rotation will become entirely invisible: a point that lies on a rotation axis is never transformed by that rotation. In a worst case scenario, the rotation error could be infinite and we wouldn't be able to tell. For that reason, a single virtual vertex isn't sufficient. 14 | 15 | If our bone transform is made entirely of translation and rotation then using two perpendicular virtual vertices is good enough: even if one is entirely colinear with our rotation axis, the other will be fully perpendicular and register the error properly. At worst, if the rotation axis lies in between, neither will be too close to lose precision. To measure our error, we transform both vertices and measure their respective error in order to retain the maximum value or the two. 16 | 17 | However, once 3D scale enters the picture, two virtual vertices are no longer sufficient. As our scale approaches zero along an axis, the error it contributes will diminish to the point where it vanishes entirely at zero. Our two vertices form a plane. If one axis collapses to zero, that plane now folds into a line. Once our second axis collapses to zero, our line now becomes a point at zero. This makes the error contributed by our third axis entirely invisible. In order to avoid this issue, ACL uses a third perpendicular virtual vertex: as long as one axis has non-zero scale, its error will be visible. 18 | 19 | The ACL virtual vertices use slightly different values to determine the accuracy. They either have a distance of **100cm** for key end effector bones or a distance of **3cm** for ordinary bones. It uses a lower value for ordinary bones because its error function is more conservative but a higher value for the special bones just in case. 20 | 21 | ## Where the error is measured in time 22 | 23 | UE stores its raw animation sequence data as uniform samples. A fixed sample rate dictates where each sample lies in time. While some of the UE codecs perform some form of sample (or key) reduction when they can be linearly interpolated from their neighbors, ACL retains every sample. Both UE and ACL use linear interpolation when sampling in between existing samples. As a result, it is sufficient to measure the error on the original raw samples in order to accurately measure the error: only two samples ever contribute to our final value and they both contribute proportionally. 24 | 25 | To do so, UE iterates over every sample and it calculates the sample time from the index and the sampling rate. This is done because it does not support sampling an individual sample directly in compressed form. However, due to floating point rounding we might end up with a small interpolation factor. More often than not this is good enough an approximation but in rare cases where the samples we interpolate are very far from one another, the reconstructed sample might lie quite far from the one we really wanted. Codecs that remove samples further complicate things because the interpolation factor calculated might differ slightly in raw and compressed form even if both samples around the one we want are retained. Last but not least, compiler optimizations such as *fast math* can also alter how the interpolation alpha is calculated in different pieces of code which can lead them to measure the error differently at the same sample time. 26 | 27 | In order to have the most accurate view possible of the error, ACL does not perform any form of interpolation either in the raw or compressed samples. This is easily achieved because its sampling function supports a rounding mode: floor (sample that immediately precedes our sample time), ceil (sample that immediately follows our sample time), nearest (closest sample to our sample time), and none (linear interpolation between our two samples). The error is always measured with the nearest sample to the sample time we care about. 28 | 29 | By removing interpolation from the picture, ACL ensures a consistent and accurate view of the compression error. 30 | 31 | ## Who to trust? 32 | 33 | If in doubt, it is best to trust the ACL error measurement. It is more accurate and very conservative. Unfortunately it isn't visible in the editor UI when compression is performed. In order to see it when compressing an animation sequence, simply switch to **verbose** the logging category for animation compression: `log LogAnimationCompression verbose` (in the console). If the error reported by ACL is unusually high, then it is possible that a bug was found and you are encouraged to log an issue and/or reach out. Note that the first thing I will ask if you report accuracy issues is what is the ACL reported error. 34 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPluginEditor/Private/ACLDatabaseBuildCommandlet.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "ACLDatabaseBuildCommandlet.h" 4 | 5 | #include "AnimationCompressionLibraryDatabase.h" 6 | 7 | #include "AnimationCompression.h" 8 | #include "AnimationUtils.h" 9 | #include "FileHelpers.h" 10 | #include "ISourceControlModule.h" 11 | #include "SourceControlOperations.h" 12 | 13 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 14 | #include "AssetRegistry/AssetRegistryModule.h" 15 | #else 16 | #include "AssetRegistryModule.h" 17 | #endif 18 | 19 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 20 | #include UE_INLINE_GENERATED_CPP_BY_NAME(ACLDatabaseBuildCommandlet) 21 | #endif 22 | 23 | 24 | ////////////////////////////////////////////////////////////////////////// 25 | // Commandlet example inspired by: https://github.com/ue4plugins/CommandletPlugin 26 | // To run the commandlet, add to the commandline: "$(SolutionDir)$(ProjectName).uproject" -run=/Script/ACLPluginEditor.ACLDatabaseBuild 27 | 28 | ////////////////////////////////////////////////////////////////////////// 29 | 30 | UACLDatabaseBuildCommandlet::UACLDatabaseBuildCommandlet(const FObjectInitializer& ObjectInitializer) 31 | : Super(ObjectInitializer) 32 | { 33 | IsClient = false; 34 | IsServer = false; 35 | IsEditor = true; 36 | LogToConsole = true; 37 | ShowErrorCount = true; 38 | } 39 | 40 | int32 UACLDatabaseBuildCommandlet::Main(const FString& Params) 41 | { 42 | TArray Tokens; 43 | TArray Switches; 44 | TMap ParamsMap; 45 | UCommandlet::ParseCommandLine(*Params, Tokens, Switches, ParamsMap); 46 | 47 | const FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")); 48 | 49 | TArray DatabaseAssets; 50 | { 51 | UE_LOG(LogAnimationCompression, Log, TEXT("Retrieving all ACL databases from current project ...")); 52 | 53 | FARFilter DatabaseFilter; 54 | 55 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 56 | DatabaseFilter.ClassPaths.Add(UAnimationCompressionLibraryDatabase::StaticClass()->GetClassPathName()); 57 | #else 58 | DatabaseFilter.ClassNames.Add(UAnimationCompressionLibraryDatabase::StaticClass()->GetFName()); 59 | #endif 60 | 61 | AssetRegistryModule.Get().GetAssets(DatabaseFilter, DatabaseAssets); 62 | } 63 | 64 | if (DatabaseAssets.Num() == 0) 65 | { 66 | UE_LOG(LogAnimationCompression, Log, TEXT("Failed to find any ACL databases, done")); 67 | return 0; 68 | } 69 | 70 | TArray AnimSequenceAssets; 71 | { 72 | UE_LOG(LogAnimationCompression, Log, TEXT("Retrieving all animation sequences from current project ...")); 73 | 74 | FARFilter AnimSequenceFilter; 75 | 76 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 77 | AnimSequenceFilter.ClassPaths.Add(UAnimSequence::StaticClass()->GetClassPathName()); 78 | #else 79 | AnimSequenceFilter.ClassNames.Add(UAnimSequence::StaticClass()->GetFName()); 80 | #endif 81 | 82 | AssetRegistryModule.Get().GetAssets(AnimSequenceFilter, AnimSequenceAssets); 83 | } 84 | 85 | if (AnimSequenceAssets.Num() == 0) 86 | { 87 | UE_LOG(LogAnimationCompression, Log, TEXT("Failed to find any animation sequences, done")); 88 | return 0; 89 | } 90 | 91 | { 92 | UE_LOG(LogAnimationCompression, Log, TEXT("Loading %u animation sequences ..."), AnimSequenceAssets.Num()); 93 | for (const FAssetData& Asset : AnimSequenceAssets) 94 | { 95 | UAnimSequence* AnimSeq = Cast(Asset.GetAsset()); 96 | if (AnimSeq == nullptr) 97 | { 98 | UE_LOG(LogAnimationCompression, Log, TEXT("Failed to load animation sequence: %s"), *Asset.PackagePath.ToString()); 99 | continue; 100 | } 101 | 102 | // Make sure all our required dependencies are loaded 103 | FAnimationUtils::EnsureAnimSequenceLoaded(*AnimSeq); 104 | } 105 | } 106 | 107 | { 108 | UE_LOG(LogAnimationCompression, Log, TEXT("Loading %u ACL databases ..."), DatabaseAssets.Num()); 109 | for (const FAssetData& Asset : DatabaseAssets) 110 | { 111 | UAnimationCompressionLibraryDatabase* Database = Cast(Asset.GetAsset()); 112 | if (Database == nullptr) 113 | { 114 | UE_LOG(LogAnimationCompression, Log, TEXT("Failed to load ACL database: %s"), *Asset.PackagePath.ToString()); 115 | continue; 116 | } 117 | } 118 | } 119 | 120 | TArray DirtyDatabasePackages; 121 | { 122 | for (const FAssetData& Asset : DatabaseAssets) 123 | { 124 | UE_LOG(LogAnimationCompression, Log, TEXT("Building mapping for ACL database: %s ..."), *Asset.PackagePath.ToString()); 125 | 126 | UAnimationCompressionLibraryDatabase* Database = Cast(Asset.GetAsset()); 127 | if (Database == nullptr) 128 | { 129 | continue; 130 | } 131 | 132 | const bool bIsDirty = Database->UpdateReferencingAnimSequenceList(); 133 | if (bIsDirty) 134 | { 135 | UE_LOG(LogAnimationCompression, Log, TEXT(" Mapping updated!")); 136 | DirtyDatabasePackages.Add(Asset.GetPackage()); 137 | } 138 | } 139 | } 140 | 141 | bool bFailedToSave = false; 142 | { 143 | if (ISourceControlModule::Get().IsEnabled()) 144 | { 145 | ISourceControlProvider& SourceControlProvider = ISourceControlModule::Get().GetProvider(); 146 | 147 | TArray PackagesToSave; 148 | for (UPackage* Package : DirtyDatabasePackages) 149 | { 150 | FSourceControlStatePtr SourceControlState = SourceControlProvider.GetState(Package, EStateCacheUsage::Use); 151 | if (SourceControlState->IsCheckedOutOther()) 152 | { 153 | UE_LOG(LogAnimationCompression, Warning, TEXT("Package %s is already checked out by someone, will not check out"), *SourceControlState->GetFilename()); 154 | } 155 | else if (!SourceControlState->IsCurrent()) 156 | { 157 | UE_LOG(LogAnimationCompression, Warning, TEXT("Package %s is not at head, will not check out"), *SourceControlState->GetFilename()); 158 | } 159 | else if (SourceControlState->CanCheckout()) 160 | { 161 | const ECommandResult::Type StatusResult = SourceControlProvider.Execute(ISourceControlOperation::Create(), Package); 162 | if (StatusResult != ECommandResult::Succeeded) 163 | { 164 | UE_LOG(LogAnimationCompression, Log, TEXT("Package %s failed to check out"), *SourceControlState->GetFilename()); 165 | bFailedToSave = true; 166 | } 167 | else 168 | { 169 | PackagesToSave.Add(Package); 170 | } 171 | } 172 | else if (!SourceControlState->IsSourceControlled() || SourceControlState->CanEdit()) 173 | { 174 | PackagesToSave.Add(Package); 175 | } 176 | } 177 | 178 | UEditorLoadingAndSavingUtils::SavePackages(PackagesToSave, true); 179 | ISourceControlModule::Get().QueueStatusUpdate(PackagesToSave); 180 | } 181 | else 182 | { 183 | // No source control, just try to save what we have 184 | UEditorLoadingAndSavingUtils::SavePackages(DirtyDatabasePackages, true); 185 | } 186 | } 187 | 188 | return bFailedToSave ? 1 : 0; 189 | } 190 | 191 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/UEDatabaseStreamer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "AnimationCompression.h" 6 | #include "CoreMinimal.h" 7 | #include "HAL/UnrealMemory.h" 8 | 9 | #include "ACLImpl.h" 10 | #include "Serialization/BulkData.h" 11 | 12 | THIRD_PARTY_INCLUDES_START 13 | #include 14 | THIRD_PARTY_INCLUDES_END 15 | 16 | // UE 4.25 doesn't expose its virtual memory management, see FPlatformMemory::FPlatformVirtualMemoryBlock 17 | #define WITH_VMEM_MANAGEMENT 0 18 | 19 | /** A simple async UE streamer. Memory is allocated on the first stream in request and deallocated on the last stream out request. */ 20 | class UEDatabaseStreamer final : public acl::database_streamer 21 | { 22 | public: 23 | UEDatabaseStreamer(const acl::compressed_database& CompressedDatabase, FByteBulkData& StreamableBulkData_) 24 | : database_streamer(Requests, acl::k_num_database_tiers) 25 | , StreamableBulkData(StreamableBulkData_) 26 | , PendingIORequest(nullptr) 27 | { 28 | BulkData[0] = BulkData[1] = nullptr; 29 | 30 | const uint32 BulkDataMediumSize = CompressedDatabase.get_bulk_data_size(acl::quality_tier::medium_importance); 31 | const uint32 BulkDataLowSize = CompressedDatabase.get_bulk_data_size(acl::quality_tier::lowest_importance); 32 | 33 | BulkDataSize[0] = BulkDataMediumSize; 34 | BulkDataSize[1] = BulkDataLowSize; 35 | 36 | #if WITH_VMEM_MANAGEMENT 37 | // Allocate but don't commit the memory until we need it 38 | // TODO: Commit right away if requested 39 | 40 | StreamedBulkDataBlock = FPlatformMemory::FPlatformVirtualMemoryBlock::AllocateVirtual(BulkDataTotalSize); 41 | BulkDataPtr = static_cast(StreamedBulkDataBlock.GetVirtualPointer()); 42 | bIsBulkDataCommited = false; 43 | #endif 44 | } 45 | 46 | virtual ~UEDatabaseStreamer() 47 | { 48 | // If we have a stream in request, wait for it to complete and clear it 49 | WaitForStreamingToComplete(); 50 | 51 | #if WITH_VMEM_MANAGEMENT 52 | StreamedBulkDataBlock.FreeVirtual(); 53 | #else 54 | delete[] BulkData[0]; 55 | delete[] BulkData[1]; 56 | #endif 57 | } 58 | 59 | virtual bool is_initialized() const override { return true; } 60 | 61 | virtual const uint8_t* get_bulk_data(acl::quality_tier Tier) const override 62 | { 63 | checkf(Tier != acl::quality_tier::highest_importance, TEXT("Unexpected quality tier")); 64 | const uint32 TierIndex = uint32(Tier) - 1; 65 | return BulkData[TierIndex]; 66 | } 67 | 68 | virtual void stream_in(uint32_t Offset, uint32_t Size, bool CanAllocateBulkData, acl::quality_tier Tier, acl::streaming_request_id RequestID) override 69 | { 70 | const uint32 TierIndex = uint32(Tier) - 1; 71 | const uint32 TierBulkDataSize = BulkDataSize[TierIndex]; 72 | 73 | checkf(Offset < TierBulkDataSize, TEXT("Steam offset is outside of the bulk data range")); 74 | checkf(Size <= TierBulkDataSize, TEXT("Stream size is larger than the bulk data size")); 75 | checkf(uint64(Offset) + uint64(Size) <= uint64(TierBulkDataSize), TEXT("Streaming request is outside of the bulk data range")); 76 | checkf(Tier != acl::quality_tier::highest_importance, TEXT("Unexpected quality tier")); 77 | 78 | // If we already did a stream in request, wait for it to complete and clear it 79 | WaitForStreamingToComplete(); 80 | 81 | FBulkDataIORequestCallBack AsyncFileCallBack = [this, RequestID](bool bWasCancelled, IBulkDataIORequest* Req) 82 | { 83 | UE_LOG(LogAnimationCompression, Log, TEXT("ACL completed the stream in request!")); 84 | 85 | // Tell ACL whether the streaming request was a success or not, this is thread safe 86 | if (bWasCancelled) 87 | this->cancel(RequestID); 88 | else 89 | this->complete(RequestID); 90 | }; 91 | 92 | UE_LOG(LogAnimationCompression, Log, TEXT("ACL starting a new stream in request!")); 93 | 94 | // Allocate our bulk data buffer on the first stream in request 95 | if (CanAllocateBulkData) 96 | { 97 | UE_LOG(LogAnimationCompression, Log, TEXT("ACL is allocating the database bulk data!")); 98 | 99 | #if WITH_VMEM_MANAGEMENT 100 | check(!bIsBulkDataCommited); 101 | StreamedBulkDataBlock.Commit(); 102 | bIsBulkDataCommited = true; 103 | #else 104 | check(BulkData[TierIndex] == nullptr); 105 | BulkData[TierIndex] = new uint8[TierBulkDataSize]; 106 | #endif 107 | } 108 | 109 | // Fire off our async streaming request 110 | const uint32 BulkDataTierStartOffset = Tier == acl::quality_tier::medium_importance ? 0 : BulkDataSize[0]; 111 | const uint64 BulkDataReadOffset = BulkDataTierStartOffset + Offset; 112 | 113 | uint8* StreamedBulkDataPtr = BulkData[TierIndex]; 114 | uint8* DestBulkDataPtr = StreamedBulkDataPtr + Offset; 115 | 116 | PendingIORequest = StreamableBulkData.CreateStreamingRequest(BulkDataReadOffset, Size, AIOP_Low, &AsyncFileCallBack, DestBulkDataPtr); 117 | if (PendingIORequest == nullptr) 118 | { 119 | UE_LOG(LogAnimationCompression, Warning, TEXT("ACL failed to initiate database stream in request!")); 120 | cancel(RequestID); 121 | } 122 | } 123 | 124 | virtual void stream_out(uint32_t Offset, uint32_t Size, bool CanDeallocateBulkData, acl::quality_tier Tier, acl::streaming_request_id RequestID) override 125 | { 126 | const uint32 TierIndex = uint32(Tier) - 1; 127 | const uint32 TierBulkDataSize = BulkDataSize[TierIndex]; 128 | 129 | checkf(Offset < TierBulkDataSize, TEXT("Steam offset is outside of the bulk data range")); 130 | checkf(Size <= TierBulkDataSize, TEXT("Stream size is larger than the bulk data size")); 131 | checkf(uint64(Offset) + uint64(Size) <= uint64(TierBulkDataSize), TEXT("Streaming request is outside of the bulk data range")); 132 | checkf(Tier != acl::quality_tier::highest_importance, TEXT("Unexpected quality tier")); 133 | 134 | // If we already did a stream in request, wait for it to complete and clear it 135 | WaitForStreamingToComplete(); 136 | 137 | UE_LOG(LogAnimationCompression, Log, TEXT("ACL is streaming out a database!")); 138 | 139 | // Free our bulk data on the last stream out request 140 | if (CanDeallocateBulkData) 141 | { 142 | // TODO: Make this optional? 143 | UE_LOG(LogAnimationCompression, Log, TEXT("ACL is deallocating the database bulk data!")); 144 | 145 | #if WITH_VMEM_MANAGEMENT 146 | check(bIsBulkDataCommited); 147 | StreamedBulkDataBlock.Decommit(); 148 | bIsBulkDataCommited = false; 149 | #else 150 | check(BulkData[TierIndex] != nullptr); 151 | delete[] BulkData[TierIndex]; 152 | BulkData[TierIndex] = nullptr; 153 | #endif 154 | } 155 | 156 | // Notify ACL that we streamed out the data, this is not thread safe and cannot run while animations are decompressing 157 | complete(RequestID); 158 | } 159 | 160 | void WaitForStreamingToComplete() 161 | { 162 | if (PendingIORequest != nullptr) 163 | { 164 | verify(PendingIORequest->WaitCompletion()); 165 | delete PendingIORequest; 166 | PendingIORequest = nullptr; 167 | } 168 | } 169 | 170 | private: 171 | UEDatabaseStreamer(const UEDatabaseStreamer&) = delete; 172 | UEDatabaseStreamer& operator=(const UEDatabaseStreamer&) = delete; 173 | 174 | FByteBulkData& StreamableBulkData; 175 | uint8* BulkData[acl::k_num_database_tiers]; 176 | uint32 BulkDataSize[acl::k_num_database_tiers]; 177 | 178 | IBulkDataIORequest* PendingIORequest; 179 | 180 | acl::streaming_request Requests[acl::k_num_database_tiers]; // One request per tier is enough 181 | 182 | #if WITH_VMEM_MANAGEMENT 183 | FPlatformMemory::FPlatformVirtualMemoryBlock StreamedBulkDataBlock; 184 | bool bIsBulkDataCommited; 185 | #endif 186 | }; 187 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Classes/AnimationCompressionLibraryDatabase.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "ACLImpl.h" 6 | 7 | THIRD_PARTY_INCLUDES_START 8 | #include 9 | #include 10 | THIRD_PARTY_INCLUDES_END 11 | 12 | #include "CoreMinimal.h" 13 | #include "Containers/Ticker.h" 14 | #include "PerPlatformProperties.h" 15 | #include "Engine/LatentActionManager.h" 16 | #include "Serialization/BulkData.h" 17 | #include "UObject/ObjectMacros.h" 18 | #include "AnimationCompressionLibraryDatabase.generated.h" 19 | 20 | // Aliases to work with tickers 21 | #if ENGINE_MAJOR_VERSION >= 5 22 | using FTickerType = FTSTicker; 23 | using FTickerDelegateHandleType = FTSTicker::FDelegateHandle; 24 | #else 25 | using FTickerType = FTicker; 26 | using FTickerDelegateHandleType = FDelegateHandle; 27 | #endif 28 | 29 | /** An enum to represent the ACL visual fidelity level. */ 30 | UENUM() 31 | enum class ACLVisualFidelity : uint8 32 | { 33 | Highest UMETA(DisplayName = "Highest"), 34 | Medium UMETA(DisplayName = "Medium"), 35 | Lowest UMETA(DisplayName = "Lowest"), 36 | }; 37 | 38 | /** An enum to represent the result of latent visual fidelity change requests. */ 39 | UENUM(BlueprintType) 40 | enum class ACLVisualFidelityChangeResult : uint8 41 | { 42 | Dispatched, 43 | Completed, 44 | Failed, 45 | }; 46 | 47 | /** Represents a pending visual fidelity change request */ 48 | struct FFidelityChangeRequest 49 | { 50 | ACLVisualFidelityChangeResult* Result; // Optional, present when triggered from a blueprint 51 | 52 | uint32 RequestID; 53 | ACLVisualFidelity Fidelity; 54 | bool bIsInProgress; 55 | }; 56 | 57 | /** An ACL database object references several UAnimSequence instances that it contains. */ 58 | UCLASS(MinimalAPI, config = Engine, meta = (DisplayName = "ACL Database")) 59 | class UAnimationCompressionLibraryDatabase : public UObject 60 | { 61 | GENERATED_UCLASS_BODY() 62 | 63 | private: 64 | /** The raw binary data for our compressed database and anim sequences. Present only in cooked builds. */ 65 | UPROPERTY() 66 | TArray CookedCompressedBytes; 67 | 68 | /** Stores a mapping for each anim sequence, where its compressed data lives in our compressed buffer. Each 64 bit value is split into 32 bits: (Hash << 32) | Offset. Present only in cooked builds. */ 69 | UPROPERTY() 70 | TArray CookedAnimSequenceMappings; 71 | 72 | /** Bulk data that we'll stream. Present only in cooked builds. */ 73 | FByteBulkData CookedBulkData; 74 | 75 | /** The database decompression context object. Bound to the compressed database instance. */ 76 | acl::database_context DatabaseContext; 77 | 78 | /** The streamer instance used by the database context. Only used in cooked builds. */ 79 | TUniquePtr DatabaseStreamer; 80 | 81 | /** The current visual fidelity level. */ 82 | ACLVisualFidelity CurrentVisualFidelity; 83 | 84 | /** The next fidelity change request ID. Always increments. */ 85 | uint32 NextFidelityChangeRequestID; 86 | 87 | /** A queue of visual fidelity change requests. */ 88 | TArray FidelityChangeRequests; 89 | 90 | /** The handle to the fidelity update ticket. */ 91 | FTickerDelegateHandleType FidelityUpdateTickerHandle; 92 | 93 | #if WITH_EDITORONLY_DATA 94 | /** What percentage of the key frames should remain in the anim sequences. */ 95 | UPROPERTY(VisibleAnywhere, Category = "Database") 96 | float HighestImportanceProportion; 97 | 98 | /** What percentage of the key frames should be moved to the database. Medium importance key frames are moved second. */ 99 | UPROPERTY(EditAnywhere, Category = "Database", meta = (ClampMin = "0", ClampMax = "1")) 100 | float MediumImportanceProportion; 101 | 102 | /** What percentage of the key frames should be moved to the database. Least important key frames are moved first. */ 103 | UPROPERTY(EditAnywhere, Category = "Database", meta = (ClampMin = "0", ClampMax = "1")) 104 | float LowestImportanceProportion; 105 | 106 | /** Whether or not to strip the lowest importance tier entirely from disk. Stripping the lowest tier means that the visual fidelity of Highest and Medium are equivalent. */ 107 | UPROPERTY(EditAnywhere, Category = "Database") 108 | FPerPlatformBool StripLowestImportanceTier; 109 | #endif 110 | 111 | /** The maximum size in KiloBytes of streaming requests. Setting this to 0 will force tiers to load in a single request regardless of their size. */ 112 | UPROPERTY(EditAnywhere, Category = "Database", meta = (ClampMin = "4", ClampMax = "1048576")) 113 | uint32 MaxStreamRequestSizeKB; 114 | 115 | /** The default level of quality to set when the database loads in-game. By default, nothing is streamed in. */ 116 | UPROPERTY(EditAnywhere, Category = "Database") 117 | ACLVisualFidelity DefaultVisualFidelity; 118 | 119 | #if WITH_EDITORONLY_DATA 120 | /** The level of quality to preview with the database when decompressing in the editor. */ 121 | UPROPERTY(EditAnywhere, Transient, Category = "Debug") 122 | ACLVisualFidelity PreviewVisualFidelity; 123 | 124 | /** Editor only, transient, preview version of 'CookedCompressedBytes'. */ 125 | TArray PreviewCompressedBytes; 126 | 127 | /** Editor only, transient, preview version of 'CookedAnimSequenceMappings'. */ 128 | TArray PreviewAnimSequenceMappings; 129 | 130 | /** Editor only, transient, preview version of 'CookedBulkData'. */ 131 | TArray PreviewBulkData; 132 | 133 | /** Editor only, transient, preview version of 'DatabaseStreamer'. */ 134 | TUniquePtr PreviewDatabaseStreamer; 135 | 136 | /** The anim sequences contained within the database. Built manually from the asset UI, content browser, or with a commandlet. */ 137 | UPROPERTY(VisibleAnywhere, Category = "Metadata") 138 | TArray AnimSequences; 139 | 140 | /** The total num of Animation Sequences in this database. */ 141 | UPROPERTY(VisibleAnywhere, Category = "Stats") 142 | int32 NumAnimSequences; 143 | 144 | /** The total size of all Animation Sequences if the database were not used. */ 145 | UPROPERTY(VisibleAnywhere, Category = "Stats") 146 | int32 AnimSequencesOldSizeKB; 147 | 148 | /** The total size of all Animation Sequences with the database in use. */ 149 | UPROPERTY(VisibleAnywhere, Category = "Stats") 150 | int32 AnimSequencesNewSizeKB; 151 | 152 | /** The total size of the database. */ 153 | UPROPERTY(VisibleAnywhere, Category = "Stats") 154 | int32 DatabaseSizeKB; 155 | 156 | /** The size of the database metadata. */ 157 | UPROPERTY(VisibleAnywhere, Category = "Stats") 158 | int32 DatabaseMetadataSizeKB; 159 | 160 | /** The size of the database medium importance streaming tier. */ 161 | UPROPERTY(VisibleAnywhere, Category = "Stats") 162 | int32 MediumImportanceSizeKB; 163 | 164 | /** The size of the database low importance streaming tier before any stripping. */ 165 | UPROPERTY(VisibleAnywhere, Category = "Stats") 166 | int32 LowImportanceSizeSizeKB; 167 | 168 | public: 169 | ////////////////////////////////////////////////////////////////////////// 170 | // UObject implementation 171 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 172 | 173 | #if ENGINE_MAJOR_VERSION >= 5 174 | virtual void PreSave(FObjectPreSaveContext ObjectSaveContext) override; 175 | #else 176 | virtual void PreSave(const class ITargetPlatform* TargetPlatform) override; 177 | #endif 178 | 179 | /** Updates the internal list of anim sequences that reference this database. Returns whether or not anything changed. */ 180 | ACLPLUGIN_API bool UpdateReferencingAnimSequenceList(); 181 | #endif 182 | 183 | public: 184 | // UObject implementation 185 | virtual void BeginDestroy() override; 186 | virtual void PostLoad() override; 187 | virtual void Serialize(FArchive& Ar) override; 188 | 189 | /** Initiate a latent database change in visual fidelity by streaming in/out as necessary. */ 190 | void SetVisualFidelity(ACLVisualFidelity VisualFidelity); 191 | 192 | /** Retrieves the current visual fidelity level. */ 193 | ACLVisualFidelity GetVisualFidelity() const { return CurrentVisualFidelity; } 194 | 195 | /** Initiate a latent database change in quality by streaming in/out as necessary. */ 196 | UFUNCTION(BlueprintCallable, Category = "Animation|ACL", meta = (DisplayName = "Set Database Visual Fidelity", WorldContext="WorldContextObject", Latent, LatentInfo = "LatentInfo", ExpandEnumAsExecs="Result")) 197 | static void SetVisualFidelity(UObject* WorldContextObject, FLatentActionInfo LatentInfo, UAnimationCompressionLibraryDatabase* DatabaseAsset, ACLVisualFidelityChangeResult& Result, ACLVisualFidelity VisualFidelity = ACLVisualFidelity::Highest); 198 | 199 | UFUNCTION(BlueprintCallable, Category = "Animation|ACL", meta = (DisplayName = "Get Database Visual Fidelity")) 200 | static ACLVisualFidelity GetVisualFidelity(UAnimationCompressionLibraryDatabase* DatabaseAsset); 201 | 202 | private: 203 | #if WITH_EDITORONLY_DATA 204 | /** Builds our database and its related mappings as well as the new anim sequence data. */ 205 | void BuildDatabase(TArray& OutCompressedBytes, TArray& OutAnimSequenceMappings, TArray& OutBulkData, bool bStripLowestTier = false); 206 | 207 | /** Updates the internal preview state and optionally builds the database when requested. */ 208 | void UpdatePreviewState(bool bBuildDatabase); 209 | #endif 210 | 211 | /** Shared implementation between C++ and blueprint interfaces. Returns the request ID necessary to abort or ~0 if no change is required. */ 212 | uint32 SetVisualFidelityImpl(ACLVisualFidelity VisualFidelity, ACLVisualFidelityChangeResult* OutResult); 213 | 214 | /** Cancels an ongoing visual fidelity request. Only supported if the request hasn't completed. Will not update the ACLVisualFidelityChangeResult of the request. */ 215 | void CancelVisualFidelityRequestImpl(uint32 RequestID); 216 | 217 | /** Core ticker update function to update our visual fidelity state. */ 218 | bool UpdateVisualFidelityTicker(float DeltaTime); 219 | 220 | friend class FACLPlugin; 221 | friend class FSetDatabaseVisualFidelityAction; 222 | friend class UAnimBoneCompressionCodec_ACLDatabase; 223 | friend struct FACLDatabaseCompressedAnimData; 224 | }; 225 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Public/ACLImpl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/ObjectMacros.h" 7 | #include "Runtime/Launch/Resources/Version.h" 8 | 9 | #if DO_GUARD_SLOW 10 | // ACL has a lot of asserts, only enabled in Debug 11 | // This decision has been made because the extensive asserts add considerable overhead and most 12 | // developers use a Development configuration for the editor. The ACL library runs extensive 13 | // unit and regression tests on a very large number of clips which minimizes the risk of 14 | // having a legitimate assert fire. 15 | 16 | #define ACL_ON_ASSERT_CUSTOM 17 | 18 | // Override the string format specifier for UE 19 | #define RTM_ASSERT_STRING_FORMAT_SPECIFIER "%hs" 20 | #define SJSON_ASSERT_STRING_FORMAT_SPECIFIER "%hs" 21 | #define ACL_ASSERT_STRING_FORMAT_SPECIFIER "%hs" 22 | 23 | #define ACL_ASSERT(expression, format, ...) checkf(expression, TEXT(format), ##__VA_ARGS__) 24 | #endif 25 | 26 | // Enable popcount usage on PS4. 27 | // We cannot use PLATFORM_ENABLE_POPCNT_INTRINSIC as that might be unsafe. 28 | // UE uses that macro to determine if the GCC intrinsic is present, not if the instruction 29 | // is supported. 30 | // UE 5.1 removed the PS4 define and we can no longer rely on it 31 | #if (ENGINE_MAJOR_VERSION <= 5 && ENGINE_MINOR_VERSION <= 0) && PLATFORM_PS4 32 | // Enable usage of popcount instruction 33 | #define ACL_USE_POPCOUNT 34 | #endif 35 | 36 | ////////////////////////////////////////////////////////////////////////// 37 | // [Bind pose stripping] 38 | // Bind pose stripping allows ACL to remove the redundant bind pose often present in the compressed 39 | // data of joints that aren't animated. For this to work properly, we must have access to the bind 40 | // pose during decompression which is not accessible in UE 5.0 and earlier. 41 | // 42 | // When stripping is disabled, here is what happens: 43 | // Non-additive anim sequence 44 | // - During compression, sub-tracks have their default value set to the identity. 45 | // Sub-tracks equal to the identity will be stripped (uncommon). 46 | // - When we decompress a whole pose, sub-tracks equal to the identity are processed 47 | // and the identity is written out. 48 | // - When we decompress a single bone, sub-tracks equal to the identity are processed 49 | // and the identity is written out. 50 | // 51 | // Additive anim sequence 52 | // - During compression, sub-tracks have their default value set to the additive identity. 53 | // Sub-tracks equal to the identity will be stripped (common). 54 | // - When we decompress a whole pose, sub-tracks equal to the additive identity are skipped entirely. 55 | // This is possible because the output pose is initialized with the additive identity before decompression. 56 | // - When we decompress a single bone, sub-tracks equal to the identity are processed 57 | // and the identity is written out. 58 | // 59 | // When stripping is enabled, here is what happens: 60 | // Non-additive anim sequence 61 | // - During compression, sub-tracks have their default value set to the bind pose. 62 | // Sub-tracks equal to the bind pose will be stripped (common). 63 | // - When we decompress a whole pose, sub-tracks equal to the bind pose are skipped entirely. 64 | // This is possible because the output pose is initialized with the bind pose before decompression. 65 | // - When we decompress a single bone, sub-tracks equal to the bind pose are processed 66 | // and the bind pose is written out. 67 | // 68 | // Additive anim sequence 69 | // - Additive sequences do not have a traditional bind pose, instead their default pose is 70 | // the additive identity. As such, ACL always does stripping by default for this scenario. 71 | // The behavior is identical as above. 72 | ////////////////////////////////////////////////////////////////////////// 73 | 74 | #define ACL_WITH_BIND_POSE_STRIPPING (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 75 | 76 | ////////////////////////////////////////////////////////////////////////// 77 | // [Keyframe stripping] 78 | // 79 | // ACL can strip keyframes that contribute the least amount of error. 80 | // This is controlled through two different variables which can be used on their own or together: 81 | // * proportion: the minimum amount (percentage) of keyframes to strip 82 | // * threshold: the threshold below which keyframes are stripped 83 | // 84 | // In order to support this per platform, the plugin requires the target platform to be exposed in 85 | // a few codec API functions. Support for this has been added in UE 5.1. 86 | ////////////////////////////////////////////////////////////////////////// 87 | 88 | #define ACL_WITH_KEYFRAME_STRIPPING (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 89 | 90 | THIRD_PARTY_INCLUDES_START 91 | #include 92 | #include 93 | #include 94 | 95 | #include 96 | #include 97 | #include 98 | THIRD_PARTY_INCLUDES_END 99 | 100 | #if WITH_EDITOR 101 | THIRD_PARTY_INCLUDES_START 102 | #include 103 | #include 104 | THIRD_PARTY_INCLUDES_END 105 | #endif 106 | 107 | #include "ACLImpl.generated.h" 108 | 109 | /** The ACL allocator implementation simply forwards to the default heap allocator. */ 110 | class ACLAllocator final : public acl::iallocator 111 | { 112 | public: 113 | virtual void* allocate(size_t size, size_t alignment = acl::iallocator::k_default_alignment) 114 | { 115 | return GMalloc->Malloc(size, alignment); 116 | } 117 | 118 | virtual void deallocate(void* ptr, size_t size) 119 | { 120 | GMalloc->Free(ptr); 121 | } 122 | }; 123 | 124 | extern ACLPLUGIN_API ACLAllocator ACLAllocatorImpl; 125 | 126 | /** RTM <-> UE conversion utilities and aliases. */ 127 | #if ENGINE_MAJOR_VERSION >= 5 128 | inline FQuat RTM_SIMD_CALL UEQuatCast(const FQuat4f& Input) { return FQuat(Input.X, Input.Y, Input.Z, Input.W); } 129 | inline FVector RTM_SIMD_CALL UEVector3Cast(const FVector3f& Input) { return FVector(Input.X, Input.Y, Input.Z); } 130 | inline FVector RTM_SIMD_CALL UEVector3Cast(const FVector3d& Input) { return FVector(Input.X, Input.Y, Input.Z); } 131 | 132 | inline rtm::vector4f RTM_SIMD_CALL UEVector3ToACL(const FVector3f& Input) { return rtm::vector_set(Input.X, Input.Y, Input.Z); } 133 | inline rtm::vector4f RTM_SIMD_CALL UEVector3ToACL(const FVector& Input) { return rtm::vector_cast(rtm::vector_set(Input.X, Input.Y, Input.Z)); } 134 | inline FVector3f RTM_SIMD_CALL ACLVector3ToUE(rtm::vector4f_arg0 Input) { return FVector3f(rtm::vector_get_x(Input), rtm::vector_get_y(Input), rtm::vector_get_z(Input)); } 135 | 136 | inline rtm::quatf RTM_SIMD_CALL UEQuatToACL(const FQuat4f& Input) { return rtm::quat_set(Input.X, Input.Y, Input.Z, Input.W); } 137 | inline rtm::quatf RTM_SIMD_CALL UEQuatToACL(const FQuat& Input) { return rtm::quat_cast(rtm::quat_set(Input.X, Input.Y, Input.Z, Input.W)); } 138 | inline FQuat4f RTM_SIMD_CALL ACLQuatToUE(rtm::quatf_arg0 Input) { return FQuat4f(rtm::quat_get_x(Input), rtm::quat_get_y(Input), rtm::quat_get_z(Input), rtm::quat_get_w(Input)); } 139 | 140 | inline FTransform RTM_SIMD_CALL ACLTransformToUE(rtm::qvvf_arg0 Input) { return FTransform(UEQuatCast(ACLQuatToUE(Input.rotation)), UEVector3Cast(ACLVector3ToUE(Input.translation)), UEVector3Cast(ACLVector3ToUE(Input.scale))); } 141 | 142 | using FRawAnimTrackQuat = FQuat4f; 143 | using FRawAnimTrackVector3 = FVector3f; 144 | #else 145 | inline FQuat RTM_SIMD_CALL UEQuatCast(const FQuat& Input) { return Input; } 146 | inline FVector RTM_SIMD_CALL UEVector3Cast(const FVector& Input) { return Input; } 147 | 148 | inline rtm::vector4f RTM_SIMD_CALL UEVector3ToACL(const FVector& Input) { return rtm::vector_set(Input.X, Input.Y, Input.Z); } 149 | inline FVector RTM_SIMD_CALL ACLVector3ToUE(rtm::vector4f_arg0 Input) { return FVector(rtm::vector_get_x(Input), rtm::vector_get_y(Input), rtm::vector_get_z(Input)); } 150 | 151 | inline rtm::quatf RTM_SIMD_CALL UEQuatToACL(const FQuat& Input) { return rtm::quat_set(Input.X, Input.Y, Input.Z, Input.W); } 152 | inline FQuat RTM_SIMD_CALL ACLQuatToUE(rtm::quatf_arg0 Input) { return FQuat(rtm::quat_get_x(Input), rtm::quat_get_y(Input), rtm::quat_get_z(Input), rtm::quat_get_w(Input)); } 153 | 154 | inline FTransform RTM_SIMD_CALL ACLTransformToUE(rtm::qvvf_arg0 Input) { return FTransform(ACLQuatToUE(Input.rotation), ACLVector3ToUE(Input.translation), ACLVector3ToUE(Input.scale)); } 155 | 156 | using FRawAnimTrackQuat = FQuat; 157 | using FRawAnimTrackVector3 = FVector; 158 | #endif 159 | 160 | /** The decompression settings used by ACL */ 161 | struct UEDefaultDecompressionSettings : public acl::default_transform_decompression_settings 162 | { 163 | // Only support our latest version 164 | static constexpr acl::compressed_tracks_version16 version_supported() { return acl::compressed_tracks_version16::latest; } 165 | 166 | #if UE_BUILD_SHIPPING 167 | // Shipping builds do not need safety checks, by then the game has been tested enough 168 | // Only data corruption could cause a safety check to fail 169 | // We keep this disabled regardless because it is generally better to output a T-pose than to have a 170 | // potential crash. Corruption can happen and it would be unfortunate if a demo or playtest failed 171 | // as a result of a crash that we can otherwise recover from. 172 | //static constexpr bool skip_initialize_safety_checks() { return true; } 173 | #endif 174 | }; 175 | 176 | struct UEDebugDecompressionSettings : public acl::debug_transform_decompression_settings 177 | { 178 | // Only support our latest version 179 | static constexpr acl::compressed_tracks_version16 version_supported() { return acl::compressed_tracks_version16::latest; } 180 | }; 181 | 182 | // Same as debug settings for now since everything is allowed 183 | using UECustomDecompressionSettings = UEDebugDecompressionSettings; 184 | 185 | struct UESafeDecompressionSettings final : public UEDefaultDecompressionSettings 186 | { 187 | static constexpr bool is_rotation_format_supported(acl::rotation_format8 format) { return format == acl::rotation_format8::quatf_full; } 188 | static constexpr acl::rotation_format8 get_rotation_format(acl::rotation_format8 /*format*/) { return acl::rotation_format8::quatf_full; } 189 | }; 190 | 191 | struct UEDefaultDatabaseSettings final : public acl::default_database_settings 192 | { 193 | // Only support our latest version 194 | static constexpr acl::compressed_tracks_version16 version_supported() { return acl::compressed_tracks_version16::latest; } 195 | }; 196 | 197 | struct UEDefaultDBDecompressionSettings final : public UEDefaultDecompressionSettings 198 | { 199 | using database_settings_type = UEDefaultDatabaseSettings; 200 | }; 201 | 202 | using UEDebugDatabaseSettings = acl::debug_database_settings; 203 | 204 | struct UEDebugDBDecompressionSettings final : public UEDebugDecompressionSettings 205 | { 206 | using database_settings_type = UEDebugDatabaseSettings; 207 | }; 208 | 209 | /** UE equivalents for some ACL enums */ 210 | /** An enum for ACL rotation formats. */ 211 | UENUM() 212 | enum ACLRotationFormat 213 | { 214 | ACLRF_Quat_128 UMETA(DisplayName = "Quat Full Bit Rate"), 215 | ACLRF_QuatDropW_96 UMETA(DisplayName = "Quat Drop W Full Bit Rate"), 216 | ACLRF_QuatDropW_Variable UMETA(DisplayName = "Quat Drop W Variable Bit Rate"), 217 | }; 218 | 219 | /** An enum for ACL Vector3 formats. */ 220 | UENUM() 221 | enum ACLVectorFormat 222 | { 223 | ACLVF_Vector3_96 UMETA(DisplayName = "Vector3 Full Bit Rate"), 224 | ACLVF_Vector3_Variable UMETA(DisplayName = "Vector3 Variable Bit Rate"), 225 | }; 226 | 227 | /** An enum for ACL compression levels. */ 228 | UENUM() 229 | enum ACLCompressionLevel 230 | { 231 | ACLCL_Lowest UMETA(DisplayName = "Lowest"), 232 | ACLCL_Low UMETA(DisplayName = "Low"), 233 | ACLCL_Medium UMETA(DisplayName = "Medium"), 234 | ACLCL_High UMETA(DisplayName = "High"), 235 | ACLCL_Highest UMETA(DisplayName = "Highest"), 236 | ACLCL_Automatic UMETA(DisplayName = "Automatic"), 237 | }; 238 | 239 | /** Editor only utilities */ 240 | #if WITH_EDITORONLY_DATA 241 | 242 | /** 243 | * An enum to control how to handle UE phantom tracks. 244 | * A phantom tracks are present in an animation sequence but aren't mapped to skeleton bones. 245 | * As such, they cannot be queried by the engine except manually through DecompressBone. 246 | * It should generally be safe to Strip them but we default to Ignore. 247 | * Re-importing the animation sequence should clean up any such stale data. 248 | */ 249 | UENUM() 250 | enum class ACLPhantomTrackMode : uint8 251 | { 252 | // Ignore phantom tracks and compress them anyway (same as UE codecs). 253 | Ignore, 254 | 255 | // Strip the phantom track to save memory by collapsing them to the identity transform while maintaining the track ordering. 256 | Strip, 257 | 258 | // We ignore the phantom tracks and output a warning to the log. 259 | Warn, 260 | }; 261 | 262 | #endif 263 | 264 | #if WITH_EDITOR 265 | struct FCompressibleAnimData; 266 | struct FPerPlatformFloat; 267 | class UAnimSequence; 268 | class ITargetPlatform; 269 | 270 | ACLPLUGIN_API acl::rotation_format8 GetRotationFormat(ACLRotationFormat Format); 271 | ACLPLUGIN_API acl::vector_format8 GetVectorFormat(ACLVectorFormat Format); 272 | ACLPLUGIN_API acl::compression_level8 GetCompressionLevel(ACLCompressionLevel Level); 273 | 274 | ACLPLUGIN_API acl::track_array_qvvf BuildACLTransformTrackArray(ACLAllocator& AllocatorImpl, const FCompressibleAnimData& CompressibleAnimData, 275 | float DefaultVirtualVertexDistance, float SafeVirtualVertexDistance, 276 | bool bBuildAdditiveBase, ACLPhantomTrackMode PhantomTrackMode); 277 | 278 | /** Compatibility utilities */ 279 | ACLPLUGIN_API uint32 GetNumSamples(const FCompressibleAnimData& CompressibleAnimData); 280 | ACLPLUGIN_API float GetSequenceLength(const UAnimSequence& AnimSeq); 281 | 282 | namespace ACL 283 | { 284 | namespace Private 285 | { 286 | ACLPLUGIN_API float GetPerPlatformFloat(const FPerPlatformFloat& PerPlatformFloat, const ITargetPlatform* TargetPlatform); 287 | } 288 | } 289 | #endif // WITH_EDITOR 290 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/ACLImpl.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "ACLImpl.h" 4 | 5 | #if WITH_EDITOR 6 | #include "AnimationCompression.h" 7 | #include "AnimationUtils.h" 8 | #include "PerPlatformProperties.h" 9 | #include "Animation/AnimCompressionTypes.h" 10 | #include "Interfaces/ITargetPlatform.h" 11 | #include "PlatformInfo.h" 12 | 13 | acl::rotation_format8 GetRotationFormat(ACLRotationFormat Format) 14 | { 15 | switch (Format) 16 | { 17 | default: 18 | case ACLRF_Quat_128: return acl::rotation_format8::quatf_full; 19 | case ACLRF_QuatDropW_96: return acl::rotation_format8::quatf_drop_w_full; 20 | case ACLRF_QuatDropW_Variable: return acl::rotation_format8::quatf_drop_w_variable; 21 | } 22 | } 23 | 24 | acl::vector_format8 GetVectorFormat(ACLVectorFormat Format) 25 | { 26 | switch (Format) 27 | { 28 | default: 29 | case ACLVF_Vector3_96: return acl::vector_format8::vector3f_full; 30 | case ACLVF_Vector3_Variable: return acl::vector_format8::vector3f_variable; 31 | } 32 | } 33 | 34 | acl::compression_level8 GetCompressionLevel(ACLCompressionLevel Level) 35 | { 36 | switch (Level) 37 | { 38 | default: 39 | case ACLCL_Lowest: return acl::compression_level8::lowest; 40 | case ACLCL_Low: return acl::compression_level8::low; 41 | case ACLCL_Medium: return acl::compression_level8::medium; 42 | case ACLCL_High: return acl::compression_level8::high; 43 | case ACLCL_Highest: return acl::compression_level8::highest; 44 | case ACLCL_Automatic: return acl::compression_level8::automatic; 45 | } 46 | } 47 | 48 | static int32 FindAnimationTrackIndex(const FCompressibleAnimData& CompressibleAnimData, int32 BoneIndex) 49 | { 50 | const TArray& TrackToSkelMap = CompressibleAnimData.TrackToSkeletonMapTable; 51 | if (BoneIndex != INDEX_NONE) 52 | { 53 | for (int32 TrackIndex = 0; TrackIndex < TrackToSkelMap.Num(); ++TrackIndex) 54 | { 55 | const FTrackToSkeletonMap& TrackToSkeleton = TrackToSkelMap[TrackIndex]; 56 | if (TrackToSkeleton.BoneTreeIndex == BoneIndex) 57 | return TrackIndex; 58 | } 59 | } 60 | 61 | return INDEX_NONE; 62 | } 63 | 64 | static bool IsAdditiveBakedIntoRaw(const FCompressibleAnimData& CompressibleAnimData) 65 | { 66 | if (!CompressibleAnimData.bIsValidAdditive) 67 | { 68 | return true; // Sequences that aren't additive don't need baking as they are already baked 69 | } 70 | 71 | if (CompressibleAnimData.RawAnimationData.Num() == 0) 72 | { 73 | return true; // Sequences with no raw data don't need baking and we can't tell if they are baked or not so assume that they are, it doesn't matter 74 | } 75 | 76 | if (CompressibleAnimData.AdditiveBaseAnimationData.Num() != 0) 77 | { 78 | return true; // Sequence has raw data and additive base data, it is baked 79 | } 80 | 81 | return false; // Sequence has raw data but no additive base data, it isn't baked 82 | } 83 | 84 | // Returns a bit array, a bit is true if the corresponding UE track has a skeleton bone mapped to it, false otherwise 85 | static TBitArray<> GetMappedUETracks(const FCompressibleAnimData& CompressibleAnimData) 86 | { 87 | const int32 NumBones = CompressibleAnimData.BoneData.Num(); 88 | const int32 NumUETracks = CompressibleAnimData.TrackToSkeletonMapTable.Num(); 89 | 90 | TBitArray<> MappedUETracks(false, NumUETracks); 91 | 92 | for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex) 93 | { 94 | const int32 UETrackIndex = FindAnimationTrackIndex(CompressibleAnimData, BoneIndex); 95 | 96 | if (UETrackIndex >= 0) 97 | { 98 | // This track has a bone mapped to it 99 | MappedUETracks[UETrackIndex] = true; 100 | } 101 | } 102 | 103 | return MappedUETracks; 104 | } 105 | 106 | static int32 CountSetBits(const TBitArray<>& Array) 107 | { 108 | #if ENGINE_MAJOR_VERSION >= 5 || (ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION >= 26) 109 | return Array.CountSetBits(); 110 | #else 111 | int32 Result = 0; 112 | for (TBitArray<>::FConstIterator BitIt(Array); BitIt; ++BitIt) 113 | { 114 | Result += BitIt.GetValue(); 115 | } 116 | return Result; 117 | #endif 118 | } 119 | 120 | acl::track_array_qvvf BuildACLTransformTrackArray(ACLAllocator& AllocatorImpl, const FCompressibleAnimData& CompressibleAnimData, 121 | float DefaultVirtualVertexDistance, float SafeVirtualVertexDistance, 122 | bool bBuildAdditiveBase, ACLPhantomTrackMode PhantomTrackMode) 123 | { 124 | const bool bIsAdditive = bBuildAdditiveBase ? false : CompressibleAnimData.bIsValidAdditive; 125 | const bool bIsAdditiveBakedIntoRaw = IsAdditiveBakedIntoRaw(CompressibleAnimData); 126 | check(!bIsAdditive || bIsAdditiveBakedIntoRaw); 127 | if (bIsAdditive && !bIsAdditiveBakedIntoRaw) 128 | { 129 | UE_LOG(LogAnimationCompression, Fatal, TEXT("Animation sequence is additive but it is not baked into the raw data, this is not supported. [%s]"), *CompressibleAnimData.FullName); 130 | return acl::track_array_qvvf(); 131 | } 132 | 133 | // Ordinary non additive sequences only contain raw data returned by GetRawAnimationData(). 134 | // 135 | // Additive sequences have their raw data baked with the raw additive base already taken out. At runtime, 136 | // the additive sequence data returned by GetRawAnimationData() gets applied on top of the 137 | // additive base sequence whose's data is returned by GetAdditiveBaseAnimationData(). In practice, 138 | // at runtime the additive base might also be compressed and thus there might be some added noise but 139 | // it typically isn't visible. 140 | // The baked additive data should have the same number of frames as the current sequence but if we only care about a specific frame, 141 | // we use it. When this happens, the additive base will contain that single frame repeated over and over: an animated static pose. 142 | // To avoid wasting memory, we just grab the first frame. 143 | 144 | const TArray& RawTracks = bBuildAdditiveBase ? CompressibleAnimData.AdditiveBaseAnimationData : CompressibleAnimData.RawAnimationData; 145 | const uint32 NumSamples = GetNumSamples(CompressibleAnimData); 146 | const bool bIsStaticPose = NumSamples <= 1 || CompressibleAnimData.SequenceLength < 0.0001f; 147 | const float SampleRate = bIsStaticPose ? 30.0f : (float(NumSamples - 1) / CompressibleAnimData.SequenceLength); 148 | const int32 NumBones = CompressibleAnimData.BoneData.Num(); 149 | const int32 NumUETracks = CompressibleAnimData.TrackToSkeletonMapTable.Num(); 150 | 151 | // Additive animations have 0,0,0 scale as the default since we add it 152 | const FRawAnimTrackVector3 UEDefaultScale(bIsAdditive ? 0.0f : 1.0f); 153 | const rtm::vector4f ACLDefaultScale = rtm::vector_set(bIsAdditive ? 0.0f : 1.0f); 154 | 155 | rtm::qvvf ACLDefaultAdditiveBindTransform = rtm::qvv_identity(); 156 | ACLDefaultAdditiveBindTransform.scale = ACLDefaultScale; 157 | 158 | // A bit array to tell which UE tracks are mapped to a skeleton bone 159 | const TBitArray<> MappedUETracks = GetMappedUETracks(CompressibleAnimData); 160 | 161 | // We need to make sure to allocate enough ACL tracks. It is very common to have a skeleton with a number of bones 162 | // and to have anim sequences that use that skeleton that have fewer tracks. This might happen if bones are added 163 | // and when this happens, we'll populate the bind pose for that missing track. A less common case can happen where 164 | // a sequence has compressed tracks for bones that no longer exist. We still need to compress this unused data to 165 | // ensure the track indices remain in sync with the CompressedTrackToSkeletonMapTable. 166 | // 167 | // We need to allocate one ACL track per UE bone and one for each unmapped UE track 168 | const int32 NumUnmappedUETracks = NumUETracks - CountSetBits(MappedUETracks); 169 | const int32 NumInputACLTracks = NumBones + NumUnmappedUETracks; 170 | int32 NumOutputACLTracks = 0; // For sanity check 171 | 172 | acl::track_array_qvvf Tracks(AllocatorImpl, NumInputACLTracks); 173 | Tracks.set_name(acl::string(AllocatorImpl, TCHAR_TO_ANSI(*CompressibleAnimData.FullName))); 174 | 175 | // Populate all our track based on our skeleton, even if some end up stripped 176 | for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex) 177 | { 178 | const FBoneData& UEBone = CompressibleAnimData.BoneData[BoneIndex]; 179 | 180 | acl::track_desc_transformf Desc; 181 | 182 | // We use a higher virtual vertex distance when bones have a socket attached or are keyed end effectors (IK, hand, camera, etc) 183 | Desc.shell_distance = (UEBone.bHasSocket || UEBone.bKeyEndEffector) ? SafeVirtualVertexDistance : DefaultVirtualVertexDistance; 184 | 185 | const int32 ParentBoneIndex = UEBone.GetParent(); 186 | Desc.parent_index = ParentBoneIndex >= 0 ? ParentBoneIndex : acl::k_invalid_track_index; 187 | 188 | const int32 UETrackIndex = FindAnimationTrackIndex(CompressibleAnimData, BoneIndex); 189 | 190 | // We output bone data in UE track order. If a track isn't present, we will use the bind pose and strip it from the 191 | // compressed stream. 192 | if (UETrackIndex >= 0) 193 | { 194 | Desc.output_index = UETrackIndex; 195 | NumOutputACLTracks++; 196 | } 197 | else 198 | { 199 | Desc.output_index = acl::k_invalid_track_index; 200 | } 201 | 202 | // Make sure the default scale value is consistent whether we are additive or not 203 | Desc.default_value.scale = ACLDefaultScale; 204 | 205 | acl::track_qvvf Track = acl::track_qvvf::make_reserve(Desc, AllocatorImpl, NumSamples, SampleRate); 206 | Track.set_name(acl::string(AllocatorImpl, TCHAR_TO_ANSI(*UEBone.Name.ToString()))); 207 | 208 | if (UETrackIndex >= 0) 209 | { 210 | // We have a track for this bone, use it 211 | const FRawAnimSequenceTrack& RawTrack = RawTracks[UETrackIndex]; 212 | 213 | for (uint32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex) 214 | { 215 | const FRawAnimTrackQuat& RotationSample = RawTrack.RotKeys.Num() == 1 ? RawTrack.RotKeys[0] : RawTrack.RotKeys[SampleIndex]; 216 | Track[SampleIndex].rotation = UEQuatToACL(RotationSample); 217 | 218 | const FRawAnimTrackVector3& TranslationSample = RawTrack.PosKeys.Num() == 1 ? RawTrack.PosKeys[0] : RawTrack.PosKeys[SampleIndex]; 219 | Track[SampleIndex].translation = UEVector3ToACL(TranslationSample); 220 | 221 | const FRawAnimTrackVector3& ScaleSample = RawTrack.ScaleKeys.Num() == 0 ? UEDefaultScale : (RawTrack.ScaleKeys.Num() == 1 ? RawTrack.ScaleKeys[0] : RawTrack.ScaleKeys[SampleIndex]); 222 | Track[SampleIndex].scale = UEVector3ToACL(ScaleSample); 223 | } 224 | } 225 | else 226 | { 227 | // No track data for this bone, it must be new. Use the bind pose instead. 228 | // Additive animations have the identity with 0 scale as their bind pose. 229 | rtm::qvvf BindTransform; 230 | if (bIsAdditive) 231 | { 232 | BindTransform = ACLDefaultAdditiveBindTransform; 233 | } 234 | else 235 | { 236 | BindTransform = rtm::qvv_set(UEQuatToACL(UEBone.Orientation), UEVector3ToACL(UEBone.Position), ACLDefaultScale); 237 | } 238 | 239 | for (uint32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex) 240 | { 241 | Track[SampleIndex] = BindTransform; 242 | } 243 | } 244 | 245 | Tracks[BoneIndex] = MoveTemp(Track); 246 | } 247 | 248 | // If we have leftover phantom tracks that do not map to any bone in our skeleton, compress them anyway to keep indices consistent 249 | // Phantom UE tracks have no associated skeleton bone and as such can only be queried at runtime individually with DecompressBone. 250 | // It should be safe to strip and collapse them to the identity. 251 | int32 ACLTrackIndex = NumBones; // Start inserting at the end 252 | for (int32 UETrackIndex = 0; UETrackIndex < NumUETracks; ++UETrackIndex) 253 | { 254 | if (MappedUETracks[UETrackIndex]) 255 | { 256 | // This track has been populated, skip it 257 | continue; 258 | } 259 | 260 | // This track has no corresponding skeleton bone, add it anyway 261 | acl::track_desc_transformf Desc; 262 | 263 | // Without a bone, we have no way of knowing if we require special treatment 264 | Desc.shell_distance = DefaultVirtualVertexDistance; 265 | 266 | // Without a bone, no way to know if we have a parent, assume we are a root bone 267 | Desc.parent_index = acl::k_invalid_track_index; 268 | 269 | // Output index is our UE track index 270 | Desc.output_index = UETrackIndex; 271 | NumOutputACLTracks++; 272 | 273 | acl::track_qvvf Track = acl::track_qvvf::make_reserve(Desc, AllocatorImpl, NumSamples, SampleRate); 274 | 275 | if (PhantomTrackMode == ACLPhantomTrackMode::Strip) 276 | { 277 | // We'll collapse the track to the identity transform 278 | // It will be compressed to maintain the track ordering but it will only use 6 bits in total 279 | const rtm::qvvf IdentityTransform = bIsAdditive ? ACLDefaultAdditiveBindTransform : rtm::qvv_identity(); 280 | 281 | for (uint32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex) 282 | { 283 | Track[SampleIndex] = IdentityTransform; 284 | } 285 | } 286 | else 287 | { 288 | if (PhantomTrackMode == ACLPhantomTrackMode::Warn) 289 | { 290 | UE_LOG(LogAnimationCompression, Warning, TEXT("Animation sequence has phantom tracks that do not map to any skeleton bone. Track %d in [%s]"), UETrackIndex, *CompressibleAnimData.FullName); 291 | } 292 | 293 | // We have raw track data, use it 294 | const FRawAnimSequenceTrack& RawTrack = RawTracks[UETrackIndex]; 295 | 296 | for (uint32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex) 297 | { 298 | const FRawAnimTrackQuat& RotationSample = RawTrack.RotKeys.Num() == 1 ? RawTrack.RotKeys[0] : RawTrack.RotKeys[SampleIndex]; 299 | Track[SampleIndex].rotation = UEQuatToACL(RotationSample); 300 | 301 | const FRawAnimTrackVector3& TranslationSample = RawTrack.PosKeys.Num() == 1 ? RawTrack.PosKeys[0] : RawTrack.PosKeys[SampleIndex]; 302 | Track[SampleIndex].translation = UEVector3ToACL(TranslationSample); 303 | 304 | const FRawAnimTrackVector3& ScaleSample = RawTrack.ScaleKeys.Num() == 0 ? UEDefaultScale : (RawTrack.ScaleKeys.Num() == 1 ? RawTrack.ScaleKeys[0] : RawTrack.ScaleKeys[SampleIndex]); 305 | Track[SampleIndex].scale = UEVector3ToACL(ScaleSample); 306 | } 307 | } 308 | 309 | // Add our extra track 310 | Tracks[ACLTrackIndex] = MoveTemp(Track); 311 | ACLTrackIndex++; 312 | } 313 | 314 | // Number of UE tracks and ACL output tracks should match 315 | check(NumOutputACLTracks == NumUETracks); 316 | 317 | return Tracks; 318 | } 319 | 320 | uint32 GetNumSamples(const FCompressibleAnimData& CompressibleAnimData) 321 | { 322 | #if ENGINE_MAJOR_VERSION >= 5 323 | return CompressibleAnimData.NumberOfKeys; 324 | #else 325 | return CompressibleAnimData.NumFrames; 326 | #endif 327 | } 328 | 329 | float GetSequenceLength(const UAnimSequence& AnimSeq) 330 | { 331 | #if ENGINE_MAJOR_VERSION >= 5 332 | return AnimSeq.GetDataModel()->GetPlayLength(); 333 | #else 334 | return AnimSeq.SequenceLength; 335 | #endif 336 | } 337 | 338 | namespace ACL 339 | { 340 | namespace Private 341 | { 342 | float GetPerPlatformFloat(const FPerPlatformFloat& PerPlatformFloat, const ITargetPlatform* TargetPlatform) 343 | { 344 | if (TargetPlatform == nullptr) 345 | { 346 | // TODO: Why does calling GetDefault() not link with undefined symbol? 347 | return PerPlatformFloat.Default; // Unknown target platform 348 | } 349 | 350 | #if ENGINE_MAJOR_VERSION >= 5 351 | return PerPlatformFloat.GetValueForPlatform(*TargetPlatform->IniPlatformName()); 352 | #else 353 | return PerPlatformFloat.GetValueForPlatformIdentifiers( 354 | TargetPlatform->GetPlatformInfo().PlatformGroupName, 355 | TargetPlatform->GetPlatformInfo().VanillaPlatformName); 356 | #endif 357 | } 358 | } 359 | } 360 | #endif // WITH_EDITOR 361 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/AnimCurveCompressionCodec_ACL.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "AnimCurveCompressionCodec_ACL.h" 4 | 5 | #include "ACLImpl.h" 6 | 7 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 8 | #include UE_INLINE_GENERATED_CPP_BY_NAME(AnimCurveCompressionCodec_ACL) 9 | #endif 10 | 11 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 12 | #include "Animation/AnimCurveUtils.h" 13 | #endif 14 | 15 | #if WITH_EDITORONLY_DATA 16 | #include "AnimationCompression.h" 17 | #include "Animation/MorphTarget.h" 18 | #include "Engine/SkeletalMesh.h" 19 | #include "Rendering/SkeletalMeshModel.h" 20 | 21 | THIRD_PARTY_INCLUDES_START 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | THIRD_PARTY_INCLUDES_END 28 | #endif 29 | 30 | THIRD_PARTY_INCLUDES_START 31 | #include 32 | THIRD_PARTY_INCLUDES_END 33 | 34 | UAnimCurveCompressionCodec_ACL::UAnimCurveCompressionCodec_ACL(const FObjectInitializer& ObjectInitializer) 35 | : Super(ObjectInitializer) 36 | { 37 | #if WITH_EDITORONLY_DATA 38 | CurvePrecision = 0.001f; 39 | MorphTargetPositionPrecision = 0.01f; // 0.01cm, conservative enough for cinematographic quality 40 | #endif 41 | } 42 | 43 | #if WITH_EDITORONLY_DATA 44 | void UAnimCurveCompressionCodec_ACL::PopulateDDCKey(FArchive& Ar) 45 | { 46 | Super::PopulateDDCKey(Ar); 47 | 48 | Ar << CurvePrecision; 49 | Ar << MorphTargetPositionPrecision; 50 | 51 | if (MorphTargetSource != nullptr) 52 | { 53 | FSkeletalMeshModel* MeshModel = MorphTargetSource->GetImportedModel(); 54 | if (MeshModel != nullptr) 55 | { 56 | Ar << MeshModel->SkeletalMeshModelGUID; 57 | } 58 | } 59 | 60 | uint32 ForceRebuildVersion = 2; 61 | Ar << ForceRebuildVersion; 62 | 63 | uint16 LatestACLVersion = static_cast(acl::compressed_tracks_version16::latest); 64 | Ar << LatestACLVersion; 65 | 66 | acl::compression_settings Settings; 67 | uint32 SettingsHash = Settings.get_hash(); 68 | Ar << SettingsHash; 69 | } 70 | 71 | static const TArray& GetRawCurves(const FCompressibleAnimData& AnimSeq) 72 | { 73 | #if ENGINE_MAJOR_VERSION >= 5 74 | return AnimSeq.RawFloatCurves; 75 | #else 76 | return AnimSeq.RawCurveData.FloatCurves; 77 | #endif 78 | } 79 | 80 | static FName GetCurveName(const FFloatCurve& Curve) 81 | { 82 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 83 | return Curve.GetName(); 84 | #else 85 | return Curve.Name.DisplayName; 86 | #endif 87 | } 88 | 89 | // For each curve, returns its largest position delta if the curve is for a morph target, 0.0 otherwise 90 | static TArray GetMorphTargetMaxPositionDeltas(const FCompressibleAnimData& AnimSeq, const USkeletalMesh* MorphTargetSource) 91 | { 92 | const TArray& RawCurves = GetRawCurves(AnimSeq); 93 | const int32 NumCurves = RawCurves.Num(); 94 | 95 | TArray MorphTargetMaxPositionDeltas; 96 | MorphTargetMaxPositionDeltas.AddZeroed(NumCurves); 97 | 98 | if (MorphTargetSource == nullptr) 99 | { 100 | return MorphTargetMaxPositionDeltas; 101 | } 102 | 103 | for (int32 CurveIndex = 0; CurveIndex < NumCurves; ++CurveIndex) 104 | { 105 | float MaxDeltaPosition = 0.0f; 106 | const FFloatCurve& Curve = RawCurves[CurveIndex]; 107 | 108 | UMorphTarget* Target = MorphTargetSource->FindMorphTarget(GetCurveName(Curve)); 109 | if (Target != nullptr) 110 | { 111 | // This curve drives a morph target, find the largest displacement it can have 112 | const int32 LODIndex = 0; 113 | int32 NumDeltas = 0; 114 | const FMorphTargetDelta* Deltas = Target->GetMorphTargetDelta(LODIndex, NumDeltas); 115 | for (int32 DeltaIndex = 0; DeltaIndex < NumDeltas; ++DeltaIndex) 116 | { 117 | const FMorphTargetDelta& Delta = Deltas[DeltaIndex]; 118 | MaxDeltaPosition = FMath::Max(MaxDeltaPosition, Delta.PositionDelta.Size()); 119 | } 120 | } 121 | 122 | MorphTargetMaxPositionDeltas[CurveIndex] = MaxDeltaPosition; 123 | } 124 | 125 | return MorphTargetMaxPositionDeltas; 126 | } 127 | 128 | bool UAnimCurveCompressionCodec_ACL::Compress(const FCompressibleAnimData& AnimSeq, FAnimCurveCompressionResult& OutResult) 129 | { 130 | const TArray MorphTargetMaxPositionDeltas = GetMorphTargetMaxPositionDeltas(AnimSeq, MorphTargetSource); 131 | const TArray& RawCurves = GetRawCurves(AnimSeq); 132 | 133 | const int32 NumCurves = RawCurves.Num(); 134 | if (NumCurves == 0) 135 | { 136 | // Nothing to compress 137 | OutResult.CompressedBytes.Empty(0); 138 | OutResult.Codec = this; 139 | return true; 140 | } 141 | 142 | const int32 NumSamples = GetNumSamples(AnimSeq); 143 | const float SequenceLength = AnimSeq.SequenceLength; 144 | 145 | const bool bIsStaticPose = NumSamples <= 1 || SequenceLength < 0.0001f; 146 | const float SampleRate = bIsStaticPose ? 30.0f : (float(NumSamples - 1) / SequenceLength); 147 | const float InvSampleRate = 1.0f / SampleRate; 148 | 149 | acl::track_array_float1f Tracks(ACLAllocatorImpl, NumCurves); 150 | 151 | for (int32 CurveIndex = 0; CurveIndex < NumCurves; ++CurveIndex) 152 | { 153 | const FFloatCurve& Curve = RawCurves[CurveIndex]; 154 | 155 | const float MaxPositionDelta = MorphTargetMaxPositionDeltas[CurveIndex]; 156 | 157 | // If our curve drives a morph target, we use a different precision value with world space units. 158 | // This is much easier to tune and control: 0.1mm precision is clear. 159 | // In order to this this, we must convert that precision value into a value that makes sense for the curve 160 | // since the animated blend weight doesn't have any units: it's a scaling factor. 161 | // The morph target math is like this for every vertex: result vtx = ref vtx + (target vtx - ref vtx) * blend weight 162 | // (target vtx - ref vtx) is our deformation difference (or delta) and we scale it between 0.0 and 1.0 with our blend weight. 163 | // At 0.0, the resulting vertex is 100% the reference vertex. 164 | // At 1.0, the resulting vertex is 100% the target vertex. 165 | // This can thus be re-written as follow: result vtx = ref vtx + vtx delta * blend weight 166 | // From this, it follows that any error we introduce into the blend weight will impact the delta linearly. 167 | // If our delta measures 1 meter, an error of 10% translates into 0.1 meter. 168 | // If our delta measures 1 cm, an error of 10% translates into 0.1 cm. 169 | // Thus, for a given error quantity, a larger delta means a larger resulting difference from the original value. 170 | // If the delta is zero, any error is irrelevant as it will have no measurable impact. 171 | // By dividing the precision value we want with the delta length, we can control how much precision our blend weight needs. 172 | // If we want 0.01 cm precision and our largest vertex displacement is 3 cm, the blend weight precision needs to be: 173 | // 0.01 cm / 3.00 cm = 0.0033 (with the units cancelling out just like we need) 174 | // Another way to think about it is that every 0.0033 increment of the blend weight results in an increment of 0.01 cm 175 | // when our displacement delta is 3 cm. 176 | // 0.01 cm / 50.00 cm = 0.0002 (if our delta increases, we need to retain more blend weight precision) 177 | // 0.01 cm / 1.00 cm = 0.01 178 | // Each blend weight curve will drive a different target position for many vertices and this way, we can specify 179 | // a single value for the world space precision we want to achieve for every vertex and every blend weight curve 180 | // will end up with the precision value it needs. 181 | // 182 | // If our curve doesn't drive a morph target, we use the supplied CurvePrecision instead. 183 | 184 | const float Precision = MaxPositionDelta > 0.0f ? (MorphTargetPositionPrecision / MaxPositionDelta) : CurvePrecision; 185 | 186 | acl::track_desc_scalarf Desc; 187 | Desc.output_index = CurveIndex; 188 | Desc.precision = Precision; 189 | 190 | acl::track_float1f Track = acl::track_float1f::make_reserve(Desc, ACLAllocatorImpl, NumSamples, SampleRate); 191 | for (int32 SampleIndex = 0; SampleIndex < NumSamples; ++SampleIndex) 192 | { 193 | const float SampleTime = FMath::Clamp(SampleIndex * InvSampleRate, 0.0f, SequenceLength); 194 | const float SampleValue = Curve.FloatCurve.Eval(SampleTime); 195 | 196 | Track[SampleIndex] = SampleValue; 197 | } 198 | 199 | Tracks[CurveIndex] = MoveTemp(Track); 200 | } 201 | 202 | acl::compression_settings Settings; 203 | 204 | acl::compressed_tracks* CompressedTracks = nullptr; 205 | acl::output_stats Stats; 206 | const acl::error_result CompressionResult = acl::compress_track_list(ACLAllocatorImpl, Tracks, Settings, CompressedTracks, Stats); 207 | 208 | if (CompressionResult.any()) 209 | { 210 | UE_LOG(LogAnimationCompression, Warning, TEXT("ACL failed to compress curves: %s [%s]"), ANSI_TO_TCHAR(CompressionResult.c_str()), *AnimSeq.FullName); 211 | return false; 212 | } 213 | 214 | checkSlow(CompressedTracks->is_valid(true).empty()); 215 | 216 | const uint32 CompressedDataSize = CompressedTracks->get_size(); 217 | 218 | OutResult.CompressedBytes.Empty(CompressedDataSize); 219 | OutResult.CompressedBytes.AddUninitialized(CompressedDataSize); 220 | FMemory::Memcpy(OutResult.CompressedBytes.GetData(), CompressedTracks, CompressedDataSize); 221 | 222 | OutResult.Codec = this; 223 | 224 | #if !NO_LOGGING 225 | { 226 | acl::decompression_context Context; 227 | Context.initialize(*CompressedTracks); 228 | const acl::track_error Error = acl::calculate_compression_error(ACLAllocatorImpl, Tracks, Context); 229 | 230 | UE_LOG(LogAnimationCompression, Verbose, TEXT("ACL Curves compressed size: %u bytes [%s]"), CompressedDataSize, *AnimSeq.FullName); 231 | UE_LOG(LogAnimationCompression, Verbose, TEXT("ACL Curves error: %.4f (curve %u @ %.3f) [%s]"), Error.error, Error.index, Error.sample_time, *AnimSeq.FullName); 232 | } 233 | #endif 234 | 235 | ACLAllocatorImpl.deallocate(CompressedTracks, CompressedDataSize); 236 | return true; 237 | } 238 | #endif // WITH_EDITORONLY_DATA 239 | 240 | struct UECurveDecompressionSettings final : public acl::decompression_settings 241 | { 242 | static constexpr bool is_track_type_supported(acl::track_type8 type) { return type == acl::track_type8::float1f; } 243 | 244 | // Only support our latest version 245 | static constexpr acl::compressed_tracks_version16 version_supported() { return acl::compressed_tracks_version16::latest; } 246 | 247 | #if UE_BUILD_SHIPPING 248 | // Shipping builds do not need safety checks, by then the game has been tested enough 249 | // Only data corruption could cause a safety check to fail 250 | // We keep this disabled regardless because it is generally better to output a T-pose than to have a 251 | // potential crash. Corruption can happen and it would be unfortunate if a demo or playtest failed 252 | // as a result of a crash that we can otherwise recover from. 253 | //static constexpr bool skip_initialize_safety_checks() { return true; } 254 | #endif 255 | }; 256 | 257 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 258 | struct UECurveWriter final : public acl::track_writer 259 | { 260 | TArray& Buffer; 261 | 262 | explicit UECurveWriter(TArray& Buffer_) 263 | : Buffer(Buffer_) 264 | { 265 | } 266 | 267 | FORCEINLINE_DEBUGGABLE void RTM_SIMD_CALL write_float1(uint32_t TrackIndex, rtm::scalarf_arg0 Value) 268 | { 269 | Buffer[TrackIndex] = rtm::scalar_cast(Value); 270 | } 271 | }; 272 | #else 273 | struct UECurveWriter final : public acl::track_writer 274 | { 275 | const TArray& CompressedCurveNames; 276 | FBlendedCurve& Curves; 277 | 278 | UECurveWriter(const TArray& CompressedCurveNames_, FBlendedCurve& Curves_) 279 | : CompressedCurveNames(CompressedCurveNames_) 280 | , Curves(Curves_) 281 | { 282 | } 283 | 284 | FORCEINLINE_DEBUGGABLE void RTM_SIMD_CALL write_float1(uint32_t TrackIndex, rtm::scalarf_arg0 Value) 285 | { 286 | const FSmartName& CurveName = CompressedCurveNames[TrackIndex]; 287 | if (Curves.IsEnabled(CurveName.UID)) 288 | { 289 | Curves.Set(CurveName.UID, rtm::scalar_cast(Value)); 290 | } 291 | } 292 | }; 293 | #endif 294 | 295 | void UAnimCurveCompressionCodec_ACL::DecompressCurves(const FCompressedAnimSequence& AnimSeq, FBlendedCurve& Curves, float CurrentTime) const 296 | { 297 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 298 | const TArray& IndexedCurveNames = AnimSeq.IndexedCurveNames; 299 | const int32 NumCurves = IndexedCurveNames.Num(); 300 | #else 301 | const TArray& CompressedCurveNames = AnimSeq.CompressedCurveNames; 302 | const int32 NumCurves = CompressedCurveNames.Num(); 303 | #endif 304 | 305 | if (NumCurves == 0) 306 | { 307 | return; 308 | } 309 | 310 | const acl::compressed_tracks* CompressedTracks = acl::make_compressed_tracks(AnimSeq.CompressedCurveByteStream.GetData()); 311 | check(CompressedTracks != nullptr && CompressedTracks->is_valid(false).empty()); 312 | 313 | acl::decompression_context Context; 314 | Context.initialize(*CompressedTracks); 315 | Context.seek(CurrentTime, acl::sample_rounding_policy::none); 316 | 317 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 318 | TArray DecompressionBuffer; 319 | DecompressionBuffer.SetNumUninitialized(NumCurves); 320 | 321 | UECurveWriter TrackWriter(DecompressionBuffer); 322 | #else 323 | UECurveWriter TrackWriter(CompressedCurveNames, Curves); 324 | #endif 325 | 326 | Context.decompress_tracks(TrackWriter); 327 | 328 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 329 | auto GetNameFromIndex = [&IndexedCurveNames](int32 InCurveIndex) 330 | { 331 | return IndexedCurveNames[IndexedCurveNames[InCurveIndex].CurveIndex].CurveName; 332 | }; 333 | 334 | auto GetValueFromIndex = [&DecompressionBuffer, &IndexedCurveNames](int32 InCurveIndex) 335 | { 336 | return DecompressionBuffer[IndexedCurveNames[InCurveIndex].CurveIndex]; 337 | }; 338 | 339 | UE::Anim::FCurveUtils::BuildSorted(Curves, NumCurves, GetNameFromIndex, GetValueFromIndex, Curves.GetFilter()); 340 | #endif 341 | } 342 | 343 | struct UEScalarCurveWriter final : public acl::track_writer 344 | { 345 | float SampleValue; 346 | 347 | UEScalarCurveWriter() 348 | : SampleValue(0.0f) 349 | { 350 | } 351 | 352 | FORCEINLINE_DEBUGGABLE void RTM_SIMD_CALL write_float1(uint32_t /*TrackIndex*/, rtm::scalarf_arg0 Value) 353 | { 354 | SampleValue = rtm::scalar_cast(Value); 355 | } 356 | }; 357 | 358 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 359 | float UAnimCurveCompressionCodec_ACL::DecompressCurve(const FCompressedAnimSequence& AnimSeq, FName CurveName, float CurrentTime) const 360 | #else 361 | float UAnimCurveCompressionCodec_ACL::DecompressCurve(const FCompressedAnimSequence& AnimSeq, SmartName::UID_Type CurveUID, float CurrentTime) const 362 | #endif 363 | { 364 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 365 | const TArray& IndexedCurveNames = AnimSeq.IndexedCurveNames; 366 | const int32 NumCurves = IndexedCurveNames.Num(); 367 | #else 368 | const TArray& CompressedCurveNames = AnimSeq.CompressedCurveNames; 369 | const int32 NumCurves = CompressedCurveNames.Num(); 370 | #endif 371 | 372 | if (NumCurves == 0) 373 | { 374 | return 0.0f; 375 | } 376 | 377 | const acl::compressed_tracks* CompressedTracks = acl::make_compressed_tracks(AnimSeq.CompressedCurveByteStream.GetData()); 378 | check(CompressedTracks != nullptr && CompressedTracks->is_valid(false).empty()); 379 | 380 | acl::decompression_context Context; 381 | Context.initialize(*CompressedTracks); 382 | Context.seek(CurrentTime, acl::sample_rounding_policy::none); 383 | 384 | int32 TrackIndex = -1; 385 | for (int32 CurveIndex = 0; CurveIndex < NumCurves; ++CurveIndex) 386 | { 387 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 388 | if (IndexedCurveNames[CurveIndex].CurveName == CurveName) 389 | #else 390 | if (CompressedCurveNames[CurveIndex].UID == CurveUID) 391 | #endif 392 | { 393 | TrackIndex = CurveIndex; 394 | break; 395 | } 396 | } 397 | 398 | if (TrackIndex < 0) 399 | { 400 | return 0.0f; // Track not found 401 | } 402 | 403 | UEScalarCurveWriter TrackWriter; 404 | Context.decompress_track(TrackIndex, TrackWriter); 405 | 406 | return TrackWriter.SampleValue; 407 | } 408 | 409 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/ACLPluginModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "CoreMinimal.h" 4 | #include "IACLPluginModule.h" 5 | #include "Misc/CoreDelegates.h" 6 | #include "Modules/ModuleManager.h" 7 | #include "ACLImpl.h" 8 | 9 | // Enable console commands only in development builds when logging is enabled 10 | #define WITH_ACL_CONSOLE_COMMANDS (!UE_BUILD_SHIPPING && !NO_LOGGING) 11 | 12 | #if WITH_ACL_CONSOLE_COMMANDS 13 | #include "AnimationCompressionLibraryDatabase.h" 14 | #include "AnimBoneCompressionCodec_ACLDatabase.h" 15 | 16 | #include "AnimationCompression.h" 17 | #include "Animation/AnimBoneCompressionCodec.h" 18 | #include "Animation/AnimBoneCompressionSettings.h" 19 | #include "Animation/AnimCurveCompressionCodec.h" 20 | #include "Animation/AnimCurveCompressionSettings.h" 21 | #include "HAL/IConsoleManager.h" 22 | #include "UObject/UObjectIterator.h" 23 | #endif 24 | 25 | #if WITH_EDITORONLY_DATA 26 | #include "EditorDatabaseMonitor.h" 27 | #endif 28 | 29 | ACLAllocator ACLAllocatorImpl; 30 | 31 | class FACLPlugin final : public IACLPlugin 32 | { 33 | private: 34 | /** IModuleInterface implementation */ 35 | virtual void StartupModule() override; 36 | virtual void ShutdownModule() override; 37 | 38 | #if WITH_ACL_CONSOLE_COMMANDS 39 | // Console commands 40 | void ListCodecs(const TArray& Args); 41 | void ListAnimSequences(const TArray& Args); 42 | void SetDatabaseVisualFidelity(const TArray& Args); 43 | 44 | TArray ConsoleCommands; 45 | #endif 46 | 47 | #if WITH_EDITORONLY_DATA 48 | void OnPostEngineInit(); 49 | #endif 50 | }; 51 | 52 | IMPLEMENT_MODULE(FACLPlugin, ACLPlugin) 53 | 54 | ////////////////////////////////////////////////////////////////////////// 55 | 56 | #if WITH_ACL_CONSOLE_COMMANDS 57 | template 58 | static TArray GetObjectInstancesSorted() 59 | { 60 | TArray Results; 61 | 62 | for (TObjectIterator It; It; ++It) 63 | { 64 | Results.Add(*It); 65 | } 66 | 67 | struct FCompareObjectNames 68 | { 69 | FORCEINLINE bool operator()(const ClassType& Lhs, const ClassType& Rhs) const 70 | { 71 | return Lhs.GetPathName().Compare(Rhs.GetPathName()) < 0; 72 | } 73 | }; 74 | Results.Sort(FCompareObjectNames()); 75 | 76 | return Results; 77 | } 78 | 79 | static double BytesToKB(SIZE_T NumBytes) 80 | { 81 | return (double)NumBytes / 1024.0; 82 | } 83 | 84 | static double BytesToMB(SIZE_T NumBytes) 85 | { 86 | return (double)NumBytes / (1024.0 * 1024.0); 87 | } 88 | 89 | template 90 | static double Percentage(SizeType Part, SizeType Whole) 91 | { 92 | return Whole != 0 ? (((double)Part / (double)Whole) * 100.0) : 0.0; 93 | } 94 | 95 | static SIZE_T GetCompressedBoneSize(const FCompressedAnimSequence& CompressedData) 96 | { 97 | SIZE_T Size = CompressedData.CompressedTrackToSkeletonMapTable.GetAllocatedSize(); 98 | if (CompressedData.CompressedDataStructure) 99 | { 100 | Size += CompressedData.CompressedDataStructure->GetApproxCompressedSize(); 101 | } 102 | return Size; 103 | } 104 | 105 | static SIZE_T GetCompressedCurveSize(const FCompressedAnimSequence& CompressedData) 106 | { 107 | SIZE_T Size = 0; 108 | #if ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 3 109 | Size += CompressedData.IndexedCurveNames.GetAllocatedSize(); 110 | #else 111 | Size += CompressedData.CompressedCurveNames.GetAllocatedSize(); 112 | #endif 113 | Size += CompressedData.CompressedCurveByteStream.GetAllocatedSize(); 114 | return Size; 115 | } 116 | 117 | void FACLPlugin::ListCodecs(const TArray& Args) 118 | { 119 | // Turn off log times to make diffing easier 120 | TGuardValue DisableLogTimes(GPrintLogTimes, ELogTimes::None); 121 | 122 | // Make sure to log everything 123 | const ELogVerbosity::Type OldVerbosity = LogAnimationCompression.GetVerbosity(); 124 | LogAnimationCompression.SetVerbosity(ELogVerbosity::All); 125 | 126 | const TArray BoneSettings = GetObjectInstancesSorted(); 127 | const TArray BoneCodecs = GetObjectInstancesSorted(); 128 | const TArray CurveSettings = GetObjectInstancesSorted(); 129 | const TArray CurveCodecs = GetObjectInstancesSorted(); 130 | const TArray AnimSequences = GetObjectInstancesSorted(); 131 | const TArray Databases = GetObjectInstancesSorted(); 132 | 133 | UE_LOG(LogAnimationCompression, Log, TEXT("===== Bone Compression Setting Assets =====")); 134 | for (const UAnimBoneCompressionSettings* Settings : BoneSettings) 135 | { 136 | int32 NumReferences = 0; 137 | SIZE_T TotalSize = 0; 138 | SIZE_T UsedSize = 0; 139 | for (const UAnimSequence* AnimSeq : AnimSequences) 140 | { 141 | const SIZE_T Size = GetCompressedBoneSize(AnimSeq->CompressedData); 142 | if (AnimSeq->BoneCompressionSettings == Settings) 143 | { 144 | NumReferences++; 145 | UsedSize += Size; 146 | } 147 | TotalSize += Size; 148 | } 149 | 150 | UE_LOG(LogAnimationCompression, Log, TEXT("%s ..."), *Settings->GetPathName()); 151 | UE_LOG(LogAnimationCompression, Log, TEXT(" used by %d / %d (%.1f %%) anim sequences"), NumReferences, AnimSequences.Num(), Percentage(NumReferences, AnimSequences.Num())); 152 | UE_LOG(LogAnimationCompression, Log, TEXT(" uses %.2f MB / %.2f MB (%.1f %%)"), BytesToMB(UsedSize), BytesToMB(TotalSize), Percentage(UsedSize, TotalSize)); 153 | } 154 | 155 | UE_LOG(LogAnimationCompression, Log, TEXT("===== Bone Compression Codecs =====")); 156 | for (const UAnimBoneCompressionCodec* Codec : BoneCodecs) 157 | { 158 | int32 NumReferences = 0; 159 | SIZE_T TotalSize = 0; 160 | SIZE_T UsedSize = 0; 161 | for (const UAnimSequence* AnimSeq : AnimSequences) 162 | { 163 | const SIZE_T Size = GetCompressedBoneSize(AnimSeq->CompressedData); 164 | if (AnimSeq->CompressedData.BoneCompressionCodec == Codec) 165 | { 166 | NumReferences++; 167 | UsedSize += Size; 168 | } 169 | TotalSize += Size; 170 | } 171 | 172 | if (Codec->Description.IsEmpty()) 173 | { 174 | UE_LOG(LogAnimationCompression, Log, TEXT("%s ..."), *Codec->GetPathName()); 175 | } 176 | else 177 | { 178 | UE_LOG(LogAnimationCompression, Log, TEXT("%s (%s) ..."), *Codec->GetPathName(), *Codec->Description); 179 | } 180 | 181 | UE_LOG(LogAnimationCompression, Log, TEXT(" used by %d / %d (%.1f %%) anim sequences"), NumReferences, AnimSequences.Num(), Percentage(NumReferences, AnimSequences.Num())); 182 | UE_LOG(LogAnimationCompression, Log, TEXT(" uses %.2f MB / %.2f MB (%.1f %%)"), BytesToMB(UsedSize), BytesToMB(TotalSize), Percentage(UsedSize, TotalSize)); 183 | } 184 | 185 | UE_LOG(LogAnimationCompression, Log, TEXT("===== Curve Compression Setting Assets =====")); 186 | for (const UAnimCurveCompressionSettings* Settings : CurveSettings) 187 | { 188 | int32 NumReferences = 0; 189 | SIZE_T TotalSize = 0; 190 | SIZE_T UsedSize = 0; 191 | for (const UAnimSequence* AnimSeq : AnimSequences) 192 | { 193 | const SIZE_T Size = GetCompressedCurveSize(AnimSeq->CompressedData); 194 | if (AnimSeq->CurveCompressionSettings == Settings) 195 | { 196 | NumReferences++; 197 | UsedSize += Size; 198 | } 199 | TotalSize += Size; 200 | } 201 | 202 | UE_LOG(LogAnimationCompression, Log, TEXT("%s ..."), *Settings->GetPathName()); 203 | UE_LOG(LogAnimationCompression, Log, TEXT(" used by %d / %d (%.1f %%) anim sequences"), NumReferences, AnimSequences.Num(), Percentage(NumReferences, AnimSequences.Num())); 204 | UE_LOG(LogAnimationCompression, Log, TEXT(" uses %.2f MB / %.2f MB (%.1f %%)"), BytesToMB(UsedSize), BytesToMB(TotalSize), Percentage(UsedSize, TotalSize)); 205 | } 206 | 207 | UE_LOG(LogAnimationCompression, Log, TEXT("===== Curve Compression Codecs =====")); 208 | for (const UAnimCurveCompressionCodec* Codec : CurveCodecs) 209 | { 210 | int32 NumReferences = 0; 211 | SIZE_T TotalSize = 0; 212 | SIZE_T UsedSize = 0; 213 | for (const UAnimSequence* AnimSeq : AnimSequences) 214 | { 215 | const SIZE_T Size = GetCompressedCurveSize(AnimSeq->CompressedData); 216 | if (AnimSeq->CompressedData.CurveCompressionCodec == Codec) 217 | { 218 | NumReferences++; 219 | UsedSize += Size; 220 | } 221 | TotalSize += Size; 222 | } 223 | 224 | UE_LOG(LogAnimationCompression, Log, TEXT("%s ..."), *Codec->GetPathName()); 225 | UE_LOG(LogAnimationCompression, Log, TEXT(" used by %d / %d (%.1f %%) anim sequences"), NumReferences, AnimSequences.Num(), Percentage(NumReferences, AnimSequences.Num())); 226 | UE_LOG(LogAnimationCompression, Log, TEXT(" uses %.2f MB / %.2f MB (%.1f %%)"), BytesToMB(UsedSize), BytesToMB(TotalSize), Percentage(UsedSize, TotalSize)); 227 | } 228 | 229 | UE_LOG(LogAnimationCompression, Log, TEXT("===== Animation Compression Library Database Assets =====")); 230 | for (const UAnimationCompressionLibraryDatabase* Database : Databases) 231 | { 232 | int32 NumReferences = 0; 233 | for (const UAnimSequence* AnimSeq : AnimSequences) 234 | { 235 | UAnimBoneCompressionCodec_ACLDatabase* Codec = Cast(AnimSeq->CompressedData.BoneCompressionCodec); 236 | if (Codec != nullptr && Codec->DatabaseAsset == Database) 237 | { 238 | NumReferences++; 239 | } 240 | } 241 | 242 | const acl::compressed_database* CompressedDatabase = acl::make_compressed_database(Database->CookedCompressedBytes.GetData()); 243 | 244 | const uint32 DatabaseTotalSize = CompressedDatabase != nullptr ? CompressedDatabase->get_total_size() : 0; 245 | const uint32 DatabaseSize = CompressedDatabase != nullptr ? CompressedDatabase->get_size() : 0; 246 | const uint32 DatabaseBulkDataSizeMedium = CompressedDatabase != nullptr ? CompressedDatabase->get_bulk_data_size(acl::quality_tier::medium_importance) : 0; 247 | const uint32 DatabaseBulkDataSizeLow = CompressedDatabase != nullptr ? CompressedDatabase->get_bulk_data_size(acl::quality_tier::lowest_importance) : 0; 248 | const uint32 DatabaseBulkDataSize = DatabaseBulkDataSizeMedium + DatabaseBulkDataSizeLow; 249 | const uint32 SequencesSize = Database->CookedCompressedBytes.Num() - DatabaseSize; // CompressedBytes contains the DB metadata and the sequences but not the bulk data 250 | 251 | UE_LOG(LogAnimationCompression, Log, TEXT("%s ..."), *Database->GetPathName()); 252 | UE_LOG(LogAnimationCompression, Log, TEXT(" used by %d / %d (%.1f %%) anim sequences"), NumReferences, AnimSequences.Num(), Percentage(NumReferences, AnimSequences.Num())); 253 | UE_LOG(LogAnimationCompression, Log, TEXT(" sequences use %.2f MB"), BytesToMB(SequencesSize)); 254 | UE_LOG(LogAnimationCompression, Log, TEXT(" database uses %.2f MB (%.2f MB streamable)"), BytesToMB(DatabaseTotalSize), BytesToMB(DatabaseBulkDataSize)); 255 | } 256 | 257 | LogAnimationCompression.SetVerbosity(OldVerbosity); 258 | } 259 | 260 | void FACLPlugin::ListAnimSequences(const TArray& Args) 261 | { 262 | // Turn off log times to make diffing easier 263 | TGuardValue DisableLogTimes(GPrintLogTimes, ELogTimes::None); 264 | 265 | // Make sure to log everything 266 | const ELogVerbosity::Type OldVerbosity = LogAnimationCompression.GetVerbosity(); 267 | LogAnimationCompression.SetVerbosity(ELogVerbosity::All); 268 | 269 | const TArray AnimSequences = GetObjectInstancesSorted(); 270 | 271 | SIZE_T BoneDataTotalSize = 0; 272 | SIZE_T CurveDataTotalSize = 0; 273 | 274 | UE_LOG(LogAnimationCompression, Log, TEXT("===== Anim Sequence Assets =====")); 275 | for (const UAnimSequence* AnimSeq : AnimSequences) 276 | { 277 | UE_LOG(LogAnimationCompression, Log, TEXT("%s ..."), *AnimSeq->GetPathName()); 278 | 279 | if (AnimSeq->CompressedData.BoneCompressionCodec->Description.IsEmpty()) 280 | { 281 | UE_LOG(LogAnimationCompression, Log, TEXT(" uses bone codec %s"), *AnimSeq->CompressedData.BoneCompressionCodec->GetPathName()); 282 | } 283 | else 284 | { 285 | UE_LOG(LogAnimationCompression, Log, TEXT(" uses bone codec %s (%s)"), *AnimSeq->CompressedData.BoneCompressionCodec->GetPathName(), *AnimSeq->CompressedData.BoneCompressionCodec->Description); 286 | } 287 | 288 | const SIZE_T BoneDataSize = GetCompressedBoneSize(AnimSeq->CompressedData); 289 | UE_LOG(LogAnimationCompression, Log, TEXT(" has %.2f KB of bone data"), BytesToKB(BoneDataSize)); 290 | 291 | #if WITH_EDITORONLY_DATA 292 | if (AnimSeq->CompressedData.CompressedDataStructure) 293 | { 294 | UE_LOG(LogAnimationCompression, Log, TEXT(" has a bone error of %.4f cm"), AnimSeq->CompressedData.CompressedDataStructure->BoneCompressionErrorStats.MaxError); 295 | } 296 | #endif 297 | 298 | UE_LOG(LogAnimationCompression, Log, TEXT(" uses curve codec %s"), *AnimSeq->CompressedData.CurveCompressionCodec->GetPathName()); 299 | 300 | const SIZE_T CurveDataSize = GetCompressedCurveSize(AnimSeq->CompressedData); 301 | UE_LOG(LogAnimationCompression, Log, TEXT(" has %.2f KB of curve data"), BytesToKB(CurveDataSize)); 302 | 303 | BoneDataTotalSize += BoneDataSize; 304 | CurveDataTotalSize += CurveDataSize; 305 | } 306 | 307 | UE_LOG(LogAnimationCompression, Log, TEXT("Total bone data size: %.2f MB"), BytesToMB(BoneDataTotalSize)); 308 | UE_LOG(LogAnimationCompression, Log, TEXT("Total curve data size: %.2f MB"), BytesToMB(CurveDataTotalSize)); 309 | 310 | LogAnimationCompression.SetVerbosity(OldVerbosity); 311 | } 312 | 313 | void FACLPlugin::SetDatabaseVisualFidelity(const TArray& Args) 314 | { 315 | // Make sure to log everything 316 | const ELogVerbosity::Type OldVerbosity = LogAnimationCompression.GetVerbosity(); 317 | LogAnimationCompression.SetVerbosity(ELogVerbosity::All); 318 | 319 | ACLVisualFidelity Fidelity = ACLVisualFidelity::Highest; 320 | if (Args.Contains(TEXT("Highest"))) 321 | { 322 | Fidelity = ACLVisualFidelity::Highest; 323 | } 324 | else if (Args.Contains(TEXT("Medium"))) 325 | { 326 | Fidelity = ACLVisualFidelity::Medium; 327 | } 328 | else if (Args.Contains(TEXT("Lowest"))) 329 | { 330 | Fidelity = ACLVisualFidelity::Lowest; 331 | } 332 | else if (Args.Num() != 0) 333 | { 334 | UE_LOG(LogAnimationCompression, Warning, TEXT("Invalid visual fidelity: %s"), *Args[0]); 335 | } 336 | 337 | const TArray DatabaseAssets = GetObjectInstancesSorted(); 338 | for (UAnimationCompressionLibraryDatabase* DatabaseAsset : DatabaseAssets) 339 | { 340 | DatabaseAsset->SetVisualFidelity(Fidelity); 341 | } 342 | 343 | LogAnimationCompression.SetVerbosity(OldVerbosity); 344 | } 345 | #endif 346 | 347 | #if WITH_EDITORONLY_DATA 348 | void FACLPlugin::OnPostEngineInit() 349 | { 350 | EditorDatabaseMonitor::RegisterMonitor(); 351 | } 352 | #endif 353 | 354 | void FACLPlugin::StartupModule() 355 | { 356 | #if WITH_ACL_CONSOLE_COMMANDS 357 | if (!IsRunningCommandlet()) 358 | { 359 | ConsoleCommands.Add(IConsoleManager::Get().RegisterConsoleCommand( 360 | TEXT("ACL.ListCodecs"), 361 | TEXT("Dumps statistics about animation codecs to the log."), 362 | FConsoleCommandWithArgsDelegate::CreateRaw(this, &FACLPlugin::ListCodecs), 363 | ECVF_Default 364 | )); 365 | 366 | ConsoleCommands.Add(IConsoleManager::Get().RegisterConsoleCommand( 367 | TEXT("ACL.ListAnimSequences"), 368 | TEXT("Dumps statistics about animation sequences to the log."), 369 | FConsoleCommandWithArgsDelegate::CreateRaw(this, &FACLPlugin::ListAnimSequences), 370 | ECVF_Default 371 | )); 372 | 373 | ConsoleCommands.Add(IConsoleManager::Get().RegisterConsoleCommand( 374 | TEXT("ACL.SetDatabaseVisualFidelity"), 375 | TEXT("Sets the visual fidelity of all ACL databases. Argument: Highest (default if no argument is provided), Medium, Lowest"), 376 | FConsoleCommandWithArgsDelegate::CreateRaw(this, &FACLPlugin::SetDatabaseVisualFidelity), 377 | ECVF_Default 378 | )); 379 | } 380 | #endif 381 | 382 | #if WITH_EDITORONLY_DATA 383 | FCoreDelegates::OnPostEngineInit.AddRaw(this, &FACLPlugin::OnPostEngineInit); 384 | #endif 385 | } 386 | 387 | void FACLPlugin::ShutdownModule() 388 | { 389 | #if WITH_EDITORONLY_DATA 390 | FCoreDelegates::OnPostEngineInit.RemoveAll(this); 391 | 392 | EditorDatabaseMonitor::UnregisterMonitor(); 393 | #endif 394 | 395 | #if WITH_ACL_CONSOLE_COMMANDS 396 | for (IConsoleObject* Cmd : ConsoleCommands) 397 | { 398 | IConsoleManager::Get().UnregisterConsoleObject(Cmd); 399 | } 400 | ConsoleCommands.Empty(); 401 | #endif 402 | } 403 | -------------------------------------------------------------------------------- /ACLPlugin/Source/ACLPlugin/Private/AnimBoneCompressionCodec_ACLDatabase.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Frechette. All Rights Reserved. 2 | 3 | #include "AnimBoneCompressionCodec_ACLDatabase.h" 4 | 5 | #include "Algo/BinarySearch.h" 6 | 7 | #if WITH_EDITORONLY_DATA 8 | #include "Animation/AnimBoneCompressionSettings.h" 9 | #include "Engine/SkeletalMesh.h" 10 | #include "Rendering/SkeletalMeshModel.h" 11 | 12 | #if ENGINE_MAJOR_VERSION >= 5 13 | #include "UObject/ObjectSaveContext.h" 14 | #endif 15 | 16 | #include "ACLImpl.h" 17 | #include "EditorDatabaseMonitor.h" 18 | 19 | THIRD_PARTY_INCLUDES_START 20 | #include 21 | #include 22 | #include 23 | #include 24 | THIRD_PARTY_INCLUDES_END 25 | #endif // WITH_EDITORONLY_DATA 26 | 27 | #include "ACLDecompressionImpl.h" 28 | 29 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 30 | #include UE_INLINE_GENERATED_CPP_BY_NAME(AnimBoneCompressionCodec_ACLDatabase) 31 | #endif 32 | 33 | void FACLDatabaseCompressedAnimData::SerializeCompressedData(FArchive& Ar) 34 | { 35 | ICompressedAnimData::SerializeCompressedData(Ar); 36 | 37 | Ar << SequenceNameHash; 38 | 39 | #if WITH_EDITORONLY_DATA 40 | if (!Ar.IsFilterEditorOnly()) 41 | { 42 | Ar << CompressedClip; 43 | } 44 | #endif 45 | } 46 | 47 | void FACLDatabaseCompressedAnimData::Bind(const TArrayView BulkData) 48 | { 49 | check(BulkData.Num() == 0); // Should always be empty 50 | 51 | #if WITH_EDITORONLY_DATA 52 | // We have fresh new compressed data which means either we ran compression or we loaded from the DDC 53 | // We can't tell which is which so mark the database as being potentially dirty 54 | EditorDatabaseMonitor::MarkDirty(Codec->DatabaseAsset); 55 | #else 56 | // In a cooked build, we lookup our anim sequence and database from the database asset 57 | // We search by the sequence hash which lives in the top 32 bits of each entry 58 | const int32 SequenceIndex = Algo::BinarySearchBy(Codec->DatabaseAsset->CookedAnimSequenceMappings, SequenceNameHash, [](uint64 InValue) { return uint32(InValue >> 32); }); 59 | if (SequenceIndex != INDEX_NONE) 60 | { 61 | const uint32 CompressedClipOffset = uint32(Codec->DatabaseAsset->CookedAnimSequenceMappings[SequenceIndex]); // Truncate top 32 bits 62 | uint8* CompressedBytes = Codec->DatabaseAsset->CookedCompressedBytes.GetData() + CompressedClipOffset; 63 | 64 | const acl::compressed_tracks* CompressedClipData = acl::make_compressed_tracks(CompressedBytes); 65 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 66 | 67 | const uint32 CompressedSize = CompressedClipData->get_size(); 68 | 69 | CompressedByteStream = TArrayView(CompressedBytes, CompressedSize); 70 | DatabaseContext = &Codec->DatabaseAsset->DatabaseContext; 71 | } 72 | else 73 | { 74 | // This sequence doesn't live in the database, the mapping must be stale 75 | UE_LOG(LogAnimationCompression, Warning, TEXT("ACL Database mapping is stale. [0x%X] should be contained but isn't."), SequenceNameHash); 76 | 77 | // Since we have no sequence data, decompression will yield a T-pose 78 | } 79 | #endif 80 | } 81 | 82 | int64 FACLDatabaseCompressedAnimData::GetApproxCompressedSize() const 83 | { 84 | #if WITH_EDITORONLY_DATA 85 | return CompressedClip.Num(); 86 | #else 87 | return CompressedByteStream.Num(); 88 | #endif 89 | } 90 | 91 | bool FACLDatabaseCompressedAnimData::IsValid() const 92 | { 93 | const acl::compressed_tracks* CompressedClipData = GetCompressedTracks(); 94 | if (CompressedClipData == nullptr || CompressedClipData->is_valid(false).any()) 95 | { 96 | return false; 97 | } 98 | 99 | #if !WITH_EDITORONLY_DATA 100 | if (DatabaseContext == nullptr) 101 | { 102 | return false; 103 | } 104 | #endif 105 | 106 | return true; 107 | } 108 | 109 | #if WITH_EDITORONLY_DATA 110 | FACLDatabaseCompressedAnimData::~FACLDatabaseCompressedAnimData() 111 | { 112 | // Our compressed data is being destroyed which means either we are unloading or we are about to have 113 | // new compressed data. If the new codec isn't an ACL instance we have no way of knowing so assume the 114 | // database is dirty and double check. 115 | EditorDatabaseMonitor::MarkDirty(Codec->DatabaseAsset); 116 | } 117 | #endif 118 | 119 | UAnimBoneCompressionCodec_ACLDatabase::UAnimBoneCompressionCodec_ACLDatabase(const FObjectInitializer& ObjectInitializer) 120 | : Super(ObjectInitializer) 121 | , DatabaseAsset(nullptr) 122 | { 123 | } 124 | 125 | #if WITH_EDITORONLY_DATA 126 | void UAnimBoneCompressionCodec_ACLDatabase::GetPreloadDependencies(TArray& OutDeps) 127 | { 128 | Super::GetPreloadDependencies(OutDeps); 129 | 130 | // We preload the database asset because we need it loaded during Serialize to lookup the proper sequence data 131 | if (DatabaseAsset != nullptr) 132 | { 133 | OutDeps.Add(DatabaseAsset); 134 | } 135 | } 136 | 137 | #if ENGINE_MAJOR_VERSION >= 5 138 | void UAnimBoneCompressionCodec_ACLDatabase::PreSave(FObjectPreSaveContext ObjectSaveContext) 139 | #else 140 | void UAnimBoneCompressionCodec_ACLDatabase::PreSave(const class ITargetPlatform* TargetPlatform) 141 | #endif 142 | { 143 | #if ENGINE_MAJOR_VERSION >= 5 144 | Super::PreSave(ObjectSaveContext); 145 | #else 146 | Super::PreSave(TargetPlatform); 147 | #endif 148 | 149 | UAnimBoneCompressionSettings* Settings = Cast(GetOuter()); 150 | if (Settings != nullptr && Settings->Codecs.Num() != 1) 151 | { 152 | UE_LOG(LogAnimationCompression, Error, TEXT("ACL database codec must be the only codec in its parent bone compression settings asset. [%s]"), *Settings->GetPathName()); 153 | } 154 | } 155 | 156 | void UAnimBoneCompressionCodec_ACLDatabase::PostCompression(const FCompressibleAnimData& CompressibleAnimData, FCompressibleAnimDataResult& OutResult) const 157 | { 158 | // After we are done compressing our animation sequence, it will contain the necessary metadata needed to build our 159 | // streaming database. The anim sequence will contain every sample and it will be used as-is in the editor where we 160 | // show the highest quality by default. 161 | // 162 | // However, the anim sequence data that we just compressed will not be used in a cooked build. When we build our 163 | // database, the sequence data will be modified since we'll remove key frames from it. Its hash will change. 164 | // The new compressed data will live in the database asset next to the compressed database data. This has the benefit 165 | // that every compressed clip and the database now live in the same region of virtual memory, reducing the TLB miss 166 | // rate (when large pages are used on console and mobile since multiple clips fit within a page) and when we do miss 167 | // the TLB, it will be cheaper since most of the mapping levels are shared. 168 | // 169 | // To that end, the data we just compressed will not be serialized in cooked builds, it only lives in the DDC and in memory 170 | // while in the editor. To be able to find our new sequence data at runtime, we compute the hash from the sequence name. 171 | 172 | FACLDatabaseCompressedAnimData& AnimData = static_cast(*OutResult.AnimData); 173 | 174 | // Store the sequence full name's hash since we need it in cooked builds to find our data 175 | AnimData.SequenceNameHash = GetTypeHash(CompressibleAnimData.FullName); 176 | 177 | // Copy the sequence data 178 | AnimData.CompressedClip = OutResult.CompressedByteStream; 179 | 180 | // When we have a database, the compressed sequence data lives in the database, zero out the compressed byte buffer 181 | // since we handle the data manually 182 | OutResult.CompressedByteStream.Empty(0); 183 | } 184 | 185 | void UAnimBoneCompressionCodec_ACLDatabase::GetCompressionSettings(const class ITargetPlatform* TargetPlatform, acl::compression_settings& OutSettings) const 186 | { 187 | OutSettings = acl::get_default_compression_settings(); 188 | 189 | OutSettings.level = GetCompressionLevel(CompressionLevel); 190 | OutSettings.enable_database_support = true; 191 | 192 | // Disable keyframe stripping, even the trivial one as it currently isn't supported 193 | OutSettings.keyframe_stripping.strip_trivial = false; 194 | } 195 | 196 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 197 | void UAnimBoneCompressionCodec_ACLDatabase::PopulateDDCKey(const UE::Anim::Compression::FAnimDDCKeyArgs& KeyArgs, FArchive& Ar) 198 | #else 199 | void UAnimBoneCompressionCodec_ACLDatabase::PopulateDDCKey(FArchive& Ar) 200 | #endif 201 | { 202 | #if (ENGINE_MAJOR_VERSION >= 5 && ENGINE_MINOR_VERSION >= 1) 203 | Super::PopulateDDCKey(KeyArgs, Ar); 204 | 205 | const class ITargetPlatform* TargetPlatform = KeyArgs.TargetPlatform; 206 | #else 207 | Super::PopulateDDCKey(Ar); 208 | 209 | const class ITargetPlatform* TargetPlatform = nullptr; 210 | #endif 211 | 212 | acl::compression_settings Settings; 213 | GetCompressionSettings(TargetPlatform, Settings); 214 | 215 | uint32 ForceRebuildVersion = 4; 216 | uint32 SettingsHash = Settings.get_hash(); 217 | 218 | Ar << ForceRebuildVersion << SettingsHash; 219 | 220 | for (USkeletalMesh* SkelMesh : OptimizationTargets) 221 | { 222 | FSkeletalMeshModel* MeshModel = SkelMesh != nullptr ? SkelMesh->GetImportedModel() : nullptr; 223 | if (MeshModel != nullptr) 224 | { 225 | Ar << MeshModel->SkeletalMeshModelGUID; 226 | } 227 | } 228 | } 229 | #endif // WITH_EDITORONLY_DATA 230 | 231 | TUniquePtr UAnimBoneCompressionCodec_ACLDatabase::AllocateAnimData() const 232 | { 233 | TUniquePtr AnimData = MakeUnique(); 234 | 235 | AnimData->Codec = const_cast(this); 236 | 237 | return AnimData; 238 | } 239 | 240 | void UAnimBoneCompressionCodec_ACLDatabase::ByteSwapIn(ICompressedAnimData& AnimData, TArrayView CompressedData, FMemoryReader& MemoryStream) const 241 | { 242 | #if !PLATFORM_LITTLE_ENDIAN 243 | #error "ACL does not currently support big-endian platforms" 244 | #endif 245 | 246 | // ByteSwapIn(..) is called on load 247 | 248 | // TODO: ACL does not support byte swapping 249 | 250 | // Because we manage the memory manually, the compressed data should always be empty 251 | check(CompressedData.Num() == 0); 252 | } 253 | 254 | void UAnimBoneCompressionCodec_ACLDatabase::ByteSwapOut(ICompressedAnimData& AnimData, TArrayView CompressedData, FMemoryWriter& MemoryStream) const 255 | { 256 | #if !PLATFORM_LITTLE_ENDIAN 257 | #error "ACL does not currently support big-endian platforms" 258 | #endif 259 | 260 | // ByteSwapOut(..) is called on save, during cooking, or when counting memory 261 | 262 | // TODO: ACL does not support byte swapping 263 | 264 | #if WITH_EDITORONLY_DATA 265 | // In the editor, if we are saving or cooking, the output should be empty since we manage the memory manually. 266 | // The real editor data lives in FACLDatabaseCompressedAnimData::CompressedClip 267 | // 268 | // Sadly, we have no way of knowing if we are counting memory from here and as such we'll contribute no size. 269 | // For a true memory report, it is best to run it with cooked data anyway. 270 | check(CompressedData.Num() == 0); 271 | #else 272 | // With cooked data, we are never saving unless it is to count memory. 273 | // Since the actual sequence data lives in the database, its size will be tracked there. 274 | // We'll do nothing here to avoid counting twice. 275 | #endif 276 | } 277 | 278 | void UAnimBoneCompressionCodec_ACLDatabase::DecompressPose(FAnimSequenceDecompressionContext& DecompContext, const BoneTrackArray& RotationPairs, const BoneTrackArray& TranslationPairs, const BoneTrackArray& ScalePairs, TArrayView& OutAtoms) const 279 | { 280 | const FACLDatabaseCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 281 | 282 | acl::decompression_context ACLContext; 283 | 284 | #if WITH_EDITORONLY_DATA 285 | acl::database_context* DatabaseContext = DatabaseAsset != nullptr ? &DatabaseAsset->DatabaseContext : nullptr; 286 | if (DatabaseContext != nullptr && DatabaseContext->is_initialized()) 287 | { 288 | // We are previewing, use the database and the anim sequence data contained within it 289 | 290 | // Lookup our anim sequence from the database asset 291 | // We search by the sequence hash which lives in the top 32 bits of each entry 292 | const int32 SequenceIndex = Algo::BinarySearchBy(DatabaseAsset->PreviewAnimSequenceMappings, AnimData.SequenceNameHash, [](uint64 InValue) { return uint32(InValue >> 32); }); 293 | if (SequenceIndex != INDEX_NONE) 294 | { 295 | const uint32 CompressedClipOffset = uint32(DatabaseAsset->PreviewAnimSequenceMappings[SequenceIndex]); // Truncate top 32 bits 296 | const uint8* CompressedBytes = DatabaseAsset->PreviewCompressedBytes.GetData() + CompressedClipOffset; 297 | 298 | const acl::compressed_tracks* CompressedClipData = acl::make_compressed_tracks(CompressedBytes); 299 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 300 | 301 | ACLContext.initialize(*CompressedClipData, *DatabaseContext); 302 | } 303 | } 304 | 305 | if (!ACLContext.is_initialized()) 306 | { 307 | // No preview or we live updated things and the monitor hasn't caught up yet 308 | // Use the full quality that lives in the anim sequence 309 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 310 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 311 | 312 | ACLContext.initialize(*CompressedClipData); 313 | } 314 | #else 315 | if (AnimData.CompressedByteStream.Num() == 0) 316 | { 317 | return; // Our mapping must have been stale 318 | } 319 | 320 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 321 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 322 | 323 | if (AnimData.DatabaseContext == nullptr || !ACLContext.initialize(*CompressedClipData, *AnimData.DatabaseContext)) 324 | { 325 | UE_LOG(LogAnimationCompression, Warning, TEXT("ACL failed initialize decompression context, database won't be used")); 326 | 327 | ACLContext.initialize(*CompressedClipData); 328 | } 329 | #endif 330 | 331 | ::DecompressPose(DecompContext, ACLContext, RotationPairs, TranslationPairs, ScalePairs, OutAtoms); 332 | } 333 | 334 | void UAnimBoneCompressionCodec_ACLDatabase::DecompressBone(FAnimSequenceDecompressionContext& DecompContext, int32 TrackIndex, FTransform& OutAtom) const 335 | { 336 | const FACLDatabaseCompressedAnimData& AnimData = static_cast(DecompContext.CompressedAnimData); 337 | 338 | acl::decompression_context ACLContext; 339 | 340 | #if WITH_EDITORONLY_DATA 341 | acl::database_context* DatabaseContext = DatabaseAsset != nullptr ? &DatabaseAsset->DatabaseContext : nullptr; 342 | if (DatabaseContext != nullptr && DatabaseContext->is_initialized()) 343 | { 344 | // We are previewing, use the database and the anim sequence data contained within it 345 | 346 | // Lookup our anim sequence from the database asset 347 | // We search by the sequence hash which lives in the top 32 bits of each entry 348 | const int32 SequenceIndex = Algo::BinarySearchBy(DatabaseAsset->PreviewAnimSequenceMappings, AnimData.SequenceNameHash, [](uint64 InValue) { return uint32(InValue >> 32); }); 349 | if (SequenceIndex != INDEX_NONE) 350 | { 351 | const uint32 CompressedClipOffset = uint32(DatabaseAsset->PreviewAnimSequenceMappings[SequenceIndex]); // Truncate top 32 bits 352 | const uint8* CompressedBytes = DatabaseAsset->PreviewCompressedBytes.GetData() + CompressedClipOffset; 353 | 354 | const acl::compressed_tracks* CompressedClipData = acl::make_compressed_tracks(CompressedBytes); 355 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 356 | 357 | ACLContext.initialize(*CompressedClipData, *DatabaseContext); 358 | } 359 | } 360 | 361 | if (!ACLContext.is_initialized()) 362 | { 363 | // No preview or we live updated things and the monitor hasn't caught up yet 364 | // Use the full quality that lives in the anim sequence 365 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 366 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 367 | 368 | ACLContext.initialize(*CompressedClipData); 369 | } 370 | #else 371 | if (AnimData.CompressedByteStream.Num() == 0) 372 | { 373 | return; // Our mapping must have been stale 374 | } 375 | 376 | const acl::compressed_tracks* CompressedClipData = AnimData.GetCompressedTracks(); 377 | check(CompressedClipData != nullptr && CompressedClipData->is_valid(false).empty()); 378 | 379 | if (AnimData.DatabaseContext == nullptr || !ACLContext.initialize(*CompressedClipData, *AnimData.DatabaseContext)) 380 | { 381 | UE_LOG(LogAnimationCompression, Warning, TEXT("ACL failed initialize decompression context, database won't be used")); 382 | 383 | ACLContext.initialize(*CompressedClipData); 384 | } 385 | #endif 386 | 387 | ::DecompressBone(DecompContext, ACLContext, TrackIndex, OutAtom); 388 | } 389 | 390 | --------------------------------------------------------------------------------