跳转至内容

OpenGL 编程/现代 OpenGL 入门

来自 Wikibooks,开放世界中的开放书籍
我们的第一个程序

大多数关于 OpenGL 的文档都使用了即将被弃用的功能,特别是“固定流水线”。OpenGL 2.0 及更高版本配备了可编程流水线,其中可编程部分由着色器完成,着色器使用类似 C 语言的GLSL编写。

本文档适用于学习 OpenGL 并希望从一开始就使用现代 OpenGL 的人员。可编程流水线更灵活,但不如固定流水线直观。但是,我们将确保我们首先使用简单的代码。我们将使用类似于 NeHe 的 OpenGL 1.x 教程的方法,通过示例和教程来更好地理解可编程流水线背后的理论。

最初,与旧的立即模式和固定渲染流水线相比,顶点数组和着色器看起来像是令人头疼的东西。[1]但是,最终,特别是如果您使用缓冲区对象,您的代码将更加简洁,图形将更快

此页面中的代码示例属于公有领域。请随意使用它们。

分享此文档给您的朋友!Wikibooks 值得获得更多认可和贡献 :)

备注

  • 在某种程度上可以混合使用固定流水线和可编程流水线,但固定流水线正在被弃用,并且在 OpenGL ES 2.0(或其 WebGL 派生版本)中完全不可用,因此我们不会使用它。
  • 现在有 OpenGL 3 和 4,它们显著引入了几何着色器,但这一次与之前的版本相比,它只是一个轻微的演变。由于截至 2012 年它在移动平台上不可用,因此我们目前将专注于 OpenGL 2.0。

基础库

[编辑 | 编辑源代码]

您需要确保 OpenGL、SDL2 和 GLEW 已准备好使用。

安装页面中描述了如何设置系统。

要将这些库置于 OpenGL 堆栈中,请查看API、库和缩写

注意:我们选择 SDL2 是因为它非常便携,并且同时面向桌面和移动平台。我们不会使用高级功能,并且我们几乎所有的代码都将是纯 OpenGL,因此您在切换到其他库(如 FreeGLUT、GLFW 或 SFML)时不会有任何麻烦。我们可能会编写专门的教程来介绍如何切换到这些库。

代码示例

[编辑 | 编辑源代码]

您可以使用 Git 从gitlab 仓库下载源代码示例。

您可以直接运行它们,或者按照说明逐步创建您的第一个 OpenGL 应用程序。

在 2D 中显示三角形

[编辑 | 编辑源代码]

让我们从简单开始 :) 而不是与一个复杂的程序作斗争,这个程序需要很长时间才能第一次运行,我们的目标是获得一个基本但功能齐全的程序,然后我们可以逐步对其进行改进。

三角形是 3D 编程中最基本的单元。实际上,您在视频游戏中看到的所有内容都是由三角形组成的!小的、带纹理的三角形,但仍然是三角形 :)

为了在可编程流水线中显示三角形,我们至少需要

  • 一个 Makefile 来构建我们的应用程序
  • 初始化 OpenGL 和辅助库
  • 一个包含三角形 3 个顶点(顶点的复数形式,即 3D 点)坐标的数组
  • 一个 GLSL 程序,其中包含
    • 一个顶点着色器:我们将每个顶点单独传递给它,它将计算它们在屏幕上的(2D)坐标
    • 一个片段(像素)着色器:OpenGL 将传递给它包含在我们三角形中的每个像素,它将计算其颜色
  • 将顶点传递给顶点着色器

代码示例存储库(请参阅页面末尾的链接)使用 Makefile,因为它们是最简单的构建工具。

GNU/Linux 或 MinGW

[编辑 | 编辑源代码]

为我们的示例配置“make”非常容易。在“Makefile”文件中写入以下内容

LDLIBS=-lglut -lGLEW -lGL -lSDL2
all: triangle
clean:
	rm -f *.o triangle
.PHONY: all clean

要编译您的应用程序,请在终端中输入

make

一个更具可移植性的 Makefile 如下所示

CPPFLAGS=$(shell sdl2-config --cflags) $(EXTRA_CPPFLAGS)
LDLIBS=$(shell sdl2-config --libs) -lGLEW $(EXTRA_LDLIBS)
EXTRA_LDLIBS?=-lGL
all: triangle
clean:
	rm -f *.o triangle
.PHONY: all clean

这允许您键入make clean,它将删除三角形二进制文件和可能已生成的任何中间对象文件。.PHONY 规则用于告诉 make“all”和“clean”不是文件,因此如果在同一目录中实际存在名为这样的文件,它不会感到困惑。

有关其他 MinGW/MXE 设置,请参阅安装/Windows

Mac OS X 的 Makefile 如下所示

CFLAGS=-I/opt/local/include/
LDFLAGS=-L/opt/local/lib/ -I/opt/local/include/
LDLIBS=-lGLEW -framework GLUT -framework OpenGL -framework Cocoa
all: triangle
clean:
        rm -f *.o triangle

此 makefile 假设您使用MacPorts安装了 glew。

如果您想使用不同的编程环境,请参阅设置 OpenGL部分。

其他系统

[编辑 | 编辑源代码]

请参阅安装页面以获取有关如何配置构建环境的提示。

初始化

[编辑 | 编辑源代码]

让我们创建一个名为triangle.cpp的文件

/* Using standard C++ output libraries */
#include <cstdlib>
#include <iostream>
using namespace std;

/* Use glew.h instead of gl.h to get all the GL prototypes declared */
#include <GL/glew.h>
/* Using SDL2 for the base window and OpenGL context init */
#include <SDL2/SDL.h>
/* ADD GLOBAL VARIABLES HERE LATER */

bool init_resources(void) {
  /* FILLED IN LATER */
  return true;
}

void render(SDL_Window* window) {
  /* FILLED IN LATER */
}

void free_resources() {
  /* FILLED IN LATER */
}

void mainLoop(SDL_Window* window) {
	while (true) {
		SDL_Event ev;
		while (SDL_PollEvent(&ev)) {
			if (ev.type == SDL_QUIT)
				return;
		}
		render(window);
	}
}

int main(int argc, char* argv[]) {
	/* SDL-related initialising functions */
	SDL_Init(SDL_INIT_VIDEO);
	SDL_Window* window = SDL_CreateWindow("My First Triangle",
		SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
		640, 480,
		SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL);
	SDL_GL_CreateContext(window);

	/* Extension wrangler initialising */
	GLenum glew_status = glewInit();
	if (glew_status != GLEW_OK) {
		cerr << "Error: glewInit: " << glewGetErrorString(glew_status) << endl;
		return EXIT_FAILURE;
	}

	/* When all init functions run without errors,
	   the program can initialise the resources */
	if (!init_resources())
		return EXIT_FAILURE;

	/* We can display something if everything goes OK */
	mainLoop(window);

	/* If the program exits in the usual way,
	   free resources and exit with a success */
	free_resources();
	return EXIT_SUCCESS;
}

init_resources中,我们将创建我们的 GLSL 程序。在render中,我们将绘制三角形。在free_resources中,我们将销毁 GLSL 程序。

顶点数组

[编辑 | 编辑源代码]

我们的第一个三角形将在 2D 中显示 - 我们很快就会转向更复杂的内容。我们使用其 3 个点的 2D(x,y)坐标来描述三角形。默认情况下,OpenGL 的坐标在 [-1,1] 范围内。

	GLfloat triangle_vertices[] = {
	    0.0,  0.8,
	   -0.8, -0.8,
	    0.8, -0.8,
	};

现在让我们先记住这个数据结构,我们稍后将在代码中编写它。

注意:坐标介于 -1 和 +1 之间,但我们的窗口不是正方形!在下一课中,我们将了解如何修复纵横比。

顶点着色器

[编辑 | 编辑源代码]

这是 GLSL 程序,它将逐个获取我们数组中的每个点,并告诉在哪里将它们放在屏幕上。在本例中,我们的点已在 2D 屏幕坐标中,因此我们不会更改它们。我们的 GLSL 程序如下所示

#version 120
attribute vec2 coord2d;
void main(void) {
  gl_Position = vec4(coord2d, 0.0, 1.0);
}
  • #version 120 表示 v1.20,即 OpenGL 2.1 中 GLSL 的版本。
  • OpenGL ES 2 的 GLSL 也基于 GLSL v1.20,但其版本为 1.00(#version 100)。[2]
  • coord2d 是当前顶点;它是我们需要在 C 代码中声明的输入变量
  • gl_Position 是最终的屏幕位置;它是一个内置的输出变量。
  • vec4 获取我们的 *x* 和 *y* 坐标,然后是 *z* 坐标的 0。最后一个,*w*=1.0 用于齐次坐标(用于 变换矩阵)。

现在我们需要让 OpenGL 编译这个着色器。在 main 函数上方开始 init_resources 函数。

bool init_resources() {
	GLint compile_ok = GL_FALSE, link_ok = GL_FALSE;

	GLuint vs = glCreateShader(GL_VERTEX_SHADER);
	const char *vs_source =
		//"#version 100\n"  // OpenGL ES 2.0
		"#version 120\n"  // OpenGL 2.1
		"attribute vec2 coord2d;                  "
		"void main(void) {                        "
		"  gl_Position = vec4(coord2d, 0.0, 1.0); "
		"}";
	glShaderSource(vs, 1, &vs_source, NULL);
	glCompileShader(vs);
	glGetShaderiv(vs, GL_COMPILE_STATUS, &compile_ok);
	if (!compile_ok) {
		cerr << "Error in vertex shader" << endl;
		return false;
	}
}

我们将源代码作为字符串传递给 glShaderSource(稍后我们将以不同且更方便的方式读取着色器代码)。我们指定类型 GL_VERTEX_SHADER

片段着色器

[编辑 | 编辑源代码]

一旦 OpenGL 获取了我们的 3 个点屏幕位置,它将填充它们之间的空间以形成一个三角形。对于 3 个点之间的每个像素,它将调用片段着色器。在我们的片段着色器中,我们将说明我们希望将每个像素颜色设置为蓝色(这设置了 RGB - 红、绿、蓝颜色分量)。

#version 120
void main(void) {
  gl_FragColor[0] = 0.0;
  gl_FragColor[1] = 0.0;
  gl_FragColor[2] = 1.0;
}

我们以类似的方式编译它,类型为 GL_FRAGMENT_SHADER。让我们继续我们的 init_resources 过程。

	GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
	const char *fs_source =
		//"#version 100\n"  // OpenGL ES 2.0
		"#version 120\n"  // OpenGL 2.1
		"void main(void) {        "
		"  gl_FragColor[0] = 0.0; "
		"  gl_FragColor[1] = 0.0; "
		"  gl_FragColor[2] = 1.0; "
		"}";
	glShaderSource(fs, 1, &fs_source, NULL);
	glCompileShader(fs);
	glGetShaderiv(fs, GL_COMPILE_STATUS, &compile_ok);
	if (!compile_ok) {
		cerr << "Error in fragment shader" << endl;
		return false;
	}

GLSL 程序

[编辑 | 编辑源代码]

GLSL 程序是顶点着色器和片段着色器的组合。通常它们一起工作,顶点着色器甚至可以将其他信息传递给片段着色器。

#include 下方创建一个全局变量来存储程序句柄。

GLuint program;

以下是将顶点和片段着色器链接到程序中的方法。继续我们的 init_resources 过程,使用以下代码:

	program = glCreateProgram();
	glAttachShader(program, vs);
	glAttachShader(program, fs);
	glLinkProgram(program);
	glGetProgramiv(program, GL_LINK_STATUS, &link_ok);
	if (!link_ok) {
		cerr << "Error in glLinkProgram" << endl;
		return false;
	}

将三角形顶点传递给顶点着色器

[编辑 | 编辑源代码]

我们提到过,我们使用 coord2d 属性将每个三角形顶点传递给顶点着色器。以下是如何在 C 代码中声明它。

首先,让我们创建一个第二个全局变量。

GLint attribute_coord2d;

使用以下代码结束我们的 init_resources 过程:

	const char* attribute_name = "coord2d";
	attribute_coord2d = glGetAttribLocation(program, attribute_name);
	if (attribute_coord2d == -1) {
		cerr << "Could not bind attribute " << attribute_name << endl;
		return false;
	}

	return true;
}

现在我们可以将三角形顶点传递给顶点着色器。让我们编写我们的 render 过程。每个部分在注释中都有解释。

void render(SDL_Window* window) {
	/* Clear the background as white */
	glClearColor(1.0, 1.0, 1.0, 1.0);
	glClear(GL_COLOR_BUFFER_BIT);

	glUseProgram(program);
	glEnableVertexAttribArray(attribute_coord2d);
	GLfloat triangle_vertices[] = {
	    0.0,  0.8,
	   -0.8, -0.8,
	    0.8, -0.8,
	};
	/* Describe our vertices array to OpenGL (it can't guess its format automatically) */
	glVertexAttribPointer(
		attribute_coord2d, // attribute
		2,                 // number of elements per vertex, here (x,y)
		GL_FLOAT,          // the type of each element
		GL_FALSE,          // take our values as-is
		0,                 // no extra data between each position
		triangle_vertices  // pointer to the C array
						  );
	
	/* Push each element in buffer_vertices to the vertex shader */
	glDrawArrays(GL_TRIANGLES, 0, 3);
	
	glDisableVertexAttribArray(attribute_coord2d);

	/* Display the result */
	SDL_GL_SwapWindow(window);
}

glVertexAttribPointer 告诉 OpenGL 从 init_resources 中创建的数据缓冲区中检索每个顶点,并将其传递给顶点着色器。这些顶点定义了每个点的屏幕位置,形成一个三角形,其像素由片段着色器着色。

注意:在下一个教程中,我们将介绍顶点缓冲对象,这是一种稍微复杂一些且更新的方法,用于在图形卡中存储顶点。

唯一剩下的部分是 free_resources,用于在退出程序时进行清理。在本例中它不是必需的,但以这种方式构建应用程序是一个好习惯。

void free_resources() {
	glDeleteProgram(program);
}

我们的第一个 OpenGL 2.0 程序完成了!

如果失败

[编辑 | 编辑源代码]

下一个教程将为我们第一个极简代码添加更多健壮性。确保尝试 tut02 代码(请参阅下面的代码链接)。

实验!

[编辑 | 编辑源代码]
我们的第一个程序,使用另一个片段着色器。

随意尝试此代码。

  • 尝试通过显示 2 个三角形来创建一个正方形。
  • 尝试更改颜色。
  • 阅读我们使用的每个函数的 OpenGL 手册页。
  • 尝试在片段着色器中使用此代码 - 它有什么作用?
gl_FragColor[0] = gl_FragCoord.x/640.0;
gl_FragColor[1] = gl_FragCoord.y/480.0;
gl_FragColor[2] = 0.5;

在下一个教程中,我们将添加一些实用函数,以便更容易地编写和调试着色器。

  1. 由于 OpenGL 2 中删除了许多 3D 功能,因此有些人很有趣地 将其定义为一个 2D 光栅化引擎
  2. "OpenGL ES Shading Language 1.0.17 Specification" (PDF). Khronos.org. 2009-05-12. 检索于 2011-09-10. OpenGL ES Shading Language(也称为 GLSL ES 或 ESSL)基于 OpenGL Shading Language(GLSL)版本 1.20

< OpenGL 编程

浏览和下载 完整代码
华夏公益教科书