Clothes and hairs

This commit is contained in:
2026-05-30 20:04:07 +03:00
parent a553621c7f
commit 765dffbed0
14 changed files with 331 additions and 72 deletions
+49 -41
View File
@@ -62,6 +62,7 @@ add_custom_command(
DEPENDS ${CMAKE_SOURCE_DIR}/assets/blender/scripts/export_models2.py
${VRM_IMPORTED_BLENDS}
${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend
# ${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-hair.stamp
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
VERBATIM
@@ -418,12 +419,28 @@ 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-male-feet.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-top.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-bottom.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-female-feet.blend)
set(SEX_LIST "male" "female")
foreach(SEX ${SEX_LIST})
set(HAIR_WEIGHTED_STAMP "${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-hair_weighted.stamp")
add_custom_command(
OUTPUT ${HAIR_WEIGHTED_STAMP}
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-hair.blend
${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py
${CMAKE_CURRENT_BINARY_DIR}/blender-addons-installed
COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_CURRENT_BINARY_DIR}/clothes
COMMAND ${BLENDER} -b -Y -P ${CMAKE_CURRENT_SOURCE_DIR}/process_clothes.py --
${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-hair.blend
./ ${CMAKE_CURRENT_BINARY_DIR}/clothes
COMMAND ${CMAKE_COMMAND} -E touch ${HAIR_WEIGHTED_STAMP}
COMMAND ${CMAKE_COMMAND} -D FILE=${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-${SEX}-hair_weighted.blend
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/check_file_size.cmake
COMMENT "Processing hair meshes (weight transfer)"
)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-top.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-bottom.blend)
weight_clothes(${CMAKE_CURRENT_SOURCE_DIR}/clothes-${SEX}-feet.blend)
endforeach()
# Propagate missing shape keys (like "fat") from Body_shapes to base body parts.
# This ensures all body part meshes have consistent shape keys, preventing seams.
@@ -452,40 +469,31 @@ add_shape_key_propagation(
)
# male
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-shapes.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-top.blend" # FINAL_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated-top.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-feet_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-male-consolidated.blend" # FINAL_OUTPUT_BLEND
)
set(SLOT_LIST "bottom" "top" "feet" "hair")
set(SLOT_INPUT "edited-normal-male-shapes.blend")
list(GET SLOT_LIST -1 LAST_SLOT)
foreach (SLOT ${SLOT_LIST})
set(SLOT_OUTPUT "edited-normal-male-consolidated-${SLOT}.blend")
if (SLOT STREQUAL LAST_SLOT)
set(SLOT_OUTPUT "edited-normal-male-consolidated.blend")
endif()
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-male-${SLOT}_weighted.blend"
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
)
set(SLOT_INPUT ${SLOT_OUTPUT})
endforeach()
# female
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-shapes.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-bottom_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # FINAL_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-bottom.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-top_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # FINAL_OUTPUT_BLEND
)
add_clothes_pipeline(
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated-top.blend" # INPUT_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-feet_weighted.blend" # WEIGHTED_BLEND
"${CMAKE_CURRENT_BINARY_DIR}/edited-normal-female-consolidated.blend" # FINAL_OUTPUT_BLEND
)
set(SLOT_INPUT "edited-normal-female-shapes.blend")
foreach (SLOT ${SLOT_LIST})
set(SLOT_OUTPUT "edited-normal-female-consolidated-${SLOT}.blend")
if (SLOT STREQUAL LAST_SLOT)
set(SLOT_OUTPUT "edited-normal-female-consolidated.blend")
endif()
add_clothes_pipeline("${CMAKE_CURRENT_BINARY_DIR}/${SLOT_INPUT}"
"${CMAKE_CURRENT_BINARY_DIR}/clothes/clothes-female-${SLOT}_weighted.blend"
"${CMAKE_CURRENT_BINARY_DIR}/${SLOT_OUTPUT}"
)
set(SLOT_INPUT ${SLOT_OUTPUT})
endforeach()
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -203,8 +203,41 @@ def process_clothing_pair(clothing_obj, target_obj, whitelist, is_clothing_copy=
clothing_obj.select_set(True)
new_target.select_set(True)
bpy.context.view_layer.objects.active = new_target
# Remember clothing's material BEFORE join so we can verify
# after Blender potentially remaps faces to a stale copy.
clothing_mat = clothing_obj.active_material
bpy.ops.object.join()
# ---- Fix: ensure clothing faces keep the clothing's material ----
# After join, Blender may have assigned clothing faces to a
# pre-existing slot with the same base name but older texture
# references (loaded from the body blend). Find the slot that
# actually holds the clothing's original material data block
# and reassign any faces that landed in a stale look-alike slot.
if clothing_mat:
clothing_slot = None
stale_slot = None
for i, slot in enumerate(new_target.material_slots):
if slot.material == clothing_mat:
clothing_slot = i
elif (slot.material and
slot.material != clothing_mat and
slot.material.name == clothing_mat.name):
# Same name, different data block -> stale
stale_slot = i
if clothing_slot is not None and stale_slot is not None:
mesh = new_target.data
fixed = 0
for poly in mesh.polygons:
if poly.material_index == stale_slot:
poly.material_index = clothing_slot
fixed += 1
if fixed:
print(f" Fixed {fixed} faces: stale mat slot "
f"{stale_slot} -> clothing slot {clothing_slot}")
# ----------------------------------------------------------------
# NEW CODE: Copy stored custom properties to combined object
# After joining, new_target is the active object and contains the combined mesh
for prop_name, prop_value in clothing_props.items():
+38 -9
View File
@@ -10,27 +10,57 @@ from transfer_shape_keys import fix_seams_across_objects, fix_normals_across_obj
def process_append(source_files, output_path):
required_props = {"age", "sex", "slot"}
for file_path in source_files:
if not os.path.exists(file_path):
print(f"Warning: Source file not found: {file_path}")
continue
# ---- Pre-import materials from source blend ----
# Blender silently reuses existing materials when appending
# objects, even if the incoming one has different properties
# (texture references). Force materials to be imported first
# so texture changes in the source .blend survive.
with bpy.data.libraries.load(file_path, link=False) as (data_from, data_to):
data_to.materials = data_from.materials
imported_materials = {}
for mat in data_to.materials:
if mat is None:
continue
existing = bpy.data.materials.get(mat.name)
if existing and existing != mat:
print(f" Material '{mat.name}' already exists with "
f"different data; incoming material will "
f"replace it on appended objects.")
imported_materials[mat.name] = mat
# ----------------------------------------------------
with bpy.data.libraries.load(file_path) as (data_from, data_to):
data_to.objects = data_from.objects
for obj in data_to.objects:
if obj is None: continue
# Check criteria
has_props = all(p in obj.keys() for p in required_props)
if obj.type == 'MESH' and has_props:
# 1. Link to the scene root temporarily
bpy.context.collection.objects.link(obj)
# ---- Remap material slots to imported versions ----
for slot in obj.material_slots:
if slot.material and slot.material.name in imported_materials:
imported = imported_materials[slot.material.name]
if imported != slot.material:
print(f" Remapping slot "
f"'{slot.material.name}' -> "
f"'{imported.name}' on '{obj.name}'")
slot.material = imported
# ---------------------------------------------------
# 2. Synchronize Names
obj.data.name = obj.name
# 3. Find Target Armature
arm_name = obj.get("sex")
arm_obj = bpy.data.objects.get(arm_name)
@@ -40,19 +70,19 @@ def process_append(source_files, output_path):
# Remove from all current collections first
for col in obj.users_collection:
col.objects.unlink(obj)
# Link to every collection the armature belongs to
for col in arm_obj.users_collection:
col.objects.link(obj)
# B. Parent to Armature
obj.parent = arm_obj
# C. Handle Armature Modifier
arm_mod = next((m for m in obj.modifiers if m.type == 'ARMATURE'), None)
if not arm_mod:
arm_mod = obj.modifiers.new(name="Armature", type='ARMATURE')
arm_mod.object = arm_obj
print(f"Processed {obj.name}: Parented and Modset to {arm_name}")
else:
@@ -90,4 +120,3 @@ if __name__ == "__main__":
process_append(sources, output)
except ValueError:
print("Error: Use '--' to separate Blender args from script args.")
Binary file not shown.
Binary file not shown.
+86 -3
View File
@@ -355,10 +355,27 @@ for mapping in[CommandLineMapping()]:
for slot in obj.material_slots:
if slot.material:
mat = slot.material
# 3. Check if material already has the prefix
# 3. Normalize material name: strip Blender auto-suffixes
# (.001,.002 from name collisions, .### from dupes)
# before applying the armature prefix. This prevents
# duplicate .material files with unpredictable names
# (e.g. male_male-clothes-ed.001 vs male_male-clothes-ed)
# that cause submeshes to reference missing materials
# at runtime.
import re
clean = re.sub(r'\.\d{3}$', '', mat.name)
if clean != mat.name:
old = mat.name
mat.name = clean
# If Blender added .001 again because the clean
# name is already taken, keep the suffixed name.
print(f"Normalized material '{old}' -> '{mat.name}'"
f" on object '{name}'")
# 4. Check if material already has the prefix
if not mat.name.startswith(prefix):
mat.name = prefix + mat.name
print(f"Renamed material '{mat.name}' on object '{name}'")
print(f"Renamed material '{mat.name}'"
f" on object '{name}'")
# 3. Export custom properties to json
save_data = {}
for key in obj.keys():
@@ -446,15 +463,81 @@ for mapping in[CommandLineMapping()]:
armobj = bpy.data.objects.get(mapping.armature_name)
armobj.data.name = armobj.name
# ---- Fix: sync Image filepath to Image name ----
# Blender silently reuses existing Images when appending
# from libraries, so the Image's filepath may still point
# to the old texture even after the user renamed it.
# blender2ogre writes the filepath to .material files,
# so we must update filepath to match the Image name.
import re as _re
seen_imgs = set()
for name in obj_names:
obj = bpy.data.objects.get(name)
if obj and obj.type == 'MESH':
for slot in obj.material_slots:
if slot.material and slot.material.node_tree:
for node in slot.material.node_tree.nodes:
if node.type == 'TEX_IMAGE' and node.image:
img = node.image
if img.name not in seen_imgs:
seen_imgs.add(img.name)
# Strip Blender auto-suffix (.NNN)
clean = _re.sub(
r'\.\d{3}$', '', img.name)
# Extract just the filename from
# the current filepath
old_name = os.path.basename(
img.filepath)
if old_name != clean and clean:
# Rebuild filepath with new
# filename
dirpart = os.path.dirname(
img.filepath)
new_path = os.path.join(
dirpart, clean)
print(f" Updating Image "
f"'{img.name}': "
f"filepath "
f"'{old_name}' -> "
f"'{clean}'")
img.filepath = new_path
# ---------------------------------------------------
bpy.ops.ogre.export(filepath=mapping.gltf_path.replace(".glb", ".scene"), EX_SELECTED_ONLY=False, EX_SHARED_ARMATURE=True, EX_LOD_GENERATION='0', EX_LOD_DISTANCE=20, EX_LOD_LEVELS=4, EX_GENERATE_TANGENTS='4')
ogre_export_dir = os.path.dirname(mapping.gltf_path.replace(".glb", ".scene"))
# Fix alpha_rejection values in ALL generated .material files.
# blender2ogre 0.9.0 writes float values (e.g. "127.5") for
# alpha_rejection thresholds, but OGRE 14 expects an integer
# in the range 0-255. Float values are truncated to integer
# during material compilation, causing 127.5 -> 127 when 128
# was intended. This makes submeshes with alpha < 128 in the
# texture atlas completely invisible.
import glob as _glob
_mat_files = _glob.glob(os.path.join(ogre_export_dir, "*.material"))
for _mf in _mat_files:
with open(_mf, 'r') as _f:
_content = _f.read()
if 'alpha_rejection' in _content:
import re as _re
_new = _re.sub(
r'alpha_rejection (\S+) (\d+\.?\d*)',
lambda m: 'alpha_rejection ' + m.group(1) +
' ' + str(int(float(m.group(2)))),
_content)
if _new != _content:
with open(_mf, 'w') as _f:
_f.write(_new)
print(f" Fixed alpha_rejection in {_mf}")
# Post-process exported OGRE meshes to fix normal/tangent seams between
# body parts. Even with identical custom normals in Blender, the OGRE
# exporter's calc_tangents() produces slightly different tangents for
# matching vertices because BodyTop and BodyBottom have different face
# topologies. This causes a visible lighting seam with normal mapping,
# especially when shape keys (like "fat") are applied.
ogre_export_dir = os.path.dirname(mapping.gltf_path.replace(".glb", ".scene"))
fix_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fix_ogre_mesh_seams.py")
if os.path.exists(fix_script):
print(f"\nPost-processing OGRE meshes in {ogre_export_dir}...")
@@ -7,10 +7,11 @@
/**
* Selection criteria for a single character slot.
* Layer 0 (nude base) is always implicit.
* Layer 0 (nude base) can be selected via combo box (e.g. hair styles).
* Layer 1 and 2 are selected via combo boxes.
*/
struct SlotSelection {
Ogre::String layer0Mesh; // "none" or exact mesh name for layer 0
Ogre::String layer1Mesh; // "none" or exact mesh name for layer 1
Ogre::String layer2Mesh; // "none" or exact mesh name for layer 2
Ogre::String explicitMesh; // backward-compat override
@@ -301,6 +301,8 @@ readPrefabAppearance(const std::string &path, CharacterSlotsComponent &cs,
for (auto &[slot, selJson] :
s["slotSelections"].items()) {
SlotSelection sel;
sel.layer0Mesh =
selJson.value("layer0Mesh", "");
sel.layer1Mesh =
selJson.value("layer1Mesh", "");
sel.layer2Mesh =
@@ -1207,6 +1209,7 @@ nlohmann::json CharacterRegistry::serialize() const
nlohmann::json selJson;
for (const auto &kv : c.inlineSlotSelections) {
nlohmann::json s;
s["layer0Mesh"] = kv.second.layer0Mesh;
s["layer1Mesh"] = kv.second.layer1Mesh;
s["layer2Mesh"] = kv.second.layer2Mesh;
s["explicitMesh"] = kv.second.explicitMesh;
@@ -1393,6 +1396,8 @@ void CharacterRegistry::deserialize(const nlohmann::json &j)
for (auto &[slot, s] :
rec["inlineSlotSelections"].items()) {
SlotSelection sel;
sel.layer0Mesh =
s.value("layer0Mesh", "");
sel.layer1Mesh =
s.value("layer1Mesh", "");
sel.layer2Mesh =
@@ -182,13 +182,34 @@ Ogre::String CharacterSlotSystem::getMeshLabel(const Ogre::String &age,
if (entry.value("mesh", "") == mesh) {
const auto &garments = entry.value(
"garments", nlohmann::json::array());
if (garments.empty())
return "nude";
Ogre::String label;
for (size_t i = 0; i < garments.size(); ++i) {
if (i > 0)
label += " + ";
label += garments[i].get<std::string>();
if (!garments.empty()) {
Ogre::String label;
for (size_t i = 0; i < garments.size(); ++i) {
if (i > 0)
label += " + ";
label += garments[i].get<std::string>();
}
return label;
}
/* For non-garment slots (hair, face, etc.), derive
* a human-readable label from the mesh filename */
Ogre::String label = mesh;
/* Strip directory prefix */
auto slashPos = label.rfind('/');
if (slashPos != Ogre::String::npos)
label = label.substr(slashPos + 1);
/* Strip .mesh extension */
auto dotPos = label.rfind(".mesh");
if (dotPos != Ogre::String::npos)
label = label.substr(0, dotPos);
/* Strip sex prefix (male_/female_) */
Ogre::String sexPrefix = sex + "_";
if (label.find(sexPrefix) == 0)
label = label.substr(sexPrefix.length());
/* Replace underscores with spaces */
for (auto &c : label) {
if (c == '_')
c = ' ';
}
return label;
}
@@ -270,6 +291,14 @@ Ogre::String CharacterSlotSystem::resolveMesh(const Ogre::String &age,
const auto &slotEntries = s_bodyParts[age][sex][slot];
/* If layer 0 is explicitly selected, use it */
if (sel.layer0Mesh != "none" && !sel.layer0Mesh.empty()) {
for (const auto &entry : slotEntries) {
if (entry.value("mesh", "") == sel.layer0Mesh)
return sel.layer0Mesh;
}
}
/* outfitLevel: 0=nude, 1=lingerie, 2=clothed */
if (outfitLevel >= 2 && sel.layer2Mesh != "none" &&
!sel.layer2Mesh.empty()) {
@@ -550,6 +579,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
*/
prepareEntityTempBlendBuffers(masterEnt);
/* Notify AnimationTreeSystem that entity changed */
if (e.has<AnimationTreeComponent>())
e.get_mut<AnimationTreeComponent>().dirty = true;
@@ -572,6 +602,9 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
if (mesh.empty())
continue;
Ogre::LogManager::getSingleton().logMessage(
"[CharacterSlotSystem] slot='" + slot +
try {
ensureMeshPoseAnimation(mesh);
Ogre::MeshPtr partMesh =
@@ -593,6 +626,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
* proper per-entity pose animation.
*/
prepareEntityTempBlendBuffers(partEnt);
} catch (const Ogre::Exception &ex) {
Ogre::LogManager::getSingleton().logMessage(
"[CharacterSlotSystem] buildCharacter: FAILED to load part '" +
@@ -603,7 +637,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
}
void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
const nlohmann::json *entry)
const nlohmann::json *entry)
{
if (!ent || !entry)
return;
@@ -659,8 +693,8 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
Ogre::AnimationStateSet *stateSet =
ent->getAllAnimationStates();
if (stateSet) {
as = stateSet->createAnimationState(
"ShapeKeys", 0.0, 1.0, 1.0, false);
as = stateSet->createAnimationState("ShapeKeys", 0.0,
1.0, 1.0, false);
} else {
Ogre::LogManager::getSingleton().logMessage(
"[CharacterSlotSystem] applyShapeKeys: entity '" +
@@ -701,8 +735,8 @@ void CharacterSlotSystem::applyShapeKeys(flecs::entity e, Ogre::Entity *ent,
t < anim->getNumVertexTracks(); ++t) {
Ogre::VertexAnimationTrack *track =
anim->getVertexTrack(t);
if (!track || track->getAnimationType() !=
Ogre::VAT_POSE)
if (!track ||
track->getAnimationType() != Ogre::VAT_POSE)
continue;
Ogre::VertexPoseKeyFrame *kf =
track->getVertexPoseKeyFrame(0);
@@ -2209,6 +2209,7 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
nlohmann::json selections = nlohmann::json::object();
for (const auto &pair : cs.slotSelections) {
nlohmann::json sel;
sel["layer0Mesh"] = pair.second.layer0Mesh;
sel["layer1Mesh"] = pair.second.layer1Mesh;
sel["layer2Mesh"] = pair.second.layer2Mesh;
sel["explicitMesh"] = pair.second.explicitMesh;
@@ -2236,6 +2237,9 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
json["slotSelections"].is_object()) {
for (auto &[slot, selJson] : json["slotSelections"].items()) {
SlotSelection sel;
if (selJson.contains("layer0Mesh"))
sel.layer0Mesh =
selJson.value("layer0Mesh", "");
if (selJson.contains("layer1Mesh"))
sel.layer1Mesh =
selJson.value("layer1Mesh", "");
@@ -220,6 +220,62 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity,
ImGui::EndCombo();
}
} else {
/* Layer 0 combo (base meshes like hair) */
std::vector<Ogre::String> layer0Meshes =
CharacterSlotSystem::
getMeshesForLayer(
currentAge,
cs.sex, slot,
0);
if (!layer0Meshes.empty()) {
Ogre::String l0Preview = "auto";
if (!sel.layer0Mesh.empty() &&
sel.layer0Mesh != "none")
l0Preview = CharacterSlotSystem::
getMeshLabel(
currentAge,
cs.sex,
slot,
sel.layer0Mesh);
if (ImGui::BeginCombo(
"Base (Layer 0)",
l0Preview.c_str())) {
if (ImGui::Selectable(
"auto",
sel.layer0Mesh.empty() ||
sel.layer0Mesh ==
"none")) {
sel.layer0Mesh =
"none";
modified = true;
cs.dirty = true;
}
for (const auto &m :
layer0Meshes) {
Ogre::String label = CharacterSlotSystem::
getMeshLabel(
currentAge,
cs.sex,
slot,
m);
bool isSelected =
(sel.layer0Mesh ==
m);
if (ImGui::Selectable(
label.c_str(),
isSelected)) {
sel.layer0Mesh =
m;
modified =
true;
cs.dirty =
true;
}
}
ImGui::EndCombo();
}
}
/* Layer 1 combo */
std::vector<Ogre::String> layer1Meshes =
CharacterSlotSystem::