Window平台OpenGL环境搭建

  本文主要介绍在Window平台下OpenGL环境搭建方法,使用GLFW、GLAD创建OpenGL环境,并展示LearnOpenGL教程中可执行程序的构建方式。

学习资源

  以下推荐一些优秀的OpenGL学习资源:

OpenGL简介

  OpenGL(Open Graphics Library)是一个跨语言、跨平台的应用程序编程接口(API),用于渲染2D和3D矢量图形。该API由Khronos Group维护,它是一个由许多公司(包括Intel、AMD、Nvidia和其他一些大型硬件、软件开发公司)组成的联盟。
  OpenGL允许开发者通过高级别的命令直接操作图形硬件。这些命令包括对几何体和图像的处理,以及对渲染管线的控制等。OpenGL的主要特点是提供了大量的函数,用于绘制复杂的三维场景。
  OpenGL的一些主要特性包括:
  1.硬件加速:OpenGL命令可以直接在GPU上执行,这使得图形渲染非常快。
  2.跨平台:OpenGL API在各种操作系统和图形硬件上都有实现,包括Windows、macOS、Linux等。
  3.渲染管线:OpenGL定义了一个可以用于渲染3D图形的完整管线,包括顶点处理、光照计算、像素处理等步骤。
  4.着色器:OpenGL允许开发者使用GLSL(OpenGL Shading Language)编写自定义的顶点着色器和片元着色器,从而实现复杂的渲染效果。
  5.扩展机制:OpenGL的功能可以通过扩展来增强。硬件厂商可以提供自己的OpenGL扩展,开发者可以在支持这些扩展的硬件上使用更高级的功能。
  OpenGL被广泛应用在CAD、虚拟现实、科学可视化、信息可视化、飞行模拟等许多领域。同时,它也是许多游戏和游戏引擎的基础图形API。

OpenGL环境搭建

环境简介

  本文介绍的环境在Windows平台上搭建,使用CMake作为构建工具,使用GLFW和GLAD创建OpenGL环境。另外,编译器使用MSVC,这意味着你需要安装Visual Studio, 并且需要安装Visual Studio的C++桌面开发工具。当然,MinGW也是可以的,鉴于大部分Windows平台的库都会由MSVC编译的版本,所以还是使用MSVC为好。

  • 操作系统:Windows 11专业版 23H2
  • 开发工具:Visual Studio Code
  • 编译器:MSVC 19.36.32534
  • 构建工具:CMake 3.27

目录组织

  为了学习的方便,本文将工程目录组织如下:

├─CMakeLists.txt
├─build/
├─opengl/
│  ├─glad/
│  ├─glfw/
│  └─glm/
├─resource/
│  └─textures/
└─source/
    ├─inc/
    └─src/
        ├─1_getting_started/
        │  └─shaders/
        ├─2_lighting/
        │  └─shaders/
        └─demo/
            └─shaders/

  其中每个目录及文件的含义分别如下:
  CMakeLists.txt:CMake构建脚本。
  build:默认的CMake构建目录,存放生成的二进制文件。
  opengl:存放Opengl相关的头文件和库文件,如glad、glfw和glm等。
  resource:存放一些资源文件,比如纹理图片。
  source:存放学习过程中编写的源码文件。
  source/inc:学习过程中的一些头文件,比如LearnOpenGL教程中的shader类和camera类等。
  source/src:学习过程中创建的cpp文件。cpp文件按照LearnOpenGL教程的章节目录进行归档,每个章节目录下的shaders/目录保存了该章节所用到的着色器源码。
  最终,构建项目时CMake会根据CMakeLists.txt文件中的配置,遍历source目录,将所有的着色器源码统一拷贝到build/shaders目录下;将resource中用到的所有纹理图片拷贝到build/textures目录下;再将所有的cpp文件按照章节目录名_源文件名的命名方式编译成可执行文件。
  必须说明的是,为了学习方便,本文构建项目时每一个cpp文件都会被单独构建成一个可执行程序,所以不要使用多个cpp文件编译,需要复用的代码和类请以头文件的方式引用。虽然这种做法并不是很好,但是极大地方便了学习过程。本文介绍的项目结构下,CMakeLists.txt文件的内容如下:

cmake_minimum_required(VERSION 3.10)
project(LearnOpenGL)
set(CMAKE_CXX_STANDARD 11)

# 添加头文件路径
include_directories(
    source/inc/
    opengl/
    opengl/glm
    opengl/glad/include
    opengl/glfw/include
)
# cmake禁止msvc 4819警告(和字符集相关的警告)
if(WIN32)
    add_compile_options(/wd4819)
endif()

# 生成glad静态库,设置glfw3.lib的路径,设置需要链接的库
add_library(glad opengl/glad/src/glad.c)
set(GLFW_LIB ${CMAKE_CURRENT_SOURCE_DIR}/opengl/glfw/lib-vc2022/glfw3.lib)
set(LINKED_LIBS glad ${GLFW_LIB} opengl32)

# 编译可执行程序的脚本函数
function(add_executable_target target_name source_file)
    add_executable(${target_name} 
                    # WIN32  # 如果需要在控制台窗口输出信息,那么就将这一行注释掉
                   ${source_file})
    if(MSVC)    # 消除LNK4089警告-———默认msvcrt.lib与其他库的使用冲突
        set_property(TARGET ${target_name} PROPERTY LINK_FLAGS /NODEFAULTLIB:MSVCRT.lib)
    endif()
    target_link_libraries(${target_name} ${LINKED_LIBS})
endfunction()

# 拷贝所有的shader文件到build目录
file(GLOB_RECURSE SHADER_FILES source/src/*.vs source/src/*.fs)
foreach(shader_file IN LISTS SHADER_FILES)
    file(COPY ${shader_file} DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/shaders)
endforeach()
message("---------------------------- message ----------------------------")
message("-----          COPY shader files to bin finished!           -----")
message("-----------------------------------------------------------------")

# 拷贝所有的资源文件到build目录
file(COPY resource/ DESTINATION ${CMAKE_BINARY_DIR}/ FILES_MATCHING PATTERN "*")
message("---------------------------- message ----------------------------")
message("-----         COPY resource files to bin finished!          -----")
message("-----------------------------------------------------------------")

# 编译所有的源文件, 需要保证所有的.cpp文件都可以单独编译成一个可执行程序
# 可以包含其他头文件,但不能多个cpp编译成一个可执行程序。
file(GLOB_RECURSE SRC_SOURCES source/src/*.cpp)
foreach(source IN LISTS SRC_SOURCES)
    get_filename_component(dir_path ${source} DIRECTORY)
    get_filename_component(dir_name ${dir_path} NAME)
    get_filename_component(src_name ${source} NAME_WE)
    set(target_name "${dir_name}_${src_name}")          #将 目录名_源文件名 作为可执行程序的名称
    add_executable_target(${target_name} ${source})
endforeach()

  关于GLFW和GLAD的安装和使用将会在后续章节介绍。

GLFW

  GLFW是一个开源的,多平台的库,用于创建窗口,接收输入和处理事件,主要用于OpenGL和Vulkan的应用程序。GLFW提供了一个简单的API,让开发者可以在不同的操作系统和窗口系统之间抽象出来,这使得它成为了创建跨平台的图形应用程序的理想选择。
  GLFW的主要特性包括:
  1.跨平台:GLFW支持Windows,macOS和许多Unix-like系统(如Linux和FreeBSD)。
  2.创建窗口和上下文:GLFW可以用于创建一个窗口,以及与之关联的OpenGL或Vulkan上下文。你可以控制窗口的各种属性,如尺寸,全屏/窗口模式,可见性等。
  3.输入处理:GLFW可以处理各种输入事件,如键盘,鼠标,触控板和游戏手柄输入。它还支持剪贴板操作(复制和粘贴)。
  4.多窗口支持:GLFW支持创建和管理多个窗口。
  5.高DPI设备支持:GLFW可以在高DPI设备上正常工作,它会自动处理像素密度和坐标的转换。
  6.扩展性:GLFW的设计允许你使用其他库来处理那些GLFW不直接处理的任务,例如加载纹理和模型,创建GUI,网络通信等。
  GLFW是一个非常轻量级的库,它不包括渲染API或者场景图形。它只提供了创建窗口和上下文,处理输入和事件的功能。这使得GLFW非常适合用于学习OpenGL和Vulkan,或者用于创建需要直接控制渲染过程的应用程序。
  前往GLFW官网下载编译好的二进制包,并解压到项目目录下。在本文的项目组织方式下,glfw存放在opengl目录下,目录结构如下(只保留了头文件和本文使用的编译器版本对应的库文件):

├─opengl
│  ├─glfw
│  │  ├─include
│  │  │  └─GLFW
│  │  └─lib-vc2022

  注意:如果你使用的路径不同,请确保设置CMakeLists.txt中正确地链接到GLFW。

GLAD

  GLAD是一个用于管理OpenGL和GLSL版本的库,它可以在运行时加载和使用OpenGL函数。GLAD是一个生成器,可以为你生成专门针对你指定的OpenGL版本和扩展的加载代码。
  在OpenGL的历史中,随着版本的更新,引入了许多新的函数和特性。但是,这些函数并不能直接在所有平台和所有显卡驱动上使用。这是因为OpenGL的实现(通常是显卡驱动)可能不支持所有的OpenGL函数。为了解决这个问题,你可以使用一个加载器(如GLAD)来在运行时检查并加载可用的OpenGL函数。
  使用GLAD的步骤通常如下:
  1.生成加载代码:你可以在GLAD的在线服务中指定你需要的OpenGL版本和扩展,然后GLAD会为你生成一个C或C++源文件和头文件。
  2.添加到项目中:你将生成的源文件和头文件添加到你的项目中,并在你的代码中包含头文件。
  3.初始化GLAD:在你创建OpenGL上下文后(例如,通过GLFW创建窗口和上下文),你需要调用GLAD的初始化函数来加载OpenGL函数。
  4.使用OpenGL函数:一旦GLAD初始化成功,你就可以像平常一样使用OpenGL函数了。如果一个函数在你的系统上不可用,GLAD会返回一个错误。
  GLAD的一个主要优点是它可以生成最小化的加载代码,只包含你实际需要的OpenGL版本和扩展的函数。这使得你的应用程序更小,更快,并且更容易理解和管理。
  前往GLAD的在线服务页面下载对应的源码文件,根据需要填写配置选项:编程语言、OpenGL版本、扩展,勾选Generate a loader选项,并点击GENERATE按钮获取源码文件。

  将下载的源码文件解压到项目目录下。在本文的项目组织方式下,glad存放在opengl目录下,目录结构如下:

├─opengl
│  ├─glad
│  │  ├─include
│  │  │  ├─glad
│  │  │  └─KHR
│  │  └─src

  注意:如果你使用的路径不同,请确保设置CMakeLists.txt中正确地编译glad。

其他库

  其他可能会用到的库,也可以按照同样的方式组织在opengl目录下,只要你能确保在CMakeLists.txt中正确配置即可。

创建可执行程序

  如何使用VS Code创建CMake工程可以猛戳这里
  对于LearnOpenGL的前几个章节,因为没有使用到Shader类和Camera类,所以只需要创建单一的可执行程序即可编译运行。以下部分,则以一个带有Shader类和纹理的可执行程序为例,介绍如何在本文工程组织方式下创建可执行程序。

编辑源码

  示例可执行程序的源码存放在src/1_getting_started/目录下,用到的着色器源码放在src/1_getting_started/shaders/,纹理放在resource/textures/目录下。完整的目录结构如下:

.
|-- CMakeLists.txt
|-- resource
|   `-- textures
|       `-- wall.jpg
`-- source
    |-- inc
    |   `-- shader.h
    `-- src
        `-- 1_getting_started
            |-- helloTriangle.cpp
            `-- shaders
                |-- fragment.fs
                `-- vertex.vs

  其中,各个源代码文件的内容如下:
  1.helloTriangle.cpp

#include <iostream>
#include "shader.h"
#include "GLFW/glfw3.h"

#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600

void framebuffer_size_callback(GLFWwindow* window, int width, int height){
    glViewport(0, 0, width, height);
}

void processInput(GLFWwindow* window){
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS){
        glfwSetWindowShouldClose(window, true);
    }
}

float vertices[] = {
    // ---- 位置 ----    - 纹理坐标 -
    -0.5f, -0.5f, 0.0f,  0.0, 0.0, // 左下角
    0.5f,  -0.5f, 0.0f,  1.0, 0.0, // 右下角
    0.0f,  0.5f,  0.0f,  0.5, 1.0, // 顶部
};

// 为简单起见,省略了一部分对初始化数据的检查
int main(){
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "LearnOpenGL", NULL, NULL);

    glfwMakeContextCurrent(window);
    gladLoadGLLoader(GLADloadproc(glfwGetProcAddress));

    glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // 生成纹理
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);
    // 为当前绑定的纹理对象设置环绕、过滤方式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);   
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    int width, height, nrChannels;
    stbi_set_flip_vertically_on_load(true);
    unsigned char* data = stbi_load("../textures/wall.jpg", &width, &height, &nrChannels, 0);
    if(data){
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    } else {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);

    Shader shader("../shaders/vertex.vs","../shaders/fragment.fs");

    // 准备顶点数据
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // 绑定VAO和VBO,拷贝VBO数据并配置顶点属性
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 配置顶点属性
    // 顶点位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
    // 纹理属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3*sizeof(float)));
    glEnableVertexAttribArray(1);
    // 操作完以后解绑VAO和VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);

    while(!glfwWindowShouldClose(window)){
        glfwPollEvents();
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        shader.use();

        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        glfwSwapBuffers(window);
    }

    // 释放VAO、VBO和着色器程序
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    glfwTerminate();
    return 0;
}

  2.vertex.vs

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

void main(){
    gl_Position = vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}

  3.fragment.fs

#version 330 core
out vec4 FragColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main(){
    FragColor = texture(ourTexture, TexCoord);
}

  注意:源代码中对着色器代码的引用路径以及对纹理图片的引用路径都使用相对路径返回到上一层。这是因为MSVC编译器生成的可执行文件在build/buildType目录下,这里的buildTypeDebugRelease,而CMakeList中的处理是把着色器代码和纹理图片直接放在build/目录下,故需要相对路径返回到build/目录。

编译并运行

  首先,在CMakeLists.txt文件上点击右键,选择配置所有项目(或者保存CMakeLists.txt),将着色器和纹理资源更新到build/目录下。选择生成,等待编译完成,或者直接选择需要运行的可执行程序,等待自动编译后执行程序。如下所示:


当珍惜每一片时光~