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/"
29 "Scripts/Add_Mesh/Planes_from_Images",
30 "category": "Import-Export",
34 from bpy
.types
import Operator
39 from bpy
.props
import (StringProperty
,
47 from bpy_extras
.object_utils
import AddObjectHelper
, object_data_add
48 from bpy_extras
.image_utils
import load_image
50 # -----------------------------------------------------------------------------
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]}
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 # -----------------------------------------------------------------------------
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 # -----------------------------------------------------------------------------
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!).
105 for socket
in node
.inputs
:
107 for link
in input_links
:
110 # Node already treated!
112 elif link
.to_socket
== socket
:
113 sorted_nodes
.append(nd
)
116 input_links
-= done_links
120 def auto_align_nodes(node_tree
):
121 print('\nAligning Nodes')
124 nodes
= node_tree
.nodes
125 links
= node_tree
.links
128 if node
.type == 'OUTPUT_MATERIAL':
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
149 if not node
.type == 'OUTPUT_MATERIAL':
151 return node_tree
.nodes
[0]
154 # -----------------------------------------------------------------------------
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'}
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'})
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
= ""
190 self
.filter_image
= False
191 self
.filter_movie
= False
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
))
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
):
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.
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)")
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
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")
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")
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")
292 row
.prop(self
, "transparency_method", expand
=True)
293 box
.prop(self
, "use_transparent_shadows")
294 elif engine
== 'CYCLES':
296 box
.label("Material Settings: (Cycles)", icon
='MATERIAL')
297 box
.prop(self
, 'shader', expand
= True)
298 box
.prop(self
, 'overwrite_node_tree')
301 box
.label("Plane dimensions:", icon
='ARROW_LEFTRIGHT')
303 row
.prop(self
, "size_mode", expand
=True)
304 if self
.size_mode
== 'ABSOLUTE':
305 box
.prop(self
, "height")
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
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'}:
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
)
348 planes
= tuple(self
.create_image_plane(context
, mat
) for mat
in materials
)
350 context
.scene
.update()
352 self
.align_planes(planes
)
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'))
369 if px
== 0 or py
== 0:
372 if self
.size_mode
== 'ABSOLUTE':
375 elif self
.size_mode
== 'DPI':
376 fact
= 1 / self
.factor
/ context
.scene
.unit_settings
.scale_length
* 0.0254
379 else: # elif self.size_mode == 'DPBU'
380 fact
= 1 / self
.factor
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'
400 def align_planes(self
, planes
):
401 gap
= self
.align_offset
403 for i
, plane
in enumerate(planes
):
404 offset
+= (plane
.dimensions
.x
/ 2.0) + gap
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
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
)
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
)
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
)
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
)
453 def set_image_options(self
, image
):
454 image
.alpha_mode
= self
.alpha_mode
455 image
.use_fields
= self
.use_fields
458 try: # can't always find the relative path (between drive letters on windows)
459 image
.filepath
= bpy
.path
.relpath(image
.filepath
)
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
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
:
475 material
.specular_alpha
= 0.0
476 slot
.use_map_alpha
= True
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 #--------------------------------------------------------------------------
488 def create_cycles_material(self
, image
):
489 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
491 for mat
in bpy
.data
.materials
:
492 if mat
.name
== name_compat
and self
.overwrite_node_tree
:
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
)
551 # -----------------------------------------------------------------------------
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')
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
)
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__":