Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorSybren A. Stüvel <sybren@blender.org>2020-11-23 14:48:04 +0300
committerSybren A. Stüvel <sybren@blender.org>2020-11-23 14:48:04 +0300
commite4ca1fc4ea43f795441a319ea96b63a58553f070 (patch)
tree2dcb89adb56958fc26d41ceac51af6dc9dd72814 /tests
parent1318eaf166947e080c05ed689dbc295817496f1a (diff)
Animation: New Euler filter implementation
This new discontinuity filter performs actions on the entire Euler rotation, rather than only on the individual X/Y/Z channels. This makes it fix a wider range of discontinuities, for example those in T52744. The filter now runs twice on the selected channels, in this order: - New: Convert X+Y+Z rotation to matrix, then back to Euler angles. - Old: Add/remove factors of 360° to minimize jumps. The messaging is streamlined; it now reports how many channels were filtered, and only warns (instead of errors) when there was an actual problem with the selected channels (like selecting three or more channels, but without X/Y/Z triplet). A new kernel function `BKE_fcurve_keyframe_move_value_with_handles()` is introduced, to make it possible to move a keyframe's value and move its handles at the same time. Manifest Task: T52744 Reviewed By: looch Differential Revision: https://developer.blender.org/D9602
Diffstat (limited to 'tests')
-rw-r--r--tests/python/bl_animation_fcurves.py96
1 files changed, 95 insertions, 1 deletions
diff --git a/tests/python/bl_animation_fcurves.py b/tests/python/bl_animation_fcurves.py
index b5772b8d335..2ec04749d70 100644
--- a/tests/python/bl_animation_fcurves.py
+++ b/tests/python/bl_animation_fcurves.py
@@ -25,11 +25,13 @@ blender -b -noaudio --factory-startup --python tests/python/bl_animation_fcurves
import pathlib
import sys
import unittest
+from math import degrees, radians
+from typing import List
import bpy
-class FCurveEvaluationTest(unittest.TestCase):
+class AbstractAnimationTest:
@classmethod
def setUpClass(cls):
cls.testdir = args.testdir
@@ -38,6 +40,7 @@ class FCurveEvaluationTest(unittest.TestCase):
self.assertTrue(self.testdir.exists(),
'Test dir %s should exist' % self.testdir)
+class FCurveEvaluationTest(AbstractAnimationTest, unittest.TestCase):
def test_fcurve_versioning_291(self):
# See D8752.
bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "fcurve-versioning-291.blend"))
@@ -73,6 +76,97 @@ class FCurveEvaluationTest(unittest.TestCase):
self.assertAlmostEqual(1.0, fcurve.evaluate(10))
+class EulerFilterTest(AbstractAnimationTest, unittest.TestCase):
+ def setUp(self):
+ super().setUp()
+ bpy.ops.wm.open_mainfile(filepath=str(self.testdir / "euler-filter.blend"))
+
+ def test_multi_channel_filter(self):
+ """Test fixing discontinuities that require all X/Y/Z rotations to work."""
+
+ self.activate_object('Three-Channel-Jump')
+ fcu_rot = self.active_object_rotation_channels()
+
+ ## Check some pre-filter values to make sure the file is as we expect.
+ # Keyframes before the "jump". These shouldn't be touched by the filter.
+ self.assertEqualAngle(-87.5742, fcu_rot[0], 22)
+ self.assertEqualAngle(69.1701, fcu_rot[1], 22)
+ self.assertEqualAngle(-92.3918, fcu_rot[2], 22)
+ # Keyframes after the "jump". These should be updated by the filter.
+ self.assertEqualAngle(81.3266, fcu_rot[0], 23)
+ self.assertEqualAngle(111.422, fcu_rot[1], 23)
+ self.assertEqualAngle(76.5996, fcu_rot[2], 23)
+
+ bpy.ops.graph.euler_filter(self.get_context())
+
+ # Keyframes before the "jump". These shouldn't be touched by the filter.
+ self.assertEqualAngle(-87.5742, fcu_rot[0], 22)
+ self.assertEqualAngle(69.1701, fcu_rot[1], 22)
+ self.assertEqualAngle(-92.3918, fcu_rot[2], 22)
+ # Keyframes after the "jump". These should be updated by the filter.
+ self.assertEqualAngle(-98.6734, fcu_rot[0], 23)
+ self.assertEqualAngle(68.5783, fcu_rot[1], 23)
+ self.assertEqualAngle(-103.4, fcu_rot[2], 23)
+
+ def test_single_channel_filter(self):
+ """Test fixing discontinuities in single channels."""
+
+ self.activate_object('One-Channel-Jumps')
+ fcu_rot = self.active_object_rotation_channels()
+
+ ## Check some pre-filter values to make sure the file is as we expect.
+ # Keyframes before the "jump". These shouldn't be touched by the filter.
+ self.assertEqualAngle(360, fcu_rot[0], 15)
+ self.assertEqualAngle(396, fcu_rot[1], 21) # X and Y are keyed on different frames.
+ # Keyframes after the "jump". These should be updated by the filter.
+ self.assertEqualAngle(720, fcu_rot[0], 16)
+ self.assertEqualAngle(72, fcu_rot[1], 22)
+
+ bpy.ops.graph.euler_filter(self.get_context())
+
+ # Keyframes before the "jump". These shouldn't be touched by the filter.
+ self.assertEqualAngle(360, fcu_rot[0], 15)
+ self.assertEqualAngle(396, fcu_rot[1], 21) # X and Y are keyed on different frames.
+ # Keyframes after the "jump". These should be updated by the filter.
+ self.assertEqualAngle(360, fcu_rot[0], 16)
+ self.assertEqualAngle(432, fcu_rot[1], 22)
+
+ def assertEqualAngle(self, angle_degrees: float, fcurve: bpy.types.FCurve, frame: int) -> None:
+ self.assertAlmostEqual(
+ radians(angle_degrees),
+ fcurve.evaluate(frame),
+ 4,
+ "Expected %.3f degrees, but FCurve %s[%d] evaluated to %.3f on frame %d" % (
+ angle_degrees, fcurve.data_path, fcurve.array_index, degrees(fcurve.evaluate(frame)), frame,
+ )
+ )
+
+ @staticmethod
+ def get_context():
+ ctx = bpy.context.copy()
+
+ for area in bpy.context.window.screen.areas:
+ if area.type != 'GRAPH_EDITOR':
+ continue
+
+ ctx['area'] = area
+ ctx['space'] = area.spaces.active
+ break
+
+ return ctx
+
+ @staticmethod
+ def activate_object(object_name: str) -> None:
+ ob = bpy.data.objects[object_name]
+ bpy.context.view_layer.objects.active = ob
+
+ @staticmethod
+ def active_object_rotation_channels() -> List[bpy.types.FCurve]:
+ ob = bpy.context.view_layer.objects.active
+ action = ob.animation_data.action
+ return [action.fcurves.find('rotation_euler', index=idx) for idx in range(3)]
+
+
def main():
global args
import argparse