FBX: reformat props.
[blender-addons.git] / io_import_images_as_planes.py
blob0bb1916a38394aa052b012b41a7364f6ad615e20
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Import Images as Planes",
21 "author": "Florian Meyer (tstscr), mont29, matali",
22 "version": (1, 9, 1),
23 "blender": (2, 69, 0),
24 "location": "File > Import > Images as Planes or Add > Mesh > Images as Planes",
25 "description": "Imports images and creates planes with the appropriate aspect ratio. "
26 "The images are mapped to the planes.",
27 "warning": "",
28 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
29 "Scripts/Add_Mesh/Planes_from_Images",
30 "category": "Import-Export",
33 import bpy
34 from bpy.types import Operator
35 import mathutils
36 import os
37 import collections
39 from bpy.props import (StringProperty,
40 BoolProperty,
41 EnumProperty,
42 IntProperty,
43 FloatProperty,
44 CollectionProperty,
47 from bpy_extras.object_utils import AddObjectHelper, object_data_add
48 from bpy_extras.image_utils import load_image
50 # -----------------------------------------------------------------------------
51 # Global Vars
53 DEFAULT_EXT = "*"
55 EXT_FILTER = getattr(collections, "OrderedDict", dict)((
56 (DEFAULT_EXT, ((), "All image formats", "Import all known image (or movie) formats.")),
57 ("jpeg", (("jpeg", "jpg", "jpe"), "JPEG ({})", "Joint Photographic Experts Group")),
58 ("png", (("png", ), "PNG ({})", "Portable Network Graphics")),
59 ("tga", (("tga", "tpic"), "Truevision TGA ({})", "")),
60 ("tiff", (("tiff", "tif"), "TIFF ({})", "Tagged Image File Format")),
61 ("bmp", (("bmp", "dib"), "BMP ({})", "Windows Bitmap")),
62 ("cin", (("cin", ), "CIN ({})", "")),
63 ("dpx", (("dpx", ), "DPX ({})", "DPX (Digital Picture Exchange)")),
64 ("psd", (("psd", ), "PSD ({})", "Photoshop Document")),
65 ("exr", (("exr", ), "OpenEXR ({})", "OpenEXR HDR imaging image file format")),
66 ("hdr", (("hdr", "pic"), "Radiance HDR ({})", "")),
67 ("avi", (("avi", ), "AVI ({})", "Audio Video Interleave")),
68 ("mov", (("mov", "qt"), "QuickTime ({})", "")),
69 ("mp4", (("mp4", ), "MPEG-4 ({})", "MPEG-4 Part 14")),
70 ("ogg", (("ogg", "ogv"), "OGG Theora ({})", "")),
73 # XXX Hack to avoid allowing videos with Cycles, crashes currently!
74 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]}
76 CYCLES_SHADERS = (
77 ('BSDF_DIFFUSE', "Diffuse", "Diffuse Shader"),
78 ('EMISSION', "Emission", "Emission Shader"),
79 ('BSDF_DIFFUSE_BSDF_TRANSPARENT', "Diffuse & Transparent", "Diffuse and Transparent Mix"),
80 ('EMISSION_BSDF_TRANSPARENT', "Emission & Transparent", "Emission and Transparent Mix")
83 # -----------------------------------------------------------------------------
84 # Misc utils.
85 def gen_ext_filter_ui_items():
86 return tuple((k, name.format(", ".join("." + e for e in exts)) if "{}" in name else name, desc)
87 for k, (exts, name, desc) in EXT_FILTER.items())
90 def is_image_fn(fn, ext_key):
91 if ext_key == DEFAULT_EXT:
92 return True # Using Blender's image/movie filter.
93 ext = os.path.splitext(fn)[1].lstrip(".").lower()
94 return ext in EXT_FILTER[ext_key][0]
97 # -----------------------------------------------------------------------------
98 # Cycles utils.
99 def get_input_nodes(node, nodes, links):
100 # Get all links going to node.
101 input_links = {lnk for lnk in links if lnk.to_node == node}
102 # Sort those links, get their input nodes (and avoid doubles!).
103 sorted_nodes = []
104 done_nodes = set()
105 for socket in node.inputs:
106 done_links = set()
107 for link in input_links:
108 nd = link.from_node
109 if nd in done_nodes:
110 # Node already treated!
111 done_links.add(link)
112 elif link.to_socket == socket:
113 sorted_nodes.append(nd)
114 done_links.add(link)
115 done_nodes.add(nd)
116 input_links -= done_links
117 return sorted_nodes
120 def auto_align_nodes(node_tree):
121 print('\nAligning Nodes')
122 x_gap = 200
123 y_gap = 100
124 nodes = node_tree.nodes
125 links = node_tree.links
126 to_node = None
127 for node in nodes:
128 if node.type == 'OUTPUT_MATERIAL':
129 to_node = node
130 break
131 if not to_node:
132 return # Unlikely, but bette check anyway...
134 def align(to_node, nodes, links):
135 from_nodes = get_input_nodes(to_node, nodes, links)
136 for i, node in enumerate(from_nodes):
137 node.location.x = to_node.location.x - x_gap
138 node.location.y = to_node.location.y
139 node.location.y -= i * y_gap
140 node.location.y += (len(from_nodes)-1) * y_gap / (len(from_nodes))
141 align(node, nodes, links)
143 align(to_node, nodes, links)
146 def clean_node_tree(node_tree):
147 nodes = node_tree.nodes
148 for node in nodes:
149 if not node.type == 'OUTPUT_MATERIAL':
150 nodes.remove(node)
151 return node_tree.nodes[0]
154 # -----------------------------------------------------------------------------
155 # Operator
157 class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
158 """Create mesh plane(s) from image files with the appropiate aspect ratio"""
159 bl_idname = "import_image.to_plane"
160 bl_label = "Import Images as Planes"
161 bl_options = {'REGISTER', 'UNDO'}
163 # -----------
164 # File props.
165 files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
167 directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
169 # Show only images/videos, and directories!
170 filter_image = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
171 filter_movie = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
172 filter_folder = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
173 filter_glob = StringProperty(default="", options={'HIDDEN', 'SKIP_SAVE'})
175 # --------
176 # Options.
177 align = BoolProperty(name="Align Planes", default=True, description="Create Planes in a row")
179 align_offset = FloatProperty(name="Offset", min=0, soft_min=0, default=0.1, description="Space between Planes")
181 # Callback which will update File window's filter options accordingly to extension setting.
182 def update_extensions(self, context):
183 is_cycles = context.scene.render.engine == 'CYCLES'
184 if self.extension == DEFAULT_EXT:
185 self.filter_image = True
186 # XXX Hack to avoid allowing videos with Cycles, crashes currently!
187 self.filter_movie = True and not is_cycles
188 self.filter_glob = ""
189 else:
190 self.filter_image = False
191 self.filter_movie = False
192 if is_cycles:
193 # XXX Hack to avoid allowing videos with Cycles!
194 flt = ";".join(("*." + e for e in EXT_FILTER[self.extension][0] if e not in VID_EXT_FILTER))
195 else:
196 flt = ";".join(("*." + e for e in EXT_FILTER[self.extension][0]))
197 self.filter_glob = flt
198 # And now update space (file select window), if possible.
199 space = bpy.context.space_data
200 # XXX Can't use direct op comparison, these are not the same objects!
201 if (space.type != 'FILE_BROWSER' or space.operator.bl_rna.identifier != self.bl_rna.identifier):
202 return
203 space.params.use_filter_image = self.filter_image
204 space.params.use_filter_movie = self.filter_movie
205 space.params.filter_glob = self.filter_glob
206 # XXX Seems to be necessary, else not all changes above take effect...
207 bpy.ops.file.refresh()
208 extension = EnumProperty(name="Extension", items=gen_ext_filter_ui_items(),
209 description="Only import files of this type", update=update_extensions)
211 # -------------------
212 # Plane size options.
213 _size_modes = (
214 ('ABSOLUTE', "Absolute", "Use absolute size"),
215 ('DPI', "Dpi", "Use definition of the image as dots per inch"),
216 ('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
218 size_mode = EnumProperty(name="Size Mode", default='ABSOLUTE', items=_size_modes,
219 description="How the size of the plane is computed")
221 height = FloatProperty(name="Height", description="Height of the created plane",
222 default=1.0, min=0.001, soft_min=0.001, subtype='DISTANCE', unit='LENGTH')
224 factor = FloatProperty(name="Definition", min=1.0, default=600.0,
225 description="Number of pixels per inch or Blender Unit")
227 # -------------------------
228 # Blender material options.
229 t = bpy.types.Material.bl_rna.properties["use_shadeless"]
230 use_shadeless = BoolProperty(name=t.name, default=False, description=t.description)
232 use_transparency = BoolProperty(name="Use Alpha", default=False, description="Use alphachannel for transparency")
234 t = bpy.types.Material.bl_rna.properties["transparency_method"]
235 items = tuple((it.identifier, it.name, it.description) for it in t.enum_items)
236 transparency_method = EnumProperty(name="Transp. Method", description=t.description, items=items)
238 t = bpy.types.Material.bl_rna.properties["use_transparent_shadows"]
239 use_transparent_shadows = BoolProperty(name=t.name, default=False, description=t.description)
241 #-------------------------
242 # Cycles material options.
243 shader = EnumProperty(name="Shader", items=CYCLES_SHADERS, description="Node shader to use")
245 overwrite_node_tree = BoolProperty(name="Overwrite Material", default=True,
246 description="Overwrite existing Material with new nodetree "
247 "(based on material name)")
249 # --------------
250 # Image Options.
251 t = bpy.types.Image.bl_rna.properties["alpha_mode"]
252 alpha_mode_items = tuple((e.identifier, e.name, e.description) for e in t.enum_items)
253 alpha_mode = EnumProperty(name=t.name, items=alpha_mode_items, default=t.default, description=t.description)
255 t = bpy.types.IMAGE_OT_match_movie_length.bl_rna
256 match_len = BoolProperty(name=t.name, default=True, description=t.description)
258 t = bpy.types.Image.bl_rna.properties["use_fields"]
259 use_fields = BoolProperty(name=t.name, default=False, description=t.description)
261 t = bpy.types.ImageUser.bl_rna.properties["use_auto_refresh"]
262 use_auto_refresh = BoolProperty(name=t.name, default=True, description=t.description)
264 relative = BoolProperty(name="Relative", default=True, description="Apply relative paths")
266 def draw(self, context):
267 engine = context.scene.render.engine
268 layout = self.layout
270 box = layout.box()
271 box.label("Import Options:", icon='FILTER')
272 box.prop(self, "extension", icon='FILE_IMAGE')
273 box.prop(self, "align")
274 box.prop(self, "align_offset")
276 row = box.row()
277 row.active = bpy.data.is_saved
278 row.prop(self, "relative")
279 # XXX Hack to avoid allowing videos with Cycles, crashes currently!
280 if engine == 'BLENDER_RENDER':
281 box.prop(self, "match_len")
282 box.prop(self, "use_fields")
283 box.prop(self, "use_auto_refresh")
285 box = layout.box()
286 if engine == 'BLENDER_RENDER':
287 box.label("Material Settings: (Blender)", icon='MATERIAL')
288 box.prop(self, "use_shadeless")
289 box.prop(self, "use_transparency")
290 box.prop(self, "alpha_mode")
291 row = box.row()
292 row.prop(self, "transparency_method", expand=True)
293 box.prop(self, "use_transparent_shadows")
294 elif engine == 'CYCLES':
295 box = layout.box()
296 box.label("Material Settings: (Cycles)", icon='MATERIAL')
297 box.prop(self, 'shader', expand = True)
298 box.prop(self, 'overwrite_node_tree')
300 box = layout.box()
301 box.label("Plane dimensions:", icon='ARROW_LEFTRIGHT')
302 row = box.row()
303 row.prop(self, "size_mode", expand=True)
304 if self.size_mode == 'ABSOLUTE':
305 box.prop(self, "height")
306 else:
307 box.prop(self, "factor")
309 def invoke(self, context, event):
310 self.update_extensions(context)
311 context.window_manager.fileselect_add(self)
312 return {'RUNNING_MODAL'}
314 def execute(self, context):
315 if not bpy.data.is_saved:
316 self.relative = False
318 # the add utils don't work in this case because many objects are added disable relevant things beforehand
319 editmode = context.user_preferences.edit.use_enter_edit_mode
320 context.user_preferences.edit.use_enter_edit_mode = False
321 if context.active_object and context.active_object.mode == 'EDIT':
322 bpy.ops.object.mode_set(mode='OBJECT')
324 self.import_images(context)
326 context.user_preferences.edit.use_enter_edit_mode = editmode
327 return {'FINISHED'}
329 # Main...
330 def import_images(self, context):
331 engine = context.scene.render.engine
332 import_list, directory = self.generate_paths()
334 images = (load_image(path, directory) for path in import_list)
336 if engine in {'BLENDER_RENDER', 'BLENDER_GAME'}:
337 textures = []
338 for img in images:
339 self.set_image_options(img)
340 textures.append(self.create_image_textures(context, img))
342 materials = (self.create_material_for_texture(tex) for tex in textures)
343 elif engine == 'CYCLES':
344 materials = (self.create_cycles_material(img) for img in images)
345 else:
346 return
348 planes = tuple(self.create_image_plane(context, mat) for mat in materials)
350 context.scene.update()
351 if self.align:
352 self.align_planes(planes)
354 for plane in planes:
355 plane.select = True
357 self.report({'INFO'}, "Added {} Image Plane(s)".format(len(planes)))
359 def create_image_plane(self, context, material):
360 engine = context.scene.render.engine
361 if engine in {'BLENDER_RENDER', 'BLENDER_GAME'}:
362 img = material.texture_slots[0].texture.image
363 elif engine == 'CYCLES':
364 nodes = material.node_tree.nodes
365 img = next((node.image for node in nodes if node.type == 'TEX_IMAGE'))
366 px, py = img.size
368 # can't load data
369 if px == 0 or py == 0:
370 px = py = 1
372 if self.size_mode == 'ABSOLUTE':
373 y = self.height
374 x = px / py * y
375 elif self.size_mode == 'DPI':
376 fact = 1 / self.factor / context.scene.unit_settings.scale_length * 0.0254
377 x = px * fact
378 y = py * fact
379 else: # elif self.size_mode == 'DPBU'
380 fact = 1 / self.factor
381 x = px * fact
382 y = py * fact
384 bpy.ops.mesh.primitive_plane_add('INVOKE_REGION_WIN')
385 plane = context.scene.objects.active
386 # Why does mesh.primitive_plane_add leave the object in edit mode???
387 if plane.mode is not 'OBJECT':
388 bpy.ops.object.mode_set(mode='OBJECT')
389 plane.dimensions = x, y, 0.0
390 plane.name = material.name
391 bpy.ops.object.transform_apply(scale=True)
392 plane.data.uv_textures.new()
393 plane.data.materials.append(material)
394 plane.data.uv_textures[0].data[0].image = img
396 material.game_settings.use_backface_culling = False
397 material.game_settings.alpha_blend = 'ALPHA'
398 return plane
400 def align_planes(self, planes):
401 gap = self.align_offset
402 offset = 0
403 for i, plane in enumerate(planes):
404 offset += (plane.dimensions.x / 2.0) + gap
405 if i == 0:
406 continue
407 move_local = mathutils.Vector((offset, 0.0, 0.0))
408 move_world = plane.location + move_local * plane.matrix_world.inverted()
409 plane.location += move_world
410 offset += (plane.dimensions.x / 2.0)
412 def generate_paths(self):
413 return (fn.name for fn in self.files if is_image_fn(fn.name, self.extension)), self.directory
415 # Internal
416 def create_image_textures(self, context, image):
417 fn_full = os.path.normpath(bpy.path.abspath(image.filepath))
419 # look for texture with importsettings
420 for texture in bpy.data.textures:
421 if texture.type == 'IMAGE':
422 tex_img = texture.image
423 if (tex_img is not None) and (tex_img.library is None):
424 fn_tex_full = os.path.normpath(bpy.path.abspath(tex_img.filepath))
425 if fn_full == fn_tex_full:
426 self.set_texture_options(context, texture)
427 return texture
429 # if no texture is found: create one
430 name_compat = bpy.path.display_name_from_filepath(image.filepath)
431 texture = bpy.data.textures.new(name=name_compat, type='IMAGE')
432 texture.image = image
433 self.set_texture_options(context, texture)
434 return texture
436 def create_material_for_texture(self, texture):
437 # look for material with the needed texture
438 for material in bpy.data.materials:
439 slot = material.texture_slots[0]
440 if slot and slot.texture == texture:
441 self.set_material_options(material, slot)
442 return material
444 # if no material found: create one
445 name_compat = bpy.path.display_name_from_filepath(texture.image.filepath)
446 material = bpy.data.materials.new(name=name_compat)
447 slot = material.texture_slots.add()
448 slot.texture = texture
449 slot.texture_coords = 'UV'
450 self.set_material_options(material, slot)
451 return material
453 def set_image_options(self, image):
454 image.alpha_mode = self.alpha_mode
455 image.use_fields = self.use_fields
457 if self.relative:
458 try: # can't always find the relative path (between drive letters on windows)
459 image.filepath = bpy.path.relpath(image.filepath)
460 except ValueError:
461 pass
463 def set_texture_options(self, context, texture):
464 texture.image.use_alpha = self.use_transparency
465 texture.image_user.use_auto_refresh = self.use_auto_refresh
466 if self.match_len:
467 ctx = context.copy()
468 ctx["edit_image"] = texture.image
469 ctx["edit_image_user"] = texture.image_user
470 bpy.ops.image.match_movie_length(ctx)
472 def set_material_options(self, material, slot):
473 if self.use_transparency:
474 material.alpha = 0.0
475 material.specular_alpha = 0.0
476 slot.use_map_alpha = True
477 else:
478 material.alpha = 1.0
479 material.specular_alpha = 1.0
480 slot.use_map_alpha = False
481 material.use_transparency = self.use_transparency
482 material.transparency_method = self.transparency_method
483 material.use_shadeless = self.use_shadeless
484 material.use_transparent_shadows = self.use_transparent_shadows
486 #--------------------------------------------------------------------------
487 # Cycles
488 def create_cycles_material(self, image):
489 name_compat = bpy.path.display_name_from_filepath(image.filepath)
490 material = None
491 for mat in bpy.data.materials:
492 if mat.name == name_compat and self.overwrite_node_tree:
493 material = mat
494 if not material:
495 material = bpy.data.materials.new(name=name_compat)
497 material.use_nodes = True
498 node_tree = material.node_tree
499 out_node = clean_node_tree(node_tree)
501 if self.shader == 'BSDF_DIFFUSE':
502 bsdf_diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
503 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
504 tex_image.image = image
505 tex_image.show_texture = True
506 node_tree.links.new(out_node.inputs[0], bsdf_diffuse.outputs[0])
507 node_tree.links.new(bsdf_diffuse.inputs[0], tex_image.outputs[0])
509 elif self.shader == 'EMISSION':
510 emission = node_tree.nodes.new('ShaderNodeEmission')
511 lightpath = node_tree.nodes.new('ShaderNodeLightPath')
512 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
513 tex_image.image = image
514 tex_image.show_texture = True
515 node_tree.links.new(out_node.inputs[0], emission.outputs[0])
516 node_tree.links.new(emission.inputs[0], tex_image.outputs[0])
517 node_tree.links.new(emission.inputs[1], lightpath.outputs[0])
519 elif self.shader == 'BSDF_DIFFUSE_BSDF_TRANSPARENT':
520 bsdf_diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
521 bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
522 mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
523 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
524 tex_image.image = image
525 tex_image.show_texture = True
526 node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0])
527 node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
528 node_tree.links.new(mix_shader.inputs[2], bsdf_diffuse.outputs[0])
529 node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
530 node_tree.links.new(bsdf_diffuse.inputs[0], tex_image.outputs[0])
532 elif self.shader == 'EMISSION_BSDF_TRANSPARENT':
533 emission = node_tree.nodes.new('ShaderNodeEmission')
534 lightpath = node_tree.nodes.new('ShaderNodeLightPath')
535 bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
536 mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
537 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
538 tex_image.image = image
539 tex_image.show_texture = True
540 node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0])
541 node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
542 node_tree.links.new(mix_shader.inputs[2], emission.outputs[0])
543 node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
544 node_tree.links.new(emission.inputs[0], tex_image.outputs[0])
545 node_tree.links.new(emission.inputs[1], lightpath.outputs[0])
547 auto_align_nodes(node_tree)
548 return material
551 # -----------------------------------------------------------------------------
552 # Register
553 def import_images_button(self, context):
554 self.layout.operator(IMPORT_OT_image_to_plane.bl_idname,
555 text="Images as Planes", icon='TEXTURE')
558 def register():
559 bpy.utils.register_module(__name__)
560 bpy.types.INFO_MT_file_import.append(import_images_button)
561 bpy.types.INFO_MT_mesh_add.append(import_images_button)
564 def unregister():
565 bpy.utils.unregister_module(__name__)
566 bpy.types.INFO_MT_file_import.remove(import_images_button)
567 bpy.types.INFO_MT_mesh_add.remove(import_images_button)
570 if __name__ == "__main__":
571 register()