Animating Spherical Harmonics with Python and Shape Keys¶
Here's an example in the captivating world of scripting in Blender! Weâll dive into the art of animating mesh vertices using Blenderâs Python API and Shape Keys. But this wonât be your typical tutorial; weâre about to bring mathematics to life. Picture this: spherical harmonic functions breathing life into a seemingly static sphere, making it dance to the rhythm of oscillating modes. Intrigued? Learn how to make the mesmerizing animation in Fig. 1 using Shape Keys and the Python API in Blender.
Introduction¶
Spherical harmonics are functions defined on the surface of a sphere. They appear in various natural phenomena, such as the mathematical description of the âorbitâ of an electron in a hydrogen atom. However, you do not need to know more about these functions to follow along in this blog. We will utilize the spherical harmonics available in the Python package SciPy. These functions will serve as standing waves on the surface of a sphere (see Fig. 1), similar to standing waves on a guitar string.
Start of our script¶
To begin, we will import essential Python packages into our script. We need bpy
to access the Blender Python API, numpy
for various mathematical functions, and Blender's mathutils
to work with vectors. Additionally, we need the spherical harmonics module from scipy.special
.
Much like a string, a sphere can oscillate in different modes. The modes of spherical harmonics are defined using two integers: \(l\) and \(m\). Value \(l\) ranges from 0, 1, 2, 3, and so on, and for each \(l\) the value \(m\) can vary from \(-l\), .., 0, to \(l\). For instance, when \(l\) is set to 3, \(m\) can take on the values -3, -2, -1, 0, 1, 2, and 3. We will create two Python variables to define \(l\) and \(m\).
Our objective is to visualize spherical harmonics as standing waves on a sphere. To achieve this, weâll start by creating a UV sphere in Blender using the code below. Afterward, weâll obtain the new sphere object by referencing the active object in Blender and assigning it a distinctive name using obj.name
.
radius = 3
bpy.ops.mesh.primitive_uv_sphere_add(segments=128, ring_count=128, \
radius=radius, calc_uvs=True, enter_editmode=False, \
align='WORLD', location=(0.0, 0.0, 0.0), \
rotation=(0.0, 0.0, 0.0), scale=(1.,1.,1.))
obj = bpy.context.active_object
obj.name = str(l)+"_"+str(m)
Getting the coordinates to work¶
The spherical harmonic function in SciPy uses polar coordinates, whereas Blender employs Cartesian coordinates. Hence, we must be capable of converting between polar and Cartesian coordinates, as well as performing the reverse conversion. Itâs not essential to have a deep understanding of these conversions, particularly the transformation from Cartesian to polar coordinates. (This is tricky near the poles of the sphere and mapping the entire sphere can be challenging, considering that trigonometric functions are defined on only a portion of the sphere.)
def getCart(r, theta, phi):
x = r * np.cos(phi) * np.sin(theta)
y = r * np.sin(phi) * np.sin(theta)
z = r * np.cos(theta)
return (x, y, z)
def getPolar(x, y, z):
# Round out small errors from the trigonometric functions
x, y, z = round(x, 5), round(y, 5), round(z, 5)
r = np.sqrt(x**2 + y**2 + z**2)
theta = np.arccos(z/r) #[0,pi]
xy = round(r * np.sin(theta), 5)
if xy == 0.0:
phi = 0.0
else:
if x >= 0:
phi = np.arcsin(y/xy) #[-1/2pi, 1/2pi]
else:# x < 0:
phi = np.arcsin(y/xy) #[-1/2pi, 1/2pi]
phi = np.pi - phi
return (r, theta, phi)
In our case, we consider \(Ξ\) as the angle measured from the z-axis, and \(Ï\) as the angle measured from the x-axis. Itâs necessary to round off some values because trigonometric functions donât consistently yield precise zeros. The \(arccosine\) function has a range from zero to \(Ï\), while the \(arcsine\) function covers only the range from \(-1/2 Ï\) to \(1/2 Ï\). As a result, we need to manually ensure that we account for both sides of the sphere, including the positive and negative sides of the x-axis.
Shape Keys¶
Shape Keys are a method to alter the form of an object and are particularly useful for animation. You can access Shape Keys in the Properties editor under the Object Data tab and the Shape Keys tab (see Fig. 2).
Using the +
` button, you can add Keys to the list. The initial Key you add is known as the Basis, representing the objectâs shape when all other Keys are set to zero. Adding additional Keys allows you to define alternative shapes. Ensure that the Key is selected in the Object Data tab and then switch to edit mode. Here, you can make adjustments, such as modifying the location of the vertices (in the next section we will do the same but through code).
Each Key is associated with a numerical value (called Value in the Shape Key tab, see Fig. 2). When this value is set to zero, the Key has no impact on the objectâs shape. Setting it to 1 results in the object taking on the shape defined by that Key. Intermediate values produce shapes that interpolate between the Basis shape and the Key shape.
Youâll notice that the Value associated with the Key has a dot next to the field. This signifies that you can animate this value, thereby animating the objectâs shape.
Using Shape Keys in the Python API¶
Weâve created a sphere in Python, and weâve imported the spherical harmonics from the SciPy library. These spherical harmonic functions provide us with the amplitude (named \(Amp\) below) at every \(Ξ\) and \(Ï\) coordinate of the vertices on the sphere. For the animation, weâll use these amplitudes within Blenderâs Shape Keys.
First, we need to create the Basis and a second Shape Key programmatically in Python. Shape Keys are part of the objectâs data and can be added using the obj.shape_key_add(name='Basis')
method. We then specify that we want linear interpolation between these Keys, and we want the Keys to be relative to the Basis Key (see the code below). Subsequently, we add a second Shape Key and set its interpolation to linear as well.
# Make the basis shape key at minimum amplitude
sk_basis = obj.shape_key_add(name='Basis')
sk_basis.interpolation = 'KEY_LINEAR'
obj.data.shape_keys.use_relative = True#False
# The next shape key is at max amplitude
sk2 = obj.shape_key_add(name='Deform')
sk2.interpolation = 'KEY_LINEAR'
Next, we proceed to modify the vertices within both Shape Keys. To achieve this, we iterate over the vertices of the object (obj.data.vertices
). We convert the Cartesian coordinates of these vertices into polar coordinates and calculate the amplitude using spherical harmonics. Since spherical harmonics are complex functions, itâs crucial to consider either the real or imaginary part of the function.
for i in range(len(obj.data.vertices)):
x, y, z = obj.data.vertices[i].co
r, theta, phi = getPolar(x, y, z)
Amp = sph_harm(m, l, phi, theta).imag if m<0 \
else sph_harm(m, l, phi, theta).real
VertMin = mathutils.Vector(getCart(r - Amp, theta, phi))
VertMax = mathutils.Vector(getCart(r + Amp, theta, phi))
sk_basis.data[i].co = VertMin
sk2.data[i].co = VertMax
Subsequently, we calculate the vertex positions corresponding to the minimum amplitude (\(r - Amp\)) and maximum amplitude (\(r + Amp\)). Afterward, we convert these positions back into Cartesian coordinates and update the vertex positions in the âBasisâ Key to reflect the minimum values and in the second Key to represent the maximum values. As a result, weâve successfully created our Shape Keys.
After running the script, you can navigate to the Object Data properties and adjust the Value
associated with the second Key (see Fig. 2) to observe the resulting spherical harmonic transformation.
Animation¶
XXX In an upcoming blog, I will demonstrate how to automate the animation of these spheres using the Python API. However, for this blog, we will create animations manually in order to achieve what you see in Fig. 1. The simplest way to proceed is by navigating to the Animation Workspace. At the bottom of the interface, youâll find the âDope Sheetâ (see Fig. 3). Iâll assume youâre already familiar with animating objects; if not, you can refer to our course site for guidance.
Begin by setting the current frame to 0. Now select the âDeform Shapeâ in the Object Data Properties tab and set the âValueâ field to zero (see Fig. 2). Use the dot next to the âValueâ field (see Fig. 2) to create a keyframe. Youâll notice a keyframe marker appearing at frame 0 on the timeline in the Dope Sheet, indicating the creation of a keyframe.
Proceed to frame 20, set the âValueâ for the Shape Key to 1, and press the diamond-shaped icon (formerly a dot) located next to the âValueâ field to create another keyframe. Repeat this process for frame 40, setting the âValueâ back to zero. Finally, set the âEndâ frame of the animation to 40. You can locate this setting at the bottom-right corner of the Dope Sheet (as depicted in Fig. 3). Now, start the animation and enjoy the result.
Materials¶
If youâre curious about the type of material Iâve used to create Fig. 1, itâs actually quite straightforward. Navigate to the Shading Workspace and add a Glass BSDF shader. Then, connect it to the output. For information regarding the shaderâs settings, see Fig. 4.
Conclusion and exercises¶
I hope this blog has helped you understand what Shape Keys and the Python API can do for you in Blender. It would be great if you could apply these techniques to your scientific visualization or any type of visualization project you are working on. I would love to hear from you and see what you have created. In the next blog we will explore how we can automate the animation and shading of these spheres using the Python API in Blender. Until then, keep creating and exploring!
Cheers, Ben