1 # SPDX-FileCopyrightText: 2010-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 "name": "Import Images as Planes",
7 "author": "Florian Meyer (tstscr), mont29, matali, Ted Schundler (SpkyElctrc), mrbimax",
10 "location": "File > Import > Images as Planes or Add > Image > Images as Planes",
11 "description": "Imports images and creates planes with the appropriate aspect ratio. "
12 "The images are mapped to the planes.",
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/import_export/images_as_planes.html",
15 "support": 'OFFICIAL',
16 "category": "Import-Export",
22 from itertools
import count
, repeat
23 from collections
import namedtuple
27 from bpy
.types
import Operator
28 from bpy
.app
.translations
import pgettext_tip
as tip_
29 from mathutils
import Vector
31 from bpy
.props
import (
39 from bpy_extras
.object_utils
import (
44 from bpy_extras
.image_utils
import load_image
46 # -----------------------------------------------------------------------------
47 # Module-level Shared State
49 watched_objects
= {} # used to trigger compositor updates on scene updates
52 # -----------------------------------------------------------------------------
55 def add_driver_prop(driver
, name
, type, id, path
):
56 """Configure a new driver variable."""
57 dv
= driver
.variables
.new()
59 dv
.type = 'SINGLE_PROP'
60 target
= dv
.targets
[0]
63 target
.data_path
= path
66 # -----------------------------------------------------------------------------
69 ImageSpec
= namedtuple(
71 ['image', 'size', 'frame_start', 'frame_offset', 'frame_duration'])
73 num_regex
= re
.compile('[0-9]') # Find a single number
74 nums_regex
= re
.compile('[0-9]+') # Find a set of numbers
77 def find_image_sequences(files
):
78 """From a group of files, detect image sequences.
80 This returns a generator of tuples, which contain the filename,
81 start frame, and length of the detected sequence
83 >>> list(find_image_sequences([
84 ... "test2-001.jp2", "test2-002.jp2",
85 ... "test3-003.jp2", "test3-004.jp2", "test3-005.jp2", "test3-006.jp2",
87 [('blaah', 1, 1), ('test2-001.jp2', 1, 2), ('test3-003.jp2', 3, 4)]
90 files
= iter(sorted(files
))
96 for filename
in files
:
97 new_pattern
= num_regex
.sub('#', filename
)
98 new_matches
= list(map(int, nums_regex
.findall(filename
)))
99 if new_pattern
== pattern
:
100 # this file looks like it may be in sequence from the previous
102 # if there are multiple sets of numbers, figure out what changed
104 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
109 # did it only change by one?
110 for i
, prev
, cur
in zip(count(), matches
, new_matches
):
112 # We expect this to increment
122 # No continuation -> spit out what we found and reset counters
125 yield prev_file
, matches
[segment
], length
127 yield prev_file
, 1, 1
130 matches
= new_matches
131 pattern
= new_pattern
137 yield prev_file
, matches
[segment
], length
139 yield prev_file
, 1, 1
142 def load_images(filenames
, directory
, force_reload
=False, frame_start
=1, find_sequences
=False):
143 """Wrapper for bpy's load_image
145 Loads a set of images, movies, or even image sequences
146 Returns a generator of ImageSpec wrapper objects later used for texture setup
148 if find_sequences
: # if finding sequences, we need some pre-processing first
149 file_iter
= find_image_sequences(filenames
)
151 file_iter
= zip(filenames
, repeat(1), repeat(1))
153 for filename
, offset
, frames
in file_iter
:
154 image
= load_image(filename
, directory
, check_existing
=True, force_reload
=force_reload
)
156 # Size is unavailable for sequences, so we grab it early
157 size
= tuple(image
.size
)
159 if image
.source
== 'MOVIE':
161 # This number is only valid when read a second time in 2.77
162 # This repeated line is not a mistake
163 frames
= image
.frame_duration
164 frames
= image
.frame_duration
166 elif frames
> 1: # Not movie, but multiple frames -> image sequence
167 image
.source
= 'SEQUENCE'
169 yield ImageSpec(image
, size
, frame_start
, offset
- 1, frames
)
172 # -----------------------------------------------------------------------------
173 # Position & Size Helpers
175 def offset_planes(planes
, gap
, axis
):
176 """Offset planes from each other by `gap` amount along a _local_ vector `axis`
178 For example, offset_planes([obj1, obj2], 0.5, Vector(0, 0, 1)) will place
179 obj2 0.5 blender units away from obj1 along the local positive Z axis.
181 This is in local space, not world space, so all planes should share
182 a common scale and rotation.
186 for current
in planes
[1:]:
187 local_offset
= abs((prior
.dimensions
+ current
.dimensions
).dot(axis
)) / 2.0 + gap
189 offset
+= local_offset
* axis
190 current
.location
= current
.matrix_world
@ offset
195 def compute_camera_size(context
, center
, fill_mode
, aspect
):
196 """Determine how large an object needs to be to fit or fill the camera's field of view."""
197 scene
= context
.scene
198 camera
= scene
.camera
199 view_frame
= camera
.data
.view_frame(scene
=scene
)
201 Vector([max(v
[i
] for v
in view_frame
) for i
in range(3)]) - \
202 Vector([min(v
[i
] for v
in view_frame
) for i
in range(3)])
203 camera_aspect
= frame_size
.x
/ frame_size
.y
205 # Convert the frame size to the correct sizing at a given distance
206 if camera
.type == 'ORTHO':
207 frame_size
= frame_size
.xy
209 # Perspective transform
210 distance
= world_to_camera_view(scene
, camera
, center
).z
211 frame_size
= distance
* frame_size
.xy
/ (-view_frame
[0].z
)
213 # Determine what axis to match to the camera
214 match_axis
= 0 # match the Y axis size
215 match_aspect
= aspect
216 if (fill_mode
== 'FILL' and aspect
> camera_aspect
) or \
217 (fill_mode
== 'FIT' and aspect
< camera_aspect
):
218 match_axis
= 1 # match the X axis size
219 match_aspect
= 1.0 / aspect
221 # scale the other axis to the correct aspect
222 frame_size
[1 - match_axis
] = frame_size
[match_axis
] / match_aspect
227 def center_in_camera(scene
, camera
, obj
, axis
=(1, 1)):
228 """Center object along specified axis of the camera"""
229 camera_matrix_col
= camera
.matrix_world
.col
230 location
= obj
.location
232 # Vector from the camera's world coordinate center to the object's center
233 delta
= camera_matrix_col
[3].xyz
- location
235 # How far off center we are along the camera's local X
236 camera_x_mag
= delta
.dot(camera_matrix_col
[0].xyz
) * axis
[0]
237 # How far off center we are along the camera's local Y
238 camera_y_mag
= delta
.dot(camera_matrix_col
[1].xyz
) * axis
[1]
240 # Now offset only along camera local axis
241 offset
= camera_matrix_col
[0].xyz
* camera_x_mag
+ \
242 camera_matrix_col
[1].xyz
* camera_y_mag
244 obj
.location
= location
+ offset
247 # -----------------------------------------------------------------------------
250 def get_input_nodes(node
, links
):
251 """Get nodes that are a inputs to the given node"""
252 # Get all links going to node.
253 input_links
= {lnk
for lnk
in links
if lnk
.to_node
== node
}
254 # Sort those links, get their input nodes (and avoid doubles!).
257 for socket
in node
.inputs
:
259 for link
in input_links
:
262 # Node already treated!
264 elif link
.to_socket
== socket
:
265 sorted_nodes
.append(nd
)
268 input_links
-= done_links
272 def auto_align_nodes(node_tree
):
273 """Given a shader node tree, arrange nodes neatly relative to the output node."""
276 nodes
= node_tree
.nodes
277 links
= node_tree
.links
280 if node
.type == 'OUTPUT_MATERIAL' or node
.type == 'GROUP_OUTPUT':
284 else: # Just in case there is no output
288 from_nodes
= get_input_nodes(to_node
, links
)
289 for i
, node
in enumerate(from_nodes
):
290 node
.location
.x
= min(node
.location
.x
, to_node
.location
.x
- x_gap
)
291 node
.location
.y
= to_node
.location
.y
292 node
.location
.y
-= i
* y_gap
293 node
.location
.y
+= (len(from_nodes
) - 1) * y_gap
/ (len(from_nodes
))
299 def clean_node_tree(node_tree
):
300 """Clear all nodes in a shader node tree except the output.
302 Returns the output node
304 nodes
= node_tree
.nodes
305 for node
in list(nodes
): # copy to avoid altering the loop's data source
306 if not node
.type == 'OUTPUT_MATERIAL':
309 return node_tree
.nodes
[0]
312 def get_shadeless_node(dest_node_tree
):
313 """Return a "shadless" cycles/eevee node, creating a node group if nonexistent"""
315 node_tree
= bpy
.data
.node_groups
['IAP_SHADELESS']
318 # need to build node shadeless node group
319 node_tree
= bpy
.data
.node_groups
.new('IAP_SHADELESS', 'ShaderNodeTree')
320 output_node
= node_tree
.nodes
.new('NodeGroupOutput')
321 input_node
= node_tree
.nodes
.new('NodeGroupInput')
323 node_tree
.outputs
.new('NodeSocketShader', 'Shader')
324 node_tree
.inputs
.new('NodeSocketColor', 'Color')
326 # This could be faster as a transparent shader, but then no ambient occlusion
327 diffuse_shader
= node_tree
.nodes
.new('ShaderNodeBsdfDiffuse')
328 node_tree
.links
.new(diffuse_shader
.inputs
[0], input_node
.outputs
[0])
330 emission_shader
= node_tree
.nodes
.new('ShaderNodeEmission')
331 node_tree
.links
.new(emission_shader
.inputs
[0], input_node
.outputs
[0])
333 light_path
= node_tree
.nodes
.new('ShaderNodeLightPath')
334 is_glossy_ray
= light_path
.outputs
['Is Glossy Ray']
335 is_shadow_ray
= light_path
.outputs
['Is Shadow Ray']
336 ray_depth
= light_path
.outputs
['Ray Depth']
337 transmission_depth
= light_path
.outputs
['Transmission Depth']
339 unrefracted_depth
= node_tree
.nodes
.new('ShaderNodeMath')
340 unrefracted_depth
.operation
= 'SUBTRACT'
341 unrefracted_depth
.label
= 'Bounce Count'
342 node_tree
.links
.new(unrefracted_depth
.inputs
[0], ray_depth
)
343 node_tree
.links
.new(unrefracted_depth
.inputs
[1], transmission_depth
)
345 refracted
= node_tree
.nodes
.new('ShaderNodeMath')
346 refracted
.operation
= 'SUBTRACT'
347 refracted
.label
= 'Camera or Refracted'
348 refracted
.inputs
[0].default_value
= 1.0
349 node_tree
.links
.new(refracted
.inputs
[1], unrefracted_depth
.outputs
[0])
351 reflection_limit
= node_tree
.nodes
.new('ShaderNodeMath')
352 reflection_limit
.operation
= 'SUBTRACT'
353 reflection_limit
.label
= 'Limit Reflections'
354 reflection_limit
.inputs
[0].default_value
= 2.0
355 node_tree
.links
.new(reflection_limit
.inputs
[1], ray_depth
)
357 camera_reflected
= node_tree
.nodes
.new('ShaderNodeMath')
358 camera_reflected
.operation
= 'MULTIPLY'
359 camera_reflected
.label
= 'Camera Ray to Glossy'
360 node_tree
.links
.new(camera_reflected
.inputs
[0], reflection_limit
.outputs
[0])
361 node_tree
.links
.new(camera_reflected
.inputs
[1], is_glossy_ray
)
363 shadow_or_reflect
= node_tree
.nodes
.new('ShaderNodeMath')
364 shadow_or_reflect
.operation
= 'MAXIMUM'
365 shadow_or_reflect
.label
= 'Shadow or Reflection?'
366 node_tree
.links
.new(shadow_or_reflect
.inputs
[0], camera_reflected
.outputs
[0])
367 node_tree
.links
.new(shadow_or_reflect
.inputs
[1], is_shadow_ray
)
369 shadow_or_reflect_or_refract
= node_tree
.nodes
.new('ShaderNodeMath')
370 shadow_or_reflect_or_refract
.operation
= 'MAXIMUM'
371 shadow_or_reflect_or_refract
.label
= 'Shadow, Reflect or Refract?'
372 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[0], shadow_or_reflect
.outputs
[0])
373 node_tree
.links
.new(shadow_or_reflect_or_refract
.inputs
[1], refracted
.outputs
[0])
375 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
376 node_tree
.links
.new(mix_shader
.inputs
[0], shadow_or_reflect_or_refract
.outputs
[0])
377 node_tree
.links
.new(mix_shader
.inputs
[1], diffuse_shader
.outputs
[0])
378 node_tree
.links
.new(mix_shader
.inputs
[2], emission_shader
.outputs
[0])
380 node_tree
.links
.new(output_node
.inputs
[0], mix_shader
.outputs
[0])
382 auto_align_nodes(node_tree
)
384 group_node
= dest_node_tree
.nodes
.new("ShaderNodeGroup")
385 group_node
.node_tree
= node_tree
390 # -----------------------------------------------------------------------------
391 # Corner Pin Driver Helpers
393 @bpy.app
.handlers
.persistent
394 def check_drivers(*args
, **kwargs
):
395 """Check if watched objects in a scene have changed and trigger compositor update
397 This is part of a hack to ensure the compositor updates
398 itself when the objects used for drivers change.
400 It only triggers if transformation matricies change to avoid
401 a cyclic loop of updates.
403 if not watched_objects
:
404 # if there is nothing to watch, don't bother running this
405 bpy
.app
.handlers
.depsgraph_update_post
.remove(check_drivers
)
409 for name
, matrix
in list(watched_objects
.items()):
411 obj
= bpy
.data
.objects
[name
]
413 # The user must have removed this object
414 del watched_objects
[name
]
416 new_matrix
= tuple(map(tuple, obj
.matrix_world
)).__hash
__()
417 if new_matrix
!= matrix
:
418 watched_objects
[name
] = new_matrix
422 # Trick to re-evaluate drivers
423 bpy
.context
.scene
.frame_current
= bpy
.context
.scene
.frame_current
426 def register_watched_object(obj
):
427 """Register an object to be monitored for transformation changes"""
430 # known object? -> we're done
431 if name
in watched_objects
:
434 if not watched_objects
:
435 # make sure check_drivers is active
436 bpy
.app
.handlers
.depsgraph_update_post
.append(check_drivers
)
438 watched_objects
[name
] = None
441 def find_plane_corner(object_name
, x
, y
, axis
, camera
=None, *args
, **kwargs
):
442 """Find the location in camera space of a plane's corner"""
444 # I've added args / kwargs as a compatibility measure with future versions
445 warnings
.warn("Unknown Parameters Passed to \"Images as Planes\". Maybe you need to upgrade?")
447 plane
= bpy
.data
.objects
[object_name
]
449 # Passing in camera doesn't work before 2.78, so we use the current one
450 camera
= camera
or bpy
.context
.scene
.camera
452 # Hack to ensure compositor updates on future changes
453 register_watched_object(camera
)
454 register_watched_object(plane
)
456 scale
= plane
.scale
* 2.0
457 v
= plane
.dimensions
.copy()
460 v
= plane
.matrix_world
@ v
462 camera_vertex
= world_to_camera_view(
463 bpy
.context
.scene
, camera
, v
)
465 return camera_vertex
[axis
]
468 @bpy.app
.handlers
.persistent
469 def register_driver(*args
, **kwargs
):
470 """Register the find_plane_corner function for use with drivers"""
471 bpy
.app
.driver_namespace
['import_image__find_plane_corner'] = find_plane_corner
474 # -----------------------------------------------------------------------------
475 # Compositing Helpers
477 def group_in_frame(node_tree
, name
, nodes
):
478 frame_node
= node_tree
.nodes
.new("NodeFrame")
479 frame_node
.label
= name
480 frame_node
.name
= name
+ "_frame"
482 min_pos
= Vector(nodes
[0].location
)
483 max_pos
= min_pos
.copy()
486 top_left
= node
.location
487 bottom_right
= top_left
+ Vector((node
.width
, -node
.height
))
490 min_pos
[i
] = min(min_pos
[i
], top_left
[i
], bottom_right
[i
])
491 max_pos
[i
] = max(max_pos
[i
], top_left
[i
], bottom_right
[i
])
493 node
.parent
= frame_node
495 frame_node
.width
= max_pos
[0] - min_pos
[0] + 50
496 frame_node
.height
= max(max_pos
[1] - min_pos
[1] + 50, 450)
497 frame_node
.shrink
= True
502 def position_frame_bottom_left(node_tree
, frame_node
):
503 newpos
= Vector((100000, 100000)) # start reasonably far top / right
505 # Align with the furthest left
506 for node
in node_tree
.nodes
.values():
507 if node
!= frame_node
and node
.parent
!= frame_node
:
508 newpos
.x
= min(newpos
.x
, node
.location
.x
+ 30)
510 # As high as we can get without overlapping anything to the right
511 for node
in node_tree
.nodes
.values():
512 if node
!= frame_node
and not node
.parent
:
513 if node
.location
.x
< newpos
.x
+ frame_node
.width
:
514 print("Below", node
.name
, node
.location
, node
.height
, node
.dimensions
)
515 newpos
.y
= min(newpos
.y
, node
.location
.y
- max(node
.dimensions
.y
, node
.height
) - 20)
517 frame_node
.location
= newpos
520 def setup_compositing(context
, plane
, img_spec
):
521 # Node Groups only work with "new" dependency graph and even
522 # then it has some problems with not updating the first time
523 # So instead this groups with a node frame, which works reliably
525 scene
= context
.scene
526 scene
.use_nodes
= True
527 node_tree
= scene
.node_tree
530 image_node
= node_tree
.nodes
.new("CompositorNodeImage")
531 image_node
.name
= name
+ "_image"
532 image_node
.image
= img_spec
.image
533 image_node
.location
= Vector((0, 0))
534 image_node
.frame_start
= img_spec
.frame_start
535 image_node
.frame_offset
= img_spec
.frame_offset
536 image_node
.frame_duration
= img_spec
.frame_duration
538 scale_node
= node_tree
.nodes
.new("CompositorNodeScale")
539 scale_node
.name
= name
+ "_scale"
540 scale_node
.space
= 'RENDER_SIZE'
541 scale_node
.location
= image_node
.location
+ \
542 Vector((image_node
.width
+ 20, 0))
543 scale_node
.show_options
= False
545 cornerpin_node
= node_tree
.nodes
.new("CompositorNodeCornerPin")
546 cornerpin_node
.name
= name
+ "_cornerpin"
547 cornerpin_node
.location
= scale_node
.location
+ \
548 Vector((0, -scale_node
.height
))
550 node_tree
.links
.new(scale_node
.inputs
[0], image_node
.outputs
[0])
551 node_tree
.links
.new(cornerpin_node
.inputs
[0], scale_node
.outputs
[0])
553 # Put all the nodes in a frame for organization
554 frame_node
= group_in_frame(
556 (image_node
, scale_node
, cornerpin_node
)
559 # Position frame at bottom / left
560 position_frame_bottom_left(node_tree
, frame_node
)
563 for corner
in cornerpin_node
.inputs
[1:]:
564 id = corner
.identifier
565 x
= -1 if 'Left' in id else 1
566 y
= -1 if 'Lower' in id else 1
567 drivers
= corner
.driver_add('default_value')
568 for i
, axis_fcurve
in enumerate(drivers
):
569 driver
= axis_fcurve
.driver
570 # Always use the current camera
571 add_driver_prop(driver
, 'camera', 'SCENE', scene
, 'camera')
572 # Track camera location to ensure Deps Graph triggers (not used in the call)
573 add_driver_prop(driver
, 'cam_loc_x', 'OBJECT', scene
.camera
, 'location[0]')
574 # Don't break if the name changes
575 add_driver_prop(driver
, 'name', 'OBJECT', plane
, 'name')
576 driver
.expression
= "import_image__find_plane_corner(name or %s, %d, %d, %d, camera=camera)" % (
580 driver
.type = 'SCRIPTED'
581 driver
.is_valid
= True
582 axis_fcurve
.is_valid
= True
583 driver
.expression
= "%s" % driver
.expression
585 context
.view_layer
.update()
588 # -----------------------------------------------------------------------------
591 class IMPORT_IMAGE_OT_to_plane(Operator
, AddObjectHelper
):
592 """Create mesh plane(s) from image files with the appropriate aspect ratio"""
594 bl_idname
= "import_image.to_plane"
595 bl_label
= "Import Images as Planes"
596 bl_options
= {'REGISTER', 'PRESET', 'UNDO'}
598 # ----------------------
599 # File dialog properties
600 files
: CollectionProperty(type=bpy
.types
.OperatorFileListElement
, options
={'HIDDEN', 'SKIP_SAVE'})
602 directory
: StringProperty(maxlen
=1024, subtype
='FILE_PATH', options
={'HIDDEN', 'SKIP_SAVE'})
604 filter_image
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
605 filter_movie
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
606 filter_folder
: BoolProperty(default
=True, options
={'HIDDEN', 'SKIP_SAVE'})
608 # ----------------------
609 # Properties - Importing
610 force_reload
: BoolProperty(
611 name
="Force Reload", default
=False,
612 description
="Force reloading of the image if already opened elsewhere in Blender"
615 image_sequence
: BoolProperty(
616 name
="Animate Image Sequences", default
=False,
617 description
="Import sequentially numbered images as an animated "
618 "image sequence instead of separate planes"
621 # -------------------------------------
622 # Properties - Position and Orientation
623 axis_id_to_vector
= {
624 'X+': Vector(( 1, 0, 0)),
625 'Y+': Vector(( 0, 1, 0)),
626 'Z+': Vector(( 0, 0, 1)),
627 'X-': Vector((-1, 0, 0)),
628 'Y-': Vector(( 0, -1, 0)),
629 'Z-': Vector(( 0, 0, -1)),
632 offset
: BoolProperty(name
="Offset Planes", default
=True, description
="Offset Planes From Each Other")
635 ('X+', "X+", "Side by Side to the Left"),
636 ('Y+', "Y+", "Side by Side, Downward"),
637 ('Z+', "Z+", "Stacked Above"),
638 ('X-', "X-", "Side by Side to the Right"),
639 ('Y-', "Y-", "Side by Side, Upward"),
640 ('Z-', "Z-", "Stacked Below"),
642 offset_axis
: EnumProperty(
643 name
="Orientation", default
='X+', items
=OFFSET_MODES
,
644 description
="How planes are oriented relative to each others' local axis"
647 offset_amount
: FloatProperty(
648 name
="Offset", soft_min
=0, default
=0.1, description
="Space between planes",
649 subtype
='DISTANCE', unit
='LENGTH'
653 ('X+', "X+", "Facing Positive X"),
654 ('Y+', "Y+", "Facing Positive Y"),
655 ('Z+', "Z+ (Up)", "Facing Positive Z"),
656 ('X-', "X-", "Facing Negative X"),
657 ('Y-', "Y-", "Facing Negative Y"),
658 ('Z-', "Z- (Down)", "Facing Negative Z"),
659 ('CAM', "Face Camera", "Facing Camera"),
660 ('CAM_AX', "Main Axis", "Facing the Camera's dominant axis"),
662 align_axis
: EnumProperty(
663 name
="Align", default
='CAM_AX', items
=AXIS_MODES
,
664 description
="How to align the planes"
666 # prev_align_axis is used only by update_size_model
667 prev_align_axis
: EnumProperty(
668 items
=AXIS_MODES
+ (('NONE', '', ''),), default
='NONE', options
={'HIDDEN', 'SKIP_SAVE'})
669 align_track
: BoolProperty(
670 name
="Track Camera", default
=False, description
="Always face the camera"
675 def update_size_mode(self
, context
):
676 """If sizing relative to the camera, always face the camera"""
677 if self
.size_mode
== 'CAMERA':
678 self
.prev_align_axis
= self
.align_axis
679 self
.align_axis
= 'CAM'
681 # if a different alignment was set revert to that when
682 # size mode is changed
683 if self
.prev_align_axis
!= 'NONE':
684 self
.align_axis
= self
.prev_align_axis
685 self
._prev
_align
_axis
= 'NONE'
688 ('ABSOLUTE', "Absolute", "Use absolute size"),
689 ('CAMERA', "Camera Relative", "Scale to the camera frame"),
690 ('DPI', "Dpi", "Use definition of the image as dots per inch"),
691 ('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
693 size_mode
: EnumProperty(
694 name
="Size Mode", default
='ABSOLUTE', items
=SIZE_MODES
,
695 update
=update_size_mode
,
696 description
="How the size of the plane is computed")
699 ('FILL', "Fill", "Fill camera frame, spilling outside the frame"),
700 ('FIT', "Fit", "Fit entire image within the camera frame"),
702 fill_mode
: EnumProperty(name
="Scale", default
='FILL', items
=FILL_MODES
,
703 description
="How large in the camera frame is the plane")
705 height
: FloatProperty(name
="Height", description
="Height of the created plane",
706 default
=1.0, min=0.001, soft_min
=0.001, subtype
='DISTANCE', unit
='LENGTH')
708 factor
: FloatProperty(name
="Definition", min=1.0, default
=600.0,
709 description
="Number of pixels per inch or Blender Unit")
711 # ------------------------------
712 # Properties - Material / Shader
714 ('PRINCIPLED',"Principled","Principled Shader"),
715 ('SHADELESS', "Shadeless", "Only visible to camera and reflections"),
716 ('EMISSION', "Emit", "Emission Shader"),
718 shader
: EnumProperty(name
="Shader", items
=SHADERS
, default
='PRINCIPLED', description
="Node shader to use")
720 emit_strength
: FloatProperty(
721 name
="Strength", min=0.0, default
=1.0, soft_max
=10.0,
722 step
=100, description
="Brightness of Emission Texture")
724 use_transparency
: BoolProperty(
725 name
="Use Alpha", default
=True,
726 description
="Use alpha channel for transparency")
729 ('BLEND',"Blend","Render polygon transparent, depending on alpha channel of the texture"),
730 ('CLIP', "Clip","Use the alpha threshold to clip the visibility (binary visibility)"),
731 ('HASHED', "Hashed","Use noise to dither the binary visibility (works well with multi-samples)"),
732 ('OPAQUE', "Opaque","Render surface without transparency"),
734 blend_method
: EnumProperty(name
="Blend Mode", items
=BLEND_METHODS
, default
='BLEND', description
="Blend Mode for Transparent Faces")
737 ('CLIP', "Clip","Use the alpha threshold to clip the visibility (binary visibility)"),
738 ('HASHED', "Hashed","Use noise to dither the binary visibility (works well with multi-samples)"),
739 ('OPAQUE',"Opaque","Material will cast shadows without transparency"),
740 ('NONE',"None","Material will cast no shadow"),
742 shadow_method
: EnumProperty(name
="Shadow Mode", items
=SHADOW_METHODS
, default
='CLIP', description
="Shadow mapping method")
744 use_backface_culling
: BoolProperty(
745 name
="Backface Culling", default
=False,
746 description
="Use back face culling to hide the back side of faces")
748 show_transparent_back
: BoolProperty(
749 name
="Show Backface", default
=True,
750 description
="Render multiple transparent layers (may introduce transparency sorting problems)")
752 overwrite_material
: BoolProperty(
753 name
="Overwrite Material", default
=True,
754 description
="Overwrite existing Material (based on material name)")
756 compositing_nodes
: BoolProperty(
757 name
="Setup Corner Pin", default
=False,
758 description
="Build Compositor Nodes to reference this image "
759 "without re-rendering")
763 INTERPOLATION_MODES
= (
764 ('Linear', "Linear", "Linear interpolation"),
765 ('Closest', "Closest", "No interpolation (sample closest texel)"),
766 ('Cubic', "Cubic", "Cubic interpolation"),
767 ('Smart', "Smart", "Bicubic when magnifying, else bilinear (OSL only)"),
769 interpolation
: EnumProperty(name
="Interpolation", items
=INTERPOLATION_MODES
, default
='Linear', description
="Texture interpolation")
772 ('CLIP', "Clip", "Clip to image size and set exterior pixels as transparent"),
773 ('EXTEND', "Extend", "Extend by repeating edge pixels of the image"),
774 ('REPEAT', "Repeat", "Cause the image to repeat horizontally and vertically"),
776 extension
: EnumProperty(name
="Extension", items
=EXTENSION_MODES
, default
='CLIP', description
="How the image is extrapolated past its original bounds")
778 t
= bpy
.types
.Image
.bl_rna
.properties
["alpha_mode"]
779 alpha_mode_items
= tuple((e
.identifier
, e
.name
, e
.description
) for e
in t
.enum_items
)
780 alpha_mode
: EnumProperty(
781 name
=t
.name
, items
=alpha_mode_items
, default
=t
.default
,
782 description
=t
.description
)
784 t
= bpy
.types
.ImageUser
.bl_rna
.properties
["use_auto_refresh"]
785 use_auto_refresh
: BoolProperty(name
=t
.name
, default
=True, description
=t
.description
)
787 relative
: BoolProperty(name
="Relative Paths", default
=True, description
="Use relative file paths")
791 def draw_import_config(self
, context
):
792 # --- Import Options --- #
796 box
.label(text
="Import Options:", icon
='IMPORT')
798 row
.active
= bpy
.data
.is_saved
799 row
.prop(self
, "relative")
801 box
.prop(self
, "force_reload")
802 box
.prop(self
, "image_sequence")
804 def draw_material_config(self
, context
):
805 # --- Material / Rendering Properties --- #
809 box
.label(text
="Compositing Nodes:", icon
='RENDERLAYERS')
810 box
.prop(self
, "compositing_nodes")
813 box
.label(text
="Material Settings:", icon
='MATERIAL')
815 box
.label(text
="Material Type")
817 row
.prop(self
, 'shader', expand
=True)
818 if self
.shader
== 'EMISSION':
819 box
.prop(self
, "emit_strength")
821 box
.label(text
="Blend Mode")
823 row
.prop(self
, 'blend_method', expand
=True)
824 if self
.use_transparency
and self
.alpha_mode
!= "NONE" and self
.blend_method
== "OPAQUE":
825 box
.label(text
="'Opaque' does not support alpha", icon
="ERROR")
826 if self
.blend_method
== 'BLEND':
828 row
.prop(self
, "show_transparent_back")
830 box
.label(text
="Shadow Mode")
832 row
.prop(self
, 'shadow_method', expand
=True)
835 row
.prop(self
, "use_backface_culling")
837 engine
= context
.scene
.render
.engine
838 if engine
not in ('CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'):
839 box
.label(text
=tip_("%s is not supported") % engine
, icon
='ERROR')
841 box
.prop(self
, "overwrite_material")
844 box
.label(text
="Texture Settings:", icon
='TEXTURE')
845 box
.label(text
="Interpolation")
847 row
.prop(self
, 'interpolation', expand
=True)
848 box
.label(text
="Extension")
850 row
.prop(self
, 'extension', expand
=True)
852 row
.prop(self
, "use_transparency")
853 if self
.use_transparency
:
855 sub
.prop(self
, "alpha_mode", text
="")
857 row
.prop(self
, "use_auto_refresh")
859 def draw_spatial_config(self
, context
):
860 # --- Spatial Properties: Position, Size and Orientation --- #
864 box
.label(text
="Position:", icon
='SNAP_GRID')
865 box
.prop(self
, "offset")
868 row
.prop(self
, "offset_axis", expand
=True)
870 row
.prop(self
, "offset_amount")
871 col
.enabled
= self
.offset
873 box
.label(text
="Plane dimensions:", icon
='ARROW_LEFTRIGHT')
875 row
.prop(self
, "size_mode", expand
=True)
876 if self
.size_mode
== 'ABSOLUTE':
877 box
.prop(self
, "height")
878 elif self
.size_mode
== 'CAMERA':
880 row
.prop(self
, "fill_mode", expand
=True)
882 box
.prop(self
, "factor")
884 box
.label(text
="Orientation:")
886 row
.enabled
= 'CAM' not in self
.size_mode
887 row
.prop(self
, "align_axis")
889 row
.enabled
= 'CAM' in self
.align_axis
890 row
.alignment
= 'RIGHT'
891 row
.prop(self
, "align_track")
893 def draw(self
, context
):
895 # Draw configuration sections
896 self
.draw_import_config(context
)
897 self
.draw_material_config(context
)
898 self
.draw_spatial_config(context
)
900 # -------------------------------------------------------------------------
902 def invoke(self
, context
, event
):
903 engine
= context
.scene
.render
.engine
904 if engine
not in {'CYCLES', 'BLENDER_EEVEE'}:
905 if engine
!= 'BLENDER_WORKBENCH':
906 self
.report({'ERROR'}, tip_("Cannot generate materials for unknown %s render engine") % engine
)
909 self
.report({'WARNING'},
910 tip_("Generating Cycles/EEVEE compatible material, but won't be visible with %s engine") % engine
)
913 context
.window_manager
.fileselect_add(self
)
914 return {'RUNNING_MODAL'}
916 def execute(self
, context
):
917 if not bpy
.data
.is_saved
:
918 self
.relative
= False
920 # this won't work in edit mode
921 editmode
= context
.preferences
.edit
.use_enter_edit_mode
922 context
.preferences
.edit
.use_enter_edit_mode
= False
923 if context
.active_object
and context
.active_object
.mode
!= 'OBJECT':
924 bpy
.ops
.object.mode_set(mode
='OBJECT')
926 self
.import_images(context
)
928 context
.preferences
.edit
.use_enter_edit_mode
= editmode
932 def import_images(self
, context
):
934 # load images / sequences
935 images
= tuple(load_images(
936 (fn
.name
for fn
in self
.files
),
938 force_reload
=self
.force_reload
,
939 find_sequences
=self
.image_sequence
942 # Create individual planes
943 planes
= [self
.single_image_spec_to_plane(context
, img_spec
) for img_spec
in images
]
945 context
.view_layer
.update()
947 # Align planes relative to each other
949 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
950 offset_planes(planes
, self
.offset_amount
, offset_axis
)
952 if self
.size_mode
== 'CAMERA' and offset_axis
.z
:
954 x
, y
= compute_camera_size(
955 context
, plane
.location
,
956 self
.fill_mode
, plane
.dimensions
.x
/ plane
.dimensions
.y
)
957 plane
.dimensions
= x
, y
, 0.0
959 # setup new selection
961 plane
.select_set(True)
964 self
.report({'INFO'}, tip_("Added {} Image Plane(s)").format(len(planes
)))
966 # operate on a single image
967 def single_image_spec_to_plane(self
, context
, img_spec
):
970 self
.apply_image_options(img_spec
.image
)
973 engine
= context
.scene
.render
.engine
974 if engine
in {'CYCLES', 'BLENDER_EEVEE', 'BLENDER_WORKBENCH'}:
975 material
= self
.create_cycles_material(context
, img_spec
)
977 # Create and position plane object
978 plane
= self
.create_image_plane(context
, material
.name
, img_spec
)
981 plane
.data
.materials
.append(material
)
983 # If applicable, setup Corner Pin node
984 if self
.compositing_nodes
:
985 setup_compositing(context
, plane
, img_spec
)
989 def apply_image_options(self
, image
):
990 if self
.use_transparency
== False:
991 image
.alpha_mode
= 'NONE'
993 image
.alpha_mode
= self
.alpha_mode
996 try: # can't always find the relative path (between drive letters on windows)
997 image
.filepath
= bpy
.path
.relpath(image
.filepath
)
1001 def apply_texture_options(self
, texture
, img_spec
):
1002 # Shared by both Cycles and Blender Internal
1003 image_user
= texture
.image_user
1004 image_user
.use_auto_refresh
= self
.use_auto_refresh
1005 image_user
.frame_start
= img_spec
.frame_start
1006 image_user
.frame_offset
= img_spec
.frame_offset
1007 image_user
.frame_duration
= img_spec
.frame_duration
1009 # Image sequences need auto refresh to display reliably
1010 if img_spec
.image
.source
== 'SEQUENCE':
1011 image_user
.use_auto_refresh
= True
1013 def apply_material_options(self
, material
, slot
):
1014 shader
= self
.shader
1016 if self
.use_transparency
:
1017 material
.alpha
= 0.0
1018 material
.specular_alpha
= 0.0
1019 slot
.use_map_alpha
= True
1021 material
.alpha
= 1.0
1022 material
.specular_alpha
= 1.0
1023 slot
.use_map_alpha
= False
1025 material
.specular_intensity
= 0
1026 material
.diffuse_intensity
= 1.0
1027 material
.use_transparency
= self
.use_transparency
1028 material
.transparency_method
= 'Z_TRANSPARENCY'
1029 material
.use_shadeless
= (shader
== 'SHADELESS')
1030 material
.use_transparent_shadows
= (shader
== 'DIFFUSE')
1031 material
.emit
= self
.emit_strength
if shader
== 'EMISSION' else 0.0
1033 # -------------------------------------------------------------------------
1035 def create_cycles_texnode(self
, context
, node_tree
, img_spec
):
1036 tex_image
= node_tree
.nodes
.new('ShaderNodeTexImage')
1037 tex_image
.image
= img_spec
.image
1038 tex_image
.show_texture
= True
1039 tex_image
.interpolation
= self
.interpolation
1040 tex_image
.extension
= self
.extension
1041 self
.apply_texture_options(tex_image
, img_spec
)
1044 def create_cycles_material(self
, context
, img_spec
):
1045 image
= img_spec
.image
1046 name_compat
= bpy
.path
.display_name_from_filepath(image
.filepath
)
1048 if self
.overwrite_material
:
1049 for mat
in bpy
.data
.materials
:
1050 if mat
.name
== name_compat
:
1053 material
= bpy
.data
.materials
.new(name
=name_compat
)
1055 material
.use_nodes
= True
1057 material
.blend_method
= self
.blend_method
1058 material
.shadow_method
= self
.shadow_method
1060 material
.use_backface_culling
= self
.use_backface_culling
1061 material
.show_transparent_back
= self
.show_transparent_back
1063 node_tree
= material
.node_tree
1064 out_node
= clean_node_tree(node_tree
)
1066 tex_image
= self
.create_cycles_texnode(context
, node_tree
, img_spec
)
1068 if self
.shader
== 'PRINCIPLED':
1069 core_shader
= node_tree
.nodes
.new('ShaderNodeBsdfPrincipled')
1070 elif self
.shader
== 'SHADELESS':
1071 core_shader
= get_shadeless_node(node_tree
)
1072 elif self
.shader
== 'EMISSION':
1073 core_shader
= node_tree
.nodes
.new('ShaderNodeBsdfPrincipled')
1074 core_shader
.inputs
['Emission Strength'].default_value
= self
.emit_strength
1075 core_shader
.inputs
['Base Color'].default_value
= (0.0, 0.0, 0.0, 1.0)
1076 core_shader
.inputs
['Specular'].default_value
= 0.0
1078 # Connect color from texture
1079 if self
.shader
in {'PRINCIPLED', 'SHADELESS'}:
1080 node_tree
.links
.new(core_shader
.inputs
[0], tex_image
.outputs
['Color'])
1081 elif self
.shader
== 'EMISSION':
1082 node_tree
.links
.new(core_shader
.inputs
['Emission'], tex_image
.outputs
['Color'])
1084 if self
.use_transparency
:
1085 if self
.shader
in {'PRINCIPLED', 'EMISSION'}:
1086 node_tree
.links
.new(core_shader
.inputs
['Alpha'], tex_image
.outputs
['Alpha'])
1088 bsdf_transparent
= node_tree
.nodes
.new('ShaderNodeBsdfTransparent')
1090 mix_shader
= node_tree
.nodes
.new('ShaderNodeMixShader')
1091 node_tree
.links
.new(mix_shader
.inputs
['Fac'], tex_image
.outputs
['Alpha'])
1092 node_tree
.links
.new(mix_shader
.inputs
[1], bsdf_transparent
.outputs
['BSDF'])
1093 node_tree
.links
.new(mix_shader
.inputs
[2], core_shader
.outputs
[0])
1094 core_shader
= mix_shader
1096 node_tree
.links
.new(out_node
.inputs
['Surface'], core_shader
.outputs
[0])
1098 auto_align_nodes(node_tree
)
1101 # -------------------------------------------------------------------------
1103 def create_image_plane(self
, context
, name
, img_spec
):
1105 width
, height
= self
.compute_plane_size(context
, img_spec
)
1108 bpy
.ops
.mesh
.primitive_plane_add('INVOKE_REGION_WIN')
1109 plane
= context
.active_object
1110 # Why does mesh.primitive_plane_add leave the object in edit mode???
1111 if plane
.mode
!= 'OBJECT':
1112 bpy
.ops
.object.mode_set(mode
='OBJECT')
1113 plane
.dimensions
= width
, height
, 0.0
1114 plane
.data
.name
= plane
.name
= name
1115 bpy
.ops
.object.transform_apply(location
=False, rotation
=False, scale
=True)
1117 # If sizing for camera, also insert into the camera's field of view
1118 if self
.size_mode
== 'CAMERA':
1119 offset_axis
= self
.axis_id_to_vector
[self
.offset_axis
]
1120 translate_axis
= [0 if offset_axis
[i
] else 1 for i
in (0, 1)]
1121 center_in_camera(context
.scene
, context
.scene
.camera
, plane
, translate_axis
)
1123 self
.align_plane(context
, plane
)
1127 def compute_plane_size(self
, context
, img_spec
):
1128 """Given the image size in pixels and location, determine size of plane"""
1129 px
, py
= img_spec
.size
1132 if px
== 0 or py
== 0:
1135 if self
.size_mode
== 'ABSOLUTE':
1139 elif self
.size_mode
== 'CAMERA':
1140 x
, y
= compute_camera_size(
1141 context
, context
.scene
.cursor
.location
,
1142 self
.fill_mode
, px
/ py
1145 elif self
.size_mode
== 'DPI':
1146 fact
= 1 / self
.factor
/ context
.scene
.unit_settings
.scale_length
* 0.0254
1150 else: # elif self.size_mode == 'DPBU'
1151 fact
= 1 / self
.factor
1157 def align_plane(self
, context
, plane
):
1158 """Pick an axis and align the plane to it"""
1159 if 'CAM' in self
.align_axis
:
1161 camera
= context
.scene
.camera
1163 # Find the axis that best corresponds to the camera's view direction
1164 axis
= camera
.matrix_world
@ \
1165 Vector((0, 0, 1)) - camera
.matrix_world
.col
[3].xyz
1166 # pick the axis with the greatest magnitude
1167 mag
= max(map(abs, axis
))
1168 # And use that axis & direction
1170 n
/ mag
if abs(n
) == mag
else 0.0
1174 # No camera? Just face Z axis
1175 axis
= Vector((0, 0, 1))
1176 self
.align_axis
= 'Z+'
1179 axis
= self
.axis_id_to_vector
[self
.align_axis
]
1181 # rotate accordingly for x/y axiis
1183 plane
.rotation_euler
.x
= pi
/ 2
1186 plane
.rotation_euler
.z
= pi
1188 plane
.rotation_euler
.z
= 0
1190 plane
.rotation_euler
.z
= pi
/ 2
1192 plane
.rotation_euler
.z
= -pi
/ 2
1194 # or flip 180 degrees for negative z
1196 plane
.rotation_euler
.y
= pi
1198 if self
.align_axis
== 'CAM':
1199 constraint
= plane
.constraints
.new('COPY_ROTATION')
1200 constraint
.target
= camera
1201 constraint
.use_x
= constraint
.use_y
= constraint
.use_z
= True
1202 if not self
.align_track
:
1203 bpy
.ops
.object.visual_transform_apply()
1204 plane
.constraints
.clear()
1206 if self
.align_axis
== 'CAM_AX' and self
.align_track
:
1207 constraint
= plane
.constraints
.new('LOCKED_TRACK')
1208 constraint
.target
= camera
1209 constraint
.track_axis
= 'TRACK_Z'
1210 constraint
.lock_axis
= 'LOCK_Y'
1213 # -----------------------------------------------------------------------------
1216 def import_images_button(self
, context
):
1217 self
.layout
.operator(IMPORT_IMAGE_OT_to_plane
.bl_idname
, text
="Images as Planes", icon
='TEXTURE')
1221 IMPORT_IMAGE_OT_to_plane
,
1227 bpy
.utils
.register_class(cls
)
1229 bpy
.types
.TOPBAR_MT_file_import
.append(import_images_button
)
1230 bpy
.types
.VIEW3D_MT_image_add
.append(import_images_button
)
1232 bpy
.app
.handlers
.load_post
.append(register_driver
)
1237 bpy
.types
.TOPBAR_MT_file_import
.remove(import_images_button
)
1238 bpy
.types
.VIEW3D_MT_image_add
.remove(import_images_button
)
1240 # This will only exist if drivers are active
1241 if check_drivers
in bpy
.app
.handlers
.depsgraph_update_post
:
1242 bpy
.app
.handlers
.depsgraph_update_post
.remove(check_drivers
)
1244 bpy
.app
.handlers
.load_post
.remove(register_driver
)
1245 del bpy
.app
.driver_namespace
['import_image__find_plane_corner']
1248 bpy
.utils
.unregister_class(cls
)
1251 if __name__
== "__main__":
1252 # Run simple doc tests