Create 3D objects and animations in Blender with Python API

Did you know that you could program in Blender to automatically create 3D objects and animations? Mina Pêcheux, Full stack web and game developer shares her best tips and trix to make it happen. Read the full guide here.

Blender is a well-known piece of software for 3D modeling, sculpting, texturing, animation and more. As the versions kept on coming out, this tool has slowly earned its place in the CGI industry, so much so that there are now a few long-feature films that are made entirely using Blender, and Youtube channels like Blender Guru entirely dedicated to learning the ins and outs of this software. And all this while it's completely free and open-source.

This has introduced a big change of mindset in the world of 3D, because it has showed people that anyone could have a go at this artform and achieve pretty incredible results.

So yes, at first, CGI software is primarily aimed at artists. But Blender, just like its competitors, also has another advantage for developers: a way to program your 3D scenes.

Today, I want to show how this programmatic approach to Blender allows you to instantly create a basic solar system, like this one:

Are you ready? Then let’s dive in! If you want to get the code directly, it's available as a Github gist 🚀

The Blender Python API

Ok now, how does Blender allow us to “program a scene”? The answer: via its Python API.

You might be wondering why it’s interesting to have a Python API for a 3D software. I mean, why embed a creative tool with stuff for coders?

There are, in truth, lots of use cases where it can be useful to automate a task: whether you want to quickly randomise your scene population algorithm, count objects and get custom stats on your scene – or even create an entire world from scratch that can be reproduced accurately with seeds. Having a way to integrate procedural generation or tailor-made tools directly into a CG context is just an amazing opportunity!

Moreover, the fact that it’s in Python, a language known for being easy-to-learn and for which there is just an endless number of tutorials online, makes it perfect for beginners to dive in to (without the fear of old C/C++-based APIs often requiring, in my opinion, higher coding skills).

By the way: this Python API is not just a shiny toy for wannabe-devs: it’s actually part of the Blender software itself and it’s used internally by the program as a core tool, although the user inputs and the results are wrapped in a user-friendly UI.

The API in itself, bpy (for “Blender Python”), can be browsed on Blender’s specific docs; it is subdivided in several submodules, the 3 most important and commonly used being:

  • bpy.context: it contains getters and readers on read-only values that describe your current working context or even the area (i.e. the panel in your window) that is currently being accessed
  • bpy.data: it gives you access to the resources in your scene (the objects, the materials, the meshes…) so you can load, add or delete them
  • bpy.ops: that’s the real meat of the API – it’s what allows you to perform actions and call operators on your objects or your views; it’s basically how you can simulate user actions via scripting (like selecting an object, entering edit mode, applying subdivisions, changing to “flat” shading, maximising a window…)

Today, we’ll focus on generating objects thanks to this API, so we’ll be doing some procedural generation. I’ve already discussed the benefits of this approach in other articles; roughly put, procedural generation is about defining a set of rules and a machine that uses those rules to automatically create valid instances. The big advantage is that, once you’re done making the engine, you can create as many instances as you want! Moreover, this generation can be pretty quick, dynamic depending on given conditions and infinite (for example for never-ending games like runners). But, of course, it’s usually a bit harder than hand-design because you need to teach your software the right and wrong patterns (i.e., the “rules”).

Procedural generation is a very vast and complex topic. There are plenty of tools for generation engines – and to be honest, a large amount relies on randomness. The idea is to start off with random values and then somehow “control the craziness” so it is valid.

Spawning and animating a random solar system

To see how powerful Blender’s Python API can be and how to use it, let’s work on a basic example: instantiating a bunch of planets around a sun (all being simple spheres) with random size, speed and colour, and having the planets rotate around the sun along circular orbits.

Now, let’s be clear: this will not be a physical simulation. We will pick all of our values at random and there will be no logical relationship whatsoever between the distance to the sun, the radius and the surface colour of the planets. The goal here is just to play around with Blender’s Python API, to see how to do some procedural generation and to have fun with glowy spheres.

Step 1: Preparing our Blender

First things first: let’s see how to setup our Blender for Python and learn the different tools we have at our disposal.

Note: this tutorial uses screenshots from Blender 2.93. Also, I will be using the Python API from Blender 2.8+, so make sure your version matches.

When you want to work with Python in Blender, something that is quite useful is to start your Blender with the console attached. This will allow you to see some logs if your Python script errors, which is pretty essential for proper debugging!

Basically, the idea is to launch Blender from your terminal rather than from your Application shortcut. The exact path to your Blender executable depends on your OS; I’m using Mac, so all I have to do is open a terminal and then run something like this:

/Applications/Blender.app/Contents/MacOS/Blender &

From that point on, whatever outputs my Python scripts give me will show up in this console.

Now, to get ready for coding, simply go to the “Scripting” tab at the top of your Blender’s window:

This will give a set of new panels in your workspace, namely:

  1. the 3D view: it’s just like you’re usually getting started screen in Blender – you’re seeing your current scene in 3D, in “solid” shade mode, and you can select your various objects as usual
  2. the Python interactive console: that tailor-made Python console is incredibly useful to browse the Blender API and explore all the fields and methods the Python Blender objects provide us with; in addition to the basic Python built-ins, this console also comes loaded with Blender’s built-ins and a few convenience variables to easily access the Blender API common submodules
  3. the Info panel: it might seem strange at first, but this panel just debugs everything you do – which is yet again great for discovering the possibilities of the API! If you try and select an object, or move your light, or any other action that you’re used to doing in Blender, you’ll see it is logged in this panel and so you can understand which part of the API you should access to reproduce this action via scripting
  4. the Text Editor: you might have guessed – it’s the main course for us today; this is where we’ll create, edit, save and run our Python script. It is a simple Python editor with syntax highlighting, line numbers, and even a few utilities like “Find & Replace” if you open the sidebar (with <Ctrl + T>, or <Cmd + T> on Mac). Note however that this editor is limited in terms of features so if you plan on making a big thing, it’s probably better to use a nice external IDE and then load your script in Blender when it’s ready
  5. the Outliner: that’s another common view you likely already know that just shows the hierarchy of objects in your current scene and allows for quick access
  6. the Properties panel: similarly, this panel is the same as in the common layouts – it’s where you can view and edit the properties of your scene, your render parameters, your world settings and the specific data for your currently selected object

To check that everything is working, simply click on the “+ New” button of the Text Editor, write a basic print('hello world') line in the Python script and click the “run” icon on the right (or use the shortcut <Alt + P>; make sure your mouse is hovering the Text Editor panel for this to work).

If everything is setup properly, you should see “hello world” appear in your attached terminal!

Step 2: Creating a basic mesh via the API

Alright! Time to get real and instantiate some objects in our scene using Python.

To begin with, delete all the initial objects: the cube, the camera and the light. We won’t be using those.

Now, let’s edit our Python script to create a new sphere object. This is done using the bpy.ops subpackage and more precisely the mesh.primitive_uv_sphere_add() method:

import bpy

bpy.ops.mesh.primitive_uv_sphere_add(
    radius=3,
    location=(0, 0, 0),
    scale=(1, 1, 1)
)

The parameters are pretty self-explanatory: we are creating a sphere with a radius of 3 at the origin point, with a normalised scale. (Feel free to browse the docs for more details on the available options)

Once again, run your code by clicking the “run” icon or with the <Alt + P> shortcode. And tadaa! We’ve just created a simple UV sphere in our 3D scene purely via scripting!

Step 3: Instantiating our planets

We’re now able to spawn one sphere – so let’s see how easy it is to spawn several!

What we want to do is:

  • to create N_PLANETS objects, each being a simple UV sphere
  • have every object use a random radius within a given range
  • and a random distance from the origin (where we’ll eventually place our sun) that depends on the index of the planet (so that their orbits are nicely spread)

Making N_PLANETS instances is straight-forward: we’ll simply wrap our primitive_uv_sphere_add() call in a for-loop and run it multiple times. To keep the script readable, we’ll actually extract this instantiation process into a util function, create_sphere(), and we’ll pass it in the random radius and distance.

To get a random value for our radius and our distance, we can rely on the Python built-in random module.

Also, we should name our objects properly: this is interesting to keep our hierarchy clear and it will be essential when, later on, we automatically clean up the scene upon script initialization. I’ll simply call each object “Planet-00”, “Planet-01” and so on, for the N_PLANETS spheres.

Here is the updated script:

from random import random
import bpy

def create_sphere(radius, distance_to_sun, obj_name):
    # instantiate a UV sphere with a given
    # radius, at a given distance from the
    # world origin point
    obj = bpy.ops.mesh.primitive_uv_sphere_add(
        radius=radius,
        location=(distance_to_sun, 0, 0),
        scale=(1, 1, 1)
    )
    # rename the object
    bpy.context.object.name = obj_name
    # return the object reference
    return bpy.context.object

N_PLANETS = 6

for n in range(N_PLANETS):
    # get a random radius (a float in [1, 5])
    r = 1 + random() * 4
    # get a random distace to the origin point:
    # - an initial offset of 30 to get out of the sun's sphere
    # - a shift depending on the index of the planet
    # - a little "noise" with a random float
    d = 30 + n * 12 + (random() * 4 - 2)
    # instantiate the planet with these parameters
    # and a custom object name
    create_sphere(r, d, "Planet-{:02d}".format(n))

If you run this again, you should get a little set of well-aligned planets with various sizes that are all named in the right format:

Step 4: Adding the sun and the radius rings

In a similar way, we can re-use our create_sphere() method and make another one, called create_torus(), to add a sphere for the sun and some torus objects to show the planet orbits:

from random import random
import bpy

def create_sphere(radius, distance_to_sun, obj_name):
    # ...

def create_torus(radius, obj_name):
    # (same as the create_sphere method)
    obj = bpy.ops.mesh.primitive_torus_add(
        location=(0, 0, 0),
        major_radius=radius,
        minor_radius=0.1,
        major_segments=60
    )
    bpy.context.object.name = obj_name
    return bpy.context.object

N_PLANETS = 6

for n in range(N_PLANETS):
    # ...
    # add the radius ring display
    create_torus(d, "Radius-{:02d}".format(n))

# add the sun sphere
sun = create_sphere(12, 0, "Sun")

The sun is of course bigger than the planets, and the rings are simply placed at the same time as the planet spheres, using the distance to the origin for the torusradiusparameter:

Step 5: Setting the materials and the shading for our objects

This is great, but those objects are a bit morose, all gray and blocky. It’s time to work on two important 3D concepts: the shading and the materials of our objects!

Generally speaking, the shading refers to how an object reacts to light and is drawn in the 3D view or in a render. Here however, I’m focusing on the “smooth” versus “flat” shading, which determines how faceted an object appears. It’s basically a second layer that further impacts how the object is rendered, but doesn’t change its actual geometry: it just looks smoother.

Setting the objects to use the “smooth” shading is really quick to do: we have a dedicated shade_smooth() method in the bpy.ops submodule:

from random import random
import bpy

def create_sphere(radius, distance_to_sun, obj_name):
    # ...
    # apply smooth shading
    bpy.ops.object.shade_smooth()
    # return the object reference
    return bpy.context.object

def create_torus(radius, obj_name):
    # ...
    # apply smooth shading
    bpy.ops.object.shade_smooth()
    # return the object reference
    return bpy.context.object

# ...

The real question here is: how can we give some colors to our planets, and how could we make our sun glow?

The answer is: thanks to materials and shaders!

What are materials and shaders?

Both those terms can be used to talk about “the palette of properties that define how an object gets rendered”.

Roughly put, shaders are what give 3D objects their color, their shininess, their glossiness, their roughness… Think about it: in the real world, what makes you say that “hum, this is wood” is just a whole set of clues: the object is some tint of brown, it reflects light a bit but not with sharp light spots like metal, it doesn’t refract rays like glass and it doesn’t reflect your image like a mirror… Well – with the ever-improving CGI technology, all of these properties can now be modeled and reproduced in our 3D scenes!

Note: when you’re really aiming for realistic renders, you’ll probably have to dive into physical-based rendering or PBR. And that’s what Blender’s Cycles engine is for creating amazingly lifelike pictures that just “feel” real, mostly thanks to a bunch of complex and well-tweaked shaders.

In Blender, shaders are usually edited via the node-based graph editor (available in the “Shading” tab) that lets you chain and combine as many built-in nodes as you want to build more or less complex shading flows. In this tutorial, however, we’ll make a super simple shader that only has one node, so we’ll do everything in our Python script.

Materials then use those shaders and apply them to your 3D geometry. An object may have several material slots, i.e. it can use different shaders for different parts of the geometry, but we won’t get into that today, and we’ll stick with one material slot per object.

Picking our shader type

Ok, so – what shader should we create?

For this project, I’m using the EEVEE engine that can also work with shader nodes, even though it doesn’t have all the same node types as the Cycles engine.

But it’s fine because, here, what I want is one that exists in both: the Emission shader. You can picture it as a big lightbulb that has an intensity (the “strength” parameter) and a color. It will make your 3D object emit light (so your object will become a light source in your scene that we’ll interact with the rest of the meshes!) and essentially make it “glow".

Creating a shader in our Python script

To create and assign a shader fully via scripting, we have to:

  • create a new shader resource using the bpy.data submodule
  • “edit” this shader as we would with the shader node editor: we’ll remove some nodes, add others, set their properties and link them together
  • retrieve the reference to our newly created material
  • and finally, add it to the materials data (i.e. the slots) of our object

Let’s work on this step by step. We’ll start by making a new function called create_emission_shader() that will receive some strength and color parameters, and that will use those to setup a basic 2-nodes graph with an Emission node and an output node.

The idea is to start from the basic node template and clear all the starter nodes; then, we can add in our emission and output nodes, configure the emission node by updating the values of its input fields and create a link between the two nodes:

def create_emission_shader(color, strength, mat_name):
    # create a new material resource (with its
    # associated shader)
    mat = bpy.data.materials.new(mat_name)
    # enable the node-graph edition mode
    mat.use_nodes = True
    
    # clear all starter nodes
    nodes = mat.node_tree.nodes
    nodes.clear()

    # add the Emission node
    node_emission = nodes.new(type="ShaderNodeEmission")
    # (input[0] is the color)
    node_emission.inputs[0].default_value = color
    # (input[1] is the strength)
    node_emission.inputs[1].default_value = strength
    
    # add the Output node
    node_output = nodes.new(type="ShaderNodeOutputMaterial")
    
    # link the two nodes
    links = mat.node_tree.links
    link = links.new(node_emission.outputs[0], node_output.inputs[0])

    # return the material reference
    return mat

It’s now pretty easy to use this method to create our material resources and apply them to our objects. We’ll need one white emissive material for the rings, one yellow emissive material for the sun and one emissive material per planet with random colour (though I’ll add in more blue for a better overall color balance):

# ...

N_PLANETS = 6

ring_mat = create_emission_shader(
    (1, 1, 1, 1), 1, "RingMat"
)
for n in range(N_PLANETS):
    # ...
    planet = create_sphere(r, d, "Planet-{:02d}".format(n))
    planet.data.materials.append(
        create_emission_shader(
            (random(), random(), 1, 1),
            2,
            "PlanetMat-{:02d}".format(n)
        )
    )
    # add the radius ring display
    ring = create_torus(d, "Radius-{:02d}".format(n))
    ring.data.materials.append(ring_mat)

# add the sun sphere
sun = create_sphere(12, 0, "Sun")
sun.data.materials.append(
    create_emission_shader(
        (1, 0.66, 0.08, 1), 10, "SunMat"
    )
)

If you change the shading mode in your 3D view to “Rendered”, you delete all the objects currently in the scene and you run the script again, you’ll see that they now have nice glowy materials on them!

Note that after you’ve run the script, you can even go to the “Shading” tab, select an object with a shader and see the shader graph:

It looks as expected: two nodes, one Emission and one Output, a link between the two, and some custom values for the “Strength” and “Color” properties.

Note: when you initially open the panel, nodes will be all packed together in the middle on top of each other. Here, I moved the nodes manually for the demo but you can actually do it in your code, too, with the .location property.

Step 6: Animating the planets

We’re getting close to having our final “solar system generator”! The last thing we need to take care of is animating the planets so that they rotate around the sun as time passes by.

To do this, we’ll use Blender’s animation curve system (the F-curves). It’s a more advanced version of keyframed animation where you specify the key values for one or more properties of your object over time (e.g. you force its position, rotation, scale… at a given frame) but also the interpolation between them.

Here is an example of the same set of key points with the three possible interpolations (from left to right: constant, linear and bézier):

You see how the interpolation affects the intermediary auto-computed values (all the segments in-between the keyframes that we defined manually) and how that impacts the overall evolution of this property for the object. Suppose this line represents the altitude of a little helicopter:

  • with constant interpolation, the helicopter just keeps on diving up and down, then going straight ahead with little plateaux
  • with linear interpolation, the helicopter slides from one point to another with sweet diagonals, but it still changes direction quite suddenly when it reaches extrema
  • with bézier interpolation, the helicopter slows down before reaching those valleys and ridges and smoothly updates its course

The “best interpolation” depends on the type of animation you want. In our case, we should pick the linear interpolation so that planets just move at a regular pace along their orbits.

To create some animation for our planets, we just need to use the animation_data_create() method and the animation_data field of our objects to create and then edit the F-curve for the Z-rotation property. This property is actually part of a 3D-vector property of the object called the “Euler rotation” (more on Blender rotation modes here) where the Z-axis is the third component, i.e. the one at index 2 (because the components are 0-indexed).

Once we’ve grabbed this property, we’ll simply add two keyframes: one for the start frame (with our current rotation of 0) and another for the end frame (with a random rotation of one or more semi-circles around the sun, so that the planets have different speeds). And we’ll make sure that those keyframes use a linear interpolation mode.

Note: all rotations must be written using radians.

from math import pi

# ...

N_PLANETS = 6
START_FRAME = 1
END_FRAME = 200

for n in range(N_PLANETS):
    # ...

    # setup the planet animation data
    planet.animation_data_create()
    planet.animation_data.action = bpy.data.actions.new(name="RotationAction")
    fcurve = planet.animation_data.action.fcurves.new(
        data_path="rotation_euler", index=2
    )
    k1 = fcurve.keyframe_points.insert(
        frame=START_FRAME,
        value=0
    )
    k1.interpolation = "LINEAR"
    k2 = fcurve.keyframe_points.insert(
        frame=END_FRAME,
        value=(2 + random() * 2) * pi
    )
    k2.interpolation = "LINEAR"

If you clean up your scene and re-run the script, you’ll see that… nothing happens! The planets aren’t moving, even when you play the animations!

And that’s because, in truth, they are rotating… but not around the right pivot! For now, the planet are simply revolving, they are rotating around their local Z axis.

To fix this, we just need to change the pivot point of our objects and snap it back to the cursor that is at the world origin point (the same location as our sun):

# ...

for n in range(N_PLANETS):
    # ...

    # set planet as active object
    bpy.context.view_layer.objects.active = planet
    planet.select_set(True)
    # set object origin at world origin
    bpy.ops.object.origin_set(type="ORIGIN_CURSOR", center="MEDIAN")
    # setup the planet animation data
    # ...

Now, if you hover the 3D view and hit the spacebar, time will start flowing and the animations will play, making our planets revolve around the sun!

Step 7 (Bonus): Auto-cleaning the scene on script init

After we’ve run our script, we have created two types of resources in our scene: the 3D objects for the planets, the sun and the orbit rings; and the dynamic materials for the planets.

Getting rid of this is pretty simple: we just need to use the bpy.data submodule to go through our objects and our materials, check for the names and remove the ones that we created while running the script:

# ...

def delete_object(name):
    # try to find the object by name
    if name in bpy.data.objects:
        # if it exists, select it and delete it
        obj = bpy.data.objects[name]
        obj.select_set(True)
        bpy.ops.object.delete(use_global=False)

N_PLANETS = 6
START_FRAME = 1
END_FRAME = 200

# clean scene + planet materials
delete_object("Sun")
for n in range(N_PLANETS):
    delete_object("Planet-{:02d}".format(n))
    delete_object("Radius-{:02d}".format(n))
for m in bpy.data.materials:
    bpy.data.materials.remove(m)

# ...

Now, you can run the script as many times as you want: each time the scene will first be cleaned up so that you get only one instance of our “solar system”.

Step 8 (bonus): Auto-setting the scene properties, the render engine and the 3D view settings

For nicer visuals, with a little bloom effect, a dark background and no grid or X/Y-axis, you can even add the following snippet at the beginning of your script to setup the scene with nice settings:

# ...

def find_3dview_space():
    # find the 3D view panel and its screen space
    area = None
    for a in bpy.data.window_managers[0].windows[0].screen.areas:
        if a.type == "VIEW_3D":
            area = a
            break
    return area.spaces[0] if area else bpy.context.space_data

def setup_scene():
    # (set a black background)
    bpy.data.worlds["World"].node_tree.nodes["Background"].inputs[0].default_value = (0, 0, 0, 1)
    # (make sure we use the EEVEE render engine + enable bloom effect)
    scene = bpy.context.scene
    scene.render.engine = "BLENDER_EEVEE"
    scene.eevee.use_bloom = True
    # (set the animation start/end/current frames)
    scene.frame_start = START_FRAME
    scene.frame_end = END_FRAME
    scene.frame_current = START_FRAME
    # get the current 3D view (among all visible windows
    # in the workspace)
    space = find_3dview_space()
    # apply a "rendered" shading mode + hide all
    # additional markers, grids, cursors...
    space.shading.type = 'RENDERED'
    space.overlay.show_floor = False
    space.overlay.show_axis_x = False
    space.overlay.show_axis_y = False
    space.overlay.show_cursor = False
    space.overlay.show_object_origins = False

N_PLANETS = 6
START_FRAME = 1
END_FRAME = 200

# setup scene settings
setup_scene()

# ...

# deselect all objects
bpy.ops.object.select_all(action='DESELECT')

Conclusion

Blender is not the only 3D software that allows you to program your scenes and automate tasks; but it is living up to the expectations and, with each new version, Blender is gradually becoming a credible all-in-one solution for CG production, from storyboarding with Grease Pencil to node-based compositing.

The fact that you can use Python scripts with just a few additional packages to batch your object instantiation generate stuff procedurally, setup your render settings or even get custom stats on your current project is great! To me, it’s a way to lighten the load on tedious tasks but also to bring developers to the party to extend this creative tool community beyond artists.

In this post, we’ve seen that with less than a hundred lines of Python, we can create a basic procedural solar system with dynamic and randomized meshes, materials and animation!

I hope you’ve enjoyed this quick peek at Blender’s Python API – and of course, feel free to comment and tell me if you have ideas of other nice visualizations we could create thanks to this tool!

Written by

Mina Pêcheux, Full stack web and game developer

About the writer
Mina Pêcheux is a freelance full-stack web & game developer, writing about about topics like CGI, music, data science and more.
Mina's LinkedIn profile

Read more

Join Demando

We make sure you find the right match for you.

Get started