/* * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or * its licensors. * * For complete copyright and license terms please see the LICENSE at the root of this * distribution (the "License"). All use of this software is governed by the License, * or, if provided, by the license below or the license accompanying this file. Do not * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace EMotionFX { struct SampleParameterSet { std::vector> m_vec2Params; std::vector> m_boolParams; std::vector> m_floatParams; }; struct PerformanceTestParameters { const char* m_description; float m_fps; float m_motionSamplingRate; float m_totalTestTimeInSeconds; size_t m_numInstances; size_t m_numSkinAttachmentsPerInstance; AZ::u32 m_lodLevel; bool m_includeSoftwareSkinning; bool m_useMultiThreading; void Print() const { AZ_Printf("EMotionFX", "-------------------------------\n"); AZ_Printf("EMotionFX", "- Performance Test Parameters -\n"); AZ_Printf("EMotionFX", "- Description: %s -\n", m_description); AZ_Printf("EMotionFX", "- FPS: %f\n", m_fps); AZ_Printf("EMotionFX", "- Motion Sampling Rate: %f\n", m_motionSamplingRate); AZ_Printf("EMotionFX", "- Total Time (s): %f\n", m_totalTestTimeInSeconds); const size_t numFrames = static_cast(m_totalTestTimeInSeconds * m_fps); AZ_Printf("EMotionFX", "- Frames: %d\n", numFrames); AZ_Printf("EMotionFX", "- Frames: %d\n", numFrames); AZ_Printf("EMotionFX", "- Num Instances: %d\n", m_numInstances); AZ_Printf("EMotionFX", "- Num Skin Attachents per Instance: %d\n", m_numSkinAttachmentsPerInstance); AZ_Printf("EMotionFX", "- LOD Level: %d\n", m_lodLevel); AZ_Printf("EMotionFX", "- Software Skin: %s\n", m_includeSoftwareSkinning ? "True" : "False"); AZ_Printf("EMotionFX", "- Multi-threading:%s\n", m_useMultiThreading ? "True" : "False"); } }; class PerformanceTestFixture : public SampleGameFixture , public ::testing::WithParamInterface { public: void SetUp() override { SampleGameFixture::SetUp(); m_random = new AZ::SimpleLcgRandom(); m_random->SetSeed(875960); SampleParameterSet params; params.m_floatParams.push_back({ "movement_speed", 0.0f }); params.m_vec2Params.push_back({"movement_direction", AZ::Vector2(0.0f, 0.0f) }); params.m_boolParams.push_back({"jumping", false}); params.m_boolParams.push_back({"attacking", true}); m_pyroParameterSet.emplace_back(params); params.m_floatParams.push_back({ "movement_speed", 1.0f }); params.m_vec2Params.push_back({ "movement_direction", AZ::Vector2(1.0f, 0.0f) }); params.m_boolParams.push_back({ "jumping", false }); params.m_boolParams.push_back({ "attacking", false }); m_pyroParameterSet.emplace_back(params); params.m_floatParams.push_back({ "movement_speed", 0.5f }); params.m_vec2Params.push_back({ "movement_direction", AZ::Vector2(0.5f, 0.5f) }); params.m_boolParams.push_back({ "jumping", false }); params.m_boolParams.push_back({ "attacking", false }); m_pyroParameterSet.emplace_back(params); params.m_floatParams.push_back({ "movement_speed", 0.0f }); params.m_vec2Params.push_back({ "movement_direction", AZ::Vector2(0.0f, 0.0f) }); params.m_boolParams.push_back({ "jumping", true }); params.m_boolParams.push_back({ "attacking", false }); m_pyroParameterSet.emplace_back(params); } void TearDown() override { delete m_random; SampleGameFixture::TearDown(); } float RandomRange(float min, float max) const { return min + m_random->GetRandomFloat() * (max - min); } AZ::Vector3 RandomRangeVec3(float min, float max) const { return AZ::Vector3( RandomRange(min, max), RandomRange(min, max), RandomRange(min, max)); } void RandomizeParameters(const AZStd::vector& actorInstances) { for (ActorInstance* actorInstance : actorInstances) { AnimGraphInstance* animGraphInstance = actorInstance->GetAnimGraphInstance(); AZ_Assert(animGraphInstance, "Actor instance should have an anim graph instance playing."); const AnimGraph* animGraph = animGraphInstance->GetAnimGraph(); const size_t parameterSetIndex = static_cast(RandomRange(0.0f, static_cast(m_pyroParameterSet.size())-0.0001f)); const SampleParameterSet& params = m_pyroParameterSet[parameterSetIndex]; // Set parameters to default values. const size_t numParameters = animGraph->GetNumValueParameters(); for (size_t i = 0; i < numParameters; ++i) { const ValueParameter* parameter = animGraph->FindValueParameter(i); MCore::Attribute* attribute = animGraphInstance->GetParameterValue(static_cast(i)); if (parameter->RTTI_GetType() == azrtti_typeid()) { const BoolParameter* boolParameter = static_cast(parameter); MCore::AttributeBool* boolAttribute = static_cast(attribute); boolAttribute->SetValue(boolParameter->GetDefaultValue()); } if (parameter->RTTI_GetType() == azrtti_typeid()) { const FloatParameter* floatParameter = static_cast(parameter); MCore::AttributeFloat* floatAttribute = static_cast(attribute); floatAttribute->SetValue(floatParameter->GetDefaultValue()); } if (parameter->RTTI_GetType() == azrtti_typeid()) { const Vector2Parameter* vec2Parameter = static_cast(parameter); MCore::AttributeVector2* vec2Attribute = static_cast(attribute); vec2Attribute->SetValue(vec2Parameter->GetDefaultValue()); } } // Bool parameters for (const auto& boolParamPair : params.m_boolParams) { const AZ::Outcome parameterIndex = animGraph->FindValueParameterIndexByName(boolParamPair.first.c_str()); ASSERT_TRUE(parameterIndex.IsSuccess()); const ValueParameter* parameter = animGraph->FindValueParameter(parameterIndex.GetValue()); ASSERT_NE(parameter, nullptr); ASSERT_EQ(parameter->GetName(), boolParamPair.first.c_str()); MCore::Attribute* attribute = animGraphInstance->GetParameterValue(static_cast(parameterIndex.GetValue())); const BoolParameter* boolParameter = static_cast(parameter); MCore::AttributeBool* boolAttribute = static_cast(attribute); boolAttribute->SetValue(boolParamPair.second); } // Vec2 parameters for (const auto& vec2ParamTuple : params.m_vec2Params) { const AZ::Outcome parameterIndex = animGraph->FindValueParameterIndexByName(std::get<0>(vec2ParamTuple).c_str()); ASSERT_TRUE(parameterIndex.IsSuccess()); const ValueParameter* parameter = animGraph->FindValueParameter(parameterIndex.GetValue()); ASSERT_NE(parameter, nullptr); ASSERT_EQ(parameter->GetName(), std::get<0>(vec2ParamTuple).c_str()); MCore::Attribute* attribute = animGraphInstance->GetParameterValue(static_cast(parameterIndex.GetValue())); const Vector2Parameter* vec2Parameter = static_cast(parameter); MCore::AttributeVector2* vec2Attribute = static_cast(attribute); vec2Attribute->SetValue(std::get<1>(vec2ParamTuple)); } // Float parameters for (const auto& floatParamPair : params.m_floatParams) { const AZ::Outcome parameterIndex = animGraph->FindValueParameterIndexByName(floatParamPair.first.c_str()); ASSERT_TRUE(parameterIndex.IsSuccess()); const ValueParameter* parameter = animGraph->FindValueParameter(parameterIndex.GetValue()); ASSERT_NE(parameter, nullptr); ASSERT_EQ(parameter->GetName(), floatParamPair.first.c_str()); MCore::Attribute* attribute = animGraphInstance->GetParameterValue(static_cast(parameterIndex.GetValue())); const FloatParameter* floatParameter = static_cast(parameter); MCore::AttributeFloat* floatAttribute = static_cast(attribute); floatAttribute->SetValue(floatParamPair.second); } } } static AZStd::tuple CalculateStats(const AZStd::vector& samples) { const size_t numSamples = samples.size(); if (numSamples == 0) { return { 0.0f, 0.0f, 0.0f, 0.0f }; } float best = samples[0]; float worst = samples[0]; float accumulated = 0.0f; for (const float sample : samples) { best = AZ::GetMin(best, sample); worst = AZ::GetMax(worst, sample); accumulated += sample; } const float mean = accumulated / static_cast(numSamples); float variance = 0.0f; for (const float sample : samples) { variance += powf(sample - mean, 2.0f); } variance = variance / static_cast(numSamples); const float stdDeviation = sqrtf(variance); return { best, mean, worst, stdDeviation }; } void PrintReport(const AZStd::vector& transformUpdateFrameTimes, const AZStd::vector& meshDeformFrameTimes, float totalTransformUpdateTime, float totalMeshDeformTime) { const PerformanceTestParameters& param = GetParam(); // Totals AZ_Printf("EMotionFX", "----------------------------------------------------\n"); AZ_Printf("EMotionFX", "- Performance Test Report -\n"); if (param.m_includeSoftwareSkinning) { AZ_Printf("EMotionFX", "- Totals:\n"); AZ_Printf("EMotionFX", " Total Time (s): %.4f s\n", totalTransformUpdateTime + totalMeshDeformTime); AZ_Printf("EMotionFX", " Total Transform Update Time (s): %.4f s\n", totalTransformUpdateTime); AZ_Printf("EMotionFX", " Total Mesh Deform Time (s): %.4f s\n", totalMeshDeformTime); AZ_Printf("EMotionFX", " Transform Mesh Ratio: %.4f %%\n", totalTransformUpdateTime / totalMeshDeformTime); } else { AZ_Printf("EMotionFX", "- Total Time (s): %.4f s\n", totalTransformUpdateTime + totalMeshDeformTime); } // Transform update float transformBest; float transformMean; float transformWorst; float transformStdDeviation; AZStd::tie(transformBest, transformMean, transformWorst, transformStdDeviation) = CalculateStats(transformUpdateFrameTimes); AZ_Printf("EMotionFX", "- Transform update:\n"); AZ_Printf("EMotionFX", " Best Frame: %.4f ms (%.1f FPS)\n", transformBest * 1000.0f, transformBest > 0.0f ? 1.0f / transformBest : 0.0f); AZ_Printf("EMotionFX", " Mean Frame: %.4f ms (%.1f FPS)\n", transformMean * 1000.0f, transformMean > 0.0f ? 1.0f / transformMean : 0.0f); AZ_Printf("EMotionFX", " Worst Frame: %.4f ms (%.1f FPS)\n", transformWorst * 1000.0f, transformWorst > 0.0f ? 1.0f / transformWorst : 0.0f); AZ_Printf("EMotionFX", " Std Deviation: %.4f ms\n", transformStdDeviation * 1000.0f); // Mesh deforms if (param.m_includeSoftwareSkinning) { float meshDeformBest; float meshDeformMean; float meshDeformWorst; float meshDeformStdDeviation; AZStd::tie(meshDeformBest, meshDeformMean, meshDeformWorst, meshDeformStdDeviation) = CalculateStats(meshDeformFrameTimes); AZ_Printf("EMotionFX", "- Mesh deforms:\n"); AZ_Printf("EMotionFX", " Best Frame: %.4f ms\n", meshDeformBest * 1000.0f); AZ_Printf("EMotionFX", " Mean Frame: %.4f ms\n", meshDeformMean * 1000.0f); AZ_Printf("EMotionFX", " Worst Frame: %.4f ms\n", meshDeformWorst * 1000.0f); AZ_Printf("EMotionFX", " Std Deviation: %.4f ms\n", meshDeformStdDeviation * 1000.0f); } AZ_Printf("EMotionFX", "----------------------------------------------------\n"); } private: AZ::SimpleLcgRandom* m_random; std::vector m_pyroParameterSet; }; TEST_P(PerformanceTestFixture, PerformanceTest) { const PerformanceTestParameters& param = GetParam(); const size_t numIterations = static_cast(param.m_totalTestTimeInSeconds * param.m_fps); const float frameTimeDelta = 1.0f / param.m_fps; param.Print(); ActorManager* actorManager = GetEMotionFX().GetActorManager(); ActorUpdateScheduler* scheduler = nullptr; if (param.m_useMultiThreading) { scheduler = MultiThreadScheduler::Create(); } else { scheduler = SingleThreadScheduler::Create(); } actorManager->SetScheduler(scheduler); const AZStd::string assetFolder = GetAssetFolder(); GetEMotionFX().SetMediaRootFolder(assetFolder.c_str()); GetEMotionFX().InitAssetFolderPaths(); // This path points to assets in the advance rin demo. // To test different assets, change the path here. const char* actorFilename = "@assets@\\AnimationSamples\\Advanced_RinLocomotion\\Actor\\rinActor.actor"; const char* motionSetFilename = "@assets@\\AnimationSamples\\Advanced_RinLocomotion\\AnimationEditorFiles\\Advanced_RinLocomotion.motionset"; const char* animGraphFilename = "@assets@\\AnimationSamples\\Advanced_RinLocomotion\\AnimationEditorFiles\\Advanced_RinLocomotion.animgraph"; Importer* importer = GetEMotionFX().GetImporter(); importer->SetLoggingEnabled(false); AZStd::unique_ptr actor = importer->LoadActor(ResolvePath(actorFilename)); ASSERT_NE(actor, nullptr) << "Actor failed to load."; MotionSet* motionSet = importer->LoadMotionSet(ResolvePath(motionSetFilename)); ASSERT_NE(motionSet, nullptr) << "Motion set failed to load."; AnimGraph* animGraph = importer->LoadAnimGraph(ResolvePath(animGraphFilename)); ASSERT_NE(animGraph, nullptr) << "Anim graph failed to load."; // Create instances and start running the anim graphs. AZStd::vector actorInstances; AZStd::vector actorInstancesIncludingAttachments; actorInstances.reserve(param.m_numInstances); for (size_t i = 0; i < param.m_numInstances; ++i) { ActorInstance* actorInstance = ActorInstance::Create(actor.get()); if (!AZ::IsClose(param.m_motionSamplingRate, 0.0f, AZ::g_fltEps)) { actorInstance->SetMotionSamplingRate(1.0f / param.m_motionSamplingRate); } actorInstance->SetLocalSpacePosition(RandomRangeVec3(-100.0f, 100.0f)); actorInstances.emplace_back(actorInstance); actorInstancesIncludingAttachments.emplace_back(actorInstance); AnimGraphInstance* animGraphInstance = AnimGraphInstance::Create(animGraph, actorInstance, motionSet); actorInstance->SetAnimGraphInstance(animGraphInstance); actorInstance->SetLODLevel(param.m_lodLevel); actorInstance->UpdateTransformations(0.0f); // Add skin attachments. for (size_t attachmentNr = 0; attachmentNr < param.m_numSkinAttachmentsPerInstance; ++attachmentNr) { ActorInstance* attachmentActorInstance = ActorInstance::Create(actor.get()); EMotionFX::Attachment* attachment = EMotionFX::AttachmentSkin::Create(/*attachmentTarget*/actorInstance, attachmentActorInstance); actorInstance->AddAttachment(attachment); actorInstancesIncludingAttachments.emplace_back(attachmentActorInstance); } } // Preload motions and make sure they got loaded successfully. motionSet->Preload(); const auto& motionEntries = motionSet->GetMotionEntries(); for (const auto& motionEntryPair : motionEntries) { const MotionSet::MotionEntry* motionEntry = motionEntryPair.second; ASSERT_NE(motionEntry->GetMotion(), nullptr); } AZ::Debug::Timer timer; AZStd::vector transformUpdateFrameTimes; AZStd::vector meshDeformFrameTimes; transformUpdateFrameTimes.reserve(numIterations); meshDeformFrameTimes.reserve(numIterations); float totalTransformUpdateTime = 0.0f; float totalMeshDeformTime = 0.0f; const float randomizeParametersEvery = 1.0; // Change parameters every second float randomizeParameterTimer = 0.0f; for (size_t i = 0; i < numIterations; ++i) { randomizeParameterTimer += frameTimeDelta; if (randomizeParameterTimer >= randomizeParametersEvery) { RandomizeParameters(actorInstances); randomizeParameterTimer = 0.0; } // Output skeletal poses. timer.Stamp(); GetEMotionFX().Update(frameTimeDelta); const float transformUpdateTime = timer.GetDeltaTimeInSeconds(); totalTransformUpdateTime += transformUpdateTime; transformUpdateFrameTimes.emplace_back(transformUpdateTime); // Update mesh deformers (software skinning). if (param.m_includeSoftwareSkinning) { timer.Stamp(); for (ActorInstance* actorInstance : actorInstancesIncludingAttachments) { actorInstance->UpdateMeshDeformers(frameTimeDelta); } const float meshDeformTime = timer.GetDeltaTimeInSeconds(); totalMeshDeformTime += meshDeformTime; meshDeformFrameTimes.emplace_back(meshDeformTime); } } PrintReport(transformUpdateFrameTimes, meshDeformFrameTimes, totalTransformUpdateTime, totalMeshDeformTime); for (ActorInstance* actorInstance : actorInstancesIncludingAttachments) { actorInstance->Destroy(); } delete animGraph; delete motionSet; } const float g_updatesPerSec = 60.0f; const float g_totalTestTime = 60.0f; // 1 minute const size_t g_numInstances = 100; std::vector performanceTestData { // Baseline { "Baseline", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning false // multi-threading }, // Multi-threading { "Multi-threading", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning true // multi-threading }, // Multi-threading with 1 skin attachments { "Multi-threading with 1 skin attachments", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 1, // num skin attachments per instance 0, // lod level false, // software skinning true // multi-threading }, // Multi-threading and restricted motion sampling rate { "Multi-threading and restricted motion sampling rate", g_updatesPerSec, // fps 60.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning true // multi-threading }, // Multi-threading and lower motion sampling rate { "Multi-threading and lower motion sampling rate", g_updatesPerSec, // fps 30.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning true // multi-threading }, // Multi-threading and lower motion sampling rate { "Multi-threading and lower motion sampling rate", g_updatesPerSec, // fps 10.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning true // multi-threading }, // Multi-threading at LOD level = 1 { "Multi-threading at LOD level = 1", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 1, // lod level false, // software skinning true // multi-threading }, // Multi-threading at LOD level = 2 { "Multi-threading at LOD level = 2", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 2, // lod level false, // software skinning true // multi-threading }, // Multi-threading at LOD level = 3 { "Multi-threading at LOD level = 3", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 3, // lod level false, // software skinning true // multi-threading }, // Multi-threading at LOD level = 4 { "Multi-threading at LOD level = 4", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 4, // lod level false, // software skinning true // multi-threading }, // Server test: LOD0 { "Server test: LOD0", 30.0f, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning false // multi-threading }, // Server test: LOD4 { "Server test: LOD4", 30.0f, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 4, // lod level false, // software skinning false // multi-threading }, // Server test: Server actor optimization (bone removal). { "Server test: Server actor optimization (bone removal)", 30.0f, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning false // multi-threading }, // Software skinning { "Software skinning", g_updatesPerSec, // fps 0.0f, // motion sampling rate g_totalTestTime, // total test time g_numInstances, // instances 0, // num skin attachments per instance 0, // lod level true, // software skinning false // multi-threading }, }; std::vector debugTestData { { "debug", g_updatesPerSec, // fps 60.0f, // motion sampling rate g_totalTestTime, // total test time 1, // instances 0, // num skin attachments per instance 0, // lod level false, // software skinning true // multi-threading } }; INSTANTIATE_TEST_CASE_P(DISABLED_PerformanceTests, PerformanceTestFixture, ::testing::ValuesIn(performanceTestData)); ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TEST_F(PerformanceTestFixture, DISABLED_DeferredInitPerformanceTest) { const AZStd::string assetFolder = GetAssetFolder(); GetEMotionFX().SetMediaRootFolder(assetFolder.c_str()); GetEMotionFX().InitAssetFolderPaths(); // This path points to assets in the advance rin demo. // To test different assets, change the path here. const char* actorFilename = "@assets@\\AnimationSamples\\Advanced_RinLocomotion\\Actor\\rinActor.actor"; const char* motionSetFilename = "@assets@\\AnimationSamples\\Advanced_RinLocomotion\\AnimationEditorFiles\\Advanced_RinLocomotion.motionset"; const char* animGraphFilename = "@assets@\\AnimationSamples\\Advanced_RinLocomotion\\AnimationEditorFiles\\Advanced_RinLocomotion.animgraph"; Importer* importer = GetEMotionFX().GetImporter(); importer->SetLoggingEnabled(false); const AZStd::string resolvedActorFilename = ResolvePath(actorFilename); EXPECT_TRUE(AZ::IO::LocalFileIO::GetInstance()->Exists(resolvedActorFilename.c_str())) << AZStd::string::format("Actor file '%s' does not exist on local hard drive.", resolvedActorFilename.c_str()).c_str(); AZStd::unique_ptr actor = importer->LoadActor(resolvedActorFilename); ASSERT_NE(actor, nullptr) << "Actor failed to load."; MotionSet* motionSet = importer->LoadMotionSet(ResolvePath(motionSetFilename)); ASSERT_NE(motionSet, nullptr) << "Motion set failed to load."; AnimGraph* animGraph = importer->LoadAnimGraph(ResolvePath(animGraphFilename)); ASSERT_NE(animGraph, nullptr) << "Anim graph failed to load."; // Create instances. const size_t numInstances = 1000; AZStd::vector actorInstances; for (size_t i = 0; i < numInstances; ++i) { ActorInstance* actorInstance = ActorInstance::Create(actor.get()); actorInstances.emplace_back(actorInstance); } // Preload motions and make sure they got loaded successfully. motionSet->Preload(); const auto& motionEntries = motionSet->GetMotionEntries(); for (const auto& motionEntryPair : motionEntries) { const MotionSet::MotionEntry* motionEntry = motionEntryPair.second; ASSERT_NE(motionEntry->GetMotion(), nullptr); } AZ::Debug::Timer timer; timer.Stamp(); for (ActorInstance* actorInstance : actorInstances) { AnimGraphInstance* animGraphInstance = AnimGraphInstance::Create(animGraph, actorInstance, motionSet); actorInstance->SetAnimGraphInstance(animGraphInstance); } const float activationTime = timer.GetDeltaTimeInSeconds(); AZ_Printf("EMotionFX", "Instantiating took = %.2f ms\n", activationTime * 1000.0f); const AZStd::string test = AZStd::string::format("- Activation Time = %.2f ms -\n", activationTime * 1000.0f); OutputDebugString(test.c_str()); for (ActorInstance* actorInstance : actorInstances) { actorInstance->Destroy(); } delete animGraph; delete motionSet; } } // namespace EMotionFX