问题出现时的环境: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,未覆盖其他平台、抗锯齿配置或所有回退路径。因此可确认问题与修正方式在本次环境中成立,但不能推断每一种环境都会表现为同样的翻转。
参考资料
- SFML 3.0.0:RenderTexture 文档:
display()的要求及纹理使用方式。 - SFML 3.0:绘制教程:窗口与离屏绘制的基本顺序。
- SFML 3.0.0:Window.cpp:窗口
display()的实现。 - SFML 官方论坛:主题 27151 中的回复 172836:当时找到解决方法的讨论,关键摘录见上文。