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()