在开始我们的探索之旅前,我们首先应该明确 OpenGL 究竟是什么。OpenGL 主要被视为一个 API(应用程序编程接口),它提供了一系列可用于操作图形和图像的函数。然而,OpenGL 本身并非 API,而仅仅是一个规范,由 Khronos Group 开发和维护。

OpenGL 规范明确规定了每个函数的输出结果及其执行方式。开发者需要根据规范 实现 这些函数,并找到相应的解决方案。由于 OpenGL 规范并未提供具体的实现细节,因此实际开发的 OpenGL 版本可以采用不同的实现方式,只要其结果符合规范(从而对用户而言相同)即可。

实际开发 OpenGL 库的是显卡制造商。你购买的每张显卡都支持特定版本的 OpenGL,这些版本是专门为该显卡(系列)开发的。在苹果系统中,OpenGL 库由苹果公司维护;而在 Linux 系统中,则存在着显卡供应商提供的版本以及爱好者对这些库的修改版本。这也意味着,如果 OpenGL 出现任何异常行为,很可能是显卡制造商(或开发/维护该库的人员)的问题。

由于大多数 OpenGL 实现都是由显卡制造商开发的,因此一旦实现中出现错误,通常可以通过更新显卡驱动程序来解决;这些驱动程序包含显卡支持的最新 OpenGL 版本。这也是为什么始终建议定期更新显卡驱动程序的原因之一。

Khronos 公开托管了所有 OpenGL 版本的规范文档。感兴趣的读者可以 在这里找到 OpenGL 3.3 版本的规范(我们将使用这个版本),如果您想深入了解 OpenGL 的细节,这份规范值得一读(请注意,其中大部分内容仅描述结果,而非具体实现)。这些规范也为理解其函数的具体工作原理提供了极佳的参考。

核心模式与即时模式#

过去,使用 OpenGL 意味着在即时模式(通常称为固定功能管线)下进行开发,这是一种易于使用的图形绘制方法。OpenGL 的大部分功能都隐藏在库内部,开发者对 OpenGL 的计算方式几乎没有控制权。随着开发者对灵活性的需求不断增长,规范也随之变得更加灵活,开发者获得了对图形的更多控制权。即时模式虽然易于使用和理解,但效率极低。因此,从 3.2 版本开始,规范逐渐弃用即时模式的功能,并鼓励开发者使用 OpenGL 的核心配置文件模式进行开发。核心配置文件模式是 OpenGL 规范的一个分支,它移除了所有旧的、已弃用的功能。

使用 OpenGL 的核心配置文件时,OpenGL 会强制我们使用现代编程实践。每当我们尝试使用 OpenGL 已弃用的函数时,OpenGL 都会抛出错误并停止绘制。学习现代方法的优势在于其灵活性和高效性。然而,它也更难学习。立即模式抽象了 OpenGL 执行的许多 实际操作,虽然易于学习,但很难理解 OpenGL 的实际运行机制。现代方法要求开发者真正理解 OpenGL 和图形编程,虽然这有点难度,但它提供了更大的灵活性、更高的效率,最重要的是:更深入地理解图形编程。

这也是本书以核心配置文件 OpenGL 3.3 版本为目标的原因。虽然难度更高,但绝对值得付出努力。

截至目前,OpenGL 已有更高版本可供选择(撰写本文时为 4.6 版本),您可能会问:既然 OpenGL 4.6 都发布了,为什么还要学习 OpenGL 3.3 呢?答案其实很简单。从 3.3 版本开始,所有后续的 OpenGL 版本都在 OpenGL 的基础上添加了额外的实用功能,但并未改变 OpenGL 的核心机制;新版本只是引入了更高效或更实用的方法来完成相同的任务。因此,所有概念和技术在现代 OpenGL 版本中都保持不变,所以学习 OpenGL 3.3 完全没问题。当您准备好或经验更加丰富时,就可以轻松地使用更高版本 OpenGL 中的特定功能。

如果使用最新版 OpenGL 的功能,则只有最先进的显卡才能运行您的应用程序。因此,大多数开发者通常会选择较低版本的 OpenGL,并根据需要启用更高版本的功能。

有些章节中会介绍一些更现代的功能,这些功能都会特别注明。

扩展#

OpenGL 的一大优势在于其对扩展的支持。每当图形公司开发出新的渲染技术或大型优化方案时,通常都会在驱动程序中实现相应的扩展。如果应用程序运行的硬件支持此类扩展,开发者就可以利用扩展提供的功能来实现更高级或更高效的图形效果。这样一来,图形开发者无需等待 OpenGL 在未来的版本中正式加入这些功能,只需检查显卡是否支持该扩展即可使用这些新的渲染技术。通常情况下,当某个扩展流行或非常实用时,它最终会被纳入到未来的 OpenGL 版本中。

开发者在使用任何扩展程序之前,必须先查询它们是否可用(或者使用 OpenGL 扩展库)。这样,开发者就可以根据扩展程序是否可用,更好地或更高效地完成工作:

if(GL_ARB_extension_name)
{
    // Do cool new and modern stuff supported by hardware
}
else
{
    // Extension not supported: do it the old way
}

使用 OpenGL 3.3 版本,大多数技术很少需要扩展,但凡有必要的地方都会提供相应的说明。

状态机#

OpenGL 本身就是一个大型状态机:它由一系列变量组成,这些变量定义了 OpenGL 当前的运行方式。OpenGL 的状态通常被称为 OpenGL 上下文。在使用 OpenGL 时,我们经常通过设置一些选项、操作一些缓冲区,然后使用当前上下文进行渲染来改变其状态。

例如,当我们告诉 OpenGL 我们现在想绘制直线而不是三角形时,我们实际上是通过改变某个上下文变量来改变 OpenGL 的状态,该变量决定了 OpenGL 的绘制方式。一旦我们通过告诉 OpenGL 应该绘制直线来改变上下文,接下来的绘制命令就会绘制直线而不是三角形。

在使用 OpenGL 时,我们会遇到一些==改变上下文的状态函数,以及一些根据 OpenGL 当前状态执行操作的状态函数。==只要记住 OpenGL 本质上是一个大型状态机,它的大部分功能就更容易理解了。

对象#

OpenGL 库是用 C 语言编写的,并允许用其他语言进行多种衍生,但其核心仍然是一个 C 库。由于 C 语言的许多结构难以很好地转换为其他高级语言,因此 OpenGL 在开发时就考虑到了多种抽象概念。OpenGL 中的对象就是其中一种抽象概念。

在 OpenGL 中,对象是一组选项的集合,代表 OpenGL 状态的一个子集。例如,我们可以创建一个对象来表示绘图窗口的设置;我们可以设置它的大小、支持的颜色数量等等。可以将对象想象成类似 C 语言的结构体:

struct object_name {
    float  option1;
    int    option2;
    char[] name;
};

当我们想要使用对象时,它通常看起来像这样(其中 OpenGL 的上下文被可视化为一个大型结构体):

// The State of OpenGL
struct OpenGL_Context {
  ...
  //context.object_Window_Target = getAddressOf(objectId); 这是给指针(也就是插槽)赋值,让指针指向objectId对应的那块内存(下面马上讲到)
  object_name* object_Window_Target;
  ...  
};
// create object
unsigned int objectId = 0;

//OpenGL 在内部账本上分配了一个数字(假设是 1)
//1. 当你只需要 1 个对象时,你传入一个变量的地址 &objectId。
//2. 当你需要 多个 对象时,你通常会传入一个数组。
glGenObject(1, &objectId);

// bind/assign object to context
//OpenGL 状态机里的 GL_WINDOW_TARGET 这个“插槽”,现在插上了 objectId 指向的那个配置包。
glBindObject(GL_WINDOW_TARGET, objectId);


// set options of object currently bound to GL_WINDOW_TARGET
//你通过 GL_WINDOW_TARGET 这个接口,把 800 和 600 写入了 objectId 对应的显存区域。
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH,  800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);

//把插槽清空(指向 0)。
// set context target back to default
glBindObject(GL_WINDOW_TARGET, 0);

这段代码展示了在使用 OpenGL 时经常会遇到的工作流程。首先,我们创建一个对象,并将其引用存储为 ID(对象的实际数据存储在后台)。然后,我们将该对象(使用其 ID)绑定到上下文的目标位置(示例窗口对象的目标位置定义为 GL_WINDOW_TARGET )。接下来,我们设置窗口选项,最后通过将窗口目标的当前对象 ID 设置为 0 来解除对象的绑定我们设置的选项存储在 objectId 引用的对象中,并在我们将对象重新绑定到 GL_WINDOW_TARGET 时立即恢复

目前提供的代码示例只是对 OpenGL 运行方式的近似描述;在本书中,您将会遇到足够多的实际示例。

使用这些对象的好处在于,我们==可以在应用程序中定义多个对象,设置它们的选项,并在每次启动使用 OpenGL 状态的操作时,将对象绑定到我们预设的设置。==例如,有些对象可以作为 3D 模型数据(例如房屋或角色)的容器,当我们想要绘制其中一个模型时,只需绑定包含该模型数据的对象(这些对象需要事先创建并设置选项)。拥有多个对象允许我们指定多个模型,并且每当我们想要绘制特定模型时,只需在绘制之前绑定相应的对象,而无需再次设置所有选项。

让我们开始吧#

现在你已经对 OpenGL 规范和库有了一定的了解,知道了 OpenGL 的底层工作原理以及它使用的一些自定义技巧。即使你没有完全理解也不用担心;本书将逐步讲解,并提供足够的示例,帮助你真正掌握 OpenGL。