Marking foot contacts by hand has been the most tedious part of my mocap cleanup workflow for years. Scrub to where the foot stops, drop a marker, move on, repeat across 40 takes. Did it again last week and finally got annoyed enough to write something.
The approach: sample world-space foot bone positions frame by frame, compute per-frame velocity, flag anything below a threshold as a plant candidate. Very dumb, very effective for the rough pass.
import bpy
def sample_bone_positions(obj, bone_name, frame_start, frame_end):
scene = bpy.context.scene
positions = {}
for f in range(frame_start, frame_end + 1):
scene.frame_set(f)
bone = obj.pose.bones.get(bone_name)
if bone:
positions[f] = (obj.matrix_world @ bone.matrix).translation.copy()
return positions
def detect_plant_frames(positions, threshold=0.008):
frames = sorted(positions.keys())
plants = []
for i in range(1, len(frames)):
delta = (positions[frames[i]] - positions[frames[i-1]]).length
if delta < threshold:
plants.append(frames[i])
return plants
def mark_contacts(bone_names=('foot_L', 'foot_R'), threshold=0.008):
obj = bpy.context.active_object
if not obj or obj.type != 'ARMATURE':
print('Select an armature.')
return
scene = bpy.context.scene
for m in list(scene.timeline_markers):
if m.name.startswith('PLANT_'):
scene.timeline_markers.remove(m)
for bone_name in bone_names:
positions = sample_bone_positions(obj, bone_name, scene.frame_start, scene.frame_end)
plants = detect_plant_frames(positions, threshold)
label = bone_name.replace('foot_', '').upper()
for f in plants:
scene.timeline_markers.new(f'PLANT_{label}', frame=f)
print(f'{bone_name}: {len(plants)} contacts detected')
mark_contacts()
Threshold of 0.008 world units works for Rokoko data at 30fps with 1 unit = 1 meter. You'll need to tune it for your scale and frame rate. At that setting I'm catching roughly 85% of contacts cleanly on the first pass. False positives tend to cluster during slow foot swing where the bone is nearly stopped but not actually planted yet.
What I haven't solved: consecutive plant frames just stack as individual markers, which is still useful but messy. I want to add a merge pass that collapses runs of adjacent frames into a single range marker so you can see "L foot: frames 12–18" as one thing instead of seven identical markers in a pile.
Also thinking about incorporating foot height from the ground plane as a secondary filter. That should help with the slow-swing false positives that raw velocity can't distinguish from actual contacts. Has anyone done something similar? Curious if there's a smarter heuristic than just velocity + height together.