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 #####
20 "name": "Import Images as Planes",
21 "author": "Florian Meyer (tstscr), mont29, matali",
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.",
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"}
33 from bpy
.types
import Operator
38 from bpy
.props
import (StringProperty
,
46 from bpy_extras
.object_utils
import AddObjectHelper
, object_data_add
47 from bpy_extras
.image_utils
import load_image
49 # -----------------------------------------------------------------------------
54 EXT_FILTER
= getattr(collections
, "OrderedDict", dict)((
55 (DEFAULT_EXT
, ((), "All image formats", "Import all known 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]}
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 # -----------------------------------------------------------------------------
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 # -----------------------------------------------------------------------------
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!).
104 for socket
in node
.inputs
:
106 for link
in input_links
:
109 # Node already treated!
111 elif link
.to_socket
== socket
:
112 sorted_nodes
.append(nd
)
115 input_links
-= done_links
119 def auto_align_nodes(node_tree
):
120 print('\nAligning Nodes')
123 nodes
= node_tree
.nodes
124 links
= node_tree
.links
127 if node
.type == 'OUTPUT_MATERIAL':
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
148 if not node
.type == 'OUTPUT_MATERIAL':
150 return node_tree
.nodes
[0]
153 # -----------------------------------------------------------------------------
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'}
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'})
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
= ""
189 self
.filter_image
= False
190 self
.filter_movie
= False
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
))
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
):
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.
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)")
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
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")
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")
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")
291 row
.prop(self
, "transparency_method", expand
=True)
292 box
.prop(self
, "use_transparent_shadows")
293 elif engine
== 'CYCLES':
295 box
.label("Material Settings: (Cycles)", icon
='MATERIAL')
296 box
.prop(self
, 'shader', expand
= True)
297 box
.prop(self
, 'overwrite_node_tree')
300 box
.label("Plane dimensions:", icon
='ARROW_LEFTRIGHT')
302 row
.prop(self
, "size_mode", expand
=True)
303 if self
.size_mode
== 'ABSOLUTE':
304 box
.prop(self
, "height")
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
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'}:
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
)
347 planes
= tuple(self
.create_image_plane(context
, mat
) for mat
in materials
)
349 context
.scene
.update()
351 self
.align_planes(planes
)
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'))
368 if px
== 0 or py
== 0:
371 if self
.size_mode
== 'ABSOLUTE':
374 elif self
.size_mode
== 'DPI':
375 fact
= 1 / self
.factor
/ context
.scene
.unit_settings
.scale_length
* 0.0254
378 else: # elif self.size_mode == 'DPBU'
379 fact
= 1 / self
.factor
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 plane
.name
= material
.name
390 bpy
.ops
.object.transform_apply(scale
=True)
391 plane
.data
.uv_textures
.new()
392 plane
.data
.materials
.append(material
)
393 plane
.data
.uv_textures
[0].data
[0].image
= img
395 material
.game_settings
.use_backface_culling
= False
396 material
.game_settings
.alpha_blend
= 'ALPHA'
399 def align_planes(self
, planes
):
400 gap
= self
.align_offset
402 for i
, plane
in enumerate(planes
):
403 offset
+= (plane
.dimensions
.x
/ 2.0) + gap
406 move_local
= mathutils
.Vector((offset
, 0.0, 0.0))
407 move_world
= plane
.location
+ move_local
* plane
.matrix_world
.inverted()
408 plane
.location
+= move_world
409 offset
+= (plane
.dimensions
.x
/ 2.0)
411 def generate_paths(self
):
412 return (fn
.name
for fn
in self
.files
if is_image_fn(fn
.name
, self
.extension
)), self
.directory
415 def create_image_textures(self
, context
, image
):
416 fn_full
= os
.path
.normpath(bpy
.path
.abspath(image
.filepath
))
418 # look for texture with importsettings
419 for texture
in bpy
.data
.textures
:
420 if texture
.type == 'IMAGE':
421 tex_img
= texture
.image
422 if (tex_img
is not None) and (tex_img
.library
is None):
423 fn_tex_full
= os
.path
.normpath(bpy
.path
.abspath(tex_img
.filepath
))
424 if fn_full
== fn_tex_full
:
425 self
.set_texture_options(context
, texture
)
428 # if no texture is found: create one
429 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
430 texture
= bpy
.data
.textures
.new(name
=name_compat
, type='IMAGE')
431 texture
.image
= image
432 self
.set_texture_options(context
, texture
)
435 def create_material_for_texture(self
, texture
):
436 # look for material with the needed texture
437 for material
in bpy
.data
.materials
:
438 slot
= material
.texture_slots
[0]
439 if slot
and slot
.texture
== texture
:
440 self
.set_material_options(material
, slot
)
443 # if no material found: create one
444 name_compat
= bpy
.path
.display_name_from_filepath(texture
.image
.filepath
)
445 material
= bpy
.data
.materials
.new(name
=name_compat
)
446 slot
= material
.texture_slots
.add()
447 slot
.texture
= texture
448 slot
.texture_coords
= 'UV'
449 self
.set_material_options(material
, slot
)
452 def set_image_options(self
, image
):
453 image
.alpha_mode
= self
.alpha_mode
454 image
.use_fields
= self
.use_fields
457 try: # can't always find the relative path (between drive letters on windows)
458 image
.filepath
= bpy
.path
.relpath(image
.filepath
)
462 def set_texture_options(self
, context
, texture
):
463 texture
.image
.use_alpha
= self
.use_transparency
464 texture
.image_user
.use_auto_refresh
= self
.use_auto_refresh
467 ctx
["edit_image"] = texture
.image
468 ctx
["edit_image_user"] = texture
.image_user
469 bpy
.ops
.image
.match_movie_length(ctx
)
471 def set_material_options(self
, material
, slot
):
472 if self
.use_transparency
:
474 material
.specular_alpha
= 0.0
475 slot
.use_map_alpha
= True
478 material
.specular_alpha
= 1.0
479 slot
.use_map_alpha
= False
480 material
.use_transparency
= self
.use_transparency
481 material
.transparency_method
= self
.transparency_method
482 material
.use_shadeless
= self
.use_shadeless
483 material
.use_transparent_shadows
= self
.use_transparent_shadows
485 #--------------------------------------------------------------------------
487 def create_cycles_material(self
, image
):
488 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
490 for mat
in bpy
.data
.materials
:
491 if mat
.name
== name_compat
and self
.overwrite_node_tree
:
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 if self
.shader
== 'BSDF_DIFFUSE':
501 bsdf_diffuse
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
502 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
503 tex_image
.image
= image
504 tex_image
.show_texture
= True
505 node_tree
.links
.new(out_node
.inputs
[0], bsdf_diffuse
.outputs
[0])
506 node_tree
.links
.new(bsdf_diffuse
.inputs
[0], tex_image
.outputs
[0])
508 elif self
.shader
== 'EMISSION':
509 emission
= node_tree
.nodes
.new('ShaderNodeEmission')
510 lightpath
= node_tree
.nodes
.new('ShaderNodeLightPath')
511 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
512 tex_image
.image
= image
513 tex_image
.show_texture
= True
514 node_tree
.links
.new(out_node
.inputs
[0], emission
.outputs
[0])
515 node_tree
.links
.new(emission
.inputs
[0], tex_image
.outputs
[0])
516 node_tree
.links
.new(emission
.inputs
[1], lightpath
.outputs
[0])
518 elif self
.shader
== 'BSDF_DIFFUSE_BSDF_TRANSPARENT':
519 bsdf_diffuse
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
520 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
521 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
522 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
523 tex_image
.image
= image
524 tex_image
.show_texture
= True
525 node_tree
.links
.new(out_node
.inputs
[0], mix_shader
.outputs
[0])
526 node_tree
.links
.new(mix_shader
.inputs
[0], tex_image
.outputs
[1])
527 node_tree
.links
.new(mix_shader
.inputs
[2], bsdf_diffuse
.outputs
[0])
528 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
[0])
529 node_tree
.links
.new(bsdf_diffuse
.inputs
[0], tex_image
.outputs
[0])
531 elif self
.shader
== 'EMISSION_BSDF_TRANSPARENT':
532 emission
= node_tree
.nodes
.new('ShaderNodeEmission')
533 lightpath
= node_tree
.nodes
.new('ShaderNodeLightPath')
534 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
535 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
536 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
537 tex_image
.image
= image
538 tex_image
.show_texture
= True
539 node_tree
.links
.new(out_node
.inputs
[0], mix_shader
.outputs
[0])
540 node_tree
.links
.new(mix_shader
.inputs
[0], tex_image
.outputs
[1])
541 node_tree
.links
.new(mix_shader
.inputs
[2], emission
.outputs
[0])
542 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
[0])
543 node_tree
.links
.new(emission
.inputs
[0], tex_image
.outputs
[0])
544 node_tree
.links
.new(emission
.inputs
[1], lightpath
.outputs
[0])
546 auto_align_nodes(node_tree
)
550 # -----------------------------------------------------------------------------
552 def import_images_button(self
, context
):
553 self
.layout
.operator(IMPORT_OT_image_to_plane
.bl_idname
,
554 text
="Images as Planes", icon
='TEXTURE')
558 bpy
.utils
.register_module(__name__
)
559 bpy
.types
.INFO_MT_file_import
.append(import_images_button
)
560 bpy
.types
.INFO_MT_mesh_add
.append(import_images_button
)
564 bpy
.utils
.unregister_module(__name__
)
565 bpy
.types
.INFO_MT_file_import
.remove(import_images_button
)
566 bpy
.types
.INFO_MT_mesh_add
.remove(import_images_button
)
569 if __name__
== "__main__":