# MPFB character export for the Blender -> Unreal pipeline (0.1.0 acceptance). # # Generates a default human, adds the standard deform rig with weights, # materializes the v2 skin, bakes its diffuse to a texture, and exports # FBX with the named export preset "ue_character_v1". # # blender -b --factory-startup --python Tools/mpfb_character_export.py -- \ # --out /tmp/gigline/character.fbx \ # --save-blend Art/blender/character/character-.blend import argparse import os import sys import bpy REPORT = "/tmp/mpfb-export-report.txt" lines = [] def log(msg): lines.append(str(msg)) def enable_mpfb(): bpy.ops.preferences.addon_enable(module="bl_ext.blender_org.mpfb") assert "bl_ext.blender_org.mpfb" in bpy.context.preferences.addons, "MPFB did not enable" def build_character(): for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True) bpy.ops.mpfb.create_human("EXEC_DEFAULT") human = bpy.context.active_object log(f"create_human: {human.name}, {len(human.data.vertices)} verts, dims={[round(d, 3) for d in human.dimensions]}") bpy.ops.mpfb.add_standard_rig("EXEC_DEFAULT") armatures = [o for o in bpy.data.objects if o.type == "ARMATURE"] log(f"add_standard_rig: {len(armatures)} armature(s), bones={[len(a.data.bones) for a in armatures]}") bpy.ops.object.select_all(action="DESELECT") human.select_set(True) bpy.context.view_layer.objects.active = human bpy.ops.mpfb.create_v2_skin("EXEC_DEFAULT") mats = [m.name for m in human.data.materials] log(f"create_v2_skin: materials={mats}") return human def bake_diffuse(human, size=1024): mat = human.data.materials[0] nodes, links = mat.node_tree.nodes, mat.node_tree.links img = bpy.data.images.new("T_Skin_Diffuse", size, size) tex_node = nodes.new("ShaderNodeTexImage") tex_node.image = img nodes.active = tex_node tex_node.select = True bsdf = next((n for n in nodes if n.type == "BSDF_PRINCIPLED"), None) bsdf_input = bsdf.inputs["Base Color"] if bsdf else None if bsdf is not None: for link in list(links): if link.to_node == bsdf and link.to_socket == bsdf_input: links.remove(link) links.new(tex_node.outputs["Color"], bsdf_input) scene = bpy.context.scene scene.render.engine = "CYCLES" scene.cycles.device = "CPU" scene.cycles.samples = 1 scene.render.bake.use_pass_direct = False scene.render.bake.use_pass_indirect = False scene.render.bake.use_pass_color = True scene.render.bake.margin = 8 bpy.ops.object.select_all(action="DESELECT") human.select_set(True) bpy.context.view_layer.objects.active = human bpy.ops.object.bake(type="DIFFUSE", pass_filter={"COLOR"}, use_clear=True) img.filepath = "/tmp/gigline/T_Skin_Diffuse.png" img.filepath_raw = img.filepath img.file_format = "PNG" img.save() log(f"bake: baked {size}x{size} diffuse -> {img.filepath} (filepath now set for FBX)") # MPFB's v2 skin is a node-group tree the FBX exporter cannot map to a # texture. Build a flat export material carrying the baked diffuse so the # FBX (and therefore Unreal) gets a real texture. export_mat = bpy.data.materials.new("M_Character_Export") export_mat.use_nodes = True nt = export_mat.node_tree nt.nodes.clear() out = nt.nodes.new("ShaderNodeOutputMaterial") bsdf = nt.nodes.new("ShaderNodeBsdfPrincipled") tex = nt.nodes.new("ShaderNodeTexImage") tex.image = img nt.links.new(tex.outputs["Color"], bsdf.inputs["Base Color"]) nt.links.new(bsdf.outputs["BSDF"], out.inputs["Surface"]) human.data.materials.clear() human.data.materials.append(export_mat) log("export material: M_Character_Export with baked texture assigned") def export_fbx(human, path): os.makedirs(os.path.dirname(path), exist_ok=True) # Named export preset "ue_character_v1" — change only via a new preset name. bpy.ops.export_scene.fbx( filepath=path, use_selection=False, object_types={"ARMATURE", "MESH"}, apply_unit_scale=True, apply_scale_options="FBX_SCALE_NONE", axis_forward="-Y", axis_up="Z", use_space_transform=True, use_mesh_modifiers=True, add_leaf_bones=False, path_mode="COPY", embed_textures=False, mesh_smooth_type="OFF", bake_space_transform=False, ) log(f"export: FBX preset ue_character_v1 -> {path}") def main(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] parser = argparse.ArgumentParser() parser.add_argument("--out", default="/tmp/gigline/character.fbx") parser.add_argument("--save-blend", default=None) args = parser.parse_args(argv) enable_mpfb() human = build_character() bake_diffuse(human) if args.save_blend: os.makedirs(os.path.dirname(args.save_blend), exist_ok=True) bpy.ops.wm.save_as_mainfile(filepath=args.save_blend) log(f"save: {args.save_blend}") export_fbx(human, args.out) with open(REPORT, "w") as f: f.write("\n".join(lines) + "\n") main()