Anim: Discuss edge cases and add regression tests for NLA strip evaluation #109212
@ -393,6 +393,11 @@ add_blender_test(
|
||||
--testdir "${TEST_SRC_DIR}/animation"
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_animation_nla_strip
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_animation_nla_strip.py
|
||||
)
|
||||
|
||||
add_blender_test(
|
||||
bl_rigging_symmetrize
|
||||
--python ${CMAKE_CURRENT_LIST_DIR}/bl_rigging_symmetrize.py
|
||||
|
121
tests/python/bl_animation_nla_strip.py
Normal file
121
tests/python/bl_animation_nla_strip.py
Normal file
@ -0,0 +1,121 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
dr.sybren marked this conversation as resolved
Outdated
|
||||
|
||||
"""
|
||||
Tests the evaluation of NLA strips based on their properties and placement on NLA tracks.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
class AbstractNlaStripTest(unittest.TestCase):
|
||||
""" Sets up a series of strips in one NLA track. """
|
||||
|
||||
test_object: bpy.types.Object = None
|
||||
""" Object whose X Location is animated to check strip evaluation. """
|
||||
|
||||
nla_tracks: bpy.types.NlaTracks = None
|
||||
""" NLA tracks of the test object, which are cleared after each test case. """
|
||||
|
||||
action: bpy.types.Action = None
|
||||
""" Action with X Location keyed on frames 1 to 4 with the same value as the frame, with constant interpolation. """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
|
||||
cls.test_object = bpy.data.objects.new(name="Object", object_data=bpy.data.meshes.new("Mesh"))
|
||||
pamadini marked this conversation as resolved
Outdated
Nate Rupsis
commented
Might be worth creating / tearing down an object as well, that way we're not reliant on the default Blender scene which could (probably won't) change. Might be worth creating / tearing down an object as well, that way we're not reliant on the default Blender scene which could (probably won't) change.
|
||||
bpy.context.collection.objects.link(cls.test_object)
|
||||
cls.test_object.animation_data_create()
|
||||
|
||||
cls.nla_tracks = cls.test_object.animation_data.nla_tracks
|
||||
|
||||
cls.action = bpy.data.actions.new(name="ObjectAction")
|
||||
x_location_fcurve = cls.action.fcurves.new(data_path="location", index=0, action_group="Object Transforms")
|
||||
for frame in range(1, 5):
|
||||
x_location_fcurve.keyframe_points.insert(frame, value=frame).interpolation = "CONSTANT"
|
||||
|
||||
def tearDown(self):
|
||||
while len(self.nla_tracks):
|
||||
self.nla_tracks.remove(self.nla_tracks[0])
|
||||
|
||||
def add_strip_no_extrapolation(self, nla_track: bpy.types.NlaTrack, start: int) -> bpy.types.NlaStrip:
|
||||
""" Places a new strip with the test action on the given track, setting extrapolation to nothing. """
|
||||
strip = nla_track.strips.new("ObjectAction", start, self.action)
|
||||
strip.extrapolation = "NOTHING"
|
||||
return strip
|
||||
|
||||
pamadini marked this conversation as resolved
Outdated
Nate Rupsis
commented
Would change the parameter name to "expected_value". Gives a clearer indication what the method does. Would change the parameter name to "expected_value". Gives a clearer indication what the method does.
|
||||
def assertFrameValue(self, frame: float, expected_value: float):
|
||||
""" Checks the evaluated X Location at the given frame. """
|
||||
int_frame, subframe = divmod(frame, 1)
|
||||
bpy.context.scene.frame_set(frame=int(int_frame), subframe=subframe)
|
||||
self.assertEqual(expected_value, self.test_object.evaluated_get(
|
||||
bpy.context.evaluated_depsgraph_get()
|
||||
).matrix_world.translation[0])
|
||||
|
||||
|
||||
class NlaStripSingleTest(AbstractNlaStripTest):
|
||||
""" Tests the inner values as well as the boundaries of one strip on one track. """
|
||||
|
||||
def test_extrapolation_nothing(self):
|
||||
""" Tests one strip with no extrapolation. """
|
||||
self.add_strip_no_extrapolation(self.nla_tracks.new(), 1)
|
||||
|
||||
self.assertFrameValue(0.9, 0.0)
|
||||
self.assertFrameValue(1.0, 1.0)
|
||||
self.assertFrameValue(1.1, 1.0)
|
||||
self.assertFrameValue(3.9, 3.0)
|
||||
self.assertFrameValue(4.0, 4.0)
|
||||
self.assertFrameValue(4.1, 0.0)
|
||||
|
||||
|
||||
class NlaStripBoundaryTest(AbstractNlaStripTest):
|
||||
""" Tests two strips, the second one starting when the first one ends. """
|
||||
|
||||
# Incorrectly, the first strip is currently evaluated at the boundary between two adjacent strips (see #113487).
|
||||
@unittest.expectedFailure
|
||||
def test_adjacent(self):
|
||||
""" The second strip should be evaluated at the boundary between two adjacent strips. """
|
||||
nla_track = self.nla_tracks.new()
|
||||
self.add_strip_no_extrapolation(nla_track, 1)
|
||||
self.add_strip_no_extrapolation(nla_track, 4)
|
||||
|
||||
self.assertFrameValue(3.9, 3.0)
|
||||
self.assertFrameValue(4.0, 1.0)
|
||||
self.assertFrameValue(4.1, 1.0)
|
||||
|
||||
def test_adjacent_muted(self):
|
||||
""" The first strip should be evaluated at the boundary if it is adjacent to a muted strip. """
|
||||
nla_track = self.nla_tracks.new()
|
||||
self.add_strip_no_extrapolation(nla_track, 1)
|
||||
self.add_strip_no_extrapolation(nla_track, 4).mute = True
|
||||
|
||||
self.assertFrameValue(3.9, 3.0)
|
||||
self.assertFrameValue(4.0, 4.0)
|
||||
self.assertFrameValue(4.1, 0.0)
|
||||
|
||||
def test_first_above_second(self):
|
||||
""" The first strip should be evaluated at the boundary, when followed by another strip on a track below. """
|
||||
self.add_strip_no_extrapolation(self.nla_tracks.new(), 4)
|
||||
self.add_strip_no_extrapolation(self.nla_tracks.new(), 1)
|
||||
|
||||
self.assertFrameValue(3.9, 3.0)
|
||||
self.assertFrameValue(4.0, 4.0)
|
||||
self.assertFrameValue(4.1, 1.0)
|
||||
|
||||
def test_second_above_first(self):
|
||||
""" The second strip should be evaluated at the boundary, when preceded by another strip on a track below. """
|
||||
self.add_strip_no_extrapolation(self.nla_tracks.new(), 1)
|
||||
self.add_strip_no_extrapolation(self.nla_tracks.new(), 4)
|
||||
|
||||
self.assertFrameValue(3.9, 3.0)
|
||||
self.assertFrameValue(4.0, 1.0)
|
||||
self.assertFrameValue(4.1, 1.0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Drop all arguments before "--", or everything if the delimiter is absent. Keep the executable path.
|
||||
dr.sybren marked this conversation as resolved
Outdated
Nate Rupsis
commented
(Unrelated to this PR. ) This seems like a pretty consistent pattern across the python tests. @dr.sybren would it make sense for this to be abstracted into a Util of some sort? (Unrelated to this PR. ) This seems like a pretty consistent pattern across the python tests.
@dr.sybren would it make sense for this to be abstracted into a Util of some sort?
Sybren A. Stüvel
commented
Maybe at some point, but not in this PR ;-) Maybe at some point, but not in this PR ;-)
|
||||
unittest.main(argv=sys.argv[:1] + (sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []))
|
Loading…
Reference in New Issue
Block a user
Just double checking we're using GPL-2.0
I'm seeing a lot of test files with
SPDX-License-Identifier: Apache-2.0
Both seem to be in use, happy to switch if it's needed once you've determined which license works best for new files here.
Blender is "GPLv2.0 or later". Other code could have been submitted with a different (but compatible) license. When in doubt, use GPLv2+.