|
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 |
|
|
|
|
|
|
|
|
|
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(None, [bones_data]) |
|
|
|
|
|
obj.parent = armature |
|
mod = obj.modifiers.new("Armature", 'ARMATURE') |
|
mod.object = armature |
|
mod.use_vertex_groups = True |
|
|
|
|
|
|
|
for bone in armature.data.edit_bones: |
|
|
|
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} |
|
|
|
|
|
|
|
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 bone_idx, weight in enumerate(weight_dict): |
|
bone_name = bones[bone_idx] |
|
|
|
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] |
|
|
|
|
|
new_sk_name = os.path.basename(path).split('.')[0] |
|
new_sk = base_obj.shape_key_add(name=new_sk_name) |
|
|
|
|
|
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) |
|
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)] |
|
|
|
|
|
sorted_vertices = sorted(vertices, key=lambda x: x[1]) |
|
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) |
|
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, |
|
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) |
|
|
|
|
|
|
|
|
|
|
|
apply_vertex_weights(base_obj, weight_data_path) |
|
|
|
|
|
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) |
|
|
|
|
|
export_as_glb(base_obj, output_glb_path, output_vertex_order_file) |
|
|
|
if __name__ == "__main__": |
|
main() |
|
|