Skip to content

Instantly share code, notes, and snippets.

Last active May 28, 2023 10:49
Show Gist options
  • Save knowuh/48136d7a17387e7cf6c3 to your computer and use it in GitHub Desktop.
Save knowuh/48136d7a17387e7cf6c3 to your computer and use it in GitHub Desktop.
Blender script to turn an image data block into 3D cubes...
import bpy
import colorsys
""" - Turns each pixel of an image into a scaled cube.
Noah Paessel | @knowuh - updated on 2022-02-13 (test w Blender 3.1b)
MIT license
WARNING: This script will generate a thousands objects (one per image pixel)
I recommend only using it with image with less than 40,000 pixels (200x200).
# We use MATERIAL named 'obj_color'. If it doesn't exist, create it.
# NB: This material is used for all cubes. This material will use an
# attribute node to read the 'custom_color' attribute from the cube.
material_name = 'obj_color'
mats =
material = mats.get(material_name,
# We put all our objects in a single COLLECTION
collection ="pixels")
# We make prototype cube for our make_cube function. We do this so we can
# reuse the mesh, and avoid creating a new one each time.
default_cube = bpy.context.object
mesh =
def make_cube(location, color):
Create a single cube at the given location with the given color.
location: the location of the cube
color: the color of the cube
x, y, unused_z = location
# break out the components of the color
r, g, b = color[0:3]
h, s, v = colorsys.rgb_to_hsv(r, g, b)
# The height of our cube, based on a component of HSV or RGB
size = 16 * s
location = [x, y, size]
scale = [0.9, 0.9, size]
cube ='pixel', object_data=mesh)
cube.scale = scale
cube.location = location
# Assign a 'custom_color' attribute value to this object.
# Used by the material for the color of the cube.
cube['custom_color'] = color
def cubify(image_name):
Invokes the make_cube() function for each pixel in the image.
The image must already exist in a data block named 'image_name'.
myImage =[image_name]
color_chans = myImage.channels
width, height = myImage.size
pixels = myImage.pixels
for y in range(0, width):
for x in range(0, height):
block_number = (y * width) + x
color = pixels[block_number *
color_chans:block_number * color_chans + color_chans]
if len(color) < color_chans:
if len(pixels) < block_number:
make_cube([x * 2, y * 2, 0], color)
# Track our progress update on for each row in terminal output:
print("y: %(y)04d / %(height)04d" % {"y": y, "height": height})
# To test the make_cube() function:
# make_cube([0,0,0], [1.0,0.2,0.3, 1.0])
# To voxelize an image:
# cubify('test.png')
Copy link

Would the script run faster if it checked the material existed before creating a new material for each pixel. The script appears to get slower and slower which suggests that it is eating more and more memory. Less materials would improve this.

Copy link

SDraw commented Mar 31, 2015

Nice trick can be used to gain performance:

  • Separate Y for equal parts: (0,32),(32,64) and etc.
  • Use script to create one part
  • Save .blend file
  • Save this .blend file as copy
  • Remove all objects
  • Create next small part.
  • Repeat
  • And then just append all created .blend files into one.

Copy link

Nice script, I've added a conditional to generate the voxel only if the Alpha is not 0, i think is useful:

        #gets the value of the Alpha
        alpha = myImage.pixels[(block_number * 4) + 3]

        if alpha != 0:
            for color_index in range(0, 4):
                index = (block_number * 4) + color_index
            draw_pix(x * 2, y * 2, color)
    print ("y: %(y)04d / %(height)04d" % {"y": y, "height": height})


Copy link

knowuh commented Feb 13, 2022

Thanks everyone for the tips.

I have a more performant version now.

  • Instead of using bpy.ops cubes are created in data mode by using calls. This is faster for a few reasons including that there is no undo/redo history.
  • We reuse a single cube mesh, instead of creating a new mesh with each object.
  • Instead of making a new material for each cube (ouch!) we just reuse one material. NB: You need to use an attribute input node in your shader.
  • All the pixel cubes are put into one collection.

There are two more approaches I will explore and document here.

  • Instead of using multiple objects, just generate a single mesh. This should be faster still and support much larger images. Also, we can apply modifiers then.
  • Instead of using Python, do it with Geometry nodes. This will be interesting to compare processes.

Copy link

knowuh commented Mar 21, 2022

FWIW, this turned into a Bmesh add-on. There is a new Repo, PRs welcome:

Video demo:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment