mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 01:23:52 +00:00
WMO buildings reference M2 doodads (chairs, candles, banners) via the MODD chunk. Their textures live in those M2 files, not the WMO root. Now collectWMOTextures walks every doodad name and collects M2 textures recursively so exported buildings include all their interior decoration textures.
133 lines
4.9 KiB
C++
133 lines
4.9 KiB
C++
#include "texture_exporter.hpp"
|
|
#include "pipeline/asset_manager.hpp"
|
|
#include "pipeline/blp_loader.hpp"
|
|
#include "pipeline/m2_loader.hpp"
|
|
#include "pipeline/wmo_loader.hpp"
|
|
#include "core/logger.hpp"
|
|
#include <filesystem>
|
|
#include <algorithm>
|
|
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
#include "stb_image_write.h"
|
|
|
|
namespace wowee {
|
|
namespace editor {
|
|
|
|
std::vector<std::string> TextureExporter::collectUsedTextures(const pipeline::ADTTerrain& terrain) {
|
|
std::unordered_set<std::string> unique;
|
|
for (const auto& tex : terrain.textures)
|
|
unique.insert(tex);
|
|
std::vector<std::string> result(unique.begin(), unique.end());
|
|
std::sort(result.begin(), result.end());
|
|
return result;
|
|
}
|
|
|
|
std::vector<std::string> TextureExporter::collectM2Textures(pipeline::AssetManager* am,
|
|
const std::string& m2Path) {
|
|
std::vector<std::string> out;
|
|
if (!am || m2Path.empty()) return out;
|
|
|
|
auto data = am->readFile(m2Path);
|
|
if (data.empty()) return out;
|
|
auto m2 = pipeline::M2Loader::load(data);
|
|
// Skin file holds geometry but textures live in the M2 header itself.
|
|
// Even if isValid() is false (no skin loaded), the texture list is populated.
|
|
|
|
std::unordered_set<std::string> unique;
|
|
for (const auto& tex : m2.textures) {
|
|
if (tex.filename.empty()) continue;
|
|
std::string p = tex.filename;
|
|
std::transform(p.begin(), p.end(), p.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
unique.insert(p);
|
|
}
|
|
out.assign(unique.begin(), unique.end());
|
|
std::sort(out.begin(), out.end());
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::string> TextureExporter::collectWMOTextures(pipeline::AssetManager* am,
|
|
const std::string& wmoPath) {
|
|
std::vector<std::string> out;
|
|
if (!am || wmoPath.empty()) return out;
|
|
|
|
auto rootData = am->readFile(wmoPath);
|
|
if (rootData.empty()) return out;
|
|
auto wmo = pipeline::WMOLoader::load(rootData);
|
|
|
|
// Load group files so any group-only texture references are populated too.
|
|
std::string base = wmoPath;
|
|
if (base.size() > 4) base = base.substr(0, base.size() - 4);
|
|
for (uint32_t gi = 0; gi < wmo.nGroups; gi++) {
|
|
char suffix[16];
|
|
std::snprintf(suffix, sizeof(suffix), "_%03u.wmo", gi);
|
|
auto gd = am->readFile(base + suffix);
|
|
if (!gd.empty()) pipeline::WMOLoader::loadGroup(gd, wmo, gi);
|
|
}
|
|
|
|
std::unordered_set<std::string> unique;
|
|
for (const auto& tex : wmo.textures) {
|
|
if (tex.empty()) continue;
|
|
std::string p = tex;
|
|
std::transform(p.begin(), p.end(), p.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
unique.insert(p);
|
|
}
|
|
|
|
// WMO doodads (props inside the building) are M2 models — their textures
|
|
// also need to ship with the zone or the building will render with missing
|
|
// chairs/decorations.
|
|
std::unordered_set<std::string> seenDoodadM2;
|
|
for (const auto& [offset, name] : wmo.doodadNames) {
|
|
(void)offset;
|
|
if (name.empty() || !seenDoodadM2.insert(name).second) continue;
|
|
for (auto& t : collectM2Textures(am, name)) unique.insert(std::move(t));
|
|
}
|
|
|
|
out.assign(unique.begin(), unique.end());
|
|
std::sort(out.begin(), out.end());
|
|
return out;
|
|
}
|
|
|
|
int TextureExporter::exportTexturesAsPng(pipeline::AssetManager* am,
|
|
const std::vector<std::string>& texturePaths,
|
|
const std::string& outputDir) {
|
|
namespace fs = std::filesystem;
|
|
int exported = 0;
|
|
|
|
for (const auto& texPath : texturePaths) {
|
|
auto blpImage = am->loadTexture(texPath);
|
|
if (!blpImage.isValid()) {
|
|
LOG_WARNING("Texture not found or invalid: ", texPath);
|
|
continue;
|
|
}
|
|
|
|
// Build output path: replace backslashes, change .blp to .png
|
|
std::string outPath = texPath;
|
|
std::replace(outPath.begin(), outPath.end(), '\\', '/');
|
|
// Lowercase
|
|
std::transform(outPath.begin(), outPath.end(), outPath.begin(),
|
|
[](unsigned char c) { return std::tolower(c); });
|
|
// Change extension
|
|
auto dotPos = outPath.rfind('.');
|
|
if (dotPos != std::string::npos)
|
|
outPath = outPath.substr(0, dotPos) + ".png";
|
|
|
|
std::string fullPath = outputDir + "/" + outPath;
|
|
fs::create_directories(fs::path(fullPath).parent_path());
|
|
|
|
// Write RGBA data as PNG
|
|
if (stbi_write_png(fullPath.c_str(), blpImage.width, blpImage.height, 4,
|
|
blpImage.data.data(), blpImage.width * 4)) {
|
|
exported++;
|
|
} else {
|
|
LOG_WARNING("Failed to write PNG: ", fullPath);
|
|
}
|
|
}
|
|
|
|
LOG_INFO("Exported ", exported, "/", texturePaths.size(), " textures as PNG to ", outputDir);
|
|
return exported;
|
|
}
|
|
|
|
} // namespace editor
|
|
} // namespace wowee
|