# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### bl_info = { "name": "Import Images as Planes", "author": "Florian Meyer (tstscr)", "version": (1, 6), "blender": (2, 6, 3), "location": "File > Import > Images as Planes or Add > Mesh > Images as Planes", "description": "Imports images and creates planes with the appropriate " "aspect ratio. The images are mapped to the planes.", "warning": "", "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" "Scripts/Add_Mesh/Planes_from_Images", "tracker_url": "https://projects.blender.org/tracker/index.php?" "func=detail&aid=21751", "category": "Import-Export"} import bpy from bpy.types import Operator import mathutils import os import collections from bpy.props import (StringProperty, BoolProperty, EnumProperty, IntProperty, FloatProperty, CollectionProperty, ) from bpy_extras.object_utils import AddObjectHelper, object_data_add from bpy_extras.image_utils import load_image # ----------------------------------------------------------------------------- # Global Vars EXT_FILTER = getattr(collections, "OrderedDict", dict)(( ("*", ((), "All image formats", "Import all know image (or movie) formats.")), ("jpeg", (("jpeg", "jpg", "jpe"), "JPEG ({})", "Joint Photographic Experts Group")), ("png", (("png", ), "PNG ({})", "Portable Network Graphics")), ("tga", (("tga", "tpic"), "Truevision TGA ({})", "")), ("tiff", (("tiff", "tif"), "TIFF ({})", "Tagged Image File Format")), ("bmp", (("bmp", "dib"), "BMP ({})", "Windows Bitmap")), ("cin", (("cin", ), "CIN ({})", "")), ("dpx", (("dpx", ), "DPX ({})", "DPX (Digital Picture Exchange)")), ("psd", (("psd", ), "PSD ({})", "Photoshop Document")), ("exr", (("exr", ), "OpenEXR ({})", "OpenEXR HDR imaging image file format")), ("hdr", (("hdr", "pic"), "Radiance HDR ({})", "")), ("avi", (("avi", ), "AVI ({})", "Audio Video Interleave")), ("mov", (("mov", "qt"), "QuickTime ({})", "")), ("mp4", (("mp4", ), "MPEG-4 ({})", "MPEG-4 Part 14")), ("ogg", (("ogg", "ogv"), "OGG Theora ({})", "")), )) # XXX Hack to avoid allowing videos with Cycles, crashes currently! VID_EXT_FILTER = {e for ext_k, ext_v in EXT_FILTER.items() if ext_k in {"avi", "mov", "mp4", "ogg"} for e in ext_v[0]} CYCLES_SHADERS = ( ('BSDF_DIFFUSE', "Diffuse", "Diffuse Shader"), ('EMISSION', "Emission", "Emission Shader"), ('BSDF_DIFFUSE_BSDF_TRANSPARENT', "Diffuse & Transparent", "Diffuse and Transparent Mix"), ('EMISSION_BSDF_TRANSPARENT', "Emission & Transparent", "Emission and Transparent Mix") ) # ----------------------------------------------------------------------------- # Misc utils. def gen_ext_filter_ui_items(): return ((k, name.format(", ".join("." + e for e in exts)) if "{}" in name else name, desc) for k, (exts, name, desc) in EXT_FILTER.items()) def is_image_fn(fn, ext_key): if ext_key == "*": return True # Using Blender's image/movie filter. ext = os.path.splitext(fn)[1].lstrip(".").lower() return ext in EXT_FILTER[ext_key][0] # ----------------------------------------------------------------------------- # Cycles utils. def get_input_nodes(node, nodes, links): # Get all links going to node. input_links = {lnk for lnk in links if lnk.to_node == node} # Sort those links, get their input nodes (and avoid doubles!). sorted_nodes = [] done_nodes = set() for socket in node.inputs: done_links = set() for link in input_links: nd = link.from_node if nd in done_nodes: # Node already treated! done_links.add(link) elif link.to_socket == socket: sorted_nodes.append(nd) done_links.add(link) done_nodes.add(nd) input_links -= done_links return sorted_nodes def auto_align_nodes(node_tree): print('\nAligning Nodes') x_gap = 200 y_gap = 100 nodes = node_tree.nodes links = node_tree.links to_node = None for node in nodes: if node.type == 'OUTPUT_MATERIAL': to_node = node break if not to_node: return # Unlikely, but bette check anyway... def align(to_node, nodes, links): from_nodes = get_input_nodes(to_node, nodes, links) for i, node in enumerate(from_nodes): node.location.x = to_node.location.x - x_gap node.location.y = to_node.location.y node.location.y -= i * y_gap node.location.y += (len(from_nodes)-1) * y_gap / (len(from_nodes)) align(node, nodes, links) align(to_node, nodes, links) def clean_node_tree(node_tree): nodes = node_tree.nodes for node in nodes: if not node.type == 'OUTPUT_MATERIAL': nodes.remove(node) return node_tree.nodes[0] # ----------------------------------------------------------------------------- # Operator class IMPORT_OT_image_to_plane(Operator, AddObjectHelper): """Create mesh plane(s) from image files """ \ """with the appropiate aspect ratio""" bl_idname = "import_image.to_plane" bl_label = "Import Images as Planes" bl_options = {'REGISTER', 'UNDO'} # ----------- # File props. files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'}) directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'}) # Show only images/videos, and directories! filter_image = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) filter_movie = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) filter_folder = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'}) filter_glob = StringProperty(default="", options={'HIDDEN', 'SKIP_SAVE'}) # -------- # Options. align = BoolProperty(name="Align Planes", default=True, description="Create Planes in a row") align_offset = FloatProperty(name="Offset", min=0, soft_min=0, default=0.1, description="Space between Planes") # Callback which will update File window's filter options accordingly # to extension setting. def update_extensions(self, context): is_cycles = context.scene.render.engine == 'CYCLES' if self.extension == "*": self.filter_image = True # XXX Hack to avoid allowing videos with Cycles, crashes currently! self.filter_movie = True and not is_cycles self.filter_glob = "" else: self.filter_image = False self.filter_movie = False if is_cycles: # XXX Hack to avoid allowing videos with Cycles! flt = ";".join(("*." + e for e in EXT_FILTER[self.extension][0] if e not in VID_EXT_FILTER)) else: flt = ";".join(("*." + e for e in EXT_FILTER[self.extension][0])) self.filter_glob = flt # And now update space (file select window), if possible. space = bpy.context.space_data # XXX Can't use direct op comparison, these are not the same objects! if (space.type != 'FILE_BROWSER' or space.operator.bl_rna.identifier != self.bl_rna.identifier): return space.params.use_filter_image = self.filter_image space.params.use_filter_movie = self.filter_movie space.params.filter_glob = self.filter_glob # XXX Seems to be necessary, else not all changes above take effect... bpy.ops.file.refresh() extension = EnumProperty(name="Extension", items=gen_ext_filter_ui_items(), description="Only import files of this type", update=update_extensions) use_dimension = BoolProperty(name="Use Image Dimensions", default=False, description="Use the images pixels to derive " "planes size") factor = IntProperty(name="Pixels/BU", min=1, default=500, description="Number of pixels per Blenderunit") # ------------------------- # Blender material options. t = bpy.types.Material.bl_rna.properties["use_shadeless"] use_shadeless = BoolProperty(name=t.name, default=False, description=t.description) use_transparency = BoolProperty(name="Use Alpha", default=False, description="Use alphachannel for " "transparency") t = bpy.types.Material.bl_rna.properties["transparency_method"] items = ((it.identifier, it.name, it.description) for it in t.enum_items) transparency_method = EnumProperty(name="Transp. Method", description=t.description, items=items) t = bpy.types.Material.bl_rna.properties["use_transparent_shadows"] use_transparent_shadows = BoolProperty(name=t.name, default=False, description=t.description) #------------------------- # Cycles material options. shader = EnumProperty(name="Shader", items=CYCLES_SHADERS, description="Node shader to use") overwrite_node_tree = BoolProperty(name="Overwrite Material", default=True, description="Overwrite existing " "Material with new nodetree (based " "on material name)") # -------------- # Image Options. t = bpy.types.Image.bl_rna.properties["use_premultiply"] use_premultiply = BoolProperty(name=t.name, default=False, description=t.description) t = bpy.types.IMAGE_OT_match_movie_length.bl_rna match_len = BoolProperty(name=t.name, default=True, description=t.description) t = bpy.types.Image.bl_rna.properties["use_fields"] use_fields = BoolProperty(name=t.name, default=False, description=t.description) t = bpy.types.ImageUser.bl_rna.properties["use_auto_refresh"] use_auto_refresh = BoolProperty(name=t.name, default=True, description=t.description) relative = BoolProperty(name="Relative", default=True, description="Apply relative paths") def draw(self, context): engine = context.scene.render.engine layout = self.layout box = layout.box() box.label("Import Options:", icon='FILTER') box.prop(self, "extension", icon='FILE_IMAGE') box.prop(self, "align") box.prop(self, "align_offset") row = box.row() row.active = bpy.data.is_saved row.prop(self, "relative") # XXX Hack to avoid allowing videos with Cycles, crashes currently! if engine == 'BLENDER_RENDER': box.prop(self, "match_len") box.prop(self, "use_fields") box.prop(self, "use_auto_refresh") box = layout.box() if engine == 'BLENDER_RENDER': box.label("Material Settings: (Blender)", icon='MATERIAL') box.prop(self, "use_shadeless") box.prop(self, "use_transparency") box.prop(self, "use_premultiply") box.prop(self, "transparency_method", expand=True) box.prop(self, "use_transparent_shadows") elif engine == 'CYCLES': box = layout.box() box.label("Material Settings: (Cycles)", icon='MATERIAL') box.prop(self, 'shader', expand = True) box.prop(self, 'overwrite_node_tree') box = layout.box() box.label("Plane dimensions:", icon='ARROW_LEFTRIGHT') box.prop(self, "use_dimension") box.prop(self, "factor", expand=True) def invoke(self, context, event): self.update_extensions(context) context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} def execute(self, context): if not bpy.data.is_saved: self.relative = False # the add utils don't work in this case # because many objects are added # disable relevant things beforehand editmode = context.user_preferences.edit.use_enter_edit_mode context.user_preferences.edit.use_enter_edit_mode = False if (context.active_object and context.active_object.mode == 'EDIT'): bpy.ops.object.mode_set(mode='OBJECT') self.import_images(context) context.user_preferences.edit.use_enter_edit_mode = editmode return {'FINISHED'} # Main... def import_images(self, context): engine = context.scene.render.engine import_list, directory = self.generate_paths() images = (load_image(path, directory) for path in import_list) if engine == 'BLENDER_RENDER': textures = [] for img in images: self.set_image_options(img) textures.append(self.create_image_textures(img)) materials = (self.create_material_for_texture(tex) for tex in textures) elif engine == 'CYCLES': materials = (self.create_cycles_material(img) for img in images) planes = tuple(self.create_image_plane(context, mat) for mat in materials) context.scene.update() if self.align: self.align_planes(planes) for plane in planes: plane.select = True self.report({'INFO'}, "Added {} Image Plane(s)".format(len(planes))) def create_image_plane(self, context, material): engine = context.scene.render.engine if engine == 'BLENDER_RENDER': img = material.texture_slots[0].texture.image elif engine == 'CYCLES': nodes = material.node_tree.nodes img = next((node.image for node in nodes if node.type == 'TEX_IMAGE')) px, py = img.size # can't load data if px == 0 or py == 0: px = py = 1 x = px / py y = 1.0 if self.use_dimension: x = (px * (1.0 / self.factor)) * 0.5 y = (py * (1.0 / self.factor)) * 0.5 verts = ((-x, -y, 0.0), (+x, -y, 0.0), (+x, +y, 0.0), (-x, +y, 0.0), ) faces = ((0, 1, 2, 3), ) mesh_data = bpy.data.meshes.new(img.name) mesh_data.from_pydata(verts, [], faces) mesh_data.update() object_data_add(context, mesh_data, operator=self) plane = context.scene.objects.active plane.data.uv_textures.new() plane.data.materials.append(material) plane.data.uv_textures[0].data[0].image = img material.game_settings.use_backface_culling = False material.game_settings.alpha_blend = 'ALPHA' return plane def align_planes(self, planes): gap = self.align_offset offset = 0 for i, plane in enumerate(planes): offset += (plane.dimensions.x / 2.0) + gap if i == 0: continue move_local = mathutils.Vector((offset, 0.0, 0.0)) move_world = plane.location + move_local * plane.matrix_world.inverted() plane.location += move_world offset += (plane.dimensions.x / 2.0) def generate_paths(self): return (fn.name for fn in self.files if is_image_fn(fn.name, self.extension)), self.directory # Internal def create_image_textures(self, image): fn_full = os.path.normpath(bpy.path.abspath(image.filepath)) # look for texture with importsettings for texture in bpy.data.textures: if texture.type == 'IMAGE': tex_img = texture.image if (tex_img is not None) and (tex_img.library is None): fn_tex_full = os.path.normpath(bpy.path.abspath(tex_img.filepath)) if fn_full == fn_tex_full: self.set_texture_options(texture) return texture # if no texture is found: create one name_compat = bpy.path.display_name_from_filepath(image.filepath) texture = bpy.data.textures.new(name=name_compat, type='IMAGE') texture.image = image self.set_texture_options(texture) return texture def create_material_for_texture(self, texture): # look for material with the needed texture for material in bpy.data.materials: slot = material.texture_slots[0] if slot and slot.texture == texture: self.set_material_options(material, slot) return material # if no material found: create one name_compat = bpy.path.display_name_from_filepath(texture.image.filepath) material = bpy.data.materials.new(name=name_compat) slot = material.texture_slots.add() slot.texture = texture slot.texture_coords = 'UV' self.set_material_options(material, slot) return material def set_image_options(self, image): image.use_premultiply = self.use_premultiply image.use_fields = self.use_fields if self.relative: image.filepath = bpy.path.relpath(image.filepath) def set_texture_options(self, texture): texture.use_alpha = self.use_transparency texture.image_user.use_auto_refresh = self.use_auto_refresh if self.match_len: ctx = {"edit_image": texture.image, "edit_image_user": texture.image_user,} bpy.ops.image.match_movie_length(ctx) def set_material_options(self, material, slot): if self.use_transparency: material.alpha = 0.0 material.specular_alpha = 0.0 slot.use_map_alpha = True else: material.alpha = 1.0 material.specular_alpha = 1.0 slot.use_map_alpha = False material.use_transparency = self.use_transparency material.transparency_method = self.transparency_method material.use_shadeless = self.use_shadeless material.use_transparent_shadows = self.use_transparent_shadows #-------------------------------------------------------------------------- # Cycles def create_cycles_material(self, image): name_compat = bpy.path.display_name_from_filepath(image.filepath) material = None for mat in bpy.data.materials: if mat.name == name_compat and self.overwrite_node_tree: material = mat if not material: material = bpy.data.materials.new(name=name_compat) material.use_nodes = True node_tree = material.node_tree out_node = clean_node_tree(node_tree) if self.shader == 'BSDF_DIFFUSE': bsdf_diffuse = node_tree.nodes.new('BSDF_DIFFUSE') tex_image = node_tree.nodes.new('TEX_IMAGE') tex_image.image = image tex_image.show_texture = True node_tree.links.new(out_node.inputs[0], bsdf_diffuse.outputs[0]) node_tree.links.new(bsdf_diffuse.inputs[0], tex_image.outputs[0]) elif self.shader == 'EMISSION': emission = node_tree.nodes.new('EMISSION') lightpath = node_tree.nodes.new('LIGHT_PATH') tex_image = node_tree.nodes.new('TEX_IMAGE') tex_image.image = image tex_image.show_texture = True node_tree.links.new(out_node.inputs[0], emission.outputs[0]) node_tree.links.new(emission.inputs[0], tex_image.outputs[0]) node_tree.links.new(emission.inputs[1], lightpath.outputs[0]) elif self.shader == 'BSDF_DIFFUSE_BSDF_TRANSPARENT': bsdf_diffuse = node_tree.nodes.new('BSDF_DIFFUSE') bsdf_transparent = node_tree.nodes.new('BSDF_TRANSPARENT') mix_shader = node_tree.nodes.new('MIX_SHADER') tex_image = node_tree.nodes.new('TEX_IMAGE') tex_image.image = image tex_image.show_texture = True node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0]) node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1]) node_tree.links.new(mix_shader.inputs[2], bsdf_diffuse.outputs[0]) node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0]) node_tree.links.new(bsdf_diffuse.inputs[0], tex_image.outputs[0]) elif self.shader == 'EMISSION_BSDF_TRANSPARENT': emission = node_tree.nodes.new('EMISSION') lightpath = node_tree.nodes.new('LIGHT_PATH') bsdf_transparent = node_tree.nodes.new('BSDF_TRANSPARENT') mix_shader = node_tree.nodes.new('MIX_SHADER') tex_image = node_tree.nodes.new('TEX_IMAGE') tex_image.image = image tex_image.show_texture = True node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0]) node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1]) node_tree.links.new(mix_shader.inputs[2], emission.outputs[0]) node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0]) node_tree.links.new(emission.inputs[0], tex_image.outputs[0]) node_tree.links.new(emission.inputs[1], lightpath.outputs[0]) auto_align_nodes(node_tree) return material # ----------------------------------------------------------------------------- # Register def import_images_button(self, context): self.layout.operator(IMPORT_OT_image_to_plane.bl_idname, text="Images as Planes", icon='TEXTURE') def register(): bpy.utils.register_module(__name__) bpy.types.INFO_MT_file_import.append(import_images_button) bpy.types.INFO_MT_mesh_add.append(import_images_button) def unregister(): bpy.utils.unregister_module(__name__) bpy.types.INFO_MT_file_import.remove(import_images_button) bpy.types.INFO_MT_mesh_add.remove(import_images_button) if __name__ == "__main__": register()