OpenGL简介与安装配置
# OpenGL简介与安装配置
OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)。实际的OpenGL库的开发者通常是显卡的生产商,购买的显卡所支持的OpenGL版本都为这个系列的显卡专门开发的。
# 概念
# 状态机
OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。
假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。
当使用OpenGL的时候,我们会遇到一些状态设置函数(State-changing Function),这类函数将会改变上下文。以及状态使用函数(State-using Function),这类函数会根据当前OpenGL的状态执行一些操作。只要你记住OpenGL本质上是个大状态机,就能更容易理解它的大部分特性。
# 对象
在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。比如,我们可以用一个对象来代表绘图窗口的设置,之后我们就可以设置它的大小、支持的颜色位数等等。可以把对象看做一个C风格的结构体(Struct):
struct object_name {
float option1;
int option2;
char[] name;
};
# 绘制一个球体的流程
在计算机图形学中,只需要表示物体的表面即可,对于计算机,任一个物体都可以用无数个三角形逼近拼凑而成,根据三角形的多少可以分为高模和低模。
我们可以利用一系列坐标表示一个物体:顶点坐标+顶点之间的连接关系。
渲染又可以分为实时渲染和离线渲染,离线渲染可以得到“照片级”渲染,但是计算成本极大。为了高效实时渲染,通常使用光栅化的方法,比路径追踪的射线与三角形求交高效得多。
为了追求真实感、立体感,需要进行着色(shade),在相应的像素点填充符合光学规律的颜色,区分亮暗,使之看起来立体。
GPU渲染一个物体的全部流程
由于图形渲染这一道道工序就像流水线,故得名渲染管线(rendering pipeline)。
# 创建窗口:使用GLFW
在我们作画之前,首先要做的就是创建一个OpenGL上下文和一个用于显示的窗口。然而,这些操作在每个系统上都是不一样的,我们手动处理创建窗口,定义OpenGL上下文以及处理用户输入是相当麻烦的。
幸运的是,有一些库已经提供了我们所需的功能,其中一部分是特别针对OpenGL的。我将使用GLFW,GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文、定义窗口参数以及处理用户输入,对我们来说这就够了。
进入GLFW的github仓库 (opens new window),下载到本地,使用如下命令进行添加链接即可。
add_subdirectory(glfw)
target_link_libraries(${PROJECT_NAME} PUBLIC glfw)
# API加载器:GLAD
古代OpenGL往往使用系统自带的头文件:
#include <GL/gl.h>
在现代OpenGL中不再使用上述头文件,取而代之的是使用GLAD加载器:
#include <glad/glad.h>
glad实际上是一个python包,他所做的是根据你指定的版本,生成加载OpenGL全部函数的glad.c
和glad.h
头文件。例如:
pip install glad
python -m glad --out-path . --generator c --api gl=4.6 --profile compatibility
也可以通过在线生成器 (opens new window)进行配置。
同时,你可以添加CMakeLists.txt
使其能够生成为一个静态库。
当使用了glad,就不要再导入gl.h了。
此外,glad头文件必须放在其他gl相关库的前面
#include <glad/glad.h> #include <GLFW/glfw3.h>
建议自定义一个头文件写这两行,用的时候添加该头文件即可。
# 数学库:glm
安装即可。
# 样板代码与项目结构
以上为环境配置的所有内容,想要测试环境是否成功搭建,可以参考下文,复制相关代码并组织为一个样板代码,进行环境测试。
# 第一个OpenGL程序
画图之前,需要初始化GLFW库
if(!glfwInit()) {
throw std::runtime_error("Failed to initialize GLFW");
}
然后使用GLFW创建一个窗口,之后把刚刚创建的窗口设为接下来gl函数的上下文。
GLFWwindow* window = glfwCreateWindow(640, 480, "Hello OpenGL", NULL, NULL);
if(!window) {
glfwTerminate();
throw std::runtime_error("GLFW failed to create window");
}
glfwMakeContextCurrent(window);
有了上下文之后,就可以初始化GLAD库了。注意,必须先gladLoadGL后才能使用gl函数,不先加载的话,gl函数就还是空指针,调用则会崩溃。
if(!gladLoadGL()) {
glfwTerminate();
throw std::runtime_error("GLAD failed to load GL functions");
}
std::cout << "OpenGL Version: " << glGetString(GL_VERSION) << '\n';
在开始画图之前还有一件重要的事情要做,必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport),这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。我们可以通过调用glViewport函数来设置窗口的维度
glViewport(0, 0, 640, 480);
此时此刻!我们终于可以画图了,只有将画图函数置于不断循环的语句中,图形才会长期存在,可以检测窗口是否被关闭作为循环是否跳出条件:
while (!glfwWindowShouldClose(window)) {
// 清除缓存
glClear(GL_COLOR_BUFFER_BIT);
// 将画图函数封装
render();
// 双缓冲提交
glfwSwapBuffers(window);
// 拉取最新事件
glfwPollEvents();
}
需要注意的是,glfwWindowShouldClose
判断属于一个事件,没有主动获取最新事件的话,依然无法判断事件的发生,所以需要通过glfwPollEvents
获取最新事件,这样才能让glfwWindowShouldClose
生效。
当然,写好的图形绘制代码需要提交才能被驱动应用并绘制出来,利用glfwSwapBuffers()
提交。
当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。我们可以在main函数的最后调用glfwTerminate函数来完成。
glfwTerminate();
return 0;
可以使用函数
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
注册窗口大小正在变化时所做的处理,例如重新设置视口、更新宽高比等。可以在循环加入对按键的检测,实现按下ESC关闭窗口:
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true);
# 项目结构参考
我使用了vcpkg包管理器来管理glm库,同时下载glfw, glad源码作为项目的模块,项目结构如下:
.
├── CMakeLists.txt
├── cmake
│ └── MyUsefulFuncs.cmake
├── glad
│ ├── CMakeLists.txt
│ ├── include
│ │ ├── KHR
│ │ │ └── khrplatform.h
│ │ └── glad
│ │ └── glad.h
│ └── src
│ └── glad.c
├── glfw
│ ├── CMake
│ ├── CMakeLists.txt
│ ├── deps
│ │ ├── ...
│ ├── include
│ │ └── GLFW
│ │ ├── glfw3.h
│ │ └── glfw3native.h
│ └── src
│ └── ...
├── include
└── src
└── main.cpp
根目录下的CMakeLists.txt配置如下,仅供参考:
cmake_minimum_required(VERSION 3.18)
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 添加 include/find_package 搜索 xxx.cmake 文件目录
set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake;${CMAKE_MODULE_PATH}")
# 设置 vcpkg.cmake 目录
set(CMAKE_TOOLCHAIN_FILE "$ENV{MY_VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
project(opengl_demo LANGUAGES CXX C)
if (PROJECT_BINARY_DIR STREQUAL PROJECT_SOURCE_DIR)
message(FATAL_ERROR "The binary directory of CMake cannot be the same as source directory! "
"Do not use 'cmake .'! Please consider use 'cmake -B build' to specify a different binary directory. "
"Otherwise this project may fail to build, or make Git hard to exclude binary files. "
"For Windows, it is highly recommended to use Visual Studio as IDE for CMake projects.")
endif()
message(STATUS "CMake build type: ${CMAKE_BUILD_TYPE}")
if (MSVC) # 1. fix string literial cannot include unicode characters; 2. rid min and max macros; 3. get back M_PI, M_E macros
add_compile_options($<$<COMPILE_LANG_AND_ID:CXX,MSVC>:/utf-8$<SEMICOLON>/DNOMINMAX$<SEMICOLON>/D_USE_MATH_DEFINES>)
add_compile_options($<$<COMPILE_LANG_AND_ID:C,MSVC>:/utf-8$<SEMICOLON>/DNOMINMAX$<SEMICOLON>/D_USE_MATH_DEFINES>)
elseif (CMAKE_COMPILER_IS_GNUCC) # Add necessary checks and warnings
if (CMAKE_BUILD_TYPE MATCHES "[Dd][Ee][Bb][Uu][Gg]")
add_compile_options($<$<COMPILE_LANG_AND_ID:CXX,GNU>:-Wall$<SEMICOLON>-Wextra>)
add_compile_options($<$<COMPILE_LANG_AND_ID:CXX,GNU>:-ftrapv$<SEMICOLON>-fbounds-check$<SEMICOLON>-fsanitize-trap>)
# add_compile_options($<$<COMPILE_LANG_AND_ID:CXX,GNU>:-fsanitize=address$<SEMICOLON>-fsanitize=leak>)
# link_libraries(asan)
endif()
endif()
file(GLOB_RECURSE sources CONFIGURE_DEPENDS src/*.cpp)
file(GLOB_RECURSE headers CONFIGURE_DEPENDS include/*.h include/*.hpp)
add_executable(${PROJECT_NAME} ${sources} ${headers})
target_include_directories(${PROJECT_NAME} PUBLIC include)
find_package(glm CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PUBLIC glm::glm)
add_subdirectory(glfw)
target_link_libraries(${PROJECT_NAME} PUBLIC glfw)
add_subdirectory(glad)
target_link_libraries(${PROJECT_NAME} PUBLIC glad)
target_compile_definitions(${PROJECT_NAME} PUBLIC -DOPENGLTUTOR_HOME=\"${CMAKE_CURRENT_SOURCE_DIR}/\")