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 #####
22 "name": "Import Images as Planes",
23 "author": "Florian Meyer (tstscr), mont29, matali, Ted Schundler (SpkyElctrc)",
25 "blender": (2, 78, 0),
26 "location": "File > Import > Images as Planes or Add > Mesh > Images as Planes",
27 "description": "Imports images and creates planes with the appropriate aspect ratio. "
28 "The images are mapped to the planes.",
30 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
31 "Scripts/Add_Mesh/Planes_from_Images",
32 "category": "Import-Export",
38 from itertools
import count
, repeat
39 from collections
import namedtuple
43 from bpy
.types
import Operator
44 from mathutils
import Vector
46 from bpy
.props
import (
54 from bpy_extras
.object_utils
import (
59 from bpy_extras
.image_utils
import load_image
61 # -----------------------------------------------------------------------------
62 # Module-level Shared State
64 watched_objects
= {} # used to trigger compositor updates on scene updates
67 # -----------------------------------------------------------------------------
70 def add_driver_prop(driver
, name
, type, id, path
):
71 """Configure a new driver variable."""
72 dv
= driver
.variables
.new()
74 dv
.type = 'SINGLE_PROP'
75 target
= dv
.targets
[0]
78 target
.data_path
= path
81 # -----------------------------------------------------------------------------
84 ImageSpec
= namedtuple(
86 ['image', 'size', 'frame_start', 'frame_offset', 'frame_duration'])
88 num_regex
= re
.compile('[0-9]') # Find a single number
89 nums_regex
= re
.compile('[0-9]+') # Find a set of numbers
92 def find_image_sequences(files
):
93 """From a group of files, detect image sequences.
95 This returns a generator of tuples, which contain the filename,
96 start frame, and length of the detected sequence
98 >>> list(find_image_sequences([
99 ... "test2-001.jp2", "test2-002.jp2",
100 ... "test3-003.jp2", "test3-004.jp2", "test3-005.jp2", "test3-006.jp2",
102 [('blaah', 1, 1), ('test2-001.jp2', 1, 2), ('test3-003.jp2', 3, 4)]
105 files
= iter(sorted(files
))
111 for filename
in files
:
112 new_pattern
= num_regex
.sub('#', filename
)
113 new_matches
= list(map(int, nums_regex
.findall(filename
)))
114 if new_pattern
== pattern
:
115 # this file looks like it may be in sequence from the previous
117 # if there are multiple sets of numbers, figure out what changed
119 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
124 # did it only change by one?
125 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
127 # We expect this to increment
137 # No continuation -> spit out what we found and reset counters
140 yield prev_file
, matches
[segment
], length
142 yield prev_file
, 1, 1
145 matches
= new_matches
146 pattern
= new_pattern
152 yield prev_file
, matches
[segment
], length
154 yield prev_file
, 1, 1
157 def load_images(filenames
, directory
, force_reload
=False, frame_start
=1, find_sequences
=False):
158 """Wrapper for bpy's load_image
160 Loads a set of images, movies, or even image sequences
161 Returns a generator of ImageSpec wrapper objects later used for texture setup
163 if find_sequences
: # if finding sequences, we need some pre-processing first
164 file_iter
= find_image_sequences(filenames
)
166 file_iter
= zip(filenames
, repeat(1), repeat(1))
168 for filename
, offset
, frames
in file_iter
:
169 image
= load_image(filename
, directory
, check_existing
=True, force_reload
=force_reload
)
171 # Size is unavailable for sequences, so we grab it early
172 size
= tuple(image
.size
)
174 if image
.source
== 'MOVIE':
176 # This number is only valid when read a second time in 2.77
177 # This repeated line is not a mistake
178 frames
= image
.frame_duration
179 frames
= image
.frame_duration
181 elif frames
> 1: # Not movie, but multiple frames -> image sequence
182 image
.use_animation
= True
183 image
.source
= 'SEQUENCE'
185 yield ImageSpec(image
, size
, frame_start
, offset
- 1, frames
)
188 # -----------------------------------------------------------------------------
189 # Position & Size Helpers
191 def offset_planes(planes
, gap
, axis
):
192 """Offset planes from each other by `gap` amount along a _local_ vector `axis`
194 For example, offset_planes([obj1, obj2], 0.5, Vector(0, 0, 1)) will place
195 obj2 0.5 blender units away from obj1 along the local positive Z axis.
197 This is in local space, not world space, so all planes should share
198 a common scale and rotation.
202 for current
in planes
[1:]:
204 local_offset
= abs((prior
.dimensions
+ current
.dimensions
) * axis
) / 2.0 + gap
206 offset
+= local_offset
* axis
207 current
.location
= current
.matrix_world
* offset
212 def compute_camera_size(context
, center
, fill_mode
, aspect
):
213 """Determine how large an object needs to be to fit or fill the camera's field of view."""
214 scene
= context
.scene
215 camera
= scene
.camera
216 view_frame
= camera
.data
.view_frame(scene
=scene
)
218 Vector([max(v
[i
] for v
in view_frame
) for i
in range(3)]) - \
219 Vector([min(v
[i
] for v
in view_frame
) for i
in range(3)])
220 camera_aspect
= frame_size
.x
/ frame_size
.y
222 # Convert the frame size to the correct sizing at a given distance
223 if camera
.type == 'ORTHO':
224 frame_size
= frame_size
.xy
226 # Perspective transform
227 distance
= world_to_camera_view(scene
, camera
, center
).z
228 frame_size
= distance
* frame_size
.xy
/ (-view_frame
[0].z
)
230 # Determine what axis to match to the camera
231 match_axis
= 0 # match the Y axis size
232 match_aspect
= aspect
233 if (fill_mode
== 'FILL' and aspect
> camera_aspect
) or \
234 (fill_mode
== 'FIT' and aspect
< camera_aspect
):
235 match_axis
= 1 # match the X axis size
236 match_aspect
= 1.0 / aspect
238 # scale the other axis to the correct aspect
239 frame_size
[1 - match_axis
] = frame_size
[match_axis
] / match_aspect
244 def center_in_camera(scene
, camera
, obj
, axis
=(1, 1)):
245 """Center object along specified axiis of the camera"""
246 camera_matrix_col
= camera
.matrix_world
.col
247 location
= obj
.location
249 # Vector from the camera's world coordinate center to the object's center
250 delta
= camera_matrix_col
[3].xyz
- location
252 # How far off center we are along the camera's local X
253 camera_x_mag
= delta
* camera_matrix_col
[0].xyz
* axis
[0]
254 # How far off center we are along the camera's local Y
255 camera_y_mag
= delta
* camera_matrix_col
[1].xyz
* axis
[1]
257 # Now offet only along camera local axiis
258 offset
= camera_matrix_col
[0].xyz
* camera_x_mag
+ \
259 camera_matrix_col
[1].xyz
* camera_y_mag
261 obj
.location
= location
+ offset
264 # -----------------------------------------------------------------------------
267 def get_input_nodes(node
, links
):
268 """Get nodes that are a inputs to the given node"""
269 # Get all links going to node.
270 input_links
= {lnk
for lnk
in links
if lnk
.to_node
== node
}
271 # Sort those links, get their input nodes (and avoid doubles!).
274 for socket
in node
.inputs
:
276 for link
in input_links
:
279 # Node already treated!
281 elif link
.to_socket
== socket
:
282 sorted_nodes
.append(nd
)
285 input_links
-= done_links
289 def auto_align_nodes(node_tree
):
290 """Given a shader node tree, arrange nodes neatly relative to the output node."""
293 nodes
= node_tree
.nodes
294 links
= node_tree
.links
297 if node
.type == 'OUTPUT_MATERIAL' or node
.type == 'GROUP_OUTPUT':
301 else: # Just in case there is no output
305 from_nodes
= get_input_nodes(to_node
, links
)
306 for i
, node
in enumerate(from_nodes
):
307 node
.location
.x
= min(node
.location
.x
, to_node
.location
.x
- x_gap
)
308 node
.location
.y
= to_node
.location
.y
309 node
.location
.y
-= i
* y_gap
310 node
.location
.y
+= (len(from_nodes
) - 1) * y_gap
/ (len(from_nodes
))
316 def clean_node_tree(node_tree
):
317 """Clear all nodes in a shader node tree except the output.
319 Returns the output node
321 nodes
= node_tree
.nodes
322 for node
in list(nodes
): # copy to avoid altering the loop's data source
323 if not node
.type == 'OUTPUT_MATERIAL':
326 return node_tree
.nodes
[0]
329 def get_shadeless_node(dest_node_tree
):
330 """Return a "shadless" cycles node, creating a node group if nonexistant"""
332 node_tree
= bpy
.data
.node_groups
['IAP_SHADELESS']
335 # need to build node shadeless node group
336 node_tree
= bpy
.data
.node_groups
.new('IAP_SHADELESS', 'ShaderNodeTree')
337 output_node
= node_tree
.nodes
.new('NodeGroupOutput')
338 input_node
= node_tree
.nodes
.new('NodeGroupInput')
340 node_tree
.outputs
.new('NodeSocketShader', 'Shader')
341 node_tree
.inputs
.new('NodeSocketColor', 'Color')
343 # This could be faster as a transparent shader, but then no ambient occlusion
344 diffuse_shader
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
345 node_tree
.links
.new(diffuse_shader
.inputs
[0], input_node
.outputs
[0])
347 emission_shader
= node_tree
.nodes
.new('ShaderNodeEmission')
348 node_tree
.links
.new(emission_shader
.inputs
[0], input_node
.outputs
[0])
350 light_path
= node_tree
.nodes
.new('ShaderNodeLightPath')
351 is_glossy_ray
= light_path
.outputs
['Is Glossy Ray']
352 is_shadow_ray
= light_path
.outputs
['Is Shadow Ray']
353 ray_depth
= light_path
.outputs
['Ray Depth']
354 transmission_depth
= light_path
.outputs
['Transmission Depth']
356 unrefracted_depth
= node_tree
.nodes
.new('ShaderNodeMath')
357 unrefracted_depth
.operation
= 'SUBTRACT'
358 unrefracted_depth
.label
= 'Bounce Count'
359 node_tree
.links
.new(unrefracted_depth
.inputs
[0], ray_depth
)
360 node_tree
.links
.new(unrefracted_depth
.inputs
[1], transmission_depth
)
362 refracted
= node_tree
.nodes
.new('ShaderNodeMath')
363 refracted
.operation
= 'SUBTRACT'
364 refracted
.label
= 'Camera or Refracted'
365 refracted
.inputs
[0].default_value
= 1.0
366 node_tree
.links
.new(refracted
.inputs
[1], unrefracted_depth
.outputs
[0])
368 reflection_limit
= node_tree
.nodes
.new('ShaderNodeMath')
369 reflection_limit
.operation
= 'SUBTRACT'
370 reflection_limit
.label
= 'Limit Reflections'
371 reflection_limit
.inputs
[0].default_value
= 2.0
372 node_tree
.links
.new(reflection_limit
.inputs
[1], ray_depth
)
374 camera_reflected
= node_tree
.nodes
.new('ShaderNodeMath')
375 camera_reflected
.operation
= 'MULTIPLY'
376 camera_reflected
.label
= 'Camera Ray to Glossy'
377 node_tree
.links
.new(camera_reflected
.inputs
[0], reflection_limit
.outputs
[0])
378 node_tree
.links
.new(camera_reflected
.inputs
[1], is_glossy_ray
)
380 shadow_or_reflect
= node_tree
.nodes
.new('ShaderNodeMath')
381 shadow_or_reflect
.operation
= 'MAXIMUM'
382 shadow_or_reflect
.label
= 'Shadow or Reflection?'
383 node_tree
.links
.new(shadow_or_reflect
.inputs
[0], camera_reflected
.outputs
[0])
384 node_tree
.links
.new(shadow_or_reflect
.inputs
[1], is_shadow_ray
)
386 shadow_or_reflect_or_refract
= node_tree
.nodes
.new('ShaderNodeMath')
387 shadow_or_reflect_or_refract
.operation
= 'MAXIMUM'
388 shadow_or_reflect_or_refract
.label
= 'Shadow, Reflect or Refract?'
389 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[0], shadow_or_reflect
.outputs
[0])
390 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[1], refracted
.outputs
[0])
392 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
393 node_tree
.links
.new(mix_shader
.inputs
[0], shadow_or_reflect_or_refract
.outputs
[0])
394 node_tree
.links
.new(mix_shader
.inputs
[1], diffuse_shader
.outputs
[0])
395 node_tree
.links
.new(mix_shader
.inputs
[2], emission_shader
.outputs
[0])
397 node_tree
.links
.new(output_node
.inputs
[0], mix_shader
.outputs
[0])
399 auto_align_nodes(node_tree
)
401 group_node
= dest_node_tree
.nodes
.new("ShaderNodeGroup")
402 group_node
.node_tree
= node_tree
407 # -----------------------------------------------------------------------------
408 # Corner Pin Driver Helpers
410 @bpy.app
.handlers
.persistent
411 def check_drivers(*args
, **kwargs
):
412 """Check if watched objects in a scene have changed and trigger compositor update
414 This is part of a hack to ensure the compositor updates
415 itself when the objects used for drivers change.
417 It only triggers if transformation matricies change to avoid
418 a cyclic loop of updates.
420 if not watched_objects
:
421 # if there is nothing to watch, don't bother running this
422 bpy
.app
.handlers
.scene_update_post
.remove(check_drivers
)
426 for name
, matrix
in list(watched_objects
.items()):
428 obj
= bpy
.data
.objects
[name
]
430 # The user must have removed this object
431 del watched_objects
[name
]
433 new_matrix
= tuple(map(tuple, obj
.matrix_world
)).__hash
__()
434 if new_matrix
!= matrix
:
435 watched_objects
[name
] = new_matrix
439 # Trick to re-evaluate drivers
440 bpy
.context
.scene
.frame_current
= bpy
.context
.scene
.frame_current
443 def register_watched_object(obj
):
444 """Register an object to be monitored for transformation changes"""
447 # known object? -> we're done
448 if name
in watched_objects
:
451 if not watched_objects
:
452 # make sure check_drivers is active
453 bpy
.app
.handlers
.scene_update_post
.append(check_drivers
)
455 watched_objects
[name
] = None
458 def find_plane_corner(object_name
, x
, y
, axis
, camera
=None, *args
, **kwargs
):
459 """Find the location in camera space of a plane's corner"""
461 # I've added args / kwargs as a compatability measure with future versions
462 warnings
.warn("Unknown Parameters Passed to \"Images as Planes\". Maybe you need to upgrade?")
464 plane
= bpy
.data
.objects
[object_name
]
466 # Passing in camera doesn't work before 2.78, so we use the current one
467 camera
= camera
or bpy
.context
.scene
.camera
469 # Hack to ensure compositor updates on future changes
470 register_watched_object(camera
)
471 register_watched_object(plane
)
473 scale
= plane
.scale
* 2.0
474 v
= plane
.dimensions
.copy()
477 v
= plane
.matrix_world
* v
479 camera_vertex
= world_to_camera_view(
480 bpy
.context
.scene
, camera
, v
)
482 return camera_vertex
[axis
]
485 @bpy.app
.handlers
.persistent
486 def register_driver(*args
, **kwargs
):
487 """Register the find_plane_corner function for use with drivers"""
488 bpy
.app
.driver_namespace
['import_image__find_plane_corner'] = find_plane_corner
491 # -----------------------------------------------------------------------------
492 # Compositing Helpers
494 def group_in_frame(node_tree
, name
, nodes
):
495 frame_node
= node_tree
.nodes
.new("NodeFrame")
496 frame_node
.label
= name
497 frame_node
.name
= name
+ "_frame"
499 min_pos
= Vector(nodes
[0].location
)
500 max_pos
= min_pos
.copy()
503 top_left
= node
.location
504 bottom_right
= top_left
+ Vector((node
.width
, -node
.height
))
507 min_pos
[i
] = min(min_pos
[i
], top_left
[i
], bottom_right
[i
])
508 max_pos
[i
] = max(max_pos
[i
], top_left
[i
], bottom_right
[i
])
510 node
.parent
= frame_node
512 frame_node
.width
= max_pos
[0] - min_pos
[0] + 50
513 frame_node
.height
= max(max_pos
[1] - min_pos
[1] + 50, 450)
514 frame_node
.shrink
= True
519 def position_frame_bottom_left(node_tree
, frame_node
):
520 newpos
= Vector((100000, 100000)) # start reasonably far top / right
522 # Align with the furthest left
523 for node
in node_tree
.nodes
.values():
524 if node
!= frame_node
and node
.parent
!= frame_node
:
525 newpos
.x
= min(newpos
.x
, node
.location
.x
+ 30)
527 # As high as we can get without overlapping anything to the right
528 for node
in node_tree
.nodes
.values():
529 if node
!= frame_node
and not node
.parent
:
530 if node
.location
.x
< newpos
.x
+ frame_node
.width
:
531 print("Below", node
.name
, node
.location
, node
.height
, node
.dimensions
)
532 newpos
.y
= min(newpos
.y
, node
.location
.y
- max(node
.dimensions
.y
, node
.height
) - 20)
534 frame_node
.location
= newpos
537 def setup_compositing(context
, plane
, img_spec
):
538 # Node Groups only work with "new" dependency graph and even
539 # then it has some problems with not updating the first time
540 # So instead this groups with a node frame, which works reliably
542 scene
= context
.scene
543 scene
.use_nodes
= True
544 node_tree
= scene
.node_tree
547 image_node
= node_tree
.nodes
.new("CompositorNodeImage")
548 image_node
.name
= name
+ "_image"
549 image_node
.image
= img_spec
.image
550 image_node
.location
= Vector((0, 0))
551 image_node
.frame_start
= img_spec
.frame_start
552 image_node
.frame_offset
= img_spec
.frame_offset
553 image_node
.frame_duration
= img_spec
.frame_duration
555 scale_node
= node_tree
.nodes
.new("CompositorNodeScale")
556 scale_node
.name
= name
+ "_scale"
557 scale_node
.space
= 'RENDER_SIZE'
558 scale_node
.location
= image_node
.location
+ \
559 Vector((image_node
.width
+ 20, 0))
560 scale_node
.show_options
= False
562 cornerpin_node
= node_tree
.nodes
.new("CompositorNodeCornerPin")
563 cornerpin_node
.name
= name
+ "_cornerpin"
564 cornerpin_node
.location
= scale_node
.location
+ \
565 Vector((0, -scale_node
.height
))
567 node_tree
.links
.new(scale_node
.inputs
[0], image_node
.outputs
[0])
568 node_tree
.links
.new(cornerpin_node
.inputs
[0], scale_node
.outputs
[0])
570 # Put all the nodes in a frame for organization
571 frame_node
= group_in_frame(
573 (image_node
, scale_node
, cornerpin_node
)
576 # Position frame at bottom / left
577 position_frame_bottom_left(node_tree
, frame_node
)
580 for corner
in cornerpin_node
.inputs
[1:]:
581 id = corner
.identifier
582 x
= -1 if 'Left' in id else 1
583 y
= -1 if 'Lower' in id else 1
584 drivers
= corner
.driver_add('default_value')
585 for i
, axis_fcurve
in enumerate(drivers
):
586 driver
= axis_fcurve
.driver
587 # Always use the current camera
588 add_driver_prop(driver
, 'camera', 'SCENE', scene
, 'camera')
589 # Track camera location to ensure Deps Graph triggers (not used in the call)
590 add_driver_prop(driver
, 'cam_loc_x', 'OBJECT', scene
.camera
, 'location[0]')
591 # Don't break if the name changes
592 add_driver_prop(driver
, 'name', 'OBJECT', plane
, 'name')
593 driver
.expression
= "import_image__find_plane_corner(name or %s, %d, %d, %d, camera=camera)" % (
597 driver
.type = 'SCRIPTED'
598 driver
.is_valid
= True
599 axis_fcurve
.is_valid
= True
600 driver
.expression
= "%s" % driver
.expression
605 # -----------------------------------------------------------------------------
608 class IMPORT_IMAGE_OT_to_plane(Operator
, AddObjectHelper
):
609 """Create mesh plane(s) from image files with the appropiate aspect ratio"""
611 bl_idname
= "import_image.to_plane"
612 bl_label
= "Import Images as Planes"
613 bl_options
= {'REGISTER', 'PRESET', 'UNDO'}
615 # ----------------------
616 # File dialog properties
617 files
= CollectionProperty(type=bpy
.types
.OperatorFileListElement
, options
={'HIDDEN', 'SKIP_SAVE'})
619 directory
= StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
621 filter_image
= BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
622 filter_movie
= BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
623 filter_folder
= BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
625 # ----------------------
626 # Properties - Importing
627 force_reload
= BoolProperty(
628 name
="Force Reload", default
=False,
629 description
="Force reloading of the image if already opened elsewhere in Blender"
632 image_sequence
= BoolProperty(
633 name
="Animate Image Sequences", default
=False,
634 description
="Import sequentially numbered images as an animated "
635 "image sequence instead of separate planes"
638 # -------------------------------------
639 # Properties - Position and Orientation
640 axis_id_to_vector
= {
641 'X+': Vector(( 1, 0, 0)),
642 'Y+': Vector(( 0, 1, 0)),
643 'Z+': Vector(( 0, 0, 1)),
644 'X-': Vector((-1, 0, 0)),
645 'Y-': Vector(( 0, -1, 0)),
646 'Z-': Vector(( 0, 0, -1)),
649 offset
= BoolProperty(name
="Offset Planes", default
=True, description
="Offset Planes From Each Other")
652 ('X+', "X+", "Side by Side to the Left"),
653 ('Y+', "Y+", "Side by Side, Downward"),
654 ('Z+', "Z+", "Stacked Above"),
655 ('X-', "X-", "Side by Side to the Right"),
656 ('Y-', "Y-", "Side by Side, Upward"),
657 ('Z-', "Z-", "Stacked Below"),
659 offset_axis
= EnumProperty(
660 name
="Orientation", default
='X+', items
=OFFSET_MODES
,
661 description
="How planes are oriented relative to each others' local axis"
664 offset_amount
= FloatProperty(
665 name
="Offset", soft_min
=0, default
=0.1, description
="Space between planes",
666 subtype
='DISTANCE', unit
='LENGTH'
670 ('X+', "X+", "Facing Positive X"),
671 ('Y+', "Y+", "Facing Positive Y"),
672 ('Z+', "Z+ (Up)", "Facing Positive Z"),
673 ('X-', "X-", "Facing Negative X"),
674 ('Y-', "Y-", "Facing Negative Y"),
675 ('Z-', "Z- (Down)", "Facing Negative Z"),
676 ('CAM', "Face Camera", "Facing Camera"),
677 ('CAM_AX', "Main Axis", "Facing the Camera's dominant axis"),
679 align_axis
= EnumProperty(
680 name
="Align", default
='CAM_AX', items
=AXIS_MODES
,
681 description
="How to align the planes"
683 # prev_align_axis is used only by update_size_model
684 prev_align_axis
= EnumProperty(
685 items
=AXIS_MODES
+ (('NONE', '', ''),), default
='NONE', options
={'HIDDEN', 'SKIP_SAVE'})
686 align_track
= BoolProperty(
687 name
="Track Camera", default
=False, description
="Always face the camera"
692 def update_size_mode(self
, context
):
693 """If sizing relative to the camera, always face the camera"""
694 if self
.size_mode
== 'CAMERA':
695 self
.prev_align_axis
= self
.align_axis
696 self
.align_axis
= 'CAM'
698 # if a different alignment was set revert to that when
699 # size mode is changed
700 if self
.prev_align_axis
!= 'NONE':
701 self
.align_axis
= self
.prev_align_axis
702 self
._prev
_align
_axis
= 'NONE'
705 ('ABSOLUTE', "Absolute", "Use absolute size"),
706 ('CAMERA', "Camera Relative", "Scale to the camera frame"),
707 ('DPI', "Dpi", "Use definition of the image as dots per inch"),
708 ('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
710 size_mode
= EnumProperty(
711 name
="Size Mode", default
='ABSOLUTE', items
=SIZE_MODES
,
712 update
=update_size_mode
,
713 description
="How the size of the plane is computed")
716 ('FILL', "Fill", "Fill camera frame, spilling outside the frame"),
717 ('FIT', "Fit", "Fit entire image within the camera frame"),
719 fill_mode
= EnumProperty(name
="Scale", default
='FILL', items
=FILL_MODES
,
720 description
="How large in the camera frame is the plane")
722 height
= FloatProperty(name
="Height", description
="Height of the created plane",
723 default
=1.0, min=0.001, soft_min
=0.001, subtype
='DISTANCE', unit
='LENGTH')
725 factor
= FloatProperty(name
="Definition", min=1.0, default
=600.0,
726 description
="Number of pixels per inch or Blender Unit")
728 # ------------------------------
729 # Properties - Material / Shader
731 ('DIFFUSE', "Diffuse", "Diffuse Shader"),
732 ('SHADELESS', "Shadeless", "Only visible to camera and reflections."),
733 ('EMISSION', "Emit", "Emission Shader"),
735 shader
= EnumProperty(name
="Shader", items
=SHADERS
, default
='DIFFUSE', description
="Node shader to use")
737 emit_strength
= FloatProperty(
738 name
="Strength", min=0.0, default
=1.0, soft_max
=10.0,
739 step
=100, description
="Brightness of Emission Texture")
741 overwrite_material
= BoolProperty(
742 name
="Overwrite Material", default
=True,
743 description
="Overwrite existing Material (based on material name)")
745 compositing_nodes
= BoolProperty(
746 name
="Setup Corner Pin", default
=False,
747 description
="Build Compositor Nodes to reference this image "
748 "without re-rendering")
752 use_transparency
= BoolProperty(
753 name
="Use Alpha", default
=True,
754 description
="Use alphachannel for transparency")
756 t
= bpy
.types
.Image
.bl_rna
.properties
["alpha_mode"]
757 alpha_mode_items
= tuple((e
.identifier
, e
.name
, e
.description
) for e
in t
.enum_items
)
758 alpha_mode
= EnumProperty(
759 name
=t
.name
, items
=alpha_mode_items
, default
=t
.default
,
760 description
=t
.description
)
762 t
= bpy
.types
.Image
.bl_rna
.properties
["use_fields"]
763 use_fields
= BoolProperty(name
=t
.name
, default
=False, description
=t
.description
)
765 t
= bpy
.types
.ImageUser
.bl_rna
.properties
["use_auto_refresh"]
766 use_auto_refresh
= BoolProperty(name
=t
.name
, default
=True, description
=t
.description
)
768 relative
= BoolProperty(name
="Relative Paths", default
=True, description
="Use relative file paths")
772 def draw_import_config(self
, context
):
773 # --- Import Options --- #
777 box
.label("Import Options:", icon
='IMPORT')
779 row
.active
= bpy
.data
.is_saved
780 row
.prop(self
, "relative")
782 box
.prop(self
, "force_reload")
783 box
.prop(self
, "image_sequence")
785 def draw_material_config(self
, context
):
786 # --- Material / Rendering Properties --- #
790 box
.label("Compositing Nodes:", icon
='RENDERLAYERS')
791 box
.prop(self
, 'compositing_nodes')
793 box
.label("Material Settings:", icon
='MATERIAL')
796 row
.prop(self
, 'shader', expand
=True)
797 if self
.shader
== 'EMISSION':
798 box
.prop(self
, 'emit_strength')
800 engine
= context
.scene
.render
.engine
801 if engine
not in ('CYCLES', 'BLENDER_RENDER'):
802 box
.label("%s is not supported" % engine
, icon
='ERROR')
804 box
.prop(self
, 'overwrite_material')
806 box
.label("Texture Settings:", icon
='TEXTURE')
808 row
.prop(self
, "use_transparency")
810 sub
.active
= self
.use_transparency
811 sub
.prop(self
, "alpha_mode", text
="")
812 box
.prop(self
, "use_fields")
813 box
.prop(self
, "use_auto_refresh")
815 def draw_spatial_config(self
, context
):
816 # --- Spatial Properties: Position, Size and Orientation --- #
820 box
.label("Position:", icon
='SNAP_GRID')
821 box
.prop(self
, 'offset')
824 row
.prop(self
, 'offset_axis', expand
=True)
826 row
.prop(self
, 'offset_amount')
827 col
.enabled
= self
.offset
829 box
.label("Plane dimensions:", icon
='ARROW_LEFTRIGHT')
831 row
.prop(self
, "size_mode", expand
=True)
832 if self
.size_mode
== 'ABSOLUTE':
833 box
.prop(self
, "height")
834 elif self
.size_mode
== 'CAMERA':
836 row
.prop(self
, 'fill_mode', expand
=True)
838 box
.prop(self
, "factor")
840 box
.label("Orientation:", icon
='MANIPUL')
842 row
.enabled
= 'CAM' not in self
.size_mode
843 row
.prop(self
, 'align_axis')
845 row
.enabled
= 'CAM' in self
.align_axis
846 row
.alignment
= 'RIGHT'
847 row
.prop(self
, 'align_track')
849 def draw(self
, context
):
851 # Draw configuration sections
852 self
.draw_import_config(context
)
853 self
.draw_material_config(context
)
854 self
.draw_spatial_config(context
)
856 # -------------------------------------------------------------------------
858 def invoke(self
, context
, event
):
859 engine
= context
.scene
.render
.engine
860 if engine
not in ('CYCLES', 'BLENDER_RENDER', 'BLENDER_GAME'):
861 # Use default blender texture, but acknowledge things may not work
862 self
.report({'WARNING'}, "Cannot generate materials for unknown %s render engine" % engine
)
865 context
.window_manager
.fileselect_add(self
)
866 return {'RUNNING_MODAL'}
868 def execute(self
, context
):
869 if not bpy
.data
.is_saved
:
870 self
.relative
= False
872 # this won't work in edit mode
873 editmode
= context
.user_preferences
.edit
.use_enter_edit_mode
874 context
.user_preferences
.edit
.use_enter_edit_mode
= False
875 if context
.active_object
and context
.active_object
.mode
== 'EDIT':
876 bpy
.ops
.object.mode_set(mode
='OBJECT')
878 self
.import_images(context
)
880 context
.user_preferences
.edit
.use_enter_edit_mode
= editmode
884 def import_images(self
, context
):
886 # load images / sequences
887 images
= tuple(load_images(
888 (fn
.name
for fn
in self
.files
),
890 force_reload
=self
.force_reload
,
891 find_sequences
=self
.image_sequence
894 # Create individual planes
895 planes
= [self
.single_image_spec_to_plane(context
, img_spec
) for img_spec
in images
]
897 context
.scene
.update()
899 # Align planes relative to each other
901 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
902 offset_planes(planes
, self
.offset_amount
, offset_axis
)
904 if self
.size_mode
== 'CAMERA' and offset_axis
.z
:
906 x
, y
= compute_camera_size(
907 context
, plane
.location
,
908 self
.fill_mode
, plane
.dimensions
.x
/ plane
.dimensions
.y
)
909 plane
.dimensions
= x
, y
, 0.0
911 # setup new selection
916 self
.report({'INFO'}, "Added {} Image Plane(s)".format(len(planes
)))
918 # operate on a single image
919 def single_image_spec_to_plane(self
, context
, img_spec
):
922 self
.apply_image_options(img_spec
.image
)
925 engine
= context
.scene
.render
.engine
926 if engine
== 'CYCLES':
927 material
= self
.create_cycles_material(context
, img_spec
)
929 tex
= self
.create_image_textures(context
, img_spec
)
930 material
= self
.create_material_for_texture(tex
)
932 # Create and position plane object
933 plane
= self
.create_image_plane(context
, material
.name
, img_spec
)
936 plane
.data
.materials
.append(material
)
937 plane
.data
.uv_textures
[0].data
[0].image
= img_spec
.image
939 # If applicable, setup Corner Pin node
940 if self
.compositing_nodes
:
941 setup_compositing(context
, plane
, img_spec
)
945 def apply_image_options(self
, image
):
946 image
.use_alpha
= self
.use_transparency
947 image
.alpha_mode
= self
.alpha_mode
948 image
.use_fields
= self
.use_fields
951 try: # can't always find the relative path (between drive letters on windows)
952 image
.filepath
= bpy
.path
.relpath(image
.filepath
)
956 def apply_texture_options(self
, texture
, img_spec
):
957 # Shared by both Cycles and Blender Internal
958 image_user
= texture
.image_user
959 image_user
.use_auto_refresh
= self
.use_auto_refresh
960 image_user
.frame_start
= img_spec
.frame_start
961 image_user
.frame_offset
= img_spec
.frame_offset
962 image_user
.frame_duration
= img_spec
.frame_duration
964 # Image sequences need auto refresh to display reliably
965 if img_spec
.image
.source
== 'SEQUENCE':
966 image_user
.use_auto_refresh
= True
968 texture
.extension
= 'CLIP' # Default of "Repeat" can cause artifacts
970 # -------------------------------------------------------------------------
971 # Blender Internal Material
972 def create_image_textures(self
, context
, img_spec
):
973 image
= img_spec
.image
974 fn_full
= os
.path
.normpath(bpy
.path
.abspath(image
.filepath
))
976 # look for texture referencing this file
977 for texture
in bpy
.data
.textures
:
978 if texture
.type == 'IMAGE':
979 tex_img
= texture
.image
980 if (tex_img
is not None) and (tex_img
.library
is None):
981 fn_tex_full
= os
.path
.normpath(bpy
.path
.abspath(tex_img
.filepath
))
982 if fn_full
== fn_tex_full
:
983 if self
.overwrite_material
:
984 self
.apply_texture_options(texture
, img_spec
)
987 # if no texture is found: create one
988 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
989 texture
= bpy
.data
.textures
.new(name
=name_compat
, type='IMAGE')
990 texture
.image
= image
991 self
.apply_texture_options(texture
, img_spec
)
994 def create_material_for_texture(self
, texture
):
995 # look for material with the needed texture
996 for material
in bpy
.data
.materials
:
997 slot
= material
.texture_slots
[0]
998 if slot
and slot
.texture
== texture
:
999 if self
.overwrite_material
:
1000 self
.apply_material_options(material
, slot
)
1003 # if no material found: create one
1004 name_compat
= bpy
.path
.display_name_from_filepath(texture
.image
.filepath
)
1005 material
= bpy
.data
.materials
.new(name
=name_compat
)
1006 slot
= material
.texture_slots
.add()
1007 slot
.texture
= texture
1008 slot
.texture_coords
= 'UV'
1009 self
.apply_material_options(material
, slot
)
1012 def apply_material_options(self
, material
, slot
):
1013 shader
= self
.shader
1015 if self
.use_transparency
:
1016 material
.alpha
= 0.0
1017 material
.specular_alpha
= 0.0
1018 slot
.use_map_alpha
= True
1020 material
.alpha
= 1.0
1021 material
.specular_alpha
= 1.0
1022 slot
.use_map_alpha
= False
1024 material
.specular_intensity
= 0
1025 material
.diffuse_intensity
= 1.0
1026 material
.use_transparency
= self
.use_transparency
1027 material
.transparency_method
= 'Z_TRANSPARENCY'
1028 material
.use_shadeless
= (shader
== 'SHADELESS')
1029 material
.use_transparent_shadows
= (shader
== 'DIFFUSE')
1030 material
.emit
= self
.emit_strength
if shader
== 'EMISSION' else 0.0
1032 # -------------------------------------------------------------------------
1034 def create_cycles_texnode(self
, context
, node_tree
, img_spec
):
1035 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
1036 tex_image
.image
= img_spec
.image
1037 tex_image
.show_texture
= True
1038 self
.apply_texture_options(tex_image
, img_spec
)
1041 def create_cycles_material(self
, context
, img_spec
):
1042 image
= img_spec
.image
1043 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
1045 if self
.overwrite_material
:
1046 for mat
in bpy
.data
.materials
:
1047 if mat
.name
== name_compat
:
1050 material
= bpy
.data
.materials
.new(name
=name_compat
)
1052 material
.use_nodes
= True
1053 node_tree
= material
.node_tree
1054 out_node
= clean_node_tree(node_tree
)
1056 tex_image
= self
.create_cycles_texnode(context
, node_tree
, img_spec
)
1058 if self
.shader
== 'DIFFUSE':
1059 core_shader
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
1060 elif self
.shader
== 'SHADELESS':
1061 core_shader
= get_shadeless_node(node_tree
)
1062 else: # Emission Shading
1063 core_shader
= node_tree
.nodes
.new('ShaderNodeEmission')
1064 core_shader
.inputs
[1].default_value
= self
.emit_strength
1066 # Connect color from texture
1067 node_tree
.links
.new(core_shader
.inputs
[0], tex_image
.outputs
[0])
1069 if self
.use_transparency
:
1070 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
1072 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
1073 node_tree
.links
.new(mix_shader
.inputs
[0], tex_image
.outputs
[1])
1074 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
[0])
1075 node_tree
.links
.new(mix_shader
.inputs
[2], core_shader
.outputs
[0])
1076 core_shader
= mix_shader
1078 node_tree
.links
.new(out_node
.inputs
[0], core_shader
.outputs
[0])
1080 auto_align_nodes(node_tree
)
1083 # -------------------------------------------------------------------------
1085 def create_image_plane(self
, context
, name
, img_spec
):
1087 width
, height
= self
.compute_plane_size(context
, img_spec
)
1090 bpy
.ops
.mesh
.primitive_plane_add('INVOKE_REGION_WIN')
1091 plane
= context
.scene
.objects
.active
1092 # Why does mesh.primitive_plane_add leave the object in edit mode???
1093 if plane
.mode
is not 'OBJECT':
1094 bpy
.ops
.object.mode_set(mode
='OBJECT')
1095 plane
.dimensions
= width
, height
, 0.0
1096 plane
.data
.name
= plane
.name
= name
1097 bpy
.ops
.object.transform_apply(scale
=True)
1098 plane
.data
.uv_textures
.new()
1100 # If sizing for camera, also insert into the camera's field of view
1101 if self
.size_mode
== 'CAMERA':
1102 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
1103 translate_axis
= [0 if offset_axis
[i
] else 1 for i
in (0, 1)]
1104 center_in_camera(context
.scene
, context
.scene
.camera
, plane
, translate_axis
)
1106 self
.align_plane(context
, plane
)
1110 def compute_plane_size(self
, context
, img_spec
):
1111 """Given the image size in pixels and location, determine size of plane"""
1112 px
, py
= img_spec
.size
1115 if px
== 0 or py
== 0:
1118 if self
.size_mode
== 'ABSOLUTE':
1122 elif self
.size_mode
== 'CAMERA':
1123 x
, y
= compute_camera_size(
1124 context
, context
.scene
.cursor_location
,
1125 self
.fill_mode
, px
/ py
1128 elif self
.size_mode
== 'DPI':
1129 fact
= 1 / self
.factor
/ context
.scene
.unit_settings
.scale_length
* 0.0254
1133 else: # elif self.size_mode == 'DPBU'
1134 fact
= 1 / self
.factor
1140 def align_plane(self
, context
, plane
):
1141 """Pick an axis and align the plane to it"""
1142 if 'CAM' in self
.align_axis
:
1144 camera
= context
.scene
.camera
1146 # Find the axis that best corresponds to the camera's view direction
1147 axis
= camera
.matrix_world
* \
1148 Vector((0, 0, 1)) - camera
.matrix_world
.col
[3].xyz
1149 # pick the axis with the greatest magnitude
1150 mag
= max(map(abs, axis
))
1151 # And use that axis & direction
1153 n
/ mag
if abs(n
) == mag
else 0.0
1157 # No camera? Just face Z axis
1158 axis
= Vector((0, 0, 1))
1159 self
.align_axis
= 'Z+'
1162 axis
= self
.axis_id_to_vector
[self
.align_axis
]
1164 # rotate accodingly for x/y axiis
1166 plane
.rotation_euler
.x
= pi
/ 2
1169 plane
.rotation_euler
.z
= pi
1171 plane
.rotation_euler
.z
= 0
1173 plane
.rotation_euler
.z
= pi
/ 2
1175 plane
.rotation_euler
.z
= -pi
/ 2
1177 # or flip 180 degrees for negative z
1179 plane
.rotation_euler
.y
= pi
1181 if self
.align_axis
== 'CAM':
1182 constraint
= plane
.constraints
.new('COPY_ROTATION')
1183 constraint
.target
= camera
1184 constraint
.use_x
= constraint
.use_y
= constraint
.use_z
= True
1185 if not self
.align_track
:
1186 bpy
.ops
.object.visual_transform_apply()
1187 plane
.constraints
.clear()
1189 if self
.align_axis
== 'CAM_AX' and self
.align_track
:
1190 constraint
= plane
.constraints
.new('LOCKED_TRACK')
1191 constraint
.target
= camera
1192 constraint
.track_axis
= 'TRACK_Z'
1193 constraint
.lock_axis
= 'LOCK_Y'
1196 # -----------------------------------------------------------------------------
1199 def import_images_button(self
, context
):
1200 self
.layout
.operator(IMPORT_IMAGE_OT_to_plane
.bl_idname
, text
="Images as Planes", icon
='TEXTURE')
1204 IMPORT_IMAGE_OT_to_plane
,
1210 bpy
.utils
.register_class(cls
)
1212 bpy
.types
.INFO_MT_file_import
.append(import_images_button
)
1213 bpy
.types
.INFO_MT_mesh_add
.append(import_images_button
)
1215 bpy
.app
.handlers
.load_post
.append(register_driver
)
1220 bpy
.types
.INFO_MT_file_import
.remove(import_images_button
)
1221 bpy
.types
.INFO_MT_mesh_add
.remove(import_images_button
)
1223 # This will only exist if drivers are active
1224 if check_drivers
in bpy
.app
.handlers
.scene_update_post
:
1225 bpy
.app
.handlers
.scene_update_post
.remove(check_drivers
)
1227 bpy
.app
.handlers
.load_post
.remove(register_driver
)
1228 del bpy
.app
.driver_namespace
['import_image__find_plane_corner']
1231 bpy
.utils
.unregister_class(cls
)
1234 if __name__
== "__main__":
1235 # Run simple doc tests