Add single object raycast method
parent
5590753550
commit
b8180ea84f
|
@ -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",
|
||||||
|
|
|
@ -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)
|
## Increasing search range
|
||||||
## Try condition (not needed)
|
if not object_hit: # or object_hit not in col.all_objects[:]:
|
||||||
# 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
|
|
||||||
# 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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
11
ui.py
|
@ -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)
|
||||||
|
|
58
utils.py
58
utils.py
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue