325 lines
10 KiB
Python
325 lines
10 KiB
Python
import bpy
|
|
import re
|
|
from mathutils import Vector
|
|
from . import fn
|
|
|
|
|
|
def raycast_from_loc_to_obj(src, tgt, direction=None, dg=None):
|
|
''' Raycast from a world space source on a target ovject using a direction Vector
|
|
|
|
:origin: a world coordinate location as source of the ray
|
|
:tgt: an object onto project the ray
|
|
:direction: a direction vector. If not passed, point bottom : Vector((0,0,-1))
|
|
return world coordiante of the hit if any
|
|
'''
|
|
|
|
if direction is None:
|
|
direction = Vector((0,0,-1))
|
|
|
|
mw = tgt.matrix_world
|
|
origin = mw.inverted() @ src
|
|
|
|
hit, loc, _norm, _face = tgt.ray_cast(origin, direction, depsgraph=dg)
|
|
|
|
if hit:
|
|
# print("Hit at ", loc, " (local)")
|
|
world_loc = mw @ loc
|
|
# bpy.ops.object.empty_add(location = world_loc) # test
|
|
return world_loc
|
|
|
|
return False
|
|
|
|
def worldspace_move_posebone(b, vec, is_target=False):
|
|
''' Move or snap a posebone using a vector in worldspace
|
|
:b: posebone
|
|
:vec: Vector to move posebone worldspace (if not attached to parent)
|
|
:is_target: if True the posebone snap to the vector, else the vector is added
|
|
'''
|
|
|
|
a = b.id_data
|
|
|
|
if is_target:
|
|
target_vec = vec
|
|
else:
|
|
target_vec = (a.matrix_world @ b.matrix).translation + vec
|
|
|
|
mw = a.convert_space(pose_bone=b,
|
|
matrix=b.matrix,
|
|
from_space='POSE',
|
|
to_space='WORLD')
|
|
|
|
mw.translation = target_vec
|
|
|
|
b.matrix = a.convert_space(pose_bone=b,
|
|
matrix=mw,
|
|
from_space='WORLD',
|
|
to_space='POSE')
|
|
return target_vec
|
|
|
|
def set_subsurf_viewport(ob, show=True):
|
|
for m in ob.modifiers:
|
|
# if m.type in ('SUBSURF', 'TRIANGULATE'):
|
|
if m.type == 'SUBSURF':
|
|
m.show_viewport = show
|
|
|
|
def snap_foot(pb, gnd):
|
|
'''Get posebone and ground to touch'''
|
|
|
|
# arm = bpy.context.object# bpy.context.scene.objects.get('Armature')
|
|
arm = pb.id_data
|
|
print('arm: ', arm)
|
|
|
|
# find tip bone :
|
|
# tip = [p for p in pb.children_recursive if p.name.startswith('DEF')][-1]
|
|
def_name = f'DEF.{pb.name}'
|
|
|
|
tip = arm.pose.bones.get(def_name)
|
|
|
|
## guess fallbacks
|
|
if not tip:
|
|
## Regex for potential separator variance
|
|
# "." dot separator is not literal anymore : DEF.foot\.L
|
|
parttern = f'DEF.{re.escape(pb.name)}'
|
|
for pb in arm.pose.bones:
|
|
if re.search(parttern, pb.name):
|
|
tip = pb
|
|
break
|
|
if not tip:
|
|
## name proximity
|
|
best_ratio = 0
|
|
for pb in arm.pose.bones:
|
|
if (ratio := fn.fuzzy_match_ratio(pb.name, def_name)) > best_ratio:
|
|
tip = pb # assign tip
|
|
best_ratio = ratio
|
|
if best_ratio < 0.85:
|
|
# no bones name is close enough
|
|
return ('ERROR', f'no bone name is close enough of "{def_name}" (best was "{tip}" with close-ratio {best_ratio})')
|
|
|
|
print('tip: ', tip)
|
|
# get deformed object VG (find skinned mesh)
|
|
|
|
ob = None
|
|
for o in arm.users_collection[0].all_objects:
|
|
if o.type != 'MESH':
|
|
continue
|
|
if not 'body' in o.name: # might be not needed if filter on vertex group
|
|
continue
|
|
if not o.vertex_groups.get(tip.name):
|
|
# has vertex_group pointing to the bone
|
|
continue
|
|
|
|
for m in o.modifiers:
|
|
if m.type == 'ARMATURE':
|
|
# print(o.name, m.object)
|
|
if m.object == arm: # if point to orignal rig
|
|
ob = o
|
|
break
|
|
|
|
## find object with old proxy method
|
|
# for o in arm.proxy_collection.instance_collection.all_objects:
|
|
# if o.type != 'MESH':
|
|
# continue
|
|
# for m in o.modifiers:
|
|
# if m.type == 'ARMATURE':
|
|
# # print(o.name, m.object)
|
|
# if m.object == arm.proxy: # if point to orignal rig
|
|
# ## here we want body, not body_deform
|
|
# if not 'body' in o.name:
|
|
# continue
|
|
# if '_deform' in o.name:
|
|
# continue
|
|
# ob = o
|
|
# break
|
|
|
|
if not ob:
|
|
return ('ERROR', 'no skinned mesh found')
|
|
|
|
print('check skinning of', ob.name)
|
|
### MESH baking
|
|
#-# Get Vertices position for a specific vertex group if over weight limit
|
|
|
|
# me0 = simple_to_mesh(ob) # if no need to apply modifier just make ob.data.copy()
|
|
# # generate new
|
|
# bm =
|
|
# bm.from_mesh(me0)
|
|
# bm.verts.ensure_lookup_table()
|
|
# bm.edges.ensure_lookup_table()
|
|
# bm.faces.ensure_lookup_table()
|
|
|
|
# # store weight values
|
|
# weight = []
|
|
# ob_tmp = bpy.data.objects.new("temp", me0)
|
|
# for g in ob.vertex_groups:
|
|
# ob_tmp.vertex_groups.new(name=g.name)
|
|
# for v in me0.vertices:
|
|
# try:
|
|
# weight.append(ob_tmp.vertex_groups[tip.name].weight(v.index))
|
|
# except:
|
|
# weight.append(0)
|
|
|
|
# verts = [vert for vid, vert in enumerate(bake_mesh.vertices) \
|
|
# if ob_tmp.vertex_groups[tip.name].index in [i.group for i in vert.groups] \
|
|
# and weight[vid] > 0.5]
|
|
|
|
#-# /
|
|
|
|
#-# Get Vertices position for a specific vertex group if over weight limit
|
|
#-# (Does not work if a subdivision modifier is on)
|
|
|
|
set_subsurf_viewport(ob, show=False)
|
|
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
obeval = ob.evaluated_get(dg) #.copy()
|
|
print('object: ', ob.name)
|
|
|
|
## bpy.context.object.proxy_collection.instance_collection.all_objects['body_deform']
|
|
## bpy.context.object.proxy_collection.instance_collection.all_objects['body']
|
|
|
|
## Hide modifier
|
|
# for m in obeval.modifiers:
|
|
# if m.type == 'SUBSURF':
|
|
# m.show_viewport = False # m.levels = 0
|
|
|
|
bake_mesh = obeval.to_mesh(preserve_all_data_layers=True, depsgraph=dg)
|
|
|
|
ct = 0
|
|
vg = obeval.vertex_groups[tip.name]
|
|
world_co = []
|
|
for idx, vert in enumerate(bake_mesh.vertices):
|
|
print('idx, vert: ', idx, vert)
|
|
grp_indexes = [i.group for i in vert.groups]
|
|
print('grp_indexes: ', grp_indexes)
|
|
print('vg.index in grp_indexes: ', vg.index in grp_indexes)
|
|
if vg.index in grp_indexes:
|
|
print(':: vg.weight(idx) > 0.5: ', vg.weight(idx) > 0.5)
|
|
# print()
|
|
if vg.index in grp_indexes and vg.weight(idx) > 0.5:
|
|
ct +=1
|
|
world_co.append(ob.matrix_world @ vert.co)
|
|
|
|
if not ct:
|
|
set_subsurf_viewport(ob)
|
|
obeval.to_mesh_clear()
|
|
return ('ERROR', 'No vertices found')
|
|
|
|
#-# # list comprehension
|
|
# verts = [vert for vid, vert in enumerate(bake_mesh.vertices) \
|
|
# if obeval.vertex_groups[tip.name].index in [i.group for i in vert.groups] \
|
|
# and obeval.vertex_groups[tip.name].weight(vid) > 0.5]
|
|
#world_co = [ob.matrix_world @ v.co for v in verts]
|
|
#-# /
|
|
|
|
|
|
print(len(world_co), 'vertices')
|
|
|
|
# sort by height
|
|
world_co.sort(key=lambda x: x[2])
|
|
|
|
### Raycast and find lowest distance
|
|
up_check = True
|
|
updists = []
|
|
dists = []
|
|
|
|
for co in world_co: # [:6] (no neede to get all)
|
|
contact = raycast_from_loc_to_obj(co, gnd, Vector((0,0,-1)), dg=dg)
|
|
if contact:
|
|
dists.append((co - contact).length)
|
|
|
|
if not contact and up_check:
|
|
contact = raycast_from_loc_to_obj(co, gnd, Vector((0,0,1)), dg=dg)
|
|
if contact:
|
|
updists.append((co - contact).length)
|
|
|
|
if not contact:
|
|
continue
|
|
# empty_at(contact, size=0.2)
|
|
|
|
if not dists and not updists:
|
|
set_subsurf_viewport(ob)
|
|
obeval.to_mesh_clear()
|
|
return ('ERROR', 'raycast could not found contact')
|
|
|
|
|
|
# move bones by the minimal amount.
|
|
if updists:
|
|
move = max(updists)
|
|
vec = Vector((0,0, move))
|
|
worldspace_move_posebone(pb, vec)
|
|
print('INFO', f'move up by {move}')
|
|
|
|
else:
|
|
move = min(dists)
|
|
vec = Vector((0,0, -move))
|
|
worldspace_move_posebone(pb, vec)
|
|
print('INFO', f'move down by {move}')
|
|
|
|
## restore
|
|
set_subsurf_viewport(ob)
|
|
obeval.to_mesh_clear()
|
|
|
|
|
|
def snap_feet():
|
|
|
|
## add undo push if launched from shelf (TODO need test !!!)
|
|
# bpy.ops.ed.undo_push(message='Snap to ground')
|
|
|
|
# if bpy.context.object.type != 'ARMATURE':
|
|
# print('ERROR', 'Selection is not an armature')
|
|
# return
|
|
|
|
if bpy.context.mode != 'POSE':
|
|
print('ERROR', 'Not in pose mode')
|
|
return
|
|
|
|
|
|
gnd = bpy.context.scene.anim_cycle_settings.gnd
|
|
print('ground: ', gnd.name)
|
|
if not gnd:
|
|
return ('ERROR', 'Need to point ground object in "ground" field')
|
|
# Snap all selected feets, posebone to ground
|
|
|
|
for pb in bpy.context.selected_pose_bones:
|
|
## find the foot bones.
|
|
if 'foot' in pb.name:
|
|
# get pb lowest surface deformed point
|
|
return snap_foot(pb, gnd)
|
|
|
|
class AW_OT_contact_to_ground(bpy.types.Operator):
|
|
bl_idname = "autowalk.contact_to_ground"
|
|
bl_label = "Ground Feet"
|
|
bl_description = "Ground selected feets"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return context.object and context.object.type == 'ARMATURE'
|
|
|
|
def execute(self, context):
|
|
# TODO: check if possible to auto detect a ground when gnd is not specified
|
|
# (still instersteing to be able to get the ground user wants)
|
|
|
|
if not context.scene.anim_cycle_settings.gnd:
|
|
self.report({'ERROR'}, 'need to choose a target mesh for "Ground"')
|
|
return {"CANCELLED"}
|
|
|
|
# context.scene.anim_cycle_settings.expand_on_selected_bones
|
|
err = snap_feet()
|
|
if err:
|
|
self.report({err[0]}, err[1])
|
|
if err[0] == 'ERROR':
|
|
return {"CANCELLED"}
|
|
|
|
return {"FINISHED"}
|
|
|
|
|
|
classes=(
|
|
AW_OT_contact_to_ground,
|
|
)
|
|
|
|
def register():
|
|
for cls in classes:
|
|
bpy.utils.register_class(cls)
|
|
|
|
def unregister():
|
|
for cls in reversed(classes):
|
|
bpy.utils.unregister_class(cls) |