Note
Go to the end to download the full example code
Making a custom interpolator#
Keyframe animation using custom interpolator.
import numpy as np
from fury import actor, window
from fury.animation import Animation, helpers
Implementing a custom interpolator#
A keyframe interpolator function must return a function which take the time as an argument and returns a value. It’s recommended to import fury.animation.helpers, which has some useful functions that would help to implement the interpolator.
In glTF, animations using cubic spline interpolator needs at least two points, and each point has two tangent vectors. The interpolation equation for such data is in the glTF tutorials below: https://github.khronos.org/glTF-Tutorials/gltfTutorial/gltfTutorial_007_Animations.html#cubic-spline-interpolation
Tangent based cubic spline interpolation function:
>>> def cubicSpline(previousPoint, previousTangent, nextPoint, nextTangent,
>>> interpolationValue):
>>> t = interpolationValue
>>> t2 = t * t
>>> t3 = t2 * t
>>> return (2 * t3 - 3 * t2 + 1) * previousPoint +
>>> (t3 - 2 * t2 + t) * previousTangent +
>>> (-2 * t3 + 3 * t2) * nextPoint +
>>> (t3 - t2) * nextTangent
First we create a function that must take a dict object that contains the animation keyframes when initialized as follows:
>>> def tan_cubic_spline_interpolator(keyframes):
>>> ...
>>> def interpolate(t):
>>> return interpolated_value
>>> return interpolator
Note: Also any other additional arguments are ok, see spline_interpolator Second step is to implement the interpolate closure that only takes the current time as input.
def tan_cubic_spline_interpolator(keyframes):
# First we must get ordered timestamps array:
timestamps = helpers.get_timestamps_from_keyframes(keyframes)
# keyframes should be on the following form:
# {
# 1: {'value': ndarray, 'in_tangent': ndarray, 'out_tangent': ndarray},
# 2: {'value': np.array([1, 2, 3], 'in_tangent': ndarray},
# }
# See here, we might get incomplete data (out_tangent) in the second
# keyframe. In this case we need to have a default behaviour dealing
# with these missing data.
# Setting the tangent to a zero vector in this case is the best choice
for time in keyframes:
data = keyframes.get(time)
value = data.get("value")
if data.get("in_tangent") is None:
data["in_tangent"] = np.zeros_like(value)
if data.get("in_tangent") is None:
data["in_tangent"] = np.zeros_like(value)
def interpolate(t):
# `get_previous_timestamp`and `get_next_timestamp` functions take
# timestamps array and current time as inputs and returns the
# surrounding timestamps.
t0 = helpers.get_previous_timestamp(timestamps, t)
t1 = helpers.get_next_timestamp(timestamps, t)
# `get_time_tau` function takes current time and surrounding
# timestamps and returns a value from 0 to 1
dt = helpers.get_time_tau(t, t0, t1)
time_delta = t1 - t0
# to get a keyframe data at a specific timestamp, use
# `keyframes.get(t0)`. This keyframe data contains `value` and any
# other data set as a custom argument using keyframe setters.
# for example:
# >>> animation = Animation()
# >>> animation.set_position(0, np.array([1, 1, 1]),
# >>> custom_field=np.array([2, 3, 1]))
# In this case `keyframes.get(0)` would return:
# {'value': array(1, 1, 1), 'custom_field': array(2, 3, 1)}
#
# now we continue with the cubic spline equation.
p0 = keyframes.get(t0).get("value")
tan_0 = keyframes.get(t0).get("out_tangent") * time_delta
p1 = keyframes.get(t1).get("value")
tan_1 = keyframes.get(t1).get("in_tangent") * time_delta
# cubic spline equation using tangents
t2 = dt * dt
t3 = t2 * dt
return (
(2 * t3 - 3 * t2 + 1) * p0
+ (t3 - 2 * t2 + dt) * tan_0
+ (-2 * t3 + 3 * t2) * p1
+ (t3 - t2) * tan_1
)
return interpolate
scene = window.Scene()
showm = window.ShowManager(
scene, size=(900, 768), reset_camera=False, order_transparent=True
)
/opt/homebrew/Caskroom/miniforge/base/envs/py39/lib/python3.9/site-packages/sphinx_gallery/gen_rst.py:722: UserWarning: We'll no longer accept the way you call the __init__ function in future versions of FURY.
Here's how to call the Function __init__: __init__(self_value, scene='value', title='value', size='value', png_magnify='value', reset_camera='value', order_transparent='value', interactor_style='value', stereo='value', multi_samples='value', max_peels='value', occlusion_ratio='value')
exec(self.code, self.fake_main.__dict__)
Cubic spline keyframes data same as the one you get from glTF file.#
# t in tangent position out tangent
translation = [
[0.0, [0.0, 0.0, 0.0], [3.3051798, 6.640117, 0.0], [1.0, 0.0, 0.0]],
[1.0, [0.0, 0.0, 0.0], [3.3051798, 8.0, 0.0], [-1.0, 0.0, 0.0]],
[2.0, [-1.0, 0.0, 0.0], [3.3051798, 6.0, 0.0], [1.0, 0.0, 0.0]],
[3.0, [0.0, 0.0, 0.0], [3.3051798, 8.0, 0.0], [-1.0, 0.0, 0.0]],
[4.0, [0, -1.0, 0.0], [3.3051798, 6.0, 0.0], [0.0, 0.0, 0.0]],
]
Initializing an Animation
and adding sphere actor to it.
animation = Animation(motion_path_res=100)
sphere = actor.sphere(np.array([[0, 0, 0]]), (1, 0, 1), radii=0.1)
animation.add_actor(sphere)
Setting position keyframes#
for keyframe_data in translation:
t, in_tan, pos, out_tan = keyframe_data
# Since we used the name 'in_tangent' and 'out_tangent' in the interpolator
# We must use the same name as an argument to set it in the keyframe data.
animation.set_position(t, pos, in_tangent=in_tan, out_tangent=out_tan)
Set the new interpolator to interpolate position keyframes
animation.set_position_interpolator(tan_cubic_spline_interpolator)
adding the animation to the show manager.
showm.add_animation(animation)
interactive = False
if interactive:
showm.start()
window.record(scene, out_path="viz_keyframe_custom_interpolator.png", size=(900, 768))
/opt/homebrew/Caskroom/miniforge/base/envs/py39/lib/python3.9/site-packages/sphinx_gallery/gen_rst.py:722: UserWarning: We'll no longer accept the way you call the record function in future versions of FURY.
Here's how to call the Function record: record(scene='value', cam_pos='value', cam_focal='value', cam_view='value', out_path='value', path_numbering='value', n_frames='value', az_ang='value', magnification='value', size='value', reset_camera='value', screen_clip='value', stereo='value', verbose='value')
exec(self.code, self.fake_main.__dict__)
Total running time of the script: (0 minutes 0.131 seconds)