问题出现时的环境:Windows、Visual Studio 2022 Community、MSVC 对应的 SFML 3.0.0 DLL。

使用 sf::RenderTexture 时,我曾遇到绘制结果意外上下颠倒的问题,原因是遗漏了 RenderTexture::display()。正确的顺序是:完成一批离屏绘制,调用该 RenderTexture 的 display(),再使用它的纹理。

不需要每调用一次 draw() 就调用一次 display()。另外,遗漏它不保证一定表现为翻转;具体症状取决于渲染路径与状态。

问题示例

下面的片段遗漏了离屏目标的 display()。假定 w 是已创建的 sf::RenderWindow,且图片加载成功:

sf::Texture t("1.png");
sf::Sprite s(t);
s.setPosition({100.f, 100.f});

sf::RenderTexture r(w.getSize());
r.clear(sf::Color::Transparent);
r.draw(s);

// 此处遗漏了 r.display()。
w.draw(sf::Sprite(r.getTexture()));
w.display();

w.display() 负责窗口显示,不能代替 r.display() 更新离屏纹理。修正这个片段,只需在使用 r.getTexture() 绘制之前补上:

r.display();

窗口每帧的绘制通常也应先调用 w.clear(),以免保留上一帧的内容。

display() 分别做什么

虽然函数名相同,它们针对的目标不同:

调用 作用 通常的调用时机
r.clear() 清空离屏绘制目标 开始重绘、需要去除旧内容时
r.draw(...) 向离屏目标绘制对象 一批绘制中可调用多次
r.display() 更新离屏目标关联的纹理,完成本批绘制所需的处理 本批绘制结束、纹理被使用前
w.clear() 清空窗口当前绘制缓冲区 每帧开始时
w.draw(...) 向窗口绘制对象 一帧中可调用多次
w.display() 显示窗口当前绘制结果;普通双缓冲窗口会交换缓冲区 一帧绘制结束时

如果 RenderTexture 的内容只绘制一次,之后一直使用这张纹理,就不需要仅因窗口进入下一帧而再次调用 r.display()。如果继续修改离屏内容,则应在再次使用更新后的纹理前调用它。

clear() 是否可以省略,也取决于目的:需要保留已有画面,或保证整个目标都被覆盖时,可以省略;刚创建或调整尺寸后的内容不能假定已经清零。透明度混合等操作可能读取已有颜色,因此“画了一个覆盖全屏的对象”也不一定意味着旧内容无关。

这些是 SFML 的使用约定,不应将“这一回没有看到异常”当成可以省略 display() 的依据。SFML 3.0.0 的 RenderTexture 文档明确要求在绘制结束后调用它。

当时找到解决方法的论坛讨论

当时查找这个问题时,我在 SFML 官方论坛的这条回复中找到了关键提示:

display() is mandatory, on some platforms it does nothing, but on others you may end up with a flipped texture (for example).

这段话指出:display() 是必需的,遗漏调用在某些平台上可能导致纹理翻转。同一回复还解释,clear() 是清空旧内容的便捷方式;是否需要清空,取决于是否要保留旧画面,以及本次绘制是否能完全替换已有内容。

这条讨论给出了排查线索。不过,其中“某些平台上什么也不做”的描述不能直接作为 SFML 3.0.0 的实现结论。对本文所用版本,仍应结合对应的官方文档、源码及下面的复现结果理解 display() 的作用。

为什么遗漏它可能上下颠倒

核对 SFML 3.0.0 源码,可以看到 RenderTexture::display() 不只是一个空操作。它会调用底层实现更新目标纹理,并设置内部纹理的 m_pixelsFlipped 标志、使旧 mipmap 失效。

纹理被绑定用于绘制时,SFML 会根据这个翻转标志调整纹理坐标方向。首次离屏绘制后如果没有调用 display(),方向标志可能尚未按离屏结果设置,因此会出现图像上下颠倒。

底层更新过程还会因实现不同而变化,例如多重采样路径需要将结果解析到目标纹理,回退路径需要复制内容。因此遗漏调用也可能表现为纹理未正确更新等其他问题,不能把“必然翻转”作为跨平台规律。

这段解释来自 RenderTexture.cpp、Texture.cpp 及 RenderTextureImplFBO.cpp 的调用关系。

不依赖外部图片的完整例子

下面用“上红下蓝”的图案代替 1.png,适用于 SFML 3.0.0 和 C++17。离屏图案只生成一次,所以 r.display() 在进入窗口循环前调用一次;窗口每帧仍需自己的 display()。

#include <SFML/Graphics.hpp>

int main()
{
    sf::RenderWindow window(sf::VideoMode({320, 240}), "RenderTexture example");
    window.setFramerateLimit(60);

    sf::RenderTexture r({320, 240});
    r.clear(sf::Color::Transparent);

    sf::RectangleShape top({320.f, 120.f});
    top.setFillColor(sf::Color::Red);
    r.draw(top);

    sf::RectangleShape bottom({320.f, 120.f});
    bottom.setPosition({0.f, 120.f});
    bottom.setFillColor(sf::Color::Blue);
    r.draw(bottom);

    // 两次 draw() 完成后调用一次,再使用结果纹理。
    r.display();
    const sf::Sprite result(r.getTexture());

    while (window.isOpen())
    {
        while (const auto event = window.pollEvent())
        {
            if (event->is<sf::Event::Closed>())
                window.close();
        }
        if (!window.isOpen())
            break;

        window.clear(sf::Color::Black);
        window.draw(result);
        window.display();
    }
}

result 引用 r 内部的纹理,使用期间必须保持 r 和它的纹理有效。该例中 r 的生命周期覆盖整个窗口循环。

若将离屏绘制改成动态内容,把 r.clear()、各次 r.draw() 和 r.display() 一起放到每次需要更新图案的位置;仍然是一批绘制完成后调用一次。

本次复现结果与范围

本次在 Windows x64 上,分别用 GCC/MinGW-w64 15.2.0 与 Visual Studio 2022 Community 17.14.24 的 MSVC 19.44.35222 编译 SFML 3.0.0 动态库,测试程序使用 C++17。显卡为 NVIDIA GeForce RTX 5060 Laptop GPU,OpenGL 报告版本为 4.6.0,驱动标识为 NVIDIA 592.01。

测试使用 64×64 的离屏目标,上半部分纯红,下半部分纯蓝;再通过 sf::Sprite 绘制到另一张 RenderTexture,完成接收端的 display() 后读取像素。这样可以核对同一条纹理绘制路径,而不用依赖人工观察窗口。

操作 接收端上方像素 接收端下方像素 结果
首次绘制后省略源目标的 display() 蓝色 红色 复现上下翻转
两次 draw() 后调用一次源目标的 display() 红色 蓝色 方向正确

同一对象先正确调用过 display(),再绘制新内容但省略下一次调用时,本次非多重采样路径仍可能看到方向正确的新内容。这个观察进一步说明:不能用是否翻转来判断调用顺序是否正确。

上述结果来自所列环境中编译的 SFML 3.0.0 DLL,未覆盖其他平台、抗锯齿配置或所有回退路径。因此可确认问题与修正方式在本次环境中成立,但不能推断每一种环境都会表现为同样的翻转。

参考资料