x
Posted on December 2, 2020
This tutorial will use Blender 2.91
Available here
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.
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
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)):]]
This will create the correct order for our clock to count down to zero. To count up, omit
reversed()
and add the partial seconds to the end
[seconds[:(60 - int(second))]]
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
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)
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.
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.
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.
Posted on December 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.