Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormeta-androcto <meta.androcto1@gmail.com>2017-01-15 07:32:56 +0300
committermeta-androcto <meta.androcto1@gmail.com>2017-01-15 07:32:56 +0300
commit97812d737e446da73f24754340cc9b7e6003c774 (patch)
tree415bb5b69e9de98ce6e458872089a1f7175e2d53 /io_import_images_as_planes.py
parent3984378b7b6af165ea047e56ea4f4b6fb7b2dd76 (diff)
import images as planes rewrite: D2239
Diffstat (limited to 'io_import_images_as_planes.py')
-rw-r--r--io_import_images_as_planes.py1403
1 files changed, 1119 insertions, 284 deletions
diff --git a/io_import_images_as_planes.py b/io_import_images_as_planes.py
index 7ba5bfa2..26dcba38 100644
--- a/io_import_images_as_planes.py
+++ b/io_import_images_as_planes.py
@@ -16,11 +16,13 @@
#
# ##### END GPL LICENSE BLOCK #####
+# <pep8 compliant>
+
bl_info = {
"name": "Import Images as Planes",
- "author": "Florian Meyer (tstscr), mont29, matali",
- "version": (2, 0, 5),
- "blender": (2, 76, 1),
+ "author": "Florian Meyer (tstscr), mont29, matali, Ted Schundler (SpkyElctrc)",
+ "version": (3, 0, 0),
+ "blender": (2, 77, 0),
"location": "File > Import > Images as Planes or Add > Mesh > Images as Planes",
"description": "Imports images and creates planes with the appropriate aspect ratio. "
"The images are mapped to the planes.",
@@ -30,72 +32,262 @@ bl_info = {
"category": "Import-Export",
}
+import os
+import warnings
+import re
+from itertools import count, repeat
+from collections import namedtuple
+from math import pi
+
import bpy
from bpy.types import Operator
-import mathutils
-import os
-import collections
+from mathutils import Vector
from bpy.props import (
- StringProperty,
- BoolProperty,
- EnumProperty,
- IntProperty,
- FloatProperty,
- CollectionProperty,
- )
+ StringProperty,
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+ CollectionProperty)
+
+from bpy_extras.object_utils import (
+ AddObjectHelper,
+ world_to_camera_view)
-from bpy_extras.object_utils import AddObjectHelper, object_data_add
from bpy_extras.image_utils import load_image
# -----------------------------------------------------------------------------
-# Global Vars
-
-DEFAULT_EXT = "*"
-
-EXT_FILTER = getattr(collections, "OrderedDict", dict)((
- (DEFAULT_EXT, ((), "All image formats", "Import all known image (or movie) formats.")),
- ("jpeg", (("jpeg", "jpg", "jpe"), "JPEG ({})", "Joint Photographic Experts Group")),
- ("png", (("png", ), "PNG ({})", "Portable Network Graphics")),
- ("tga", (("tga", "tpic"), "Truevision TGA ({})", "")),
- ("tiff", (("tiff", "tif"), "TIFF ({})", "Tagged Image File Format")),
- ("bmp", (("bmp", "dib"), "BMP ({})", "Windows Bitmap")),
- ("cin", (("cin", ), "CIN ({})", "")),
- ("dpx", (("dpx", ), "DPX ({})", "DPX (Digital Picture Exchange)")),
- ("psd", (("psd", ), "PSD ({})", "Photoshop Document")),
- ("exr", (("exr", ), "OpenEXR ({})", "OpenEXR HDR imaging image file format")),
- ("hdr", (("hdr", "pic"), "Radiance HDR ({})", "")),
- ("avi", (("avi", ), "AVI ({})", "Audio Video Interleave")),
- ("mov", (("mov", "qt"), "QuickTime ({})", "")),
- ("mp4", (("mp4", ), "MPEG-4 ({})", "MPEG-4 Part 14")),
- ("ogg", (("ogg", "ogv"), "OGG Theora ({})", "")),
-))
-
-# XXX Hack to avoid allowing videos with Cycles, crashes currently!
-VID_EXT_FILTER = {e for ext_k, ext_v in EXT_FILTER.items() if ext_k in {"avi", "mov", "mp4", "ogg"} for e in ext_v[0]}
-
-CYCLES_SHADERS = (
- ('BSDF_DIFFUSE', "Diffuse", "Diffuse Shader"),
- ('EMISSION', "Emission", "Emission Shader")
-)
+# Module-level Shared State
+
+watched_objects = {} # used to trigger compositor updates on scene updates
+
# -----------------------------------------------------------------------------
# Misc utils.
-def gen_ext_filter_ui_items():
- return tuple((k, name.format(", ".join("." + e for e in exts)) if "{}" in name else name, desc)
- for k, (exts, name, desc) in EXT_FILTER.items())
+def str_to_vector(s):
+ """Convert a string into a vector.
+
+ >>> str_to_vector("1,2,3")
+ Vector((1.0, 2.0, 3.0))
+
+ >>> str_to_vector("1.0, 0.0, 0, 1")
+ Vector((1.0, 0.0, 0.0, 1.0))
+ """
+ return Vector(map(float, s.split(',')))
+
+
+def add_driver_prop(driver, name, type, id, path):
+ """Configure a new driver variable."""
+ dv = driver.variables.new()
+ dv.name = name
+ dv.type = 'SINGLE_PROP'
+ target = dv.targets[0]
+ target.id_type = type
+ target.id = id
+ target.data_path = path
+
+
+def context_to_dict(context):
+ """Create a dictionary from the current context
+
+ >>> import bpy
+ >>> ctx = context_to_dict(bpy.context)
+ >>> assert ctx['window'] == bpy.context.window
+ """
+ return {
+ k: getattr(context, k) for k in
+ ('window', 'screen', 'area', 'scene', 'object', 'selected_objects')
+ }
+
+# -----------------------------------------------------------------------------
+# Image loading
+
+ImageSpec = namedtuple(
+ 'ImageSpec',
+ ['image', 'size', 'frame_start', 'frame_offset', 'frame_duration'])
+
+num_regex = re.compile('[0-9]') # Find a single number
+nums_regex = re.compile('[0-9]+') # Find a set of numbers
+
+
+def find_image_sequences(files):
+ """From a group of files, detect image sequences.
+
+ This returns a generator of tuples, which contain the filename,
+ start frame, and length of the detected sequence
+
+ >>> list(find_image_sequences([
+ ... "test2-001.jp2", "test2-002.jp2",
+ ... "test3-003.jp2", "test3-004.jp2", "test3-005.jp2", "test3-006.jp2",
+ ... "blaah"]))
+ [('blaah', 1, 1), ('test2-001.jp2', 1, 2), ('test3-003.jp2', 3, 4)]
+
+ """
+ files = iter(sorted(files))
+ prev_file = None
+ pattern = ""
+ matches = []
+ segment = None
+ length = 1
+ for filename in files:
+ new_pattern = num_regex.sub('#', filename)
+ new_matches = list(map(int, nums_regex.findall(filename)))
+ if new_pattern == pattern:
+ # this file looks like it may be in sequence from the previous
+
+ # if there are multiple sets of numbers, figure out what changed
+ if segment is None:
+ for i, prev, cur in zip(count(), matches, new_matches):
+ if prev != cur:
+ segment = i
+ break
+
+ # did it only change by one?
+ for i, prev, cur in zip(count(), matches, new_matches):
+ if i == segment:
+ # We expect this to increment
+ prev = prev + length
+ if prev != cur:
+ break
+
+ # All good!
+ else:
+ length += 1
+ continue
+
+ # No continuation -> spit out what we found and reset counters
+ if prev_file:
+ if length > 1:
+ yield prev_file, matches[segment], length
+ else:
+ yield prev_file, 1, 1
+
+ prev_file = filename
+ matches = new_matches
+ pattern = new_pattern
+ segment = None
+ length = 1
-def is_image_fn(fn, ext_key):
- if ext_key == DEFAULT_EXT:
- return True # Using Blender's image/movie filter.
- ext = os.path.splitext(fn)[1].lstrip(".").lower()
- return ext in EXT_FILTER[ext_key][0]
+ if prev_file:
+ if length > 1:
+ yield prev_file, matches[segment], length
+ else:
+ yield prev_file, 1, 1
+
+
+def load_images(filenames, directory, force_reload=False, frame_start=1, find_sequences=False):
+ """Wrapper for bpy's load_image
+
+ Loads a set of images, movies, or even image sequences
+ Returns a generator of ImageSpec wrapper objects later used for texture setup
+ """
+ if find_sequences: # if finding sequences, we need some pre-processing first
+ file_iter = find_image_sequences(filenames)
+ else:
+ file_iter = zip(filenames, repeat(1), repeat(1))
+
+ for filename, offset, frames in file_iter:
+ image = load_image(filename, directory, check_existing=True, force_reload=force_reload)
+
+ # Size is unavailable for sequences, so we grab it early
+ size = tuple(image.size)
+
+ if image.source == 'MOVIE':
+ # Blender BPY BUG!
+ # This number is only valid when read a second time in 2.77
+ # This repeated line is not a mistake
+ frames = image.frame_duration
+ frames = image.frame_duration
+
+ elif frames > 1: # Not movie, but multiple frames -> image sequence
+ image.use_animation = True
+ image.source = 'SEQUENCE'
+
+ yield ImageSpec(image, size, frame_start, offset - 1, frames)
# -----------------------------------------------------------------------------
-# Cycles utils.
-def get_input_nodes(node, nodes, links):
+# Position & Size Helpers
+
+def offset_planes(planes, gap, axis):
+ """Offset planes from each other by `gap` amount along a _local_ vector `axis`
+
+ For example, offset_planes([obj1, obj2], 0.5, Vector(0, 0, 1)) will place
+ obj2 0.5 blender units away from obj1 along the local positive Z axis.
+
+ This is in local space, not world space, so all planes should share
+ a common scale and rotation.
+ """
+ prior = planes[0]
+ offset = Vector()
+ for current in planes[1:]:
+
+ local_offset = abs((prior.dimensions + current.dimensions) * axis) / 2.0 + gap
+
+ offset += local_offset * axis
+ current.location = current.matrix_world * offset
+
+ prior = current
+
+
+def compute_camera_size(context, center, fill_mode, aspect):
+ """Determine how large an object needs to be to fit or fill the camera's field of view."""
+ scene = context.scene
+ camera = scene.camera
+ view_frame = camera.data.view_frame(scene=scene)
+ frame_size = \
+ Vector([max(v[i] for v in view_frame) for i in range(3)]) - \
+ Vector([min(v[i] for v in view_frame) for i in range(3)])
+ camera_aspect = frame_size.x / frame_size.y
+
+ # Convert the frame size to the correct sizing at a given distance
+ if camera.type == 'ORTHO':
+ frame_size = frame_size.xy
+ else:
+ # Perspective transform
+ distance = world_to_camera_view(scene, camera, center).z
+ frame_size = distance * frame_size.xy / (-view_frame[0].z)
+
+ # Determine what axis to match to the camera
+ match_axis = 0 # match the Y axis size
+ match_aspect = aspect
+ if (fill_mode == 'FILL' and aspect > camera_aspect) or \
+ (fill_mode == 'FIT' and aspect < camera_aspect):
+ match_axis = 1 # match the X axis size
+ match_aspect = 1.0 / aspect
+
+ # scale the other axis to the correct aspect
+ frame_size[1 - match_axis] = frame_size[match_axis] / match_aspect
+
+ return frame_size
+
+
+def center_in_camera(scene, camera, obj, axis=(1, 1)):
+ """Center object along specified axiis of the camera"""
+ camera_matrix_col = camera.matrix_world.col
+ location = obj.location
+
+ # Vector from the camera's world coordinate center to the object's center
+ delta = camera_matrix_col[3].xyz - location
+
+ # How far off center we are along the camera's local X
+ camera_x_mag = delta * camera_matrix_col[0].xyz * axis[0]
+ # How far off center we are along the camera's local Y
+ camera_y_mag = delta * camera_matrix_col[1].xyz * axis[1]
+
+ # Now offet only along camera local axiis
+ offset = camera_matrix_col[0].xyz * camera_x_mag + \
+ camera_matrix_col[1].xyz * camera_y_mag
+
+ obj.location = location + offset
+
+
+# -----------------------------------------------------------------------------
+# Cycles utils
+
+def get_input_nodes(node, links):
+ """Get nodes that are a inputs to the given node"""
# Get all links going to node.
input_links = {lnk for lnk in links if lnk.to_node == node}
# Sort those links, get their input nodes (and avoid doubles!).
@@ -117,102 +309,428 @@ def get_input_nodes(node, nodes, links):
def auto_align_nodes(node_tree):
- print('\nAligning Nodes')
+ """Given a shader node tree, arrange nodes neatly relative to the output node."""
x_gap = 200
- y_gap = 100
+ y_gap = 180
nodes = node_tree.nodes
links = node_tree.links
- to_node = None
+ output_node = None
for node in nodes:
- if node.type == 'OUTPUT_MATERIAL':
- to_node = node
+ if node.type == 'OUTPUT_MATERIAL' or node.type == 'GROUP_OUTPUT':
+ output_node = node
break
- if not to_node:
- return # Unlikely, but bette check anyway...
- def align(to_node, nodes, links):
- from_nodes = get_input_nodes(to_node, nodes, links)
+ else: # Just in case there is no output
+ return
+
+ def align(to_node):
+ from_nodes = get_input_nodes(to_node, links)
for i, node in enumerate(from_nodes):
- node.location.x = to_node.location.x - x_gap
+ node.location.x = min(node.location.x, to_node.location.x - x_gap)
node.location.y = to_node.location.y
node.location.y -= i * y_gap
- node.location.y += (len(from_nodes)-1) * y_gap / (len(from_nodes))
- align(node, nodes, links)
+ node.location.y += (len(from_nodes) - 1) * y_gap / (len(from_nodes))
+ align(node)
- align(to_node, nodes, links)
+ align(output_node)
def clean_node_tree(node_tree):
+ """Clear all nodes in a shader node tree except the output.
+
+ Returns the output node
+ """
nodes = node_tree.nodes
- for node in nodes:
+ for node in list(nodes): # copy to avoid altering the loop's data source
if not node.type == 'OUTPUT_MATERIAL':
nodes.remove(node)
+
return node_tree.nodes[0]
+def get_shadeless_node(dest_node_tree):
+ """Return a "shadless" cycles node, creating a node group if nonexistant"""
+ try:
+ node_tree = bpy.data.node_groups['IAP_SHADELESS']
+
+ except KeyError:
+ # need to build node shadeless node group
+ node_tree = bpy.data.node_groups.new('IAP_SHADELESS', 'ShaderNodeTree')
+ output_node = node_tree.nodes.new('NodeGroupOutput')
+ input_node = node_tree.nodes.new('NodeGroupInput')
+
+ node_tree.outputs.new('NodeSocketShader', 'Shader')
+ node_tree.inputs.new('NodeSocketColor', 'Color')
+
+ # This could be faster as a transparent shader, but then no ambient occlusion
+ diffuse_shader = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
+ node_tree.links.new(diffuse_shader.inputs[0], input_node.outputs[0])
+
+ emission_shader = node_tree.nodes.new('ShaderNodeEmission')
+ node_tree.links.new(emission_shader.inputs[0], input_node.outputs[0])
+
+ light_path = node_tree.nodes.new('ShaderNodeLightPath')
+ is_glossy_ray = light_path.outputs['Is Glossy Ray']
+ is_shadow_ray = light_path.outputs['Is Shadow Ray']
+ ray_depth = light_path.outputs['Ray Depth']
+ transmission_depth = light_path.outputs['Transmission Depth']
+
+ unrefracted_depth = node_tree.nodes.new('ShaderNodeMath')
+ unrefracted_depth.operation = 'SUBTRACT'
+ unrefracted_depth.label = 'Bounce Count'
+ node_tree.links.new(unrefracted_depth.inputs[0], ray_depth)
+ node_tree.links.new(unrefracted_depth.inputs[1], transmission_depth)
+
+ refracted = node_tree.nodes.new('ShaderNodeMath')
+ refracted.operation = 'SUBTRACT'
+ refracted.label = 'Camera or Refracted'
+ refracted.inputs[0].default_value = 1.0
+ node_tree.links.new(refracted.inputs[1], unrefracted_depth.outputs[0])
+
+ reflection_limit = node_tree.nodes.new('ShaderNodeMath')
+ reflection_limit.operation = 'SUBTRACT'
+ reflection_limit.label = 'Limit Reflections'
+ reflection_limit.inputs[0].default_value = 2.0
+ node_tree.links.new(reflection_limit.inputs[1], ray_depth)
+
+ camera_reflected = node_tree.nodes.new('ShaderNodeMath')
+ camera_reflected.operation = 'MULTIPLY'
+ camera_reflected.label = 'Camera Ray to Glossy'
+ node_tree.links.new(camera_reflected.inputs[0], reflection_limit.outputs[0])
+ node_tree.links.new(camera_reflected.inputs[1], is_glossy_ray)
+
+ shadow_or_reflect = node_tree.nodes.new('ShaderNodeMath')
+ shadow_or_reflect.operation = 'MAXIMUM'
+ shadow_or_reflect.label = 'Shadow or Reflection?'
+ node_tree.links.new(shadow_or_reflect.inputs[0], camera_reflected.outputs[0])
+ node_tree.links.new(shadow_or_reflect.inputs[1], is_shadow_ray)
+
+ shadow_or_reflect_or_refract = node_tree.nodes.new('ShaderNodeMath')
+ shadow_or_reflect_or_refract.operation = 'MAXIMUM'
+ shadow_or_reflect_or_refract.label = 'Shadow, Reflect or Refract?'
+ node_tree.links.new(shadow_or_reflect_or_refract.inputs[0], shadow_or_reflect.outputs[0])
+ node_tree.links.new(shadow_or_reflect_or_refract.inputs[1], refracted.outputs[0])
+
+ mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
+ node_tree.links.new(mix_shader.inputs[0], shadow_or_reflect_or_refract.outputs[0])
+ node_tree.links.new(mix_shader.inputs[1], diffuse_shader.outputs[0])
+ node_tree.links.new(mix_shader.inputs[2], emission_shader.outputs[0])
+
+ node_tree.links.new(output_node.inputs[0], mix_shader.outputs[0])
+
+ auto_align_nodes(node_tree)
+
+ group_node = dest_node_tree.nodes.new("ShaderNodeGroup")
+ group_node.node_tree = node_tree
+
+ return group_node
+
+
+# -----------------------------------------------------------------------------
+# Corner Pin Driver Helpers
+
+@bpy.app.handlers.persistent
+def check_drivers(*args, **kwargs):
+ """Check if watched objects in a scene have changed and trigger compositor update
+
+ This is part of a hack to ensure the compositor updates
+ itself when the objects used for drivers change.
+
+ It only triggers if transformation matricies change to avoid
+ a cyclic loop of updates.
+ """
+ if not watched_objects:
+ # if there is nothing to watch, don't bother running this
+ bpy.app.handlers.scene_update_post.remove(check_drivers)
+ return
+
+ update = False
+ for name, matrix in list(watched_objects.items()):
+ try:
+ obj = bpy.data.objects[name]
+ except KeyError:
+ # The user must have removed this object
+ del watched_objects[name]
+ else:
+ new_matrix = tuple(map(tuple, obj.matrix_world)).__hash__()
+ if new_matrix != matrix:
+ watched_objects[name] = new_matrix
+ update = True
+
+ if update:
+ # Trick to re-evaluate drivers
+ bpy.context.scene.frame_current = bpy.context.scene.frame_current
+
+
+def register_watched_object(obj):
+ """Register an object to be monitored for transformation changes"""
+ name = obj.name
+
+ # known object? -> we're done
+ if name in watched_objects:
+ return
+
+ if not watched_objects:
+ # make sure check_drivers is active
+ bpy.app.handlers.scene_update_post.append(check_drivers)
+
+ watched_objects[name] = None
+
+
+def find_plane_corner(object_name, x, y, axis, camera=None, *args, **kwargs):
+ """Find the location in camera space of a plane's corner"""
+ if args or kwargs:
+ # I've added args / kwargs as a compatability measure with future versions
+ warnings.warn("Unknown Parameters Passed to \"Images as Planes\". Maybe you need to upgrade?")
+
+ plane = bpy.data.objects[object_name]
+
+ # Passing in camera doesn't work before 2.78, so we use the current one
+ camera = camera or bpy.context.scene.camera
+
+ # Hack to ensure compositor updates on future changes
+ register_watched_object(camera)
+ register_watched_object(plane)
+
+ scale = plane.scale * 2.0
+ v = plane.dimensions.copy()
+ v.x *= x / scale.x
+ v.y *= y / scale.y
+ v = plane.matrix_world * v
+
+ camera_vertex = world_to_camera_view(
+ bpy.context.scene, camera, v)
+
+ return camera_vertex[axis]
+
+
+@bpy.app.handlers.persistent
+def register_driver(*args, **kwargs):
+ """Register the find_plane_corner function for use with drivers"""
+ bpy.app.driver_namespace['import_image__find_plane_corner'] = find_plane_corner
+
+
+# -----------------------------------------------------------------------------
+# Compositing Helpers
+
+def group_in_frame(node_tree, name, nodes):
+ frame_node = node_tree.nodes.new("NodeFrame")
+ frame_node.label = name
+ frame_node.name = name + "_frame"
+
+ min_pos = Vector(nodes[0].location)
+ max_pos = min_pos.copy()
+
+ for node in nodes:
+ top_left = node.location
+ bottom_right = top_left + Vector((node.width, -node.height))
+
+ for i in (0, 1):
+ min_pos[i] = min(min_pos[i], top_left[i], bottom_right[i])
+ max_pos[i] = max(max_pos[i], top_left[i], bottom_right[i])
+
+ node.parent = frame_node
+
+ frame_node.width = max_pos[0] - min_pos[0] + 50
+ frame_node.height = max(max_pos[1] - min_pos[1] + 50, 450)
+ frame_node.shrink = True
+
+ return frame_node
+
+
+def position_frame_bottom_left(node_tree, frame_node):
+ newpos = Vector((100000, 100000)) # start reasonably far top / right
+
+ # Align with the furthest left
+ for node in node_tree.nodes.values():
+ if node != frame_node and node.parent != frame_node:
+ newpos.x = min(newpos.x, node.location.x + 30)
+
+ # As high as we can get without overlapping anything to the right
+ for node in node_tree.nodes.values():
+ if node != frame_node and not node.parent:
+ if node.location.x < newpos.x + frame_node.width:
+ print("Below", node.name, node.location, node.height, node.dimensions)
+ newpos.y = min(newpos.y, node.location.y - max(node.dimensions.y, node.height) - 20)
+
+ frame_node.location = newpos
+
+
+def setup_compositing(context, plane, img_spec):
+ # Node Groups only work with "new" dependency graph and even
+ # then it has some problems with not updating the first time
+ # So instead this groups with a node frame, which works reliably
+
+ scene = context.scene
+ scene.use_nodes = True
+ node_tree = scene.node_tree
+ name = plane.name
+
+ image_node = node_tree.nodes.new("CompositorNodeImage")
+ image_node.name = name + "_image"
+ image_node.image = img_spec.image
+ image_node.location = Vector((0, 0))
+ image_node.frame_start = img_spec.frame_start
+ image_node.frame_offset = img_spec.frame_offset
+ image_node.frame_duration = img_spec.frame_duration
+
+ scale_node = node_tree.nodes.new("CompositorNodeScale")
+ scale_node.name = name + "_scale"
+ scale_node.space = 'RENDER_SIZE'
+ scale_node.location = image_node.location + \
+ Vector((image_node.width + 20, 0))
+ scale_node.show_options = False
+
+ cornerpin_node = node_tree.nodes.new("CompositorNodeCornerPin")
+ cornerpin_node.name = name + "_cornerpin"
+ cornerpin_node.location = scale_node.location + \
+ Vector((0, -scale_node.height))
+
+ node_tree.links.new(scale_node.inputs[0], image_node.outputs[0])
+ node_tree.links.new(cornerpin_node.inputs[0], scale_node.outputs[0])
+
+ # Put all the nodes in a frame for organization
+ frame_node = group_in_frame(
+ node_tree, name,
+ (image_node, scale_node, cornerpin_node)
+ )
+
+ # Position frame at bottom / left
+ position_frame_bottom_left(node_tree, frame_node)
+
+ # Configure Drivers
+ for corner in cornerpin_node.inputs[1:]:
+ id = corner.identifier
+ x = -1 if 'Left' in id else 1
+ y = -1 if 'Lower' in id else 1
+ drivers = corner.driver_add('default_value')
+ for i, axis_fcurve in enumerate(drivers):
+ driver = axis_fcurve.driver
+ # Always use the current camera
+ add_driver_prop(driver, 'camera', 'SCENE', scene, 'camera')
+ # Track camera location to ensure Deps Graph triggers (not used in the call)
+ add_driver_prop(driver, 'cam_loc_x', 'OBJECT', scene.camera, 'location[0]')
+ # Don't break if the name changes
+ add_driver_prop(driver, 'name', 'OBJECT', plane, 'name')
+ driver.expression = "import_image__find_plane_corner(name or %s, %d, %d, %d, camera=camera)" % (
+ repr(plane.name),
+ x, y, i
+ )
+ driver.type = 'SCRIPTED'
+ driver.is_valid = True
+ axis_fcurve.is_valid = True
+ driver.expression = "%s" % driver.expression
+
+ scene.update()
+
+
# -----------------------------------------------------------------------------
# Operator
-class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
+class IMPORT_IMAGE_OT_to_plane_v2(Operator, AddObjectHelper):
"""Create mesh plane(s) from image files with the appropiate aspect ratio"""
- bl_idname = "import_image.to_plane"
+
+ bl_idname = "import_image.to_plane_v2"
bl_label = "Import Images as Planes"
- bl_options = {'REGISTER', 'UNDO'}
+ bl_options = {'REGISTER', 'PRESET', 'UNDO'}
+
+ # --------------
+ # Internal State
+ _extensions_updated = False # used by update_extensions below
- # -----------
- # File props.
+ # ----------------------
+ # File dialog properties
files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
directory = StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
- # Show only images/videos, and directories!
- filter_image = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
- filter_movie = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
- filter_folder = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
- filter_glob = StringProperty(default="", options={'HIDDEN', 'SKIP_SAVE'})
+ # ----------------------
+ # Properties - Importing
+ force_reload = BoolProperty(
+ name="Force Reload", default=False,
+ description="Force reloading of the image if already opened elsewhere in Blender"
+ )
+
+ image_sequence = BoolProperty(
+ name="Animate Image Sequences", default=False,
+ description="Import sequentially numbered images as an animated "
+ "image sequence instead of separate planes"
+ )
- # --------
- # Options.
- align = BoolProperty(name="Align Planes", default=True, description="Create Planes in a row")
+ # -------------------------------------
+ # Properties - Position and Orientation
+ offset = BoolProperty(name="Offset Planes", default=True, description="Offset Planes From Each Other")
+
+ OFFSET_MODES = (
+ ('1,0,0', "X+", "Side by Side to the Left"),
+ ('0,1,0', "Y+", "Side by Side, Downward"),
+ ('0,0,1', "Z+", "Stacked Above"),
+ ('-1,0,0', "X-", "Side by Side to the Right"),
+ ('0,-1,0', "Y-", "Side by Side, Upward"),
+ ('0,0,-1', "Z-", "Stacked Below"),
+ )
+ offset_axis = EnumProperty(
+ name="Orientation", default='1,0,0', items=OFFSET_MODES,
+ description="How planes are oriented relative to each others' local axis"
+ )
- align_offset = FloatProperty(name="Offset", min=0, soft_min=0, default=0.1, description="Space between Planes")
+ offset_amount = FloatProperty(
+ name="Offset", soft_min=0, default=0.1, description="Space between planes",
+ subtype='DISTANCE', unit='LENGTH'
+ )
- force_reload = BoolProperty(name="Force Reload", default=False,
- description="Force reloading of the image if already opened elsewhere in Blender")
+ AXIS_MODES = (
+ ('1,0,0', "X+", "Facing Positive X"),
+ ('0,1,0', "Y+", "Facing Positive Y"),
+ ('0,0,1', "Z+ (Up)", "Facing Positive Z"),
+ ('-1,0,0', "X-", "Facing Negative X"),
+ ('0,-1,0', "Y-", "Facing Negative Y"),
+ ('0,0,-1', "Z- (Down)", "Facing Negative Z"),
+ ('CAM', "Face Camera", "Facing Camera"),
+ ('CAM_AX', "Main Axis", "Facing the Camera's dominant axis"),
+ )
+ align_axis = EnumProperty(
+ name="Align", default='CAM_AX', items=AXIS_MODES,
+ description="How to align the planes"
+ )
+ # prev_align_axis is used only by update_size_model
+ prev_align_axis = EnumProperty(
+ items=AXIS_MODES + (('NONE', '', ''),), default='NONE', options={'HIDDEN', 'SKIP_SAVE'})
+ align_track = BoolProperty(
+ name="Track Camera", default=False, description="Always face the camera"
+ )
- # Callback which will update File window's filter options accordingly to extension setting.
- def update_extensions(self, context):
- if self.extension == DEFAULT_EXT:
- self.filter_image = True
- self.filter_movie = True
- self.filter_glob = ""
+ # -----------------
+ # Properties - Size
+ def update_size_mode(self, context):
+ """If sizing relative to the camera, always face the camera"""
+ if self.size_mode == 'CAMERA':
+ self.prev_align_axis = self.align_axis
+ self.align_axis = 'CAM'
else:
- self.filter_image = False
- self.filter_movie = False
- flt = ";".join(("*." + e for e in EXT_FILTER[self.extension][0]))
- self.filter_glob = flt
- # And now update space (file select window), if possible.
- space = bpy.context.space_data
- # XXX Can't use direct op comparison, these are not the same objects!
- if (space.type != 'FILE_BROWSER' or space.operator.bl_rna.identifier != self.bl_rna.identifier):
- return
- space.params.use_filter_image = self.filter_image
- space.params.use_filter_movie = self.filter_movie
- space.params.filter_glob = self.filter_glob
- # XXX Seems to be necessary, else not all changes above take effect...
- #~ bpy.ops.file.refresh()
- extension = EnumProperty(name="Extension", items=gen_ext_filter_ui_items(),
- description="Only import files of this type", update=update_extensions)
-
- # -------------------
- # Plane size options.
- _size_modes = (
+ # if a different alignment was set revert to that when
+ # size mode is changed
+ if self.prev_align_axis != 'NONE':
+ self.align_axis = self.prev_align_axis
+ self._prev_align_axis = 'NONE'
+
+ SIZE_MODES = (
('ABSOLUTE', "Absolute", "Use absolute size"),
+ ('CAMERA', "Camera Relative", "Scale to the camera frame"),
('DPI', "Dpi", "Use definition of the image as dots per inch"),
('DPBU', "Dots/BU", "Use definition of the image as dots per Blender Unit"),
)
- size_mode = EnumProperty(name="Size Mode", default='ABSOLUTE', items=_size_modes,
- description="How the size of the plane is computed")
+ size_mode = EnumProperty(
+ name="Size Mode", default='ABSOLUTE', items=SIZE_MODES,
+ update=update_size_mode,
+ description="How the size of the plane is computed")
+
+ FILL_MODES = (
+ ('FILL', "Fill", "Fill camera frame, spilling outside the frame"),
+ ('FIT', "Fit", "Fit entire image within the camera frame"),
+ )
+ fill_mode = EnumProperty(name="Scale", default='FILL', items=FILL_MODES,
+ description="How large in the camera frame is the plane")
height = FloatProperty(name="Height", description="Height of the created plane",
default=1.0, min=0.001, soft_min=0.001, subtype='DISTANCE', unit='LENGTH')
@@ -220,36 +738,39 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
factor = FloatProperty(name="Definition", min=1.0, default=600.0,
description="Number of pixels per inch or Blender Unit")
- # -------------------------
- # Blender material options.
- t = bpy.types.Material.bl_rna.properties["use_shadeless"]
- use_shadeless = BoolProperty(name=t.name, default=False, description=t.description)
-
- use_transparency = BoolProperty(name="Use Alpha", default=False, description="Use alphachannel for transparency")
+ # ------------------------------
+ # Properties - Material / Shader
+ SHADERS = (
+ ('DIFFUSE', "Diffuse", "Diffuse Shader"),
+ ('SHADELESS', "Shadeless", "Only visible to camera and reflections."),
+ ('EMISSION', "Emit", "Emission Shader"),
+ )
+ shader = EnumProperty(name="Shader", items=SHADERS, default='DIFFUSE', description="Node shader to use")
- t = bpy.types.Material.bl_rna.properties["transparency_method"]
- items = tuple((it.identifier, it.name, it.description) for it in t.enum_items)
- transparency_method = EnumProperty(name="Transp. Method", description=t.description, items=items)
+ emit_strength = FloatProperty(
+ name="Strength", min=0.0, default=1.0, soft_max=10.0,
+ step=100, description="Brightness of Emission Texture")
- t = bpy.types.Material.bl_rna.properties["use_transparent_shadows"]
- use_transparent_shadows = BoolProperty(name=t.name, default=False, description=t.description)
+ overwrite_material = BoolProperty(
+ name="Overwrite Material", default=True,
+ description="Overwrite existing Material (based on material name)")
- #-------------------------
- # Cycles material options.
- shader = EnumProperty(name="Shader", items=CYCLES_SHADERS, description="Node shader to use")
+ compositing_nodes = BoolProperty(
+ name="Setup Corner Pin", default=False,
+ description="Build Compositor Nodes to reference this image "
+ "without re-rendering")
- overwrite_node_tree = BoolProperty(name="Overwrite Material", default=True,
- description="Overwrite existing Material with new nodetree "
- "(based on material name)")
+ # ------------------
+ # Properties - Image
+ use_transparency = BoolProperty(
+ name="Use Alpha", default=True,
+ description="Use alphachannel for transparency")
- # --------------
- # Image Options.
t = bpy.types.Image.bl_rna.properties["alpha_mode"]
alpha_mode_items = tuple((e.identifier, e.name, e.description) for e in t.enum_items)
- alpha_mode = EnumProperty(name=t.name, items=alpha_mode_items, default=t.default, description=t.description)
-
- t = bpy.types.IMAGE_OT_match_movie_length.bl_rna
- match_len = BoolProperty(name=t.name, default=True, description=t.description)
+ alpha_mode = EnumProperty(
+ name=t.name, items=alpha_mode_items, default=t.default,
+ description=t.description)
t = bpy.types.Image.bl_rna.properties["use_fields"]
use_fields = BoolProperty(name=t.name, default=False, description=t.description)
@@ -257,24 +778,45 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
t = bpy.types.ImageUser.bl_rna.properties["use_auto_refresh"]
use_auto_refresh = BoolProperty(name=t.name, default=True, description=t.description)
- relative = BoolProperty(name="Relative", default=True, description="Apply relative paths")
+ relative = BoolProperty(name="Relative Paths", default=True, description="Use relative file paths")
- def draw(self, context):
- engine = context.scene.render.engine
+ # -------
+ # Draw UI
+ def draw_import_config(self, context):
+ # --- Import Options --- #
layout = self.layout
-
box = layout.box()
- box.label("Import Options:", icon='FILTER')
- box.prop(self, "extension", icon='FILE_IMAGE')
- box.prop(self, "align")
- box.prop(self, "align_offset")
- row = box.row()
- row.prop(self, "force_reload")
+ box.label("Import Options:", icon='IMPORT')
row = box.row()
row.active = bpy.data.is_saved
row.prop(self, "relative")
- box.prop(self, "match_len")
+
+ box.prop(self, "force_reload")
+ box.prop(self, "image_sequence")
+
+ def draw_material_config(self, context):
+ # --- Material / Rendering Properties --- #
+ layout = self.layout
+ box = layout.box()
+
+ box.label("Compositing Nodes:", icon='RENDERLAYERS')
+ box.prop(self, 'compositing_nodes')
+
+ box.label("Material Settings:", icon='MATERIAL')
+
+ row = box.row()
+ row.prop(self, 'shader', expand=True)
+ if self.shader == 'EMISSION':
+ box.prop(self, 'emit_strength')
+
+ engine = context.scene.render.engine
+ if engine not in ('CYCLES', 'BLENDER_RENDER'):
+ box.label("%s is not supported" % engine, icon='ERROR')
+
+ box.prop(self, 'overwrite_material')
+
+ box.label("Texture Settings:", icon='TEXTURE')
row = box.row()
row.prop(self, "use_transparency")
sub = row.row()
@@ -283,30 +825,70 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
box.prop(self, "use_fields")
box.prop(self, "use_auto_refresh")
+ def draw_spatial_config(self, context):
+ # --- Spatial Properties: Position, Size and Orientation --- #
+ layout = self.layout
box = layout.box()
- if engine == 'BLENDER_RENDER':
- box.label("Material Settings: (Blender)", icon='MATERIAL')
- box.prop(self, "use_shadeless")
- row = box.row()
- row.prop(self, "transparency_method", expand=True)
- box.prop(self, "use_transparent_shadows")
- elif engine == 'CYCLES':
- box = layout.box()
- box.label("Material Settings: (Cycles)", icon='MATERIAL')
- box.prop(self, 'shader', expand = True)
- box.prop(self, 'overwrite_node_tree')
- box = layout.box()
+ box.label("Position:", icon='SNAP_GRID')
+ box.prop(self, 'offset')
+ col = box.column()
+ row = col.row()
+ row.prop(self, 'offset_axis', expand=True)
+ row = col.row()
+ row.prop(self, 'offset_amount')
+ col.enabled = self.offset
+
box.label("Plane dimensions:", icon='ARROW_LEFTRIGHT')
row = box.row()
row.prop(self, "size_mode", expand=True)
if self.size_mode == 'ABSOLUTE':
box.prop(self, "height")
+ elif self.size_mode == 'CAMERA':
+ row = box.row()
+ row.prop(self, 'fill_mode', expand=True)
else:
box.prop(self, "factor")
+ box.label("Orientation:", icon='MANIPUL')
+ row = box.row()
+ row.enabled = 'CAM' not in self.size_mode
+ row.prop(self, 'align_axis')
+ row = box.row()
+ row.enabled = 'CAM' in self.align_axis
+ row.alignment = 'RIGHT'
+ row.prop(self, 'align_track')
+
+ def draw(self, context):
+
+ # Draw configuration sections
+ self.draw_import_config(context)
+ self.draw_material_config(context)
+ self.draw_spatial_config(context)
+
+ # And now update space (file select window), if possible.
+ space = bpy.context.space_data
+ # XXX Can't use direct op comparison, these are not the same objects!
+ if (space.type != 'FILE_BROWSER' or space.operator.bl_rna.identifier != self.bl_rna.identifier):
+ return
+
+ # Only se filters once per invokation
+ if not self._extensions_updated:
+ space.params.use_filter_image = True
+ space.params.use_filter_movie = True
+ space.params.use_filter_folder = True
+ space.params.filter_glob = ""
+ self._extensions_updated = True
+
+ # -------------------------------------------------------------------------
+ # Core functionality
def invoke(self, context, event):
- self.update_extensions(context)
+ engine = context.scene.render.engine
+ if engine not in ('CYCLES', 'BLENDER_RENDER', 'BLENDER_GAME'):
+ # Use default blender texture, but acknowledge things may not work
+ self.report({'WARNING'}, "Cannot generate materials for unknown %s render engine" % engine)
+
+ # Open file browser
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
@@ -314,7 +896,7 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
if not bpy.data.is_saved:
self.relative = False
- # the add utils don't work in this case because many objects are added disable relevant things beforehand
+ # this won't work in edit mode
editmode = context.user_preferences.edit.use_enter_edit_mode
context.user_preferences.edit.use_enter_edit_mode = False
if context.active_object and context.active_object.mode == 'EDIT':
@@ -323,110 +905,121 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
self.import_images(context)
context.user_preferences.edit.use_enter_edit_mode = editmode
+
return {'FINISHED'}
- # Main...
def import_images(self, context):
- engine = context.scene.render.engine
- import_list, directory = self.generate_paths()
- images = tuple(load_image(path, directory, check_existing=True, force_reload=self.force_reload)
- for path in import_list)
+ # load images / sequences
+ images = tuple(load_images(
+ (fn.name for fn in self.files),
+ self.directory,
+ force_reload=self.force_reload,
+ find_sequences=self.image_sequence
+ ))
- for img in images:
- self.set_image_options(img)
+ # Create individual planes
+ planes = [self.single_image_spec_to_plane(context, img_spec) for img_spec in images]
- if engine in {'BLENDER_RENDER', 'BLENDER_GAME'}:
- textures = (self.create_image_textures(context, img) for img in images)
- materials = (self.create_material_for_texture(tex) for tex in textures)
- elif engine == 'CYCLES':
- materials = (self.create_cycles_material(context, img) for img in images)
- else:
- self.report({'ERROR'}, "Cannot generate materials for unknown %s render engine" % engine)
- return
+ context.scene.update()
- planes = tuple(self.create_image_plane(context, mat) for mat in materials)
+ # Align planes relative to each other
+ if self.offset:
+ offset_axis = str_to_vector(self.offset_axis)
+ offset_planes(planes, self.offset_amount, offset_axis)
- context.scene.update()
- if self.align:
- self.align_planes(planes)
+ if self.size_mode == 'CAMERA' and offset_axis.z:
+ for plane in planes:
+ x, y = compute_camera_size(
+ context, plane.location,
+ self.fill_mode, plane.dimensions.x / plane.dimensions.y)
+ plane.dimensions = x, y, 0.0
+ # setup new selection
for plane in planes:
plane.select = True
+ # all done!
self.report({'INFO'}, "Added {} Image Plane(s)".format(len(planes)))
- def create_image_plane(self, context, material):
+ # operate on a single image
+ def single_image_spec_to_plane(self, context, img_spec):
+
+ # Configure image
+ self.apply_image_options(img_spec.image)
+
+ # Configure material
engine = context.scene.render.engine
- if engine in {'BLENDER_RENDER', 'BLENDER_GAME'}:
- img = material.texture_slots[0].texture.image
- elif engine == 'CYCLES':
- nodes = material.node_tree.nodes
- img = next((node.image for node in nodes if node.type == 'TEX_IMAGE'))
- px, py = img.size
+ if engine == 'CYCLES':
+ material = self.create_cycles_material(context, img_spec)
+ else:
+ tex = self.create_image_textures(context, img_spec)
+ material = self.create_material_for_texture(tex)
- # can't load data
- if px == 0 or py == 0:
- px = py = 1
+ # Game Engine Material Settings
+ material.game_settings.use_backface_culling = False
+ material.game_settings.alpha_blend = 'ALPHA'
- if self.size_mode == 'ABSOLUTE':
- y = self.height
- x = px / py * y
- elif self.size_mode == 'DPI':
- fact = 1 / self.factor / context.scene.unit_settings.scale_length * 0.0254
- x = px * fact
- y = py * fact
- else: # elif self.size_mode == 'DPBU'
- fact = 1 / self.factor
- x = px * fact
- y = py * fact
+ # Create and position plane object
+ plane = self.create_image_plane(context, material.name, img_spec)
- bpy.ops.mesh.primitive_plane_add('INVOKE_REGION_WIN')
- plane = context.scene.objects.active
- # Why does mesh.primitive_plane_add leave the object in edit mode???
- if plane.mode is not 'OBJECT':
- bpy.ops.object.mode_set(mode='OBJECT')
- plane.dimensions = x, y, 0.0
- plane.data.name = plane.name = material.name
- bpy.ops.object.transform_apply(scale=True)
- plane.data.uv_textures.new()
+ # Assign Material
plane.data.materials.append(material)
- plane.data.uv_textures[0].data[0].image = img
+ plane.data.uv_textures[0].data[0].image = img_spec.image
+
+ # If applicable, setup Corner Pin node
+ if self.compositing_nodes:
+ setup_compositing(context, plane, img_spec)
- material.game_settings.use_backface_culling = False
- material.game_settings.alpha_blend = 'ALPHA'
return plane
- def align_planes(self, planes):
- gap = self.align_offset
- offset = (planes[0].dimensions.x / 2.0) + gap
- for plane in planes[1:]:
- move_global = mathutils.Vector((offset + (plane.dimensions.x / 2.0), 0.0, 0.0))
- plane.location = plane.matrix_world * move_global
- offset += plane.dimensions.x + gap
+ def apply_image_options(self, image):
+ image.use_alpha = self.use_transparency
+ image.alpha_mode = self.alpha_mode
+ image.use_fields = self.use_fields
+
+ if self.relative:
+ try: # can't always find the relative path (between drive letters on windows)
+ image.filepath = bpy.path.relpath(image.filepath)
+ except ValueError:
+ pass
+
+ def apply_texture_options(self, texture, img_spec):
+ # Shared by both Cycles and Blender Internal
+ image_user = texture.image_user
+ image_user.use_auto_refresh = self.use_auto_refresh
+ image_user.frame_start = img_spec.frame_start
+ image_user.frame_offset = img_spec.frame_offset
+ image_user.frame_duration = img_spec.frame_duration
- def generate_paths(self):
- return (fn.name for fn in self.files if is_image_fn(fn.name, self.extension)), self.directory
+ # Image sequences need auto refresh to display reliably
+ if img_spec.image.source == 'SEQUENCE':
+ image_user.use_auto_refresh = True
- # Internal
- def create_image_textures(self, context, image):
+ texture.extension = 'CLIP' # Default of "Repeat" can cause artifacts
+
+ # -------------------------------------------------------------------------
+ # Blender Internal Material
+ def create_image_textures(self, context, img_spec):
+ image = img_spec.image
fn_full = os.path.normpath(bpy.path.abspath(image.filepath))
- # look for texture with importsettings
+ # look for texture referencing this file
for texture in bpy.data.textures:
if texture.type == 'IMAGE':
tex_img = texture.image
if (tex_img is not None) and (tex_img.library is None):
fn_tex_full = os.path.normpath(bpy.path.abspath(tex_img.filepath))
if fn_full == fn_tex_full:
- self.set_texture_options(context, texture)
+ if self.overwrite_material:
+ self.apply_texture_options(texture, img_spec)
return texture
# if no texture is found: create one
name_compat = bpy.path.display_name_from_filepath(image.filepath)
texture = bpy.data.textures.new(name=name_compat, type='IMAGE')
texture.image = image
- self.set_texture_options(context, texture)
+ self.apply_texture_options(texture, img_spec)
return texture
def create_material_for_texture(self, texture):
@@ -434,7 +1027,8 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
for material in bpy.data.materials:
slot = material.texture_slots[0]
if slot and slot.texture == texture:
- self.set_material_options(material, slot)
+ if self.overwrite_material:
+ self.apply_material_options(material, slot)
return material
# if no material found: create one
@@ -443,26 +1037,12 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
slot = material.texture_slots.add()
slot.texture = texture
slot.texture_coords = 'UV'
- self.set_material_options(material, slot)
+ self.apply_material_options(material, slot)
return material
- def set_image_options(self, image):
- image.use_alpha = self.use_transparency
- image.alpha_mode = self.alpha_mode
- image.use_fields = self.use_fields
-
- if self.relative:
- try: # can't always find the relative path (between drive letters on windows)
- image.filepath = bpy.path.relpath(image.filepath)
- except ValueError:
- pass
+ def apply_material_options(self, material, slot):
+ shader = self.shader
- def set_texture_options(self, context, texture):
- texture.image_user.use_auto_refresh = self.use_auto_refresh
- if self.match_len:
- texture.image_user.frame_duration = texture.image.frame_duration
-
- def set_material_options(self, material, slot):
if self.use_transparency:
material.alpha = 0.0
material.specular_alpha = 0.0
@@ -471,26 +1051,32 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
material.alpha = 1.0
material.specular_alpha = 1.0
slot.use_map_alpha = False
+
+ material.specular_intensity = 0
+ material.diffuse_intensity = 1.0
material.use_transparency = self.use_transparency
- material.transparency_method = self.transparency_method
- material.use_shadeless = self.use_shadeless
- material.use_transparent_shadows = self.use_transparent_shadows
+ material.transparency_method = 'Z_TRANSPARENCY'
+ material.use_shadeless = (shader == 'SHADELESS')
+ material.use_transparent_shadows = (shader == 'DIFFUSE')
+ material.emit = self.emit_strength if shader == 'EMISSION' else 0.0
- #--------------------------------------------------------------------------
+ # -------------------------------------------------------------------------
# Cycles
- def create_cycles_texnode(self, context, node_tree, image):
+ def create_cycles_texnode(self, context, node_tree, img_spec):
tex_image = node_tree.nodes.new('ShaderNodeTexImage')
- tex_image.image = image
+ tex_image.image = img_spec.image
tex_image.show_texture = True
- self.set_texture_options(context, tex_image)
+ self.apply_texture_options(tex_image, img_spec)
return tex_image
- def create_cycles_material(self, context, image):
+ def create_cycles_material(self, context, img_spec):
+ image = img_spec.image
name_compat = bpy.path.display_name_from_filepath(image.filepath)
material = None
- for mat in bpy.data.materials:
- if mat.name == name_compat and self.overwrite_node_tree:
- material = mat
+ if self.overwrite_material:
+ for mat in bpy.data.materials:
+ if mat.name == name_compat:
+ material = mat
if not material:
material = bpy.data.materials.new(name=name_compat)
@@ -498,44 +1084,273 @@ class IMPORT_OT_image_to_plane(Operator, AddObjectHelper):
node_tree = material.node_tree
out_node = clean_node_tree(node_tree)
- tex_image = self.create_cycles_texnode(context, node_tree, image)
-
- if self.shader == 'BSDF_DIFFUSE':
- bsdf_diffuse = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
- node_tree.links.new(bsdf_diffuse.inputs[0], tex_image.outputs[0])
- if self.use_transparency:
- bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
- mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
- node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0])
- node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
- node_tree.links.new(mix_shader.inputs[2], bsdf_diffuse.outputs[0])
- node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
- else:
- node_tree.links.new(out_node.inputs[0], bsdf_diffuse.outputs[0])
-
- elif self.shader == 'EMISSION':
- emission = node_tree.nodes.new('ShaderNodeEmission')
- lightpath = node_tree.nodes.new('ShaderNodeLightPath')
- node_tree.links.new(emission.inputs[0], tex_image.outputs[0])
- node_tree.links.new(emission.inputs[1], lightpath.outputs[0])
- if self.use_transparency:
- bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
- mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
- node_tree.links.new(out_node.inputs[0], mix_shader.outputs[0])
- node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
- node_tree.links.new(mix_shader.inputs[2], emission.outputs[0])
- node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
- else:
- node_tree.links.new(out_node.inputs[0], emission.outputs[0])
+ tex_image = self.create_cycles_texnode(context, node_tree, img_spec)
+
+ if self.shader == 'DIFFUSE':
+ core_shader = node_tree.nodes.new('ShaderNodeBsdfDiffuse')
+ elif self.shader == 'SHADELESS':
+ core_shader = get_shadeless_node(node_tree)
+ else: # Emission Shading
+ core_shader = node_tree.nodes.new('ShaderNodeEmission')
+ core_shader.inputs[1].default_value = self.emit_strength
+
+ # Connect color from texture
+ node_tree.links.new(core_shader.inputs[0], tex_image.outputs[0])
+
+ if self.use_transparency:
+ bsdf_transparent = node_tree.nodes.new('ShaderNodeBsdfTransparent')
+
+ mix_shader = node_tree.nodes.new('ShaderNodeMixShader')
+ node_tree.links.new(mix_shader.inputs[0], tex_image.outputs[1])
+ node_tree.links.new(mix_shader.inputs[1], bsdf_transparent.outputs[0])
+ node_tree.links.new(mix_shader.inputs[2], core_shader.outputs[0])
+ core_shader = mix_shader
+
+ node_tree.links.new(out_node.inputs[0], core_shader.outputs[0])
auto_align_nodes(node_tree)
return material
+ # -------------------------------------------------------------------------
+ # Geometry Creation
+ def create_image_plane(self, context, name, img_spec):
+
+ width, height = self.compute_plane_size(context, img_spec)
+
+ # Create new mesh
+ bpy.ops.mesh.primitive_plane_add('INVOKE_REGION_WIN')
+ plane = context.scene.objects.active
+ # Why does mesh.primitive_plane_add leave the object in edit mode???
+ if plane.mode is not 'OBJECT':
+ bpy.ops.object.mode_set(mode='OBJECT')
+ plane.dimensions = width, height, 0.0
+ plane.data.name = plane.name = name
+ bpy.ops.object.transform_apply(scale=True)
+ plane.data.uv_textures.new()
+
+ # If sizing for camera, also insert into the camera's field of view
+ if self.size_mode == 'CAMERA':
+ offset_axis = str_to_vector(self.offset_axis)
+ translate_axis = [0 if offset_axis[i] else 1 for i in (0, 1)]
+ center_in_camera(context.scene, context.scene.camera, plane, translate_axis)
+
+ self.align_plane(context, plane)
+
+ return plane
+
+ def compute_plane_size(self, context, img_spec):
+ """Given the image size in pixels and location, determine size of plane"""
+ px, py = img_spec.size
+
+ # can't load data
+ if px == 0 or py == 0:
+ px = py = 1
+
+ if self.size_mode == 'ABSOLUTE':
+ y = self.height
+ x = px / py * y
+
+ elif self.size_mode == 'CAMERA':
+ x, y = compute_camera_size(
+ context, context.scene.cursor_location,
+ self.fill_mode, px / py
+ )
+
+ elif self.size_mode == 'DPI':
+ fact = 1 / self.factor / context.scene.unit_settings.scale_length * 0.0254
+ x = px * fact
+ y = py * fact
+
+ else: # elif self.size_mode == 'DPBU'
+ fact = 1 / self.factor
+ x = px * fact
+ y = py * fact
+
+ return x, y
+
+ def align_plane(self, context, plane):
+ """Pick an axis and align the plane to it"""
+ if 'CAM' in self.align_axis:
+ # Camera-aligned
+ camera = context.scene.camera
+ if (camera):
+ # Find the axis that best corresponds to the camera's view direction
+ axis = camera.matrix_world * \
+ Vector((0, 0, 1)) - camera.matrix_world.col[3].xyz
+ # pick the axis with the greatest magnitude
+ mag = max(map(abs, axis))
+ # And use that axis & direction
+ axis = Vector([
+ n / mag if abs(n) == mag else 0.0
+ for n in axis
+ ])
+ else:
+ # No camera? Just face Z axis
+ axis = Vector((0, 0, 1))
+ self.align_axis = '0,0,1'
+
+ else:
+ # Axis-aligned
+ axis = str_to_vector(self.align_axis)
+
+ # rotate accodingly for x/y axiis
+ if not axis.z:
+ plane.rotation_euler.x = pi / 2
+
+ if axis.y > 0:
+ plane.rotation_euler.z = pi
+ elif axis.y < 0:
+ plane.rotation_euler.z = 0
+ elif axis.x > 0:
+ plane.rotation_euler.z = pi / 2
+ elif axis.x < 0:
+ plane.rotation_euler.z = -pi / 2
+
+ # or flip 180 degrees for negative z
+ elif axis.z < 0:
+ plane.rotation_euler.y = pi
+
+ if self.align_axis == 'CAM':
+ constraint = plane.constraints.new('COPY_ROTATION')
+ constraint.target = camera
+ constraint.use_x = constraint.use_y = constraint.use_z = True
+ if not self.align_track:
+ bpy.ops.object.visual_transform_apply()
+ plane.constraints.clear()
+
+ if self.align_axis == 'CAM_AX' and self.align_track:
+ constraint = plane.constraints.new('LOCKED_TRACK')
+ constraint.target = camera
+ constraint.track_axis = 'TRACK_Z'
+ constraint.lock_axis = 'LOCK_Y'
+
+
+# -----------------------------------------------------------------------------
+# Legacy Interface Support
+
+class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper):
+ """Helper for add-ons expecting the old interface
+
+ This maps maps properties from the old interface to their equivalents,
+ if any, in the newer add-on.
+ """
+
+ bl_idname = "import_image.to_plane"
+ bl_options = {'REGISTER', 'INTERNAL'}
+ bl_label = "Import Images as Planes (Deprecated Interface)"
+
+ # Properties that have not changed
+ NO_CHANGE = (
+ 'rotation', 'location', 'view_align', 'layers', # from AddObjectHelper
+ 'files', 'directory', 'relative', 'force_reload', # File Loading
+ 'size_mode', 'height', 'factor', # Size
+ 'use_transparency', 'alpha_mode', # Alpha
+ 'use_fields', 'use_auto_refresh', # Texture Properties
+ )
+ for k in NO_CHANGE:
+ locals()[k] = getattr(IMPORT_IMAGE_OT_to_plane_v2, k)
+
+ # Properties that have been renamed
+ REMAP = {
+ 'overwrite_node_tree': 'overwrite_material',
+ 'align': 'offset',
+ 'offset': 'offset_amount',
+ }
+ for k, v in REMAP.items():
+ locals()[k] = getattr(IMPORT_IMAGE_OT_to_plane_v2, v)
+
+ DEFAULTS = {
+ 'align_axis': '0,0,1',
+ 'offset_axis': '1,0,0',
+ }
+
+ # Properties that need translation
+ t = bpy.types.Material.bl_rna.properties["use_shadeless"]
+ use_shadeless = BoolProperty(name=t.name, default=False, description=t.description)
+
+ t = bpy.types.Material.bl_rna.properties["transparency_method"]
+ items = tuple((it.identifier, it.name, it.description) for it in t.enum_items)
+ transparency_method = EnumProperty(name="Transp. Method", description=t.description, items=items)
+
+ CYCLES_SHADERS = (
+ ('BSDF_DIFFUSE', 'Diffuse', 'Diffuse Shader'),
+ ('EMISSION', 'Emission', 'Emission Shader'),
+ ('BSDF_DIFFUSE_BSDF_TRANSPARENT', 'Diffuse & Transparent',
+ 'Diffuse and Transparent Mix'),
+ ('EMISSION_BSDF_TRANSPARENT', 'Emission & Transparent',
+ 'Emission and Transparent Mix')
+ )
+ shader = bpy.props.EnumProperty(
+ name='Shader', items=CYCLES_SHADERS,
+ default='BSDF_DIFFUSE_BSDF_TRANSPARENT',
+ description='Node shader to use'
+ )
+
+ def translate_properties(self, context, target):
+ engine = context.scene.render.engine
+
+ target['shader'] = 'DIFFUSE'
+
+ if engine == 'CYCLES' and 'EMISSION' in self.shader:
+ target['shader'] = 'SHADELESS'
+
+ if self.use_shadeless:
+ target['shader'] = 'SHADELESS'
+
+ if 'TRANSPARENT' in self.shader:
+ target['use_transparency'] = True
+ if 'Z_TRANS' in self.transparency_method or 'RAY' in self.transparency_method:
+ target['use_transparency'] = True
+
+ # No change in this field, but it needs some remapping to work
+ target['files'] = [{'name': file.name} for file in self.files]
+
+ # Removed Properties
+ FILE_TYPES = (
+ '*', 'jpeg', 'png', 'tga', 'tiff', 'bmp', 'cin',
+ 'dpx', 'psd', 'exr', 'hdr', 'avi', 'mov', 'mp4', 'ogg')
+ extension = EnumProperty(
+ name="Extension",
+ items=list(zip(FILE_TYPES, FILE_TYPES, FILE_TYPES)),
+ description="Deprecated")
+
+ filter_image = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
+ filter_movie = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
+ filter_folder = BoolProperty(default=True, options={'HIDDEN', 'SKIP_SAVE'})
+ filter_glob = StringProperty(default="", options={'HIDDEN', 'SKIP_SAVE'})
+
+ t = bpy.types.Material.bl_rna.properties["use_transparent_shadows"]
+ use_transparent_shadows = BoolProperty(name=t.name, default=False, description=t.description)
+
+ t = bpy.types.IMAGE_OT_match_movie_length.bl_rna
+ match_len = BoolProperty(name=t.name, default=True, description=t.description)
+
+ def invoke(self, context, event):
+ # for intractive use, divert to new implementation
+ return bpy.ops.import_image.to_plane_v2(
+ context_to_dict(context), 'INVOKE_DEFAULT'
+ )
+
+ def execute(self, context):
+ target_props = dict(self.DEFAULTS) # start by copying defaults
+ # copy settings with no change in name
+ for k in self.NO_CHANGE:
+ target_props[k] = getattr(self, k)
+ # map settings with new names
+ for k, v in self.REMAP.items():
+ target_props[v] = getattr(self, k)
+ # Handle indirect mappings
+ self.translate_properties(context, target_props)
+
+ return bpy.ops.import_image.to_plane_v2(
+ context_to_dict(context), **target_props
+ )
+
# -----------------------------------------------------------------------------
# Register
def import_images_button(self, context):
- self.layout.operator(IMPORT_OT_image_to_plane.bl_idname,
+ self.layout.operator(IMPORT_IMAGE_OT_to_plane_v2.bl_idname,
text="Images as Planes", icon='TEXTURE')
@@ -543,6 +1358,8 @@ def register():
bpy.utils.register_module(__name__)
bpy.types.INFO_MT_file_import.append(import_images_button)
bpy.types.INFO_MT_mesh_add.append(import_images_button)
+ bpy.app.handlers.load_post.append(register_driver)
+ register_driver()
def unregister():
@@ -550,6 +1367,24 @@ def unregister():
bpy.types.INFO_MT_file_import.remove(import_images_button)
bpy.types.INFO_MT_mesh_add.remove(import_images_button)
+ # This will only exist if drivers are active
+ try:
+ bpy.app.handlers.scene_update_post.remove(check_drivers)
+ except:
+ pass
+
+ try:
+ bpy.app.handlers.load_post.remove(register_driver)
+ del bpy.app.driver_namespace['import_image__find_plane_corner']
+ except:
+ pass
+
if __name__ == "__main__":
+
+ # Run simple doc tests
+ import doctest
+ doctest.testmod()
+
+ unregister()
register()