OpenGL 编程/现代 OpenGL 入门
大多数关于 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 应用程序。
让我们从简单开始 :) 而不是与一个复杂的程序作斗争,这个程序需要很长时间才能第一次运行,我们的目标是获得一个基本但功能齐全的程序,然后我们可以逐步对其进行改进。
三角形是 3D 编程中最基本的单元。实际上,您在视频游戏中看到的所有内容都是由三角形组成的!小的、带纹理的三角形,但仍然是三角形 :)
为了在可编程流水线中显示三角形,我们至少需要
- 一个 Makefile 来构建我们的应用程序
- 初始化 OpenGL 和辅助库
- 一个包含三角形 3 个顶点(顶点的复数形式,即 3D 点)坐标的数组
- 一个 GLSL 程序,其中包含
- 一个顶点着色器:我们将每个顶点单独传递给它,它将计算它们在屏幕上的(2D)坐标
- 一个片段(像素)着色器:OpenGL 将传递给它包含在我们三角形中的每个像素,它将计算其颜色
- 将顶点传递给顶点着色器
代码示例存储库(请参阅页面末尾的链接)使用 Makefile,因为它们是最简单的构建工具。
为我们的示例配置“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 程序是顶点着色器和片段着色器的组合。通常它们一起工作,顶点着色器甚至可以将其他信息传递给片段着色器。
在 #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;
在下一个教程中,我们将添加一些实用函数,以便更容易地编写和调试着色器。
- ↑ 由于 OpenGL 2 中删除了许多 3D 功能,因此有些人很有趣地 将其定义为一个 2D 光栅化引擎!
- ↑ "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