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, 91, 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 "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/images_as_planes.html",
31 "support": 'OFFICIAL',
32 "category": "Import-Export",
38 from itertools
import count
, repeat
39 from collections
import namedtuple
43 from bpy
.types
import Operator
44 from mathutils
import Vector
46 from bpy
.props
import (
54 from bpy_extras
.object_utils
import (
59 from bpy_extras
.image_utils
import load_image
61 # -----------------------------------------------------------------------------
62 # Module-level Shared State
64 watched_objects
= {} # used to trigger compositor updates on scene updates
67 # -----------------------------------------------------------------------------
70 def add_driver_prop(driver
, name
, type, id, path
):
71 """Configure a new driver variable."""
72 dv
= driver
.variables
.new()
74 dv
.type = 'SINGLE_PROP'
75 target
= dv
.targets
[0]
78 target
.data_path
= path
81 # -----------------------------------------------------------------------------
84 ImageSpec
= namedtuple(
86 ['image', 'size', 'frame_start', 'frame_offset', 'frame_duration'])
88 num_regex
= re
.compile('[0-9]') # Find a single number
89 nums_regex
= re
.compile('[0-9]+') # Find a set of numbers
92 def find_image_sequences(files
):
93 """From a group of files, detect image sequences.
95 This returns a generator of tuples, which contain the filename,
96 start frame, and length of the detected sequence
98 >>> list(find_image_sequences([
99 ... "test2-001.jp2", "test2-002.jp2",
100 ... "test3-003.jp2", "test3-004.jp2", "test3-005.jp2", "test3-006.jp2",
102 [('blaah', 1, 1), ('test2-001.jp2', 1, 2), ('test3-003.jp2', 3, 4)]
105 files
= iter(sorted(files
))
111 for filename
in files
:
112 new_pattern
= num_regex
.sub('#', filename
)
113 new_matches
= list(map(int, nums_regex
.findall(filename
)))
114 if new_pattern
== pattern
:
115 # this file looks like it may be in sequence from the previous
117 # if there are multiple sets of numbers, figure out what changed
119 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
124 # did it only change by one?
125 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
127 # We expect this to increment
137 # No continuation -> spit out what we found and reset counters
140 yield prev_file
, matches
[segment
], length
142 yield prev_file
, 1, 1
145 matches
= new_matches
146 pattern
= new_pattern
152 yield prev_file
, matches
[segment
], length
154 yield prev_file
, 1, 1
157 def load_images(filenames
, directory
, force_reload
=False, frame_start
=1, find_sequences
=False):
158 """Wrapper for bpy's load_image
160 Loads a set of images, movies, or even image sequences
161 Returns a generator of ImageSpec wrapper objects later used for texture setup
163 if find_sequences
: # if finding sequences, we need some pre-processing first
164 file_iter
= find_image_sequences(filenames
)
166 file_iter
= zip(filenames
, repeat(1), repeat(1))
168 for filename
, offset
, frames
in file_iter
:
169 image
= load_image(filename
, directory
, check_existing
=True, force_reload
=force_reload
)
171 # Size is unavailable for sequences, so we grab it early
172 size
= tuple(image
.size
)
174 if image
.source
== 'MOVIE':
176 # This number is only valid when read a second time in 2.77
177 # This repeated line is not a mistake
178 frames
= image
.frame_duration
179 frames
= image
.frame_duration
181 elif frames
> 1: # Not movie, but multiple frames -> image sequence
182 image
.source
= 'SEQUENCE'
184 yield ImageSpec(image
, size
, frame_start
, offset
- 1, frames
)
187 # -----------------------------------------------------------------------------
188 # Position & Size Helpers
190 def offset_planes(planes
, gap
, axis
):
191 """Offset planes from each other by `gap` amount along a _local_ vector `axis`
193 For example, offset_planes([obj1, obj2], 0.5, Vector(0, 0, 1)) will place
194 obj2 0.5 blender units away from obj1 along the local positive Z axis.
196 This is in local space, not world space, so all planes should share
197 a common scale and rotation.
201 for current
in planes
[1:]:
202 local_offset
= abs((prior
.dimensions
+ current
.dimensions
).dot(axis
)) / 2.0 + gap
204 offset
+= local_offset
* axis
205 current
.location
= current
.matrix_world
@ offset
210 def compute_camera_size(context
, center
, fill_mode
, aspect
):
211 """Determine how large an object needs to be to fit or fill the camera's field of view."""
212 scene
= context
.scene
213 camera
= scene
.camera
214 view_frame
= camera
.data
.view_frame(scene
=scene
)
216 Vector([max(v
[i
] for v
in view_frame
) for i
in range(3)]) - \
217 Vector([min(v
[i
] for v
in view_frame
) for i
in range(3)])
218 camera_aspect
= frame_size
.x
/ frame_size
.y
220 # Convert the frame size to the correct sizing at a given distance
221 if camera
.type == 'ORTHO':
222 frame_size
= frame_size
.xy
224 # Perspective transform
225 distance
= world_to_camera_view(scene
, camera
, center
).z
226 frame_size
= distance
* frame_size
.xy
/ (-view_frame
[0].z
)
228 # Determine what axis to match to the camera
229 match_axis
= 0 # match the Y axis size
230 match_aspect
= aspect
231 if (fill_mode
== 'FILL' and aspect
> camera_aspect
) or \
232 (fill_mode
== 'FIT' and aspect
< camera_aspect
):
233 match_axis
= 1 # match the X axis size
234 match_aspect
= 1.0 / aspect
236 # scale the other axis to the correct aspect
237 frame_size
[1 - match_axis
] = frame_size
[match_axis
] / match_aspect
242 def center_in_camera(scene
, camera
, obj
, axis
=(1, 1)):
243 """Center object along specified axis of the camera"""
244 camera_matrix_col
= camera
.matrix_world
.col
245 location
= obj
.location
247 # Vector from the camera's world coordinate center to the object's center
248 delta
= camera_matrix_col
[3].xyz
- location
250 # How far off center we are along the camera's local X
251 camera_x_mag
= delta
.dot(camera_matrix_col
[0].xyz
) * axis
[0]
252 # How far off center we are along the camera's local Y
253 camera_y_mag
= delta
.dot(camera_matrix_col
[1].xyz
) * axis
[1]
255 # Now offset only along camera local axis
256 offset
= camera_matrix_col
[0].xyz
* camera_x_mag
+ \
257 camera_matrix_col
[1].xyz
* camera_y_mag
259 obj
.location
= location
+ offset
262 # -----------------------------------------------------------------------------
265 def get_input_nodes(node
, links
):
266 """Get nodes that are a inputs to the given node"""
267 # Get all links going to node.
268 input_links
= {lnk
for lnk
in links
if lnk
.to_node
== node
}
269 # Sort those links, get their input nodes (and avoid doubles!).
272 for socket
in node
.inputs
:
274 for link
in input_links
:
277 # Node already treated!
279 elif link
.to_socket
== socket
:
280 sorted_nodes
.append(nd
)
283 input_links
-= done_links
287 def auto_align_nodes(node_tree
):
288 """Given a shader node tree, arrange nodes neatly relative to the output node."""
291 nodes
= node_tree
.nodes
292 links
= node_tree
.links
295 if node
.type == 'OUTPUT_MATERIAL' or node
.type == 'GROUP_OUTPUT':
299 else: # Just in case there is no output
303 from_nodes
= get_input_nodes(to_node
, links
)
304 for i
, node
in enumerate(from_nodes
):
305 node
.location
.x
= min(node
.location
.x
, to_node
.location
.x
- x_gap
)
306 node
.location
.y
= to_node
.location
.y
307 node
.location
.y
-= i
* y_gap
308 node
.location
.y
+= (len(from_nodes
) - 1) * y_gap
/ (len(from_nodes
))
314 def clean_node_tree(node_tree
):
315 """Clear all nodes in a shader node tree except the output.
317 Returns the output node
319 nodes
= node_tree
.nodes
320 for node
in list(nodes
): # copy to avoid altering the loop's data source
321 if not node
.type == 'OUTPUT_MATERIAL':
324 return node_tree
.nodes
[0]
327 def get_shadeless_node(dest_node_tree
):
328 """Return a "shadless" cycles/eevee node, creating a node group if nonexistent"""
330 node_tree
= bpy
.data
.node_groups
['IAP_SHADELESS']
333 # need to build node shadeless node group
334 node_tree
= bpy
.data
.node_groups
.new('IAP_SHADELESS', 'ShaderNodeTree')
335 output_node
= node_tree
.nodes
.new('NodeGroupOutput')
336 input_node
= node_tree
.nodes
.new('NodeGroupInput')
338 node_tree
.outputs
.new('NodeSocketShader', 'Shader')
339 node_tree
.inputs
.new('NodeSocketColor', 'Color')
341 # This could be faster as a transparent shader, but then no ambient occlusion
342 diffuse_shader
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
343 node_tree
.links
.new(diffuse_shader
.inputs
[0], input_node
.outputs
[0])
345 emission_shader
= node_tree
.nodes
.new('ShaderNodeEmission')
346 node_tree
.links
.new(emission_shader
.inputs
[0], input_node
.outputs
[0])
348 light_path
= node_tree
.nodes
.new('ShaderNodeLightPath')
349 is_glossy_ray
= light_path
.outputs
['Is Glossy Ray']
350 is_shadow_ray
= light_path
.outputs
['Is Shadow Ray']
351 ray_depth
= light_path
.outputs
['Ray Depth']
352 transmission_depth
= light_path
.outputs
['Transmission Depth']
354 unrefracted_depth
= node_tree
.nodes
.new('ShaderNodeMath')
355 unrefracted_depth
.operation
= 'SUBTRACT'
356 unrefracted_depth
.label
= 'Bounce Count'
357 node_tree
.links
.new(unrefracted_depth
.inputs
[0], ray_depth
)
358 node_tree
.links
.new(unrefracted_depth
.inputs
[1], transmission_depth
)
360 refracted
= node_tree
.nodes
.new('ShaderNodeMath')
361 refracted
.operation
= 'SUBTRACT'
362 refracted
.label
= 'Camera or Refracted'
363 refracted
.inputs
[0].default_value
= 1.0
364 node_tree
.links
.new(refracted
.inputs
[1], unrefracted_depth
.outputs
[0])
366 reflection_limit
= node_tree
.nodes
.new('ShaderNodeMath')
367 reflection_limit
.operation
= 'SUBTRACT'
368 reflection_limit
.label
= 'Limit Reflections'
369 reflection_limit
.inputs
[0].default_value
= 2.0
370 node_tree
.links
.new(reflection_limit
.inputs
[1], ray_depth
)
372 camera_reflected
= node_tree
.nodes
.new('ShaderNodeMath')
373 camera_reflected
.operation
= 'MULTIPLY'
374 camera_reflected
.label
= 'Camera Ray to Glossy'
375 node_tree
.links
.new(camera_reflected
.inputs
[0], reflection_limit
.outputs
[0])
376 node_tree
.links
.new(camera_reflected
.inputs
[1], is_glossy_ray
)
378 shadow_or_reflect
= node_tree
.nodes
.new('ShaderNodeMath')
379 shadow_or_reflect
.operation
= 'MAXIMUM'
380 shadow_or_reflect
.label
= 'Shadow or Reflection?'
381 node_tree
.links
.new(shadow_or_reflect
.inputs
[0], camera_reflected
.outputs
[0])
382 node_tree
.links
.new(shadow_or_reflect
.inputs
[1], is_shadow_ray
)
384 shadow_or_reflect_or_refract
= node_tree
.nodes
.new('ShaderNodeMath')
385 shadow_or_reflect_or_refract
.operation
= 'MAXIMUM'
386 shadow_or_reflect_or_refract
.label
= 'Shadow, Reflect or Refract?'
387 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[0], shadow_or_reflect
.outputs
[0])
388 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[1], refracted
.outputs
[0])
390 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
391 node_tree
.links
.new(mix_shader
.inputs
[0], shadow_or_reflect_or_refract
.outputs
[0])
392 node_tree
.links
.new(mix_shader
.inputs
[1], diffuse_shader
.outputs
[0])
393 node_tree
.links
.new(mix_shader
.inputs
[2], emission_shader
.outputs
[0])
395 node_tree
.links
.new(output_node
.inputs
[0], mix_shader
.outputs
[0])
397 auto_align_nodes(node_tree
)
399 group_node
= dest_node_tree
.nodes
.new("ShaderNodeGroup")
400 group_node
.node_tree
= node_tree
405 # -----------------------------------------------------------------------------
406 # Corner Pin Driver Helpers
408 @bpy.app
.handlers
.persistent
409 def check_drivers(*args
, **kwargs
):
410 """Check if watched objects in a scene have changed and trigger compositor update
412 This is part of a hack to ensure the compositor updates
413 itself when the objects used for drivers change.
415 It only triggers if transformation matricies change to avoid
416 a cyclic loop of updates.
418 if not watched_objects
:
419 # if there is nothing to watch, don't bother running this
420 bpy
.app
.handlers
.depsgraph_update_post
.remove(check_drivers
)
424 for name
, matrix
in list(watched_objects
.items()):
426 obj
= bpy
.data
.objects
[name
]
428 # The user must have removed this object
429 del watched_objects
[name
]
431 new_matrix
= tuple(map(tuple, obj
.matrix_world
)).__hash
__()
432 if new_matrix
!= matrix
:
433 watched_objects
[name
] = new_matrix
437 # Trick to re-evaluate drivers
438 bpy
.context
.scene
.frame_current
= bpy
.context
.scene
.frame_current
441 def register_watched_object(obj
):
442 """Register an object to be monitored for transformation changes"""
445 # known object? -> we're done
446 if name
in watched_objects
:
449 if not watched_objects
:
450 # make sure check_drivers is active
451 bpy
.app
.handlers
.depsgraph_update_post
.append(check_drivers
)
453 watched_objects
[name
] = None
456 def find_plane_corner(object_name
, x
, y
, axis
, camera
=None, *args
, **kwargs
):
457 """Find the location in camera space of a plane's corner"""
459 # I've added args / kwargs as a compatibility measure with future versions
460 warnings
.warn("Unknown Parameters Passed to \"Images as Planes\". Maybe you need to upgrade?")
462 plane
= bpy
.data
.objects
[object_name
]
464 # Passing in camera doesn't work before 2.78, so we use the current one
465 camera
= camera
or bpy
.context
.scene
.camera
467 # Hack to ensure compositor updates on future changes
468 register_watched_object(camera
)
469 register_watched_object(plane
)
471 scale
= plane
.scale
* 2.0
472 v
= plane
.dimensions
.copy()
475 v
= plane
.matrix_world
@ v
477 camera_vertex
= world_to_camera_view(
478 bpy
.context
.scene
, camera
, v
)
480 return camera_vertex
[axis
]
483 @bpy.app
.handlers
.persistent
484 def register_driver(*args
, **kwargs
):
485 """Register the find_plane_corner function for use with drivers"""
486 bpy
.app
.driver_namespace
['import_image__find_plane_corner'] = find_plane_corner
489 # -----------------------------------------------------------------------------
490 # Compositing Helpers
492 def group_in_frame(node_tree
, name
, nodes
):
493 frame_node
= node_tree
.nodes
.new("NodeFrame")
494 frame_node
.label
= name
495 frame_node
.name
= name
+ "_frame"
497 min_pos
= Vector(nodes
[0].location
)
498 max_pos
= min_pos
.copy()
501 top_left
= node
.location
502 bottom_right
= top_left
+ Vector((node
.width
, -node
.height
))
505 min_pos
[i
] = min(min_pos
[i
], top_left
[i
], bottom_right
[i
])
506 max_pos
[i
] = max(max_pos
[i
], top_left
[i
], bottom_right
[i
])
508 node
.parent
= frame_node
510 frame_node
.width
= max_pos
[0] - min_pos
[0] + 50
511 frame_node
.height
= max(max_pos
[1] - min_pos
[1] + 50, 450)
512 frame_node
.shrink
= True
517 def position_frame_bottom_left(node_tree
, frame_node
):
518 newpos
= Vector((100000, 100000)) # start reasonably far top / right
520 # Align with the furthest left
521 for node
in node_tree
.nodes
.values():
522 if node
!= frame_node
and node
.parent
!= frame_node
:
523 newpos
.x
= min(newpos
.x
, node
.location
.x
+ 30)
525 # As high as we can get without overlapping anything to the right
526 for node
in node_tree
.nodes
.values():
527 if node
!= frame_node
and not node
.parent
:
528 if node
.location
.x
< newpos
.x
+ frame_node
.width
:
529 print("Below", node
.name
, node
.location
, node
.height
, node
.dimensions
)
530 newpos
.y
= min(newpos
.y
, node
.location
.y
- max(node
.dimensions
.y
, node
.height
) - 20)
532 frame_node
.location
= newpos
535 def setup_compositing(context
, plane
, img_spec
):
536 # Node Groups only work with "new" dependency graph and even
537 # then it has some problems with not updating the first time
538 # So instead this groups with a node frame, which works reliably
540 scene
= context
.scene
541 scene
.use_nodes
= True
542 node_tree
= scene
.node_tree
545 image_node
= node_tree
.nodes
.new("CompositorNodeImage")
546 image_node
.name
= name
+ "_image"
547 image_node
.image
= img_spec
.image
548 image_node
.location
= Vector((0, 0))
549 image_node
.frame_start
= img_spec
.frame_start
550 image_node
.frame_offset
= img_spec
.frame_offset
551 image_node
.frame_duration
= img_spec
.frame_duration
553 scale_node
= node_tree
.nodes
.new("CompositorNodeScale")
554 scale_node
.name
= name
+ "_scale"
555 scale_node
.space
= 'RENDER_SIZE'
556 scale_node
.location
= image_node
.location
+ \
557 Vector((image_node
.width
+ 20, 0))
558 scale_node
.show_options
= False
560 cornerpin_node
= node_tree
.nodes
.new("CompositorNodeCornerPin")
561 cornerpin_node
.name
= name
+ "_cornerpin"
562 cornerpin_node
.location
= scale_node
.location
+ \
563 Vector((0, -scale_node
.height
))
565 node_tree
.links
.new(scale_node
.inputs
[0], image_node
.outputs
[0])
566 node_tree
.links
.new(cornerpin_node
.inputs
[0], scale_node
.outputs
[0])
568 # Put all the nodes in a frame for organization
569 frame_node
= group_in_frame(
571 (image_node
, scale_node
, cornerpin_node
)
574 # Position frame at bottom / left
575 position_frame_bottom_left(node_tree
, frame_node
)
578 for corner
in cornerpin_node
.inputs
[1:]:
579 id = corner
.identifier
580 x
= -1 if 'Left' in id else 1
581 y
= -1 if 'Lower' in id else 1
582 drivers
= corner
.driver_add('default_value')
583 for i
, axis_fcurve
in enumerate(drivers
):
584 driver
= axis_fcurve
.driver
585 # Always use the current camera
586 add_driver_prop(driver
, 'camera', 'SCENE', scene
, 'camera')
587 # Track camera location to ensure Deps Graph triggers (not used in the call)
588 add_driver_prop(driver
, 'cam_loc_x', 'OBJECT', scene
.camera
, 'location[0]')
589 # Don't break if the name changes
590 add_driver_prop(driver
, 'name', 'OBJECT', plane
, 'name')
591 driver
.expression
= "import_image__find_plane_corner(name or %s, %d, %d, %d, camera=camera)" % (
595 driver
.type = 'SCRIPTED'
596 driver
.is_valid
= True
597 axis_fcurve
.is_valid
= True
598 driver
.expression
= "%s" % driver
.expression
600 context
.view_layer
.update()
603 # -----------------------------------------------------------------------------
606 class IMPORT_IMAGE_OT_to_plane(Operator
, AddObjectHelper
):
607 """Create mesh plane(s) from image files with the appropriate aspect ratio"""
609 bl_idname
= "import_image.to_plane"
610 bl_label
= "Import Images as Planes"
611 bl_options
= {'REGISTER', 'PRESET', 'UNDO'}
613 # ----------------------
614 # File dialog properties
615 files
: CollectionProperty(type=bpy
.types
.OperatorFileListElement
, options
={'HIDDEN', 'SKIP_SAVE'})
617 directory
: StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
619 filter_image
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
620 filter_movie
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
621 filter_folder
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
623 # ----------------------
624 # Properties - Importing
625 force_reload
: BoolProperty(
626 name
="Force Reload", default
=False,
627 description
="Force reloading of the image if already opened elsewhere in Blender"
630 image_sequence
: BoolProperty(
631 name
="Animate Image Sequences", default
=False,
632 description
="Import sequentially numbered images as an animated "
633 "image sequence instead of separate planes"
636 # -------------------------------------
637 # Properties - Position and Orientation
638 axis_id_to_vector
= {
639 'X+': Vector(( 1, 0, 0)),
640 'Y+': Vector(( 0, 1, 0)),
641 'Z+': Vector(( 0, 0, 1)),
642 'X-': Vector((-1, 0, 0)),
643 'Y-': Vector(( 0, -1, 0)),
644 'Z-': Vector(( 0, 0, -1)),
647 offset
: BoolProperty(name
="Offset Planes", default
=True, description
="Offset Planes From Each Other")
650 ('X+', "X+", "Side by Side to the Left"),
651 ('Y+', "Y+", "Side by Side, Downward"),
652 ('Z+', "Z+", "Stacked Above"),
653 ('X-', "X-", "Side by Side to the Right"),
654 ('Y-', "Y-", "Side by Side, Upward"),
655 ('Z-', "Z-", "Stacked Below"),
657 offset_axis
: EnumProperty(
658 name
="Orientation", default
='X+', items
=OFFSET_MODES
,
659 description
="How planes are oriented relative to each others' local axis"
662 offset_amount
: FloatProperty(
663 name
="Offset", soft_min
=0, default
=0.1, description
="Space between planes",
664 subtype
='DISTANCE', unit
='LENGTH'
668 ('X+', "X+", "Facing Positive X"),
669 ('Y+', "Y+", "Facing Positive Y"),
670 ('Z+', "Z+ (Up)", "Facing Positive Z"),
671 ('X-', "X-", "Facing Negative X"),
672 ('Y-', "Y-", "Facing Negative Y"),
673 ('Z-', "Z- (Down)", "Facing Negative Z"),
674 ('CAM', "Face Camera", "Facing Camera"),
675 ('CAM_AX', "Main Axis", "Facing the Camera's dominant axis"),
677 align_axis
: EnumProperty(
678 name
="Align", default
='CAM_AX', items
=AXIS_MODES
,
679 description
="How to align the planes"
681 # prev_align_axis is used only by update_size_model
682 prev_align_axis
: EnumProperty(
683 items
=AXIS_MODES
+ (('NONE', '', ''),), default
='NONE', options
={'HIDDEN', 'SKIP_SAVE'})
684 align_track
: BoolProperty(
685 name
="Track Camera", default
=False, description
="Always face the camera"
690 def update_size_mode(self
, context
):
691 """If sizing relative to the camera, always face the camera"""
692 if self
.size_mode
== 'CAMERA':
693 self
.prev_align_axis
= self
.align_axis
694 self
.align_axis
= 'CAM'
696 # if a different alignment was set revert to that when
697 # size mode is changed
698 if self
.prev_align_axis
!= 'NONE':
699 self
.align_axis
= self
.prev_align_axis
700 self
._prev
_align
_axis
= 'NONE'
703 ('ABSOLUTE', "Absolute", "Use absolute size"),
704 ('CAMERA', "Camera Relative", "Scale to the camera frame"),
705 ('DPI', "Dpi", "Use definition of the image as dots per inch"),
706 ('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
708 size_mode
: EnumProperty(
709 name
="Size Mode", default
='ABSOLUTE', items
=SIZE_MODES
,
710 update
=update_size_mode
,
711 description
="How the size of the plane is computed")
714 ('FILL', "Fill", "Fill camera frame, spilling outside the frame"),
715 ('FIT', "Fit", "Fit entire image within the camera frame"),
717 fill_mode
: EnumProperty(name
="Scale", default
='FILL', items
=FILL_MODES
,
718 description
="How large in the camera frame is the plane")
720 height
: FloatProperty(name
="Height", description
="Height of the created plane",
721 default
=1.0, min=0.001, soft_min
=0.001, subtype
='DISTANCE', unit
='LENGTH')
723 factor
: FloatProperty(name
="Definition", min=1.0, default
=600.0,
724 description
="Number of pixels per inch or Blender Unit")
726 # ------------------------------
727 # Properties - Material / Shader
729 ('PRINCIPLED',"Principled","Principled Shader"),
730 ('SHADELESS', "Shadeless", "Only visible to camera and reflections"),
731 ('EMISSION', "Emit", "Emission Shader"),
733 shader
: EnumProperty(name
="Shader", items
=SHADERS
, default
='PRINCIPLED', description
="Node shader to use")
735 emit_strength
: FloatProperty(
736 name
="Strength", min=0.0, default
=1.0, soft_max
=10.0,
737 step
=100, description
="Brightness of Emission Texture")
739 overwrite_material
: BoolProperty(
740 name
="Overwrite Material", default
=True,
741 description
="Overwrite existing Material (based on material name)")
743 compositing_nodes
: BoolProperty(
744 name
="Setup Corner Pin", default
=False,
745 description
="Build Compositor Nodes to reference this image "
746 "without re-rendering")
750 use_transparency
: BoolProperty(
751 name
="Use Alpha", default
=True,
752 description
="Use alpha channel for transparency")
754 t
= bpy
.types
.Image
.bl_rna
.properties
["alpha_mode"]
755 alpha_mode_items
= tuple((e
.identifier
, e
.name
, e
.description
) for e
in t
.enum_items
)
756 alpha_mode
: EnumProperty(
757 name
=t
.name
, items
=alpha_mode_items
, default
=t
.default
,
758 description
=t
.description
)
760 t
= bpy
.types
.ImageUser
.bl_rna
.properties
["use_auto_refresh"]
761 use_auto_refresh
: BoolProperty(name
=t
.name
, default
=True, description
=t
.description
)
763 relative
: BoolProperty(name
="Relative Paths", default
=True, description
="Use relative file paths")
767 def draw_import_config(self
, context
):
768 # --- Import Options --- #
772 box
.label(text
="Import Options:", icon
='IMPORT')
774 row
.active
= bpy
.data
.is_saved
775 row
.prop(self
, "relative")
777 box
.prop(self
, "force_reload")
778 box
.prop(self
, "image_sequence")
780 def draw_material_config(self
, context
):
781 # --- Material / Rendering Properties --- #
785 box
.label(text
="Compositing Nodes:", icon
='RENDERLAYERS')
786 box
.prop(self
, "compositing_nodes")
788 box
.label(text
="Material Settings:", icon
='MATERIAL')
791 row
.prop(self
, 'shader', expand
=True)
792 if self
.shader
== 'EMISSION':
793 box
.prop(self
, "emit_strength")
795 engine
= context
.scene
.render
.engine
796 if engine
not in ('CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'):
797 box
.label(text
="%s is not supported" % engine
, icon
='ERROR')
799 box
.prop(self
, "overwrite_material")
801 box
.label(text
="Texture Settings:", icon
='TEXTURE')
803 row
.prop(self
, "use_transparency")
805 sub
.active
= self
.use_transparency
806 sub
.prop(self
, "alpha_mode", text
="")
807 box
.prop(self
, "use_auto_refresh")
809 def draw_spatial_config(self
, context
):
810 # --- Spatial Properties: Position, Size and Orientation --- #
814 box
.label(text
="Position:", icon
='SNAP_GRID')
815 box
.prop(self
, "offset")
818 row
.prop(self
, "offset_axis", expand
=True)
820 row
.prop(self
, "offset_amount")
821 col
.enabled
= self
.offset
823 box
.label(text
="Plane dimensions:", icon
='ARROW_LEFTRIGHT')
825 row
.prop(self
, "size_mode", expand
=True)
826 if self
.size_mode
== 'ABSOLUTE':
827 box
.prop(self
, "height")
828 elif self
.size_mode
== 'CAMERA':
830 row
.prop(self
, "fill_mode", expand
=True)
832 box
.prop(self
, "factor")
834 box
.label(text
="Orientation:")
836 row
.enabled
= 'CAM' not in self
.size_mode
837 row
.prop(self
, "align_axis")
839 row
.enabled
= 'CAM' in self
.align_axis
840 row
.alignment
= 'RIGHT'
841 row
.prop(self
, "align_track")
843 def draw(self
, context
):
845 # Draw configuration sections
846 self
.draw_import_config(context
)
847 self
.draw_material_config(context
)
848 self
.draw_spatial_config(context
)
850 # -------------------------------------------------------------------------
852 def invoke(self
, context
, event
):
853 engine
= context
.scene
.render
.engine
854 if engine
not in {'CYCLES', 'BLENDER_EEVEE'}:
855 if engine
!= 'BLENDER_WORKBENCH':
856 self
.report({'ERROR'}, "Cannot generate materials for unknown %s render engine" % engine
)
859 self
.report({'WARNING'},
860 "Generating Cycles/EEVEE compatible material, but won't be visible with %s engine" % engine
)
863 context
.window_manager
.fileselect_add(self
)
864 return {'RUNNING_MODAL'}
866 def execute(self
, context
):
867 if not bpy
.data
.is_saved
:
868 self
.relative
= False
870 # this won't work in edit mode
871 editmode
= context
.preferences
.edit
.use_enter_edit_mode
872 context
.preferences
.edit
.use_enter_edit_mode
= False
873 if context
.active_object
and context
.active_object
.mode
!= 'OBJECT':
874 bpy
.ops
.object.mode_set(mode
='OBJECT')
876 self
.import_images(context
)
878 context
.preferences
.edit
.use_enter_edit_mode
= editmode
882 def import_images(self
, context
):
884 # load images / sequences
885 images
= tuple(load_images(
886 (fn
.name
for fn
in self
.files
),
888 force_reload
=self
.force_reload
,
889 find_sequences
=self
.image_sequence
892 # Create individual planes
893 planes
= [self
.single_image_spec_to_plane(context
, img_spec
) for img_spec
in images
]
895 context
.view_layer
.update()
897 # Align planes relative to each other
899 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
900 offset_planes(planes
, self
.offset_amount
, offset_axis
)
902 if self
.size_mode
== 'CAMERA' and offset_axis
.z
:
904 x
, y
= compute_camera_size(
905 context
, plane
.location
,
906 self
.fill_mode
, plane
.dimensions
.x
/ plane
.dimensions
.y
)
907 plane
.dimensions
= x
, y
, 0.0
909 # setup new selection
911 plane
.select_set(True)
914 self
.report({'INFO'}, "Added {} Image Plane(s)".format(len(planes
)))
916 # operate on a single image
917 def single_image_spec_to_plane(self
, context
, img_spec
):
920 self
.apply_image_options(img_spec
.image
)
923 engine
= context
.scene
.render
.engine
924 if engine
in {'CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}:
925 material
= self
.create_cycles_material(context
, img_spec
)
927 # Create and position plane object
928 plane
= self
.create_image_plane(context
, material
.name
, img_spec
)
931 plane
.data
.materials
.append(material
)
933 # If applicable, setup Corner Pin node
934 if self
.compositing_nodes
:
935 setup_compositing(context
, plane
, img_spec
)
939 def apply_image_options(self
, image
):
940 if self
.use_transparency
== False:
941 image
.alpha_mode
= 'NONE'
943 image
.alpha_mode
= self
.alpha_mode
946 try: # can't always find the relative path (between drive letters on windows)
947 image
.filepath
= bpy
.path
.relpath(image
.filepath
)
951 def apply_texture_options(self
, texture
, img_spec
):
952 # Shared by both Cycles and Blender Internal
953 image_user
= texture
.image_user
954 image_user
.use_auto_refresh
= self
.use_auto_refresh
955 image_user
.frame_start
= img_spec
.frame_start
956 image_user
.frame_offset
= img_spec
.frame_offset
957 image_user
.frame_duration
= img_spec
.frame_duration
959 # Image sequences need auto refresh to display reliably
960 if img_spec
.image
.source
== 'SEQUENCE':
961 image_user
.use_auto_refresh
= True
963 texture
.extension
= 'CLIP' # Default of "Repeat" can cause artifacts
965 def apply_material_options(self
, material
, slot
):
968 if self
.use_transparency
:
970 material
.specular_alpha
= 0.0
971 slot
.use_map_alpha
= True
974 material
.specular_alpha
= 1.0
975 slot
.use_map_alpha
= False
977 material
.specular_intensity
= 0
978 material
.diffuse_intensity
= 1.0
979 material
.use_transparency
= self
.use_transparency
980 material
.transparency_method
= 'Z_TRANSPARENCY'
981 material
.use_shadeless
= (shader
== 'SHADELESS')
982 material
.use_transparent_shadows
= (shader
== 'DIFFUSE')
983 material
.emit
= self
.emit_strength
if shader
== 'EMISSION' else 0.0
985 # -------------------------------------------------------------------------
987 def create_cycles_texnode(self
, context
, node_tree
, img_spec
):
988 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
989 tex_image
.image
= img_spec
.image
990 tex_image
.show_texture
= True
991 self
.apply_texture_options(tex_image
, img_spec
)
994 def create_cycles_material(self
, context
, img_spec
):
995 image
= img_spec
.image
996 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
998 if self
.overwrite_material
:
999 for mat
in bpy
.data
.materials
:
1000 if mat
.name
== name_compat
:
1003 material
= bpy
.data
.materials
.new(name
=name_compat
)
1005 material
.use_nodes
= True
1006 if self
.use_transparency
:
1007 material
.blend_method
= 'BLEND'
1008 node_tree
= material
.node_tree
1009 out_node
= clean_node_tree(node_tree
)
1011 tex_image
= self
.create_cycles_texnode(context
, node_tree
, img_spec
)
1013 if self
.shader
== 'PRINCIPLED':
1014 core_shader
= node_tree
.nodes
.new('ShaderNodeBsdfPrincipled')
1015 elif self
.shader
== 'SHADELESS':
1016 core_shader
= get_shadeless_node(node_tree
)
1017 elif self
.shader
== 'EMISSION':
1018 core_shader
= node_tree
.nodes
.new('ShaderNodeBsdfPrincipled')
1019 core_shader
.inputs
['Emission Strength'].default_value
= self
.emit_strength
1020 core_shader
.inputs
['Base Color'].default_value
= (0.0, 0.0, 0.0, 1.0)
1021 core_shader
.inputs
['Specular'].default_value
= 0.0
1023 # Connect color from texture
1024 if self
.shader
in {'PRINCIPLED', 'SHADELESS'}:
1025 node_tree
.links
.new(core_shader
.inputs
[0], tex_image
.outputs
['Color'])
1026 elif self
.shader
== 'EMISSION':
1027 node_tree
.links
.new(core_shader
.inputs
['Emission'], tex_image
.outputs
['Color'])
1029 if self
.use_transparency
:
1030 if self
.shader
in {'PRINCIPLED', 'EMISSION'}:
1031 node_tree
.links
.new(core_shader
.inputs
['Alpha'], tex_image
.outputs
['Alpha'])
1033 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
1035 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
1036 node_tree
.links
.new(mix_shader
.inputs
['Fac'], tex_image
.outputs
['Alpha'])
1037 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
['BSDF'])
1038 node_tree
.links
.new(mix_shader
.inputs
[2], core_shader
.outputs
[0])
1039 core_shader
= mix_shader
1041 node_tree
.links
.new(out_node
.inputs
['Surface'], core_shader
.outputs
[0])
1043 auto_align_nodes(node_tree
)
1046 # -------------------------------------------------------------------------
1048 def create_image_plane(self
, context
, name
, img_spec
):
1050 width
, height
= self
.compute_plane_size(context
, img_spec
)
1053 bpy
.ops
.mesh
.primitive_plane_add('INVOKE_REGION_WIN')
1054 plane
= context
.active_object
1055 # Why does mesh.primitive_plane_add leave the object in edit mode???
1056 if plane
.mode
!= 'OBJECT':
1057 bpy
.ops
.object.mode_set(mode
='OBJECT')
1058 plane
.dimensions
= width
, height
, 0.0
1059 plane
.data
.name
= plane
.name
= name
1060 bpy
.ops
.object.transform_apply(location
=False, rotation
=False, scale
=True)
1062 # If sizing for camera, also insert into the camera's field of view
1063 if self
.size_mode
== 'CAMERA':
1064 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
1065 translate_axis
= [0 if offset_axis
[i
] else 1 for i
in (0, 1)]
1066 center_in_camera(context
.scene
, context
.scene
.camera
, plane
, translate_axis
)
1068 self
.align_plane(context
, plane
)
1072 def compute_plane_size(self
, context
, img_spec
):
1073 """Given the image size in pixels and location, determine size of plane"""
1074 px
, py
= img_spec
.size
1077 if px
== 0 or py
== 0:
1080 if self
.size_mode
== 'ABSOLUTE':
1084 elif self
.size_mode
== 'CAMERA':
1085 x
, y
= compute_camera_size(
1086 context
, context
.scene
.cursor
.location
,
1087 self
.fill_mode
, px
/ py
1090 elif self
.size_mode
== 'DPI':
1091 fact
= 1 / self
.factor
/ context
.scene
.unit_settings
.scale_length
* 0.0254
1095 else: # elif self.size_mode == 'DPBU'
1096 fact
= 1 / self
.factor
1102 def align_plane(self
, context
, plane
):
1103 """Pick an axis and align the plane to it"""
1104 if 'CAM' in self
.align_axis
:
1106 camera
= context
.scene
.camera
1108 # Find the axis that best corresponds to the camera's view direction
1109 axis
= camera
.matrix_world
@ \
1110 Vector((0, 0, 1)) - camera
.matrix_world
.col
[3].xyz
1111 # pick the axis with the greatest magnitude
1112 mag
= max(map(abs, axis
))
1113 # And use that axis & direction
1115 n
/ mag
if abs(n
) == mag
else 0.0
1119 # No camera? Just face Z axis
1120 axis
= Vector((0, 0, 1))
1121 self
.align_axis
= 'Z+'
1124 axis
= self
.axis_id_to_vector
[self
.align_axis
]
1126 # rotate accordingly for x/y axiis
1128 plane
.rotation_euler
.x
= pi
/ 2
1131 plane
.rotation_euler
.z
= pi
1133 plane
.rotation_euler
.z
= 0
1135 plane
.rotation_euler
.z
= pi
/ 2
1137 plane
.rotation_euler
.z
= -pi
/ 2
1139 # or flip 180 degrees for negative z
1141 plane
.rotation_euler
.y
= pi
1143 if self
.align_axis
== 'CAM':
1144 constraint
= plane
.constraints
.new('COPY_ROTATION')
1145 constraint
.target
= camera
1146 constraint
.use_x
= constraint
.use_y
= constraint
.use_z
= True
1147 if not self
.align_track
:
1148 bpy
.ops
.object.visual_transform_apply()
1149 plane
.constraints
.clear()
1151 if self
.align_axis
== 'CAM_AX' and self
.align_track
:
1152 constraint
= plane
.constraints
.new('LOCKED_TRACK')
1153 constraint
.target
= camera
1154 constraint
.track_axis
= 'TRACK_Z'
1155 constraint
.lock_axis
= 'LOCK_Y'
1158 # -----------------------------------------------------------------------------
1161 def import_images_button(self
, context
):
1162 self
.layout
.operator(IMPORT_IMAGE_OT_to_plane
.bl_idname
, text
="Images as Planes", icon
='TEXTURE')
1166 IMPORT_IMAGE_OT_to_plane
,
1172 bpy
.utils
.register_class(cls
)
1174 bpy
.types
.TOPBAR_MT_file_import
.append(import_images_button
)
1175 bpy
.types
.VIEW3D_MT_image_add
.append(import_images_button
)
1177 bpy
.app
.handlers
.load_post
.append(register_driver
)
1182 bpy
.types
.TOPBAR_MT_file_import
.remove(import_images_button
)
1183 bpy
.types
.VIEW3D_MT_image_add
.remove(import_images_button
)
1185 # This will only exist if drivers are active
1186 if check_drivers
in bpy
.app
.handlers
.depsgraph_update_post
:
1187 bpy
.app
.handlers
.depsgraph_update_post
.remove(check_drivers
)
1189 bpy
.app
.handlers
.load_post
.remove(register_driver
)
1190 del bpy
.app
.driver_namespace
['import_image__find_plane_corner']
1193 bpy
.utils
.unregister_class(cls
)
1196 if __name__
== "__main__":
1197 # Run simple doc tests