Pipeline update
This commit is contained in:
@@ -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.
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user