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, 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.",
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"),
80 ('BSDF_DIFFUSE_BSDF_TRANSPARENT', "Diffuse & Transparent", "Diffuse and Transparent Mix"),
81 ('EMISSION_BSDF_TRANSPARENT', "Emission & Transparent", "Emission and Transparent Mix")
84 # -----------------------------------------------------------------------------
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 # -----------------------------------------------------------------------------
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!).
106 for socket
in node
.inputs
:
108 for link
in input_links
:
111 # Node already treated!
113 elif link
.to_socket
== socket
:
114 sorted_nodes
.append(nd
)
117 input_links
-= done_links
121 def auto_align_nodes(node_tree
):
122 print('\nAligning Nodes')
125 nodes
= node_tree
.nodes
126 links
= node_tree
.links
129 if node
.type == 'OUTPUT_MATERIAL':
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
150 if not node
.type == 'OUTPUT_MATERIAL':
152 return node_tree
.nodes
[0]
155 # -----------------------------------------------------------------------------
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'}
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'})
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
= ""
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
):
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.
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)")
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
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")
272 row
.active
= bpy
.data
.is_saved
273 row
.prop(self
, "relative")
274 box
.prop(self
, "match_len")
276 row
.prop(self
, "use_transparency")
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")
284 if engine
== 'BLENDER_RENDER':
285 box
.label("Material Settings: (Blender)", icon
='MATERIAL')
286 box
.prop(self
, "use_shadeless")
288 row
.prop(self
, "transparency_method", expand
=True)
289 box
.prop(self
, "use_transparent_shadows")
290 elif engine
== 'CYCLES':
292 box
.label("Material Settings: (Cycles)", icon
='MATERIAL')
293 box
.prop(self
, 'shader', expand
= True)
294 box
.prop(self
, 'overwrite_node_tree')
297 box
.label("Plane dimensions:", icon
='ARROW_LEFTRIGHT')
299 row
.prop(self
, "size_mode", expand
=True)
300 if self
.size_mode
== 'ABSOLUTE':
301 box
.prop(self
, "height")
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
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
)
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
)
343 planes
= tuple(self
.create_image_plane(context
, mat
) for mat
in materials
)
345 context
.scene
.update()
347 self
.align_planes(planes
)
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'))
364 if px
== 0 or py
== 0:
367 if self
.size_mode
== 'ABSOLUTE':
370 elif self
.size_mode
== 'DPI':
371 fact
= 1 / self
.factor
/ context
.scene
.unit_settings
.scale_length
* 0.0254
374 else: # elif self.size_mode == 'DPBU'
375 fact
= 1 / self
.factor
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'
395 def align_planes(self
, planes
):
396 gap
= self
.align_offset
398 for i
, plane
in enumerate(planes
):
399 offset
+= (plane
.dimensions
.x
/ 2.0) + gap
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
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(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
)
540 # -----------------------------------------------------------------------------
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')
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
)
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__":