Blender Tutorial: Python

@Brian Kim (2023)

Overview

This tutorial will demonstrate how to use Python to automate the creation, placement, and coloring of objects in Blender. This is extremely useful for developers who are trying to create Blender objects using large and complex volumes of data, as this takes advantage of Python's built-in functions and libraries.

 For this tutorial, we will be creating the heat map below:

Preliminaries

Before starting the project, please download the image at this link and the CSV file at this link. Ideally, they should go in the same, separate directory as your Blender project.

Step 1

Create a new, empty Blender file, and delete the camera, cube, and light. Also, create a new Python file in the same directory as the CSV file. If you've never worked with Python before, this tutorial should not require any intense knowledge, but it will still be useful to read through the Python documentation. In this case, I'll just call my file 'blender.py'.

Step 2

In Blender, go to Edit > Preferences and open the Add-ons tab:

Find the add-on "Images as Planes" and install it.

Now, we can import the map as a plane into the Blender project:

Make sure to turn on "Viewport Shading" so you can see what's on the plane.

Step 3

In Blender, open the "Scripting" window. This is where Python code can be open and run:

Click the "Open" button at the top of the screen, and find your empty Python file:

Finally, make sure to save your Blender file in the same directory as your data.

Step 4

At this point, I will go through the relevant code that needs to be added.

First, include the following import statements:

import os

import math

from inspect import getsourcefile

from os.path import abspath

import csv

import bpy

import mathutils

from string import ascii_uppercase as alc

Add the following function. This is not a particularly complex function, but it is necessary to avoid overlap. Without going into too much detail, there are repeated data points in the CSV file, and this will let us avoid creating unnecessary columns

def same_cylinder(a, b):

   if len(a) != len(b):

       return False


   similar = True

   for i in range(len(a)):

       if not math.isclose(a[i], b[i]):

           similar = similar and False


   return similar

The next function is very important, as it will control the creation of cylinders based on data from the CSV file. Feel free to take some time to read through the comments to understand what is going on in the code:

def create_cylinder(location, radius, height, name, color=(1, 0, 0, 1), letter=None):

   bpy.ops.mesh.primitive_cylinder_add(

       location=mathutils.Vector((location[0] - 0.5, location[1] - 0.5, height / 2.0)),

       radius=radius,

       depth=height,

   )

   bpy.data.objects["Cylinder"].name = name

   obj = bpy.context.object


   # Create a material

   mat = bpy.data.materials.new("Color Material")

   # Activate its nodes

   mat.use_nodes = True

   # Get the principled BSDF (created by default)

   principled = mat.node_tree.nodes["Principled BSDF"]

   # Assign the color

   principled.inputs["Base Color"].default_value = color

   # Assign the material to the object

   obj.data.materials.append(mat)


   if letter:

       font_curve = bpy.data.curves.new(type="FONT", name="numberPlate")

       font_curve.body = letter

       obj = bpy.data.objects.new(name="Font Object", object_data=font_curve)


       # Set scale and location

       obj.location = mathutils.Vector(

           (location[0] - 0.5, location[1] - 0.5, (height / 2.0) + 0.1)

       )

       obj.scale = (0.05, 0.05, 0.05)

       obj.rotation_euler = mathutils.Euler((1.0, 0, 0), "XYZ")

       obj.color = (0, 0, 1, 1)

       bpy.context.scene.collection.objects.link(obj)


       # Create a material

       mat = bpy.data.materials.new("Color Material")

       # Activate its nodes

       mat.use_nodes = True

       # Get the principled BSDF (created by default)

       principled = mat.node_tree.nodes["Principled BSDF"]

       # Assign the color

       principled.inputs["Base Color"].default_value = color

       # Assign the material to the object

       obj.data.materials.append(mat)


Now, we move from creating functions to reading the CSV files. The first line of the code will set our directory to the same location as the Blender file. Be aware that the way that Blender runs Python means that you will need to fetch the directory location manually like, otherwise Blender will assume the directory is the same as the Blender executable.

dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))


test_pumps = []

with open(dir_path + "/pump_map.csv") as fd:

   rd = csv.reader(fd, delimiter=",")

   for row in rd:

       if len(row) > 0 and not any(

           [

               same_cylinder(test_pump, [float(row[0]), float(row[1])])

               for test_pump in test_pumps

           ]

       ):

           test_pumps.append([float(row[0]), float(row[1])])


The final line of code will bring this all together by going through the list of locations we have found and applying the cylinder creation function to each point:

for i, point in enumerate(test_pumps):

   create_cylinder(point, 0.01, 0.2, f"pump_{i}", (0, 0, 1, 1), letter=str(i))

Step 5

Now we can finally run the program. If we look at the layout before, we see:

Back in Scripting, we hit the run button:

And we now have the following results:

Congratulations! You've successfully run a Python code file in Blender. While this is a pretty small data sample, using Python can be a huge time-saver when you're working on large data samples. As an example, and a possible next step, try to tackle this much larger dataset next.