// Copyright Epic Games, Inc. All Rights Reserved. #include "CoreMinimal.h" #include "Evaluation/MovieSceneSequenceTransform.h" #include "Evaluation/MovieSceneSectionParameters.h" #include "Variants/MovieSceneTimeWarpVariantPayloads.h" #include "Containers/ArrayView.h" #include "Misc/AutomationTest.h" #include "MovieSceneTimeHelpers.h" #include "UObject/Package.h" #define LOCTEXT_NAMESPACE "MovieSceneTransformTests" // Range equality. bool IsEqual(TRangeBound A, TRangeBound B) { if (A.IsOpen() || B.IsOpen()) { return A.IsOpen() == B.IsOpen(); } else if (A.IsInclusive() != B.IsInclusive()) { return false; } return A.GetValue() == B.GetValue(); } // Range equality. bool IsEqual(TRange A, TRange B) { return IsEqual(A.GetLowerBound(), B.GetLowerBound()) && IsEqual(A.GetUpperBound(), B.GetUpperBound()); } // Frame number equality. bool IsEqual(FFrameNumber A, FFrameNumber B) { return A.Value == B.Value; } // Frame time equality. bool IsEqual(FFrameTime A, FFrameTime B) { return IsEqual(A.FrameNumber, B.FrameNumber) && FMath::IsNearlyEqual(A.GetSubFrame(), B.GetSubFrame()); } // Most time transformations are not "round" so they return a frame time that must be rounded down to a frame number, // except for time warping which doesn't stretch anything and returns a frame number. template FFrameNumber TransformToFrameNumber(TTransform Transform, FFrameNumber Value) { return (Value * Transform).FloorToFrame(); } // Generic method for testing the transform of frames and times. template bool TestTransform(FAutomationTestBase& Test, TTransform Transform, TArrayView InSource, TArrayView InExpected, const TCHAR* TestName) { check(InSource.Num() == InExpected.Num()); bool bSuccess = true; for (int32 Index = 0; Index < InSource.Num(); ++Index) { FFrameNumber Result = TransformToFrameNumber(Transform, InSource[Index]); if (!IsEqual(Result, InExpected[Index])) { Test.AddError(FString::Printf(TEXT("Test '%s' failed (Index %d). Transform %s did not apply correctly (%s != %s)"), TestName, Index, *LexToString(Transform), *LexToString(Result), *LexToString(InExpected[Index]))); bSuccess = false; } } return bSuccess; } // A variant of the above method for testing the transform of ranges. template bool TestTransform(FAutomationTestBase& Test, TTransform Transform, TArrayView> InSource, TArrayView> InExpected, const TCHAR* TestName) { check(InSource.Num() == InExpected.Num()); bool bSuccess = true; for (int32 Index = 0; Index < InSource.Num(); ++Index) { TRange Result = InSource[Index] * Transform; if (!IsEqual(Result, InExpected[Index])) { Test.AddError(FString::Printf(TEXT("Test '%s' failed (Index %d). Transform %s did not apply correctly (%s != %s)"), TestName, Index, *LexToString(Transform), *LexToString(Result), *LexToString(InExpected[Index]))); bSuccess = false; } } return bSuccess; } // Calculate the transform that transforms from range A to range B FMovieSceneSequenceTransform TransformRange(FFrameNumber StartA, FFrameNumber EndA, FFrameNumber StartB, FFrameNumber EndB) { float Scale = double( (EndB - StartB).Value ) / (EndA - StartA).Value; return FMovieSceneSequenceTransform(StartB, Scale) * FMovieSceneSequenceTransform(-StartA); } // Linear transform tests IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMovieSceneSubSectionCoreLinearTransformsTest, "System.Engine.Sequencer.Core.LinearTransforms", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FMovieSceneSubSectionCoreLinearTransformsTest::RunTest(const FString& Parameters) { FFrameNumber SourceTimes[] = { FFrameNumber(500), FFrameNumber(525) }; bool bSuccess = true; { FFrameNumber ExpectedTimes[] = { FFrameNumber(500), FFrameNumber(525) }; FMovieSceneTimeTransform Transform(0); bSuccess = TestTransform(*this, Transform, SourceTimes, ExpectedTimes, TEXT("IdentityTransform")) && bSuccess; Transform = Transform.Inverse(); bSuccess = TestTransform(*this, Transform, ExpectedTimes, SourceTimes, TEXT("IdentityTransformInverse")) && bSuccess; } { FFrameNumber ExpectedTimes[] = { FFrameNumber(1000), FFrameNumber(1050) }; FMovieSceneTimeTransform Transform(0, 2.f); bSuccess = TestTransform(*this, Transform, SourceTimes, ExpectedTimes, TEXT("OffsetTransform")) && bSuccess; Transform = Transform.Inverse(); bSuccess = TestTransform(*this, Transform, ExpectedTimes, SourceTimes, TEXT("OffsetTransformInverse")) && bSuccess; } { FFrameNumber ExpectedTimes[] = { FFrameNumber(0), FFrameNumber(50) }; FMovieSceneTimeTransform Transform(-1000, 2.f); bSuccess = TestTransform(*this, Transform, SourceTimes, ExpectedTimes, TEXT("OffsetAndScaleTransform")) && bSuccess; Transform = Transform.Inverse(); bSuccess = TestTransform(*this, Transform, ExpectedTimes, SourceTimes, TEXT("OffsetAndScaleTransformInverse")) && bSuccess; } { FFrameNumber ExpectedTimes[] = { FFrameNumber(0), FFrameNumber(50) }; FMovieSceneTimeTransform Transform = FMovieSceneTimeTransform(0, 2.f) * FMovieSceneTimeTransform(-500); bSuccess = TestTransform(*this, Transform, SourceTimes, ExpectedTimes, TEXT("OffsetAndScaleTransformObtainedFromMultiplication")) && bSuccess; } return bSuccess; } // Sequence transform tests IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMovieSceneSubSectionCoreSequenceTransformsTest, "System.Engine.Sequencer.Core.SequenceTransforms", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FMovieSceneSubSectionCoreSequenceTransformsTest::RunTest(const FString& Parameters) { // We test using ranges since that implicitly tests frame number transformation as well static const TRangeBound OpenBound; TRange InfiniteRange(OpenBound, OpenBound); TRange OpenLowerRange(OpenBound, FFrameNumber(200)); TRange OpenUpperRange(FFrameNumber(100), OpenBound); TRange ClosedRange(FFrameNumber(100), FFrameNumber(200)); TRange SourceRanges[] = { InfiniteRange, OpenLowerRange, OpenUpperRange, ClosedRange }; bool bSuccess = true; { // Test Multiplication with an identity transform FMovieSceneSequenceTransform IdentityTransform; TRange Expected[] = { InfiniteRange, OpenLowerRange, OpenUpperRange, ClosedRange }; bSuccess = TestTransform(*this, IdentityTransform.LinearTransform, SourceRanges, Expected, TEXT("IdentityTransform")) && bSuccess; } { // Test a simple translation FMovieSceneSequenceTransform Transform(100, 1); TRange Expected[] = { InfiniteRange, TRange(OpenBound, FFrameNumber(300)), TRange(FFrameNumber(200), OpenBound), TRange(200, 300) }; bSuccess = TestTransform(*this, Transform.LinearTransform, SourceRanges, Expected, TEXT("Simple Translation")) && bSuccess; } { // Test a simple translation + time scale // Transform 100 - 200 to -200 - 1000 FMovieSceneSequenceTransform Transform = TransformRange(100, 200, -200, 1000); TRange Expected[] = { InfiniteRange, TRange(OpenBound, FFrameNumber(1000)), TRange(FFrameNumber(-200), OpenBound), TRange(-200, 1000) }; bSuccess = TestTransform(*this, Transform.LinearTransform, SourceRanges, Expected, TEXT("Simple Translation + half speed")) && bSuccess; } { // Test that transforming a frame number by the same transform multiple times, does the same as the equivalent accumulated transform // scales by 2, then offsets by 100 FMovieSceneSequenceTransform SeedTransform = FMovieSceneSequenceTransform(100, 0.5f); FMovieSceneSequenceTransform AccumulatedTransform; FFrameTime SeedValue = 10; for (int32 i = 0; i < 5; ++i) { AccumulatedTransform = SeedTransform * AccumulatedTransform; SeedValue = SeedValue * SeedTransform; } FFrameTime AccumValue = FFrameTime(10) * AccumulatedTransform; if (AccumValue != SeedValue) { AddError(FString::Printf(TEXT("Accumulated transform does not have the same effect as separate transformations (%i+%.5f != %i+%.5f)"), AccumValue.FrameNumber.Value, AccumValue.GetSubFrame(), SeedValue.FrameNumber.Value, SeedValue.GetSubFrame())); } FMovieSceneInverseSequenceTransform InverseTransform = AccumulatedTransform.Inverse(); TOptional InverseValue = InverseTransform.TryTransformTime(AccumValue); if (!InverseValue.IsSet()) { AddError(FString::Printf(TEXT("Inverse accumulated transform did not return a valid time"))); } else if (InverseValue.GetValue() != 10) { AddError(FString::Printf(TEXT("Inverse accumulated transform does not return value back to its original value (%i+%.5f != 10)"), InverseValue->FrameNumber.Value, InverseValue->GetSubFrame())); } } return true; } // Miscellaneous warping and scaling tests IMPLEMENT_SIMPLE_AUTOMATION_TEST(FMovieSceneSubSectionCoreWarpingAndScalingTransformsTest, "System.Engine.Sequencer.Core.WarpingAndScalingTransforms", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) bool FMovieSceneSubSectionCoreWarpingAndScalingTransformsTest::RunTest(const FString& Parameters) { { // Sub-sequence at 0, playing at x2 FMovieSceneSequenceTransform Transform; Transform.LinearTransform = FMovieSceneTimeTransform(0, 2.f); Transform.AddLoop(0, 30); TestEqual("Transform time 1", FFrameNumber(10) * Transform, FFrameTime(20)); FMovieSceneTransformBreadcrumbs Breadcrumbs; FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); Breadcrumbs.AddBreadcrumb(15); TestEqual("Inverse time 3", Inv.TryTransformTime(FFrameNumber(20), Breadcrumbs), TOptional(10)); Breadcrumbs.Reset(); Breadcrumbs.AddBreadcrumb(45); TestEqual("Inverse time 4", Inv.TryTransformTime(FFrameNumber(20), Breadcrumbs), TOptional(FFrameTime(25))); } { // Sub-sequence at 0, playing at x2, with start offset 20 FMovieSceneSequenceTransform Transform; Transform.LinearTransform = FMovieSceneTimeTransform(20, 2.f); Transform.AddLoop(20, 50); TestEqual("Transform time 5", FFrameNumber(10) * Transform, FFrameTime(40)); TestEqual("Transform time 6", FFrameNumber(18) * Transform, FFrameTime(26)); FMovieSceneTransformBreadcrumbs Breadcrumbs; FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); Breadcrumbs.AddBreadcrumb(35); // 35 should be in the middle of the first loop TestEqual("Inverse time 7", Inv.TryTransformTime(FFrameNumber(40), Breadcrumbs), TOptional(FFrameTime(10))); Breadcrumbs.Reset(); Breadcrumbs.AddBreadcrumb(65); // 65 should be in the middle of the second loop TestEqual("Inverse time 8", Inv.TryTransformTime(FFrameNumber(40), Breadcrumbs), TOptional(FFrameTime(25))); } { // Sub-sequence at 3, playing at x2 FMovieSceneSequenceTransform Transform; Transform.LinearTransform = FMovieSceneTimeTransform(-6, 2.f); Transform.AddLoop(0, 30); TestEqual("Transform time 9", FFrameNumber(13) * Transform, FFrameTime(20)); TestEqual("Transform time 10", FFrameNumber(21) * Transform, FFrameTime(6)); FMovieSceneTransformBreadcrumbs Breadcrumbs; FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); Breadcrumbs.AddBreadcrumb(15); // 15 should be in the middle of the first loop TestEqual("Inverse time 11", Inv.TryTransformTime(FFrameNumber(20), Breadcrumbs), TOptional(FFrameTime(13))); Breadcrumbs.Reset(); Breadcrumbs.AddBreadcrumb(45); // 45 should be in the middle of the second loop TestEqual("Inverse time 12", Inv.TryTransformTime(FFrameNumber(20), Breadcrumbs), TOptional(FFrameTime(28))); } { // Sub-sequence at 3, playing at x2, with start offset 20 FMovieSceneSequenceTransform Transform; Transform.LinearTransform = FMovieSceneTimeTransform(-6 + 20, 2.f); Transform.AddLoop(20, 50); TestEqual("Transform time 13", FFrameNumber(13) * Transform, FFrameTime(40)); TestEqual("Transform time 14", FFrameNumber(21) * Transform, FFrameTime(26)); FMovieSceneTransformBreadcrumbs Breadcrumbs; FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); Breadcrumbs.AddBreadcrumb(35); // 35 should be in the middle of the first loop TestEqual("Inverse time 15", Inv.TryTransformTime(FFrameNumber(40), Breadcrumbs), TOptional(FFrameTime(13))); Breadcrumbs.Reset(); Breadcrumbs.AddBreadcrumb(65); // 65 should be in the middle of the second loop TestEqual("Inverse time 16", Inv.TryTransformTime(FFrameNumber(40), Breadcrumbs), TOptional(FFrameTime(28))); } { // Two levels of sub-sequences: one placed at 10 and warping, the second placed at 6 with x2 scaling FMovieSceneSequenceTransform Transform; Transform.LinearTransform = FMovieSceneTimeTransform(-10, 1.f); Transform.AddLoop(0, 30); Transform.NestedTransforms.Emplace(FMovieSceneTimeTransform(-12, 2.f)); TestEqual("Transform time 17", FFrameNumber(18) * Transform, FFrameTime(4)); TestEqual("Transform time 18", FFrameNumber(55) * Transform, FFrameTime(18)); FMovieSceneTransformBreadcrumbs Breadcrumbs; FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); Breadcrumbs.AddBreadcrumb(15); TestEqual("Inverse time 17", Inv.TryTransformTime(FFrameNumber(4), Breadcrumbs), TOptional(FFrameTime(18))); Breadcrumbs.Reset(); Breadcrumbs.AddBreadcrumb(45); TestEqual("Inverse time 18", Inv.TryTransformTime(FFrameNumber(18), Breadcrumbs), TOptional(FFrameTime(55))); } { // Two levels of sub-sequences: one placed at 10, the second placed at 6 with x2 scaling and warping FMovieSceneSequenceTransform Transform; Transform.LinearTransform = FMovieSceneTimeTransform(-10, 1.f); Transform.NestedTransforms.Emplace(FMovieSceneTimeTransform(-12, 2.f)); Transform.AddLoop(0, 14); TestEqual("Transform time 17", FFrameNumber(19) * Transform, FFrameTime(6)); TestEqual("Transform time 18", FFrameNumber(32) * Transform, FFrameTime(4)); FMovieSceneTransformBreadcrumbs Breadcrumbs; FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); Breadcrumbs.AddBreadcrumb(7); // half way through loop 0 TestEqual("Inverse time 17", Inv.TryTransformTime(FFrameNumber(6), Breadcrumbs), TOptional(FFrameTime(19))); Breadcrumbs.Reset(); Breadcrumbs.AddBreadcrumb(28); // half way through loop 2 TestEqual("Inverse time 18", Inv.TryTransformTime(FFrameNumber(4), Breadcrumbs), TOptional(FFrameTime(32))); } { // Sub-sequence at 3, playing at x2, with start offset 20, but all contained inside a higher offset of 100. FMovieSceneSequenceTransform Transform; Transform.LinearTransform.Offset = FFrameTime(-100); Transform.NestedTransforms.Emplace(FMovieSceneTimeTransform(-6 + 20, 2.f)); Transform.AddLoop(20, 50); TestEqual("Transform time 19", FFrameNumber(113) * Transform, FFrameTime(40)); TestEqual("Transform time 20", FFrameNumber(121) * Transform, FFrameTime(26)); FMovieSceneTransformBreadcrumbs Breadcrumbs; FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); Breadcrumbs.AddBreadcrumb(35); // Loop 0 TestEqual("Inverse time 21", Inv.TryTransformTime(FFrameNumber(40), Breadcrumbs), TOptional(FFrameTime(113))); Breadcrumbs.Reset(); Breadcrumbs.AddBreadcrumb(65); // Loop 1 TestEqual("Inverse time 22", Inv.TryTransformTime(FFrameNumber(40), Breadcrumbs), TOptional(FFrameTime(128))); } { // Zero-timescale transform on a sub-sequence. Any frame numbers transformed in should be equal to the frame offset FMovieSceneSequenceTransform Transform; Transform.NestedTransforms.Add(FMovieSceneTimeTransform(0)); // no outer offset Transform.NestedTransforms.Add(FMovieSceneTimeWarpVariant(0.0)); // 0 timescale Transform.NestedTransforms.Add(FMovieSceneTimeTransform(30)); // 30 inner frame offset TestEqual("Outer time 40 through 0 timescale with 30 offset", FFrameNumber(40) * Transform, FFrameTime(30)); TestEqual("Outer time 0 through 0 timescale with 30 offset", FFrameNumber(0) * Transform, FFrameTime(30)); TestEqual("Outer time 173 through 0 timescale with 30 offset", FFrameNumber(173) * Transform, FFrameTime(30)); } { // Zero-timescale transform on a sub-sequence. Same as previous, but we also will invert this transform and ensure timescale is correctly infinite // and any transforms by that infinite transform. Anything transformed out should just be equal to the outer offset FMovieSceneSequenceTransform Transform; Transform.NestedTransforms.Add(FMovieSceneTimeTransform(-10)); // 10 outer offset Transform.NestedTransforms.Add(FMovieSceneTimeWarpVariant(0.0)); // 0 timescale Transform.NestedTransforms.Add(FMovieSceneTimeTransform(30)); // 30 inner frame offset FMovieSceneInverseSequenceTransform Inv = Transform.Inverse(); if (Inv.IsLinear()) { AddError(FString::Printf(TEXT("Inverse of a transform with zero timescale is not correctly warping"))); } TestEqual("Inner time 40 through inf timescale with 10 outer offset", Inv.TryTransformTime(FFrameNumber(40)), TOptional()); TestEqual("Inner time 0 through inf timescale with 10 outer offset", Inv.TryTransformTime(FFrameNumber(0)), TOptional()); TestEqual("Inner time 173 through inf timescale with 10 outer offset", Inv.TryTransformTime(FFrameNumber(173)), TOptional()); TestEqual("Inner time 30 through inf timescale with 10 outer offset", Inv.TryTransformTime(FFrameNumber(30)), TOptional(FFrameTime(10))); // Re-invert the inverse transform. This should be equivalent to the original transform and we shouldn't have lost anything. // @todo: is this necessary? We haven't needed an Inverse for an Inverse anywhere else in the codebase // FMovieSceneSequenceTransform InvInv = Inv.Inverse(); // TestEqual("Doubly inverted zero-timescale transform should be equal to original", InvInv, Transform); } { // Multiple levels of sub sequences with zero-timescale thrown in FMovieSceneSequenceTransform OuterTransform; OuterTransform.NestedTransforms.Add(FMovieSceneTimeTransform(-10)); // 10 outer offset OuterTransform.NestedTransforms.Add(FMovieSceneTimeWarpVariant(0.0)); // 0 timescale OuterTransform.NestedTransforms.Add(FMovieSceneTimeTransform(30)); // 30 inner frame offset FMovieSceneSequenceTransform InnerTransform; InnerTransform.LinearTransform.Offset = FFrameNumber(5); // An inner frame offset of 5 FMovieSceneSequenceTransform CompleteTransform = InnerTransform * OuterTransform; TestEqual("Subsequence frame through zero timescale transform", FFrameNumber(40)* CompleteTransform, FFrameTime(35)); TestEqual("Subsequence frame through zero timescale transform", FFrameNumber(0)* CompleteTransform, FFrameTime(35)); TestEqual("Subsequence frame through zero timescale transform", FFrameNumber(173)* CompleteTransform, FFrameTime(35)); FMovieSceneInverseSequenceTransform InvCompleteTransform = CompleteTransform.Inverse(); TestEqual("Inner time 40 through inf timescale with 10 outer offset", InvCompleteTransform.TryTransformTime(FFrameNumber(40)), TOptional()); TestEqual("Inner time 0 through inf timescale with 10 outer offset", InvCompleteTransform.TryTransformTime(FFrameNumber(0)), TOptional()); TestEqual("Inner time 173 through inf timescale with 10 outer offset", InvCompleteTransform.TryTransformTime(FFrameNumber(173)), TOptional()); TestEqual("Inner time 35 through inf timescale with 10 outer offset", InvCompleteTransform.TryTransformTime(FFrameNumber(35)), TOptional(FFrameTime(10))); // Re-invert the inverse transform. This should be equivalent to the original transform and we shouldn't have lost anything. // @todo: is this necessary? We haven't needed an Inverse for an Inverse anywhere else in the codebase //FMovieSceneSequenceTransform InvInv = InvCompleteTransform.InverseNoLooping(); //TestEqual("Doubly inverted zero-timescale transform should be equal to original", InvInv, CompleteTransform); } return true; } #undef LOCTEXT_NAMESPACE