为什么调整图集大小后纹理无法在纹理图集中找到其位置?

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 改变了它的大小,但 leftright 仍然保持不变。 NullptrPack(..) 方法中的此步骤返回:

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)
{
}