为什么调整图集大小后纹理无法在纹理图集中找到其位置?
Why a texture cannot find its position in a texture atlas after the atlas has been resized?
我有纹理图集来自:https://straypixels.net/texture-packing-for-fonts/
struct TextureNode
{
TextureNode(const Vector2<uint32>& origin, const Vector2<uint32>& size)
: origin(origin), size(size)
{
}
Vector2<uint32> origin = 0; // Top left of the rectangle this node represents
Vector2<uint32> size = 0; // Size of the rectangle this node represents
bool empty = true; // true if this node is a leaf and is filled
std::unique_ptr<TextureNode> left = nullptr; // Left (or top) subdivision
std::unique_ptr<TextureNode> right = nullptr; // Right (or bottom) subdivision
};
struct TextureAtlas
{
TextureAtlas() = default;
~TextureAtlas()
{
buffer.clear();
}
void Create(const Vector2<uint32>& size)
{
textureSize = size;
rootNode = std::make_unique<::TextureNode>(0, size);
buffer.resize(size.w * size.h);
}
TextureNode* Pack(TextureNode* node, const Vector2<uint32>& size)
{
if (!node->empty) {
// The node is filled, not gonna fit anything else here
assert(!node->left && !node->right);
return nullptr;
} else if (node->left && node->right) {
// Non-leaf, try inserting to the left and then to the right
TextureNode* retval = Pack(node->left.get(), size);
if (retval != nullptr) {
return retval;
}
return Pack(node->right.get(), size);
} else {
// This is an unfilled leaf - let's see if we can fill it
Vector2<uint32> realSize = node->size;
// If we're along a boundary, calculate the actual size
if (node->origin.x + node->size.x == INT_MAX) {
realSize.x = textureSize.x - node->origin.x;
}
if (node->origin.y + node->size.y == INT_MAX) {
realSize.y = textureSize.y - node->origin.y;
}
if (node->size.x == size.x && node->size.y == size.y) {
// Perfect size - just pack into this node
node->empty = false;
return node;
} else if (realSize.x < size.x || realSize.y < size.y) {
// Not big enough
return nullptr;
} else {
// Large enough - split until we get a perfect fit
TextureNode* left = nullptr;
TextureNode* right = nullptr;
// Determine how much space we'll have left if we split each way
int remainX = realSize.x - size.x;
int remainY = realSize.y - size.y;
// Split the way that will leave the most room
bool verticalSplit = remainX < remainY;
if (remainX == 0 && remainY == 0) {
// Edge case - we are are going to hit the border of
// the texture atlas perfectly, split at the border instead
if (node->size.x > node->size.y) {
verticalSplit = false;
} else verticalSplit = true;
}
if (verticalSplit) {
// Split vertically (left is top)
left = new TextureNode(node->origin, Vector2<uint32>(node->size.x, size.y));
right = new TextureNode(Vector2<uint32>(node->origin.x, node->origin.y + size.y), Vector2<uint32>(node->size.x, node->size.y - size.y));
} else {
// Split horizontally
left = new TextureNode(node->origin, Vector2<uint32>(size.x, node->size.y));
right = new TextureNode(Vector2<uint32>(node->origin.x + size.x, node->origin.y), Vector2<uint32>(node->size.x - size.x, node->size.y));
}
node->left.reset(left);
node->right.reset(right);
return Pack(node->left.get(), size);
}
}
return nullptr;
}
Vector2<uint32> PackTexture(unsigned char* textureBuffer, const Vector2<uint32>& bufferSize)
{
TextureNode* node = Pack(rootNode.get(), bufferSize);
if (node == nullptr) {
ResizeBuffer(textureSize * 2U);
node = Pack(rootNode.get(), bufferSize);
// Note: this assert will be hit if we try to pack a texture larger
// than the current size of the texture
assert(node != nullptr);
}
assert(bufferSize.x == node->size.x);
assert(bufferSize.y == node->size.y);
// Copy the texture to the texture atlas' buffer
for (uint32 ly = 0; ly < bufferSize.y; ly++) {
for (uint32 lx = 0; lx < bufferSize.x; lx++) {
int y = node->origin.y + ly;
int x = node->origin.x + lx;
buffer[y * textureSize.x + x] = textureBuffer[ly * bufferSize.x + lx];
}
}
return node->origin;
}
void ResizeBuffer(const Vector2<uint32>& newSize)
{
vector<uchar> newBuffer;
newBuffer.resize(newSize.y * newSize.x);
for (uint32 y = 0; y < textureSize.y; y++) {
for (uint32 x = 0; x < textureSize.x; x++) {
newBuffer[y * newSize.x + x] = buffer[y * textureSize.x + x];
}
}
textureSize = newSize;
buffer = std::move(newBuffer);
rootNode->size = newSize;
}
vector<uchar> buffer;
Vector2<uint32> textureSize = 512;
std::unique_ptr<TextureNode> rootNode = nullptr;
}
int main()
{
TextureAtlas atlas;
atlas.Create({256, 256};
Font font;
// size
font.Create("arial.ttf", 100.0f);
std::string chars = "ABCDEFGHIJKLMNOPRSTUWXYZabcdefghijklmnoprstuwzyw1234567890";
for(const auto& c : chars) {
auto txt = font.LoadGlyph(c);
atlas.PackTexture(txt.data(), txt.w, txt.h);
}
}
在图集space没有改动的情况下有效。当没有 space 剩余并且需要调整图集大小时,大小发生变化,但算法找不到放置另一个纹理的位置,它在 PackTexture(..)
方法中命中 assert(node != nullptr);
。你可以使用任何你想要的纹理。结果是一样的。 运行 通过断点执行代码后,看起来 rootNode
改变了它的大小,但 left
和 right
仍然保持不变。 Nullptr
由 Pack(..)
方法中的此步骤返回:
if (realSize.x < size.x || realSize.y < size.y) {
// Not big enough
return nullptr;
}
这是为什么?如何更改算法,使其能够正确处理大小调整?
您没有正确设置根的大小,所以 realSize
永远不会调整。
请注意,默认构造的 TextureAtlas
处于无效状态。为什么有单独的 Create
?
此外,您的析构函数与默认析构函数执行相同的操作。最好省略。
explicit TextureAtlas(const Vector2<uint32>& size = { 512, 512 })
: textureSize(size),
rootNode(std::make_unique<TextureNode>(0, { INT_MAX, INT_MAX })),
buffer(size.w * size.h)
{
}
我有纹理图集来自:https://straypixels.net/texture-packing-for-fonts/
struct TextureNode
{
TextureNode(const Vector2<uint32>& origin, const Vector2<uint32>& size)
: origin(origin), size(size)
{
}
Vector2<uint32> origin = 0; // Top left of the rectangle this node represents
Vector2<uint32> size = 0; // Size of the rectangle this node represents
bool empty = true; // true if this node is a leaf and is filled
std::unique_ptr<TextureNode> left = nullptr; // Left (or top) subdivision
std::unique_ptr<TextureNode> right = nullptr; // Right (or bottom) subdivision
};
struct TextureAtlas
{
TextureAtlas() = default;
~TextureAtlas()
{
buffer.clear();
}
void Create(const Vector2<uint32>& size)
{
textureSize = size;
rootNode = std::make_unique<::TextureNode>(0, size);
buffer.resize(size.w * size.h);
}
TextureNode* Pack(TextureNode* node, const Vector2<uint32>& size)
{
if (!node->empty) {
// The node is filled, not gonna fit anything else here
assert(!node->left && !node->right);
return nullptr;
} else if (node->left && node->right) {
// Non-leaf, try inserting to the left and then to the right
TextureNode* retval = Pack(node->left.get(), size);
if (retval != nullptr) {
return retval;
}
return Pack(node->right.get(), size);
} else {
// This is an unfilled leaf - let's see if we can fill it
Vector2<uint32> realSize = node->size;
// If we're along a boundary, calculate the actual size
if (node->origin.x + node->size.x == INT_MAX) {
realSize.x = textureSize.x - node->origin.x;
}
if (node->origin.y + node->size.y == INT_MAX) {
realSize.y = textureSize.y - node->origin.y;
}
if (node->size.x == size.x && node->size.y == size.y) {
// Perfect size - just pack into this node
node->empty = false;
return node;
} else if (realSize.x < size.x || realSize.y < size.y) {
// Not big enough
return nullptr;
} else {
// Large enough - split until we get a perfect fit
TextureNode* left = nullptr;
TextureNode* right = nullptr;
// Determine how much space we'll have left if we split each way
int remainX = realSize.x - size.x;
int remainY = realSize.y - size.y;
// Split the way that will leave the most room
bool verticalSplit = remainX < remainY;
if (remainX == 0 && remainY == 0) {
// Edge case - we are are going to hit the border of
// the texture atlas perfectly, split at the border instead
if (node->size.x > node->size.y) {
verticalSplit = false;
} else verticalSplit = true;
}
if (verticalSplit) {
// Split vertically (left is top)
left = new TextureNode(node->origin, Vector2<uint32>(node->size.x, size.y));
right = new TextureNode(Vector2<uint32>(node->origin.x, node->origin.y + size.y), Vector2<uint32>(node->size.x, node->size.y - size.y));
} else {
// Split horizontally
left = new TextureNode(node->origin, Vector2<uint32>(size.x, node->size.y));
right = new TextureNode(Vector2<uint32>(node->origin.x + size.x, node->origin.y), Vector2<uint32>(node->size.x - size.x, node->size.y));
}
node->left.reset(left);
node->right.reset(right);
return Pack(node->left.get(), size);
}
}
return nullptr;
}
Vector2<uint32> PackTexture(unsigned char* textureBuffer, const Vector2<uint32>& bufferSize)
{
TextureNode* node = Pack(rootNode.get(), bufferSize);
if (node == nullptr) {
ResizeBuffer(textureSize * 2U);
node = Pack(rootNode.get(), bufferSize);
// Note: this assert will be hit if we try to pack a texture larger
// than the current size of the texture
assert(node != nullptr);
}
assert(bufferSize.x == node->size.x);
assert(bufferSize.y == node->size.y);
// Copy the texture to the texture atlas' buffer
for (uint32 ly = 0; ly < bufferSize.y; ly++) {
for (uint32 lx = 0; lx < bufferSize.x; lx++) {
int y = node->origin.y + ly;
int x = node->origin.x + lx;
buffer[y * textureSize.x + x] = textureBuffer[ly * bufferSize.x + lx];
}
}
return node->origin;
}
void ResizeBuffer(const Vector2<uint32>& newSize)
{
vector<uchar> newBuffer;
newBuffer.resize(newSize.y * newSize.x);
for (uint32 y = 0; y < textureSize.y; y++) {
for (uint32 x = 0; x < textureSize.x; x++) {
newBuffer[y * newSize.x + x] = buffer[y * textureSize.x + x];
}
}
textureSize = newSize;
buffer = std::move(newBuffer);
rootNode->size = newSize;
}
vector<uchar> buffer;
Vector2<uint32> textureSize = 512;
std::unique_ptr<TextureNode> rootNode = nullptr;
}
int main()
{
TextureAtlas atlas;
atlas.Create({256, 256};
Font font;
// size
font.Create("arial.ttf", 100.0f);
std::string chars = "ABCDEFGHIJKLMNOPRSTUWXYZabcdefghijklmnoprstuwzyw1234567890";
for(const auto& c : chars) {
auto txt = font.LoadGlyph(c);
atlas.PackTexture(txt.data(), txt.w, txt.h);
}
}
在图集space没有改动的情况下有效。当没有 space 剩余并且需要调整图集大小时,大小发生变化,但算法找不到放置另一个纹理的位置,它在 PackTexture(..)
方法中命中 assert(node != nullptr);
。你可以使用任何你想要的纹理。结果是一样的。 运行 通过断点执行代码后,看起来 rootNode
改变了它的大小,但 left
和 right
仍然保持不变。 Nullptr
由 Pack(..)
方法中的此步骤返回:
if (realSize.x < size.x || realSize.y < size.y) {
// Not big enough
return nullptr;
}
这是为什么?如何更改算法,使其能够正确处理大小调整?
您没有正确设置根的大小,所以 realSize
永远不会调整。
请注意,默认构造的 TextureAtlas
处于无效状态。为什么有单独的 Create
?
此外,您的析构函数与默认析构函数执行相同的操作。最好省略。
explicit TextureAtlas(const Vector2<uint32>& size = { 512, 512 })
: textureSize(size),
rootNode(std::make_unique<TextureNode>(0, { INT_MAX, INT_MAX })),
buffer(size.w * size.h)
{
}