DEV Community

x
x

Posted on

How to script a digital clock in Blender using Python

This tutorial will use Blender 2.91

Available here

Sample countdown

Set up the scene.

This won't cover the basics. I highly suggest Andrew Price's excellent tutorial videos on YouTube to get you started. However, everything needed to duplicate this scene exactly is listed below.

Everything for this scene is default except for removing the default cube, repositioning the camera, and adding the text with the settings shown.

The material for the plane and the text is the default principled shader, in this case the plane is black and the text is white.

Beginning setup.

This is what we start with. Take note that the text object has the name "Text". We will use that to reference this object later. Also, do not convert the text to mesh. Some fonts require you to convert them to a static mesh to render correctly because of artifacts when you give them 3d geometry as text objects. You just have to choose compatible fonts to do it this way. Smooth corners and edges are best.

Release the Python

Much danger noodles

Everything that I have done to set this scene up using the UI can be scripted with Python as well. To learn the API, simply use Blender and look at the output from the Python console. In this case, I used the UI to make the scene and left Python to code the interesting part, but if you duplicated the commands from the Python console in a script as well, you would get the same scene. In fact, you don't need the UI at all and can construct an entire scene from the Python shown if you want a challenge.

How it works

The following will break down the data structure, generator, and callback that makes the clock work.

Data structure.

This total_frames in the following section is being calculated for a full motion display. This example doesn't need to render all these frames if none of the scene elements move and the way to do that is covered later.

minute, second = '10', '01' # variable assignment
total_frames = ((int(minute) * 60) + int(second)) * 30 # 30fps video times the total seconds
scene.frame_end = total_frames # let Blender know how long the scene is.

seconds = ['%02d'% (i,) for i in reversed(range(60))] # list comprehension to create all the seconds in a minute

seconds_arr = [seconds] * int(minute) # this is what we give to our generator

if not second == '00': # if the timer is not a whole minute, we need a partial list to add to the front.
    seconds_arr[:0] = [seconds[(60 - int(second)):]]
Enter fullscreen mode Exit fullscreen mode

This will create the correct order for our clock to count down to zero. To count up, omit

reversed()
Enter fullscreen mode Exit fullscreen mode

and add the partial seconds to the end

[seconds[:(60 - int(second))]]
Enter fullscreen mode Exit fullscreen mode

Generator

There are several ways to iterate over the structure we just made. The method I chose was to create a generator because I knew the frame change callback would be the perfect time to call a next iteration function.

# this function will count through the seconds_arr and give us the correct clock face string each time it is called.
@persistent # this decorator keeps the generator from being refreshed during the render.
def time_remaining(seconds_arr):
    while seconds_arr:
        current_minute = seconds_arr.pop(0)
        for sec in current_minute:
            yield (str(len(seconds_arr)) + ':' + str(sec))


clock = time_remaining(seconds_arr) # now we have the generator, we just need a way to call it
Enter fullscreen mode Exit fullscreen mode

Complications

Skip this if you just want the countdown. I wanted the clock to move gently around the scene and I didn't want to set all those keyframes by hand. You can use other methods you may remember from geometry to draw circles or figure 8 patterns. I just wanted random movement in all axis within a small range.

text.location = (0,0,.2) # set the starting location x, y, z
text.keyframe_insert('location', frame=1) # first keyframe
text.keyframe_insert('location', frame=scene.frame_end) # last keyframe

for frame in range(total_frames):
    if frame % 300 == 0: # every 10 seconds
        text.location = (random.uniform(-2,2),random.uniform(-1.2,1.2),random.uniform(.2, 1.0)) # pick a random number for all three axis
        text.keyframe_insert('location', frame=frame) # add keyframe

text.location = (0,0,.2) # move the text back to the start before the render starts (for a test render)
Enter fullscreen mode Exit fullscreen mode

Callback

Time to tie it all together and automate our scene. Blender provides several powerful events that you can trigger callbacks with.

# this is the callback function that will fire on each frame change. The callback passes the current scene as its first argument.
def draw_clock(scene):
    global clock # global variable
    text = scene.objects['Text'] # grab the text object
    current_frame = scene.frame_current
    if current_frame % 30 == 0: # every second at the project fps
        text.data.body = next(clock) # invoke the generator

bpy.app.handlers.frame_change_pre.append(draw_clock) # attach callback to the frame change event.
Enter fullscreen mode Exit fullscreen mode

The method of choice for this example is frame_change_pre. This will fire on each frame change, before the frame is drawn. If the frame number on change is divisible by the frame rate, then it will decrement the second count until it reaches zero just before the last rendered frame.

Time to render

You only need to run the script one time. In the above screenshot, I left the commented code for using this script from the command line. Once the script has run, you should have the following first frame if you render the image.

Time to start

If it all looks good, check your output directory, change the project fps to 30, and hit Render Animation. A 10:01 animation is 18030 frames, so expect this render to take a long time if the clock is moving around the screen with the optional code! If you are rendering without motion, you can start on frame 0 and skip 30 frames each sequence since the light source is not moving and there is no reason to render the exact same frame repeatedly.

Top comments (0)