Animation

[1]:
from pythreejs import *
import ipywidgets
from IPython.display import display
[2]:
# Reduce repo churn for examples with embedded state:
from pythreejs._example_helper import use_example_model_ids
use_example_model_ids()
[3]:
view_width = 600
view_height = 400

Let’s first set up a basic scene with a cube and a sphere,

[4]:
sphere = Mesh(
    SphereBufferGeometry(1, 32, 16),
    MeshStandardMaterial(color='red')
)
[5]:
cube = Mesh(
    BoxBufferGeometry(1, 1, 1),
    MeshPhysicalMaterial(color='green'),
    position=[2, 0, 4]
)

as well as lighting and camera:

[6]:
camera = PerspectiveCamera( position=[10, 6, 10], aspect=view_width/view_height)
key_light = DirectionalLight(position=[0, 10, 10])
ambient_light = AmbientLight()

Keyframe animation

The three.js animation system is built as a keyframe system. We’ll demonstrate this by animating the position and rotation of our camera.

First, we set up the keyframes for the position and the rotation separately:

[7]:
positon_track = VectorKeyframeTrack(name='.position',
    times=[0, 2, 5],
    values=[10, 6, 10,
            6.3, 3.78, 6.3,
            -2.98, 0.84, 9.2,
           ])
rotation_track = QuaternionKeyframeTrack(name='.quaternion',
    times=[0, 2, 5],
    values=[-0.184, 0.375, 0.0762, 0.905,
            -0.184, 0.375, 0.0762, 0.905,
            -0.0430, -0.156, -0.00681, 0.987,
           ])

Next, we create an animation clip combining the two tracks, and finally an animation action to control the animation. See the three.js docs for more details on the different responsibilities of the different classes.

[8]:
camera_clip = AnimationClip(tracks=[positon_track, rotation_track])
camera_action = AnimationAction(AnimationMixer(camera), camera_clip, camera)

Now, let’s see it in action:

[9]:
scene = Scene(children=[sphere, cube, camera, key_light, ambient_light])
controller = OrbitControls(controlling=camera)
renderer = Renderer(camera=camera, scene=scene, controls=[controller],
                    width=view_width, height=view_height)
[10]:
renderer
[10]:
[11]:
camera_action
[11]:

Let’s add another animation clip, this time animating the color of the sphere’s material:

[12]:
color_track = ColorKeyframeTrack(name='.material.color',
    times=[0, 1], values=[1, 0, 0,  0, 0, 1])  # red to blue

color_clip = AnimationClip(tracks=[color_track], duration=1.5)
color_action = AnimationAction(AnimationMixer(sphere), color_clip, sphere)
[13]:
color_action
[13]:

Note how the two animation clips can freely be combined since they affect different properties. It’s also worth noting that the color animation can be combined with manual camera control, while the camera animation cannot. When animating the camera, you might want to consider disabling the manual controls.

Animating rotation

When animating the camera rotation above, we used the camera’s quaternion. This is the most robust method for animating free-form rotations. For example, the animation above was created by first moving the camera manually, and then reading out its position and quaternion properties at the wanted views. If you want more intuitive axes control, it is possible to animate the rotation sub-attributes instead, as shown below.

[14]:
f = """
function f(origu, origv, out) {
    // scale u and v to the ranges I want: [0, 2*pi]
    var u = 2*Math.PI*origu;
    var v = 2*Math.PI*origv;

    var x = Math.sin(u);
    var y = Math.cos(v);
    var z = Math.cos(u+v);

    out.set(x,y,z)
}
"""
surf_g = ParametricGeometry(func=f, slices=16, stacks=16);

surf1 = Mesh(geometry=surf_g,
             material=MeshLambertMaterial(color='green', side='FrontSide'))
surf2 = Mesh(geometry=surf_g,
             material=MeshLambertMaterial(color='yellow', side='BackSide'))
surf = Group(children=[surf1, surf2])

camera2 = PerspectiveCamera( position=[10, 6, 10], aspect=view_width/view_height)
scene2 = Scene(children=[surf, camera2,
                         DirectionalLight(position=[3, 5, 1], intensity=0.6),
                         AmbientLight(intensity=0.5)])
renderer2 = Renderer(camera=camera2, scene=scene2,
                     controls=[OrbitControls(controlling=camera2)],
                     width=view_width, height=view_height)
display(renderer2)
[15]:
spin_track = NumberKeyframeTrack(name='.rotation[y]', times=[0, 2], values=[0, 6.28])
spin_clip = AnimationClip(tracks=[spin_track])
spin_action = AnimationAction(AnimationMixer(surf), spin_clip, surf)
spin_action
[15]:

Note that we are spinning the object itself, and that we are therefore free to manipulate the camera at will.

Morph targets

Set up a simple sphere geometry, and add a morph target that is an oblong pill shape:

[16]:
# This lets three.js create the geometry, then syncs back vertex positions etc.
# For this reason, you should allow for the sync to complete before executing
# the next cell.
morph = BufferGeometry.from_geometry(SphereBufferGeometry(1, 32, 16))
[17]:
import numpy as np

# Set up morph targets:
vertices = np.array(morph.attributes['position'].array)
for i in range(len(vertices)):
    if vertices[i, 0] > 0:
        vertices[i, 0] += 1
morph.morphAttributes = {'position': [
    BufferAttribute(vertices),
]}

morphMesh = Mesh(morph, MeshPhongMaterial(
    color='#ff3333', shininess=150, morphTargets=True))

Set up animation for going back and forth between the sphere and pill shape:

[18]:
pill_track = NumberKeyframeTrack(
    name='.morphTargetInfluences[0]', times=[0, 1.5, 3], values=[0, 2.5, 0])
pill_clip = AnimationClip(tracks=[pill_track])
pill_action = AnimationAction(AnimationMixer(morphMesh), pill_clip, morphMesh)
[19]:
camera3 = PerspectiveCamera( position=[5, 3, 5], aspect=view_width/view_height)
scene3 = Scene(children=[morphMesh, camera3,
                         DirectionalLight(position=[3, 5, 1], intensity=0.6),
                         AmbientLight(intensity=0.5)])
renderer3 = Renderer(camera=camera3, scene=scene3,
                     controls=[OrbitControls(controlling=camera3)],
                     width=view_width, height=view_height)
display(renderer3, pill_action)

Skeletal animation

First, set up a skinned mesh with some bones:

[20]:
import numpy as np

N_BONES = 3

ref_cylinder = CylinderBufferGeometry(5, 5, 50, 5, N_BONES * 5, True)
cylinder = BufferGeometry.from_geometry(ref_cylinder)
[21]:
skinIndices = []
skinWeights = []
vertices = cylinder.attributes['position'].array
boneHeight = ref_cylinder.height / (N_BONES - 1)
for i in range(vertices.shape[0]):

    y = vertices[i, 1] + 0.5 * ref_cylinder.height

    skinIndex = y // boneHeight
    skinWeight = ( y % boneHeight ) / boneHeight

    # Ease between each bone
    skinIndices.append([skinIndex, skinIndex + 1, 0, 0 ])
    skinWeights.append([1 - skinWeight, skinWeight, 0, 0 ])

cylinder.attributes = dict(
    cylinder.attributes,
    skinIndex=BufferAttribute(skinIndices),
    skinWeight=BufferAttribute(skinWeights),
)

shoulder = Bone(position=(0, -25, 0))
elbow = Bone(position=(0, 25, 0))
hand = Bone(position=(0, 25, 0))

shoulder.add(elbow)
elbow.add(hand)
bones = [shoulder, elbow, hand]
skeleton = Skeleton(bones)

mesh = SkinnedMesh(cylinder, MeshPhongMaterial(side='DoubleSide', skinning=True))
mesh.add(bones[0])
mesh.skeleton = skeleton
[22]:
helper = SkeletonHelper(mesh)

Next, set up some simple rotation animations for the bones:

[23]:
# Rotate on x and z axes:
bend_tracks = [
    NumberKeyframeTrack(
        name='.bones[1].rotation[x]',
        times=[0, 0.5, 1.5, 2],
        values=[0, 0.3, -0.3, 0]),
    NumberKeyframeTrack(
        name='.bones[1].rotation[z]',
        times=[0, 0.5, 1.5, 2],
        values=[0, 0.3, -0.3, 0]),
    NumberKeyframeTrack(
        name='.bones[2].rotation[x]',
        times=[0, 0.5, 1.5, 2],
        values=[0, -0.3, 0.3, 0]),
    NumberKeyframeTrack(
        name='.bones[2].rotation[z]',
        times=[0, 0.5, 1.5, 2],
        values=[0, -0.3, 0.3, 0]),
]
bend_clip = AnimationClip(tracks=bend_tracks)
bend_action = AnimationAction(AnimationMixer(mesh), bend_clip, mesh)

# Rotate on y axis:
wring_tracks = [
    NumberKeyframeTrack(name='.bones[1].rotation[y]', times=[0, 0.5, 1.5, 2], values=[0, 0.7, -0.7, 0]),
    NumberKeyframeTrack(name='.bones[2].rotation[y]', times=[0, 0.5, 1.5, 2], values=[0, 0.7, -0.7, 0]),
]

wring_clip = AnimationClip(tracks=wring_tracks)
wring_action = AnimationAction(AnimationMixer(mesh), wring_clip, mesh)
[24]:
camera4 = PerspectiveCamera( position=[40, 24, 40], aspect=view_width/view_height)
scene4 = Scene(children=[mesh, helper, camera4,
                         DirectionalLight(position=[3, 5, 1], intensity=0.6),
                         AmbientLight(intensity=0.5)])
renderer4 = Renderer(camera=camera4, scene=scene4,
                     controls=[OrbitControls(controlling=camera4)],
                     width=view_width, height=view_height)
display(renderer4)
[25]:
bend_action
[25]:
[26]:
wring_action
[26]:
[ ]: