Merge branch 'master' into blender2.8
[blender-addons.git] / io_import_images_as_planes.py
blob292d41582f670c7a0fa019bc641f49c6ee4e1e6b
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 #####
19 # <pep8 compliant>
21 bl_info = {
22 "name": "Import Images as Planes",
23 "author": "Florian Meyer (tstscr), mont29, matali, Ted Schundler (SpkyElctrc)",
24 "version": (3, 1, 1),
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.",
29 "warning": "",
30 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
31 "Scripts/Add_Mesh/Planes_from_Images",
32 "category": "Import-Export",
35 import os
36 import warnings
37 import re
38 from itertools import count, repeat
39 from collections import namedtuple
40 from math import pi
42 import bpy
43 from bpy.types import Operator
44 from mathutils import Vector
46 from bpy.props import (
47 StringProperty,
48 BoolProperty,
49 EnumProperty,
50 FloatProperty,
51 CollectionProperty,
54 from bpy_extras.object_utils import (
55 AddObjectHelper,
56 world_to_camera_view,
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 # -----------------------------------------------------------------------------
68 # Misc utils.
70 def add_driver_prop(driver, name, type, id, path):
71 """Configure a new driver variable."""
72 dv = driver.variables.new()
73 dv.name = name
74 dv.type = 'SINGLE_PROP'
75 target = dv.targets[0]
76 target.id_type = type
77 target.id = id
78 target.data_path = path
81 # -----------------------------------------------------------------------------
82 # Image loading
84 ImageSpec = namedtuple(
85 'ImageSpec',
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",
101 ... "blaah"]))
102 [('blaah', 1, 1), ('test2-001.jp2', 1, 2), ('test3-003.jp2', 3, 4)]
105 files = iter(sorted(files))
106 prev_file = None
107 pattern = ""
108 matches = []
109 segment = None
110 length = 1
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
118 if segment is None:
119 for i, prev, cur in zip(count(), matches, new_matches):
120 if prev != cur:
121 segment = i
122 break
124 # did it only change by one?
125 for i, prev, cur in zip(count(), matches, new_matches):
126 if i == segment:
127 # We expect this to increment
128 prev = prev + length
129 if prev != cur:
130 break
132 # All good!
133 else:
134 length += 1
135 continue
137 # No continuation -> spit out what we found and reset counters
138 if prev_file:
139 if length > 1:
140 yield prev_file, matches[segment], length
141 else:
142 yield prev_file, 1, 1
144 prev_file = filename
145 matches = new_matches
146 pattern = new_pattern
147 segment = None
148 length = 1
150 if prev_file:
151 if length > 1:
152 yield prev_file, matches[segment], length
153 else:
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)
165 else:
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':
175 # Blender BPY BUG!
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.
200 prior = planes[0]
201 offset = Vector()
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
209 prior = current
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)
217 frame_size = \
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
225 else:
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
241 return frame_size
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 # -----------------------------------------------------------------------------
265 # Cycles utils
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!).
272 sorted_nodes = []
273 done_nodes = set()
274 for socket in node.inputs:
275 done_links = set()
276 for link in input_links:
277 nd = link.from_node
278 if nd in done_nodes:
279 # Node already treated!
280 done_links.add(link)
281 elif link.to_socket == socket:
282 sorted_nodes.append(nd)
283 done_links.add(link)
284 done_nodes.add(nd)
285 input_links -= done_links
286 return sorted_nodes
289 def auto_align_nodes(node_tree):
290 """Given a shader node tree, arrange nodes neatly relative to the output node."""
291 x_gap = 200
292 y_gap = 180
293 nodes = node_tree.nodes
294 links = node_tree.links
295 output_node = None
296 for node in nodes:
297 if node.type == 'OUTPUT_MATERIAL' or node.type == 'GROUP_OUTPUT':
298 output_node = node
299 break
301 else: # Just in case there is no output
302 return
304 def align(to_node):
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))
311 align(node)
313 align(output_node)
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':
324 nodes.remove(node)
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"""
331 try:
332 node_tree = bpy.data.node_groups['IAP_SHADELESS']
334 except KeyError:
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
404 return group_node
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)
423 return
425 update = False
426 for name, matrix in list(watched_objects.items()):
427 try:
428 obj = bpy.data.objects[name]
429 except KeyError:
430 # The user must have removed this object
431 del watched_objects[name]
432 else:
433 new_matrix = tuple(map(tuple, obj.matrix_world)).__hash__()
434 if new_matrix != matrix:
435 watched_objects[name] = new_matrix
436 update = True
438 if update:
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"""
445 name = obj.name
447 # known object? -> we're done
448 if name in watched_objects:
449 return
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"""
460 if args or kwargs:
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()
475 v.x *= x / scale.x
476 v.y *= y / scale.y
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()
502 for node in nodes:
503 top_left = node.location
504 bottom_right = top_left + Vector((node.width, -node.height))
506 for i in (0, 1):
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
516 return frame_node
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
545 name = plane.name
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(
572 node_tree, name,
573 (image_node, scale_node, cornerpin_node)
576 # Position frame at bottom / left
577 position_frame_bottom_left(node_tree, frame_node)
579 # Configure Drivers
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)" % (
594 repr(plane.name),
595 x, y, i
597 driver.type = 'SCRIPTED'
598 driver.is_valid = True
599 axis_fcurve.is_valid = True
600 driver.expression = "%s" % driver.expression
602 scene.update()
605 # -----------------------------------------------------------------------------
606 # Operator
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")
651 OFFSET_MODES = (
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'
669 AXIS_MODES = (
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"
690 # -----------------
691 # Properties - Size
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'
697 else:
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'
704 SIZE_MODES = (
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")
715 FILL_MODES = (
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
730 SHADERS = (
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")
750 # ------------------
751 # Properties - Image
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")
770 # -------
771 # Draw UI
772 def draw_import_config(self, context):
773 # --- Import Options --- #
774 layout = self.layout
775 box = layout.box()
777 box.label("Import Options:", icon='IMPORT')
778 row = box.row()
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 --- #
787 layout = self.layout
788 box = layout.box()
790 box.label("Compositing Nodes:", icon='RENDERLAYERS')
791 box.prop(self, 'compositing_nodes')
793 box.label("Material Settings:", icon='MATERIAL')
795 row = box.row()
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')
807 row = box.row()
808 row.prop(self, "use_transparency")
809 sub = row.row()
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 --- #
817 layout = self.layout
818 box = layout.box()
820 box.label("Position:", icon='SNAP_GRID')
821 box.prop(self, 'offset')
822 col = box.column()
823 row = col.row()
824 row.prop(self, 'offset_axis', expand=True)
825 row = col.row()
826 row.prop(self, 'offset_amount')
827 col.enabled = self.offset
829 box.label("Plane dimensions:", icon='ARROW_LEFTRIGHT')
830 row = box.row()
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':
835 row = box.row()
836 row.prop(self, 'fill_mode', expand=True)
837 else:
838 box.prop(self, "factor")
840 box.label("Orientation:", icon='MANIPUL')
841 row = box.row()
842 row.enabled = 'CAM' not in self.size_mode
843 row.prop(self, 'align_axis')
844 row = box.row()
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 # -------------------------------------------------------------------------
857 # Core functionality
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)
864 # Open file browser
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
882 return {'FINISHED'}
884 def import_images(self, context):
886 # load images / sequences
887 images = tuple(load_images(
888 (fn.name for fn in self.files),
889 self.directory,
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
900 if self.offset:
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:
905 for plane in planes:
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
912 for plane in planes:
913 plane.select = True
915 # all done!
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):
921 # Configure image
922 self.apply_image_options(img_spec.image)
924 # Configure material
925 engine = context.scene.render.engine
926 if engine == 'CYCLES':
927 material = self.create_cycles_material(context, img_spec)
928 else:
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)
935 # Assign Material
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)
943 return plane
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
950 if self.relative:
951 try: # can't always find the relative path (between drive letters on windows)
952 image.filepath = bpy.path.relpath(image.filepath)
953 except ValueError:
954 pass
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)
985 return texture
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)
992 return texture
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)
1001 return material
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)
1010 return material
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
1019 else:
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 # -------------------------------------------------------------------------
1033 # Cycles
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)
1039 return tex_image
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)
1044 material = None
1045 if self.overwrite_material:
1046 for mat in bpy.data.materials:
1047 if mat.name == name_compat:
1048 material = mat
1049 if not material:
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)
1081 return material
1083 # -------------------------------------------------------------------------
1084 # Geometry Creation
1085 def create_image_plane(self, context, name, img_spec):
1087 width, height = self.compute_plane_size(context, img_spec)
1089 # Create new mesh
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)
1108 return 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
1114 # can't load data
1115 if px == 0 or py == 0:
1116 px = py = 1
1118 if self.size_mode == 'ABSOLUTE':
1119 y = self.height
1120 x = px / py * y
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
1130 x = px * fact
1131 y = py * fact
1133 else: # elif self.size_mode == 'DPBU'
1134 fact = 1 / self.factor
1135 x = px * fact
1136 y = py * fact
1138 return x, y
1140 def align_plane(self, context, plane):
1141 """Pick an axis and align the plane to it"""
1142 if 'CAM' in self.align_axis:
1143 # Camera-aligned
1144 camera = context.scene.camera
1145 if (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
1152 axis = Vector([
1153 n / mag if abs(n) == mag else 0.0
1154 for n in axis
1156 else:
1157 # No camera? Just face Z axis
1158 axis = Vector((0, 0, 1))
1159 self.align_axis = 'Z+'
1160 else:
1161 # Axis-aligned
1162 axis = self.axis_id_to_vector[self.align_axis]
1164 # rotate accodingly for x/y axiis
1165 if not axis.z:
1166 plane.rotation_euler.x = pi / 2
1168 if axis.y > 0:
1169 plane.rotation_euler.z = pi
1170 elif axis.y < 0:
1171 plane.rotation_euler.z = 0
1172 elif axis.x > 0:
1173 plane.rotation_euler.z = pi / 2
1174 elif axis.x < 0:
1175 plane.rotation_euler.z = -pi / 2
1177 # or flip 180 degrees for negative z
1178 elif axis.z < 0:
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 # -----------------------------------------------------------------------------
1197 # Register
1199 def import_images_button(self, context):
1200 self.layout.operator(IMPORT_IMAGE_OT_to_plane.bl_idname, text="Images as Planes", icon='TEXTURE')
1203 classes = (
1204 IMPORT_IMAGE_OT_to_plane,
1208 def register():
1209 for cls in classes:
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)
1216 register_driver()
1219 def unregister():
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']
1230 for cls in classes:
1231 bpy.utils.unregister_class(cls)
1234 if __name__ == "__main__":
1235 # Run simple doc tests
1236 import doctest
1237 doctest.testmod()
1239 unregister()
1240 register()