LearnOpenGL从入门到入魔(1):OpenGL简介

687 阅读7分钟

1. OpenGL简介

 OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由OpenGL库的开发者(显卡生产商)。广义来说,OpenGL是一套开放图形库,它包含了一系列可以操作图形、图像的函数。

1.1 立即渲染模式与核心模式

  • 立即渲染模式

 早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由,立即渲染模式确实容易使用和理解,但是效率太低。

  • 核心模式

 从OpenGL3.2开始,规范文档开始废弃立即渲染模式,当使用OpenGL的核心模式时,OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时,OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率,然而也更难于学习。现代函数要求使用者真正理解OpenGL和图形编程,它有一些难度,然而提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。

1.2 状态机与对象

  • 状态机

 OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

  • 对象

 OpenGL库是用C语言写的,同时也支持多种语言的派生,但其内核仍是一个C库。在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体(Struct):

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

当我们使用一个对象时,通常看起来像如下一样(把OpenGL上下文看作一个大的结构体):

// OpenGL的状态
struct OpenGL_Context {
    ...
    object* object_Window_Target;
    ...     
};

使用OpenGL时常见的工作流:

// 创建对象
// objectId为对象的引用
unsigned int objectId = 0;
glGenObject(1, &objectId);
// 绑定对象至上下文(目标位置GL_WINDOW_TARGET)
glBindObject(GL_WINDOW_TARGET, objectId);
// 设置当前绑定到 GL_WINDOW_TARGET 的对象的一些选项
// 设置的选项将被保存在objectId所引用的对象中
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
// 将上下文对象设回默认
glBindObject(GL_WINDOW_TARGET, 0);

2. OpenGL环境搭建

 本系列教程主要分为两个部分:OpenGLOpenGL ES,前者主要应用于PC端,后者主要应用于移动端。本文主要介绍基于Windows10平台OpenGL环境的搭建,即Window10+VS2019+OpenGL3.3。VS安装过程就不详细说了,自己网上搜一下就好了。至于OpenGL,在我们安装VS的时候就已经包含在Mircosoft SDK中,无需安装,在项目中直接使用就好。但是有个问题,OpenGL渲染显示需要窗口承载,但不同的操作系统对窗口的创建方式不同,因此OpenGL库中并没有提供创建窗口的API,而是把这部分抽象出去让操作系统自己处理。对于Windows平台,LearnOpenGL文档中推荐使用GLFWGLFW是一个专门针对OpenGL的C语言库,创建一个OpenGL上下文(Context)和一个用于显示的窗口

2.1 构建GLFW

 GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文,定义窗口参数以及处理用户输入。为了保证完整性,我们从GLFW官方直接下载源码编译得到所需的库文件glfw3.lib。编译步骤如下:

  • 首先,下载GLFW3.3.2源码,并解压到指定目录;

  • 其次,安装Cmake,双击bin目录下cmake-gui.exe启动Cmake GUI;

  • 第三,指定GLFW源码路径和目的路径,依次执行ConfigureGernarate

  • 最后,将build目录下的VS项目导入到VS中(双击GLFW.sln即可),编译即可生成所需的glfw3.lib,位于.../glfw-3.3.2/build/src/Debug目录下,另外,所需的头文件位于../glfw-3.3.2/include目录。

2.2 获得GLAD

 因为OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用,比如:

// 定义函数原型
typedef void (*GL_GENBUFFERS) (GLsizei, GLuint*);
// 找到正确的函数并赋值给函数指针
GL_GENBUFFERS glGenBuffers  = (GL_GENBUFFERS)wglGetProcAddress("glGenBuffers");
// 现在函数可以被正常调用了
GLuint buffer;
glGenBuffers(1, &buffer);

 你可以看到代码非常复杂,而且很繁琐,我们需要对每个可能使用的函数都要重复这个过程。幸运的是,有些库能简化此过程,其中GLAD是目前最新,也是最流行的库。获得GLAD库非常简单,官方提供了一个在线服务,我们按照以下步骤即可获得。

 点击GENRATE后,最终生成一个glad.zip文件,该文件中包含了头文件glad.h和源文件glad.c,在配置OpenGL环境时直接将其当作源码编译就好。

2.3 配置OpenGL开发环境

 OpenGL开发环境配置流程:

  • 首先,打开VS2019,创建一个新的Cmake项目
  • 其次,将GLFWGLAD相关文件拷贝到自定义目录3rdPart下;
  • 最后,配置CmakeLists.txt文件,具体配置如下:
# Cmake版本
cmake_minimum_required (VERSION 3.8)

# Project名称
project ("LearnOpenGL")

# 导入第三方库
include_directories(${PROJECT_SOURCE_DIR}/3rdPart/glad/include/)
include_directories(${PROJECT_SOURCE_DIR}/3rdPart/glad/include/)
include_directories(${PROJECT_SOURCE_DIR}/3rdPart/glfw/include/)

# 指定库位置
set(GLFW_LINK ${PROJECT_SOURCE_DIR}/3rdPart/glfw/lib)
link_directories(${GLFW_LINK})

# 将源代码添加到此项目的可执行文件
add_executable (LearnOpenGL         # 要生成的可执行文件
				"LearnOpenGL.cpp"   # 自定义源文件
				"LearnOpenGL.h" 
				${PROJECT_SOURCE_DIR}/3rdPart/glad/src/glad.c # 第三方库源文件
				)

# 将第三方库链接到可执行文件
target_link_libraries(LearnOpenGL glfw3)

 整个项目结构如下:

.
└── LearnOpenGL
    ├── 3rdPart
    │   ├── glad
    │   │   ├── include
    │   │   │   ├── KHR
    │   │   │   │   └── khrplatform.h
    │   │   │   └── glad
    │   │   │       └── glad.h
    │   │   └── src
    │   │       └── glad.c
    │   └── glfw
    │       ├── include
    │       │   └── GLFW
    │       │       ├── glfw3.h
    │       │       └── glfw3native.h
    │       └── lib
    │           └── glfw3.lib
    ├── CMakeLists.txt
    ├── LearnOpenGL.cpp
    ├── LearnOpenGL.h
    ├── README.md
    ├── out
    └── src
        ├── lesson01
        │   ├── CMakeLists.txt
        │   ├── CreateWindow.cpp
        │   └── CreateWindow.hpp
        └── lesson02

2.4 创建窗口

 在配置好GLFWGLAD后,在这一小节,我们利用GLFWGLAD库创建一个纯色的空窗口,下一节将讲解OpenGL库的基本使用。创建窗口流程如下:

  • 首先,初始化GLFW,并配置选项;
  • 其次,创建窗口对象GLFWWindow;
  • 第三,将窗口的上下文设置为当前线程的主上下文;
  • 第四,调用GLFW的glViewport函数设置OpenGL渲染视口的大小;
  • 第五,初始化GLAD;
  • 第六,开启渲染循环,渲染并显示窗口;
  • 最后,释放GLFW资源。

具体源码如下:

// CreateWindow.cpp: 定义应用程序的入口点。
//

#include "CreateWindow.h"
// glad.h优先于glfw,opengl导入
// 因为后者依赖于前者
#include "glad/glad.h"   
#include "GLFW/glfw3.h"

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

using namespace std;

const unsigned int WINDWO_WIDTH = 800;
const unsigned int WINDOW_HEIGHT = 600;

int main()
{
	// 1. 初始化GLFW,并配置选项
	//(1)指定OpenGL版本,便于GLFW创建合适的OpenGL上下文;
	//(2)指定使用的模式为核心模式;
	glfwInit();
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

	// 2. 创建窗口对象GLFWWindow
	// 参数说明:
	//		   width  窗口的宽
	//		   height 窗口的高
	//         title  窗口的名称
	GLFWwindow* window = glfwCreateWindow(WINDWO_WIDTH, WINDOW_HEIGHT, "HelloOpenGL", NULL, NULL);
	if (window == NULL) {
		cout << "create GLFW window failed" << endl;
		glfwTerminate();
		return -1;
	}

	// 3. 将窗口的上下文设置为当前线程的主上下文
	glfwMakeContextCurrent(window);

	// 4. 通过回调的形式,设置OpenGL渲染视口的大小(<=窗口的大小)
	// 即当窗口大小变化时,同步更新渲染窗口的大小
	glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

	// 5. 初始化GLAD
	if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
		cout << "init glad failed" << endl;
		glfwTerminate();
		return -1;
	}
	
	// 6. 开启渲染循环,显示一个纯红色窗口
	while (!glfwWindowShouldClose(window)) {
		// (1) 捕获ESC按键事件
		// 并设置退出渲染循环标识
		processInput(window);

		// (2) render
		// 待实现....

		// (3) 清屏
		// 便于下一次绘制不回显示上一次绘制的内容
		glClearColor(0.3f, 0.6f, 0.3f, 0.8f);
		glClear(GL_COLOR_BUFFER_BIT);
		
		// (4) glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态
		// glfwSwapBuffers函数会交换颜色缓冲,绘制缓冲数据并显示
		glfwSwapBuffers(window);
		glfwPollEvents();
	}

	// 7. 结束渲染,释放GLFW资源
	glfwTerminate();
	return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
	// 设置OpenGL渲染视口大小
	// 参数说明:
	//    x,y  表示位置
	//    width,height 表示渲染窗口宽高
	glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window) {
	// 检测到esc按键,将循环标识置为true
	// 结束循环
	if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
		glfwSetWindowShouldClose(window, true);
	}
}

 执行结果:

.

3. 参考文献

1. LearnOpenGL
2. CMake入门实战
3. CMake从入门到应用


Github源码:LearnOpenGL(如果觉得有用,记得给个小star哈~)