Spaces:
Running on Zero
Running on Zero
File size: 4,789 Bytes
5c331a4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | """Transfer a provider's learned skin to the unchanged production GLB."""
import argparse
import json
import sys
from pathlib import Path
import bpy
from mathutils import Matrix
from mathutils.kdtree import KDTree
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--source", required=True)
parser.add_argument("--rig", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--normalization")
args = parser.parse_args(sys.argv[sys.argv.index("--") + 1 :])
bpy.ops.wm.read_factory_settings(use_empty=True)
if Path(args.rig).suffix.lower() == ".fbx":
bpy.ops.import_scene.fbx(filepath=args.rig)
else:
bpy.ops.import_scene.gltf(filepath=args.rig)
rigs = [o for o in bpy.context.scene.objects if o.type == "ARMATURE"]
if len(rigs) != 1:
raise RuntimeError("Learned rig must contain exactly one armature")
rig = rigs[0]
rig.animation_data_clear()
for bone in rig.pose.bones:
bone.matrix_basis.identity()
donors = [o for o in bpy.context.scene.objects if o.type == "MESH"]
if args.normalization:
correction = json.loads(Path(args.normalization).read_text())
donor_coords = [o.matrix_world @ v.co for o in donors for v in o.data.vertices]
span = max(v.z for v in donor_coords) - min(v.z for v in donor_coords)
scale = span / correction["normalized_height"]
matrix = Matrix(correction["inverse_blender"]) @ Matrix.Diagonal((1 / scale, 1 / scale, 1 / scale, 1))
worlds = {o: o.matrix_world.copy() for o in [rig, *donors]}
rig.matrix_world = matrix @ worlds[rig]
for obj in donors:
obj.matrix_world = matrix @ worlds[obj]
bpy.context.view_layer.update()
points = []
weights = []
for donor in donors:
names = {g.index: g.name for g in donor.vertex_groups}
for vertex in donor.data.vertices:
points.append(donor.matrix_world @ vertex.co)
influences = [
(names[g.group], g.weight)
for g in vertex.groups
if names[g.group] in rig.data.bones and g.weight > 0
]
weights.append(sorted(influences, key=lambda p: -p[1])[:4])
tree = KDTree(len(points))
for index, point in enumerate(points):
tree.insert(point, index)
tree.balance()
before = set(bpy.context.scene.objects)
bpy.ops.import_scene.gltf(filepath=args.source)
targets = [o for o in bpy.context.scene.objects if o not in before and o.type == "MESH"]
coords = [o.matrix_world @ v.co for o in targets for v in o.data.vertices]
height = max(p.z for p in coords) - min(p.z for p in coords)
max_distance = 0.0
for obj in targets:
obj.vertex_groups.clear()
groups = {b.name: obj.vertex_groups.new(name=b.name) for b in rig.data.bones}
for vertex in obj.data.vertices:
_, index, distance = tree.find(obj.matrix_world @ vertex.co)
max_distance = max(max_distance, distance)
influences = weights[index]
total = sum(weight for _, weight in influences)
if total <= 0:
raise RuntimeError("Provider returned unweighted vertices")
for name, weight in influences:
groups[name].add([vertex.index], weight / total, "REPLACE")
world = obj.matrix_world.copy()
obj.parent = rig
obj.matrix_world = world
modifier = obj.modifiers.new("Learned skin", "ARMATURE")
modifier.object = rig
if max_distance > max(height * 0.005, 1e-5):
raise RuntimeError(f"Learned rig geometry differs from source: {max_distance / height:.4%} height")
for obj in donors:
bpy.data.objects.remove(obj, do_unlink=True)
for action in list(bpy.data.actions):
bpy.data.actions.remove(action)
bpy.ops.object.select_all(action="DESELECT")
for obj in [rig, *targets]:
obj.select_set(True)
bpy.ops.export_scene.gltf(
filepath=args.output,
export_format="GLB",
use_selection=True,
export_animations=False,
export_skins=True,
export_yup=True,
)
Path(args.output).with_suffix(".transfer.json").write_text(
json.dumps(
{
"components": len(targets),
"bones": len(rig.data.bones),
"vertices": len(coords),
"max_transfer_distance": max_distance,
"height": height,
"method": "learned skin nearest original rest vertex; top four normalized",
"materials": "original meshes, UVs and material nodes retained",
},
indent=2,
)
)
if __name__ == "__main__":
main()
|