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.
This commit is contained in:
1 parent
01939de9a0
commit
1c6b956435
21 files changed
+614
-33
No files matched your search
@@ -0,0 +1,148 @@
|
||||
# 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()
|
||||
Reference in new issue
Block a user