diff --git a/__init__.py b/__init__.py index d8e0aa0..52d40c3 100755 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "gp interpolate", "author": "Christophe Seux, Samuel Bernou", - "version": (0, 4, 2), + "version": (0, 5, 0), "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 e7b04d4..a5d3a4d 100644 --- a/interpolate_strokes/operators.py +++ b/interpolate_strokes/operators.py @@ -6,12 +6,14 @@ from mathutils import Vector, Matrix from gp_interpolate.utils import (matrix_transform, plane_on_bone, ray_cast_point, + obj_ray_cast, intersect_with_tesselated_plane, triangle_normal, search_square, get_gp_draw_plane, create_plane, following_keys, + index_list_from_bools, attr_set) from mathutils.geometry import (barycentric_transform, @@ -22,6 +24,7 @@ from mathutils.geometry import (barycentric_transform, ## TODO: add bake animation to empty for later GP layer parenting +## TODO: Occlusion management class GP_OT_interpolate_stroke(bpy.types.Operator): bl_idname = "gp.interpolate_stroke" @@ -63,10 +66,10 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): gp = context.object - # matrix = gp.matrix_world - # origin = scn.camera.matrix_world.to_translation() - matrix = np.array(gp.matrix_world, dtype='float64') - origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') + # matrix = np.array(gp.matrix_world, dtype='float64') + # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') + matrix = gp.matrix_world + origin = scn.camera.matrix_world.to_translation() col = settings.target_collection if not col: @@ -87,10 +90,10 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): return {'CANCELLED'} included_cols = [c.name for c in gp.users_collection] + target_obj = None start = time() if settings.method == 'BONE': - ## Follow Bone method (WIP) if not settings.target_rig or not settings.target_bone: self.report({'ERROR'}, 'No Bone selected') return {'CANCELLED'} @@ -115,13 +118,23 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): if plane.name not in toolcol.objects: toolcol.objects.link(plane) - ## TODO: Ensure the plane is not animated! + target_obj = plane - else: - # Geometry mode + elif settings.method == 'GEOMETRY': if col != context.scene.collection: included_cols.append(col.name) - ## Maybe include a plane just behing geo ? probably bad idea + ## Maybe include a plane just behind geo ? probably bad idea + + elif settings.method == 'OBJECT': + if not settings.target_object: + self.report({'ERROR'}, 'No Object selected') + return {'CANCELLED'} + col = scn.collection # Reset collection filter + target_obj = settings.target_object + if target_obj.library: + ## Look if an override exists in scene to use instead of default object + if (override := next((o for o in scn.objects if o.name == target_obj.name and o.override_library), None)): + target_obj = override ## Prepare context manager store_list = [ @@ -130,8 +143,9 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): # (bpy.context.scene.render, 'simplify_subdivision', 0), ] - # TODO : Customize below filter to use in geo mode as well - # so it does not exclude collections containing rig + # TODO: for now, the collection filter is not used at all in GEOMETRY mode + # it can be used to hide collection for faster animation mode + if settings.method == 'BONE': ## TEST: Add collections containing rig (cannot be excluded) # rig_parent_cols = [c.name for c in scn.collection.children_recursive if settings.target_rig.name in c.all_objects] @@ -173,38 +187,26 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): stroke_data = [] for i, point in enumerate(stroke.points): - # print(si, i) point_co_world = world_co_3d[i] - - object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - ## Try condition (not needed) - # try: - # object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - # except Exception as e: - # print(f'Error on first {si}:{i}') - # self.report({'ERROR'}, f'Error on first {si}:{i}') - # for p in stroke.points: - # p.select = False - # stroke.points[i].select = True - # print(e) - # return {'CANCELLED'} + if target_obj: + object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg) + else: + # scene raycast + object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) - ## with one simple extra search - # if not object_hit or object_hit not in col.all_objects[:]: - # for square_co in search_square(point_co_world, factor=settings.search_range): - # object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg) - # if object_hit and object_hit in col.all_objects[:]: - # hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) - # break - - ### with increasing search range - if not object_hit or object_hit not in col.all_objects[:]: + ## Increasing search range + if not object_hit: # or object_hit not in col.all_objects[:]: found = False for iteration in range(1, 6): for square_co in search_square(point_co_world, factor=settings.search_range * iteration): - object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg) - if object_hit and object_hit in col.all_objects[:]: + if target_obj: + object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(square_co), origin, dg) + else: + # scene raycast + object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) + + if object_hit: # and object_hit in col.all_objects[:]: hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) found = True # print(f'{si}:{i} iteration {iteration}') # Dbg @@ -241,8 +243,8 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): for f in frames_to_jump: wm.progress_update(f) # Pgs scn.frame_set(f) - # origin = scn.camera.matrix_world.to_translation() - origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') + origin = scn.camera.matrix_world.to_translation() + # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') plan_co, plane_no = get_gp_draw_plane(gp) bpy.ops.gpencil.paste() @@ -256,7 +258,7 @@ 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):] - for new_stroke, stroke_data in zip(new_strokes, strokes_data): + for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(strokes_data)): world_co_3d = [] for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: eval_ob = object_hit.evaluated_get(dg) @@ -288,6 +290,45 @@ class GP_OT_interpolate_stroke(bpy.types.Operator): new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3)) new_stroke.points.update() + + ## TODO: Occlusion management + ## Tag occlusion on points for removal (need to create all substrokes from existing strokes) + # if settings.method == 'GEOMETRY': + # ## WIP + # occlusion_list = [False]*len(world_co_3d) + # for i, nco in enumerate(world_co_3d): + # vec_direction = nco - origin + # ## Maybe distance need to be reduced by tiny segment... + # n_hit, _hit_location, _normal, _n_face_index, n_object_hit, _matrix = scn.ray_cast(dg, origin, vec_direction, distance=vec_direction.length) + # # if n_hit and n_object_hit != object_hit: # note: Arm could still hit from torso... + # if n_hit: + # # if there is a hit, it's occluded + # # Occluded ! + # occlusion_list[i] = True + + # if all(occlusion_list): + # # all occluded, Just remove stroke (! Safer to reverse both list in zip iteration !) + # gp.data.layers.active.active_frame.strokes.remove(new_stroke) + + # if any(occlusion_list): + # # Create substroke according to indices in original stroke + # for sublist in index_list_from_bools(occlusion_list): + # ## Clear if only one isolated point ? + # # if len(sublist) == 1: + # # continue + # ns = gp.data.layers.active.active_frame.strokes.new() + # for elem in ('hardness', 'material_index', 'line_width'): + # setattr(ns, elem, getattr(new_strokes, elem)) + + # ns.points.add(len(sublist)) + # for i, point_index in enumerate(sublist): + # for elem in ('uv_factor', 'uv_fill', 'uv_rotation', 'pressure', 'co', 'strength', 'vertex_color'): + # setattr(ns.points[i], elem, getattr(new_strokes.points[point_index], elem)) + + # ## Delete original stroke + # gp.data.layers.active.active_frame.strokes.remove(new_stroke) + + wm.progress_end() # Pgs diff --git a/interpolate_strokes/properties.py b/interpolate_strokes/properties.py index 38afea0..ace8f78 100644 --- a/interpolate_strokes/properties.py +++ b/interpolate_strokes/properties.py @@ -19,6 +19,7 @@ class GP_PG_interpolate_settings(PropertyGroup): items= ( ('GEOMETRY', 'Geometry', 'Directly follow underlying geometry', 0), ('BONE', 'Bone', 'Pick an armature bone and follow it', 1), + ('OBJECT', 'Object', 'Directly follow a specific object, even if occluded', 2), ), default='GEOMETRY', description='Select method for interpolating strokes' @@ -34,7 +35,6 @@ class GP_PG_interpolate_settings(PropertyGroup): \nThe value is as percentage of the camera width", default=0.05, precision=2, step=3, options={'HIDDEN'}) - mode : EnumProperty( name='Mode', # Combined ?markers ? @@ -64,10 +64,11 @@ class GP_PG_interpolate_settings(PropertyGroup): description='Rig to use as target', type=bpy.types.Object) - # target_rig : StringProperty( - # name='Rig', - # description='Rig to use as target') - + target_object : PointerProperty( + name='Object', + description='Object to interpolate on', + type=bpy.types.Object) + target_bone : StringProperty( name='Bone', description='Bone of the rig to follow when interpolating') # Bone diff --git a/ui.py b/ui.py index a4a7fae..1e06516 100755 --- a/ui.py +++ b/ui.py @@ -26,14 +26,6 @@ class GP_PT_interpolate(bpy.types.Panel): row.scale_x = 3 row.operator("gp.interpolate_stroke", text="", icon=prev_icon).next = False row.operator("gp.interpolate_stroke", text="", icon=next_icon).next = True - - ## Old version to test (TODO: delete later) - # col.label(text='Test Old Ops') - # row = col.row(align=True) - # row.scale_x = 3 - # row.operator("gp.interpolate_stroke_simple", text="", icon=prev_icon).next = False - # row.operator("gp.interpolate_stroke_simple", text="", icon=next_icon).next = True - col.prop(settings, 'use_animation', text='Animation') col.prop(settings, 'method', text='Method') @@ -50,6 +42,9 @@ class GP_PT_interpolate(bpy.types.Panel): elif settings.method == 'GEOMETRY': col.prop(settings, 'target_collection', text='Collection') col.prop(settings, 'search_range') + + elif settings.method == 'OBJECT': + col.prop(settings, 'target_object', text='Object') col.separator() col = layout.column(align=True) diff --git a/utils.py b/utils.py index 164767c..9aa7022 100644 --- a/utils.py +++ b/utils.py @@ -68,15 +68,8 @@ def search_square(point, factor=0.05, cam=None): return matrix_transform(plane, mat @ mat_scale) -def ray_cast_point(point, origin, depsgraph): - ray = (point - origin)#.normalized() - hit, hit_location, normal, face_index, object_hit, matrix = bpy.context.scene.ray_cast(depsgraph, origin, ray) - - if not hit: - return None, None, None, None - +def get_tri_from_face(hit_location, face_index, object_hit, depsgraph): eval_ob = object_hit.evaluated_get(depsgraph) - face = eval_ob.data.polygons[face_index] vertices = [eval_ob.data.vertices[i] for i in face.vertices] face_co = matrix_transform([v.co for v in vertices], eval_ob.matrix_world) @@ -88,8 +81,37 @@ def ray_cast_point(point, origin, depsgraph): if intersect_point_tri(hit_location, *tri): break + return tri, tri_indices + +def ray_cast_point(point, origin, depsgraph): + ray = (point - origin) + hit, hit_location, normal, face_index, object_hit, matrix = bpy.context.scene.ray_cast(depsgraph, origin, ray) + + if not hit: + return None, None, None, None + + tri, tri_indices = get_tri_from_face(hit_location, face_index, object_hit, depsgraph) + return object_hit, np.array(hit_location), tri, tri_indices +def obj_ray_cast(obj, point, origin, depsgraph): + """Wrapper for ray casting that moves the ray into object space""" + + # get the ray relative to the object + matrix_inv = obj.matrix_world.inverted() + ray_origin_obj = matrix_inv @ origin # matrix_transform(origin, matrix_inv) + ray_target_obj = matrix_inv @ point # matrix_transform(point, matrix_inv) + ray_direction_obj = ray_target_obj - ray_origin_obj + # cast the ray + success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj, depsgraph=depsgraph) + if not success: + return None, None, None, None + + # Get hit location world_space + hit_location = obj.matrix_world @ location + tri, tri_indices = get_tri_from_face(hit_location, face_index, obj, depsgraph) + return obj, np.array(hit_location), tri, tri_indices + def empty_at(name='Empty', pos=(0,0,0), collection=None, type='PLAIN_AXES', size=1, show_name=False): ''' @@ -425,6 +447,26 @@ def following_keys(forward=True, all_keys=False) -> list:# -> list[int] | list | return [int(new)] +def index_list_from_bools(bool_list) -> list: + '''Receive a list of boolean + Return a list of sublists of indices where there is a continuity of True. + e.g., [True, True, False, True] will return [[0,1][3]] + ''' + result = [] + current_sublist = [] + + for i, value in enumerate(bool_list): + if value: + current_sublist.append(i) + elif current_sublist: + result.append(current_sublist) + current_sublist = [] + + if current_sublist: + result.append(current_sublist) + + return result + ## -- animation def is_animated(obj):