Working on web version and also improving dorm
This commit is contained in:
1 parent
5d427af9f7
commit
b80e3fdc5e
194 files changed
+3188
-4170281
No files matched your search
@@ -0,0 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Remove bones from a text-format animation file and redistribute their vertex
|
||||
weights to the nearest surviving ancestor bone.
|
||||
|
||||
Default: removes all pinky-finger bones (LeftHandPinky* and RightHandPinky*),
|
||||
reducing a 63-bone rig to 55 bones to stay within WebGL uniform limits.
|
||||
|
||||
Usage:
|
||||
python reduce_bones_txt.py <input.txt> <output.txt> [--remove Bone1,Bone2,...]
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
DEFAULT_REMOVE = [
|
||||
'LeftHandPinky1', 'LeftHandPinky2', 'LeftHandPinky3', 'LeftHandPinky3_end',
|
||||
'RightHandPinky1', 'RightHandPinky2', 'RightHandPinky3', 'RightHandPinky3_end',
|
||||
]
|
||||
|
||||
|
||||
def parse_children_line(line):
|
||||
return re.findall(r"'([^']+)'", line)
|
||||
|
||||
|
||||
def format_children_line(names):
|
||||
if not names:
|
||||
return " Children: []\n"
|
||||
return " Children: [" + ', '.join(f"'{n}'" for n in names) + "]\n"
|
||||
|
||||
|
||||
def build_remap(bone_parent, remove_set):
|
||||
"""Map each removed bone to its nearest non-removed ancestor (or None)."""
|
||||
remap = {}
|
||||
for bone in remove_set:
|
||||
cur = bone_parent.get(bone)
|
||||
while cur is not None and cur in remove_set:
|
||||
cur = bone_parent.get(cur)
|
||||
remap[bone] = cur
|
||||
return remap
|
||||
|
||||
|
||||
def process(lines, remove_set):
|
||||
# ── Pass 1: collect bone parent/children to build weight remap ────────────
|
||||
bone_parent = {}
|
||||
|
||||
i = 0
|
||||
while i < len(lines) and not lines[i].startswith('=== Armature Bones:'):
|
||||
i += 1
|
||||
i += 1 # skip header
|
||||
|
||||
current_bone = None
|
||||
while i < len(lines) and not lines[i].startswith('=== TOTAL MESHES'):
|
||||
line = lines[i].rstrip()
|
||||
if line.startswith('Bone: '):
|
||||
current_bone = line[6:]
|
||||
bone_parent.setdefault(current_bone, None)
|
||||
elif current_bone and line.startswith(' Parent:'):
|
||||
p = line[9:].strip()
|
||||
bone_parent[current_bone] = None if p == 'None' else p
|
||||
i += 1
|
||||
|
||||
remap = build_remap(bone_parent, remove_set)
|
||||
|
||||
missing = remove_set - set(bone_parent.keys())
|
||||
if missing:
|
||||
print(f'Warning: bones not found in file and will be skipped: {missing}', file=sys.stderr)
|
||||
|
||||
# ── Pass 2: emit modified output ──────────────────────────────────────────
|
||||
out = []
|
||||
i = 0
|
||||
|
||||
# Armature matrix — verbatim until bone header
|
||||
while i < len(lines) and not lines[i].startswith('=== Armature Bones:'):
|
||||
out.append(lines[i])
|
||||
i += 1
|
||||
|
||||
# Updated bone count
|
||||
original_count = int(re.search(r'\d+', lines[i]).group())
|
||||
new_count = original_count - len(remove_set & set(bone_parent.keys()))
|
||||
out.append(f'=== Armature Bones: {new_count}\n')
|
||||
i += 1
|
||||
|
||||
# Bone blocks
|
||||
while i < len(lines) and not lines[i].startswith('=== TOTAL MESHES'):
|
||||
line = lines[i]
|
||||
if line.startswith('Bone: '):
|
||||
bone_name = line[6:].rstrip()
|
||||
# Collect the whole block up to the next bone or section boundary
|
||||
i += 1
|
||||
block = []
|
||||
while i < len(lines) and not lines[i].startswith('Bone: ') \
|
||||
and not lines[i].startswith('=== TOTAL MESHES'):
|
||||
block.append(lines[i])
|
||||
i += 1
|
||||
if bone_name not in remove_set:
|
||||
out.append(f'Bone: {bone_name}\n')
|
||||
for bline in block:
|
||||
if bline.rstrip().startswith(' Children:'):
|
||||
children = [c for c in parse_children_line(bline) if c not in remove_set]
|
||||
out.append(format_children_line(children))
|
||||
else:
|
||||
out.append(bline)
|
||||
else:
|
||||
out.append(line)
|
||||
i += 1
|
||||
|
||||
# Mesh geometry sections and vertex weights
|
||||
in_vertex_weights = False
|
||||
|
||||
while i < len(lines) and not lines[i].startswith('=== Animation Keyframes'):
|
||||
line = lines[i]
|
||||
|
||||
if line.startswith('=== Vertex Weights'):
|
||||
in_vertex_weights = True
|
||||
out.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if in_vertex_weights:
|
||||
# A new '=== ' header means we've left this mesh's vertex weights
|
||||
if line.startswith('=== '):
|
||||
in_vertex_weights = False
|
||||
out.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
# Vertex block: 'Vertex N:\n'
|
||||
if re.match(r'Vertex \d+:\s*$', line):
|
||||
out.append(line)
|
||||
i += 1
|
||||
|
||||
groups_line = lines[i]
|
||||
k = int(re.search(r'\d+', groups_line).group())
|
||||
i += 1
|
||||
|
||||
weights = {}
|
||||
for _ in range(k):
|
||||
m = re.match(r"\s*Group: '([^']+)', Weight: ([\d.]+)", lines[i])
|
||||
i += 1
|
||||
if m:
|
||||
bname, w = m.group(1), float(m.group(2))
|
||||
target = remap.get(bname, bname)
|
||||
if target is not None:
|
||||
weights[target] = weights.get(target, 0.0) + w
|
||||
|
||||
total = sum(weights.values())
|
||||
if total > 0:
|
||||
weights = {b: w / total for b, w in weights.items()}
|
||||
|
||||
out.append(f'Vertex groups: {len(weights)}\n')
|
||||
for bname, w in weights.items():
|
||||
out.append(f" Group: '{bname}', Weight: {w:.6f}\n")
|
||||
continue
|
||||
|
||||
out.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
out.append(line)
|
||||
i += 1
|
||||
|
||||
# Animation section header lines ('=== Animation Keyframes ===',
|
||||
# '=== Bone Transforms per Keyframe ===', 'Keyframes: N')
|
||||
while i < len(lines) and not lines[i].startswith('Frame: '):
|
||||
out.append(lines[i])
|
||||
i += 1
|
||||
|
||||
# Per-frame bone blocks
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
if line.startswith('Frame: '):
|
||||
out.append(line)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
if line.startswith(' Bone: '):
|
||||
bone_name = line[8:].rstrip()
|
||||
i += 1
|
||||
block = []
|
||||
while i < len(lines) and not lines[i].startswith(' Bone: ') \
|
||||
and not lines[i].startswith('Frame: '):
|
||||
block.append(lines[i])
|
||||
i += 1
|
||||
if bone_name not in remove_set:
|
||||
out.append(f' Bone: {bone_name}\n')
|
||||
out.extend(block)
|
||||
continue
|
||||
|
||||
out.append(line)
|
||||
i += 1
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Remove bones from a text animation file and redistribute weights.')
|
||||
parser.add_argument('input', help='Input .txt animation file')
|
||||
parser.add_argument('output', help='Output .txt animation file')
|
||||
parser.add_argument('--remove', default=','.join(DEFAULT_REMOVE),
|
||||
help='Comma-separated bone names to remove (default: all pinky bones)')
|
||||
args = parser.parse_args()
|
||||
|
||||
remove_set = set(args.remove.split(','))
|
||||
|
||||
with open(args.input, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
out_lines = process(lines, remove_set)
|
||||
|
||||
with open(args.output, 'w', encoding='utf-8') as f:
|
||||
f.writelines(out_lines)
|
||||
|
||||
removed_count = len(remove_set)
|
||||
print(f'Done. Removed {removed_count} bones. Output written to: {args.output}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user