Clothes and hairs
This commit is contained in:
@@ -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::
|
||||
|
||||
Reference in New Issue
Block a user