fix [#36995] FBX Importer does not import fbx model
[blender-addons.git] / io_import_images_as_planes.py
blob269de1bfd5de383016e1779c47793489c7d77dc1
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),
23 "blender": (2, 66, 4),
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/Scripts/Add_Mesh/Planes_from_Images",
29 "tracker_url": "https://projects.blender.org/tracker/index.php?func=detail&aid=21751",
30 "category": "Import-Export"}
32 import bpy
33 from bpy.types import Operator
34 import mathutils
35 import os
36 import collections
38 from bpy.props import (StringProperty,
39 BoolProperty,
40 EnumProperty,
41 IntProperty,
42 FloatProperty,
43 CollectionProperty,
46 from bpy_extras.object_utils import AddObjectHelper, object_data_add
47 from bpy_extras.image_utils import load_image
49 # -----------------------------------------------------------------------------
50 # Global Vars
52 DEFAULT_EXT = "*"
54 EXT_FILTER = getattr(collections, "OrderedDict", dict)((
55 (DEFAULT_EXT, ((), "All image formats", "Import all know image (or movie) formats.")),
56 ("jpeg", (("jpeg", "jpg", "jpe"), "JPEG ({})", "Joint Photographic Experts Group")),
57 ("png", (("png", ), "PNG ({})", "Portable Network Graphics")),
58 ("tga", (("tga", "tpic"), "Truevision TGA ({})", "")),
59 ("tiff", (("tiff", "tif"), "TIFF ({})", "Tagged Image File Format")),
60 ("bmp", (("bmp", "dib"), "BMP ({})", "Windows Bitmap")),
61 ("cin", (("cin", ), "CIN ({})", "")),
62 ("dpx", (("dpx", ), "DPX ({})", "DPX (Digital Picture Exchange)")),
63 ("psd", (("psd", ), "PSD ({})", "Photoshop Document")),
64 ("exr", (("exr", ), "OpenEXR ({})", "OpenEXR HDR imaging image file format")),
65 ("hdr", (("hdr", "pic"), "Radiance HDR ({})", "")),
66 ("avi", (("avi", ), "AVI ({})", "Audio Video Interleave")),
67 ("mov", (("mov", "qt"), "QuickTime ({})", "")),
68 ("mp4", (("mp4", ), "MPEG-4 ({})", "MPEG-4 Part 14")),
69 ("ogg", (("ogg", "ogv"), "OGG Theora ({})", "")),
72 # XXX Hack to avoid allowing videos with Cycles, crashes currently!
73 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]}
75 CYCLES_SHADERS = (
76 ('BSDF_DIFFUSE', "Diffuse", "Diffuse Shader"),
77 ('EMISSION', "Emission", "Emission Shader"),
78 ('BSDF_DIFFUSE_BSDF_TRANSPARENT', "Diffuse & Transparent", "Diffuse and Transparent Mix"),
79 ('EMISSION_BSDF_TRANSPARENT', "Emission & Transparent", "Emission and Transparent Mix")
82 # -----------------------------------------------------------------------------
83 # Misc utils.
84 def gen_ext_filter_ui_items():
85 return tuple((k, name.format(", ".join("." + e for e in exts)) if "{}" in name else name, desc)
86 for k, (exts, name, desc) in EXT_FILTER.items())
89 def is_image_fn(fn, ext_key):
90 if ext_key == DEFAULT_EXT:
91 return True # Using Blender's image/movie filter.
92 ext = os.path.splitext(fn)[1].lstrip(".").lower()
93 return ext in EXT_FILTER[ext_key][0]
96 # -----------------------------------------------------------------------------
97 # Cycles utils.
98 def get_input_nodes(node, nodes, links):
99 # Get all links going to node.
100 input_links = {lnk for lnk in links if lnk.to_node == node}
101 # Sort those links, get their input nodes (and avoid doubles!).
102 sorted_nodes = []
103 done_nodes = set()
104 for socket in node.inputs:
105 done_links = set()
106 for link in input_links:
107 nd = link.from_node
108 if nd in done_nodes:
109 # Node already treated!
110 done_links.add(link)
111 elif link.to_socket == socket:
112 sorted_nodes.append(nd)
113 done_links.add(link)
114 done_nodes.add(nd)
115 input_links -= done_links
116 return sorted_nodes
119 def auto_align_nodes(node_tree):
120 print('\nAligning Nodes')
121 x_gap = 200
122 y_gap = 100
123 nodes = node_tree.nodes
124 links = node_tree.links
125 to_node = None
126 for node in nodes:
127 if node.type == 'OUTPUT_MATERIAL':
128 to_node = node
129 break
130 if not to_node:
131 return # Unlikely, but bette check anyway...
133 def align(to_node, nodes, links):
134 from_nodes = get_input_nodes(to_node, nodes, links)
135 for i, node in enumerate(from_nodes):
136 node.location.x = to_node.location.x - x_gap
137 node.location.y = to_node.location.y
138 node.location.y -= i * y_gap
139 node.location.y += (len(from_nodes)-1) * y_gap / (len(from_nodes))
140 align(node, nodes, links)
142 align(to_node, nodes, links)
145 def clean_node_tree(node_tree):
146 nodes = node_tree.nodes
147 for node in nodes:
148 if not node.type == 'OUTPUT_MATERIAL':
149 nodes.remove(node)
150 return node_tree.nodes[0]
153 # -----------------------------------------------------------------------------
154 # Operator
156 class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
157 """Create mesh plane(s) from image files with the appropiate aspect ratio"""
158 bl_idname = "import_image.to_plane"
159 bl_label = "Import Images as Planes"
160 bl_options = {'REGISTER', 'UNDO'}
162 # -----------
163 # File props.
164 files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
166 directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
168 # Show only images/videos, and directories!
169 filter_image = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
170 filter_movie = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
171 filter_folder = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
172 filter_glob = StringProperty(default="", options={'HIDDEN', 'SKIP_SAVE'})
174 # --------
175 # Options.
176 align = BoolProperty(name="Align Planes", default=True, description="Create Planes in a row")
178 align_offset = FloatProperty(name="Offset", min=0, soft_min=0, default=0.1, description="Space between Planes")
180 # Callback which will update File window's filter options accordingly to extension setting.
181 def update_extensions(self, context):
182 is_cycles = context.scene.render.engine == 'CYCLES'
183 if self.extension == DEFAULT_EXT:
184 self.filter_image = True
185 # XXX Hack to avoid allowing videos with Cycles, crashes currently!
186 self.filter_movie = True and not is_cycles
187 self.filter_glob = ""
188 else:
189 self.filter_image = False
190 self.filter_movie = False
191 if is_cycles:
192 # XXX Hack to avoid allowing videos with Cycles!
193 flt = ";".join(("*." + e for e in EXT_FILTER[self.extension][0] if e not in VID_EXT_FILTER))
194 else:
195 flt = ";".join(("*." + e for e in EXT_FILTER[self.extension][0]))
196 self.filter_glob = flt
197 # And now update space (file select window), if possible.
198 space = bpy.context.space_data
199 # XXX Can't use direct op comparison, these are not the same objects!
200 if (space.type != 'FILE_BROWSER' or space.operator.bl_rna.identifier != self.bl_rna.identifier):
201 return
202 space.params.use_filter_image = self.filter_image
203 space.params.use_filter_movie = self.filter_movie
204 space.params.filter_glob = self.filter_glob
205 # XXX Seems to be necessary, else not all changes above take effect...
206 bpy.ops.file.refresh()
207 extension = EnumProperty(name="Extension", items=gen_ext_filter_ui_items(),
208 description="Only import files of this type", update=update_extensions)
210 # -------------------
211 # Plane size options.
212 _size_modes = (
213 ('ABSOLUTE', "Absolute", "Use absolute size"),
214 ('DPI', "Dpi", "Use definition of the image as dots per inch"),
215 ('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
217 size_mode = EnumProperty(name="Size Mode", default='ABSOLUTE', items=_size_modes,
218 description="How the size of the plane is computed")
220 height = FloatProperty(name="Height", description="Height of the created plane",
221 default=1.0, min=0.001, soft_min=0.001, subtype='DISTANCE', unit='LENGTH')
223 factor = FloatProperty(name="Definition", min=1.0, default=600.0,
224 description="Number of pixels per inch or Blender Unit")
226 # -------------------------
227 # Blender material options.
228 t = bpy.types.Material.bl_rna.properties["use_shadeless"]
229 use_shadeless = BoolProperty(name=t.name, default=False, description=t.description)
231 use_transparency = BoolProperty(name="Use Alpha", default=False, description="Use alphachannel for transparency")
233 t = bpy.types.Material.bl_rna.properties["transparency_method"]
234 items = tuple((it.identifier, it.name, it.description) for it in t.enum_items)
235 transparency_method = EnumProperty(name="Transp. Method", description=t.description, items=items)
237 t = bpy.types.Material.bl_rna.properties["use_transparent_shadows"]
238 use_transparent_shadows = BoolProperty(name=t.name, default=False, description=t.description)
240 #-------------------------
241 # Cycles material options.
242 shader = EnumProperty(name="Shader", items=CYCLES_SHADERS, description="Node shader to use")
244 overwrite_node_tree = BoolProperty(name="Overwrite Material", default=True,
245 description="Overwrite existing Material with new nodetree "
246 "(based on material name)")
248 # --------------
249 # Image Options.
250 t = bpy.types.Image.bl_rna.properties["alpha_mode"]
251 alpha_mode_items = tuple((e.identifier, e.name, e.description) for e in t.enum_items)
252 alpha_mode = EnumProperty(name=t.name, items=alpha_mode_items, default=t.default, description=t.description)
254 t = bpy.types.IMAGE_OT_match_movie_length.bl_rna
255 match_len = BoolProperty(name=t.name, default=True, description=t.description)
257 t = bpy.types.Image.bl_rna.properties["use_fields"]
258 use_fields = BoolProperty(name=t.name, default=False, description=t.description)
260 t = bpy.types.ImageUser.bl_rna.properties["use_auto_refresh"]
261 use_auto_refresh = BoolProperty(name=t.name, default=True, description=t.description)
263 relative = BoolProperty(name="Relative", default=True, description="Apply relative paths")
265 def draw(self, context):
266 engine = context.scene.render.engine
267 layout = self.layout
269 box = layout.box()
270 box.label("Import Options:", icon='FILTER')
271 box.prop(self, "extension", icon='FILE_IMAGE')
272 box.prop(self, "align")
273 box.prop(self, "align_offset")
275 row = box.row()
276 row.active = bpy.data.is_saved
277 row.prop(self, "relative")
278 # XXX Hack to avoid allowing videos with Cycles, crashes currently!
279 if engine == 'BLENDER_RENDER':
280 box.prop(self, "match_len")
281 box.prop(self, "use_fields")
282 box.prop(self, "use_auto_refresh")
284 box = layout.box()
285 if engine == 'BLENDER_RENDER':
286 box.label("Material Settings: (Blender)", icon='MATERIAL')
287 box.prop(self, "use_shadeless")
288 box.prop(self, "use_transparency")
289 box.prop(self, "alpha_mode")
290 row = box.row()
291 row.prop(self, "transparency_method", expand=True)
292 box.prop(self, "use_transparent_shadows")
293 elif engine == 'CYCLES':
294 box = layout.box()
295 box.label("Material Settings: (Cycles)", icon='MATERIAL')
296 box.prop(self, 'shader', expand = True)
297 box.prop(self, 'overwrite_node_tree')
299 box = layout.box()
300 box.label("Plane dimensions:", icon='ARROW_LEFTRIGHT')
301 row = box.row()
302 row.prop(self, "size_mode", expand=True)
303 if self.size_mode == 'ABSOLUTE':
304 box.prop(self, "height")
305 else:
306 box.prop(self, "factor")
308 def invoke(self, context, event):
309 self.update_extensions(context)
310 context.window_manager.fileselect_add(self)
311 return {'RUNNING_MODAL'}
313 def execute(self, context):
314 if not bpy.data.is_saved:
315 self.relative = False
317 # the add utils don't work in this case because many objects are added disable relevant things beforehand
318 editmode = context.user_preferences.edit.use_enter_edit_mode
319 context.user_preferences.edit.use_enter_edit_mode = False
320 if context.active_object and context.active_object.mode == 'EDIT':
321 bpy.ops.object.mode_set(mode='OBJECT')
323 self.import_images(context)
325 context.user_preferences.edit.use_enter_edit_mode = editmode
326 return {'FINISHED'}
328 # Main...
329 def import_images(self, context):
330 engine = context.scene.render.engine
331 import_list, directory = self.generate_paths()
333 images = (load_image(path, directory) for path in import_list)
335 if engine in {'BLENDER_RENDER', 'BLENDER_GAME'}:
336 textures = []
337 for img in images:
338 self.set_image_options(img)
339 textures.append(self.create_image_textures(context, img))
341 materials = (self.create_material_for_texture(tex) for tex in textures)
342 elif engine == 'CYCLES':
343 materials = (self.create_cycles_material(img) for img in images)
344 else:
345 return
347 planes = tuple(self.create_image_plane(context, mat) for mat in materials)
349 context.scene.update()
350 if self.align:
351 self.align_planes(planes)
353 for plane in planes:
354 plane.select = True
356 self.report({'INFO'}, "Added {} Image Plane(s)".format(len(planes)))
358 def create_image_plane(self, context, material):
359 engine = context.scene.render.engine
360 if engine in {'BLENDER_RENDER', 'BLENDER_GAME'}:
361 img = material.texture_slots[0].texture.image
362 elif engine == 'CYCLES':
363 nodes = material.node_tree.nodes
364 img = next((node.image for node in nodes if node.type == 'TEX_IMAGE'))
365 px, py = img.size
367 # can't load data
368 if px == 0 or py == 0:
369 px = py = 1
371 if self.size_mode == 'ABSOLUTE':
372 y = self.height
373 x = px / py * y
374 elif self.size_mode == 'DPI':
375 fact = 1 / self.factor / context.scene.unit_settings.scale_length * 0.0254
376 x = px * fact
377 y = py * fact
378 else: # elif self.size_mode == 'DPBU'
379 fact = 1 / self.factor
380 x = px * fact
381 y = py * fact
383 bpy.ops.mesh.primitive_plane_add('INVOKE_REGION_WIN')
384 plane = context.scene.objects.active
385 # Why does mesh.primitive_plane_add leave the object in edit mode???
386 if plane.mode is not 'OBJECT':
387 bpy.ops.object.mode_set(mode='OBJECT')
388 plane.dimensions = x, y, 0.0
389 bpy.ops.object.transform_apply(scale=True)
390 plane.data.uv_textures.new()
391 plane.data.materials.append(material)
392 plane.data.uv_textures[0].data[0].image = img
394 material.game_settings.use_backface_culling = False
395 material.game_settings.alpha_blend = 'ALPHA'
396 return plane
398 def align_planes(self, planes):
399 gap = self.align_offset
400 offset = 0
401 for i, plane in enumerate(planes):
402 offset += (plane.dimensions.x / 2.0) + gap
403 if i == 0:
404 continue
405 move_local = mathutils.Vector((offset, 0.0, 0.0))
406 move_world = plane.location + move_local * plane.matrix_world.inverted()
407 plane.location += move_world
408 offset += (plane.dimensions.x / 2.0)
410 def generate_paths(self):
411 return (fn.name for fn in self.files if is_image_fn(fn.name, self.extension)), self.directory
413 # Internal
414 def create_image_textures(self, context, image):
415 fn_full = os.path.normpath(bpy.path.abspath(image.filepath))
417 # look for texture with importsettings
418 for texture in bpy.data.textures:
419 if texture.type == 'IMAGE':
420 tex_img = texture.image
421 if (tex_img is not None) and (tex_img.library is None):
422 fn_tex_full = os.path.normpath(bpy.path.abspath(tex_img.filepath))
423 if fn_full == fn_tex_full:
424 self.set_texture_options(context, texture)
425 return texture
427 # if no texture is found: create one
428 name_compat = bpy.path.display_name_from_filepath(image.filepath)
429 texture = bpy.data.textures.new(name=name_compat, type='IMAGE')
430 texture.image = image
431 self.set_texture_options(context, texture)
432 return texture
434 def create_material_for_texture(self, texture):
435 # look for material with the needed texture
436 for material in bpy.data.materials:
437 slot = material.texture_slots[0]
438 if slot and slot.texture == texture:
439 self.set_material_options(material, slot)
440 return material
442 # if no material found: create one
443 name_compat = bpy.path.display_name_from_filepath(texture.image.filepath)
444 material = bpy.data.materials.new(name=name_compat)
445 slot = material.texture_slots.add()
446 slot.texture = texture
447 slot.texture_coords = 'UV'
448 self.set_material_options(material, slot)
449 return material
451 def set_image_options(self, image):
452 image.alpha_mode = self.alpha_mode
453 image.use_fields = self.use_fields
455 if self.relative:
456 try: # can't always find the relative path (between drive letters on windows)
457 image.filepath = bpy.path.relpath(image.filepath)
458 except ValueError:
459 pass
461 def set_texture_options(self, context, texture):
462 texture.image.use_alpha = self.use_transparency
463 texture.image_user.use_auto_refresh = self.use_auto_refresh
464 if self.match_len:
465 ctx = context.copy()
466 ctx["edit_image"] = texture.image
467 ctx["edit_image_user"] = texture.image_user
468 bpy.ops.image.match_movie_length(ctx)
470 def set_material_options(self, material, slot):
471 if self.use_transparency:
472 material.alpha = 0.0
473 material.specular_alpha = 0.0
474 slot.use_map_alpha = True
475 else:
476 material.alpha = 1.0
477 material.specular_alpha = 1.0
478 slot.use_map_alpha = False
479 material.use_transparency = self.use_transparency
480 material.transparency_method = self.transparency_method
481 material.use_shadeless = self.use_shadeless
482 material.use_transparent_shadows = self.use_transparent_shadows
484 #--------------------------------------------------------------------------
485 # Cycles
486 def create_cycles_material(self, image):
487 name_compat = bpy.path.display_name_from_filepath(image.filepath)
488 material = None
489 for mat in bpy.data.materials:
490 if mat.name == name_compat and self.overwrite_node_tree:
491 material = mat
492 if not material:
493 material = bpy.data.materials.new(name=name_compat)
495 material.use_nodes = True
496 node_tree = material.node_tree
497 out_node = clean_node_tree(node_tree)
499 if self.shader == 'BSDF_DIFFUSE':
500 bsdf_diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
501 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
502 tex_image.image = image
503 tex_image.show_texture = True
504 node_tree.links.new(out_node.inputs[0], bsdf_diffuse.outputs[0])
505 node_tree.links.new(bsdf_diffuse.inputs[0], tex_image.outputs[0])
507 elif self.shader == 'EMISSION':
508 emission = node_tree.nodes.new('ShaderNodeEmission')
509 lightpath = node_tree.nodes.new('ShaderNodeLightPath')
510 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
511 tex_image.image = image
512 tex_image.show_texture = True
513 node_tree.links.new(out_node.inputs[0], emission.outputs[0])
514 node_tree.links.new(emission.inputs[0], tex_image.outputs[0])
515 node_tree.links.new(emission.inputs[1], lightpath.outputs[0])
517 elif self.shader == 'BSDF_DIFFUSE_BSDF_TRANSPARENT':
518 bsdf_diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
519 bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
520 mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
521 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
522 tex_image.image = image
523 tex_image.show_texture = True
524 node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0])
525 node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
526 node_tree.links.new(mix_shader.inputs[2], bsdf_diffuse.outputs[0])
527 node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
528 node_tree.links.new(bsdf_diffuse.inputs[0], tex_image.outputs[0])
530 elif self.shader == 'EMISSION_BSDF_TRANSPARENT':
531 emission = node_tree.nodes.new('ShaderNodeEmission')
532 lightpath = node_tree.nodes.new('ShaderNodeLightPath')
533 bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
534 mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
535 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
536 tex_image.image = image
537 tex_image.show_texture = True
538 node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0])
539 node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
540 node_tree.links.new(mix_shader.inputs[2], emission.outputs[0])
541 node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
542 node_tree.links.new(emission.inputs[0], tex_image.outputs[0])
543 node_tree.links.new(emission.inputs[1], lightpath.outputs[0])
545 auto_align_nodes(node_tree)
546 return material
549 # -----------------------------------------------------------------------------
550 # Register
551 def import_images_button(self, context):
552 self.layout.operator(IMPORT_OT_image_to_plane.bl_idname,
553 text="Images as Planes", icon='TEXTURE')
556 def register():
557 bpy.utils.register_module(__name__)
558 bpy.types.INFO_MT_file_import.append(import_images_button)
559 bpy.types.INFO_MT_mesh_add.append(import_images_button)
562 def unregister():
563 bpy.utils.unregister_module(__name__)
564 bpy.types.INFO_MT_file_import.remove(import_images_button)
565 bpy.types.INFO_MT_mesh_add.remove(import_images_button)
568 if __name__ == "__main__":
569 register()