介绍
本教程不是关于OpenGL本身,而是如何使用SFML作为OpenGL的环境,以及如何将它们组合在一起。
如你所知,OpenGL最重要的特性之一是可移植性。但是单靠OpenGL并不足以创建完整的程序:你需要一个窗口、一个渲染上下文、用户输入等等。你别无选择,只能自己编写特定于操作系统的代码来处理这些东西。这就是sfml-window模块发挥作用的地方。让我们看看它如何让你使用OpenGL。
包含并链接OpenGL到你的应用程序
OpenGL头文件在每个操作系统上并不相同。因此,SFML提供了一个“抽象”头文件,负责为您包含正确的文件。
#include <SFML/OpenGL.hpp>
这个头文件包含OpenGL函数,没有别的内容。有些人认为SFML会自动包含OpenGL扩展头文件,因为SFML会自动加载扩展,但这只是实现细节。从用户的角度来看,OpenGL扩展加载必须像任何其他外部库一样处理。
然后,您需要将程序链接到OpenGL库。与头文件不同,SFML不能提供统一的OpenGL链接方式。因此,您需要根据使用的操作系统知道要链接哪个库(在Windows上是“opengl32”,在Linux上是“GL”等)。
OpenGL函数以“gl”前缀开头。当您遇到链接器错误时,请记住这一点,它将帮助您找到您忘记链接的库。
创建一个OpenGL窗口
由于SFML是基于OpenGL的,所以它的窗口已经为OpenGL调用做好了准备,而不需要任何额外的努力。
sf::Window window(sf::VideoMode(800, 600), "OpenGL");
// it works out of the box
glEnable(GL_TEXTURE_2D);
...
如果您认为它太自动化,sf::Window的构造函数有一个额外的参数,它允许您更改底层OpenGL上下文的设置。这个参数是sf::ContextSettings结构的一个实例,它提供了以下设置的访问:
- depthBits是用于深度缓冲区的每像素位数(0为禁用)
- stencilBits是用于模板缓冲区的每像素位数(0为禁用)
- antialiasingLevel是多重采样级别
- majorVersion和minorVersion包括所请求的OpenGL版本
sf::ContextSettings settings;
settings.depthBits = 24;
settings.stencilBits = 8;
settings.antialiasingLevel = 4;
settings.majorVersion = 3;
settings.minorVersion = 0;
sf::Window window(sf::VideoMode(800, 600), "OpenGL", sf::Style::Default, settings);
如果任何这些设置不受显卡支持,SFML会尝试找到最接近的有效匹配。例如,如果4倍抗锯齿过高,它会尝试2倍,然后回退到0。
无论如何,您可以使用getSettings函数检查SFML实际使用的设置:
sf::ContextSettings settings = window.getSettings();
std::cout << "depth bits:" << settings.depthBits << std::endl;
std::cout << "stencil bits:" << settings.stencilBits << std::endl;
std::cout << "antialiasing level:" << settings.antialiasingLevel << std::endl;
std::cout << "version:" << settings.majorVersion << "." << settings.minorVersion << std::endl;
SFML支持OpenGL版本3.0以上(只要您的显卡驱动程序可以处理它们)。在SFML 2.3中添加了选择3.2+上下文的配置文件以及设置上下文调试标志的支持。不支持向前兼容性标志。默认情况下,SFML使用兼容性配置文件创建3.2+ 上下文,因为图形模块使用遗留的OpenGL功能。如果您打算使用图形模块,请务必创建不带核心配置文件设置的上下文,否则图形模块将无法正常工作。在OS X上,SFML仅支持使用核心配置文件创建OpenGL 3.2+上下文。如果您想在OS X上使用图形模块,则仅限于使用遗留上下文,这意味着OpenGL版本为2.1。
典型的OpenGL-with-SFML程序
下面是一个完整的使用SFML编写的OpenGL程序示例:
#include <SFML/Window.hpp>
#include <SFML/OpenGL.hpp>
int main()
{
// create the window
sf::Window window(sf::VideoMode(800, 600), "OpenGL", sf::Style::Default, sf::ContextSettings(32));
window.setVerticalSyncEnabled(true);
// activate the window
window.setActive(true);
// load resources, initialize the OpenGL states, ...
// run the main loop
bool running = true;
while (running)
{
// handle events
sf::Event event;
while (window.pollEvent(event))
{
if (event.type == sf::Event::Closed)
{
// end the program
running = false;
}
else if (event.type == sf::Event::Resized)
{
// adjust the viewport when the window is resized
glViewport(0, 0, event.size.width, event.size.height);
}
}
// clear the buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// draw...
// end the current frame (internally swaps the front and back buffers)
window.display();
}
// release resources...
return 0;
}
在这里,我们不使用window.isOpen()作为主循环的条件,因为我们需要窗口在程序结束之前保持打开状态,以便在循环的最后一次迭代和清理代码时仍具有有效的OpenGL上下文。
如果您有其他问题,请不要犹豫,可以在SFML SDK中查看“OpenGL”和“Window”示例,它们更加完整,并且很可能包含您问题的解决方案。
管理OpenGL上下文
SFML自动为每个创建的窗口提供OpenGL上下文。调用任何OpenGL函数时,它们会在当前活动的上下文上运行。因此,在调用OpenGL函数时,需要激活上下文。如果在调用OpenGL函数时未激活上下文,则函数调用不会产生所需的效果,因为没有状态可以对其产生影响。
为了激活窗口的上下文,使用window.setActive(),该函数与window.setActive(true)相同。在另一个上下文处于活动状态时激活上下文,将导致当前活动的上下文被隐式停用,然后再激活新的上下文。为了显式停用窗口的上下文,请使用window.setActive(false)。如果要在另一个线程上激活上下文,则必须这样做。然而,在每次完成一批OpenGL操作后,建议简单地停用上下文。遵循这个建议,每个操作批次将在激活和停用调用之间可见地包装。可以编写一个RAII帮助程序类来实现此目的。
// activate the window's context
window.setActive(true);
// set up OpenGL states
// clear framebuffers
// draw to the window
// deactivate the window's context
window.setActive(false);
在调试SFML中的OpenGL问题时,第一步始终是确保在调用OpenGL函数时有一个活动上下文。不要假定SFML会隐式激活上下文,或者SFML在调用库时会保留当前活动的上下文。唯一提供的保证是当前线程上激活的上下文在调用window.setActive(true)和window.setActive(false)之间不会更改,只要在两个调用之间没有进行其他调用。在所有其他情况下,必须假定当前上下文可能已更改,因此需要显式重新激活先前的活动上下文,以确保之前的活动上下文再次处于活动状态。还要确保在调用OpenGL函数时激活正确的上下文。活动上下文不仅为OpenGL操作提供执行环境,还指定了任何绘图命令的目标帧缓冲区。在活动上下文没有可见帧缓冲区的情况下调用OpenGL绘图函数将导致这些绘图命令不会产生任何可见输出。将OpenGL操作分为多个上下文还会将状态更改分散在上下文中。如果任何后续绘图操作假定已设置某些状态,则在这种情况下将不会产生正确的结果。
编写OpenGL代码时的一个极其推荐的做法是,在每个OpenGL函数调用后始终检查是否产生了任何OpenGL错误。这可以通过glGetError()函数来完成。在每个函数调用后检查错误将有助于缩小可能出现错误的位置,并显著提高调试效率。
根据可用的上下文版本和功能,必须小心地调用实际在当前上下文中有效的函数。否则,通常会生成GL_INVALID_OPERATION或GL_INVALID_ENUM错误。要查询使用窗口或分别创建的上下文的实际版本和功能,请分别使用window.getSettings()或context.getSettings()。请注意,如果OpenGL实现无法满足所有要求,则这些设置可能与创建上下文时传递的设置不同。建议始终检查创建的上下文实际上是否提供OpenGL代码执行所需的功能。当在更高级别的上下文中加载OpenGL扩展并尝试在更低级别的上下文中使用它们时,这可能会令人困惑,反之亦然。
管理多个OpenGL窗口
管理多个OpenGL窗口并不比管理一个更复杂,只需要记住一些事情就可以。
OpenGL调用是在活动上下文(即活动窗口)上进行的。因此,如果您想在同一程序中绘制到两个不同的窗口,您必须在绘制某些内容之前选择哪个窗口是活动的。这可以使用setActive函数来完成:
// activate the first window
window1.setActive(true);
// draw to the first window...
// activate the second window
window2.setActive(true);
// draw to the second window...
在线程中只能有一个上下文(窗口)处于活动状态,因此您无需在激活另一个窗口之前取消激活窗口,它会自动取消激活。这就是OpenGL的工作原理。
另一个需要知道的是,SFML创建的所有OpenGL上下文共享它们的资源。这意味着您可以在任何活动的上下文中创建纹理或顶点缓冲器,并在任何其他上下文中使用它。这还意味着您无需在重新创建窗口时重新加载所有OpenGL资源。只有可共享的OpenGL资源才可以在上下文之间共享。一个不可共享资源的例子是顶点数组对象。
没有窗口的OpenGL
有时可能需要在没有活动窗口(因此没有OpenGL上下文)的情况下调用OpenGL函数。例如,当您从单独的线程中加载纹理或在创建第一个窗口之前。SFML允许您使用sf::Context类创建无窗口上下文。您只需实例化它即可获得有效的上下文。
int main()
{
sf::Context context;
// load OpenGL resources...
sf::Window window(sf::VideoMode(800, 600), "OpenGL");
...
return 0;
}
多线程渲染
典型的多线程程序配置是在一个线程中处理窗口和其事件(主线程),在另一个线程中进行渲染。如果你这样做,有一个重要的规则需要记住:如果一个上下文(窗口)在另一个线程中已经被激活,那么你不能再次激活它。这意味着在启动渲染线程之前,你必须先取消激活窗口。
void renderingThread(sf::Window* window)
{
// activate the window's context
window->setActive(true);
// the rendering loop
while (window->isOpen())
{
// draw...
// end the current frame -- this is a rendering function (it requires the context to be active)
window->display();
}
}
int main()
{
// create the window (remember: it's safer to create it in the main thread due to OS limitations)
sf::Window window(sf::VideoMode(800, 600), "OpenGL");
// deactivate its OpenGL context
window.setActive(false);
// launch the rendering thread
sf::Thread thread(&renderingThread, &window);
thread.launch();
// the event/logic/whatever loop
while (window.isOpen())
{
...
}
return 0;
}
使用OpenGL配合图形模块
本教程涉及将OpenGL与sfml-window模块混合使用,这相对比较容易,因为这个模块的唯一目的就是与OpenGL混合使用。与graphics模块混合使用则稍微复杂一些:sfml-graphics模块也使用了OpenGL,因此必须特别小心,以避免SFML和用户状态之间发生冲突。
如果你还不了解graphics模块,你只需要知道sf::Window类被替换为sf::RenderWindow,它继承了所有sf::Window函数,并添加了用于绘制SFML特定实体的功能。
避免SFML和自己的OpenGL状态之间发生冲突的唯一方法是在从OpenGL切换到SFML时保存/恢复它们。
- draw with OpenGL
- save OpenGL states
- draw with SFML
- restore OpenGL states
- draw with OpenGL
...
最简单的解决方案是让SFML为你完成,使用pushGLStates/popGLStates函数:
glDraw...
window.pushGLStates();
window.draw(...);
window.popGLStates();
glDraw...
由于SFML对你的OpenGL代码没有任何了解,因此它无法优化这些步骤,结果是它保存/恢复了所有可用的OpenGL状态和矩阵。这对于小型项目来说可能是可以接受的,但对于需要最大性能的大型程序来说可能过于缓慢。在这种情况下,你可以自己处理保存和恢复OpenGL状态,使用glPushAttrib/glPopAttrib、glPushMatrix/glPopMatrix等函数。
如果你这样做,仍然需要在绘制之前恢复SFML的状态。这可以通过resetGLStates函数来实现。
glDraw...
glPush...
window.resetGLStates();
window.draw(...);
glPop...
glDraw...
自己保存和恢复OpenGL状态,可以管理你真正需要的状态,从而减少不必要的驱动程序调用。这样可以提高程序的性能。