Fix T52833: OBJ triangulate doesn't match viewport
[blender-addons.git] / io_import_images_as_planes.py
blobb5099da0a232d8477d4367cbda56df3ae78da3dc
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 # Game Engine Material Settings
933 material.game_settings.use_backface_culling = False
934 material.game_settings.alpha_blend = 'ALPHA'
936 # Create and position plane object
937 plane = self.create_image_plane(context, material.name, img_spec)
939 # Assign Material
940 plane.data.materials.append(material)
941 plane.data.uv_textures[0].data[0].image = img_spec.image
943 # If applicable, setup Corner Pin node
944 if self.compositing_nodes:
945 setup_compositing(context, plane, img_spec)
947 return plane
949 def apply_image_options(self, image):
950 image.use_alpha = self.use_transparency
951 image.alpha_mode = self.alpha_mode
952 image.use_fields = self.use_fields
954 if self.relative:
955 try: # can't always find the relative path (between drive letters on windows)
956 image.filepath = bpy.path.relpath(image.filepath)
957 except ValueError:
958 pass
960 def apply_texture_options(self, texture, img_spec):
961 # Shared by both Cycles and Blender Internal
962 image_user = texture.image_user
963 image_user.use_auto_refresh = self.use_auto_refresh
964 image_user.frame_start = img_spec.frame_start
965 image_user.frame_offset = img_spec.frame_offset
966 image_user.frame_duration = img_spec.frame_duration
968 # Image sequences need auto refresh to display reliably
969 if img_spec.image.source == 'SEQUENCE':
970 image_user.use_auto_refresh = True
972 texture.extension = 'CLIP' # Default of "Repeat" can cause artifacts
974 # -------------------------------------------------------------------------
975 # Blender Internal Material
976 def create_image_textures(self, context, img_spec):
977 image = img_spec.image
978 fn_full = os.path.normpath(bpy.path.abspath(image.filepath))
980 # look for texture referencing this file
981 for texture in bpy.data.textures:
982 if texture.type == 'IMAGE':
983 tex_img = texture.image
984 if (tex_img is not None) and (tex_img.library is None):
985 fn_tex_full = os.path.normpath(bpy.path.abspath(tex_img.filepath))
986 if fn_full == fn_tex_full:
987 if self.overwrite_material:
988 self.apply_texture_options(texture, img_spec)
989 return texture
991 # if no texture is found: create one
992 name_compat = bpy.path.display_name_from_filepath(image.filepath)
993 texture = bpy.data.textures.new(name=name_compat, type='IMAGE')
994 texture.image = image
995 self.apply_texture_options(texture, img_spec)
996 return texture
998 def create_material_for_texture(self, texture):
999 # look for material with the needed texture
1000 for material in bpy.data.materials:
1001 slot = material.texture_slots[0]
1002 if slot and slot.texture == texture:
1003 if self.overwrite_material:
1004 self.apply_material_options(material, slot)
1005 return material
1007 # if no material found: create one
1008 name_compat = bpy.path.display_name_from_filepath(texture.image.filepath)
1009 material = bpy.data.materials.new(name=name_compat)
1010 slot = material.texture_slots.add()
1011 slot.texture = texture
1012 slot.texture_coords = 'UV'
1013 self.apply_material_options(material, slot)
1014 return material
1016 def apply_material_options(self, material, slot):
1017 shader = self.shader
1019 if self.use_transparency:
1020 material.alpha = 0.0
1021 material.specular_alpha = 0.0
1022 slot.use_map_alpha = True
1023 else:
1024 material.alpha = 1.0
1025 material.specular_alpha = 1.0
1026 slot.use_map_alpha = False
1028 material.specular_intensity = 0
1029 material.diffuse_intensity = 1.0
1030 material.use_transparency = self.use_transparency
1031 material.transparency_method = 'Z_TRANSPARENCY'
1032 material.use_shadeless = (shader == 'SHADELESS')
1033 material.use_transparent_shadows = (shader == 'DIFFUSE')
1034 material.emit = self.emit_strength if shader == 'EMISSION' else 0.0
1036 # -------------------------------------------------------------------------
1037 # Cycles
1038 def create_cycles_texnode(self, context, node_tree, img_spec):
1039 tex_image = node_tree.nodes.new('ShaderNodeTexImage')
1040 tex_image.image = img_spec.image
1041 tex_image.show_texture = True
1042 self.apply_texture_options(tex_image, img_spec)
1043 return tex_image
1045 def create_cycles_material(self, context, img_spec):
1046 image = img_spec.image
1047 name_compat = bpy.path.display_name_from_filepath(image.filepath)
1048 material = None
1049 if self.overwrite_material:
1050 for mat in bpy.data.materials:
1051 if mat.name == name_compat:
1052 material = mat
1053 if not material:
1054 material = bpy.data.materials.new(name=name_compat)
1056 material.use_nodes = True
1057 node_tree = material.node_tree
1058 out_node = clean_node_tree(node_tree)
1060 tex_image = self.create_cycles_texnode(context, node_tree, img_spec)
1062 if self.shader == 'DIFFUSE':
1063 core_shader = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
1064 elif self.shader == 'SHADELESS':
1065 core_shader = get_shadeless_node(node_tree)
1066 else: # Emission Shading
1067 core_shader = node_tree.nodes.new('ShaderNodeEmission')
1068 core_shader.inputs[1].default_value = self.emit_strength
1070 # Connect color from texture
1071 node_tree.links.new(core_shader.inputs[0], tex_image.outputs[0])
1073 if self.use_transparency:
1074 bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
1076 mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
1077 node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
1078 node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
1079 node_tree.links.new(mix_shader.inputs[2], core_shader.outputs[0])
1080 core_shader = mix_shader
1082 node_tree.links.new(out_node.inputs[0], core_shader.outputs[0])
1084 auto_align_nodes(node_tree)
1085 return material
1087 # -------------------------------------------------------------------------
1088 # Geometry Creation
1089 def create_image_plane(self, context, name, img_spec):
1091 width, height = self.compute_plane_size(context, img_spec)
1093 # Create new mesh
1094 bpy.ops.mesh.primitive_plane_add('INVOKE_REGION_WIN')
1095 plane = context.scene.objects.active
1096 # Why does mesh.primitive_plane_add leave the object in edit mode???
1097 if plane.mode is not 'OBJECT':
1098 bpy.ops.object.mode_set(mode='OBJECT')
1099 plane.dimensions = width, height, 0.0
1100 plane.data.name = plane.name = name
1101 bpy.ops.object.transform_apply(scale=True)
1102 plane.data.uv_textures.new()
1104 # If sizing for camera, also insert into the camera's field of view
1105 if self.size_mode == 'CAMERA':
1106 offset_axis = self.axis_id_to_vector[self.offset_axis]
1107 translate_axis = [0 if offset_axis[i] else 1 for i in (0, 1)]
1108 center_in_camera(context.scene, context.scene.camera, plane, translate_axis)
1110 self.align_plane(context, plane)
1112 return plane
1114 def compute_plane_size(self, context, img_spec):
1115 """Given the image size in pixels and location, determine size of plane"""
1116 px, py = img_spec.size
1118 # can't load data
1119 if px == 0 or py == 0:
1120 px = py = 1
1122 if self.size_mode == 'ABSOLUTE':
1123 y = self.height
1124 x = px / py * y
1126 elif self.size_mode == 'CAMERA':
1127 x, y = compute_camera_size(
1128 context, context.scene.cursor_location,
1129 self.fill_mode, px / py
1132 elif self.size_mode == 'DPI':
1133 fact = 1 / self.factor / context.scene.unit_settings.scale_length * 0.0254
1134 x = px * fact
1135 y = py * fact
1137 else: # elif self.size_mode == 'DPBU'
1138 fact = 1 / self.factor
1139 x = px * fact
1140 y = py * fact
1142 return x, y
1144 def align_plane(self, context, plane):
1145 """Pick an axis and align the plane to it"""
1146 if 'CAM' in self.align_axis:
1147 # Camera-aligned
1148 camera = context.scene.camera
1149 if (camera):
1150 # Find the axis that best corresponds to the camera's view direction
1151 axis = camera.matrix_world * \
1152 Vector((0, 0, 1)) - camera.matrix_world.col[3].xyz
1153 # pick the axis with the greatest magnitude
1154 mag = max(map(abs, axis))
1155 # And use that axis & direction
1156 axis = Vector([
1157 n / mag if abs(n) == mag else 0.0
1158 for n in axis
1160 else:
1161 # No camera? Just face Z axis
1162 axis = Vector((0, 0, 1))
1163 self.align_axis = 'Z+'
1164 else:
1165 # Axis-aligned
1166 axis = self.axis_id_to_vector[self.align_axis]
1168 # rotate accodingly for x/y axiis
1169 if not axis.z:
1170 plane.rotation_euler.x = pi / 2
1172 if axis.y > 0:
1173 plane.rotation_euler.z = pi
1174 elif axis.y < 0:
1175 plane.rotation_euler.z = 0
1176 elif axis.x > 0:
1177 plane.rotation_euler.z = pi / 2
1178 elif axis.x < 0:
1179 plane.rotation_euler.z = -pi / 2
1181 # or flip 180 degrees for negative z
1182 elif axis.z < 0:
1183 plane.rotation_euler.y = pi
1185 if self.align_axis == 'CAM':
1186 constraint = plane.constraints.new('COPY_ROTATION')
1187 constraint.target = camera
1188 constraint.use_x = constraint.use_y = constraint.use_z = True
1189 if not self.align_track:
1190 bpy.ops.object.visual_transform_apply()
1191 plane.constraints.clear()
1193 if self.align_axis == 'CAM_AX' and self.align_track:
1194 constraint = plane.constraints.new('LOCKED_TRACK')
1195 constraint.target = camera
1196 constraint.track_axis = 'TRACK_Z'
1197 constraint.lock_axis = 'LOCK_Y'
1200 # -----------------------------------------------------------------------------
1201 # Register
1203 def import_images_button(self, context):
1204 self.layout.operator(IMPORT_IMAGE_OT_to_plane.bl_idname, text="Images as Planes", icon='TEXTURE')
1207 classes = (
1208 IMPORT_IMAGE_OT_to_plane,
1212 def register():
1213 for cls in classes:
1214 bpy.utils.register_class(cls)
1216 bpy.types.INFO_MT_file_import.append(import_images_button)
1217 bpy.types.INFO_MT_mesh_add.append(import_images_button)
1219 bpy.app.handlers.load_post.append(register_driver)
1220 register_driver()
1223 def unregister():
1224 bpy.types.INFO_MT_file_import.remove(import_images_button)
1225 bpy.types.INFO_MT_mesh_add.remove(import_images_button)
1227 # This will only exist if drivers are active
1228 if check_drivers in bpy.app.handlers.scene_update_post:
1229 bpy.app.handlers.scene_update_post.remove(check_drivers)
1231 bpy.app.handlers.load_post.remove(register_driver)
1232 del bpy.app.driver_namespace['import_image__find_plane_corner']
1234 for cls in classes:
1235 bpy.utils.unregister_class(cls)
1238 if __name__ == "__main__":
1239 # Run simple doc tests
1240 import doctest
1241 doctest.testmod()
1243 unregister()
1244 register()