File size: 8,380 Bytes
5c48b81
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
import json
import bpy
import os

def import_obj(filepath):
    """导入OBJ文件"""
    if not os.path.exists(filepath):
        raise FileNotFoundError(f"文件不存在:{filepath}")
    bpy.ops.wm.obj_import(filepath=filepath)
    print(f"成功导入:{filepath}")

def create_armature_from_bone_tree(obj, bone_tree_path):
    """根据骨骼树JSON创建骨骼系统并绑定到模型"""
    with open(bone_tree_path, 'r') as f:
        bones_data = json.load(f)['bones'][0]

    # 创建新骨骼
    bpy.ops.object.armature_add(enter_editmode=True)
    armature = bpy.context.object
    armature.name = "ModelArmature"
    edit_bones = armature.data.edit_bones

    # 创建骨骼树的递归函数
    # root_bone = edit_bones.new(bones_data['name'])
    # print(bones_data['name'])
    def recursive_create_bones(parent_bone, bone_list):
        for bone_data in bone_list:
            new_bone = edit_bones.new(bone_data['name'])
            print(new_bone.name)
            new_bone.head = bone_data['position']
            new_bone.tail = [*new_bone.head[:2], new_bone.head[2]+1]  # 设置轴向长度
            if parent_bone:
                new_bone.parent = parent_bone
            if 'children' in bone_data:
                recursive_create_bones(new_bone, bone_data['children'])
            # recursive_create_bones(new_bone, bone_list=bone_data.get('children', []))

    # 从根骨骼开始构建
    # print(bones_data.get('children', []))
    recursive_create_bones(None, [bones_data])  

    # 设置骨骼与模型绑定
    obj.parent = armature
    mod = obj.modifiers.new("Armature", 'ARMATURE')
    mod.object = armature
    mod.use_vertex_groups = True

    # 创建顶点组(与骨骼同名)
    # print(armature.data.edit_bones)
    for bone in armature.data.edit_bones:
        # print(bone.name)
        if bone.name not in obj.vertex_groups:
            obj.vertex_groups.new(name=bone.name)
        else:
            print(f"顶点组 {bone.name} 已存在")

    
    # 返回创建的骨骼对象用于后续操作
    bpy.ops.object.mode_set(mode='OBJECT')
    return armature

def apply_vertex_weights(obj, weight_file):
    """根据权重JSON设置顶点权重"""
    with open(weight_file, 'r') as f:
        js_data = json.load(f)
        try:
            weights = js_data.get('vertex_weights', {})
        except:
            weights = js_data

    # 获取所有顶点组名称
    bpy.ops.object.mode_set(mode='OBJECT')
    existing_vertex_groups = {vg.name: vg for vg in obj.vertex_groups}

    # print(existing_vertex_groups)
    # 遍历每个顶点的权重
    for vert_idx, weight_dict in enumerate(weights):
        if vert_idx >= len(obj.data.vertices):
            print(f"顶点索引 {vert_idx} 超出范围")
            continue

        # 清除当前顶点的所有权重
        for group in obj.vertex_groups:
            if vert_idx < len(obj.data.vertices):
                group.remove([vert_idx])

        # 重新分配权重
        bones = ['root', 'neck', 'jaw', 'leftEye', 'rightEye']
        #  for vert_idx, weight_dict in enumerate(weights):
        
        # print(obj.vertex_groups)
        for bone_idx, weight in enumerate(weight_dict):
            bone_name = bones[bone_idx]
            # print(bone_name)
            if bone_name not in existing_vertex_groups:
                print(f"顶点组 {bone_name} 不存在,跳过")
                continue

            vgroup = existing_vertex_groups[bone_name]
            if vgroup and vert_idx < len(obj.data.vertices):
                vgroup.add([vert_idx], weight, 'REPLACE')
            else:
                print(f"顶点索引 {vert_idx} 或顶点组 {bone_name} 无效")

def add_shape_keys(base_obj, bs_obj_files):
    """添加多个Shape Keys(表情文件)"""
    if not base_obj.data.shape_keys:
        base_obj.shape_key_add(name="Basis")
    
    for idx, path in enumerate(bs_obj_files):
        if not os.path.exists(path):
            print(f"表情文件缺失:{path}")
            continue
        # 导入表情模型(需保持基础模型选中)
        bpy.ops.wm.obj_import(filepath=path)
        imported_obj = [obj for obj in bpy.data.objects if obj.select_get()][0]
        
        # 创建新的Shape Key
        new_sk_name = os.path.basename(path).split('.')[0]
        new_sk = base_obj.shape_key_add(name=new_sk_name)
        
        # 复制顶点位置到Shape Key
        for v in base_obj.data.vertices:
            if v.index < len(imported_obj.data.vertices):
                new_sk.data[v.index].co = imported_obj.data.vertices[v.index].co
        
        # 清理临时对象
        bpy.data.objects.remove(imported_obj)

def layout_bones_pose(armature, pose_config):
    """设置骨骼的初始姿势(可选)"""
    if pose_config:
        with open(pose_config, 'r') as f:
            pose_data = json.load(f)
        for bone in armature.pose.bones:
            if bone.name in pose_data:
                bone.rotation_euler = tuple(pose_data[bone.name]['rotation'])
                bone.location = tuple(pose_data[bone.name]['location'])

def apply_rotation(obj):
    """手动应用 90 度旋转(绕 X 轴)并将变换应用到模型"""
    obj.rotation_euler = (1.5708, 0, 0)  # 90 度旋转(弧度制:1.5708 = π/2)
    bpy.context.view_layer.update()      # 更新场景以应用旋转
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)  # 应用旋转
    print(f"Applied 90-degree rotation to object: {obj.name}")

def export_as_glb(obj, output_path, output_vertex_order_file):
    """导出为GLB格式,确保包含骨骼信息"""
    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    # 切换到对象模式
    bpy.ops.object.mode_set(mode='OBJECT') 

    # 获取基础模型
    base_objects = [obj for obj in bpy.context.scene.objects if obj.type == 'MESH']
    if len(base_objects) != 1:
        raise ValueError("Scene should contain exactly one base mesh object.")
    base_obj = base_objects[0]

    # 获取顶点数据
    vertices = [(i, v.co.z) for i, v in enumerate(base_obj.data.vertices)]

    # 根据 Z 轴坐标排序
    sorted_vertices = sorted(vertices, key=lambda x: x[1])  # 按 Z 坐标从小到大排序
    sorted_vertex_indices = [idx for idx, z in sorted_vertices]

    # 输出顶点顺序到文件
    with open(output_vertex_order_file, "w") as f:
        json.dump(sorted_vertex_indices, f, indent=4)  # 保存为 JSON 数组
    print(f"Exported vertex order to: {output_vertex_order_file}")
    
    # 执行导出
    bpy.ops.export_scene.gltf(filepath=output_path, 
                            export_format='GLB',
                            export_skins=True,
                            export_texcoords=False,         # 不导出 UV 数据
                            export_normals=False            # 不导出法线数据
                            )
    print(f"导出成功:{output_path}")

def main():
    base_model_path = "runtime_data/nature.obj"  # 基础模型路径
    expression_dir = "runtime_data/bs"  # 表情文件夹
    bone_tree_path = "runtime_data/bone_tree.json"  # 骨骼结构配置
    weight_data_path = "runtime_data/lbs_weight_20k.json"  # 权重数据
    output_glb_path = "runtime_data/skin.glb"
    output_vertex_order_file = "runtime_data/vertex_order.json"  # 输出顶点顺序文件

    # 清空场景
    bpy.ops.wm.read_homefile(use_empty=True)

    # 导入基础模型
    import_obj(base_model_path)
    base_obj = bpy.context.view_layer.objects.active

    # 创建骨骼系统
    armature = create_armature_from_bone_tree(base_obj, bone_tree_path)
    
    # 设置骨骼姿势(如果需要)
    # layout_bones_pose(armature, "pose_config.json") 

    # 应用顶点权重
    apply_vertex_weights(base_obj, weight_data_path)

    # 加载所有Shape Keys(表情)
    expression_files = [
        os.path.join(expression_dir, f)
        for f in os.listdir(expression_dir) 
        if f.endswith(('.obj', '.OBJ'))
    ]
    add_shape_keys(base_obj, expression_files)
    apply_rotation(base_obj)

    # 导出为GLB格式
    export_as_glb(base_obj, output_glb_path, output_vertex_order_file)

if __name__ == "__main__":
    main()