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')