A simple task. I have hundreds of objects with the same material, but they must have an individual image texture on each. What is a correct way to get it done? I know the "ObjectInfo" node's "ObjectId" property. But how can I use it for choosing an image texture? There are properties "StartFrame", "Offset" in the "ImageTexture (image sequence)" node. But they have no inputs. And I cannot put drivers there. Maybe an object's driver can change the value by changing material's '''nodes["Image Texture.001"].image_user.frame_start''', but it seems to be very unreliable hack. Another idea is to duplicate materials for each object, but for now it seems as a nightmare. What do you think? Also if there is a way to get the same in Blender render, I'd be glad to know it.
-
1$\begingroup$ are the texture images supposed to be randomized or do you need to control which image goes to which object? if you need to control you could make a group with your current material (except the Image Texture node), and use the same group for each material, the only thing that will change is the Image Texture plugged into the node group? If you don't need to control there must be a way to randomize $\endgroup$– moonbootsMay 16, 2020 at 12:48
-
$\begingroup$ I need to control which one is used. Moreover, they don't change during the animation. Your answer assumes having a dedicated material per object anyway. It seems that is the simplest way. So it seems it's time to dig how to create those materials and images duplicates from a script. $\endgroup$– MechanicMay 16, 2020 at 13:41
1 Answer
So, I ended up with a script which clones all the materials, both cycles and blender internal (all the materials in the scene render ok in both engines). To set image id I used custom properties. The actual image id calculation seems complicated, but there are tiles in the scene which are parented to some hosts, and they all are duplicated within their respective parents, so it's much easier to handle only parents' properties instead of each child. When resetting materials back it just clears all the extra references, so blender will kill those orphans itself on save / revert.
Here is the script, in case somebody will face the same problem.
#----- scene-tools.py
'''Some tools to automate this very scene tasks.
@author (c) LV A.S. Mechanic. Kharkov city
'''
import bpy
import glob
# base names of objects of interest
TILE_NAMES = ('tileCA','tileCB','tileCC')
IMAGE_PATH = r'//textures\img' # blender notation
IMAGE_MASK = 'img_*.jpg'
PROP_ID_PARENT = 'host_id' # custom property name for image Id calculation
PROP_ID_OBJECT = 'image_id'
# (Cycles)
TILE_MAT_NAME = 'tile-face' # tile material (cycles)
TILE_MAT_SLOT = 0 # material slot id to work with
NODE_IMG_NAME = 'ImgTex' # image texture node name in the material
IMG_NAME = 'img_' # image base name
# (Blender Internal)
TILE_MAT_NAME_BI = ('tile-face-bi') # tile material (BI)
NODE_MAT_BI = 'MatBi' # BI material input node name
TILE_TEX_SLOT_BI = 0 # texture slot id within Bi matertial to work with
def get_my_tiles():
''' this returns a list of tuples for all the tiles in the scene.
(0 - object; 1 - parent id; 2 - object id; 3 - real image id)
'''
_res = []
scn = bpy.context.scene
_maxs = {} # here we store max obj_id within parent_id
_pids = [] # parents' ids
for obj in scn.objects:
if (obj.type == 'MESH'):
for _base in TILE_NAMES:
if (obj.name.startswith(_base)):
# create descriptor and get everything (no real id yet)
try:
_pid = int(obj.parent[PROP_ID_PARENT])
except:
_pid = 0
try:
_oid = int(obj[PROP_ID_OBJECT])
except:
_oid = 0
# update maximums
_maxv = _maxs.get(_pid)
_maxs[_pid] = (_oid if _maxv == None else max(_maxv, _oid))
# update pids
if not (_pid in _pids): _pids.append(_pid)
# store values
_d = tuple((obj,_pid,_oid))
_res.append(_d)
#print('found object: {}'.format(obj.name))
break
# now we calculate the real image id for each object
# get sorted _pids
_pids.sort()
# replace _maxs values with start offset
_offset = 0
for _pid in _pids:
_maxv = _maxs[_pid]
_maxs[_pid] = _offset
_offset += _maxv + 1
# and calc real id with id offsets
_res1 = []
for _d in _res:
_res1.append(_d+(_d[2]+_maxs[_d[1]],))
'''
print("--------------- final tiles")
_n = 0
for _d in _res1:
print(_n," : (", _d[0], _d[1], _d[2], _d[3], ")")
_n += 1
'''
return _res1
def error_stop(msg):
''' show message and exit with error '''
raise Exception(msg)
def get_filenames(path, mask):
''' returns sorted list of files matching the mask in given path '''
_path = bpy.path.abspath(path)
#print('path: ', _path)
_files = glob.glob(_path+"\\"+mask)
if len(_files)>0:
_files.sort()
return _files
def name_with_id(name, id):
''' composes indexed name with a base name and an id (name.00N)'''
return name + "." + "{:03d}".format(id)
def reset_tile_materials():
""" Resets materials to defaults for all the tiles in the scene """
# first find the defaults
mat_default = bpy.data.materials[TILE_MAT_NAME]
if (mat_default is None):
error_stop('Material {} not found!'.format(TILE_MAT_NAME))
mat_default_bi = bpy.data.materials[TILE_MAT_NAME_BI]
if (mat_default_bi is None):
error_stop('Material (Bi) {} not found!'.format(TILE_MAT_NAME_BI))
tex_default = mat_default_bi.texture_slots[TILE_TEX_SLOT_BI].texture
img_default = tex_default.image
# get all the tiles
my_tiles = get_my_tiles()
if (len(my_tiles) == 0):
print('Tiles not found. There is nothing to do.')
return
# set default material for all
print('Setting defaults for {} found objects'.format(len(my_tiles)))
for _d in my_tiles:
obj = _d[0]
# reset all possible references
mat = obj.material_slots[TILE_MAT_SLOT].material
mat_bi = mat.node_tree.nodes[NODE_MAT_BI].material
tex = mat_bi.texture_slots[TILE_TEX_SLOT_BI].texture
tex.image = img_default
mat_bi.texture_slots[TILE_TEX_SLOT_BI].texture = tex_default
mat.node_tree.nodes[NODE_MAT_BI].material = mat_default_bi
mat.node_tree.nodes[NODE_IMG_NAME].image = img_default
obj.material_slots[TILE_MAT_SLOT].material = mat_default
print('All tile materials'' defaults set.')
def find_or_create_image(name, img_ref, filepath):
''' it finds the image by name. (if not found it's created by copying img_ref),
then given path will be set as the image filepath '''
img = bpy.data.images.get(name)
if img == None:
img = img_ref.copy()
img.name = name
img.filepath = filepath
return img
def find_or_create_material(base_name, base_name_bi, id, mat_ref, mat_ref_bi):
''' it finds the material by name. (if not found ot's created by copying mat_ref),
then the image will be set as the image material's image '''
# image is already ok, hust find it
img = bpy.data.images[name_with_id(IMG_NAME, id)]
# first handle Bi material
tex_ref = mat_ref_bi.texture_slots[TILE_TEX_SLOT_BI].texture
# find or create a texture
tex = bpy.data.textures.get(name_with_id(base_name_bi, id))
if tex == None:
tex = tex_ref.copy()
tex.name = name_with_id(base_name_bi, id)
# put needed image to the new texture
tex.image = img
# now find bi material
mat_bi = bpy.data.materials.get(name_with_id(base_name_bi, id))
if mat_bi == None:
mat_bi = mat_ref_bi.copy()
mat_bi.name = name_with_id(base_name_bi, id)
# set new texture
mat_bi.texture_slots[TILE_TEX_SLOT_BI].texture = tex
# now cycles
mat = bpy.data.materials.get(name_with_id(base_name, id))
if mat == None:
mat = mat_ref.copy()
mat.name = name_with_id(base_name, id)
node = mat.node_tree.nodes[NODE_IMG_NAME]
node.image = bpy.data.images[name_with_id(IMG_NAME, id)]
# and set bi material to Bi node
node_bi = mat.node_tree.nodes[NODE_MAT_BI]
node_bi.material = mat_bi
return mat
def create_tile_materials():
''' this creates a bunch of materials for the tiles, and sets images from the img directory '''
# first get all the image file names available
print("Getting available images' names")
img_files = get_filenames(IMAGE_PATH, IMAGE_MASK)
if (len(img_files) == 0):
error_stop('No imahes found in '+IMAGE_PATH)
print("Found {} images to process".format(len(img_files)))
# find all the tiles
my_tiles = get_my_tiles()
if (len(my_tiles) == 0):
print('Tiles not found. There is nothing to do.')
return
# count images needed (with max image id)
_max_id = 0
for _d in my_tiles: _max_id = max(_max_id, _d[3])
print("It's needed {} images for {} objects".format(_max_id + 1, len(my_tiles)))
if (len(img_files) < (_max_id + 1)):
error_stop("There is not enough images ({} of {})!".format(len(img_files), _max_id + 1))
# create images copying from the orignial one
ids = []
for _d in my_tiles:
_id = _d[3]
if not _id in ids:
ids.append(_id)
ids.sort()
# find the original image to copy
mat_ref = bpy.data.materials[TILE_MAT_NAME]
img_ref = mat_ref.node_tree.nodes[NODE_IMG_NAME].image
# Bi
mat_ref_bi = bpy.data.materials[TILE_MAT_NAME_BI]
# actual creation
for id in ids:
find_or_create_image(name_with_id(IMG_NAME, id), img_ref, img_files[id])
# and materials creation
for id in ids:
#base_name, base_name_bi, id, mat_ref, mat_ref_bi):
find_or_create_material(TILE_MAT_NAME, TILE_MAT_NAME_BI, id, mat_ref, mat_ref_bi)
# assigning materials to the objects
for _d in my_tiles:
obj = _d[0]
id = _d[3]
# cycles
mat = bpy.data.materials[name_with_id(TILE_MAT_NAME, id)]
obj.material_slots[TILE_MAT_SLOT].material = mat
print("Materials created and assigned to the objects.")
class CreateTileMaterialsOperator(bpy.types.Operator):
"""Creates new materials with different images for all the tiles in the scene"""
bl_idname = "scene.my_create_tile_materials"
bl_label = "Create tile materials"
def execute(self, context):
create_tile_materials()
return {'FINISHED'}
class ResetTileMaterialsOperator(bpy.types.Operator):
""" Resets materials to defaults for all the tiles in the scene """
bl_idname = "scene.my_reset_tile_materials"
bl_label = "Reset tile materials"
def execute(self, context):
reset_tile_materials()
return {'FINISHED'}
class MySceneToolsPanel(bpy.types.Panel):
"""Creates a Panel in the Scene properties window"""
bl_label = "Scene tools"
bl_idname = "SCENE_PT_MySceneTools"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "scene"
def draw(self, context):
layout = self.layout
row = layout.row()
row.operator("scene.my_create_tile_materials")
row = layout.row()
row.operator("scene.my_reset_tile_materials")
def register():
bpy.utils.register_class(ResetTileMaterialsOperator)
bpy.utils.register_class(CreateTileMaterialsOperator)
bpy.utils.register_class(MySceneToolsPanel)
def unregister():
bpy.utils.unregister_class(ResetTileMaterialsOperator)
bpy.utils.unregister_class(CreateTileMaterialsOperator)
bpy.utils.unregister_class(MySceneToolsPanel)
register()
#-----/scene-tools.py