Mega Region Update.

This commit is contained in:
2026-02-05 21:34:42 -05:00
parent 00bff4a91f
commit 64d6a1d95a
45 changed files with 1590 additions and 205 deletions

View File

@@ -1,6 +1,12 @@
#include "anm2.h"
#include <algorithm>
#include <cmath>
#include <format>
#include <limits>
#include <ranges>
#include <unordered_map>
#include <vector>
#include "map_.h"
#include "path_.h"
@@ -23,6 +29,367 @@ namespace anm2ed::anm2
return true;
}
bool Anm2::spritesheet_pack(int id)
{
constexpr int PACKING_PADDING = 1;
struct RectI
{
int x{};
int y{};
int w{};
int h{};
};
struct PackItem
{
int regionID{-1};
int srcX{};
int srcY{};
int width{};
int height{};
int packWidth{};
int packHeight{};
};
class MaxRectsPacker
{
int width{};
int height{};
std::vector<RectI> freeRects{};
static bool intersects(const RectI& a, const RectI& b)
{
return !(b.x >= a.x + a.w || b.x + b.w <= a.x || b.y >= a.y + a.h || b.y + b.h <= a.y);
}
static bool contains(const RectI& a, const RectI& b)
{
return b.x >= a.x && b.y >= a.y && b.x + b.w <= a.x + a.w && b.y + b.h <= a.y + a.h;
}
void split_free_rects(const RectI& used)
{
std::vector<RectI> next{};
next.reserve(freeRects.size() * 2);
for (auto& free : freeRects)
{
if (!intersects(free, used))
{
next.push_back(free);
continue;
}
if (used.x > free.x) next.push_back({free.x, free.y, used.x - free.x, free.h});
if (used.x + used.w < free.x + free.w)
next.push_back({used.x + used.w, free.y, free.x + free.w - (used.x + used.w), free.h});
if (used.y > free.y) next.push_back({free.x, free.y, free.w, used.y - free.y});
if (used.y + used.h < free.y + free.h)
next.push_back({free.x, used.y + used.h, free.w, free.y + free.h - (used.y + used.h)});
}
freeRects = std::move(next);
}
void prune_free_rects()
{
for (int i = 0; i < (int)freeRects.size(); i++)
{
if (freeRects[i].w <= 0 || freeRects[i].h <= 0)
{
freeRects.erase(freeRects.begin() + i--);
continue;
}
for (int j = i + 1; j < (int)freeRects.size();)
{
if (contains(freeRects[i], freeRects[j]))
freeRects.erase(freeRects.begin() + j);
else if (contains(freeRects[j], freeRects[i]))
{
freeRects.erase(freeRects.begin() + i);
i--;
break;
}
else
j++;
}
}
}
public:
MaxRectsPacker(int width, int height) : width(width), height(height), freeRects({{0, 0, width, height}}) {}
bool insert(int width, int height, RectI& result)
{
int bestShort = std::numeric_limits<int>::max();
int bestLong = std::numeric_limits<int>::max();
RectI best{};
bool found{};
for (auto& free : freeRects)
{
if (width > free.w || height > free.h) continue;
int leftOverW = free.w - width;
int leftOverH = free.h - height;
int shortSide = std::min(leftOverW, leftOverH);
int longSide = std::max(leftOverW, leftOverH);
if (shortSide < bestShort || (shortSide == bestShort && longSide < bestLong))
{
bestShort = shortSide;
bestLong = longSide;
best = {free.x, free.y, width, height};
found = true;
}
}
if (!found) return false;
result = best;
split_free_rects(best);
prune_free_rects();
return true;
}
};
auto pack_regions = [&](const std::vector<PackItem>& items, int& packedWidth, int& packedHeight,
std::unordered_map<int, RectI>& packedRects)
{
if (items.empty()) return false;
int maxWidth{};
int maxHeight{};
int sumWidth{};
int sumHeight{};
int64_t totalArea{};
for (auto& item : items)
{
maxWidth = std::max(maxWidth, item.packWidth);
maxHeight = std::max(maxHeight, item.packHeight);
sumWidth += item.packWidth;
sumHeight += item.packHeight;
totalArea += (int64_t)item.packWidth * item.packHeight;
}
if (maxWidth <= 0 || maxHeight <= 0) return false;
int bestSquareDelta = std::numeric_limits<int>::max();
int bestArea = std::numeric_limits<int>::max();
int bestWidth{};
int bestHeight{};
std::unordered_map<int, RectI> bestRects{};
int startWidth = maxWidth;
int endWidth = std::max(startWidth, sumWidth);
int step = std::max(1, (endWidth - startWidth) / 512);
for (int candidateWidth = startWidth; candidateWidth <= endWidth; candidateWidth += step)
{
int candidateHeightMin = std::max(maxHeight, (int)std::ceil((double)totalArea / candidateWidth));
bool isValid{};
int usedWidth{};
int usedHeight{};
std::unordered_map<int, RectI> candidateRects{};
// Grow candidate height until this width can actually fit all rectangles.
for (int candidateHeight = candidateHeightMin; candidateHeight <= sumHeight; candidateHeight++)
{
MaxRectsPacker packer(candidateWidth, candidateHeight);
candidateRects.clear();
isValid = true;
usedWidth = 0;
usedHeight = 0;
for (auto& item : items)
{
RectI rect{};
if (!packer.insert(item.packWidth, item.packHeight, rect))
{
isValid = false;
break;
}
candidateRects[item.regionID] = rect;
usedWidth = std::max(usedWidth, rect.x + rect.w);
usedHeight = std::max(usedHeight, rect.y + rect.h);
}
if (isValid) break;
}
if (!isValid) continue;
int area = usedWidth * usedHeight;
int squareDelta = std::abs(usedWidth - usedHeight);
if (squareDelta < bestSquareDelta || (squareDelta == bestSquareDelta && area < bestArea))
{
bestSquareDelta = squareDelta;
bestArea = area;
bestWidth = usedWidth;
bestHeight = usedHeight;
bestRects = std::move(candidateRects);
if (bestArea == totalArea && bestSquareDelta == 0) break;
}
}
if (bestArea == std::numeric_limits<int>::max()) return false;
packedWidth = bestWidth;
packedHeight = bestHeight;
packedRects = std::move(bestRects);
return true;
};
if (!content.spritesheets.contains(id)) return false;
auto& spritesheet = content.spritesheets.at(id);
if (!spritesheet.texture.is_valid() || spritesheet.texture.pixels.empty()) return false;
if (spritesheet.regions.empty()) return false;
std::vector<PackItem> items{};
items.reserve(spritesheet.regions.size());
for (auto& [regionID, region] : spritesheet.regions)
{
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
auto size = glm::max(maxPoint - minPoint, glm::ivec2(1));
int packWidth = size.x + PACKING_PADDING * 2;
int packHeight = size.y + PACKING_PADDING * 2;
items.push_back({regionID, minPoint.x, minPoint.y, size.x, size.y, packWidth, packHeight});
}
std::sort(items.begin(), items.end(), [](const PackItem& a, const PackItem& b)
{
int areaA = a.width * a.height;
int areaB = b.width * b.height;
if (areaA != areaB) return areaA > areaB;
return a.regionID < b.regionID;
});
int packedWidth{};
int packedHeight{};
std::unordered_map<int, RectI> packedRects{};
if (!pack_regions(items, packedWidth, packedHeight, packedRects)) return false;
if (packedWidth <= 0 || packedHeight <= 0) return false;
auto textureSize = spritesheet.texture.size;
auto& sourcePixels = spritesheet.texture.pixels;
std::vector<uint8_t> packedPixels((size_t)packedWidth * packedHeight * resource::texture::CHANNELS, 0);
for (auto& item : items)
{
if (!packedRects.contains(item.regionID)) continue;
auto destinationRect = packedRects.at(item.regionID);
for (int y = 0; y < item.height; y++)
{
for (int x = 0; x < item.width; x++)
{
int sourceX = item.srcX + x;
int sourceY = item.srcY + y;
int destinationX = destinationRect.x + PACKING_PADDING + x;
int destinationY = destinationRect.y + PACKING_PADDING + y;
if (sourceX < 0 || sourceY < 0 || sourceX >= textureSize.x || sourceY >= textureSize.y) continue;
if (destinationX < 0 || destinationY < 0 || destinationX >= packedWidth || destinationY >= packedHeight)
continue;
auto sourceIndex = ((size_t)sourceY * textureSize.x + sourceX) * resource::texture::CHANNELS;
auto destinationIndex =
((size_t)destinationY * packedWidth + destinationX) * resource::texture::CHANNELS;
std::copy_n(sourcePixels.data() + sourceIndex, resource::texture::CHANNELS,
packedPixels.data() + destinationIndex);
}
}
}
spritesheet.texture = resource::Texture(packedPixels.data(), {packedWidth, packedHeight});
for (auto& [regionID, region] : spritesheet.regions)
if (packedRects.contains(regionID))
{
auto& rect = packedRects.at(regionID);
region.crop = {rect.x + PACKING_PADDING, rect.y + PACKING_PADDING};
}
return true;
}
bool Anm2::regions_trim(int spritesheetID, const std::set<int>& ids)
{
auto spritesheet = spritesheet_get(spritesheetID);
if (!spritesheet || !spritesheet->texture.is_valid() || spritesheet->texture.pixels.empty() || ids.empty())
return false;
auto& texture = spritesheet->texture;
bool changed{};
for (auto id : ids)
{
if (!spritesheet->regions.contains(id)) continue;
auto& region = spritesheet->regions.at(id);
auto minPoint = glm::ivec2(glm::min(region.crop, region.crop + region.size));
auto maxPoint = glm::ivec2(glm::max(region.crop, region.crop + region.size));
int minX = std::max(0, minPoint.x);
int minY = std::max(0, minPoint.y);
int maxX = std::min(texture.size.x, maxPoint.x);
int maxY = std::min(texture.size.y, maxPoint.y);
if (minX >= maxX || minY >= maxY) continue;
int contentMinX = std::numeric_limits<int>::max();
int contentMinY = std::numeric_limits<int>::max();
int contentMaxX = std::numeric_limits<int>::min();
int contentMaxY = std::numeric_limits<int>::min();
for (int y = minY; y < maxY; y++)
{
for (int x = minX; x < maxX; x++)
{
auto index = ((size_t)y * texture.size.x + x) * resource::texture::CHANNELS;
if (index + resource::texture::CHANNELS > texture.pixels.size()) continue;
auto r = texture.pixels[index + 0];
auto g = texture.pixels[index + 1];
auto b = texture.pixels[index + 2];
auto a = texture.pixels[index + 3];
if (r == 0 && g == 0 && b == 0 && a == 0) continue;
contentMinX = std::min(contentMinX, x);
contentMinY = std::min(contentMinY, y);
contentMaxX = std::max(contentMaxX, x);
contentMaxY = std::max(contentMaxY, y);
}
}
if (contentMinX == std::numeric_limits<int>::max()) continue;
auto newCrop = glm::vec2(contentMinX, contentMinY);
auto newSize = glm::vec2(contentMaxX - contentMinX + 1, contentMaxY - contentMinY + 1);
if (region.crop != newCrop || region.size != newSize)
{
auto previousCrop = region.crop;
region.crop = newCrop;
region.size = newSize;
if (region.origin == Spritesheet::Region::TOP_LEFT)
region.pivot = {};
else if (region.origin == Spritesheet::Region::ORIGIN_CENTER)
region.pivot = {static_cast<int>(region.size.x / 2.0f), static_cast<int>(region.size.y / 2.0f)};
else
// Preserve the same texture-space pivot location when trimming shifts region crop.
region.pivot -= (region.crop - previousCrop);
changed = true;
}
}
return changed;
}
std::set<int> Anm2::spritesheets_unused()
{
std::set<int> used{};
@@ -36,6 +403,117 @@ namespace anm2ed::anm2
return unused;
}
bool Anm2::spritesheets_merge(const std::set<int>& ids, SpritesheetMergeOrigin mergeOrigin, bool isMakeRegions,
origin::Type regionOrigin)
{
if (ids.size() < 2) return false;
auto baseId = *ids.begin();
if (!content.spritesheets.contains(baseId)) return false;
for (auto id : ids)
if (!content.spritesheets.contains(id)) return false;
auto& base = content.spritesheets.at(baseId);
if (!base.texture.is_valid()) return false;
std::unordered_map<int, glm::ivec2> offsets{};
offsets[baseId] = {};
auto mergedTexture = base.texture;
for (auto id : ids)
{
if (id == baseId) continue;
auto& spritesheet = content.spritesheets.at(id);
if (!spritesheet.texture.is_valid()) return false;
offsets[id] = mergeOrigin == APPEND_RIGHT ? glm::ivec2(mergedTexture.size.x, 0)
: glm::ivec2(0, mergedTexture.size.y);
mergedTexture = resource::Texture::merge_append(mergedTexture, spritesheet.texture,
mergeOrigin == APPEND_RIGHT);
}
base.texture = std::move(mergedTexture);
std::unordered_map<int, std::unordered_map<int, int>> regionIdMap{};
if (isMakeRegions)
{
if (base.regionOrder.size() != base.regions.size())
{
base.regionOrder.clear();
base.regionOrder.reserve(base.regions.size());
for (auto id : base.regions | std::views::keys)
base.regionOrder.push_back(id);
}
for (auto id : ids)
{
if (id == baseId) continue;
auto& source = content.spritesheets.at(id);
auto sheetOffset = offsets.at(id);
auto locationRegionID = map::next_id_get(base.regions);
auto sourceFilename = path::to_utf8(source.path.stem());
auto locationRegionName = sourceFilename.empty() ? std::format("#{}", id) : sourceFilename;
auto locationRegionPivot =
regionOrigin == origin::ORIGIN_CENTER ? glm::vec2(source.texture.size) * 0.5f : glm::vec2();
base.regions[locationRegionID] = {
.name = locationRegionName,
.crop = sheetOffset,
.pivot = glm::ivec2(locationRegionPivot),
.size = source.texture.size,
.origin = regionOrigin,
};
base.regionOrder.push_back(locationRegionID);
for (auto& [sourceRegionID, sourceRegion] : source.regions)
{
auto destinationRegionID = map::next_id_get(base.regions);
auto destinationRegion = sourceRegion;
destinationRegion.crop += sheetOffset;
base.regions[destinationRegionID] = destinationRegion;
base.regionOrder.push_back(destinationRegionID);
regionIdMap[id][sourceRegionID] = destinationRegionID;
}
}
}
std::unordered_map<int, int> layerSpritesheetBefore{};
for (auto& [layerID, layer] : content.layers)
{
if (!ids.contains(layer.spritesheetID)) continue;
layerSpritesheetBefore[layerID] = layer.spritesheetID;
layer.spritesheetID = baseId;
}
for (auto& animation : animations.items)
{
for (auto& [layerID, item] : animation.layerAnimations)
{
if (!layerSpritesheetBefore.contains(layerID)) continue;
auto sourceSpritesheetID = layerSpritesheetBefore.at(layerID);
if (sourceSpritesheetID == baseId) continue;
for (auto& frame : item.frames)
{
if (frame.regionID == -1) continue;
if (isMakeRegions && regionIdMap.contains(sourceSpritesheetID) &&
regionIdMap.at(sourceSpritesheetID).contains(frame.regionID))
frame.regionID = regionIdMap.at(sourceSpritesheetID).at(frame.regionID);
else
frame.regionID = -1;
}
}
}
for (auto id : ids)
if (id != baseId) content.spritesheets.erase(id);
return true;
}
std::vector<std::string> Anm2::spritesheet_labels_get()
{
std::vector<std::string> labels{};
@@ -57,18 +535,60 @@ namespace anm2ed::anm2
std::vector<std::string> Anm2::region_labels_get(Spritesheet& spritesheet)
{
auto rebuild_order = [&]()
{
spritesheet.regionOrder.clear();
spritesheet.regionOrder.reserve(spritesheet.regions.size());
for (auto id : spritesheet.regions | std::views::keys)
spritesheet.regionOrder.push_back(id);
};
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
rebuild_order();
else
{
bool isOrderValid = true;
for (auto id : spritesheet.regionOrder)
if (!spritesheet.regions.contains(id))
{
isOrderValid = false;
break;
}
if (!isOrderValid) rebuild_order();
}
std::vector<std::string> labels{};
labels.emplace_back(localize.get(BASIC_NONE));
for (auto& region : spritesheet.regions | std::views::values)
labels.emplace_back(region.name);
for (auto id : spritesheet.regionOrder)
labels.emplace_back(spritesheet.regions.at(id).name);
return labels;
}
std::vector<int> Anm2::region_ids_get(Spritesheet& spritesheet)
{
auto rebuild_order = [&]()
{
spritesheet.regionOrder.clear();
spritesheet.regionOrder.reserve(spritesheet.regions.size());
for (auto id : spritesheet.regions | std::views::keys)
spritesheet.regionOrder.push_back(id);
};
if (spritesheet.regionOrder.size() != spritesheet.regions.size())
rebuild_order();
else
{
bool isOrderValid = true;
for (auto id : spritesheet.regionOrder)
if (!spritesheet.regions.contains(id))
{
isOrderValid = false;
break;
}
if (!isOrderValid) rebuild_order();
}
std::vector<int> ids{};
ids.emplace_back(-1);
for (auto& id : spritesheet.regions | std::views::keys)
for (auto id : spritesheet.regionOrder)
ids.emplace_back(id);
return ids;
}
@@ -93,6 +613,40 @@ namespace anm2ed::anm2
return unused;
}
void Anm2::scan_and_set_regions()
{
for (auto& animation : animations.items)
{
for (auto& [layerID, item] : animation.layerAnimations)
{
auto layer = map::find(content.layers, layerID);
if (!layer) continue;
auto spritesheet = spritesheet_get(layer->spritesheetID);
if (!spritesheet || spritesheet->regions.empty()) continue;
for (auto& frame : item.frames)
{
if (frame.regionID != -1) continue;
auto frameCrop = glm::ivec2(frame.crop);
auto frameSize = glm::ivec2(frame.size);
auto framePivot = glm::ivec2(frame.pivot);
for (auto& [regionID, region] : spritesheet->regions)
{
if (glm::ivec2(region.crop) == frameCrop && glm::ivec2(region.size) == frameSize &&
glm::ivec2(region.pivot) == framePivot)
{
frame.regionID = regionID;
break;
}
}
}
}
}
}
bool Anm2::spritesheets_deserialize(const std::string& string, const std::filesystem::path& directory,
merge::Type type, std::string* errorString)
{