Last active
May 28, 2023 10:49
-
-
Save knowuh/48136d7a17387e7cf6c3 to your computer and use it in GitHub Desktop.
Blender script to turn an image data block into 3D cubes...
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
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.
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')
Thanks everyone for the tips.
I have a more performant version now.
- Instead of using
bpy.ops
cubes are created in data mode by usingbpy.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.
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
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.