Skip to content

Instantly share code, notes, and snippets.

@knowuh
Last active May 28, 2023 10:49
Show Gist options
  • Star 33 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • 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
"""
cubify-image.py - 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 http://opensource.org/licenses/MIT
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 = bpy.data.materials
material = mats.get(material_name, mats.new(material_name))
# We put all our objects in a single COLLECTION
collection = bpy.data.collections.new("pixels")
bpy.context.scene.collection.children.link(collection)
# 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.
bpy.ops.mesh.primitive_cube_add()
default_cube = bpy.context.object
default_cube.data.materials.append(material)
mesh = default_cube.data
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 = bpy.data.objects.new(name='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
collection.objects.link(cube)
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 = bpy.data.images[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:
break
if len(pixels) < block_number:
break
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')
@Steveps3
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.

@SDraw
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.

@papataci
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
                color.append(myImage.pixels[index])              
            draw_pix(x * 2, y * 2, color)
    print ("y: %(y)04d / %(height)04d" % {"y": y, "height": height})

cubify('image.png')

@knowuh
Copy link
Author

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 bpy.data 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.

@knowuh
Copy link
Author

knowuh commented Mar 21, 2022

FWIW, this turned into a Bmesh add-on. There is a new Repo, PRs welcome:
https://github.com/knowuh/blender-image-voxel

Video demo: https://youtu.be/kH-hKRg6rPo

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