Pipeline update

This commit is contained in:
2026-05-10 14:43:57 +03:00
parent 333a0b9938
commit ce888bc5bb
9 changed files with 552 additions and 23 deletions
+7 -4
View File
@@ -47,15 +47,12 @@ set(VRM_IMPORTED_BLENDS
# COMMAND ${CMAKE_COMMAND} -E touch_nocreate ${CHARACTER_GLBS}
# DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models.py ${VRM_IMPORTED_BLENDS} ${EDITED_BLEND_TARGETS}
# WORKING_DIRECTORY ${CMAKE_BINARY_DIR})
set(FEMALE_OBJECTS "BodyTopRobe;BodyTop;BodyBottom;BodyFeet;Hair;Face;BackHair;Accessoty")
set(MALE_OBJECTS "BodyTopRobe;BodyTop;BodyBottomPants;BodyBottom_Panties001;BodyBottom;BodyFeetPants;BodyFeetPantsShoes;BodyFeet;Hair;Face;BackHair;Accessory")
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
COMMAND ${CMAKE_COMMAND} -E make_directory ${CREATE_DIRECTORIES}
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend
${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
"${MALE_OBJECTS}"
"male"
tmp-edited-male.blend
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/male/normal-male.glb
@@ -76,7 +73,6 @@ add_custom_command(
COMMAND ${BLENDER} -b -Y -P ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py --
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend
${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
"${FEMALE_OBJECTS}"
"female"
tmp-edited-female.blend
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_BINARY_DIR}/characters/female/normal-female.glb
@@ -422,12 +418,19 @@ function(add_clothes_pipeline INPUT_BLEND WEIGHTED_BLEND FINAL_OUTPUT_BLEND)
)
endfunction()
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-top.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-male-bottom.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend)
add_clothes_pipeline(
"${CMAKE_CURRENT_SOURCE_DIR}/edited-normal-male.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-bottom_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # BOTTOM_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-bottom.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-top_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND
)
Binary file not shown.
Binary file not shown.
Binary file not shown.
+54 -1
View File
@@ -2,6 +2,7 @@ import bpy
import bmesh
import sys
import os
import json
import mathutils
from mathutils.bvhtree import BVHTree
@@ -158,6 +159,13 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
clothing_props[prop_name] = clothing_obj[prop_name]
print(f" Stored property '{prop_name}' = {clothing_obj[prop_name]} from clothing")
# Also store metadata tags for export pipeline
clothing_meta = {}
for prop_name in ["ref_layer", "ref_part", "garment_id", "tags", "age", "sex", "slot"]:
if prop_name in clothing_obj:
clothing_meta[prop_name] = clothing_obj[prop_name]
print(f" Stored meta '{prop_name}' = {clothing_obj[prop_name]} from clothing")
# Step A: Raycast & adjust vertices
out_ray_length = 0.015
if "ref_ray_length" in clothing_obj:
@@ -204,6 +212,26 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
new_target[prop_name] = prop_value
print(f" Copied property '{prop_name}' = {prop_value} to combined object")
# NEW CODE: Aggregate clothing metadata onto combined object
# Collect from all clothing objects processed for this target
# Use JSON string since Blender ID properties don't support Python lists/sets
if "_combined_meta" in new_target:
meta = json.loads(new_target["_combined_meta"])
else:
meta = {"garments": [], "tags": [], "layers": []}
if "garment_id" in clothing_meta:
meta["garments"].append(clothing_meta["garment_id"])
else:
meta["garments"].append(clothing_name_for_final)
if "tags" in clothing_meta and clothing_meta["tags"]:
for t in str(clothing_meta["tags"]).split(";"):
t = t.strip()
if t and t not in meta["tags"]:
meta["tags"].append(t)
if "ref_layer" in clothing_meta:
meta["layers"].append(int(clothing_meta["ref_layer"]))
new_target["_combined_meta"] = json.dumps(meta)
# Rename the combined object using the appropriate clothing name
new_target.name = f"{target_name}_{clothing_name_for_final}"
whitelist.add(new_target)
@@ -349,9 +377,34 @@ def run_batch_combine():
print(f"Layer 2 direct to body results: {len(layer2_results_direct)}")
print(f"Total combined objects: {len(all_results)}")
# Final cleanup - keep all combined objects and their armatures
# Finalize aggregated metadata on all combined objects
for obj in all_results:
whitelist.add(obj)
if "_combined_meta" in obj:
meta = json.loads(obj["_combined_meta"])
layers = meta.get("layers", [])
garments = meta.get("garments", [])
tags = meta.get("tags", [])
if layers:
obj["layer"] = max(layers)
else:
obj["layer"] = 0
if garments:
obj["garments"] = ";".join(garments)
else:
obj["garments"] = ""
if tags:
obj["clothing_tags"] = ";".join(sorted(set(tags)))
else:
obj["clothing_tags"] = ""
print(f"Finalized metadata for {obj.name}: layer={obj['layer']}, garments={obj['garments']}, tags={obj['clothing_tags']}")
# Remove temporary meta property
del obj["_combined_meta"]
cleanup_unused_objects(whitelist)
@@ -0,0 +1,397 @@
#!/usr/bin/env python3
"""
Garment System UI Panel for Blender
Run this script in Blender's Scripting editor (or via blender -b -P garment_system_ui.py)
to add a side-panel for editing garment-system custom properties on mesh objects.
Properties managed:
Body Part -> age, sex, slot, layer, garments, clothing_tags
Clothing -> ref_layer, ref_part, garment_id, ref_sex, ref_age, ref_clothing, tags
"""
import bpy
# ---------------------------------------------------------------------------
# Property helpers
# ---------------------------------------------------------------------------
SLOT_ITEMS = [
("face", "Face", ""),
("bottom", "Bottom", ""),
("top", "Top", ""),
("feet", "Feet", ""),
("hair", "Hair", ""),
]
SEX_ITEMS = [
("male", "Male", ""),
("female", "Female", ""),
]
AGE_ITEMS = [
("adult", "Adult", ""),
("child", "Child", ""),
("teen", "Teen", ""),
("baby", "Baby", ""),
]
def _ensure_prop(obj, key, default):
if key not in obj:
obj[key] = default
return obj[key]
def _get_prop_str(obj, key, default=""):
if key not in obj:
return default
return str(obj[key])
def _get_prop_int(obj, key, default=0):
if key not in obj:
return default
try:
return int(obj[key])
except (ValueError, TypeError):
return default
def _set_prop_str(obj, key, value):
if value:
obj[key] = value
elif key in obj:
del obj[key]
def _set_prop_int(obj, key, value):
obj[key] = int(value)
def _del_prop(obj, key):
if key in obj:
del obj[key]
# ---------------------------------------------------------------------------
# Operators
# ---------------------------------------------------------------------------
class GARMENT_OT_apply_body_part(bpy.types.Operator):
bl_idname = "garment.apply_body_part"
bl_label = "Apply Body Part"
bl_description = "Write body-part custom properties to the active object"
bl_options = {"REGISTER", "UNDO"}
age: bpy.props.EnumProperty(name="Age", items=AGE_ITEMS)
sex: bpy.props.EnumProperty(name="Sex", items=SEX_ITEMS)
slot: bpy.props.EnumProperty(name="Slot", items=SLOT_ITEMS)
layer: bpy.props.IntProperty(name="Layer", min=0, max=2, default=0)
garments: bpy.props.StringProperty(name="Garments", description="Semicolon-separated garment names")
clothing_tags: bpy.props.StringProperty(name="Clothing Tags", description="Semicolon-separated tags")
def execute(self, context):
obj = context.active_object
if not obj or obj.type != "MESH":
self.report({"WARNING"}, "Active object is not a mesh")
return {"CANCELLED"}
_set_prop_str(obj, "age", self.age)
_set_prop_str(obj, "sex", self.sex)
_set_prop_str(obj, "slot", self.slot)
_set_prop_int(obj, "layer", self.layer)
_set_prop_str(obj, "garments", self.garments)
_set_prop_str(obj, "clothing_tags", self.clothing_tags)
# Remove clothing-only props so they don't confuse the pipeline
for k in ("ref_layer", "ref_part", "garment_id", "ref_sex",
"ref_age", "ref_clothing", "tags"):
_del_prop(obj, k)
self.report({"INFO"}, f"Body-part props applied to {obj.name}")
return {"FINISHED"}
def invoke(self, context, event):
obj = context.active_object
if obj:
self.age = _get_prop_str(obj, "age", "adult")
self.sex = _get_prop_str(obj, "sex", "male")
self.slot = _get_prop_str(obj, "slot", "bottom")
self.layer = _get_prop_int(obj, "layer", 0)
self.garments = _get_prop_str(obj, "garments", "")
self.clothing_tags = _get_prop_str(obj, "clothing_tags", "")
return context.window_manager.invoke_props_dialog(self)
class GARMENT_OT_apply_clothing(bpy.types.Operator):
bl_idname = "garment.apply_clothing"
bl_label = "Apply Clothing"
bl_description = "Write clothing custom properties to the active object"
bl_options = {"REGISTER", "UNDO"}
ref_layer: bpy.props.IntProperty(name="Ref Layer", min=1, max=2, default=1,
description="1 = lingerie, 2 = clothing")
ref_part: bpy.props.StringProperty(name="Ref Part",
description="Target body-part object name, e.g. BodyBottom")
garment_id: bpy.props.StringProperty(name="Garment ID",
description="Garment name used for labels, e.g. panties7")
ref_sex: bpy.props.EnumProperty(name="Ref Sex", items=SEX_ITEMS)
ref_age: bpy.props.EnumProperty(name="Ref Age", items=AGE_ITEMS)
ref_clothing: bpy.props.StringProperty(name="Ref Clothing",
description="Mesh name used for weight transfer, e.g. BodyBottom")
tags: bpy.props.StringProperty(name="Tags", description="Semicolon-separated tags")
def execute(self, context):
obj = context.active_object
if not obj or obj.type != "MESH":
self.report({"WARNING"}, "Active object is not a mesh")
return {"CANCELLED"}
_set_prop_int(obj, "ref_layer", self.ref_layer)
_set_prop_str(obj, "ref_part", self.ref_part)
_set_prop_str(obj, "garment_id", self.garment_id)
_set_prop_str(obj, "ref_sex", self.ref_sex)
_set_prop_str(obj, "ref_age", self.ref_age)
_set_prop_str(obj, "ref_clothing", self.ref_clothing)
_set_prop_str(obj, "tags", self.tags)
# Remove body-part-only props so they don't confuse the pipeline
for k in ("age", "sex", "slot", "layer", "garments", "clothing_tags"):
_del_prop(obj, k)
self.report({"INFO"}, f"Clothing props applied to {obj.name}")
return {"FINISHED"}
def invoke(self, context, event):
obj = context.active_object
if obj:
self.ref_layer = _get_prop_int(obj, "ref_layer", 1)
self.ref_part = _get_prop_str(obj, "ref_part", "")
self.garment_id = _get_prop_str(obj, "garment_id", "")
self.ref_sex = _get_prop_str(obj, "ref_sex", "male")
self.ref_age = _get_prop_str(obj, "ref_age", "adult")
self.ref_clothing = _get_prop_str(obj, "ref_clothing", "")
self.tags = _get_prop_str(obj, "tags", "")
return context.window_manager.invoke_props_dialog(self)
class GARMENT_OT_auto_detect(bpy.types.Operator):
bl_idname = "garment.auto_detect"
bl_label = "Auto-detect from Name"
bl_description = "Guess sex/slot from object name"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj or obj.type != "MESH":
self.report({"WARNING"}, "Active object is not a mesh")
return {"CANCELLED"}
name = obj.name.lower()
changed = []
# Guess sex
if "female" in name:
obj["sex"] = "female"
changed.append("sex=female")
elif "male" in name:
obj["sex"] = "male"
changed.append("sex=male")
# Guess slot
slot_map = {
"face": "face",
"bottom": "bottom",
"top": "top",
"feet": "feet",
"hair": "hair",
}
for key, slot in slot_map.items():
if key in name:
obj["slot"] = slot
changed.append(f"slot={slot}")
break
# Guess layer from name hints
if any(x in name for x in ("panties", "lingerie", "underwear")):
obj["layer"] = 1
changed.append("layer=1")
elif any(x in name for x in ("pants", "skirt", "robe", "shirt", "dress", "cloth")):
obj["layer"] = 2
changed.append("layer=2")
else:
obj["layer"] = 0
changed.append("layer=0")
# Set garment_id from object name for clothing
if "ref_layer" in obj or "ref_part" in obj:
if "garment_id" not in obj or not obj["garment_id"]:
obj["garment_id"] = obj.name
changed.append(f"garment_id={obj.name}")
self.report({"INFO"}, "Auto-detect: " + ", ".join(changed) if changed else "nothing changed")
return {"FINISHED"}
class GARMENT_OT_clear_props(bpy.types.Operator):
bl_idname = "garment.clear_props"
bl_label = "Clear All Garment Props"
bl_description = "Remove all garment-system custom properties from the active object"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
keys = list(obj.keys())
removed = []
for k in keys:
if k in ("age", "sex", "slot", "layer", "garments", "clothing_tags",
"ref_layer", "ref_part", "garment_id", "ref_sex", "ref_age",
"ref_clothing", "tags"):
del obj[k]
removed.append(k)
self.report({"INFO"}, f"Removed {len(removed)} props" if removed else "No garment props found")
return {"FINISHED"}
class GARMENT_OT_sync_tags(bpy.types.Operator):
bl_idname = "garment.sync_tags"
bl_label = "Sync Tags from Garments"
bl_description = "Copy clothing_tags from garments field"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
garments = _get_prop_str(obj, "garments", "")
obj["clothing_tags"] = garments
self.report({"INFO"}, f"Tags set to: {garments}")
return {"FINISHED"}
# ---------------------------------------------------------------------------
# Panel
# ---------------------------------------------------------------------------
class GARMENT_PT_panel(bpy.types.Panel):
bl_label = "Garment System"
bl_idname = "GARMENT_PT_panel"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "Garment"
def draw(self, context):
layout = self.layout
obj = context.active_object
if not obj:
layout.label(text="No active object")
return
layout.label(text=f"Object: {obj.name}", icon="OBJECT_DATA")
# Detect current type
is_body = any(k in obj for k in ("age", "sex", "slot"))
is_cloth = any(k in obj for k in ("ref_layer", "ref_part", "garment_id"))
if is_body and is_cloth:
layout.alert = True
layout.label(text="WARNING: mixed props", icon="ERROR")
layout.alert = False
elif is_body:
layout.label(text="Type: Body Part", icon="MESH_DATA")
elif is_cloth:
layout.label(text="Type: Clothing", icon="MOD_CLOTH")
else:
layout.label(text="Type: (unset)", icon="QUESTION")
layout.separator()
# Quick action buttons
row = layout.row(align=True)
row.operator("garment.apply_body_part", icon="MESH_DATA")
row = layout.row(align=True)
row.operator("garment.apply_clothing", icon="MOD_CLOTH")
layout.separator()
row = layout.row(align=True)
row.operator("garment.auto_detect", icon="VIEWZOOM")
row.operator("garment.clear_props", icon="X")
layout.separator()
# Show current body-part props
if is_body or not is_cloth:
box = layout.box()
box.label(text="Body Part Props", icon="MESH_DATA")
col = box.column(align=True)
col.label(text=f"age: {_get_prop_str(obj, 'age', '')}")
col.label(text=f"sex: {_get_prop_str(obj, 'sex', '')}")
col.label(text=f"slot: {_get_prop_str(obj, 'slot', '')}")
col.label(text=f"layer: {_get_prop_int(obj, 'layer', 0)}")
col.label(text=f"garments: {_get_prop_str(obj, 'garments', '')}")
col.label(text=f"clothing_tags: {_get_prop_str(obj, 'clothing_tags', '')}")
if _get_prop_str(obj, "garments", ""):
col.operator("garment.sync_tags", icon="FILE_REFRESH")
# Show current clothing props
if is_cloth:
box = layout.box()
box.label(text="Clothing Props", icon="MOD_CLOTH")
col = box.column(align=True)
col.label(text=f"ref_layer: {_get_prop_int(obj, 'ref_layer', 1)}")
col.label(text=f"ref_part: {_get_prop_str(obj, 'ref_part', '')}")
col.label(text=f"garment_id: {_get_prop_str(obj, 'garment_id', '')}")
col.label(text=f"ref_sex: {_get_prop_str(obj, 'ref_sex', '')}")
col.label(text=f"ref_age: {_get_prop_str(obj, 'ref_age', '')}")
col.label(text=f"ref_clothing: {_get_prop_str(obj, 'ref_clothing', '')}")
col.label(text=f"tags: {_get_prop_str(obj, 'tags', '')}")
layout.separator()
# Scene overview
box = layout.box()
box.label(text="Scene Overview", icon="SCENE_DATA")
body_count = 0
cloth_count = 0
for o in bpy.data.objects:
if o.type != "MESH":
continue
if any(k in o for k in ("age", "sex", "slot")):
body_count += 1
elif any(k in o for k in ("ref_layer", "ref_part", "garment_id")):
cloth_count += 1
col = box.column(align=True)
col.label(text=f"Body parts: {body_count}")
col.label(text=f"Clothing: {cloth_count}")
# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------
classes = [
GARMENT_OT_apply_body_part,
GARMENT_OT_apply_clothing,
GARMENT_OT_auto_detect,
GARMENT_OT_clear_props,
GARMENT_OT_sync_tags,
GARMENT_PT_panel,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()
@@ -122,6 +122,10 @@ def process_batch():
remove_empty_vertex_groups(clothing, threshold=0.001)
# Set garment_id from object name if not already set
if "garment_id" not in clothing:
clothing["garment_id"] = clothing.name
# 7. Final Parenting
clothing.parent = rig
arm_mod = clothing.modifiers.new(name="Armature", type='ARMATURE')
@@ -354,7 +354,11 @@ class OgreMaterialGenerator(object):
else:
image_filepath = bpy.path.abspath(image.filepath, library=image.library)
image_filepath = os.path.normpath(image_filepath)
if not os.path.isfile(image_filepath):
logger.warning("Skipping texture copy: source path is not a file (%s)", image_filepath)
return
# Should we update the file
update = False
if os.path.isfile(target_filepath):
+78 -13
View File
@@ -186,26 +186,32 @@ def extra_linear(angle, offset):
mapping_blend_path = argv[0]
mapping_gltf_path = argv[1]
mapping_objects = argv[2]
mapping_armature_name = argv[3]
mapping_outfile = argv[4]
# Backward compat: support both 4-arg (blend, gltf, armature, outfile) and 5-arg (blend, gltf, objects, armature, outfile)
if len(argv) >= 5:
mapping_objects = argv[2]
mapping_armature_name = argv[3]
mapping_outfile = argv[4]
else:
mapping_objects = "AUTO"
mapping_armature_name = argv[2]
mapping_outfile = argv[3]
#for mapping in [ExportMappingFemale(), ExportMappingMale(), ExportMappingMaleBabyShape(), ExportMappingMaleEdited(), ExportMappingFemaleEdited(), ExportMappingMaleTestShapeEdited(), ExportMappingMaleBaseShapeEdited()]:
class CommandLineMapping:
blend_path = mapping_blend_path
gltf_path = mapping_gltf_path
# ogre_scene = "characters/female/vroid-normal-female.scene"
inner_path = "Object"
# objs = ["male", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
# objs = ["female", "Body", "Hair", "Face", "BackHair", "Tops", "Bottoms", "Shoes", "Accessory"]
objs = []
armature_name = mapping_armature_name
outfile = mapping_outfile
default_action = 'default'
auto_discover = False
def __init__(self):
self.objs = [mapping_armature_name]
if len(mapping_objects) > 0:
if len(mapping_objects) > 0 and mapping_objects != "AUTO":
self.objs += [o.strip() for o in mapping_objects.split(";")]
else:
self.auto_discover = True
self.files = []
for fobj in self.objs:
self.files.append({"name": fobj})
@@ -224,11 +230,30 @@ for mapping in[CommandLineMapping()]:
bpy.app.driver_namespace["angle_to_linear_x"] = angle_to_linear_x
print("Driver setup done...")
bpy.ops.wm.append(
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
directory=os.path.join(mapping.blend_path, mapping.inner_path),
files=mapping.files)
print("Append done...")
if mapping.auto_discover:
# Load all objects from the blend file so we can discover meshes by custom props
with bpy.data.libraries.load(mapping.blend_path) as (data_from, data_to):
data_to.objects = data_from.objects
for obj in data_to.objects:
if obj is not None:
try:
bpy.context.collection.objects.link(obj)
except RuntimeError:
pass # Already linked
print("Library load done...")
discovered = []
for ob in bpy.data.objects:
if ob.type == 'MESH' and all(p in ob.keys() for p in ["age", "sex", "slot"]):
discovered.append(ob.name)
mapping.objs += discovered
print(f"Auto-discovered {len(discovered)} body part objects: {discovered}")
else:
bpy.ops.wm.append(
filepath=os.path.join(mapping.blend_path, mapping.inner_path),
directory=os.path.join(mapping.blend_path, mapping.inner_path),
files=mapping.files)
print("Append done...")
prepare_armature(mapping)
print("Armature done...")
@@ -239,6 +264,10 @@ for mapping in[CommandLineMapping()]:
bpy.data.objects.remove(ob)
elif ob.name.startswith("Face") and ob.name != "Face":
bpy.data.objects.remove(ob)
# Remove auto-discovered objects that weren't meant for export
elif mapping.auto_discover and ob.type == 'MESH' and ob.name not in mapping.objs:
if not ob.name.endswith("-noimp"):
ob.name = ob.name + "-noimp"
print("Removing original armature and actions...")
orig_arm = bpy.data.objects[mapping.armature_name + '_orig']
@@ -317,12 +346,48 @@ for mapping in[CommandLineMapping()]:
save_data[key] = obj[key]
if key.startswith("body_"):
save_data[key.replace("body_", "", 1)] = obj[key]
# Export aggregated clothing metadata
if "layer" in obj:
save_data["layer"] = int(obj["layer"])
else:
save_data["layer"] = 0
tags = []
if "garments" in obj and obj["garments"]:
garments = [g.strip() for g in str(obj["garments"]).split(";") if g.strip()]
save_data["garments"] = garments
tags.extend(garments)
else:
save_data["garments"] = []
if "clothing_tags" in obj and obj["clothing_tags"]:
clothing_tags = [t.strip() for t in str(obj["clothing_tags"]).split(";") if t.strip()]
tags.extend(clothing_tags)
# Deduplicate and sort
seen = set()
unique_tags = []
for t in tags:
t_lower = t.lower()
if t_lower not in seen:
seen.add(t_lower)
unique_tags.append(t)
save_data["tags"] = unique_tags
# Export shape keys if present
shape_keys = []
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
for sk in obj.data.shape_keys.key_blocks:
shape_keys.append(sk.name)
save_data["shape_keys"] = shape_keys
save_data["mesh"] = obj.data.name + ".mesh"
json_dir = os.path.dirname(mapping.gltf_path)
save_file = json_dir + "/body_part_" + obj.data.name + ".json"
json_filepath = os.path.join(json_dir, save_file)
with open(json_filepath, 'w') as f:
json.dump(save_data, f)
json.dump(save_data, f, indent=2)
armobj = bpy.data.objects.get(mapping.armature_name)
armobj.data.name = armobj.name