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
@@ -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::