Add single object raycast method

master
pullusb 2024-02-06 15:57:35 +01:00
parent 5590753550
commit b8180ea84f
5 changed files with 141 additions and 62 deletions

View File

@ -1,7 +1,7 @@
bl_info = { bl_info = {
"name": "gp interpolate", "name": "gp interpolate",
"author": "Christophe Seux, Samuel Bernou", "author": "Christophe Seux, Samuel Bernou",
"version": (0, 4, 2), "version": (0, 5, 0),
"blender": (3, 6, 0), "blender": (3, 6, 0),
"location": "Sidebar > Gpencil Tab > Interpolate", "location": "Sidebar > Gpencil Tab > Interpolate",
"description": "Interpolate Grease pencil strokes over 3D", "description": "Interpolate Grease pencil strokes over 3D",

View File

@ -6,12 +6,14 @@ from mathutils import Vector, Matrix
from gp_interpolate.utils import (matrix_transform, from gp_interpolate.utils import (matrix_transform,
plane_on_bone, plane_on_bone,
ray_cast_point, ray_cast_point,
obj_ray_cast,
intersect_with_tesselated_plane, intersect_with_tesselated_plane,
triangle_normal, triangle_normal,
search_square, search_square,
get_gp_draw_plane, get_gp_draw_plane,
create_plane, create_plane,
following_keys, following_keys,
index_list_from_bools,
attr_set) attr_set)
from mathutils.geometry import (barycentric_transform, 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: add bake animation to empty for later GP layer parenting
## TODO: Occlusion management
class GP_OT_interpolate_stroke(bpy.types.Operator): class GP_OT_interpolate_stroke(bpy.types.Operator):
bl_idname = "gp.interpolate_stroke" bl_idname = "gp.interpolate_stroke"
@ -63,10 +66,10 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
gp = context.object gp = context.object
# matrix = gp.matrix_world # matrix = np.array(gp.matrix_world, dtype='float64')
# origin = scn.camera.matrix_world.to_translation() # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64')
matrix = np.array(gp.matrix_world, dtype='float64') matrix = gp.matrix_world
origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') origin = scn.camera.matrix_world.to_translation()
col = settings.target_collection col = settings.target_collection
if not col: if not col:
@ -87,10 +90,10 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
return {'CANCELLED'} return {'CANCELLED'}
included_cols = [c.name for c in gp.users_collection] included_cols = [c.name for c in gp.users_collection]
target_obj = None
start = time() start = time()
if settings.method == 'BONE': if settings.method == 'BONE':
## Follow Bone method (WIP)
if not settings.target_rig or not settings.target_bone: if not settings.target_rig or not settings.target_bone:
self.report({'ERROR'}, 'No Bone selected') self.report({'ERROR'}, 'No Bone selected')
return {'CANCELLED'} return {'CANCELLED'}
@ -115,13 +118,23 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
if plane.name not in toolcol.objects: if plane.name not in toolcol.objects:
toolcol.objects.link(plane) toolcol.objects.link(plane)
## TODO: Ensure the plane is not animated! target_obj = plane
else: elif settings.method == 'GEOMETRY':
# Geometry mode
if col != context.scene.collection: if col != context.scene.collection:
included_cols.append(col.name) 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 ## Prepare context manager
store_list = [ store_list = [
@ -130,8 +143,9 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
# (bpy.context.scene.render, 'simplify_subdivision', 0), # (bpy.context.scene.render, 'simplify_subdivision', 0),
] ]
# TODO : Customize below filter to use in geo mode as well # TODO: for now, the collection filter is not used at all in GEOMETRY mode
# so it does not exclude collections containing rig # it can be used to hide collection for faster animation mode
if settings.method == 'BONE': if settings.method == 'BONE':
## TEST: Add collections containing rig (cannot be excluded) ## 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] # 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 = [] stroke_data = []
for i, point in enumerate(stroke.points): for i, point in enumerate(stroke.points):
# print(si, i)
point_co_world = world_co_3d[i] point_co_world = world_co_3d[i]
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) 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'}
## with one simple extra search ## Increasing search range
# if not object_hit or object_hit not in col.all_objects[:]: 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[:]:
found = False found = False
for iteration in range(1, 6): for iteration in range(1, 6):
for square_co in search_square(point_co_world, factor=settings.search_range * iteration): 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 target_obj:
if object_hit and object_hit in col.all_objects[:]: 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)) hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
found = True found = True
# print(f'{si}:{i} iteration {iteration}') # Dbg # 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: for f in frames_to_jump:
wm.progress_update(f) # Pgs wm.progress_update(f) # Pgs
scn.frame_set(f) scn.frame_set(f)
# origin = scn.camera.matrix_world.to_translation() origin = scn.camera.matrix_world.to_translation()
origin = np.array(scn.camera.matrix_world.to_translation(), 'float64') # origin = np.array(scn.camera.matrix_world.to_translation(), 'float64')
plan_co, plane_no = get_gp_draw_plane(gp) plan_co, plane_no = get_gp_draw_plane(gp)
bpy.ops.gpencil.paste() 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() matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted()
new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] 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 = [] world_co_3d = []
for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data:
eval_ob = object_hit.evaluated_get(dg) 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.foreach_set('co', new_local_co_3d.reshape(nb_points*3))
new_stroke.points.update() 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 wm.progress_end() # Pgs

View File

@ -19,6 +19,7 @@ class GP_PG_interpolate_settings(PropertyGroup):
items= ( items= (
('GEOMETRY', 'Geometry', 'Directly follow underlying geometry', 0), ('GEOMETRY', 'Geometry', 'Directly follow underlying geometry', 0),
('BONE', 'Bone', 'Pick an armature bone and follow it', 1), ('BONE', 'Bone', 'Pick an armature bone and follow it', 1),
('OBJECT', 'Object', 'Directly follow a specific object, even if occluded', 2),
), ),
default='GEOMETRY', default='GEOMETRY',
description='Select method for interpolating strokes' 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", \nThe value is as percentage of the camera width",
default=0.05, precision=2, step=3, options={'HIDDEN'}) default=0.05, precision=2, step=3, options={'HIDDEN'})
mode : EnumProperty( mode : EnumProperty(
name='Mode', name='Mode',
# Combined ?markers ? # Combined ?markers ?
@ -64,9 +64,10 @@ class GP_PG_interpolate_settings(PropertyGroup):
description='Rig to use as target', description='Rig to use as target',
type=bpy.types.Object) type=bpy.types.Object)
# target_rig : StringProperty( target_object : PointerProperty(
# name='Rig', name='Object',
# description='Rig to use as target') description='Object to interpolate on',
type=bpy.types.Object)
target_bone : StringProperty( target_bone : StringProperty(
name='Bone', name='Bone',

11
ui.py
View File

@ -27,14 +27,6 @@ class GP_PT_interpolate(bpy.types.Panel):
row.operator("gp.interpolate_stroke", text="", icon=prev_icon).next = False row.operator("gp.interpolate_stroke", text="", icon=prev_icon).next = False
row.operator("gp.interpolate_stroke", text="", icon=next_icon).next = True 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, 'use_animation', text='Animation')
col.prop(settings, 'method', text='Method') col.prop(settings, 'method', text='Method')
@ -51,6 +43,9 @@ class GP_PT_interpolate(bpy.types.Panel):
col.prop(settings, 'target_collection', text='Collection') col.prop(settings, 'target_collection', text='Collection')
col.prop(settings, 'search_range') col.prop(settings, 'search_range')
elif settings.method == 'OBJECT':
col.prop(settings, 'target_object', text='Object')
col.separator() col.separator()
col = layout.column(align=True) col = layout.column(align=True)
row = col.row(align=True) row = col.row(align=True)

View File

@ -68,15 +68,8 @@ def search_square(point, factor=0.05, cam=None):
return matrix_transform(plane, mat @ mat_scale) return matrix_transform(plane, mat @ mat_scale)
def ray_cast_point(point, origin, depsgraph): def get_tri_from_face(hit_location, face_index, object_hit, 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
eval_ob = object_hit.evaluated_get(depsgraph) eval_ob = object_hit.evaluated_get(depsgraph)
face = eval_ob.data.polygons[face_index] face = eval_ob.data.polygons[face_index]
vertices = [eval_ob.data.vertices[i] for i in face.vertices] 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) 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): if intersect_point_tri(hit_location, *tri):
break 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 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): 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)] 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 ## -- animation
def is_animated(obj): def is_animated(obj):