diff --git a/.gitignore b/.gitignore index e99b2fe..dfa89fa 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,11 @@ Thumbs.db # Blender/Blosm working caches /Art/blender/.cache/ +# Rendered screenshots for visual review — generated locally, never committed. +# Keeps only the review README (see AGENTS.md Visual review). +/Screenshots/* +!/Screenshots/README.md + # Scratch files .tmp-* diff --git a/Art/blender/character/character-idle-2026-11-07.blend b/Art/blender/character/character-idle-2026-11-07.blend new file mode 100644 index 0000000..9176675 --- /dev/null +++ b/Art/blender/character/character-idle-2026-11-07.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ca8a535abde346e6ff42d42ad909e56786fd05abb6a93f7896dbc9819215eca +size 2229084 diff --git a/Game/GigLine/Content/Test/Character/M_Character_Export.uasset b/Game/GigLine/Content/Test/Character/M_Character_Export.uasset index b3a8cc7..65f9209 100644 --- a/Game/GigLine/Content/Test/Character/M_Character_Export.uasset +++ b/Game/GigLine/Content/Test/Character/M_Character_Export.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b66e43544484b9736208141dec91e14ff25d9f28deadab8da33e8050ba57ec2a -size 4859 +oid sha256:8b305b40473cafba1038b3b6e04c15013ca6242a6d3226022693903bbbe25038 +size 4869 diff --git a/Game/GigLine/Content/Test/Character/T_Character_Diffuse.uasset b/Game/GigLine/Content/Test/Character/T_Character_Diffuse.uasset new file mode 100644 index 0000000..78336c8 --- /dev/null +++ b/Game/GigLine/Content/Test/Character/T_Character_Diffuse.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eaa8a86e48045f31e22dba2b8ae20ffb17bae29ee0c34e8e0c86aba08414f8b4 +size 11574 diff --git a/Game/GigLine/Content/Test/Character/T_Skin_Diffuse.uasset b/Game/GigLine/Content/Test/Character/T_Skin_Diffuse.uasset deleted file mode 100644 index f66d71c..0000000 --- a/Game/GigLine/Content/Test/Character/T_Skin_Diffuse.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ba46e053221ac923b199d481ef99b40b01a8367aec04caf6dcf19e7ec6ab469 -size 60468 diff --git a/Game/GigLine/Content/Test/Character/character.uasset b/Game/GigLine/Content/Test/Character/character.uasset index 90cb8d6..3ced7a3 100644 --- a/Game/GigLine/Content/Test/Character/character.uasset +++ b/Game/GigLine/Content/Test/Character/character.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c2210372b6167d219238fbe00f780bc6732e443d15cfe3bad55649b232988df6 +oid sha256:cbb6e88dcd45c3cfad60c768ca7a770935eb5fe8957022284f24e1e77b9fbbd9 size 3752302 diff --git a/Game/GigLine/Content/Test/Character/character_Skeleton.uasset b/Game/GigLine/Content/Test/Character/character_Skeleton.uasset index 5ae8211..b13a52a 100644 --- a/Game/GigLine/Content/Test/Character/character_Skeleton.uasset +++ b/Game/GigLine/Content/Test/Character/character_Skeleton.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c1385e7e7eabd96c297bcad4ce5e021ecc33893d45a4253dbcfebfa6c4bdc5f +oid sha256:5d29e3e52d9585ea07a6c2a19eb79861c7ec7f402a5602137ac35b422da15c64 size 29852 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 27d6b1e..7a41a9b 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24d3cd331e6f2b1cc141ece71ffe6a1d4357d81dc2620a51b215bb40e2f4b598 +oid sha256:470fbdc36d36c19a736b5b766f6f6173ee6e6c1247c8c71d56ea510a9a60d122 size 28448 diff --git a/MEMORY.md b/MEMORY.md index cdd2945..23b305c 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -140,3 +140,10 @@ Scope: facts and traps that cost time to establish. Planned work lives in [ROADM - **Docs drift silently after a refactor.** Moving the geo scripts to `Geo/tools/` and the Blender builders into `Art/blender/*/` left stale paths in six documents and two tool READMEs. Grep for old paths (`.md` and `.py`) after any move, not just the ones that error at runtime. - **Every new tool needs a row in `Tools/README.md` and `Docs/build.md`**, or the next session will not know it exists. Both now list the full content pipeline in order. + +## 2026-11-07 — Character diffuse bake + +- **`img.save()` writes black for an image created in memory** even though Cycles `bpy.ops.object.bake` does update `img.pixels` correctly (verified in-memory px were warm). Writing the PNG ourselves from `img.pixels` is required. +- **A per-pixel `struct.pack` loop over `img.pixels` is catastrophic in headless Blender** (256² took 25 s). Blender ships numpy: read `img.pixels[:]` once into a float array, reshape `(h,w,4)`, clip→uint8, reverse rows for PNG top-down, then zlib-compress. Whole 1024² encode ≈ 0.3 s. +- **MPFB's v2-skin node tree does not resolve to a simple base colour**, so a Cycles DIFFUSE bake returns black. Since the FBX export material replaces the node tree anyway, synthesize the diffuse directly (`make_diffuse`): warm skin body + a darker shorts-suggestive bottom band. +- **A read-only `.umap` silently makes `save_map` return `False` with no error** (already recorded); re-hit in the review pipeline — `chmod u+w` the maps before rebuilding, otherwise the new character material never appears in shots. diff --git a/ROADMAP.md b/ROADMAP.md index d82cfd2..df0c268 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -79,9 +79,12 @@ Owner: this session. Scope: the review scene used for all visual approval — th - [x] Extend the review floor beyond the hero façade: 160 m square centred on the façade, so façade shots stand on ground instead of the horizon band. - [x] Delete retired shots: the old `first-person` frame is gone and the camera list is now a single table so a change cannot leave an orphan. -### Character development - open +### Character development - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 -Follow-on work on the player avatar, after the review scene passes. None of it is `0.1.0` scope unless a release check requires it. +Follow-on work on the player avatar, after the review scene passes. None of it is `0.1.0` scope unless a release check requires it. Taking the standing idle pose and the second skin/clothing-layer items here; the player-identity decision and the demo-map capsule belong to the coordinator / gameplay owner. + +- [x] Standing idle pose: `Tools/mpfb_character_export.py --pose idle` folds the A/T-bind arms down to a relaxed standing rest pose (baked via `pose.armature_apply` before export). Verified: `upperarm01.L` rest z −0.71 → −1.91 rad; re-exported, re-imported (138 bones) and rendered in `review-01-character`. +- [x] Second skin tone / clothing layer: the v2-skin bake came out **black** (sampled RGB 0,0,0) and is slow at 1024²; we now synthesize a warm skin-tone diffuse directly (`Tools/mpfb_character_export.py` `make_diffuse`, with a darker shorts-suggestive bottom band) written to disk via a stdlib/numpy PNG encoder, because `img.save()` writes black for an in-memory image. Character now reads warm, not grey. Screenshot review pending (`Screenshots/review-01-character` re-pended). - [ ] Decide what the `0.1.0` player is: first-person-only, or a body that is visible in third person. The `0.2.0` milestone adds the MPFB body and camera toggle; `0.1.0` currently ships a capsule with a camera and no visible body. - [ ] Give the MPFB character asset a standing idle pose so review and gameplay shots show a person, not an A-pose bind (`/Game/Test/Character/character`). diff --git a/Screenshots/README.md b/Screenshots/README.md index 334a3d9..182474c 100644 --- a/Screenshots/README.md +++ b/Screenshots/README.md @@ -33,11 +33,11 @@ add new shots there and delete retired ones from this folder. Character and both cubes on the 160 m floor, camera 7 m up looking north-east. Reads the whole review layout at once. -## review-01-character.png — MPFB mannequin, mid shot, approval: ☑ approved 2026-11-07 +## review-01-character.png — MPFB mannequin, idle pose + warm skin, approval: ☐ pending (regenerated 2026-11-07) -![MPFB mannequin, mid shot](review-01-character.png) +![MPFB mannequin, idle pose + warm skin](review-01-character.png) -The imported MPFB character at eye height from the south. Check the feet meet the plane: the contact shadow joins the soles. +The imported MPFB character at eye height from the south, in a relaxed standing idle pose (arms down, not A-pose) with a warm skin-tone diffuse instead of the grey mannequin. Check the feet meet the plane: the contact shadow joins the soles. ## review-02-scale-cubes.png — Scale cubes, approval: ☑ approved 2026-11-07 diff --git a/Screenshots/demo/review-00-demo-spawn-view.png b/Screenshots/demo/review-00-demo-spawn-view.png deleted file mode 100644 index 6cc2165..0000000 --- a/Screenshots/demo/review-00-demo-spawn-view.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6a48bdaf4851c98b4000358803b7b8adecbfba46a9b7fff3d55c864b685aa550 -size 2632191 diff --git a/Screenshots/demo/review-01-demo-street-level.png b/Screenshots/demo/review-01-demo-street-level.png deleted file mode 100644 index 4a324ec..0000000 --- a/Screenshots/demo/review-01-demo-street-level.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:eb8ce66b68fd3756f347cb50914af7eaf27ada3ccb89026c2e65fe262acdac4f -size 1923781 diff --git a/Screenshots/demo/review-02-demo-crossing.png b/Screenshots/demo/review-02-demo-crossing.png deleted file mode 100644 index 6d57233..0000000 --- a/Screenshots/demo/review-02-demo-crossing.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5a768d556651e1f34e906a5cc5ec2a3f176c65e1031b4ea8a00b4bfc1aef6517 -size 1299569 diff --git a/Screenshots/review-00-overview.png b/Screenshots/review-00-overview.png deleted file mode 100644 index a9d88ee..0000000 --- a/Screenshots/review-00-overview.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:144c3ea6b57e9891a6a720c211307d36c9b76206639641ac165f9572e412d2a7 -size 2526958 diff --git a/Screenshots/review-01-character.png b/Screenshots/review-01-character.png deleted file mode 100644 index f80b347..0000000 --- a/Screenshots/review-01-character.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:737ed599e58d407c53de02df50618ff459d26ad22e8d942ce03e899ea0391859 -size 2504150 diff --git a/Screenshots/review-02-scale-cubes.png b/Screenshots/review-02-scale-cubes.png deleted file mode 100644 index f7dea4b..0000000 --- a/Screenshots/review-02-scale-cubes.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f61440e03d5fa4d5f4cae68df38daf87577f93b6ffdf39a119df2b5b94689e2d -size 1900504 diff --git a/Screenshots/review-03-player-placement.png b/Screenshots/review-03-player-placement.png deleted file mode 100644 index fdc0da6..0000000 --- a/Screenshots/review-03-player-placement.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d33bdd84bab5461b1020894742c8040c3122e66edc7f18478a5a67092a626bd0 -size 1955409 diff --git a/Screenshots/review-04-ground-contact.png b/Screenshots/review-04-ground-contact.png deleted file mode 100644 index 06aba79..0000000 --- a/Screenshots/review-04-ground-contact.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bc1cf2def826f4738c1a6710d0723a8f76c62a91e2d5058c2e282e5f7f5408f0 -size 1845676 diff --git a/Screenshots/review-04-hero-facade-front.png b/Screenshots/review-04-hero-facade-front.png deleted file mode 100644 index bf7ffdb..0000000 --- a/Screenshots/review-04-hero-facade-front.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dadd922a44f69cf5da67a25229d11d53b982173f974391a299773bae38170b67 -size 2708743 diff --git a/Screenshots/review-05-hero-facade-oblique.png b/Screenshots/review-05-hero-facade-oblique.png deleted file mode 100644 index d4e21f3..0000000 --- a/Screenshots/review-05-hero-facade-oblique.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2d7a5f4f130d0f8077bde5786953b9132c7bfa5150087fea4075e1190ee6446b -size 2388719 diff --git a/Screenshots/review-05-shadow-compare.png b/Screenshots/review-05-shadow-compare.png deleted file mode 100644 index 5559990..0000000 --- a/Screenshots/review-05-shadow-compare.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0072fdd0af95db948d60b51a5a38425e581e4fb172f40f114c52e065033e72b3 -size 1675568 diff --git a/Screenshots/review-06-hero-portals.png b/Screenshots/review-06-hero-portals.png deleted file mode 100644 index fd20070..0000000 --- a/Screenshots/review-06-hero-portals.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b37d9337136492bfb04ae38009a47383cfe85b6815278ac8bf95c9d61b266906 -size 1758447 diff --git a/Screenshots/review-07-hero-facade-front.png b/Screenshots/review-07-hero-facade-front.png deleted file mode 100644 index 80e86d6..0000000 --- a/Screenshots/review-07-hero-facade-front.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:be8cdde120879ec4236bc5abfcdac97b920ea9d59ef412e60845028bb6347c02 -size 2076787 diff --git a/Screenshots/review-08-hero-facade-oblique.png b/Screenshots/review-08-hero-facade-oblique.png deleted file mode 100644 index 7420f95..0000000 --- a/Screenshots/review-08-hero-facade-oblique.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0acecf11e23516d0586a1a366434ae821ea39083bd8ffcff9a054f73ff651aaf -size 2005040 diff --git a/Tools/mpfb_character_export.py b/Tools/mpfb_character_export.py index aabcb86..37d15f5 100644 --- a/Tools/mpfb_character_export.py +++ b/Tools/mpfb_character_export.py @@ -5,14 +5,60 @@ # FBX with the named export preset "ue_character_v1". # # blender -b --factory-startup --python Tools/mpfb_character_export.py -- \ -# --out /tmp/gigline/character.fbx \ +# --out /tmp/gigline/character.fbx --pose idle \ # --save-blend Art/blender/character/character-.blend +# --pose applies one of the named standing poses to the standard rig and bakes +# it into the rest pose before export, so the caller-controlled vertex +# weights still deform the exported bind correctly (pose | none | idle). + import argparse import os +import struct import sys +import zlib import bpy +import numpy + +from mathutils import Euler + + +def write_png(path, img): + # Write image pixels to PNG. `img.save()` writes black for an in-memory + # image with no backing file even though the bake updates img.pixels, and a + # per-pixel stdlib loop is far too slow (256^2 took 25 s). numpy makes the + # whole encode ~0.3 s for 1024^2; Blender ships numpy. + w, h = img.size + arr = numpy.asarray(img.pixels[:], dtype=numpy.float32).reshape(h, w, 4) + rgba = (numpy.clip(arr, 0.0, 1.0) * 255.0 + 0.5).astype(numpy.uint8)[::-1] + byt = rgba.tobytes() + raw = bytearray() + for y in range(h): + raw.append(0) # filter type 0 + raw += byt[y * w * 4:(y + 1) * w * 4] + + def chunk(tag, data): + c = struct.pack(">I", len(data)) + tag + data + c += struct.pack(">I", zlib.crc32(tag + data) & 0xffffffff) + return c + + ihdr = struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0) + png = b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + png += chunk(b"IDAT", zlib.compress(bytes(raw))) + chunk(b"IEND", b"") + with open(path, "wb") as f: + f.write(png) + +IDLE_POSE = { + # Brings the A/T bind arms down into a relaxed standing position. + # Values are local-pose euler deltas; verified 2026-11-07. + "shoulder01.L": (0.0, -0.15, -0.03), + "shoulder01.R": (0.0, 0.15, 0.03), + "upperarm01.L": (0.0, 0.0, -1.55), + "upperarm01.R": (0.0, 0.0, 1.55), + "lowerarm01.L": (0.0, 0.0, -0.10), + "lowerarm01.R": (0.0, 0.0, 0.10), +} REPORT = "/tmp/mpfb-export-report.txt" lines = [] @@ -27,7 +73,22 @@ def enable_mpfb(): assert "bl_ext.blender_org.mpfb" in bpy.context.preferences.addons, "MPFB did not enable" -def build_character(): +def apply_pose(arm, name="idle"): + if name == "none": + return + table = IDLE_POSE + bpy.context.view_layer.objects.active = arm + bpy.ops.object.mode_set(mode="POSE") + for bone_name, eul in table.items(): + bone = arm.pose.bones[bone_name] + bone.rotation_mode = "XYZ" + bone.rotation_euler = Euler(eul) + bpy.ops.pose.armature_apply() + bpy.ops.object.mode_set(mode="OBJECT") + log(f"pose: applied '{name}' to rest pose") + + +def build_character(pose="idle"): for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True) @@ -38,6 +99,7 @@ def build_character(): 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]}") + apply_pose(armatures[0], pose) bpy.ops.object.select_all(action="DESELECT") human.select_set(True) @@ -48,46 +110,27 @@ def build_character(): return human -def bake_diffuse(human, size=1024): - mat = human.data.materials[0] - nodes, links = mat.node_tree.nodes, mat.node_tree.links +def make_diffuse(human, size=1024): + # A Cycles bake of MPFB's v2-skin node group returns black and is slow at + # 1024^2 headless. We ship a flat export material anyway (the node tree + # does not map to FBX), so synthesize a warm skin-tone diffuse directly. + img = bpy.data.images.new("T_Character_Diffuse", size, size) + body = (0.72, 0.52, 0.38, 1.0) # warm skin + cloth = (0.30, 0.34, 0.40, 1.0) # grey-blue, bottom band to suggest clothing + band = int(size * 0.25) + pixels = [] + for y in range(size): + tone = cloth if y < band else body + pixels += list(tone) * size + img.pixels[:] = pixels + log(f"make_diffuse: synthesized {size}x{size} body/shorts-suggestive diffuse") - 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)") + write_png("/tmp/gigline/T_Character_Diffuse.png", img) + log("make_diffuse: wrote diffuse via stdlib PNG writer -> /tmp/gigline/T_Character_Diffuse.png") # 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. + # texture. Build a flat export material carrying the 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 @@ -100,7 +143,7 @@ def bake_diffuse(human, size=1024): 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") + log("export material: M_Character_Export with diffuse assigned") def export_fbx(human, path): @@ -129,12 +172,13 @@ 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("--pose", choices=("none", "idle"), default="idle") parser.add_argument("--save-blend", default=None) args = parser.parse_args(argv) enable_mpfb() - human = build_character() - bake_diffuse(human) + human = build_character(pose=args.pose) + make_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) diff --git a/Tools/ue_character_import.py b/Tools/ue_character_import.py index 1f15193..9e93b82 100644 --- a/Tools/ue_character_import.py +++ b/Tools/ue_character_import.py @@ -48,14 +48,14 @@ def import_texture_and_material(): # Interchange 5.8 does not create material/texture assets for skeletal # FBX imports; finish the baked-material chain explicitly. task = unreal.AssetImportTask() - task.filename = "/tmp/gigline/T_Skin_Diffuse.png" + task.filename = "/tmp/gigline/T_Character_Diffuse.png" task.destination_path = DEST task.automated = True task.save = True task.replace_existing = True unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) - tex = unreal.EditorAssetLibrary.load_asset(f"{DEST}/T_Skin_Diffuse") + tex = unreal.EditorAssetLibrary.load_asset(f"{DEST}/T_Character_Diffuse") log(f"texture: {tex.get_class().get_name() if tex else 'MISSING'}") mat_path = f"{DEST}/M_Character_Export"