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, script and textures from here: https://db.tt/dhTMLXe0