/render: support for rendering out character and clothing sheets

This commit is contained in:
Kae 2025-01-01 19:41:42 +11:00
parent a589a41fb4
commit 5159b073bd
7 changed files with 208 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -441,10 +441,152 @@ String ClientCommandProcessor::respawnInWorld(String const& argumentsString) {
return strf("Respawn in this world set to {} (This is client-side!)", respawnInWorld ? "true" : "false"); return strf("Respawn in this world set to {} (This is client-side!)", respawnInWorld ? "true" : "false");
} }
// Temporary hardcoded render command for debugging purposes, future version will write to the clipboard // Hardcoded render command, future version will write to the clipboard and possibly be implemented in Lua
String ClientCommandProcessor::render(String const& imagePath) { String ClientCommandProcessor::render(String const& path) {
auto image = Root::singleton().assets()->image(imagePath); if (path.empty()) {
image->writePng(File::open("render.png", IOMode::Write)); return "Specify a path to render an image, or for worn armor: "
"^cyan;hat^reset;/^cyan;chest^reset;/^cyan;legs^reset;/^cyan;back^reset; "
"or for body parts: "
"^cyan;head^reset;/^cyan;body^reset;/^cyan;hair^reset;/^cyan;facialhair^reset;/"
"^cyan;facialmask^reset;/^cyan;frontarm^reset;/^cyan;backarm^reset;/^cyan;emote^reset;";
}
AssetPath assetPath;
bool outputSheet = false;
String outputName = "render";
auto player = m_universeClient->mainPlayer();
if (player && path.utf8Size() < 100) {
auto args = m_parser.tokenizeToStringList(path);
auto first = args.maybeFirst().value().toLower();
auto humanoid = player->humanoid();
auto& identity = humanoid->identity();
auto species = identity.imagePath.value(identity.species);
outputSheet = true;
outputName = first;
if (first.equals("hat")) {
assetPath.basePath = humanoid->headArmorFrameset();
assetPath.directives += humanoid->headArmorDirectives();
} else if (first.equals("chest")) {
if (args.size() <= 1) {
return "Chest armors have multiple spritesheets. Do: "
"^white;/chest torso ^cyan;front^reset;/^cyan;torso^reset;/^cyan;back^reset;. "
"To repair old generated clothes, then also specify ^cyan;old^reset;.";
}
String sheet = args[1].toLower();
outputName += " " + sheet;
if (sheet == "torso") {
assetPath.basePath = humanoid->chestArmorFrameset();
assetPath.directives += humanoid->chestArmorDirectives();
} else if (sheet == "front") {
assetPath.basePath = humanoid->frontSleeveFrameset();
assetPath.directives += humanoid->chestArmorDirectives();
} else if (sheet == "back") {
assetPath.basePath = humanoid->backSleeveFrameset();
assetPath.directives += humanoid->chestArmorDirectives();
} else {
return strf("^red;Invalid chest sheet type '{}'^reset;", sheet);
}
// recovery for custom chests made by a very old generator
if (args.size() <= 2 && args[2].toLower() == "old" && assetPath.basePath.beginsWith("/items/armors/avian/avian-tier6separator/"))
assetPath.basePath = "/items/armors/avian/avian-tier6separator/old/" + assetPath.basePath.substr(41);
} else if (first.equals("legs")) {
assetPath.basePath = humanoid->legsArmorFrameset();
assetPath.directives += humanoid->legsArmorDirectives();
} else if (first.equals("back")) {
assetPath.basePath = humanoid->backArmorFrameset();
assetPath.directives += humanoid->backArmorDirectives();
} else if (first.equals("body")) {
assetPath.basePath = humanoid->getBodyFromIdentity();
assetPath.directives += identity.bodyDirectives;
} else if (first.equals("head")) {
outputSheet = false;
assetPath.basePath = humanoid->getHeadFromIdentity();
assetPath.directives += identity.bodyDirectives;
} else if (first.equals("hair")) {
outputSheet = false;
assetPath.basePath = humanoid->getHairFromIdentity();
assetPath.directives += identity.hairDirectives;
} else if (first.equals("facialhair")) {
outputSheet = false;
assetPath.basePath = humanoid->getFacialHairFromIdentity();
assetPath.directives += identity.facialHairDirectives;
} else if (first.equals("facialmask")) {
outputSheet = false;
assetPath.basePath = humanoid->getFacialMaskFromIdentity();
assetPath.directives += identity.facialMaskDirectives;
} else if (first.equals("frontarm")) {
assetPath.basePath = humanoid->getFrontArmFromIdentity();
assetPath.directives += identity.bodyDirectives;
} else if (first.equals("backarm")) {
assetPath.basePath = humanoid->getBackArmFromIdentity();
assetPath.directives += identity.bodyDirectives;
} else if (first.equals("emote")) {
assetPath.basePath = humanoid->getFacialEmotesFromIdentity();
assetPath.directives += identity.emoteDirectives;
} else {
outputName = "render";
}
if (!outputSheet)
assetPath.subPath = String("normal");
}
if (assetPath == AssetPath()) {
assetPath = AssetPath::split(path);
if (!assetPath.basePath.beginsWith("/"))
assetPath.basePath = "/assetmissing.png" + assetPath.basePath;
}
auto assets = Root::singleton().assets();
ImageConstPtr image;
if (outputSheet) {
auto sheet = make_shared<Image>(*assets->image(assetPath.basePath));
sheet->convert(PixelFormat::RGBA32);
AssetPath framePath = assetPath;
StringMap<pair<RectU, ImageConstPtr>> frames;
auto imageFrames = assets->imageFrames(assetPath.basePath);
for (auto& pair : imageFrames->frames)
frames[pair.first] = make_pair(pair.second, ImageConstPtr());
if (frames.empty())
return "^red;Failed to save image^reset;";
for (auto& entry : frames) {
framePath.subPath = entry.first;
entry.second.second = assets->image(framePath);
}
Vec2U frameSize = frames.begin()->second.first.size();
Vec2U imageSize = frames.begin()->second.second->size().piecewiseMin(Vec2U{256, 256});
if (imageSize.min() == 0)
return "^red;Resulting image is empty^reset;";
for (auto& frame : frames) {
RectU& box = frame.second.first;
box.setXMin((box.xMin() / frameSize[0]) * imageSize[0]);
box.setYMin(((sheet->height() - box.yMin() - box.height()) / frameSize[1]) * imageSize[1]);
box.setXMax(box.xMin() + imageSize[0]);
box.setYMax(box.yMin() + imageSize[1]);
}
if (frameSize != imageSize) {
unsigned sheetWidth = (sheet->width() / frameSize[0]) * imageSize[0];
unsigned sheetHeight = (sheet->height() / frameSize[0]) * imageSize[0];
sheet->reset(sheetWidth, sheetHeight, PixelFormat::RGBA32);
}
for (auto& entry : frames)
sheet->copyInto(entry.second.first.min(), *entry.second.second);
image = std::move(sheet);
} else {
image = assets->image(assetPath);
}
if (image->size().min() == 0)
return "^red;Resulting image is empty^reset;";
auto outputDirectory = Root::singleton().toStoragePath("output");
auto outputPath = File::relativeTo(outputDirectory, strf("{}.png", outputName));
if (!File::isDirectory(outputDirectory))
File::makeDirectory(outputDirectory);
image->writePng(File::open(outputPath, IOMode::Write | IOMode::Truncate));
return strf("Saved {}x{} image to render.png", image->width(), image->height()); return strf("Saved {}x{} image to render.png", image->width(), image->height());
} }

View File

@ -390,6 +390,46 @@ void Humanoid::setHelmetMaskDirectives(Directives helmetMaskDirectives) {
m_helmetMaskDirectives = std::move(helmetMaskDirectives); m_helmetMaskDirectives = std::move(helmetMaskDirectives);
} }
Directives const& Humanoid::headArmorDirectives() const {
return m_headArmorDirectives;
};
String const& Humanoid::headArmorFrameset() const {
return m_headArmorFrameset;
};
Directives const& Humanoid::chestArmorDirectives() const {
return m_chestArmorDirectives;
};
String const& Humanoid::chestArmorFrameset() const {
return m_chestArmorFrameset;
};
String const& Humanoid::backSleeveFrameset() const {
return m_backSleeveFrameset;
};
String const& Humanoid::frontSleeveFrameset() const {
return m_frontSleeveFrameset;
};
Directives const& Humanoid::legsArmorDirectives() const {
return m_legsArmorDirectives;
};
String const& Humanoid::legsArmorFrameset() const {
return m_legsArmorFrameset;
};
Directives const& Humanoid::backArmorDirectives() const {
return m_backArmorDirectives;
};
String const& Humanoid::backArmorFrameset() const {
return m_backArmorFrameset;
};
void Humanoid::setBodyHidden(bool hidden) { void Humanoid::setBodyHidden(bool hidden) {
m_bodyHidden = hidden; m_bodyHidden = hidden;
} }

View File

@ -158,6 +158,18 @@ public:
void setHelmetMaskDirectives(Directives helmetMaskDirectives); void setHelmetMaskDirectives(Directives helmetMaskDirectives);
// Getters for all of the above
Directives const& headArmorDirectives() const;
String const& headArmorFrameset() const;
Directives const& chestArmorDirectives() const;
String const& chestArmorFrameset() const;
String const& backSleeveFrameset() const;
String const& frontSleeveFrameset() const;
Directives const& legsArmorDirectives() const;
String const& legsArmorFrameset() const;
Directives const& backArmorDirectives() const;
String const& backArmorFrameset() const;
void setBodyHidden(bool hidden); void setBodyHidden(bool hidden);
void setState(State state); void setState(State state);
@ -248,6 +260,16 @@ public:
Json const& defaultMovementParameters() const; Json const& defaultMovementParameters() const;
String getHeadFromIdentity() const;
String getBodyFromIdentity() const;
String getFacialEmotesFromIdentity() const;
String getHairFromIdentity() const;
String getFacialHairFromIdentity() const;
String getFacialMaskFromIdentity() const;
String getBackArmFromIdentity() const;
String getFrontArmFromIdentity() const;
String getVaporTrailFrameset() const;
// Extracts scalenearest from directives and returns the combined scale and // Extracts scalenearest from directives and returns the combined scale and
// a new Directives without those scalenearest directives. // a new Directives without those scalenearest directives.
static pair<Vec2F, Directives> extractScaleFromDirectives(Directives const& directives); static pair<Vec2F, Directives> extractScaleFromDirectives(Directives const& directives);
@ -269,16 +291,6 @@ private:
String frameBase(State state) const; String frameBase(State state) const;
String emoteFrameBase(HumanoidEmote state) const; String emoteFrameBase(HumanoidEmote state) const;
String getHeadFromIdentity() const;
String getBodyFromIdentity() const;
String getFacialEmotesFromIdentity() const;
String getHairFromIdentity() const;
String getFacialHairFromIdentity() const;
String getFacialMaskFromIdentity() const;
String getBackArmFromIdentity() const;
String getFrontArmFromIdentity() const;
String getVaporTrailFrameset() const;
Directives getBodyDirectives() const; Directives getBodyDirectives() const;
Directives getHairDirectives() const; Directives getHairDirectives() const;
Directives getEmoteDirectives() const; Directives getEmoteDirectives() const;