跨平台渲染器在OpenGL ES中

5
我正在编写一个跨平台的渲染器。我希望在Windows、Linux、Android和iOS上使用它。
您认为避免绝对抽象并直接使用OpenGL ES 2.0编写是个好主意吗?
据我所知,我应该能够针对标准OpenGL在PC上进行编译,在处理上下文和连接到窗口系统的代码中仅进行小的更改。

你听说过Kivy吗?它是一个开源的跨平台编程语言,支持Linux、Windows、MacOSX、Android和IOS,使用OpenGL渲染所有视图(http://kivy.org)。该语言还包括自己的小部件工具包。我只是想提一下,因为如果已经存在解决方案,而你可能还没有发现它,那么你可能不想重新发明轮子。 - trusktr
如果您拥有Android或iOS设备,请尝试在Play Store或App Store中搜索“Kivy”以查看其使用示例。 - trusktr
3个回答

9
你认为避免绝对抽象并直接使用OpenGL ES 2.0编写是一个好主意吗?你在处理ES 2.0规范中与OpenGL 2.1实际上不同的部分时会遇到困难。例如,你不能将ES 2.0着色器强制转换为桌面GLSL 1.20编译器。在ES 2.0中,需要使用诸如精度指定之类的东西;这些在GLSL 1.20中是非法结构。你可以使用#define来解决这个问题,但这需要一些手动操作。你需要在着色器源文件中插入#ifdef。有一些着色器编译技巧可以使这个过程更加容易。实际上,因为GL ES使用了一个完全不同的扩展集合(尽管其中一些是桌面GL扩展的镜像和子集),你可能想要这样做。每个GLSL着色器(桌面或ES)都需要一个“导言”。着色器中第一个非注释内容必须是一个#version声明。值得庆幸的是,版本在桌面GL 2.1和GL ES 2.0之间是相同的:#version 1.20。问题在于接下来的内容:扩展列表(如果有)。这启用了着色器所需的扩展。由于GL ES使用不同的扩展来自桌面GL,因此你需要更改此扩展列表。而且,由于你很可能需要比桌面GL 2.1扩展更多的GLSL ES扩展,这些列表不仅是1:1映射,而且完全不同的列表。我的建议是利用给GLSL着色器提供多个字符串的能力。也就是说,你的实际着色器文件没有任何导言内容。它们只有实际的定义和函数-着色器的主体。在运行GL ES时,你将在着色器开头添加一个全局导言。在桌面GL中,你将有一个不同的全局导言。代码看起来像这样:
GLuint shader = glCreateShader(/*shader type*/);
const char *shaderList[2];
shaderList[0] = GetGlobalPreambleString(); //Gets preamble for the right platform
shaderList[1] = LoadShaderFile(); //Get the actual shader file
glShaderSource(shader, 2, shaderList, NULL);

引言部分也可以包括一个特定平台的# define。当然是用户定义的。这样,您就可以为不同的平台编写条件代码(#ifdef)。

两者之间还有其他差异。例如,虽然在桌面GL 2.1中有效的ES 2.0纹理上传函数调用会正常工作,但它们不一定是最佳的。 在像所有移动系统这样的大端机器上可以上传的东西将需要来自小端桌面机器驱动程序的一些位操作。 因此,您可能希望在GL ES和桌面GL上指定不同的像素传输参数的方法。

此外,ES 2.0和桌面GL 2.1中有不同的扩展集,您将想要利用它们。尽管它们中的许多尝试相互映射(OES_framebuffer_object是EXT_framebuffer_object的子集),但您可能会遇到类似于上面提到的“不完全子集”的问题。


感谢您详尽的回答。那么您认为创建某种OpenGL渲染器抽象会更好吗?例如,我可以使用Texture2D类表示纹理。该类将包含两个规范共同的内容,但某些内容的实现将不同。 - runnydead
@hubrobin:不需要那么抽象。你只需要在特定的位置加入一些平台相关的代码。但是,如果你的目标是GL 3.3而不是2.1,那么你就需要更多的抽象了。 - Nicol Bolas
我不想在 PC 上支持更多功能。那么,你基本上是在说,这是可行的吗? - runnydead
注意:现代 ARM 设备是小端模式,和 x86 一样。 - karunski

3
根据我的经验,在这种需求下最好的方法是使用纯C语言开发引擎,不需要任何额外的层次。
我是PATRIA 3D引擎的主要开发者,我们基于你提到的基本原则来实现可移植性,只使用基本标准库开发工具。
在不同平台上编译代码的实际努力非常小。
整个解决方案的移植工作量可以根据您想要嵌入引擎的组件进行计算。
例如:
标准C:
3D引擎
游戏逻辑
游戏AI
物理学
+
窗口界面(GLUT,EGL等)-取决于平台,无论如何都可以是桌面的GLUT和移动设备的EGL。
人机界面-取决于移植,Android使用Java,IOS使用OC,桌面版本使用任何版本。
声音管理器-取决于移植
市场服务-取决于移植
通过这种方式,您可以以无缝的方式重新使用95%的工作量。
我们已经采用了这种解决方案来开发我们的引擎,到目前为止,它真的值得最初的投资。

0

以下是我在商业地图和路由库运行的各种平台上实现OpenGL ES 2.0支持的经验结果。

渲染类被设计为在单独的线程中运行。它引用包含地图数据和当前视图信息的对象,并使用互斥锁来避免在绘制时读取该信息时发生冲突。它在图形内存中维护OpenGL ES向量数据的缓存。

所有渲染逻辑均以C++编写,并在以下所有平台上使用。

Windows(MFC)

使用ANGLE库:链接到libEGL.lib和libGLESv2.lib,并确保可执行文件可以访问DLLs libEGL.dll和libGLESv2.dll。 C++代码创建一个线程,以适当的速率(例如每秒25次)重新绘制图形。

Windows(.NET和WPF)

使用C++/CLI包装器创建EGL上下文并调用直接在MFC实现中使用的C++渲染代码。 C++代码创建一个线程,以适当的速率(例如每秒25次)重新绘制图形。

Windows(UWP)

在UWP应用程序代码中创建EGL上下文,并通过C++/CXX包装器调用C++渲染代码。您需要使用SwapChainPanel并创建自己的渲染循环,在不同的线程中运行。请参见GLUWP项目以获取示例代码。

Windows、Linux和Mac OS上的Qt

将QOpenGLWidget用作窗口。使用Qt OpenGL ES包装器创建EGL上下文,然后在paintGL()函数中调用C++渲染代码。

Android

创建实现android.opengl.GLSurfaceView.Renderer的渲染器类。为C++渲染对象创建JNI包装器。在onSurfaceCreated()函数中创建C++渲染对象。在onDrawFrame()函数中调用C++渲染对象的绘图函数。您需要为渲染器类导入以下库:

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLSurfaceView.Renderer;

创建一个继承自GLSurfaceView的视图类。在你的视图类构造函数中,首先设置好EGL配置:
setEGLContextClientVersion(2); // use OpenGL ES 2.0
setEGLConfigChooser(8,8,8,8,24,0);

然后创建您的渲染器类的实例并调用setRenderer来安装它。

iOS

使用METALAngle库,而不是GLKit,因为苹果已经弃用并最终将不再支持GLKit。

创建一个Objective C++渲染器类来调用您的C++ OpenGL ES绘图逻辑。

创建一个从MGLKView派生的视图类。在您的视图类的drawRect()函数中,如果渲染器对象尚不存在,则创建一个渲染器对象,然后调用其绘图函数。也就是说,您的drawRect函数应该像这样:

-(void)drawRect:(CGRect)rect
    {
    if (m_renderer == nil && m_my_other_data != nil)
        m_renderer = [[MyRenderer alloc] init:m_my_other_data];
    if (m_renderer)
        [m_renderer draw];
    }

在你的应用程序中,你需要一个视图控制器类来创建OpenGL上下文并设置它,使用类似以下代码的方式:
MGLContext* opengl_context = [[MGLContext alloc] initWithAPI:kMGLRenderingAPIOpenGLES2];
m_view = [[MyView alloc] initWithFrame:aBounds context:opengl_context];
m_view.drawableDepthFormat = MGLDrawableDepthFormat24;
self.view = m_view;
self.preferredFramesPerSecond = 30;

Linux

在Linux上使用Qt最为简单(参见上文),但也可以使用GLFW框架。在您的应用程序类构造函数中,调用glfwCreateWindow创建一个窗口并将其存储为数据成员。调用glfwMakeContextCurrent使EGL上下文当前,然后创建一个持有渲染器类实例的数据成员;例如:

m_window = glfwCreateWindow(1024,1024,"My Window Title",nullptr,nullptr);
glfwMakeContextCurrent(m_window);
m_renderer = std::make_unique<CMyRenderer>();

在你的应用程序类中添加一个绘制函数:
bool MapWindow::Draw()
    {
    if (glfwWindowShouldClose(m_window))
        return false;
    m_renderer->Draw();
    /* Swap front and back buffers */
    glfwSwapBuffers(m_window);
    return true;
    }

你的main()函数将是:

int main(void)
    {
    /* Initialize the library */
    if (!glfwInit())
        return -1;

    // Create the app.
    MyApp app;

    /* Draw continuously until the user closes the window */
    while (app.Draw())
        {

        /* Poll for and process events */
        glfwPollEvents();
        }

    glfwTerminate();
    return 0;
    }

着色器不兼容

各种OpenGL ES 2.0实现所接受的着色器语言存在不兼容性。我在C++代码中使用以下有条件编译的代码来克服这些问题,放在我的CompileShader函数中:

const char* preamble = "";

#if defined(_POSIX_VERSION) && !defined(ANDROID) && !defined(__ANDROID__) && !defined(__APPLE__) && !defined(__EMSCRIPTEN__)
// for Ubuntu using Qt or GLFW
preamble = "#version 100\n";
#elif defined(USING_QT) && defined(__APPLE__)
// On the Mac #version doesn't work so the precision qualifiers are suppressed.
preamble = "#define lowp\n#define mediump\n#define highp\n";
#endif

然后将 preamble 前缀添加到着色器代码中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接