New triangle interpolation method and fixes
- Triangle mode: - Add bind modal to set points - interpolate based on user defined triangle bound to geometry - changed: in geometry mode, points out of geometry should follow more consistantly (needs more testing) - Better method names is UI - fix : error when strokes are not fully selectedmaster
parent
fca531bf35
commit
94f24ad5f6
|
@ -1,7 +1,7 @@
|
|||
bl_info = {
|
||||
"name": "gp interpolate",
|
||||
"author": "Christophe Seux, Samuel Bernou",
|
||||
"version": (0, 7, 4),
|
||||
"version": (0, 8, 0),
|
||||
"blender": (3, 6, 0),
|
||||
"location": "Sidebar > Gpencil Tab > Interpolate",
|
||||
"description": "Interpolate Grease pencil strokes over 3D",
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
from gp_interpolate.interpolate_strokes import (operators,
|
||||
properties,
|
||||
debug,
|
||||
bind_points,
|
||||
#interpolate_simple
|
||||
)
|
||||
|
||||
modules = (
|
||||
properties,
|
||||
operators,
|
||||
debug
|
||||
debug,
|
||||
bind_points,
|
||||
#interpolate_simple,
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,384 @@
|
|||
import bpy
|
||||
import numpy as np
|
||||
from time import perf_counter, time, sleep
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
from .. import utils
|
||||
|
||||
from mathutils.geometry import (barycentric_transform,
|
||||
intersect_point_tri,
|
||||
intersect_point_line,
|
||||
intersect_line_plane,
|
||||
tessellate_polygon)
|
||||
|
||||
import math
|
||||
import gpu
|
||||
from bpy_extras import view3d_utils
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
|
||||
|
||||
def raycast_objects(context, event, dg):
|
||||
"""
|
||||
Execute ray cast
|
||||
return object hit, hit world location, normal of hitted face and face index
|
||||
"""
|
||||
|
||||
region = context.region
|
||||
rv3d = context.region_data
|
||||
coord = event.mouse_region_x, event.mouse_region_y
|
||||
|
||||
view_vector = view3d_utils.region_2d_to_vector_3d(region, rv3d, coord)
|
||||
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, coord)
|
||||
ray_target = ray_origin + view_vector
|
||||
|
||||
def visible_objects_and_duplis():# -> Generator[tuple, Any, None]:
|
||||
"""Loop over (object, matrix) pairs (mesh only)"""
|
||||
|
||||
# depsgraph = dg ## TRY to used passed depsgraph
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
for dup in depsgraph.object_instances:
|
||||
if dup.is_instance: # Real dupli instance
|
||||
obj = dup.instance_object
|
||||
yield (obj, dup.matrix_world.copy())
|
||||
else: # Usual object
|
||||
obj = dup.object
|
||||
yield (obj, obj.matrix_world.copy())
|
||||
|
||||
def obj_ray_cast(obj, matrix):
|
||||
"""Wrapper for ray casting that moves the ray into object space"""
|
||||
|
||||
# get the ray relative to the object
|
||||
matrix_inv = matrix.inverted()
|
||||
ray_origin_obj = matrix_inv @ ray_origin
|
||||
ray_target_obj = matrix_inv @ ray_target
|
||||
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)
|
||||
|
||||
if success:
|
||||
return location, normal, face_index
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
# cast rays and find the closest object
|
||||
best_length_squared = -1.0
|
||||
best_obj = None
|
||||
face_normal = None
|
||||
hit_loc = None
|
||||
obj_face_index = None
|
||||
for obj, matrix in visible_objects_and_duplis():
|
||||
if obj.type == 'MESH':
|
||||
hit, normal, face_index = obj_ray_cast(obj, matrix)
|
||||
if hit is not None:
|
||||
hit_world = matrix @ hit
|
||||
# scene.cursor.location = hit_world
|
||||
length_squared = (hit_world - ray_origin).length_squared
|
||||
if best_obj is None or length_squared < best_length_squared:
|
||||
# print('length_squared',length_squared)
|
||||
best_length_squared = length_squared
|
||||
best_obj = obj
|
||||
face_normal = normal
|
||||
hit_loc = hit_world
|
||||
obj_face_index = face_index
|
||||
|
||||
if best_obj is not None:
|
||||
best_original = best_obj.original
|
||||
return best_original, hit_loc, face_normal, obj_face_index
|
||||
|
||||
return None, None, None, None
|
||||
|
||||
|
||||
# -----------------
|
||||
### Drawing
|
||||
# -----------------
|
||||
|
||||
def circle_2d(coord, r, num_segments):
|
||||
'''create circle, ref: http://slabode.exofire.net/circle_draw.shtml'''
|
||||
cx, cy = coord
|
||||
points = []
|
||||
theta = 2 * 3.1415926 / num_segments
|
||||
c = math.cos(theta) #precalculate the sine and cosine
|
||||
s = math.sin(theta)
|
||||
x = r # we start at angle = 0
|
||||
y = 0
|
||||
for i in range(num_segments):
|
||||
#bgl.glVertex2f(x + cx, y + cy) # output vertex
|
||||
points.append((x + cx, y + cy))
|
||||
# apply the rotation matrix
|
||||
t = x
|
||||
x = c * x - s * y
|
||||
y = s * t + c * y
|
||||
|
||||
return points
|
||||
|
||||
def draw_callback_px(self, context):
|
||||
if context.area != self.current_area:
|
||||
return
|
||||
|
||||
# 50% alpha, 2 pixel width line
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
gpu.state.blend_set('ALPHA')
|
||||
gpu.state.line_width_set(2.0)
|
||||
|
||||
view_rot = context.region_data.view_rotation
|
||||
|
||||
if self.current_points:
|
||||
for coord in self.current_points:
|
||||
# circle3d = [(view_normal.to_track_quat('-Z', 'Z') @ cp) + coord for cp in self.circle_pts]
|
||||
circle3d = [(view_rot @ cp) + coord for cp in self.mini_circle_pts]
|
||||
circle3d.append(circle3d[0]) # Loop with last point
|
||||
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": circle3d})
|
||||
shader.bind()
|
||||
shader.uniform_float("color", (0.8, 0.8, 0.0, 0.9))
|
||||
batch.draw(shader)
|
||||
|
||||
# Draw clicked path
|
||||
if not self.point_list:
|
||||
return
|
||||
|
||||
positions = [pt['co'] for pt in self.point_list]
|
||||
|
||||
line_color = color = (0.9, 0.1, 0.2, 0.8)
|
||||
|
||||
## Draw lines
|
||||
|
||||
if len(self.point_list) >= 3:
|
||||
## Duplicate first position at last to loop triangle
|
||||
positions += [self.point_list[0]['co']]
|
||||
line_color = (0.9, 0.1, 0.3, 1.0)
|
||||
|
||||
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": positions})
|
||||
shader.bind()
|
||||
shader.uniform_float("color", line_color)
|
||||
batch.draw(shader)
|
||||
|
||||
# Draw circle on point aligned with view
|
||||
|
||||
# view_normal = context.region_data.view_matrix.inverted() @ Vector((0,0,1))
|
||||
|
||||
for pt in self.point_list:
|
||||
coord = pt['co']
|
||||
|
||||
# circle3d = [(view_normal.to_track_quat('-Z', 'Z') @ cp) + coord for cp in self.circle_pts]
|
||||
circle3d = [(view_rot @ cp) + coord for cp in self.circle_pts]
|
||||
circle3d.append(circle3d[0]) # Loop with last point
|
||||
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": circle3d})
|
||||
shader.bind()
|
||||
shader.uniform_float("color", color)
|
||||
batch.draw(shader)
|
||||
|
||||
# restore opengl defaults
|
||||
gpu.state.line_width_set(1.0)
|
||||
gpu.state.blend_set('NONE')
|
||||
|
||||
## POST_PIXEL for text
|
||||
|
||||
|
||||
# -----------------
|
||||
### Operator
|
||||
# -----------------
|
||||
|
||||
class GP_OT_bind_points(bpy.types.Operator):
|
||||
bl_idname = "gp.bind_points"
|
||||
bl_label = "Bind Points"
|
||||
bl_description = 'Bind points to use as reference for interpolation'
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object and context.object.type == 'GPENCIL'
|
||||
|
||||
clear : bpy.props.BoolProperty(name='Clear', default=False, options={'SKIP_SAVE'})
|
||||
|
||||
def invoke(self, context, event):
|
||||
# print('INVOKE')
|
||||
self.debug = False
|
||||
self.gp = context.object
|
||||
self.settings = context.scene.gp_interpo_settings
|
||||
self.point_list = []
|
||||
self.current_points = []
|
||||
wm = context.window_manager
|
||||
if tri_dump := wm.get(f'tri_{self.gp.name}'):
|
||||
if self.clear:
|
||||
del wm[f'tri_{self.gp.name}']
|
||||
return {'FINISHED'}
|
||||
|
||||
## load from current frame
|
||||
## Dict to list -> cast to dict (get ID prop array)
|
||||
self.point_list = [dict(tri_dump[str(i)]) for i in range(3)]
|
||||
|
||||
## Update world coordinate position at current frame
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
for point in self.point_list:
|
||||
ob = bpy.context.scene.objects.get(point['object'])
|
||||
ob_eval = ob.evaluated_get(dg)
|
||||
point['co'] = ob_eval.matrix_world @ ob_eval.data.vertices[point['index']].co
|
||||
|
||||
if self.clear:
|
||||
return {'FINISHED'}
|
||||
|
||||
## Prepare circle 3D coordinate (create in invoke)
|
||||
self.circle_pts = [Vector((p[0], p[1], 0)) for p in circle_2d((0,0), 0.01, 12)]
|
||||
self.mini_circle_pts = [Vector((p[0], p[1], 0)) for p in circle_2d((0,0), 0.005, 12)]
|
||||
|
||||
# self._timer = wm.event_timer_add(0.01, window=context.window)
|
||||
|
||||
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
|
||||
self.current_area = context.area
|
||||
args = (self, context)
|
||||
self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_VIEW')
|
||||
context.area.header_text_set('Bind points | Enter: Valid | Backspace: remove last point | Esc / Right-Click: Cancel')
|
||||
wm.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def exit_modal(self, context, status='INFO', text=None):
|
||||
# print('Exit modal') # Dbg
|
||||
## Reset all drawing and report
|
||||
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
|
||||
context.area.header_text_set(None)
|
||||
context.area.tag_redraw()
|
||||
if text:
|
||||
self.report({status}, text)
|
||||
else:
|
||||
## report standard info
|
||||
self.report({'INFO'}, 'Done')
|
||||
|
||||
def get_closest_vert(self, object_hit, hit_location, _normal, face_index, dg):
|
||||
|
||||
ob_eval = object_hit.evaluated_get(dg)
|
||||
## Get closest index on face and store
|
||||
face = ob_eval.data.polygons[face_index]
|
||||
|
||||
## list(dict)
|
||||
vertices_infos = [{
|
||||
'object': object_hit.name, # store object 'name'
|
||||
'index': vert_idx, # store vertex index
|
||||
'co': ob_eval.matrix_world @ ob_eval.data.vertices[vert_idx].co # store initial absolute coordinate
|
||||
}
|
||||
# vertex: ob_eval.data.vertices[vert_idx],
|
||||
for vert_idx in face.vertices]
|
||||
|
||||
## Filter vertices by closest to hit_location
|
||||
vertices_infos.sort(key=lambda x: (x['co'] - hit_location).length)
|
||||
return vertices_infos[0]
|
||||
|
||||
def bind(self, context):
|
||||
## store points on scene/wm properties associated with GP object
|
||||
context.window_manager[f'tri_{context.object.name}'] = {str(i) : d for i, d in enumerate(self.point_list)}
|
||||
self.exit_modal(context, text='Bound!')
|
||||
|
||||
def modal(self, context, event):
|
||||
context.area.tag_redraw()
|
||||
if event.type in ('RIGHTMOUSE', 'ESC'):
|
||||
context.area.header_text_set(f'Cancelling')
|
||||
self.exit_modal(context, text='Cancelled')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if event.type in ('WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'MIDDLEMOUSE'):
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
## disable hint if too intensive
|
||||
if event.type in {'MOUSEMOVE'}:
|
||||
## permanent update on closest point position (too heavy to always compute ?)
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
object_hit, hit_location, _normal, face_index = raycast_objects(context, event, bpy.context.evaluated_depsgraph_get())
|
||||
if object_hit is None:
|
||||
self.current_points = []
|
||||
else:
|
||||
pt = self.get_closest_vert(object_hit, hit_location, _normal, face_index, dg)
|
||||
self.current_points = [pt['co']]
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
elif event.type in ('BACK_SPACE', 'DEL') and event.value == 'PRESS':
|
||||
if self.point_list:
|
||||
self.report({'INFO'}, 'Removed last point')
|
||||
self.point_list.pop()
|
||||
|
||||
|
||||
elif event.type in ('RET', 'SPACE') and event.value == 'PRESS':
|
||||
## Valid
|
||||
if len(self.point_list) < 3:
|
||||
self.exit_modal(context, status='ERROR', text='Not enough point selected, Cancelling')
|
||||
return {'CANCELLED'}
|
||||
else:
|
||||
self.bind(context)
|
||||
return {'FINISHED'}
|
||||
|
||||
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
|
||||
if len(self.point_list) >= 3:
|
||||
# self.report({'WARNING'}, 'Already got 3 point')
|
||||
self.bind(context)
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
## Raycast surface and store point
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
|
||||
## Basic rayvast (Do not consider object modifier or instance !)
|
||||
# mouse = event.mouse_region_x, event.mouse_region_y
|
||||
# view_mat = context.region_data.view_matrix.inverted()
|
||||
# origin = view_mat.to_translation()
|
||||
# depth3d = view_mat @ Vector((0, 0, -1))
|
||||
# point = utils.region_to_location(mouse, depth3d)
|
||||
# ray = (point - origin)
|
||||
# hit, hit_location, normal, face_index, object_hit, matrix = bpy.context.scene.ray_cast(dg, origin, ray)
|
||||
|
||||
object_hit, hit_location, _normal, face_index = raycast_objects(context, event, dg)
|
||||
|
||||
## Also use triangle coordinate in triangle ?! using raycast with tesselated triangle infos
|
||||
# object_hit, hit_location, tri, tri_indices = ray_cast_point(point, origin, dg)
|
||||
|
||||
if object_hit is None:
|
||||
self.report({'WARNING'}, 'Nothing hit, Retry on a surface')
|
||||
else:
|
||||
# print('object_hit: ', object_hit, object_hit.is_evaluated)
|
||||
# print('hit_location: ', hit_location)
|
||||
# print('face_index: ', face_index)
|
||||
|
||||
# context.scene.cursor.location = hit_location # Dbg
|
||||
|
||||
### // get vert on-place
|
||||
# ob_eval = object_hit.evaluated_get(dg)
|
||||
# print('ob_eval: ', ob_eval)
|
||||
# ## Get closest index on face and store
|
||||
# face = ob_eval.data.polygons[face_index]
|
||||
# ## Store list of tuples [(index, world_co, object_hit), ...]
|
||||
# vertices_infos = [(vert_idx,
|
||||
# ob_eval.matrix_world @ ob_eval.data.vertices[vert_idx].co,
|
||||
# object_hit) # Store original object.
|
||||
# for vert_idx in face.vertices]
|
||||
|
||||
# ## Filter vedrtices by closest to hit_location
|
||||
# vertices_infos.sort(key=lambda x: (x['co'] - hit_location).length)
|
||||
|
||||
# vert = vertices_infos[0]
|
||||
### get vert on-place //
|
||||
|
||||
vert = self.get_closest_vert(object_hit, hit_location, _normal, face_index, dg)
|
||||
|
||||
# print('vert: ', vert)
|
||||
|
||||
# if self.point_list and [x for x in self.point_list if vert[0] == x[0] and vert[3] == x[3]]:
|
||||
if any(vert['index'] == x['index'] and vert['object'] == x['object'] for x in self.point_list):
|
||||
self.report({'WARNING'}, "Cannot use same point twice !")
|
||||
else:
|
||||
self.point_list += [vert]
|
||||
self.report({'INFO'}, f"Set point {len(self.point_list)}")
|
||||
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def execute(self, context):
|
||||
return {"FINISHED"}
|
||||
|
||||
classes = (
|
||||
GP_OT_bind_points,
|
||||
)
|
||||
|
||||
def register():
|
||||
for c in classes:
|
||||
bpy.utils.register_class(c)
|
||||
|
||||
|
||||
def unregister():
|
||||
for c in reversed(classes):
|
||||
bpy.utils.unregister_class(c)
|
|
@ -174,13 +174,28 @@ class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator):
|
|||
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg)
|
||||
|
||||
is_hit = 'HIT' if object_hit else ''
|
||||
empty_at(name=f'{is_hit}search_{iteration}({ct})', pos=square_co, collection=debug_col, size=0.001, show_name=True) # type='SPHERE'
|
||||
## Show all search square elements
|
||||
# empty_at(name=f'{is_hit}{i}search_{iteration}({ct})', pos=square_co, collection=debug_col, size=0.001, show_name=False) # type='SPHERE'
|
||||
|
||||
## Show only hit location (at hit location)
|
||||
if is_hit:
|
||||
empty_at(name=f'{is_hit}{i}search_{iteration}({ct})', pos=hit_location, collection=debug_col, size=0.001, show_name=False) # type='SPHERE'
|
||||
|
||||
if object_hit: # and object_hit in col.all_objects[:]:
|
||||
context.scene.cursor.location = hit_location
|
||||
## Get location coplanar with triangle
|
||||
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
|
||||
empty_at(name=f'{object_hit.name}_{i}-{iteration}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=True)
|
||||
## On location coplanar with face triangle
|
||||
# hit_location = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
|
||||
|
||||
## On view plane
|
||||
view_vec = context.scene.camera.matrix_world.to_quaternion() @ Vector((0,0,1))
|
||||
hit_location = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
|
||||
|
||||
## An average of the two ?
|
||||
# hit_location_1 = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
|
||||
# hit_location_2 = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
|
||||
# hit_location = (hit_location_1 + hit_location_2) / 2
|
||||
|
||||
empty_at(name=f'{object_hit.name}_{i}-{iteration}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=False)
|
||||
found = True
|
||||
# print(f'{si}:{i} iteration {iteration}') # Dbg
|
||||
break
|
||||
|
@ -196,7 +211,7 @@ class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator):
|
|||
else:
|
||||
## Hit at original position
|
||||
print(object_hit.name)
|
||||
empty_at(name=f'{object_hit.name}_{i}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=True)
|
||||
empty_at(name=f'{object_hit.name}_{i}', pos=hit_location, collection=debug_col, type='SPHERE', size=0.001, show_name=False)
|
||||
|
||||
print('Done')
|
||||
return {'FINISHED'}
|
||||
|
|
|
@ -75,6 +75,12 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
self.frames_to_jump = None
|
||||
self.cancelled = False
|
||||
|
||||
## Remove interpolation_plane collection ! (unseen, but can be hit)
|
||||
if interp_plane := bpy.data.objects.get('interpolation_plane'):
|
||||
bpy.data.objects.remove(interp_plane)
|
||||
if interp_col := bpy.data.collections.get('interpolation_tool'):
|
||||
bpy.data.collections.remove(interp_col)
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
self._timer = context.window_manager.event_timer_add(0.01, window=context.window)
|
||||
context.area.header_text_set('Starting interpolation | Esc: Cancel')
|
||||
|
@ -127,14 +133,16 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
# (frame: {self.frames_to_jump[self.loop_count]})
|
||||
|
||||
if self.status == 'START':
|
||||
if self.settings.method == 'TRI' and not context.window_manager.get(f'tri_{self.gp.name}'):
|
||||
self.exit_modal(context, status='ERROR', text='Need to bind coordinate first. Use "Bind Tri Point" button')
|
||||
return {'CANCELLED'}
|
||||
|
||||
scn = bpy.context.scene
|
||||
## Determine on what key/keys to jump
|
||||
self.frames_to_jump = following_keys(forward=self.next, all_keys=self.settings.use_animation)
|
||||
if not len(self.frames_to_jump):
|
||||
self.exit_modal(context, status='WARNING', text='No keyframe available in this direction')
|
||||
return {'CANCELLED'}
|
||||
# print('self.frames_to_jump: ', self.frames_to_jump)
|
||||
|
||||
self.gp = context.object
|
||||
|
||||
# matrix = np.array(self.gp.matrix_world, dtype='float64')
|
||||
|
@ -154,6 +162,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
self.exit_modal(context, status='ERROR', text='No active frame')
|
||||
return {'CANCELLED'}
|
||||
|
||||
## Get strokes to interpolate
|
||||
tgt_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select]
|
||||
|
||||
## If nothing selected in sculpt/paint, Select all before triggering
|
||||
|
@ -169,6 +178,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
included_cols = [c.name for c in self.gp.users_collection]
|
||||
target_obj = None
|
||||
|
||||
## Setup depending on method
|
||||
if self.settings.method == 'BONE':
|
||||
if not self.settings.target_rig or not self.settings.target_bone:
|
||||
self.exit_modal(context, status='ERROR', text='No Bone selected')
|
||||
|
@ -197,12 +207,6 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
target_obj = self.plane
|
||||
|
||||
elif self.settings.method == 'GEOMETRY':
|
||||
## Remove interpolation_plane collection ! (unseen, but can be hit)
|
||||
if interp_plane := bpy.data.objects.get('interpolation_plane'):
|
||||
bpy.data.objects.remove(interp_plane)
|
||||
if interp_col := bpy.data.collections.get('interpolation_tool'):
|
||||
bpy.data.collections.remove(interp_col)
|
||||
|
||||
if col != context.scene.collection:
|
||||
included_cols.append(col.name)
|
||||
## Maybe include a plane just behind geo ? probably bad idea
|
||||
|
@ -257,6 +261,20 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
# Override collection
|
||||
col = intercol
|
||||
|
||||
if self.settings.method == 'TRI':
|
||||
point_dict = context.window_manager.get(f'tri_{self.gp.name}')
|
||||
## point_dict -> {'0': {'object': object_name_as_str, 'index': 450}, ...}
|
||||
|
||||
## Get triangle dumped in context.window_manager
|
||||
self.object_hit_source = object_hit = [bpy.context.scene.objects.get(point_dict[str(i)]['object']) for i in range(3)]
|
||||
self.tri_indices_source = tri_indices = [point_dict[str(i)]['index'] for i in range(3)] # List of vertices index corresponding to tri coordinates
|
||||
|
||||
tri = []
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
for source_obj, idx in zip(object_hit, tri_indices):
|
||||
ob_eval = source_obj.evaluated_get(dg)
|
||||
tri.append(ob_eval.matrix_world @ ob_eval.data.vertices[idx].co)
|
||||
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
self.strokes_data = []
|
||||
|
||||
|
@ -271,6 +289,17 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
stroke_data = []
|
||||
for i, point in enumerate(stroke.points):
|
||||
point_co_world = world_co_3d[i]
|
||||
|
||||
if self.settings.method == 'TRI':
|
||||
## Set hit location at same coordinate as point
|
||||
# hit_location = point_co_world
|
||||
## Set hit location on tri plane
|
||||
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
|
||||
|
||||
# In this case object_hit is a list of object !
|
||||
stroke_data.append((stroke, point_co_world, object_hit, hit_location, tri, tri_indices))
|
||||
continue
|
||||
|
||||
if target_obj:
|
||||
object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg)
|
||||
else:
|
||||
|
@ -290,8 +319,18 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg)
|
||||
|
||||
if object_hit:
|
||||
## Get location coplanar with triangle
|
||||
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
|
||||
## On location coplanar with face triangle
|
||||
# hit_location = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
|
||||
|
||||
## On view plane
|
||||
view_vec = context.scene.camera.matrix_world.to_quaternion() @ Vector((0,0,1))
|
||||
hit_location = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
|
||||
|
||||
## An average of the two ?
|
||||
# hit_location_1 = intersect_line_plane(origin, point_co_world, hit_location, triangle_normal(*tri))
|
||||
# hit_location_2 = intersect_line_plane(origin, point_co_world, hit_location, view_vec)
|
||||
# hit_location = (hit_location_1 + hit_location_2) / 2
|
||||
|
||||
found = True
|
||||
# print(f'{si}:{i} iteration {iteration}') # Dbg
|
||||
break
|
||||
|
@ -318,6 +357,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
print(f'Scan time {self.scan_time:.4f}s')
|
||||
|
||||
# Copy stroke selection
|
||||
bpy.ops.gpencil.select_linked() # Ensure whole stroke are selected before copy
|
||||
bpy.ops.gpencil.copy()
|
||||
|
||||
# Jump frame and paste
|
||||
|
@ -343,18 +383,26 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
|||
arm=self.settings.target_rig,
|
||||
set_rotation=self.settings.use_bone_rotation,
|
||||
mesh=True)
|
||||
|
||||
|
||||
dg = bpy.context.evaluated_depsgraph_get()
|
||||
matrix_inv = np.array(self.gp.matrix_world.inverted(), dtype='float64')#.inverted()
|
||||
new_strokes = self.gp.data.layers.active.active_frame.strokes[-len(self.strokes_data):]
|
||||
|
||||
if self.settings.method == 'TRI':
|
||||
## All points have the same new frame triangle (tri_b)
|
||||
tri_b = []
|
||||
for source_obj, idx in zip(self.object_hit_source, self.tri_indices_source):
|
||||
ob_eval = source_obj.evaluated_get(dg)
|
||||
tri_b.append(ob_eval.matrix_world @ ob_eval.data.vertices[idx].co)
|
||||
|
||||
# for new_stroke, stroke_data in zip(new_strokes, self.strokes_data):
|
||||
for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(self.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)
|
||||
tri_b = [eval_ob.data.vertices[i].co for i in tri_indices]
|
||||
tri_b = matrix_transform(tri_b, eval_ob.matrix_world)
|
||||
if not self.settings.method == 'TRI':
|
||||
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)
|
||||
# try:
|
||||
|
|
|
@ -18,8 +18,9 @@ class GP_PG_interpolate_settings(PropertyGroup):
|
|||
name='Method',
|
||||
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),
|
||||
('OBJECT', 'Object Geometry', 'Same as Geometry mode, but target only a specific object, even if occluded (ignore all the others)', 1),
|
||||
('BONE', 'Bone', 'Pick an armature bone and follow it', 2),
|
||||
('TRI', 'Triangle', 'Interpolate based on triangle traced manually over geometry', 3),
|
||||
),
|
||||
default='GEOMETRY',
|
||||
description='Select method for interpolating strokes'
|
||||
|
|
12
ui.py
12
ui.py
|
@ -53,6 +53,7 @@ class GP_PT_interpolate(bpy.types.Panel):
|
|||
col.prop(settings, 'remove_occluded')
|
||||
|
||||
elif settings.method == 'OBJECT':
|
||||
col.prop(settings, 'search_range')
|
||||
col.prop(settings, 'target_object', text='Object')
|
||||
|
||||
col.separator()
|
||||
|
@ -76,6 +77,17 @@ class GP_PT_interpolate(bpy.types.Panel):
|
|||
row = col.row(align=True)
|
||||
row.operator('gp.debug_geometry_interpolation_targets', text='Debug points')
|
||||
row.operator('gp.debug_geometry_interpolation_targets', text='', icon='X').clear = True
|
||||
|
||||
if context.scene.gp_interpo_settings.method == 'TRI':
|
||||
col.separator()
|
||||
row=layout.row(align=True)
|
||||
wm = bpy.context.window_manager
|
||||
binded = context.object and context.object.type == 'GPENCIL' and wm.get(f'tri_{context.object.name}')
|
||||
txt = 'Show Tri Points' if binded else 'Bind Tri Points'
|
||||
row.operator('gp.bind_points', text=txt, icon='MESH_DATA')
|
||||
if binded:
|
||||
row.operator('gp.bind_points', text='', icon='TRASH').clear = True
|
||||
|
||||
|
||||
classes = (
|
||||
GP_PT_interpolate,
|
||||
|
|
4
utils.py
4
utils.py
|
@ -124,7 +124,7 @@ def ray_cast_point(point, origin, depsgraph):
|
|||
|
||||
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, hit_location, tri, tri_indices
|
||||
|
||||
def obj_ray_cast(obj, point, origin, depsgraph):
|
||||
"""Wrapper for ray casting that moves the ray into object space"""
|
||||
|
@ -142,7 +142,7 @@ def obj_ray_cast(obj, point, origin, depsgraph):
|
|||
# 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
|
||||
return obj, hit_location, tri, tri_indices
|
||||
|
||||
|
||||
def empty_at(name='Empty', pos=(0,0,0), collection=None, type='PLAIN_AXES', size=1, show_name=False):
|
||||
|
|
Loading…
Reference in New Issue