Animation¶
In [1]:
from pythreejs import *
import ipywidgets
from IPython.display import display
In [2]:
# Reduce repo churn for examples with embedded state:
from pythreejs._example_helper import use_example_model_ids
use_example_model_ids()
In [3]:
view_width = 600
view_height = 400
Let’s first set up a basic scene with a cube and a sphere,
In [4]:
sphere = Mesh(
SphereBufferGeometry(1, 32, 16),
MeshStandardMaterial(color='red')
)
In [5]:
cube = Mesh(
BoxBufferGeometry(1, 1, 1),
MeshPhysicalMaterial(color='green'),
position=[2, 0, 4]
)
as well as lighting and camera:
In [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:
In [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.
In [8]:
camera_clip = AnimationClip(tracks=[positon_track, rotation_track])
camera_action = AnimationAction(AnimationMixer(camera), camera_clip, camera)
Now, let’s see it in action:
In [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)
In [10]:
renderer
In [11]:
camera_action
Let’s add another animation clip, this time animating the color of the sphere’s material:
In [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)
In [13]:
color_action
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.
In [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)
In [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
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:
In [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))
In [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:
In [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)
In [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:
In [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)
In [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
In [22]:
helper = SkeletonHelper(mesh)
Next, set up some simple rotation animations for the bones:
In [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)
In [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)
In [25]:
bend_action
In [26]:
wring_action
In [ ]: