Skip to content

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.

Figure 1: an animation of Spherical Harmonics in three different modes. From left to right the modes are (l=3, m=0), (l=10, m=3) and (l=10, m=0)

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.

import bpy
import numpy as np
from scipy.special import sph_harm 
import mathutils

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\).

l, m = 3,1

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).

Figure 2: a screenshot of where the Shape Keys can be found in Blenders interface. The Shape Keys tab is located within the Object Data tab in the Properties Editor.

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.

Figure 3: the Dope Sheet Editor.

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.

Figure 4: the shader nodes used for the animation in Fig. 1.

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


Last update: 28 August 2024 16:25:09