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)