My problem is mainly with UV texturing from Python.

I am building a complex model from Python. The model consists of several cubes which have different colors. On top of each cube there's supposed to be an image texture. The goal is to add the cubes from Python, color and texturize them, then union everything with a boolean modifier to get rid of overlapping parts and then to bake the separate textures into a single texture file. In the end I'll upload the 3D model to Shapeways to 3D print it.

In the basic example provided I have 5 cubes. The textures for the cubes are in 5 separate PNG files. The output from my script looks like this:

The output is fine, textures are in the right place and the overlaps are gone. I can now export this to X3D and upload to Shapeways, but on more complex models I'll have 500 separate unoptimized PNG files. This doesn't play well with Shapeways, so I'd like to bake the separate textures into a single texture of 2048x2048. But when I try to do this manually, I get the following result:

This is supposed to be the texture for the whole model, so clearly something isn't right.

The question: How to texturize the top of cubes, so it will survive the Union boolean modifier and Texture bake operation from Python?

My code (I'm 100% sure my approach to applying the Union boolean modifier is wrong, but other approaches I tried didn't maintain the materials):

import json
import bpy
import mathutils
import os
from mathutils import Vector

bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()

# Configurable variables
path_to_json_and_textures = "/path/to/question/folder"
max_model_dimension = 20 # Blender units are in meters
cube_height_relative_to_width = 0.1
overlap_fraction = 0.25 # overlap of cubes stacked on top each other
scale_factor = 0.01

def isJipObject(ob):
return ob.name.startswith('jip-') and ob.type == 'MESH'

def union_model():
scene = bpy.context.scene
web_space = scene.web_space
obs = []
# Only union objects made by this script
for ob in scene.objects:
if isJipObject(ob):
obs.append(ob)

nr_of_cubes = len(obs)
if not nr_of_cubes:
return

# Add some noise, give each object has unique dimensions
# Boolean modifier doesn't work with perfectly aligned cubes
boolean_hack = 0.0
boolean_hack_step_size = float(0.001/nr_of_cubes) # spread out 0.1mm of noise over all cubes

# Slightly change the scale of all of the cubes, before union
for ob in obs:
ob.scale.x += boolean_hack
ob.scale.y += boolean_hack
ob.scale.z += boolean_hack
boolean_hack += boolean_hack_step_size

# Don't touch current context...
ctx = bpy.context.copy()
# one of the objects to join
ctx['active_object'] = obs[0]
ctx['selected_objects'] = obs
# we need the scene bases as well for joining
ctx['selected_editable_bases'] = [scene.object_bases[ob.name] for ob in obs]
# Join the objects - this will assign all materials correctly to all objects
bpy.ops.object.join(ctx)
# Break them apart again - not sure what happens,
# but joining and then breaking apart allows to do a union while perserving correct UV mapping...
# Perhaps it has to do with the origin of the cubes? Joining then separating assigns all cubes
# the same point of origin, as well as the same location, scale and z dimension
# The only variance is in the x and y dimension
bpy.ops.mesh.separate(type='LOOSE')

prevCube = False

scene = bpy.context.scene
# Need to reselect the objects, because of 'separate by loose parts' the objects in obs no longer exist
for ob in scene.objects:
# whatever objects you want to join...
if isJipObject(ob):
if(not prevCube):
prevCube = ob
else:
modifier = prevCube.modifiers.new('Modifier', 'BOOLEAN')
modifier.object = ob
modifier.operation = 'UNION'
bpy.context.scene.objects.active = prevCube
bpy.ops.object.modifier_apply(apply_as='DATA', modifier=modifier.name)

bpy.context.scene.objects.active = prevCube

def makeImageMaterial(index):
material_name = "jip-material"+str(index)
if material_name in bpy.data.materials:
return bpy.data.materials[material_name]

mat = bpy.data.materials.new(material_name)

# Try, if not exists don't make texture
try:
realpath = os.path.join(path_to_json_and_textures, 'cropped', str(index)+'.png')
tex = bpy.data.textures.new(material_name, type = 'IMAGE')
tex.use_alpha = True
# Create texture
mtex.texture = tex
mtex.texture_coords = 'UV'
mtex.use_map_color_diffuse = True
except:

return mat

def makeMaterial(red,green,blue):
# return existing
name = "jip-material-%d-%d-%d" % (red,green,blue)
if name in bpy.data.materials:
return bpy.data.materials[name]

colorName = "jip-material"

material = bpy.data.materials.new(name)
material.diffuse_color = (red/100., green/100., blue/100.)
# return new
return material

with open(os.path.join(path_to_json_and_textures, "data.json")) as data_file:

# Non-configurable variables
model_width = (model_data['right'] - model_data['left']) * scale_factor
model_height = (model_data['bottom'] - model_data['top']) * scale_factor

initial_cube_height = model_width*cube_height_relative_to_width
half_current_cube_height = initial_cube_height/2 # divide by 2 because the scale operation increases height towards top and bottom
non_overlapping_fraction = 1 - overlap_fraction
current_z = -half_current_cube_height + overlap_fraction*initial_cube_height

mylayers = [False] * 20
mylayers[0] = True
prevCube = False
count = 0

# Make initial object
origin = Vector((0,0,0))

ob = bpy.context.object
obs = []
sce = bpy.context.scene

# Loop over each level and make all the cubes, with color and texture
for i, level in enumerate(model_data['levels']):
current_z += half_current_cube_height*2 * non_overlapping_fraction # compensate for overlap in z coordinate
for element in level:

copy = ob.copy()
node_width = element['w']
node_height = element['h']
copy.location = Vector(((element['y']*2 + node_height)*scale_factor,
(element['x']*2 + node_width)*scale_factor,
current_z))
copy.scale = (node_height*scale_factor, node_width*scale_factor, half_current_cube_height)
copy.name = "jip-" + str(element['nodeIndex']).zfill(6) # leading zeros for correct sorting in GUI
copy.data = copy.data.copy() # also duplicate mesh, remove for linked duplicate

material = makeMaterial(element['r'], element['g'], element['b'])
copy.data.materials.append(material)

image_material = makeImageMaterial(element['nodeIndex'])
copy.data.materials.append(image_material)

# Set image material for top face only
copy.data.polygons[5].material_index = 1

obs.append(copy)

# remove initial object
bpy.ops.object.delete()

# Now add them to the scene, adding large number of cubes is quicker this way
for ob in obs:

union_model()


You can download the .blend file, script and textures from here: https://db.tt/dhTMLXe0

• rapid reading... but can that be because you need to apply the modified scales before the union ? – lemon Aug 15 '16 at 15:46
• Unfortunately the result is the same. Steps taken: Apply scale for each cube. Manually assign all the materials to the bottom cube. Manually apply boolean modifier (union) 4 times to unite with the other cubes. Then in UV Editing window I bake to a newly created image. Same thing happens when I also apply the location. – Jip Aug 15 '16 at 17:15
• The UVs from the sides and bottom overlap, is that expected ? – lemon Aug 15 '16 at 17:32
• What do you mean by that @lemon? How can I check this? The bottom and side should have the same material and color. But overlapping UVs doesn't sound right... Also, do you mean before the union or the end result? – Jip Aug 15 '16 at 18:26
• you are baking all the faces, including the sides. And the sides (UVs) overlap the top picture. So probably the result is dependent on the order (materials) the bake is working – lemon Aug 15 '16 at 18:27

The PNG files were actually cropped form an original large PNG containing all the image data already. So I didn't have to bake to a new file.

Join all the cubes (ctrl+j). With this script I was able to automatically unwrap all the cubes as if doing "UVs -> Unwrap -> Project From View (bounds)" in an orthographic top view.

import bpy, bmesh
from mathutils import Vector

# https://blender.stackexchange.com/questions/32283/what-are-all-values-in-bound-box
def bounds(obj, local=False):

local_coords = obj.bound_box[:]
om = obj.matrix_world

if not local:
worldify = lambda p: om * Vector(p[:])
coords = [worldify(p).to_tuple() for p in local_coords]
else:
coords = [p[:] for p in local_coords]

rotated = zip(*coords[::-1])

push_axis = []
for (axis, _list) in zip('xyz', rotated):
info = lambda: None
info.max = max(_list)
info.min = min(_list)
info.distance = info.max - info.min
push_axis.append(info)

import collections

originals = dict(zip(['x', 'y', 'z'], push_axis))

o_details = collections.namedtuple('object_details', 'x y z')
return o_details(**originals)

obj = bpy.context.scene.objects.active

if not obj is None:

# Get the lowest and highest x and y values from the bounding box corners
object_details = bounds(obj)

lowest_x_cor = object_details.x.min
highest_x_cor = object_details.x.max

lowest_y_cor = object_details.y.min
highest_y_cor = object_details.y.max

obj.data.uv_textures.new("map1")
# Get a BMesh representation
bm = bmesh.new()   # create an empty BMesh
bm.from_mesh(obj.data)   # fill it in from a Mesh

bm.faces.ensure_lookup_table()

uv_layer = bm.loops.layers.uv[0]

nFaces = len(bm.faces)
for fi in range(nFaces):
face = bm.faces[fi]

for loop in face.loops:
# Scale the vertex coordinates into 0 to 1 range
NewXValue = (((loop.vert.co[0] - lowest_x_cor) * 1) / (highest_x_cor - lowest_x_cor))
NewYValue = (((loop.vert.co[1] - lowest_y_cor) * 1) / (highest_y_cor - lowest_y_cor))
loop[uv_layer].uv = (NewXValue, NewYValue)

# Finish up, write the bmesh back to the mesh
bm.to_mesh(obj.data)
bm.free()


This als UV maps the side and bottom faces. It stretches the pixels on the border of top of the cubes down the sides, which I like. To only texture the top I should not loop over all the faces, but instead only the faces with a normal of (0, 0, 1 ).

For this I could use these functions:

# https://blender.stackexchange.com/questions/75517/selecting-faces-in-python

def NormalInDirection( normal, direction, limit = 0.5 ):
return direction.dot( normal ) > limit

def GoingUp( normal, limit = 0.5 ):
return NormalInDirection( normal, Vector( (0, 0, 1 ) ), limit )

def GoingDown( normal, limit = 0.5 ):
return NormalInDirection( normal, Vector( (0, 0, -1 ) ), limit )


To get rid of the internal geometry I found Netfabb (for Windows only) is the way to go if the goal is to prepare for 3D-printing, instead of the Boolean modifier in Blender. After UV unwrapping I export a 3DS file from Blender, then import into Netfabb. Let it do auto repair upon import. Export to 3DS again and import into Blender. The UV mapping is still correct but the internal geometry is removed.