mega wip bone plane interpo

master
pullusb 2023-11-30 19:03:20 +01:00
parent 6ec11e1170
commit 7d05e7390d
3 changed files with 197 additions and 63 deletions

View File

@ -101,6 +101,36 @@ def ray_cast_point(point, origin, depsgraph):
return object_hit, np.array(hit_location), tri, tri_indices return object_hit, np.array(hit_location), tri, tri_indices
def plane_on_bone(bone, arm=None, cam=None):
if cam is None:
cam = bpy.context.scene.camera
if arm is None:
arm = bone.id_data
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)
return matrix_transform(plane, mat @ mat_scale)
def cast_on_plane(point, origin, face_co):
'''
face_co: World face coordinate
'''
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))
if intersect_point_tri(hit_location, *tri):
break
return np.array(hit_location), tri, tri_indices
def following_key(forward=True): def following_key(forward=True):
direction = 1 if forward else -1 direction = 1 if forward else -1
cur_frame = bpy.context.scene.frame_current cur_frame = bpy.context.scene.frame_current
@ -159,13 +189,6 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
next : bpy.props.BoolProperty(name='Next', default=True, options={'SKIP_SAVE'}) next : bpy.props.BoolProperty(name='Next', default=True, options={'SKIP_SAVE'})
# jump : bpy.props.EnumProperty(name='Direction', default='NEXT',
# items=(
# ('NEXT', 'Next', 'Next frame', 0),
# ('PREV', 'Previous', 'Previous frame', 0)
# ),
# )
def execute(self, context): def execute(self, context):
settings = context.scene.gp_interpo_settings settings = context.scene.gp_interpo_settings
@ -195,7 +218,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
tgt_strokes = [s for s in gp.data.layers.active.active_frame.strokes if s.select] tgt_strokes = [s for s in gp.data.layers.active.active_frame.strokes if s.select]
## If nothing selected in sculpt/paint, Select all befaore triggering ## If nothing selected in sculpt/paint, Select all before triggering
if not tgt_strokes and context.mode in ('SCULPT_GPENCIL', 'PAINT_GPENCIL'): if not tgt_strokes and context.mode in ('SCULPT_GPENCIL', 'PAINT_GPENCIL'):
for s in gp.data.layers.active.active_frame.strokes: for s in gp.data.layers.active.active_frame.strokes:
s.select = True s.select = True
@ -205,66 +228,135 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
self.report({'ERROR'}, 'No stroke selected !') self.report({'ERROR'}, 'No stroke selected !')
return {'CANCELLED'} return {'CANCELLED'}
strokes_data = [] strokes_data = []
for stroke in tgt_strokes: if settings.method == 'BONE':
nb_points = len(stroke.points) self.report({'ERROR'}, 'Mega WIP')
return {'CANCELLED'}
local_co = np.empty(nb_points * 3, dtype='float64') ## Follow Bone method (Full WIP)
stroke.points.foreach_get('co', local_co) if not settings.target_rig or not settings.target_bone:
# local_co_3d = local_co.reshape((nb_points, 3)) self.report({'ERROR'}, 'No Bone selected')
world_co_3d = matrix_transform(local_co.reshape((nb_points, 3)), matrix) return {'CANCELLED'}
stroke_data = [] bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), arm=settings.target_rig)
for i, point in enumerate(stroke.points):
point_co_world = world_co_3d[i]
object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg) strokes_data = []
if not object_hit or object_hit not in col.all_objects[:]: for stroke in tgt_strokes:
for square_co in search_square(point_co_world, factor=settings.search_range): nb_points = len(stroke.points)
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg) local_co = np.empty(nb_points * 3, dtype='float64')
if object_hit and object_hit in col.all_objects[:]: stroke.points.foreach_get('co', local_co)
# local_co_3d = local_co.reshape((nb_points, 3))
world_co_3d = matrix_transform(local_co.reshape((nb_points, 3)), matrix)
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri)) stroke_data = []
break 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)
## 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))
stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices)) ## 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) strokes_data.append(stroke_data)
bpy.ops.gpencil.copy()
scn.frame_set(frame_to_jump)
dg = bpy.context.evaluated_depsgraph_get()
plan_co, plane_no = get_gp_draw_plane(gp)
bpy.ops.gpencil.paste()
bpy.ops.gpencil.copy() matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted()
new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):]
scn.frame_set(frame_to_jump) bone_plane = plane_on_bone(settings.target_rig.pose.bones.get(settings.target_bone), arm=settings.target_rig)
dg = bpy.context.evaluated_depsgraph_get() 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)
plan_co, plane_no = get_gp_draw_plane(gp) new_loc = barycentric_transform(hit_location, *tri_a, *tri_b)
world_co_3d.append(new_loc)
bpy.ops.gpencil.paste() # 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)
nb_points = len(new_stroke.points)
new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3))
new_stroke.points.update()
matrix_inv = np.array(gp.matrix_world.inverted(), dtype='float64')#.inverted() else:
new_strokes = gp.data.layers.active.active_frame.strokes[-len(strokes_data):] ## Geometry method
for stroke in tgt_strokes:
nb_points = len(stroke.points)
for new_stroke, stroke_data in zip(new_strokes, strokes_data): local_co = np.empty(nb_points * 3, dtype='float64')
world_co_3d = [] # np.array(len()dtype='float64')#np. stroke.points.foreach_get('co', local_co)
for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data: # local_co_3d = local_co.reshape((nb_points, 3))
eval_ob = object_hit.evaluated_get(dg) world_co_3d = matrix_transform(local_co.reshape((nb_points, 3)), matrix)
tri_b = [eval_ob.data.vertices[i].co for i in tri_indices]
tri_b = matrix_transform(tri_b, eval_ob.matrix_world)
new_loc = barycentric_transform(hit_location, *tri_a, *tri_b) stroke_data = []
world_co_3d.append(new_loc) for i, point in enumerate(stroke.points):
point_co_world = world_co_3d[i]
# Reproject on plane object_hit, hit_location, tri, tri_indices = ray_cast_point(point_co_world, origin, dg)
new_world_co_3d = [intersect_line_plane(origin, p, plan_co, plane_no) for p in world_co_3d] if not object_hit or object_hit not in col.all_objects[:]:
new_local_co_3d = matrix_transform(new_world_co_3d, matrix_inv) 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[:]:
nb_points = len(new_stroke.points) hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3))
new_stroke.points.update() break
stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices))
strokes_data.append(stroke_data)
bpy.ops.gpencil.copy()
scn.frame_set(frame_to_jump)
dg = bpy.context.evaluated_depsgraph_get()
plan_co, plane_no = get_gp_draw_plane(gp)
bpy.ops.gpencil.paste()
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):
world_co_3d = [] # np.array(len()dtype='float64')#np.
for stroke, point_co, object_hit, hit_location, tri_a, tri_indices in stroke_data:
eval_ob = object_hit.evaluated_get(dg)
tri_b = [eval_ob.data.vertices[i].co for i in tri_indices]
tri_b = matrix_transform(tri_b, eval_ob.matrix_world)
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)
nb_points = len(new_stroke.points)
new_stroke.points.foreach_set('co', new_local_co_3d.reshape(nb_points*3))
new_stroke.points.update()
context.tool_settings.use_keyframe_insert_auto = auto_key_status context.tool_settings.use_keyframe_insert_auto = auto_key_status

View File

@ -1,6 +1,11 @@
import bpy import bpy
from bpy.props import EnumProperty, IntProperty, FloatProperty, BoolProperty, PointerProperty
from bpy.types import PropertyGroup from bpy.types import PropertyGroup
from bpy.props import (EnumProperty,
IntProperty,
FloatProperty,
BoolProperty,
PointerProperty,
StringProperty)
class GP_PG_interpolate_settings(PropertyGroup): class GP_PG_interpolate_settings(PropertyGroup):
@ -9,11 +14,22 @@ class GP_PG_interpolate_settings(PropertyGroup):
# description="Do not change anything, just print the messages", # description="Do not change anything, just print the messages",
# default=False, options={'HIDDEN'}) # default=False, options={'HIDDEN'})
method : EnumProperty(
name='Method',
items= (
('BONE', 'Bone', 'Pick an armature bone and follow it', 0),
('GEOMETRY', 'Geometry', 'Directly follow underlying geometry', 1) ,
),
default='BONE',
description='Select method for interpolating strokes'
)
search_range : FloatProperty( search_range : FloatProperty(
name="Search Range", name="Search Range",
description="Search range size when points are out of mesh", description="Search range size when points are out of mesh",
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 ?
@ -32,11 +48,24 @@ class GP_PG_interpolate_settings(PropertyGroup):
min=1) min=1)
target_collection : PointerProperty( target_collection : PointerProperty(
name='Collection',
type=bpy.types.Collection, type=bpy.types.Collection,
description='Target collection to check armature keyframes from', description='Target collection to check armature keyframes from',
# placeholder='Collection' # placeholder='Collection'
) )
target_rig : PointerProperty(
name='Rig',
description='Rig to use as target',
type=bpy.types.Object)
# target_rig : StringProperty(
# name='Rig',
# description='Rig to use as target')
target_bone : StringProperty(
name='Bone',
description='Bone of the rig to follow when interpolating') # Bone
classes = ( classes = (
GP_PG_interpolate_settings, GP_PG_interpolate_settings,

25
ui.py
View File

@ -8,30 +8,43 @@ class GP_PT_interpolate(bpy.types.Panel):
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
return context.object and context.object.type == 'GPENCIL' return context.object # and context.object.type == 'GPENCIL'
def draw(self, context): def draw(self, context):
settings = bpy.context.scene.gp_interpo_settings settings = bpy.context.scene.gp_interpo_settings
layout = self.layout layout = self.layout
layout.use_property_split = True
col = layout.column(align=False) col = layout.column(align=False)
## interpolation buttons
if settings.mode == 'FRAME': if settings.mode == 'FRAME':
prev_icon, next_icon = 'FRAME_PREV', 'FRAME_NEXT' prev_icon, next_icon = 'FRAME_PREV', 'FRAME_NEXT'
else: else:
prev_icon, next_icon = 'PREV_KEYFRAME', 'NEXT_KEYFRAME' prev_icon, next_icon = 'PREV_KEYFRAME', 'NEXT_KEYFRAME'
row = col.row(align=True) row = col.row(align=True)
row.scale_x = 3 row.scale_x = 3
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
# col.separator() # col.separator()
col.prop(settings, 'target_collection', text='Collection')
col.prop(settings, 'search_range')
col = layout.column(align=True)
col.prop(settings, 'method', text='Method')
if settings.method == 'BONE':
col = layout.column(align=True)
col.prop_search(settings, 'target_rig', context.scene, 'objects', text='Rig')
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')
elif settings.method == 'GEOMETRY':
col.prop(settings, 'target_collection', text='Collection')
col.prop(settings, 'search_range')
col.separator()
col = layout.column(align=True)
row = col.row(align=True) row = col.row(align=True)
row.prop(settings, 'mode', expand=True) row.prop(settings, 'mode', expand=True)