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 = {
|
bl_info = {
|
||||||
"name": "gp interpolate",
|
"name": "gp interpolate",
|
||||||
"author": "Christophe Seux, Samuel Bernou",
|
"author": "Christophe Seux, Samuel Bernou",
|
||||||
"version": (0, 7, 4),
|
"version": (0, 8, 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",
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
from gp_interpolate.interpolate_strokes import (operators,
|
from gp_interpolate.interpolate_strokes import (operators,
|
||||||
properties,
|
properties,
|
||||||
debug,
|
debug,
|
||||||
|
bind_points,
|
||||||
#interpolate_simple
|
#interpolate_simple
|
||||||
)
|
)
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
properties,
|
properties,
|
||||||
operators,
|
operators,
|
||||||
debug
|
debug,
|
||||||
|
bind_points,
|
||||||
#interpolate_simple,
|
#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)
|
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg)
|
||||||
|
|
||||||
is_hit = 'HIT' if object_hit else ''
|
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[:]:
|
if object_hit: # and object_hit in col.all_objects[:]:
|
||||||
context.scene.cursor.location = hit_location
|
context.scene.cursor.location = hit_location
|
||||||
## Get location coplanar with triangle
|
## On location coplanar with face triangle
|
||||||
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
|
# hit_location = intersect_line_plane(origin, point_co_world, hit_location, 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 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
|
found = True
|
||||||
# print(f'{si}:{i} iteration {iteration}') # Dbg
|
# print(f'{si}:{i} iteration {iteration}') # Dbg
|
||||||
break
|
break
|
||||||
|
@ -196,7 +211,7 @@ class GP_OT_debug_geometry_interpolation_targets(bpy.types.Operator):
|
||||||
else:
|
else:
|
||||||
## Hit at original position
|
## Hit at original position
|
||||||
print(object_hit.name)
|
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')
|
print('Done')
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
|
@ -75,6 +75,12 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
||||||
self.frames_to_jump = None
|
self.frames_to_jump = None
|
||||||
self.cancelled = False
|
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)
|
context.window_manager.modal_handler_add(self)
|
||||||
self._timer = context.window_manager.event_timer_add(0.01, window=context.window)
|
self._timer = context.window_manager.event_timer_add(0.01, window=context.window)
|
||||||
context.area.header_text_set('Starting interpolation | Esc: Cancel')
|
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]})
|
# (frame: {self.frames_to_jump[self.loop_count]})
|
||||||
|
|
||||||
if self.status == 'START':
|
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
|
scn = bpy.context.scene
|
||||||
## Determine on what key/keys to jump
|
## Determine on what key/keys to jump
|
||||||
self.frames_to_jump = following_keys(forward=self.next, all_keys=self.settings.use_animation)
|
self.frames_to_jump = following_keys(forward=self.next, all_keys=self.settings.use_animation)
|
||||||
if not len(self.frames_to_jump):
|
if not len(self.frames_to_jump):
|
||||||
self.exit_modal(context, status='WARNING', text='No keyframe available in this direction')
|
self.exit_modal(context, status='WARNING', text='No keyframe available in this direction')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
# print('self.frames_to_jump: ', self.frames_to_jump)
|
|
||||||
|
|
||||||
self.gp = context.object
|
self.gp = context.object
|
||||||
|
|
||||||
# matrix = np.array(self.gp.matrix_world, dtype='float64')
|
# 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')
|
self.exit_modal(context, status='ERROR', text='No active frame')
|
||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
## Get strokes to interpolate
|
||||||
tgt_strokes = [s for s in self.gp.data.layers.active.active_frame.strokes if s.select]
|
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
|
## 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]
|
included_cols = [c.name for c in self.gp.users_collection]
|
||||||
target_obj = None
|
target_obj = None
|
||||||
|
|
||||||
|
## Setup depending on method
|
||||||
if self.settings.method == 'BONE':
|
if self.settings.method == 'BONE':
|
||||||
if not self.settings.target_rig or not self.settings.target_bone:
|
if not self.settings.target_rig or not self.settings.target_bone:
|
||||||
self.exit_modal(context, status='ERROR', text='No Bone selected')
|
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
|
target_obj = self.plane
|
||||||
|
|
||||||
elif self.settings.method == 'GEOMETRY':
|
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:
|
if col != context.scene.collection:
|
||||||
included_cols.append(col.name)
|
included_cols.append(col.name)
|
||||||
## Maybe include a plane just behind geo ? probably bad idea
|
## Maybe include a plane just behind geo ? probably bad idea
|
||||||
|
@ -257,6 +261,20 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
||||||
# Override collection
|
# Override collection
|
||||||
col = intercol
|
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()
|
dg = bpy.context.evaluated_depsgraph_get()
|
||||||
self.strokes_data = []
|
self.strokes_data = []
|
||||||
|
|
||||||
|
@ -271,6 +289,17 @@ 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):
|
||||||
point_co_world = world_co_3d[i]
|
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:
|
if target_obj:
|
||||||
object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg)
|
object_hit, hit_location, tri, tri_indices = obj_ray_cast(target_obj, Vector(point_co_world), origin, dg)
|
||||||
else:
|
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)
|
object_hit, hit_location, tri, tri_indices = ray_cast_point(square_co, origin, dg)
|
||||||
|
|
||||||
if object_hit:
|
if object_hit:
|
||||||
## Get location coplanar with triangle
|
## On location coplanar with face triangle
|
||||||
hit_location = intersect_line_plane(origin, point_co_world, tri[0], triangle_normal(*tri))
|
# 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
|
found = True
|
||||||
# print(f'{si}:{i} iteration {iteration}') # Dbg
|
# print(f'{si}:{i} iteration {iteration}') # Dbg
|
||||||
break
|
break
|
||||||
|
@ -318,6 +357,7 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
||||||
print(f'Scan time {self.scan_time:.4f}s')
|
print(f'Scan time {self.scan_time:.4f}s')
|
||||||
|
|
||||||
# Copy stroke selection
|
# Copy stroke selection
|
||||||
|
bpy.ops.gpencil.select_linked() # Ensure whole stroke are selected before copy
|
||||||
bpy.ops.gpencil.copy()
|
bpy.ops.gpencil.copy()
|
||||||
|
|
||||||
# Jump frame and paste
|
# Jump frame and paste
|
||||||
|
@ -348,13 +388,21 @@ class GP_OT_interpolate_stroke(bpy.types.Operator):
|
||||||
matrix_inv = np.array(self.gp.matrix_world.inverted(), dtype='float64')#.inverted()
|
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):]
|
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(new_strokes, self.strokes_data):
|
||||||
for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(self.strokes_data)):
|
for new_stroke, stroke_data in zip(reversed(new_strokes), reversed(self.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)
|
if not self.settings.method == 'TRI':
|
||||||
tri_b = [eval_ob.data.vertices[i].co for i in tri_indices]
|
eval_ob = object_hit.evaluated_get(dg)
|
||||||
tri_b = matrix_transform(tri_b, eval_ob.matrix_world)
|
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)
|
new_loc = barycentric_transform(hit_location, *tri_a, *tri_b)
|
||||||
# try:
|
# try:
|
||||||
|
|
|
@ -18,8 +18,9 @@ class GP_PG_interpolate_settings(PropertyGroup):
|
||||||
name='Method',
|
name='Method',
|
||||||
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),
|
('OBJECT', 'Object Geometry', 'Same as Geometry mode, but target only a specific object, even if occluded (ignore all the others)', 1),
|
||||||
('OBJECT', 'Object', 'Directly follow a specific object, even if occluded', 2),
|
('BONE', 'Bone', 'Pick an armature bone and follow it', 2),
|
||||||
|
('TRI', 'Triangle', 'Interpolate based on triangle traced manually over geometry', 3),
|
||||||
),
|
),
|
||||||
default='GEOMETRY',
|
default='GEOMETRY',
|
||||||
description='Select method for interpolating strokes'
|
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')
|
col.prop(settings, 'remove_occluded')
|
||||||
|
|
||||||
elif settings.method == 'OBJECT':
|
elif settings.method == 'OBJECT':
|
||||||
|
col.prop(settings, 'search_range')
|
||||||
col.prop(settings, 'target_object', text='Object')
|
col.prop(settings, 'target_object', text='Object')
|
||||||
|
|
||||||
col.separator()
|
col.separator()
|
||||||
|
@ -77,6 +78,17 @@ class GP_PT_interpolate(bpy.types.Panel):
|
||||||
row.operator('gp.debug_geometry_interpolation_targets', text='Debug points')
|
row.operator('gp.debug_geometry_interpolation_targets', text='Debug points')
|
||||||
row.operator('gp.debug_geometry_interpolation_targets', text='', icon='X').clear = True
|
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 = (
|
classes = (
|
||||||
GP_PT_interpolate,
|
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)
|
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):
|
def obj_ray_cast(obj, point, origin, depsgraph):
|
||||||
"""Wrapper for ray casting that moves the ray into object space"""
|
"""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
|
# Get hit location world_space
|
||||||
hit_location = obj.matrix_world @ location
|
hit_location = obj.matrix_world @ location
|
||||||
tri, tri_indices = get_tri_from_face(hit_location, face_index, obj, depsgraph)
|
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):
|
def empty_at(name='Empty', pos=(0,0,0), collection=None, type='PLAIN_AXES', size=1, show_name=False):
|
||||||
|
|
Loading…
Reference in New Issue