Files
gig-line/Tools/mpfb_character_export.py
T
zwitschi 1c6b956435 bootstrap: finish project bootstrap — scale test, MPFB character, build docs
- Static-mesh scale test passes: 1 m = 100.00 cm, origin preserved, axis
  mapping east→+X / north→−Y / up→+Z (named presets ue_metric_v1 both sides).
- MPFB 2.0.18 acceptance passes: generated human + standard 137-bone rig
  with weights + v2 skin baked to 1024² diffuse; imports as SkeletalMesh
  with Skeleton, Material, and Texture2D in /Game/Test/Character.
- MPFB headless requires XDG_CONFIG_HOME at the Flatpak config tree;
  documented in Art/blender/README.md, probe in Tools/addon_compat_probe.py.
- Docs/build.md completed: fresh-checkout build, packaging, headless checks.
- Docs/setup.md reconciled with reality; ROADMAP/bootstrap closed out.
2026-11-07 09:29:11 +01:00

149 lines
5.1 KiB
Python

# 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-<date>.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()