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, 80, 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 "support": 'OFFICIAL',
33 "category": "Import-Export",
39 from itertools
import count
, repeat
40 from collections
import namedtuple
44 from bpy
.types
import Operator
45 from mathutils
import Vector
47 from bpy
.props
import (
55 from bpy_extras
.object_utils
import (
60 from bpy_extras
.image_utils
import load_image
62 # -----------------------------------------------------------------------------
63 # Module-level Shared State
65 watched_objects
= {} # used to trigger compositor updates on scene updates
68 # -----------------------------------------------------------------------------
71 def add_driver_prop(driver
, name
, type, id, path
):
72 """Configure a new driver variable."""
73 dv
= driver
.variables
.new()
75 dv
.type = 'SINGLE_PROP'
76 target
= dv
.targets
[0]
79 target
.data_path
= path
82 # -----------------------------------------------------------------------------
85 ImageSpec
= namedtuple(
87 ['image', 'size', 'frame_start', 'frame_offset', 'frame_duration'])
89 num_regex
= re
.compile('[0-9]') # Find a single number
90 nums_regex
= re
.compile('[0-9]+') # Find a set of numbers
93 def find_image_sequences(files
):
94 """From a group of files, detect image sequences.
96 This returns a generator of tuples, which contain the filename,
97 start frame, and length of the detected sequence
99 >>> list(find_image_sequences([
100 ... "test2-001.jp2", "test2-002.jp2",
101 ... "test3-003.jp2", "test3-004.jp2", "test3-005.jp2", "test3-006.jp2",
103 [('blaah', 1, 1), ('test2-001.jp2', 1, 2), ('test3-003.jp2', 3, 4)]
106 files
= iter(sorted(files
))
112 for filename
in files
:
113 new_pattern
= num_regex
.sub('#', filename
)
114 new_matches
= list(map(int, nums_regex
.findall(filename
)))
115 if new_pattern
== pattern
:
116 # this file looks like it may be in sequence from the previous
118 # if there are multiple sets of numbers, figure out what changed
120 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
125 # did it only change by one?
126 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
128 # We expect this to increment
138 # No continuation -> spit out what we found and reset counters
141 yield prev_file
, matches
[segment
], length
143 yield prev_file
, 1, 1
146 matches
= new_matches
147 pattern
= new_pattern
153 yield prev_file
, matches
[segment
], length
155 yield prev_file
, 1, 1
158 def load_images(filenames
, directory
, force_reload
=False, frame_start
=1, find_sequences
=False):
159 """Wrapper for bpy's load_image
161 Loads a set of images, movies, or even image sequences
162 Returns a generator of ImageSpec wrapper objects later used for texture setup
164 if find_sequences
: # if finding sequences, we need some pre-processing first
165 file_iter
= find_image_sequences(filenames
)
167 file_iter
= zip(filenames
, repeat(1), repeat(1))
169 for filename
, offset
, frames
in file_iter
:
170 image
= load_image(filename
, directory
, check_existing
=True, force_reload
=force_reload
)
172 # Size is unavailable for sequences, so we grab it early
173 size
= tuple(image
.size
)
175 if image
.source
== 'MOVIE':
177 # This number is only valid when read a second time in 2.77
178 # This repeated line is not a mistake
179 frames
= image
.frame_duration
180 frames
= image
.frame_duration
182 elif frames
> 1: # Not movie, but multiple frames -> image sequence
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:]:
203 local_offset
= abs((prior
.dimensions
+ current
.dimensions
).dot(axis
)) / 2.0 + gap
205 offset
+= local_offset
* axis
206 current
.location
= current
.matrix_world
@ offset
211 def compute_camera_size(context
, center
, fill_mode
, aspect
):
212 """Determine how large an object needs to be to fit or fill the camera's field of view."""
213 scene
= context
.scene
214 camera
= scene
.camera
215 view_frame
= camera
.data
.view_frame(scene
=scene
)
217 Vector([max(v
[i
] for v
in view_frame
) for i
in range(3)]) - \
218 Vector([min(v
[i
] for v
in view_frame
) for i
in range(3)])
219 camera_aspect
= frame_size
.x
/ frame_size
.y
221 # Convert the frame size to the correct sizing at a given distance
222 if camera
.type == 'ORTHO':
223 frame_size
= frame_size
.xy
225 # Perspective transform
226 distance
= world_to_camera_view(scene
, camera
, center
).z
227 frame_size
= distance
* frame_size
.xy
/ (-view_frame
[0].z
)
229 # Determine what axis to match to the camera
230 match_axis
= 0 # match the Y axis size
231 match_aspect
= aspect
232 if (fill_mode
== 'FILL' and aspect
> camera_aspect
) or \
233 (fill_mode
== 'FIT' and aspect
< camera_aspect
):
234 match_axis
= 1 # match the X axis size
235 match_aspect
= 1.0 / aspect
237 # scale the other axis to the correct aspect
238 frame_size
[1 - match_axis
] = frame_size
[match_axis
] / match_aspect
243 def center_in_camera(scene
, camera
, obj
, axis
=(1, 1)):
244 """Center object along specified axis of the camera"""
245 camera_matrix_col
= camera
.matrix_world
.col
246 location
= obj
.location
248 # Vector from the camera's world coordinate center to the object's center
249 delta
= camera_matrix_col
[3].xyz
- location
251 # How far off center we are along the camera's local X
252 camera_x_mag
= delta
.dot(camera_matrix_col
[0].xyz
) * axis
[0]
253 # How far off center we are along the camera's local Y
254 camera_y_mag
= delta
.dot(camera_matrix_col
[1].xyz
) * axis
[1]
256 # Now offset only along camera local axis
257 offset
= camera_matrix_col
[0].xyz
* camera_x_mag
+ \
258 camera_matrix_col
[1].xyz
* camera_y_mag
260 obj
.location
= location
+ offset
263 # -----------------------------------------------------------------------------
266 def get_input_nodes(node
, links
):
267 """Get nodes that are a inputs to the given node"""
268 # Get all links going to node.
269 input_links
= {lnk
for lnk
in links
if lnk
.to_node
== node
}
270 # Sort those links, get their input nodes (and avoid doubles!).
273 for socket
in node
.inputs
:
275 for link
in input_links
:
278 # Node already treated!
280 elif link
.to_socket
== socket
:
281 sorted_nodes
.append(nd
)
284 input_links
-= done_links
288 def auto_align_nodes(node_tree
):
289 """Given a shader node tree, arrange nodes neatly relative to the output node."""
292 nodes
= node_tree
.nodes
293 links
= node_tree
.links
296 if node
.type == 'OUTPUT_MATERIAL' or node
.type == 'GROUP_OUTPUT':
300 else: # Just in case there is no output
304 from_nodes
= get_input_nodes(to_node
, links
)
305 for i
, node
in enumerate(from_nodes
):
306 node
.location
.x
= min(node
.location
.x
, to_node
.location
.x
- x_gap
)
307 node
.location
.y
= to_node
.location
.y
308 node
.location
.y
-= i
* y_gap
309 node
.location
.y
+= (len(from_nodes
) - 1) * y_gap
/ (len(from_nodes
))
315 def clean_node_tree(node_tree
):
316 """Clear all nodes in a shader node tree except the output.
318 Returns the output node
320 nodes
= node_tree
.nodes
321 for node
in list(nodes
): # copy to avoid altering the loop's data source
322 if not node
.type == 'OUTPUT_MATERIAL':
325 return node_tree
.nodes
[0]
328 def get_shadeless_node(dest_node_tree
):
329 """Return a "shadless" cycles/eevee node, creating a node group if nonexistent"""
331 node_tree
= bpy
.data
.node_groups
['IAP_SHADELESS']
334 # need to build node shadeless node group
335 node_tree
= bpy
.data
.node_groups
.new('IAP_SHADELESS', 'ShaderNodeTree')
336 output_node
= node_tree
.nodes
.new('NodeGroupOutput')
337 input_node
= node_tree
.nodes
.new('NodeGroupInput')
339 node_tree
.outputs
.new('NodeSocketShader', 'Shader')
340 node_tree
.inputs
.new('NodeSocketColor', 'Color')
342 # This could be faster as a transparent shader, but then no ambient occlusion
343 diffuse_shader
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
344 node_tree
.links
.new(diffuse_shader
.inputs
[0], input_node
.outputs
[0])
346 emission_shader
= node_tree
.nodes
.new('ShaderNodeEmission')
347 node_tree
.links
.new(emission_shader
.inputs
[0], input_node
.outputs
[0])
349 light_path
= node_tree
.nodes
.new('ShaderNodeLightPath')
350 is_glossy_ray
= light_path
.outputs
['Is Glossy Ray']
351 is_shadow_ray
= light_path
.outputs
['Is Shadow Ray']
352 ray_depth
= light_path
.outputs
['Ray Depth']
353 transmission_depth
= light_path
.outputs
['Transmission Depth']
355 unrefracted_depth
= node_tree
.nodes
.new('ShaderNodeMath')
356 unrefracted_depth
.operation
= 'SUBTRACT'
357 unrefracted_depth
.label
= 'Bounce Count'
358 node_tree
.links
.new(unrefracted_depth
.inputs
[0], ray_depth
)
359 node_tree
.links
.new(unrefracted_depth
.inputs
[1], transmission_depth
)
361 refracted
= node_tree
.nodes
.new('ShaderNodeMath')
362 refracted
.operation
= 'SUBTRACT'
363 refracted
.label
= 'Camera or Refracted'
364 refracted
.inputs
[0].default_value
= 1.0
365 node_tree
.links
.new(refracted
.inputs
[1], unrefracted_depth
.outputs
[0])
367 reflection_limit
= node_tree
.nodes
.new('ShaderNodeMath')
368 reflection_limit
.operation
= 'SUBTRACT'
369 reflection_limit
.label
= 'Limit Reflections'
370 reflection_limit
.inputs
[0].default_value
= 2.0
371 node_tree
.links
.new(reflection_limit
.inputs
[1], ray_depth
)
373 camera_reflected
= node_tree
.nodes
.new('ShaderNodeMath')
374 camera_reflected
.operation
= 'MULTIPLY'
375 camera_reflected
.label
= 'Camera Ray to Glossy'
376 node_tree
.links
.new(camera_reflected
.inputs
[0], reflection_limit
.outputs
[0])
377 node_tree
.links
.new(camera_reflected
.inputs
[1], is_glossy_ray
)
379 shadow_or_reflect
= node_tree
.nodes
.new('ShaderNodeMath')
380 shadow_or_reflect
.operation
= 'MAXIMUM'
381 shadow_or_reflect
.label
= 'Shadow or Reflection?'
382 node_tree
.links
.new(shadow_or_reflect
.inputs
[0], camera_reflected
.outputs
[0])
383 node_tree
.links
.new(shadow_or_reflect
.inputs
[1], is_shadow_ray
)
385 shadow_or_reflect_or_refract
= node_tree
.nodes
.new('ShaderNodeMath')
386 shadow_or_reflect_or_refract
.operation
= 'MAXIMUM'
387 shadow_or_reflect_or_refract
.label
= 'Shadow, Reflect or Refract?'
388 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[0], shadow_or_reflect
.outputs
[0])
389 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[1], refracted
.outputs
[0])
391 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
392 node_tree
.links
.new(mix_shader
.inputs
[0], shadow_or_reflect_or_refract
.outputs
[0])
393 node_tree
.links
.new(mix_shader
.inputs
[1], diffuse_shader
.outputs
[0])
394 node_tree
.links
.new(mix_shader
.inputs
[2], emission_shader
.outputs
[0])
396 node_tree
.links
.new(output_node
.inputs
[0], mix_shader
.outputs
[0])
398 auto_align_nodes(node_tree
)
400 group_node
= dest_node_tree
.nodes
.new("ShaderNodeGroup")
401 group_node
.node_tree
= node_tree
406 # -----------------------------------------------------------------------------
407 # Corner Pin Driver Helpers
409 @bpy.app
.handlers
.persistent
410 def check_drivers(*args
, **kwargs
):
411 """Check if watched objects in a scene have changed and trigger compositor update
413 This is part of a hack to ensure the compositor updates
414 itself when the objects used for drivers change.
416 It only triggers if transformation matricies change to avoid
417 a cyclic loop of updates.
419 if not watched_objects
:
420 # if there is nothing to watch, don't bother running this
421 bpy
.app
.handlers
.scene_update_post
.remove(check_drivers
)
425 for name
, matrix
in list(watched_objects
.items()):
427 obj
= bpy
.data
.objects
[name
]
429 # The user must have removed this object
430 del watched_objects
[name
]
432 new_matrix
= tuple(map(tuple, obj
.matrix_world
)).__hash
__()
433 if new_matrix
!= matrix
:
434 watched_objects
[name
] = new_matrix
438 # Trick to re-evaluate drivers
439 bpy
.context
.scene
.frame_current
= bpy
.context
.scene
.frame_current
442 def register_watched_object(obj
):
443 """Register an object to be monitored for transformation changes"""
446 # known object? -> we're done
447 if name
in watched_objects
:
450 if not watched_objects
:
451 # make sure check_drivers is active
452 bpy
.app
.handlers
.scene_update_post
.append(check_drivers
)
454 watched_objects
[name
] = None
457 def find_plane_corner(object_name
, x
, y
, axis
, camera
=None, *args
, **kwargs
):
458 """Find the location in camera space of a plane's corner"""
460 # I've added args / kwargs as a compatibility measure with future versions
461 warnings
.warn("Unknown Parameters Passed to \"Images as Planes\". Maybe you need to upgrade?")
463 plane
= bpy
.data
.objects
[object_name
]
465 # Passing in camera doesn't work before 2.78, so we use the current one
466 camera
= camera
or bpy
.context
.scene
.camera
468 # Hack to ensure compositor updates on future changes
469 register_watched_object(camera
)
470 register_watched_object(plane
)
472 scale
= plane
.scale
* 2.0
473 v
= plane
.dimensions
.copy()
476 v
= plane
.matrix_world
@ v
478 camera_vertex
= world_to_camera_view(
479 bpy
.context
.scene
, camera
, v
)
481 return camera_vertex
[axis
]
484 @bpy.app
.handlers
.persistent
485 def register_driver(*args
, **kwargs
):
486 """Register the find_plane_corner function for use with drivers"""
487 bpy
.app
.driver_namespace
['import_image__find_plane_corner'] = find_plane_corner
490 # -----------------------------------------------------------------------------
491 # Compositing Helpers
493 def group_in_frame(node_tree
, name
, nodes
):
494 frame_node
= node_tree
.nodes
.new("NodeFrame")
495 frame_node
.label
= name
496 frame_node
.name
= name
+ "_frame"
498 min_pos
= Vector(nodes
[0].location
)
499 max_pos
= min_pos
.copy()
502 top_left
= node
.location
503 bottom_right
= top_left
+ Vector((node
.width
, -node
.height
))
506 min_pos
[i
] = min(min_pos
[i
], top_left
[i
], bottom_right
[i
])
507 max_pos
[i
] = max(max_pos
[i
], top_left
[i
], bottom_right
[i
])
509 node
.parent
= frame_node
511 frame_node
.width
= max_pos
[0] - min_pos
[0] + 50
512 frame_node
.height
= max(max_pos
[1] - min_pos
[1] + 50, 450)
513 frame_node
.shrink
= True
518 def position_frame_bottom_left(node_tree
, frame_node
):
519 newpos
= Vector((100000, 100000)) # start reasonably far top / right
521 # Align with the furthest left
522 for node
in node_tree
.nodes
.values():
523 if node
!= frame_node
and node
.parent
!= frame_node
:
524 newpos
.x
= min(newpos
.x
, node
.location
.x
+ 30)
526 # As high as we can get without overlapping anything to the right
527 for node
in node_tree
.nodes
.values():
528 if node
!= frame_node
and not node
.parent
:
529 if node
.location
.x
< newpos
.x
+ frame_node
.width
:
530 print("Below", node
.name
, node
.location
, node
.height
, node
.dimensions
)
531 newpos
.y
= min(newpos
.y
, node
.location
.y
- max(node
.dimensions
.y
, node
.height
) - 20)
533 frame_node
.location
= newpos
536 def setup_compositing(context
, plane
, img_spec
):
537 # Node Groups only work with "new" dependency graph and even
538 # then it has some problems with not updating the first time
539 # So instead this groups with a node frame, which works reliably
541 scene
= context
.scene
542 scene
.use_nodes
= True
543 node_tree
= scene
.node_tree
546 image_node
= node_tree
.nodes
.new("CompositorNodeImage")
547 image_node
.name
= name
+ "_image"
548 image_node
.image
= img_spec
.image
549 image_node
.location
= Vector((0, 0))
550 image_node
.frame_start
= img_spec
.frame_start
551 image_node
.frame_offset
= img_spec
.frame_offset
552 image_node
.frame_duration
= img_spec
.frame_duration
554 scale_node
= node_tree
.nodes
.new("CompositorNodeScale")
555 scale_node
.name
= name
+ "_scale"
556 scale_node
.space
= 'RENDER_SIZE'
557 scale_node
.location
= image_node
.location
+ \
558 Vector((image_node
.width
+ 20, 0))
559 scale_node
.show_options
= False
561 cornerpin_node
= node_tree
.nodes
.new("CompositorNodeCornerPin")
562 cornerpin_node
.name
= name
+ "_cornerpin"
563 cornerpin_node
.location
= scale_node
.location
+ \
564 Vector((0, -scale_node
.height
))
566 node_tree
.links
.new(scale_node
.inputs
[0], image_node
.outputs
[0])
567 node_tree
.links
.new(cornerpin_node
.inputs
[0], scale_node
.outputs
[0])
569 # Put all the nodes in a frame for organization
570 frame_node
= group_in_frame(
572 (image_node
, scale_node
, cornerpin_node
)
575 # Position frame at bottom / left
576 position_frame_bottom_left(node_tree
, frame_node
)
579 for corner
in cornerpin_node
.inputs
[1:]:
580 id = corner
.identifier
581 x
= -1 if 'Left' in id else 1
582 y
= -1 if 'Lower' in id else 1
583 drivers
= corner
.driver_add('default_value')
584 for i
, axis_fcurve
in enumerate(drivers
):
585 driver
= axis_fcurve
.driver
586 # Always use the current camera
587 add_driver_prop(driver
, 'camera', 'SCENE', scene
, 'camera')
588 # Track camera location to ensure Deps Graph triggers (not used in the call)
589 add_driver_prop(driver
, 'cam_loc_x', 'OBJECT', scene
.camera
, 'location[0]')
590 # Don't break if the name changes
591 add_driver_prop(driver
, 'name', 'OBJECT', plane
, 'name')
592 driver
.expression
= "import_image__find_plane_corner(name or %s, %d, %d, %d, camera=camera)" % (
596 driver
.type = 'SCRIPTED'
597 driver
.is_valid
= True
598 axis_fcurve
.is_valid
= True
599 driver
.expression
= "%s" % driver
.expression
604 # -----------------------------------------------------------------------------
607 class IMPORT_IMAGE_OT_to_plane(Operator
, AddObjectHelper
):
608 """Create mesh plane(s) from image files with the appropriate aspect ratio"""
610 bl_idname
= "import_image.to_plane"
611 bl_label
= "Import Images as Planes"
612 bl_options
= {'REGISTER', 'PRESET', 'UNDO'}
614 # ----------------------
615 # File dialog properties
616 files
: CollectionProperty(type=bpy
.types
.OperatorFileListElement
, options
={'HIDDEN', 'SKIP_SAVE'})
618 directory
: StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
620 filter_image
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
621 filter_movie
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
622 filter_folder
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
624 # ----------------------
625 # Properties - Importing
626 force_reload
: BoolProperty(
627 name
="Force Reload", default
=False,
628 description
="Force reloading of the image if already opened elsewhere in Blender"
631 image_sequence
: BoolProperty(
632 name
="Animate Image Sequences", default
=False,
633 description
="Import sequentially numbered images as an animated "
634 "image sequence instead of separate planes"
637 # -------------------------------------
638 # Properties - Position and Orientation
639 axis_id_to_vector
= {
640 'X+': Vector(( 1, 0, 0)),
641 'Y+': Vector(( 0, 1, 0)),
642 'Z+': Vector(( 0, 0, 1)),
643 'X-': Vector((-1, 0, 0)),
644 'Y-': Vector(( 0, -1, 0)),
645 'Z-': Vector(( 0, 0, -1)),
648 offset
: BoolProperty(name
="Offset Planes", default
=True, description
="Offset Planes From Each Other")
651 ('X+', "X+", "Side by Side to the Left"),
652 ('Y+', "Y+", "Side by Side, Downward"),
653 ('Z+', "Z+", "Stacked Above"),
654 ('X-', "X-", "Side by Side to the Right"),
655 ('Y-', "Y-", "Side by Side, Upward"),
656 ('Z-', "Z-", "Stacked Below"),
658 offset_axis
: EnumProperty(
659 name
="Orientation", default
='X+', items
=OFFSET_MODES
,
660 description
="How planes are oriented relative to each others' local axis"
663 offset_amount
: FloatProperty(
664 name
="Offset", soft_min
=0, default
=0.1, description
="Space between planes",
665 subtype
='DISTANCE', unit
='LENGTH'
669 ('X+', "X+", "Facing Positive X"),
670 ('Y+', "Y+", "Facing Positive Y"),
671 ('Z+', "Z+ (Up)", "Facing Positive Z"),
672 ('X-', "X-", "Facing Negative X"),
673 ('Y-', "Y-", "Facing Negative Y"),
674 ('Z-', "Z- (Down)", "Facing Negative Z"),
675 ('CAM', "Face Camera", "Facing Camera"),
676 ('CAM_AX', "Main Axis", "Facing the Camera's dominant axis"),
678 align_axis
: EnumProperty(
679 name
="Align", default
='CAM_AX', items
=AXIS_MODES
,
680 description
="How to align the planes"
682 # prev_align_axis is used only by update_size_model
683 prev_align_axis
: EnumProperty(
684 items
=AXIS_MODES
+ (('NONE', '', ''),), default
='NONE', options
={'HIDDEN', 'SKIP_SAVE'})
685 align_track
: BoolProperty(
686 name
="Track Camera", default
=False, description
="Always face the camera"
691 def update_size_mode(self
, context
):
692 """If sizing relative to the camera, always face the camera"""
693 if self
.size_mode
== 'CAMERA':
694 self
.prev_align_axis
= self
.align_axis
695 self
.align_axis
= 'CAM'
697 # if a different alignment was set revert to that when
698 # size mode is changed
699 if self
.prev_align_axis
!= 'NONE':
700 self
.align_axis
= self
.prev_align_axis
701 self
._prev
_align
_axis
= 'NONE'
704 ('ABSOLUTE', "Absolute", "Use absolute size"),
705 ('CAMERA', "Camera Relative", "Scale to the camera frame"),
706 ('DPI', "Dpi", "Use definition of the image as dots per inch"),
707 ('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
709 size_mode
: EnumProperty(
710 name
="Size Mode", default
='ABSOLUTE', items
=SIZE_MODES
,
711 update
=update_size_mode
,
712 description
="How the size of the plane is computed")
715 ('FILL', "Fill", "Fill camera frame, spilling outside the frame"),
716 ('FIT', "Fit", "Fit entire image within the camera frame"),
718 fill_mode
: EnumProperty(name
="Scale", default
='FILL', items
=FILL_MODES
,
719 description
="How large in the camera frame is the plane")
721 height
: FloatProperty(name
="Height", description
="Height of the created plane",
722 default
=1.0, min=0.001, soft_min
=0.001, subtype
='DISTANCE', unit
='LENGTH')
724 factor
: FloatProperty(name
="Definition", min=1.0, default
=600.0,
725 description
="Number of pixels per inch or Blender Unit")
727 # ------------------------------
728 # Properties - Material / Shader
730 ('DIFFUSE', "Diffuse", "Diffuse Shader"),
731 ('SHADELESS', "Shadeless", "Only visible to camera and reflections."),
732 ('EMISSION', "Emit", "Emission Shader"),
734 shader
: EnumProperty(name
="Shader", items
=SHADERS
, default
='DIFFUSE', description
="Node shader to use")
736 emit_strength
: FloatProperty(
737 name
="Strength", min=0.0, default
=1.0, soft_max
=10.0,
738 step
=100, description
="Brightness of Emission Texture")
740 overwrite_material
: BoolProperty(
741 name
="Overwrite Material", default
=True,
742 description
="Overwrite existing Material (based on material name)")
744 compositing_nodes
: BoolProperty(
745 name
="Setup Corner Pin", default
=False,
746 description
="Build Compositor Nodes to reference this image "
747 "without re-rendering")
751 use_transparency
: BoolProperty(
752 name
="Use Alpha", default
=True,
753 description
="Use alphachannel for transparency")
755 t
= bpy
.types
.Image
.bl_rna
.properties
["alpha_mode"]
756 alpha_mode_items
= tuple((e
.identifier
, e
.name
, e
.description
) for e
in t
.enum_items
)
757 alpha_mode
: EnumProperty(
758 name
=t
.name
, items
=alpha_mode_items
, default
=t
.default
,
759 description
=t
.description
)
761 t
= bpy
.types
.ImageUser
.bl_rna
.properties
["use_auto_refresh"]
762 use_auto_refresh
: BoolProperty(name
=t
.name
, default
=True, description
=t
.description
)
764 relative
: BoolProperty(name
="Relative Paths", default
=True, description
="Use relative file paths")
768 def draw_import_config(self
, context
):
769 # --- Import Options --- #
773 box
.label(text
="Import Options:", icon
='IMPORT')
775 row
.active
= bpy
.data
.is_saved
776 row
.prop(self
, "relative")
778 box
.prop(self
, "force_reload")
779 box
.prop(self
, "image_sequence")
781 def draw_material_config(self
, context
):
782 # --- Material / Rendering Properties --- #
786 box
.label(text
="Compositing Nodes:", icon
='RENDERLAYERS')
787 box
.prop(self
, "compositing_nodes")
789 box
.label(text
="Material Settings:", icon
='MATERIAL')
792 row
.prop(self
, 'shader', expand
=True)
793 if self
.shader
== 'EMISSION':
794 box
.prop(self
, "emit_strength")
796 engine
= context
.scene
.render
.engine
797 if engine
not in ('CYCLES', 'BLENDER_EEVEE', 'BLENDER_OPENGL'):
798 box
.label(text
="%s is not supported" % engine
, icon
='ERROR')
800 box
.prop(self
, "overwrite_material")
802 box
.label(text
="Texture Settings:", icon
='TEXTURE')
804 row
.prop(self
, "use_transparency")
806 sub
.active
= self
.use_transparency
807 sub
.prop(self
, "alpha_mode", text
="")
808 box
.prop(self
, "use_auto_refresh")
810 def draw_spatial_config(self
, context
):
811 # --- Spatial Properties: Position, Size and Orientation --- #
815 box
.label(text
="Position:", icon
='SNAP_GRID')
816 box
.prop(self
, "offset")
819 row
.prop(self
, "offset_axis", expand
=True)
821 row
.prop(self
, "offset_amount")
822 col
.enabled
= self
.offset
824 box
.label(text
="Plane dimensions:", icon
='ARROW_LEFTRIGHT')
826 row
.prop(self
, "size_mode", expand
=True)
827 if self
.size_mode
== 'ABSOLUTE':
828 box
.prop(self
, "height")
829 elif self
.size_mode
== 'CAMERA':
831 row
.prop(self
, "fill_mode", expand
=True)
833 box
.prop(self
, "factor")
835 box
.label(text
="Orientation:")
837 row
.enabled
= 'CAM' not in self
.size_mode
838 row
.prop(self
, "align_axis")
840 row
.enabled
= 'CAM' in self
.align_axis
841 row
.alignment
= 'RIGHT'
842 row
.prop(self
, "align_track")
844 def draw(self
, context
):
846 # Draw configuration sections
847 self
.draw_import_config(context
)
848 self
.draw_material_config(context
)
849 self
.draw_spatial_config(context
)
851 # -------------------------------------------------------------------------
853 def invoke(self
, context
, event
):
854 engine
= context
.scene
.render
.engine
855 if engine
not in {'CYCLES', 'BLENDER_EEVEE'}:
856 if engine
not in {'BLENDER_OPENGL'}:
857 self
.report({'ERROR'}, "Cannot generate materials for unknown %s render engine" % engine
)
860 self
.report({'WARNING'},
861 "Generating Cycles/EEVEE compatible material, but won't be visible with %s engine" % engine
)
864 context
.window_manager
.fileselect_add(self
)
865 return {'RUNNING_MODAL'}
867 def execute(self
, context
):
868 if not bpy
.data
.is_saved
:
869 self
.relative
= False
871 # this won't work in edit mode
872 editmode
= context
.user_preferences
.edit
.use_enter_edit_mode
873 context
.user_preferences
.edit
.use_enter_edit_mode
= False
874 if context
.active_object
and context
.active_object
.mode
== 'EDIT':
875 bpy
.ops
.object.mode_set(mode
='OBJECT')
877 self
.import_images(context
)
879 context
.user_preferences
.edit
.use_enter_edit_mode
= editmode
883 def import_images(self
, context
):
885 # load images / sequences
886 images
= tuple(load_images(
887 (fn
.name
for fn
in self
.files
),
889 force_reload
=self
.force_reload
,
890 find_sequences
=self
.image_sequence
893 # Create individual planes
894 planes
= [self
.single_image_spec_to_plane(context
, img_spec
) for img_spec
in images
]
896 context
.scene
.update()
898 # Align planes relative to each other
900 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
901 offset_planes(planes
, self
.offset_amount
, offset_axis
)
903 if self
.size_mode
== 'CAMERA' and offset_axis
.z
:
905 x
, y
= compute_camera_size(
906 context
, plane
.location
,
907 self
.fill_mode
, plane
.dimensions
.x
/ plane
.dimensions
.y
)
908 plane
.dimensions
= x
, y
, 0.0
910 # setup new selection
912 plane
.select_set(True)
915 self
.report({'INFO'}, "Added {} Image Plane(s)".format(len(planes
)))
917 # operate on a single image
918 def single_image_spec_to_plane(self
, context
, img_spec
):
921 self
.apply_image_options(img_spec
.image
)
924 engine
= context
.scene
.render
.engine
925 if engine
in {'CYCLES', 'BLENDER_EEVEE', 'BLENDER_OPENGL'}:
926 material
= self
.create_cycles_material(context
, img_spec
)
928 # Create and position plane object
929 plane
= self
.create_image_plane(context
, material
.name
, img_spec
)
932 plane
.data
.materials
.append(material
)
934 # If applicable, setup Corner Pin node
935 if self
.compositing_nodes
:
936 setup_compositing(context
, plane
, img_spec
)
940 def apply_image_options(self
, image
):
941 image
.use_alpha
= self
.use_transparency
942 image
.alpha_mode
= self
.alpha_mode
945 try: # can't always find the relative path (between drive letters on windows)
946 image
.filepath
= bpy
.path
.relpath(image
.filepath
)
950 def apply_texture_options(self
, texture
, img_spec
):
951 # Shared by both Cycles and Blender Internal
952 image_user
= texture
.image_user
953 image_user
.use_auto_refresh
= self
.use_auto_refresh
954 image_user
.frame_start
= img_spec
.frame_start
955 image_user
.frame_offset
= img_spec
.frame_offset
956 image_user
.frame_duration
= img_spec
.frame_duration
958 # Image sequences need auto refresh to display reliably
959 if img_spec
.image
.source
== 'SEQUENCE':
960 image_user
.use_auto_refresh
= True
962 texture
.extension
= 'CLIP' # Default of "Repeat" can cause artifacts
964 def apply_material_options(self
, material
, slot
):
967 if self
.use_transparency
:
969 material
.specular_alpha
= 0.0
970 slot
.use_map_alpha
= True
973 material
.specular_alpha
= 1.0
974 slot
.use_map_alpha
= False
976 material
.specular_intensity
= 0
977 material
.diffuse_intensity
= 1.0
978 material
.use_transparency
= self
.use_transparency
979 material
.transparency_method
= 'Z_TRANSPARENCY'
980 material
.use_shadeless
= (shader
== 'SHADELESS')
981 material
.use_transparent_shadows
= (shader
== 'DIFFUSE')
982 material
.emit
= self
.emit_strength
if shader
== 'EMISSION' else 0.0
984 # -------------------------------------------------------------------------
986 def create_cycles_texnode(self
, context
, node_tree
, img_spec
):
987 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
988 tex_image
.image
= img_spec
.image
989 tex_image
.show_texture
= True
990 self
.apply_texture_options(tex_image
, img_spec
)
993 def create_cycles_material(self
, context
, img_spec
):
994 image
= img_spec
.image
995 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
997 if self
.overwrite_material
:
998 for mat
in bpy
.data
.materials
:
999 if mat
.name
== name_compat
:
1002 material
= bpy
.data
.materials
.new(name
=name_compat
)
1004 material
.use_nodes
= True
1005 node_tree
= material
.node_tree
1006 out_node
= clean_node_tree(node_tree
)
1008 tex_image
= self
.create_cycles_texnode(context
, node_tree
, img_spec
)
1010 if self
.shader
== 'DIFFUSE':
1011 core_shader
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
1012 elif self
.shader
== 'SHADELESS':
1013 core_shader
= get_shadeless_node(node_tree
)
1014 else: # Emission Shading
1015 core_shader
= node_tree
.nodes
.new('ShaderNodeEmission')
1016 core_shader
.inputs
[1].default_value
= self
.emit_strength
1018 # Connect color from texture
1019 node_tree
.links
.new(core_shader
.inputs
[0], tex_image
.outputs
[0])
1021 if self
.use_transparency
:
1022 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
1024 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
1025 node_tree
.links
.new(mix_shader
.inputs
[0], tex_image
.outputs
[1])
1026 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
[0])
1027 node_tree
.links
.new(mix_shader
.inputs
[2], core_shader
.outputs
[0])
1028 core_shader
= mix_shader
1030 node_tree
.links
.new(out_node
.inputs
[0], core_shader
.outputs
[0])
1032 auto_align_nodes(node_tree
)
1035 # -------------------------------------------------------------------------
1037 def create_image_plane(self
, context
, name
, img_spec
):
1039 width
, height
= self
.compute_plane_size(context
, img_spec
)
1042 bpy
.ops
.mesh
.primitive_plane_add('INVOKE_REGION_WIN')
1043 plane
= context
.active_object
1044 # Why does mesh.primitive_plane_add leave the object in edit mode???
1045 if plane
.mode
is not 'OBJECT':
1046 bpy
.ops
.object.mode_set(mode
='OBJECT')
1047 plane
.dimensions
= width
, height
, 0.0
1048 plane
.data
.name
= plane
.name
= name
1049 bpy
.ops
.object.transform_apply(scale
=True)
1051 # If sizing for camera, also insert into the camera's field of view
1052 if self
.size_mode
== 'CAMERA':
1053 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
1054 translate_axis
= [0 if offset_axis
[i
] else 1 for i
in (0, 1)]
1055 center_in_camera(context
.scene
, context
.scene
.camera
, plane
, translate_axis
)
1057 self
.align_plane(context
, plane
)
1061 def compute_plane_size(self
, context
, img_spec
):
1062 """Given the image size in pixels and location, determine size of plane"""
1063 px
, py
= img_spec
.size
1066 if px
== 0 or py
== 0:
1069 if self
.size_mode
== 'ABSOLUTE':
1073 elif self
.size_mode
== 'CAMERA':
1074 x
, y
= compute_camera_size(
1075 context
, context
.scene
.cursor_location
,
1076 self
.fill_mode
, px
/ py
1079 elif self
.size_mode
== 'DPI':
1080 fact
= 1 / self
.factor
/ context
.scene
.unit_settings
.scale_length
* 0.0254
1084 else: # elif self.size_mode == 'DPBU'
1085 fact
= 1 / self
.factor
1091 def align_plane(self
, context
, plane
):
1092 """Pick an axis and align the plane to it"""
1093 if 'CAM' in self
.align_axis
:
1095 camera
= context
.scene
.camera
1097 # Find the axis that best corresponds to the camera's view direction
1098 axis
= camera
.matrix_world
@ \
1099 Vector((0, 0, 1)) - camera
.matrix_world
.col
[3].xyz
1100 # pick the axis with the greatest magnitude
1101 mag
= max(map(abs, axis
))
1102 # And use that axis & direction
1104 n
/ mag
if abs(n
) == mag
else 0.0
1108 # No camera? Just face Z axis
1109 axis
= Vector((0, 0, 1))
1110 self
.align_axis
= 'Z+'
1113 axis
= self
.axis_id_to_vector
[self
.align_axis
]
1115 # rotate accordingly for x/y axiis
1117 plane
.rotation_euler
.x
= pi
/ 2
1120 plane
.rotation_euler
.z
= pi
1122 plane
.rotation_euler
.z
= 0
1124 plane
.rotation_euler
.z
= pi
/ 2
1126 plane
.rotation_euler
.z
= -pi
/ 2
1128 # or flip 180 degrees for negative z
1130 plane
.rotation_euler
.y
= pi
1132 if self
.align_axis
== 'CAM':
1133 constraint
= plane
.constraints
.new('COPY_ROTATION')
1134 constraint
.target
= camera
1135 constraint
.use_x
= constraint
.use_y
= constraint
.use_z
= True
1136 if not self
.align_track
:
1137 bpy
.ops
.object.visual_transform_apply()
1138 plane
.constraints
.clear()
1140 if self
.align_axis
== 'CAM_AX' and self
.align_track
:
1141 constraint
= plane
.constraints
.new('LOCKED_TRACK')
1142 constraint
.target
= camera
1143 constraint
.track_axis
= 'TRACK_Z'
1144 constraint
.lock_axis
= 'LOCK_Y'
1147 # -----------------------------------------------------------------------------
1150 def import_images_button(self
, context
):
1151 self
.layout
.operator(IMPORT_IMAGE_OT_to_plane
.bl_idname
, text
="Images as Planes", icon
='TEXTURE')
1155 IMPORT_IMAGE_OT_to_plane
,
1161 bpy
.utils
.register_class(cls
)
1163 bpy
.types
.TOPBAR_MT_file_import
.append(import_images_button
)
1164 bpy
.types
.VIEW3D_MT_image_add
.append(import_images_button
)
1166 bpy
.app
.handlers
.load_post
.append(register_driver
)
1171 bpy
.types
.TOPBAR_MT_file_import
.remove(import_images_button
)
1172 bpy
.types
.VIEW3D_MT_image_add
.remove(import_images_button
)
1174 # This will only exist if drivers are active
1175 if check_drivers
in bpy
.app
.handlers
.scene_update_post
:
1176 bpy
.app
.handlers
.scene_update_post
.remove(check_drivers
)
1178 bpy
.app
.handlers
.load_post
.remove(register_driver
)
1179 del bpy
.app
.driver_namespace
['import_image__find_plane_corner']
1182 bpy
.utils
.unregister_class(cls
)
1185 if __name__
== "__main__":
1186 # Run simple doc tests