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 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) scene.objects.unlink(ob) 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] # Create shadeless material mat = bpy.data.materials.new(material_name) mat.use_shadeless = True # 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.image = bpy.data.images.load(realpath) tex.use_alpha = True # Create texture mtex = mat.texture_slots.add() mtex.texture = tex mtex.texture_coords = 'UV' mtex.use_map_color_diffuse = True except: print("Texture %d not found" % index) 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.) material.use_shadeless = True # return new return material with open(os.path.join(path_to_json_and_textures, "data.json")) as data_file: model_data = json.load(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 = True add_cube = bpy.ops.mesh.primitive_cube_add prevCube = False count = 0 # Make initial object origin = Vector((0,0,0)) bpy.ops.mesh.primitive_cube_add(location=origin) ob = bpy.context.object bpy.ops.mesh.uv_texture_add() 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.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: sce.objects.link(ob) union_model()
You can download the .blend file, script and textures from here: