Featured post

new redirect for blender.org bpy docs.

http://www.blender.org/api/blender_python_api_current/ As of 10/11 november 2015 we can now link to the current api docs and not be worr...

June 03, 2012

Scripted import of materials from a .blend

Part 1 - The setup

This is the first step towards a material library loader for cycles node view. In the following scenario imagine you have a folder inside /scripts called addons_contrib/io_material_loader. io_material_loader will contain a .blend called practicality.blend. That blend will contain a material called 'vibrant'.
The import material function takes a known name of a material and a known name of a blend and looks in that blend for the material. It performs no error checking in this scenario, mainly because eventually the function will only be called with materials that I(or the script) know are present.

There are some drawbacks to this approach as it relies on ops, (insert clarification later). But for now it will suffice to act as example.

Part 2 - Reading the material content of a .blend

This will allow the script to populate a menu with the materials present in a blend file. It will rely on the methods used by ideasman_42 in this gist : here. My modification step which is viewable here takes a large chunk of that script and prints the names of blends located inside addons_contrib/io_material_loader. The next step below will make more sense now. It should print filenames and material names contained therein.
import bpy
import os
from os.path import join
def get_source_files():
path_to_files = bpy.utils.script_paths()[0]
DIR = join(path_to_files, 'addons_contrib', 'io_material_loader')
source_files = []
source_files.extend(source_list(DIR, filename_check=lambda f:f.endswith(".blend")))
return source_files
def source_list(path, filename_check=None):
for dirpath, dirnames, filenames in os.walk(path):
# skip '.svn'
if dirpath.startswith("."):
continue
for filename in filenames:
filepath = join(dirpath, filename)
if filename_check is None or filename_check(filepath):
yield filepath
def print_materials_and_blendnames(source_files):
print("-"*40)
for i, filepath in enumerate(source_files):
# print only filename
file_name = filepath.split(os.sep)[-1]
print(file_name, i+1, "of", len(source_files))
# print list of materials with users, if present.
with bpy.data.libraries.load(filepath) as (data_from, data_to):
if data_from.materials:
for material in data_from.materials:
print("\t-"+ material)
source_files = get_source_files()
print_materials_and_blendnames(source_files)


Part 3 - ditching part 1

Now we have the basic code soup required to get the job done. After having spent the best part of a day trying to pass a property along with layout.operator_menu_enum( ).some_property = some_value, which seems like an utterly trivial thing to do, it appears that the API doesn't provide this functionality. I died a little inside as ideasman_42 confirmed that it probably wasn't a feature of the API. If you would like to see an example of how I had hoped to implement the menu ( example script, displays new menu in nodeview ). Perhaps there is still a neat solution, but i've exhausted my desire to find a solution to the first idea.

Now the decision is where to add the menu then, in nodeview properties panel? Uch... if i must, albeit just to have a working prototype - so i can use it. This leaves me to settle on the uglier flat list, because doing an eval for creating menus is not an idea i like to think about. This is my current (lame) solution, so i can focus on combining parts 1 and 2. clicky

What follows is the combination of all parts:
import os
import bpy
from bpy.props import StringProperty
from os.path import join
from collections import OrderedDict
# helper constant strings
split_token = "<--->"
material_locator = "\\Material\\"
locator_prefix = "//"
path_to_scripts = bpy.utils.script_paths()[0]
path_to_script = join(path_to_scripts, 'addons_contrib', 'io_material_loader')
# helper functions
def import_material(file_name, matname):
"""
takes input file_name, and the material name located within, and imports into
current .blend
"""
file_name += '.blend'
opath = locator_prefix + file_name + material_locator + matname
dpath = join(path_to_script, file_name) + material_locator
bpy.ops.wm.link_append(
filepath=opath, # "//filename.blend\\Folder\\"
filename=matname, # "material_name
directory=dpath, # "fullpath + \\Folder\\
filemode=1,
link=False,
autoselect=False,
active_layer=True,
instance_groups=False,
relative_path=True)
def get_source_files():
"""
given the predefined place to look for materials, this populates source_files
with the list of .blends inside the directory pointed at.
"""
source_files = []
source_files.extend(source_list(path_to_script,
filename_check=lambda f:f.endswith(".blend")))
return source_files
def source_list(path, filename_check=None):
"""
generates the iterable used to find all .blend files.
"""
for dirpath, dirnames, filenames in os.walk(path):
# skip hidden dirs
if dirpath.startswith("."):
continue
for filename in filenames:
# skip non .blend
if not filename.endswith('.blend'):
continue
# we want these
filepath = join(dirpath, filename)
if filename_check is None or filename_check(filepath):
yield filepath
def get_materials_dict():
"""
Generates an ordered dictionary to map materials to the blend files that hold them.
"""
source_files = get_source_files()
mlist = OrderedDict()
for i, filepath in enumerate(source_files):
file_name = filepath.split(os.sep)[-1].replace('.blend', '')
with bpy.data.libraries.load(filepath) as (data_from, data_to):
if data_from.materials:
materials_in_file = [mat for mat in data_from.materials]
mlist[file_name] = materials_in_file
return mlist
class AddMaterial(bpy.types.Operator):
bl_idname = "scene.add_material_operator"
bl_label = "Add Material Operator"
selection = StringProperty()
def execute(self, context):
file_name, file_material = self.selection.split(split_token)
import_material(file_name, file_material)
print(file_name, file_material)
return {'FINISHED'}
class CustomMenu(bpy.types.Menu):
bl_label = "Materials"
bl_idname = "OBJECT_MT_custom_menu"
my_mat_dict = get_materials_dict()
def draw(self, context):
layout = self.layout
for key in self.my_mat_dict:
for material_choice in self.my_mat_dict[key]:
info = key + split_token + material_choice
layout.operator( 'scene.add_material_operator',
text=material_choice).selection = info
layout.label(text=key, icon='MATERIAL')
def draw_item(self, context):
layout = self.layout
layout.menu(CustomMenu.bl_idname)
def register():
bpy.utils.register_class(AddMaterial)
bpy.utils.register_class(CustomMenu)
bpy.types.NODE_HT_header.append(draw_item)
def unregister():
bpy.utils.unregister_class(AddMaterial)
bpy.utils.unregister_class(CustomMenu)
bpy.types.NODE_HT_header.remove(draw_item)
if __name__ == "__main__":
register()
view raw protoyper.py hosted with ❤ by GitHub


Part 4 - create the addon boilerplate

What if i want to be able to use this as a full addon, by enabling it permanently in the addons menu? Well, it's not very difficult - but it probably doesn't make much sense to you if you haven't done it a few times. So this is what must be done, this zip contains a folder addons_contrib which you should dump (along with its contents) directly into the /scripts folder. To get a deeper appreciation of the content, take your time to read through the .py files. Note: to use the library feature, you need to have a few .blend files located inside addons_contrib/io_material_loader, because that's where the script expects to find them. Then you will find a new Materials menu on the Node View header.