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, 76, 1),
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 (
48 from bpy_extras
.object_utils
import AddObjectHelper
, object_data_add
49 from bpy_extras
.image_utils
import load_image
51 # -----------------------------------------------------------------------------
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]}
78 ('BSDF_DIFFUSE', "Diffuse", "Diffuse Shader"),
79 ('EMISSION', "Emission", "Emission Shader")
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 force_reload
= BoolProperty(name
="Force Reload", default
=False,
181 description
="Force reloading of the image if already opened elsewhere in Blender")
183 # Callback which will update File window's filter options accordingly to extension setting.
184 def update_extensions(self
, context
):
185 if self
.extension
== DEFAULT_EXT
:
186 self
.filter_image
= True
187 self
.filter_movie
= True
188 self
.filter_glob
= ""
190 self
.filter_image
= False
191 self
.filter_movie
= False
192 flt
= ";".join(("*." + e
for e
in EXT_FILTER
[self
.extension
][0]))
193 self
.filter_glob
= flt
194 # And now update space (file select window), if possible.
195 space
= bpy
.context
.space_data
196 # XXX Can't use direct op comparison, these are not the same objects!
197 if (space
.type != 'FILE_BROWSER' or space
.operator
.bl_rna
.identifier
!= self
.bl_rna
.identifier
):
199 space
.params
.use_filter_image
= self
.filter_image
200 space
.params
.use_filter_movie
= self
.filter_movie
201 space
.params
.filter_glob
= self
.filter_glob
202 # XXX Seems to be necessary, else not all changes above take effect...
203 #~ bpy.ops.file.refresh()
204 extension
= EnumProperty(name
="Extension", items
=gen_ext_filter_ui_items(),
205 description
="Only import files of this type", update
=update_extensions
)
207 # -------------------
208 # Plane size options.
210 ('ABSOLUTE', "Absolute", "Use absolute size"),
211 ('DPI', "Dpi", "Use definition of the image as dots per inch"),
212 ('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
214 size_mode
= EnumProperty(name
="Size Mode", default
='ABSOLUTE', items
=_size_modes
,
215 description
="How the size of the plane is computed")
217 height
= FloatProperty(name
="Height", description
="Height of the created plane",
218 default
=1.0, min=0.001, soft_min
=0.001, subtype
='DISTANCE', unit
='LENGTH')
220 factor
= FloatProperty(name
="Definition", min=1.0, default
=600.0,
221 description
="Number of pixels per inch or Blender Unit")
223 # -------------------------
224 # Blender material options.
225 t
= bpy
.types
.Material
.bl_rna
.properties
["use_shadeless"]
226 use_shadeless
= BoolProperty(name
=t
.name
, default
=False, description
=t
.description
)
228 use_transparency
= BoolProperty(name
="Use Alpha", default
=False, description
="Use alphachannel for transparency")
230 t
= bpy
.types
.Material
.bl_rna
.properties
["transparency_method"]
231 items
= tuple((it
.identifier
, it
.name
, it
.description
) for it
in t
.enum_items
)
232 transparency_method
= EnumProperty(name
="Transp. Method", description
=t
.description
, items
=items
)
234 t
= bpy
.types
.Material
.bl_rna
.properties
["use_transparent_shadows"]
235 use_transparent_shadows
= BoolProperty(name
=t
.name
, default
=False, description
=t
.description
)
237 #-------------------------
238 # Cycles material options.
239 shader
= EnumProperty(name
="Shader", items
=CYCLES_SHADERS
, description
="Node shader to use")
241 overwrite_node_tree
= BoolProperty(name
="Overwrite Material", default
=True,
242 description
="Overwrite existing Material with new nodetree "
243 "(based on material name)")
247 t
= bpy
.types
.Image
.bl_rna
.properties
["alpha_mode"]
248 alpha_mode_items
= tuple((e
.identifier
, e
.name
, e
.description
) for e
in t
.enum_items
)
249 alpha_mode
= EnumProperty(name
=t
.name
, items
=alpha_mode_items
, default
=t
.default
, description
=t
.description
)
251 t
= bpy
.types
.IMAGE_OT_match_movie_length
.bl_rna
252 match_len
= BoolProperty(name
=t
.name
, default
=True, description
=t
.description
)
254 t
= bpy
.types
.Image
.bl_rna
.properties
["use_fields"]
255 use_fields
= BoolProperty(name
=t
.name
, default
=False, description
=t
.description
)
257 t
= bpy
.types
.ImageUser
.bl_rna
.properties
["use_auto_refresh"]
258 use_auto_refresh
= BoolProperty(name
=t
.name
, default
=True, description
=t
.description
)
260 relative
= BoolProperty(name
="Relative", default
=True, description
="Apply relative paths")
262 def draw(self
, context
):
263 engine
= context
.scene
.render
.engine
267 box
.label("Import Options:", icon
='FILTER')
268 box
.prop(self
, "extension", icon
='FILE_IMAGE')
269 box
.prop(self
, "align")
270 box
.prop(self
, "align_offset")
273 row
.prop(self
, "force_reload")
275 row
.active
= bpy
.data
.is_saved
276 row
.prop(self
, "relative")
277 box
.prop(self
, "match_len")
279 row
.prop(self
, "use_transparency")
281 sub
.active
= self
.use_transparency
282 sub
.prop(self
, "alpha_mode", text
="")
283 box
.prop(self
, "use_fields")
284 box
.prop(self
, "use_auto_refresh")
287 if engine
== 'BLENDER_RENDER':
288 box
.label("Material Settings: (Blender)", icon
='MATERIAL')
289 box
.prop(self
, "use_shadeless")
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
= tuple(load_image(path
, directory
, check_existing
=True, force_reload
=self
.force_reload
)
334 for path
in import_list
)
337 self
.set_image_options(img
)
339 if engine
in {'BLENDER_RENDER', 'BLENDER_GAME'}:
340 textures
= (self
.create_image_textures(context
, img
) for img
in images
)
341 materials
= (self
.create_material_for_texture(tex
) for tex
in textures
)
342 elif engine
== 'CYCLES':
343 materials
= (self
.create_cycles_material(context
, 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
.data
.name
= 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
401 offset
= (planes
[0].dimensions
.x
/ 2.0) + gap
402 for plane
in planes
[1:]:
403 move_global
= mathutils
.Vector((offset
+ (plane
.dimensions
.x
/ 2.0), 0.0, 0.0))
404 plane
.location
= plane
.matrix_world
* move_global
405 offset
+= plane
.dimensions
.x
+ gap
407 def generate_paths(self
):
408 return (fn
.name
for fn
in self
.files
if is_image_fn(fn
.name
, self
.extension
)), self
.directory
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
)
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
)
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
)
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
)
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
454 try: # can't always find the relative path (between drive letters on windows)
455 image
.filepath
= bpy
.path
.relpath(image
.filepath
)
459 def set_texture_options(self
, context
, texture
):
460 texture
.image_user
.use_auto_refresh
= self
.use_auto_refresh
462 texture
.image_user
.frame_duration
= texture
.image
.frame_duration
464 def set_material_options(self
, material
, slot
):
465 if self
.use_transparency
:
467 material
.specular_alpha
= 0.0
468 slot
.use_map_alpha
= True
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 #--------------------------------------------------------------------------
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
)
487 def create_cycles_material(self
, context
, 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 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(bsdf_diffuse
.inputs
[0], tex_image
.outputs
[0])
505 if self
.use_transparency
:
506 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
507 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
508 node_tree
.links
.new(out_node
.inputs
[0], mix_shader
.outputs
[0])
509 node_tree
.links
.new(mix_shader
.inputs
[0], tex_image
.outputs
[1])
510 node_tree
.links
.new(mix_shader
.inputs
[2], bsdf_diffuse
.outputs
[0])
511 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
[0])
513 node_tree
.links
.new(out_node
.inputs
[0], bsdf_diffuse
.outputs
[0])
515 elif self
.shader
== 'EMISSION':
516 emission
= node_tree
.nodes
.new('ShaderNodeEmission')
517 lightpath
= node_tree
.nodes
.new('ShaderNodeLightPath')
518 node_tree
.links
.new(emission
.inputs
[0], tex_image
.outputs
[0])
519 node_tree
.links
.new(emission
.inputs
[1], lightpath
.outputs
[0])
520 if self
.use_transparency
:
521 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
522 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
523 node_tree
.links
.new(out_node
.inputs
[0], mix_shader
.outputs
[0])
524 node_tree
.links
.new(mix_shader
.inputs
[0], tex_image
.outputs
[1])
525 node_tree
.links
.new(mix_shader
.inputs
[2], emission
.outputs
[0])
526 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
[0])
528 node_tree
.links
.new(out_node
.inputs
[0], emission
.outputs
[0])
530 auto_align_nodes(node_tree
)
534 # -----------------------------------------------------------------------------
536 def import_images_button(self
, context
):
537 self
.layout
.operator(IMPORT_OT_image_to_plane
.bl_idname
,
538 text
="Images as Planes", icon
='TEXTURE')
542 bpy
.utils
.register_module(__name__
)
543 bpy
.types
.INFO_MT_file_import
.append(import_images_button
)
544 bpy
.types
.INFO_MT_mesh_add
.append(import_images_button
)
548 bpy
.utils
.unregister_module(__name__
)
549 bpy
.types
.INFO_MT_file_import
.remove(import_images_button
)
550 bpy
.types
.INFO_MT_mesh_add
.remove(import_images_button
)
553 if __name__
== "__main__":