Add cubes->add UV textures->union->bake single texture image

Hi,

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)
                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[0] = 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[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:
        sce.objects.link(ob)
    
    union_model()


You can download the .blend file, PNG textures, the JSON data and the script from this link: https://db.tt/dhTMLXe0