diff --git a/__init__.py b/__init__.py index 2cc6458..ce839d4 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "gp interpolate", "author": "Christophe Seux, Samuel Bernou", - "version": (0, 1, 0), + "version": (0, 1, 1), "blender": (3, 6, 0), "location": "Sidebar > Gpencil Tab > Interpolate", "description": "Interpolate Grease pencil strokes over 3D", diff --git a/interpolate_strokes/operators.py b/interpolate_strokes/operators.py index 46ed626..288ae5c 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -1,10 +1,12 @@ import bpy import numpy as np -from math import tan +from math import tan, acos, degrees from time import perf_counter from mathutils import Vector, Matrix +from gp_interpolate import utils + from mathutils.geometry import (barycentric_transform, intersect_point_tri, intersect_point_line, intersect_line_plane, tessellate_polygon) @@ -101,7 +103,7 @@ def ray_cast_point(point, origin, depsgraph): return object_hit, np.array(hit_location), tri, tri_indices -def plane_on_bone(bone, arm=None, cam=None): +def plane_on_bone(bone, arm=None, cam=None, set_rotation=True): if cam is None: cam = bpy.context.scene.camera @@ -110,22 +112,47 @@ def plane_on_bone(bone, arm=None, cam=None): plane = plane_coords() mat = cam.matrix_world.copy() - ## center - mat.translation = arm @ ((bone.tail + bone.head) / 2) - ## plane is 1 unit side - mat_scale = Matrix.Scale(10,4) + + + if set_rotation: + head_world_coord = arm.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 = utils.get_bone_head_tail_2d(bone, cam=cam) + 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 = utils.rotate_matrix_around_pivot(mat, angle, head_world_coord, rot_axis) + + else: + ## Use mid bone to better follow movement + mat.translation = arm.matrix_world @ ((bone.tail + bone.head) / 2) # Mid bone + + mat_scale = Matrix.Scale(10, 4) return matrix_transform(plane, mat @ mat_scale) -def cast_on_plane(point, origin, face_co): +def intersect_with_tesselated_plane(point, origin, face_co): ''' - face_co: World face coordinate + face_co: World face coordinates ''' tri = None for tri_idx in tessellate_polygon([face_co]): tri = [face_co[i] for i in tri_idx] tri_indices = [i for i in tri_idx] - hit_location = intersect_line_plane(origin, point, tri, triangle_normal(*tri)) + hit_location = intersect_line_plane(origin, point, sum((Vector(v) for v in tri), Vector()) / 3, triangle_normal(*tri)) if intersect_point_tri(hit_location, *tri): break @@ -230,15 +257,14 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): strokes_data = [] if settings.method == 'BONE': - self.report({'ERROR'}, 'Mega WIP') - return {'CANCELLED'} - ## Follow Bone method (Full WIP) if not settings.target_rig or not settings.target_bone: self.report({'ERROR'}, 'No Bone selected') return {'CANCELLED'} - bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), arm=settings.target_rig) + bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), + arm=settings.target_rig, + set_rotation=settings.use_bone_rotation) strokes_data = [] for stroke in tgt_strokes: @@ -252,11 +278,10 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): for i, point in enumerate(stroke.points): point_co_world = world_co_3d[i] - hit_location = cast_on_plane(point_co_world, origin, bone_plane) + hit_location, tri, tri_indices = intersect_with_tesselated_plane(point_co_world, origin, bone_plane) ## probably easier to just generate a single vast triangle and use it - # hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) - ## Store same as other method, without object hit + ## Store same as other method (without object hit) stroke_data.append((stroke, point_co_world, hit_location, tri, tri_indices)) strokes_data.append(stroke_data) @@ -275,19 +300,21 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted() new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] - bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), arm=settings.target_rig) + bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), + arm=settings.target_rig, + set_rotation=settings.use_bone_rotation) for new_stroke, stroke_data in zip(new_strokes, strokes_data): world_co_3d = [] # np.array(len()dtype='float64')#np. for stroke, point_co, hit_location, tri_a, tri_indices in stroke_data: - ## MEGA WIP - ## TODO: use new bone_plane to set new coordinate tri_b = [bone_plane[i] for i in tri_indices] - tri_b = matrix_transform(tri_b, eval_ob.matrix_world) - + # tri_b = matrix_transform(tri_b, settings.target_rig.matrix_world) + ## rotate tri_b by bone differential angle camera's aim axis ? + new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) world_co_3d.append(new_loc) + # Reproject on plane new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] new_local_co_3d = matrix_transform(new_world_co_3d, matrix_inv) diff --git a/interpolate_strokes/properties.py b/interpolate_strokes/properties.py index 13f4cce..d2488c7 100644 --- a/interpolate_strokes/properties.py +++ b/interpolate_strokes/properties.py @@ -67,6 +67,11 @@ class GP_PG_interpolate_settings(PropertyGroup): name='Bone', description='Bone of the rig to follow when interpolating') # Bone + use_bone_rotation : BoolProperty( + name='Use Bone Rotation', + default=True, + description='Apply rotation of the bone') # Bone + classes = ( GP_PG_interpolate_settings, ) diff --git a/ui.py b/ui.py index 1df94f7..9a7e24e 100755 --- a/ui.py +++ b/ui.py @@ -37,6 +37,7 @@ class GP_PT_interpolate(bpy.types.Panel): if settings.target_rig: # col.prop_search(ob.rig_picker, 'name', settings.target_rig.pose, 'bones', text='Bone') col.prop_search(settings, 'target_bone', settings.target_rig.pose, 'bones', text='Bone') + col.prop(settings, 'use_bone_rotation', text='Use Bone Rotation') elif settings.method == 'GEOMETRY': diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..b6fcb7c --- /dev/null +++ b/utils.py @@ -0,0 +1,52 @@ +import bpy +import math + +from mathutils import Vector, Matrix +from bpy_extras.object_utils import world_to_camera_view + +def get_bone_head_tail_2d(posebone, scene=None, cam=None) -> tuple[Vector, Vector]: + '''Get 2D vectors in camera view of bone head and tails + return tuple of 2d vectors (head_2d and tail_2d) + ''' + scene = scene or bpy.context.scene + cam = cam or scene.camera + + arm = posebone.id_data + + # Get 3D locations of head and tail + head_3d = arm.matrix_world @ posebone.head + tail_3d = arm.matrix_world @ posebone.tail + + # Convert 3D locations to 2D + head_2d = world_to_camera_view(scene, cam, head_3d) + 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 + + return Vector((head_2d.x, head_2d.y)), Vector((tail_2d.x, tail_2d.y)) + + +def rotate_matrix_around_pivot(matrix, angle, pivot, axis): + '''Rotate a given matrix by a CW angle around pivot on a given axis + matrix (Matrix): the matrix to rotate + angle (Float, Radians): the angle in radians + pivot (Vector3): the pivot 3D coordinate + axis (Vector3): the vector axis of rotation + ''' + + # Convert angle to radians ? + # angle = math.radians(angle) + + # Create a rotation matrix + rot_matrix = Matrix.Rotation(angle, 4, axis) + + # Create translation matrices + translate_to_origin = Matrix.Translation(-pivot) + translate_back = Matrix.Translation(pivot) + + # Combine the transformations : The order of multiplication is important + new_matrix = translate_back @ rot_matrix @ translate_to_origin @ matrix + + return new_matrix \ No newline at end of file