diff --git a/__init__.py b/__init__.py index d775141..e800dcc 100755 --- a/__init__.py +++ b/__init__.py @@ -20,10 +20,11 @@ module_name = Path(__file__).parent.name sys.modules.update({'gp_interpolate': importlib.import_module(module_name)}) -from gp_interpolate import interpolate_strokes, ui +from gp_interpolate import interpolate_strokes, parent_layer, ui modules = ( interpolate_strokes, + parent_layer, ui, ) diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 4206926..2005dc5 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -149,7 +149,6 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): # Override collection col = intercol - ## TODO: Hide all other collections dg = bpy.context.evaluated_depsgraph_get() strokes_data = [] diff --git a/parent_layer/operator.py b/parent_layer/operator.py deleted file mode 100644 index 92a5c24..0000000 --- a/parent_layer/operator.py +++ /dev/null @@ -1,140 +0,0 @@ -import bpy -import numpy as np -from time import perf_counter, time -from mathutils import Vector, Matrix - -from gp_interpolate.utils import (matrix_transform, - plane_on_bone, - ray_cast_point, - intersect_with_tesselated_plane, - triangle_normal, - search_square, - get_gp_draw_plane, - create_plane, - following_keys, - attr_set) - - -class GP_OT_parent_layer(bpy.types.Operator): - bl_idname = "gp.parent_layer" - bl_label = "Parent Layer" - bl_description = 'Parent Layer' - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - if context.active_object\ - and context.object.type == 'GPENCIL'\ - and context.object.layers.active: - return True - cls.poll_message_set("Need a Grease pencil object with an active layer") - return False - - # @classmethod - # def description(cls, context, properties): - # if properties.next: - # return f"Interpolate Stroke Forward" - # else: - # return f"Interpolate Stroke Backward" - - - def execute(self, context): - settings = context.scene.gp_interpo_settings - scn = bpy.context.scene - - gp = context.object - lay = gp.layers.active - # matrix = np.array(gp.matrix_world, dtype='float64')#.inverted() - # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') - - col = settings.target_collection - if not col: - col = scn.collection - - parent_name = f'ref_{gp.name}_{lay.info}' - parent_collec_name = 'gp_parents' - # print('----') - - included_cols = [c.name for c in gp.users_collection] - start = time() - if settings.method == 'BONE': - if not settings.target_rig or not settings.target_bone: - self.report({'ERROR'}, 'No Bone Selected') - return {'CANCELLED'} - - included_cols.append(parent_collec_name) - - ## Ensure collection and plane exists - # Get/create collection - col = bpy.data.collections.get(parent_collec_name) - if not col: - col = bpy.data.collections.new(parent_collec_name) - - if col.name not in bpy.context.scene.collection.children: - bpy.context.scene.collection.children.link(col) - col.hide_viewport = True - - # Get/create meshplane - plane = bpy.data.objects.get(parent_name) - if not plane: - plane = create_plane(name=parent_name) - plane.select_set(False) - - if plane.name not in col.objects: - col.objects.link(plane) - ## TODO: Ensure the plane is not animated! - - else: - # Geometry mode - if col != context.scene.collection: - included_cols.append(col.name) - ## Maybe include a plane just behing geo ? probably bad idea - - ## Prepare context manager - store_list = [ - # (context.view_layer.objects, 'active', gp), - (context.tool_settings, 'use_keyframe_insert_auto', True), - # (bpy.context.scene.render, 'simplify_subdivision', 0), - ] - - for vlc in context.view_layer.layer_collection.children: - store_list.append( - (vlc, 'exclude', vlc.name not in included_cols), - # (vlc, 'hide_viewport', vlc.name not in included_cols), # viewport viz - ) - - # print(f'Preparation {time()-start:.4f}s') - - with attr_set(store_list): - if settings.method == 'BONE': - ## replace plane - _bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), - arm=settings.target_rig, - set_rotation=settings.use_bone_rotation, - mesh=True) - - ## Set collection visibility - intercol = bpy.data.collections.get(parent_collec_name) - vl_col = bpy.context.view_layer.layer_collection.children.get(intercol.name) - intercol.hide_viewport = vl_col.exclude = vl_col.hide_viewport = False - - # Override collection - col = intercol - ## TODO: Hide all other collections - - print('Done') - return {'FINISHED'} - - -classes = ( - GP_OT_parent_layer, -) - -def register(): - for c in classes: - bpy.utils.register_class(c) - - -def unregister(): - for c in reversed(classes): - bpy.utils.unregister_class(c) diff --git a/parent_layer/operators.py b/parent_layer/operators.py new file mode 100644 index 0000000..4540bcb --- /dev/null +++ b/parent_layer/operators.py @@ -0,0 +1,151 @@ +import bpy +import numpy as np +from time import perf_counter, time +from mathutils import Vector, Matrix + +from gp_interpolate.utils import (get_gp_draw_plane, + is_animated, + create_plane, + following_keys, + place_object_to_ref_facing_cam, + attr_set) + + +class GP_OT_parent_layer(bpy.types.Operator): + bl_idname = "gp.parent_layer" + bl_label = "Parent Layer" + bl_description = 'Parent active layer to object or bone\ + \nBake intermediate parent object to compensate GP offset and moves' + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + if context.object\ + and context.object.type == 'GPENCIL'\ + and context.object.data.layers.active: + return True + cls.poll_message_set("Need a Grease pencil object with an active layer") + return False + + direct_parent : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) + + def parent_and_compensate(self, context): + settings = context.scene.gp_interpo_settings + bone = None + if settings.target_rig.type == 'ARMATURE': + bone = settings.target_rig.pose.bones.get(settings.target_bone) + if not bone: + self.report({'ERROR'}, f'{settings.target_bone} not found in armature {settings.target_rig.name}') + return {'CANCELLED'} + + self.lay.parent = settings.target_rig + if bone: + self.lay.parent_type = 'BONE' + self.lay.parent_bone = bone.name + print(f'Parent to {self.lay.parent.name} > bone {bone.name}') + + ## Offset parent + if bone: + bone_mat = self.lay.parent.matrix_world @ bone.matrix + self.lay.matrix_inverse = bone_mat.inverted() @ self.gp.matrix_world + + else: + print(f'Parent to {self.lay.parent.name}') + self.lay.matrix_inverse = self.lay.parent.matrix_world.inverted() @ self.gp.matrix_world + + return {'FINISHED'} + + def execute(self, context): + settings = context.scene.gp_interpo_settings + scn = bpy.context.scene + + self.gp = context.object + self.lay = self.gp.data.layers.active + + if not settings.target_rig: + self.report({'ERROR'}, 'No object Selected') + return {'CANCELLED'} + + if not settings.target_bone: + self.report({'ERROR'}, 'No Bone Selected') + return {'CANCELLED'} + + + if self.direct_parent: + return self.parent_and_compensate(context) + + + return {'FINISHED'} + + parent_name = f'ref_{self.gp.name}_{self.lay.info}' + parent_collec_name = 'gp_parents' + included_cols = [c.name for c in self.gp.users_collection] + included_cols.append(parent_collec_name) + + ## Ensure collection and parent exists + # Get/create collection + col = bpy.data.collections.get(parent_collec_name) + if not col: + col = bpy.data.collections.new(parent_collec_name) + + if col.name not in bpy.context.scene.collection.children: + bpy.context.scene.collection.children.link(col) + col.hide_viewport = True + + # Get/create meshplane + parent = bpy.data.objects.get(parent_name) + if not parent: + parent = create_plane(name=parent_name) + parent.select_set(False) + + if parent.name not in col.objects: + col.objects.link(parent) + + ## Prepare context manager + store_list = [ + (context.tool_settings, 'use_keyframe_insert_auto', False), + ] + + for vlc in context.view_layer.layer_collection.children: + store_list.append( + (vlc, 'exclude', vlc.name not in included_cols), + # (vlc, 'hide_viewport', vlc.name not in included_cols), # viewport viz + ) + + ## Offset at curent frame to compensate for object, GP (and GP layer ?) transformations + + + + + ## If GP object is animated, animate parent obj to compensate + + ## How to smart-test if self.gp is animated in space ? + if not is_animated(self.gp): + # direct parent with one offset + pass + + else: + with attr_set(store_list): + place_object_to_ref_facing_cam(parent, + ref_ob=settings.target_rig, + bone=bone, + set_rotation=settings.use_bone_rotation + ) + + + print('Done') + return {'FINISHED'} + + +classes = ( + GP_OT_parent_layer, +) + +def register(): + for c in classes: + bpy.utils.register_class(c) + + +def unregister(): + for c in reversed(classes): + bpy.utils.unregister_class(c) diff --git a/ui.py b/ui.py index c99eef6..1e2c359 100755 --- a/ui.py +++ b/ui.py @@ -58,6 +58,8 @@ class GP_PT_interpolate(bpy.types.Panel): if settings.mode == 'FRAME': col.prop(settings, 'padding') + + layout.operator('gp.parent_layer', text='Parent Layer To Target') classes = ( GP_PT_interpolate, diff --git a/utils.py b/utils.py index f4bc6fe..7274141 100644 --- a/utils.py +++ b/utils.py @@ -159,6 +159,72 @@ def plane_on_bone(bone, arm=None, cam=None, set_rotation=True, mesh=True): plane = plane_coords() return matrix_transform(plane, mat @ mat_scale) +def place_object_to_ref_facing_cam(obj, ref_ob, bone=None, cam=None, set_rotation=True): + ''' + obj (Object): the object to place + ref_ob (Object): the reference object or armature + bone (posebone): reference pose bone + arm (optional: Armature): Armature of the pose bone (if not passed found using bone.id_data) + cam (optional: Camera) : Camera to align plane to (if not passed use scene camera) + set_rotation (bool): rotate the plane on cam view axis according to bone direction in 2d cam space + ''' + + if cam is None: + cam = bpy.context.scene.camera + + # if ref_ob is None: + # ref_ob = bone.id_data + + mat = cam.matrix_world.copy() + + if set_rotation: + if bone: + head_world_coord = ref_ob.matrix_world @ bone.head + mat.translation = head_world_coord + + ## Apply 2d bone rotation facing camera + # Get 2d camera space coords (NDC: normalized device coordinate, 0,0 is bottom-left) + head_2d, tail_2d = get_bone_head_tail_2d(bone, cam=cam) + else: + mat.translation = ref_ob.matrix_world + # Get 2d camera space coords (NDC: normalized device coordinate, 0,0 is bottom-left) + scene = bpy.context.scene + up_vec = Vector((0,0,1)) + up_vec.rotate(ref_ob.matrix_world) + tail_3d = ref_ob.matrix_world.to_translation() + up_vec + head_2d = world_to_camera_view(scene, cam, ref_ob.matrix_world.to_translation()) + tail_2d = world_to_camera_view(scene, cam, tail_3d) + ratio = scene.render.resolution_y / scene.render.resolution_x + head_2d.y *= ratio + tail_2d.y *= ratio + + vec_from_corner_2d = (tail_2d - head_2d).normalized() + up_vec_2d = Vector((0,1)) + # angle = acos(up_vec_2d.dot(vec_from_corner_2d)) ## equivalent but not signed! + angle = up_vec_2d.angle_signed(vec_from_corner_2d) + + ## Axis camera aim (seem slightly off) + # rot_axis = Vector((0, 0, -1)) + # rot_axis.rotate(cam.matrix_world) + + ## Axis camera origin -> pivot + rot_axis = head_world_coord - cam.matrix_world.translation + mat = rotate_matrix_around_pivot(mat, angle, head_world_coord, rot_axis) + + else: + if bone: + ## Use mid bone to better follow movement + mat.translation = ref_ob.matrix_world @ ((bone.tail + bone.head) / 2) # Mid bone + else: + mat.translation = ref_ob.matrix_world + + ## change/adapt scale + # mat_scale = Matrix.Scale(10, 4) # maybe move above mesh condition + # mat = mat @ mat_scale + obj.matrix_world = mat + + + def create_plane(name='Plane', collection=None): '''Create a plane using pydata collection: link in passed collection, else do not link in scene @@ -335,3 +401,9 @@ def following_keys(forward=True, all_keys=False) -> list:# -> list[int] | list | if new is None: return [] return [int(new)] + + +## -- animation + +def is_animated(obj): + return True \ No newline at end of file