OpenGL 学习笔记1 快速上手

您所在的位置:网站首页 clion怎么运行c++语言程序 OpenGL 学习笔记1 快速上手

OpenGL 学习笔记1 快速上手

2023-04-15 03:12| 来源: 网络整理| 查看: 265

Overview

OpenGL (Open Graphics Library) 被认为是一个 API (an Application Programming Interface),提供了一组大型的函数,可以用来操作图形和图像。然而,其实它本身不是API,而只是一个规范 (specification) 。OpenGL 规范了每个函数的输出和执行方式,并不会给出实现细节,具体实现一般由显卡制造商来完成。

除了 OpenGL,当前流行的图形 API 还有 DirectX (11 & 12),Vulkan,Metal。在嵌入式和移动端,通常使用 OpenGL ES,它是 OpenGL 的精简版。

图形 API 通常用于与 GPU 交互,以实现硬件加速渲染(hardware-accelerated rendering)。OpenGL 被定义为一组由客户程序调用的函数(如 glViewport()),以及一组命名的整数常量(如常量GL_TEXTURE_2D 对应于十进制数3553)。

SGI (Silicon Graphics, Inc.) 于1991年开始开发 OpenGL,自2006年以来,OpenGL 由非盈利性技术联盟 Khronos Group 管理。

苹果在2018年6月,在其所有平台将 OpenGL API 标记为弃用(deprecated),但它仍可使用。当前学习 OpenGL,主要是为了学习计算机图形学和图形api的使用和原理。可见的未来,DirectX12 和 Vulkan 将逐渐成为主流。

Core-profile vs Immediate mode

旧时,通常在固定管线(Immediate mode, 直接模式)下使用OpenGL,这种方法易于使用,但开发人员的控制权较少。从3.2版本开始开始废弃固定管线,鼓励开发者在核心模式(Core-profile)下进行开发。这种方法虽然相对困难,但是更加底层,灵活,有效率。能帮助我们更好地理解图形编程。

State machine

OpenGL 本身是一个大的状态机(State machine),用一组变量来定义 OpenGL 当下该如何进行操作。OpenGL的状态通常称为 OpenGL 上下文。在使用OpenGL时,我们经常通过设置一些选项、操作一些缓冲区,然后使用当前上下文进行渲染来改变它的状态。

Object

对象(Object)是一个选项集合,表示 OpenGL 状态的子集,例如:

// The State of OpenGL struct OpenGL_Context { ... object_name* object_Window_Target; ... }; int main() { // create object unsignedint objectId = 0; glGenObject(1, &objectId); // bind/assign object to context glBindObject(GL_WINDOW_TARGET, objectId); // set options of object currently bound to GL_WINDOW_TARGET glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800); glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600); // set context target back to default glBindObject(GL_WINDOW_TARGET, 0); }

将 OpenGL 的上下文可视化为一个大的结构体。

首先创建一个对象,并获得它对应的 ID。然后将其绑定到上下文的目标位置。接下来,我们设置选项,最后通过将目标位置的当前对象id设置为0来解除绑定对象。我们设置的选项存储在 objectId 引用的对象中,并在重新将对象绑定回 GL_WINDOW_TARGET 时恢复。

Create a window

由于 OpenGL 只关注渲染部分,我们需要自己来创建窗口,定义上下文并处理用户输入。这里我们采用 GLFW 库。

IDE 的话,我使用的是 Jetbrains CLion。编译器采用 Mingw-w64。

Building GLFW

通过官网下载 GLFW Source package,解压,并使用 CMake GUI 来构建64位二进制文件。source code 填 GLFW 解压后的目录,build the binaries 填 glfw 下新建一个 build 目录。环境我用的是 mingw makefiles。

构建完之后,cd 进入 build 目录,使用 mingw32-make all 命令。等进度条结束之后,在 src 目录下发现 libglfw3.a 静态链接库文件,将其复制到我们的 CLion 项目中的 lib 目录下。再将 GLFW 文件夹放在项目的 include 目录下。

+--bin +--include | +--GLFW | +--glfw3.h | +--glfw3native.h +--lib | +--libglfw3.a +--src | +--main.cpp +--CMakeLists.txtSetting up GLAD

在 GLAD 官网中,Language 选择 C/C++,Specification 选择 OpenGL,API 选择 gl version 3.3,Profile 选择 Compatibility,然后勾选上 Generate a loader。点击 Generate,下载生成的 zip 文件,解压。

使用 CLion 的 C++ 库项目可以将 glad.c 编译为静态链接库文件,方便我们导入项目中。随后将 glad 和 KHR 目录移动到项目的 include 目录下,将编译出的 libglad.a 复制到 lib 目录下。

配置顶级 CMakeLists.txt,包含 include 文件夹,链接两个库,编译 src 下的源文件,并将可执行文件放入 bin 目录。

// CMakeLists.txt cmake_minimum_required(VERSION 3.24) project(LearnOpenGL) set(CMAKE_CXX_STANDARD 17) include_directories(include) set(EXECUTABLE_OUTPUT_PATH ${CMAKE_SOURCE_DIR}/bin) file(GLOB SRC_FILES src/*.cpp) add_executable(LearnOpenGL ${SRC_FILES}) target_link_libraries(LearnOpenGL ${CMAKE_SOURCE_DIR}/lib/libglfw3.a ${CMAKE_SOURCE_DIR}/lib/libglad.a) +--bin | +--main.exe +--include | +--glad | | +--glad.h | +--GLFW | | +--glfw3.h | | +--glfw3native.h | +--KHR | +--khrplatform.h +--lib | +--libglad.a | +--libglfw3.a +--src | +--main.cpp +--CMakeLists.txtHello WindowInitialize GLAD and GLFW

在 src 下的 main.cpp 中编写我们的主程序。

首先包含 glad 和 glfw 的头文件。要在 glfw 之前包含 glad,因为 glad 中含有 OpenGL 的头文件。(如 GL/gl.h)

#include #include

接下来在 main 函数中初始化 GLFW window,并配置 opengl 的版本为 3.3 ,使用核心模式(Core Profile)。

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(800, 600, "LearnOpenGL", NULL, NULL); if (window == nullptr) { std::cout Fragment Shader -> Tests and blending

Vertex Data[] 顶点数据是顶点的集合,包含顶点的属性,如位置和颜色。

Vertex Shader 顶点着色器以单个顶点作为输入,变换 3D 坐标和顶点属性。

Shape Assembly 图元装配将顶点形成一个或多个图元,为了让OpenGL知道如何利用坐标和颜色值,需要提示OpenGL要用这些数据形成什么样的渲染类型。这些提示被称为图元(primitive),并在调用任何绘图命令时提供给OpenGL。其中一些提示包括GL_POINTS,GL_TRIANGLES和GL_LINE_STRIP。

Geometry Shader 几何着色器以形成一个图元的顶点集合作为输入,并有能力散发新顶点来形成新的图元来形成其他形状。

Rasterization 光栅化将结果图元映射到最终屏幕上的相应像素,从而生成片元以供片元着色器使用。在片元着色器运行之前,执行剪切(clipping),剪切会丢弃所有视图外的片段,提高性能。

Fragment Shader 片元着色器用来计算像素的最终颜色。(如光照,阴影)

Tests and blending 在所有相应的颜色值都确定之后,最终对象将通过 alpha测试 和 混合 阶段。该阶段检查片段的相应深度(和模板)值并使用它们来检查结果片段是否在其他对象的前面或后面,并相应地丢弃。该阶段还检查“alpha”值(alpha值定义对象的不透明度),并相应地混合对象。因此,即使在片段着色器中计算了像素输出颜色,当渲染多个三角形时,最终像素颜色仍可能完全不同。

在现代OpenGL中,我们必须自定义至少一个顶点着色器和一个片段着色器

Graphic Pipline

着色器使用 GLSL 语言

// Vertex shader #version 330 core layout (location = 0)in vec3 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); } // Fragment shader #version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }Compile Shader

现在将 shader 源代码存储在 const char 字符串中。

const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0";

在运行时动态编译

unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader);

检查编译错误

int success; chat infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if (!sucess) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout Wireframe mode

仅绘制线框的话,可以开启线框模式

glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

完整代码 在此

Shaders

着色器是运行在GPU上的小程序。这些程序为图形管线中的每个特定部分运行。从基本意义上讲,着色器只是将输入转换为输出的程序。着色器也是非常隔离的程序,它们不允许互相通信;它们唯一的通信方式就是通过它们的输入和输出。

GLSL

着色器是用类似于 C 的 GLSL 语言编写的。GLSL专为图形设计,包含特定于向量和矩阵操作的有用功能。

着色器始终以版本声明开头,后跟输入和输出变量、统一变量及其主函数的列表。每个着色器的入口点都在其主函数中,在此处理任何输入变量,并将结果输出到其输出变量中。

#version version_number in type in_variable_name; in type in_variable_name; out type out_variable_name; uniform type uniform_name; void main() { // process input(s) and do some weird graphics stuff ... // output processed stuff to output variable out_variable_name = weird_stuff_we_processed; }

每个顶点变量也称为顶点属性,我们允许声明的顶点属性的最大数量受硬件限制。OpenGL保证至少有16个4组分顶点属性可用,但某些硬件可能允许更多,可以通过查询 GL_MAX_VERTEX_ATTRIBS 获取更多信息。

Types

GLSL具有大多数我们从诸如C语言等语言中知道的默认基本类型:int,float,double,uint 和 bool。 GLSL还具有两种容器类型,我们将经常使用,即向量和矩阵。

Vectors

在GLSL中,向量是一个包含2、3或4个基本类型组件的容器。它们可以采用以下形式(n表示组件数量):

vecn: the default vector of n floats.bvecn: a vector of n booleans.ivecn: a vector of n integers.uvecn: a vector of n unsigned integers.dvecn: a vector of n double components.

向量的组件可以通过 vec.x 访问,其中 x 是该向量的第一个组件。 您可以使用 .x,.y,.z 和.w分别访问它们的第一,第二,第三和第四个组件。 GLSL还允许您使用 rgba 表示颜色或 stpq 表示纹理坐标,以访问相同的组件。

向量数据类型允许进行一些有趣和灵活的部件选择,称为混合。混合允许我们使用以下语法:

vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

只要原始向量具有这些组件,就可以使用最多4个字母的任何组合来创建一个新向量(相同类型)。例如,不允许访问vec2的.z组件。我们还可以将向量作为参数传递给不同的向量构造函数调用,从而减少所需的参数数量:

vec2 vect = vec2(0.5, 0.7); vec4 result = vec4(vect, 0.0, 0.0); vec4 otherResult = vec4(result.xyz, 1.0); 历史上,OpenGL 的纹理坐标坐标系以及对应的分量名称是受到提出它们的计算机图形标准——IRIS GL 的影响的。IRIS GL 采用的坐标系和分量名称与 OpenGL 相同。它们是:- s 表示纹理在横向上的偏移量,也就是 u 轴的分量- t 表示纹理在纵向上的偏移量,也就是 v 轴的分量;- p 表示纹理在垂直于屏幕方向的深度上的偏移量,也就是 w 轴的分量;- q 表示纹理坐标的切插比,不常用。由于 OpenGL 是从 IRIS GL 的基础上发展而来的,因此保留了 IRIS GL 的纹理坐标系和分量名称。需要注意的是,在 OpenGL ES 中,纹理坐标的坐标系是从左下角开始的二维坐标系,而纹理坐标的分量名称则统一采用 u、v、w,不再使用 s、t、p。Ins and outs

GLSL定义了“in”和“out”关键字。每个着色器都可以使用这些关键字指定输入和输出,无论何时一个输出变量与下一个着色器阶段的输入变量匹配,它们都会被传递。然而,顶点着色器和片段着色器有所不同。

顶点着色器直接从顶点数据接收其输入。为了定义顶点数据的组织方式,我们使用位置元数据(location metadata)指定输入变量,以便我们可以在CPU上配置顶点属性。我们在前一章中已经看到了这一点,如layout(location=0)。因此,顶点着色器需要额外的布局规范来处理其输入,以便我们可以将其链接到顶点数据。

你可以通过glGetAttribLocation在你的OpenGL代码中指定属性位置,但我更喜欢在顶点着色器中设置它们。这样更容易理解,也可以节省你(和OpenGL)的一些工作。

另一个例外是片段着色器需要一个vec4颜色输出变量,因为片段着色器需要生成最终输出颜色。如果您未在片段着色器中指定输出颜色,则这些片段的颜色缓冲输出将是未定义的(这通常意味着OpenGL将将它们渲染为黑色或白色)。

// vertex shader #version 330 core layout (location = 0)in vec3 aPos; // the position variable has attribute position 0 out vec4 vertexColor; // specify a color output to the fragment shader void main() { gl_Position = vec4(aPos, 1.0); // see how we directly give a vec3 to vec4's constructor vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // set the output variable to a dark-red color } // fragment shader #version 330 core out vec4 FragColor; in vec4 vertexColor; // the input variable from the vertex shader (same name and same type) void main() { FragColor = vertexColor; }Uniforms

Uniform 变量是全局的,意味着在每个着色器程序对象中是唯一的,并且可以在着色器程序的任何阶段从任何着色器访问。无论将 uniform variable 设置为什么,都会保持其值,直到它们被重置或更新。

在 shader 中声明一个 uniform 变量。

#version 330 core out vec4 FragColor; uniform vec4 ourColor; // we set this variable in the OpenGL code. void main() { FragColor = ourColor; }

在 openGL 程序中查询 uniform 变量的位置,并更新它的值。

float timeValue = glfwGetTime(); float greenValue = (sin(timeValue) / 2.0f) + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

在 render loop 中更新 uniform 变量的值,使其可以每帧变化。

while(!glfwWindowShouldClose(window)) { // input processInput(window); // render // clear the colorbuffer glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // be sure to activate the shader glUseProgram(shaderProgram); // update the uniform color float timeValue = glfwGetTime(); float greenValue = sin(timeValue) / 2.0f + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // now render the triangle glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); // swap buffers and poll IO events glfwSwapBuffers(window); glfwPollEvents(); }

因为 OpenGL 本质上是一个 C 库,它没有原生的函数重载支持,因此无论何时一个函数可以用不同的类型调用,OpenGL 都会为每个需要的类型定义新函数;glUniform 是一个完美的例子。该函数需要一个特定的后缀来设置您想要设置的 uniform 的类型。一些可能的后缀如下:

f: the function expects a float as its value.i: the function expects an int as its value.ui: the function expects an unsigned int as its value.3f: the function expects 3 floats as its value.fv: the function expects a float vector/array as its value.More attributesfloat vertices[] = { // positions // colors 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // bottom right -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // bottom left 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // top }; // position attribute glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 *sizeof(float), (void*)0); glEnableVertexAttribArray(0); // color attribute glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 *sizeof(float), (void*)(3*sizeof(float))); glEnableVertexAttribArray(1);

// vertex shader #version 330 core // the position variable has attribute position 0 layout (location = 0) in vec3 aPos; // the color variable has attribute position 1 layout (location = 1) in vec3 aColor; out vec3 ourColor; // output a color to the fragment shader void main() { gl_Position = vec4(aPos, 1.0); // set ourColor to the input color we got from the vertex data ourColor = aColor; } // fragment shader #version 330 core out vec4 FragColor; in vec3 ourColor; void main() { FragColor = vec4(ourColor, 1.0); }

在渲染三角形时,光栅化阶段通常会产生比最初指定的顶点更多的片段。然后,光栅化器根据片段在三角形形状上的位置确定每个片段的位置。根据这些位置,它会插值所有片段着色器的输入变量。

这正是三角形发生的事情。我们有3个顶点和3种颜色,根据三角形的像素,它可能包含大约50000个片段,其中片段着色器在这些像素之间插值颜色。

Shader class

编写、编译和管理着色器可能会非常繁琐。因此我们将通过构建一个着色器类来使我们的工作变得更加轻松,它从磁盘读取着色器,编译和链接它们,检查错误并易于使用。

着色器类保存着着色器程序的ID。它的构造函数需要顶点着色器源代码和片段着色器源代码的文件路径,我们可以将它们作为简单的文本文件存储在磁盘上。为了额外增加一些便利,我们还添加了几个实用函数:use 激活着色器程序,所有的 set... 函数查询统一变量的位置并设置其值。

#ifndef SHADER_H #define SHADER_H #include // include glad to get all the required OpenGL headers #include #include #include #include class Shader { public: // the program ID unsignedint ID; // constructor reads and builds the shader Shader(constchar* vertexPath,constchar* fragmentPath); // use/activate the shader void use(); // utility uniform functions void setBool(const std::string &name,bool value)const; void setInt(const std::string &name,int value)const; void setFloat(const std::string &name,float value)const; }; #endif Reading from file

使用 C++ 文件流将文件内容读入多个 string 中。

Shader(constchar* vertexPath,constchar* fragmentPath) { // 1. retrieve the vertex/fragment source code from filePath std::string vertexCode; std::string fragmentCode; std::ifstream vShaderFile; std::ifstream fShaderFile; // ensure ifstream objects can throw exceptions: vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit); try { // open files vShaderFile.open(vertexPath); fShaderFile.open(fragmentPath); std::stringstream vShaderStream, fShaderStream; // read file's buffer contents into streams vShaderStream 标量-向量运算 Scalar vector operations

负数向量 Vector negation

加法和减法 Addition and subtraction

取模 Length

单位化 normalizing a vector

向量-向量相乘 Vector-vector multiplication

点乘 Dot product

叉乘 Cross product

Matrices

矩阵是数字(number)、符号(symbol)和数学表达式(mathematical expression)的矩形数组(rectanglar array)。

加法和减法 Addition and subtraction

矩阵-常量相乘 Matrix-scalar product

矩阵-矩阵乘法 Matrix-matrix multiplication

Matrix-Vector multiplication

我们用向量来表示位置、颜色和纹理坐标等等,可以看作是 N * 1 的矩阵,这样我们就可以用 M * N 的矩阵和 N * 1 的向量相乘。我们用矩阵来表示 2D/3D 变换。OpenGL 中通常使用 4 * 4 变换矩阵。

单位矩阵 Identity matrix

对角线全是 1 ,其余为 0 的矩阵被称为单位矩阵。

缩放 Scaling

平移 Translation

齐次坐标 Homogeneous coordinates

向量的 w 分量被称为齐次坐标,我们将x,y和 z 坐标除以其 w 坐标,就能得到 3D 向量。使用齐次坐标的好处是可以对 3D 向量进行矩阵平移。我们能通过 w 分量来创建 3D 透视。

旋转 Rotation

我们依次绕 X 轴,Y 轴,Z轴旋转,这被称为欧拉角 Eular angles,但是存在万向节死锁 Gimbol Lock,因此我们一般会用四元数来表示旋转。

Combining matrices

In practice

GLM 是为 OpenGL 设计的 header-only 数学库。

#include #include #include glm::vec4 vec(1.0f, 0.0f, 0.0f, 1.0f); glm::mat4 trans = glm::mat4(1.0f); // identity matrix trans = glm::translate(trans, glm::vec3(1.0f, 1.0f, 0.0f)); vec = trans * vec; std::cout


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3