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