OpenGL教程:画出属于你的第一个三角形

OpenGL教程:画出属于你的第一个三角形

目录:

图形API:什么是OpenGL?

OpenGL的魅力

使用OpenGL画出一个三角形

什么是图形API?什么是OpenGL?

图形API(Application Programming Interface)是一种软件库,为开发者提供一系列的函数和命令,允许他们在软件中创建和操作图形和图像,尤其是在三维空间中。这些API抽象化了底层图形硬件(如GPU)的复杂性,使得开发者能够进行图形编程而无需深入了解硬件细节。

图形API定义了一套与硬件交互的标准方式,包括渲染图形、计算几何体、管理图形数据的内存和执行图像处理的操作。通过这些API,开发者可以在不同的硬件和操作系统上实现一致的图形表现,从而开发出跨平台的应用程序。他们旨在提供一种有效的方法来利用现代图形处理单元(GPU)的强大能力,进行复杂的图形计算和渲染操作。通过使用图形API,开发者可以创建图形密集型的应用程序,如视频游戏、模拟器、图形设计软件以及虚拟现实和增强现实应用等。

有一些游戏中的画面设置允许你定义游戏绘制时使用的图形接口 OpenGL(Open Graphics Library)是一个跨语言、跨平台的应用程序编程接口(API),用于渲染二维和三维向量图形。该API由非盈利技术联盟Khronos Group维护,旨在提供一个清晰的、硬件无关的接口来与图形硬件交互。OpenGL广泛应用于计算机游戏、虚拟现实、科学可视化以及图形设计等领域。(底层性)

OpenGL以其高度抽象化的方式工作,允许开发者在不同操作系统和平台上编写能直接利用图形处理单元(GPU)加速的代码。它定义了一系列函数,使得程序员能够执行各种图形渲染任务,例如几何图形的构建、纹理映射、光照控制和阴影处理等。OpenGL通过驱动程序模型,使得在不同硬件上的表现能够最大化地优化,同时保持应用程序代码的一致性。 (兼容性)

OpenGL的版本迭代伴随着图形硬件的发展,不断引入新的渲染技术标准和编程接口,如OpenGL ES(嵌入式系统)及WebGL(网页图形库)等衍生标准,扩展了其应用范围到移动设备和网页平台。因此,OpenGL构成了现代图形编程的基础之一,是实现高性能图形计算的重要工具。(拓展性)

事实上,现在流行的图形api还有DirectX(主要用于Windows平台)、Metal(用于苹果设备)以及Vulkan(同样由Khronos Group推出,被认为是下一代跨平台图形API)。但我仍然推荐你将OpenGL作为学习图形编程的起点。

OpenGL的魅力

底层性,兼容性,拓展性,奠定了OpenGL在图形编程中的地位,我愿称它为图形界的C语言(虽然它本身就是用c语言写的,但我们比的就是那种重大的意义hhh),一些同学还有可能听说用过easyX图形库,b站上有很多编程教学都用它来写小游戏。但是此图形库非图形库,它只是一个基于windows简化版本的操作图形的接口,主要用来教学编程,实际应用意义并不大。

没错,OpenGL是一个api,但它不需要你去下载,去实现,它事实上只是一种规范,它有着不同的实现,具体的实现是由你的显卡制造商写好了放在显卡驱动程序中了。英伟达,AMD的OpenGL实现可能并不是完全相同。但不影响我们使用和学习。

学习了计算机图形学和OpenGL,你能: 对计算机图形学光栅化方面的知识不再停于理论层面,会对知识理解得更深入透彻。 在打游戏时,不再看着一堆的画质设置而一头雾水,知道什么影响性能帧数,什么影响画质(想想就太兴奋了)

推荐两个b站大学的宝藏视频

【GAMES101-现代计算机图形学入门-闫令琪】www.bilibili.com/video/BV1X7... games101典中典,建议跟着视频看的同时学习OpenGL。

【【双语】【TheCherno】OpenGL】www.bilibili.com/video/BV1Ni... OpenGL的进一步了解,新手勿碰!!!!!!!!!新手勿碰!!!!!!!!!新手勿碰!!!!!!!!!!!! 重要的事情说三遍!!!!!! 但是你必须要跟着这个视频学习如何配置OpenGL环境,所以看完第三集就撤退吧! 只看第三集

让我们开始吧!

用OpenGL画一个三角形

让我们假设你的OpenGL环境都已经配置好了哈哈(这实在不容易)

你应该和我一样包含了这三个头文件(放在哪个文件夹不重要,重要的是你要包含这两个.h头文件)

我好像听见有人问:啥啊,只包含头文件有啥用啊,头文件不是只有函数声明吗?

你说得对,但是函数实现已经躺在你的显卡驱动程序里面了。。这些gl库存在的意义也就是加载那些躺在你显卡驱动里面的GL函数指针。等你进一步学习,尝试自己写一套简单的函数实现吧!

创建窗口

我们在main函数中先写下这段代码:

ini 复制代码
if (!glfwInit()) return -1;//easy to make error
GLFWwindow* window=glfwCreateWindow(800, 600, "Cberpunk2077", NULL,NULL);
if (window == nullptr) {
	glfwTerminate();
	return -1;
}
glfwMakeContextCurrent(window);

if (glewInit() != GLEW_OK)
	return -1;
glViewport(0, 0, 800, 600);

第一行glfwinit函数,它负责初始化GLFW库,为渲染做准备。 第二行是调用glfwcreatewindow函数,创建一个窗口,返回glfwwindow对象类型的指针,所以我们也要定义一个指针接收它,这是我们后续管理这个窗口的凭证。

进一步看看glfwcreatewindow的形参,第一,二个参数分别是窗口的宽,高,第三个参数是窗口的名字(假设我们正在开发3A大作hhhh),第四个参数是指定这个窗口创建在哪个显示器上,我们填NULL表示我们只创建在主显示器上。第五个参数我们暂时用不上,填NULL。

glfwMakeContextCurrent(window)用于设置指定窗口的上下文为当前上下文。它的作用是将 OpenGL 渲染上下文与指定窗口进行关联,使得后续的 OpenGL 调用都会在该窗口的上下文中执行。说人话就是把后面的操作与这个窗口相绑定。

glewinit用于初始化glew库,如果初始化失败,及时退出。

glviewport函数用于设置视口(viewport),即OpenGL渲染的范围,这里我们把它设置成和窗口一样大没什么问题,有时候,可以把它设置的比窗口小。

我猜有的人已经汗流浃背了,是的,我们前期都一直在做配置参数的工作,这是逃不掉的,你设置的越多,说明你对这个过程的控制权就越多。

接下来让我们引入渲染循环吧!

js 复制代码
int main(){
if (!glfwInit()) return -1;//easy to make error
	GLFWwindow* window=glfwCreateWindow(800, 600, "Cberpunk2077", NULL,NULL);
	if (window == nullptr) {
		glfwTerminate();
		return -1;
	}
	glfwMakeContextCurrent(window);
	
	if (glewInit() != GLEW_OK)
		return -1;
	glViewport(0, 0, 800, 600);
while (!glfwWindowShouldClose(window))
{
	// input

	

	// render
	
	
	glClear(GL_COLOR_BUFFER_BIT);

	//  swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
	glfwSwapBuffers(window);
	glfwPollEvents();
}

  // glfw: terminate, clearing all previously allocated GLFW resources.

  glfwTerminate();
  return 0;
}

渲染循环是基础,游戏和动画都是基于这个渲染循环一帧一帧的渲染出来营造出画面运动的错觉。我们来逐个分析渲染循环:

glfwWindowShouldClose(window)函数:名字介绍功能,这个函数传入窗口对象指针,分析其状态,应不应该关闭?如果不应该关闭,返回假,循环条件为真,继续循环;如果应该关闭,循环条件为假,循环终止。

glClear(GL_COLOR_BUFFER_BIT); 这个函数负责清除缓冲,我们在绘制图形,所以每一帧刷新时都要清除掉上一帧的颜色缓冲。

glfwSwapBuffers(window);这个函数用于交换颜色缓冲,它基于双缓冲绘图的原理(恕我不在这里介绍双缓冲绘图了),这个函数还会更新窗口状态。

glfwPollEvents();这个函数会监听各种事件,如键盘输入,鼠标移动等等,并调用相应的函数。此外还会刷新事件状态。

终于看见一个黑不拉几神么都没有黑窗口了!

三角形来了

先思考一下,我们在本子上要想画一个三角形是怎么画的?是不是画出三个不重叠的点,然后一一连接起来就成为了三角形?OpenGL里面也是这样,我们只要指定顶点,指定绘制的方式,就能调用相应的函数画出对应的图元。

那么在这里我就不得不提图形学光栅化的有关知识了,这对我们接下来画出三角形尤为重要:

首先我们应该都清楚,电子设备的显示屏幕都是由一个个像素组成。我的电脑是2k屏,那么它水平方向有2560个像素,竖直方向有1440个像素。我们通常取每个像素的中心坐标去代替这个像素的坐标。我们指定一个三维空间中的图元位置,它显示在2D屏幕上通常要经过下面的步骤:

引用自gmaes104,游戏引擎架构。

我不能详细展开说,我们重点关注第一步到第二步,和第五步。

第一步到第二步我们把原始的数据输入进入程序处理,经过一系列变换,最终得到在2d上投影到的形状。这个过程也是无比的复杂,让我们只先关注OpenGL是怎么处理输入进程序的原始数据的吧!

顶点着色器 Vertex Shader

OpenGL使用顶点着色器对输入的原始数据进行基本的处理,使它符合OpenGL的格式。着色器(Shader)是运行在显卡上的小程序,OpenGL中着色器允许我们自定义更改,这给了人们权力去充分发挥自己的想象力和创造力去渲染出不一样的图形风格。

OpenGL的着色器程序使用GLSL语言编写的,我们编写一个最最基本能用的顶点着色器:

js 复制代码
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

你可以看出和c语言风格很相似。我们首先声明我们使用的OpenGL核心版本,然后我们指定顶点数据所在的位置。用in关键字表示从外界输入变量,具体是一个表示位置的三维向量。因为我们暂时只用到位置,我们输入的数据只有一个,所以它肯定在第一个位置,也就是第零号位置。

接下来是main函数,我们指定顶点的位置就是这个向量的数据。前面三个xyz就是代表位置,后面的1.0我们暂时先不管。

简单的不能再简单的顶点着色器配置好了,它没有对输入进来的坐标做任何处理就原封不动的把它当作位置坐标了!

片段着色器 fragment Shader

在光栅化的第五步中,我们对每一个像素都运行一次片段着色器,它是用来综合处理前面流程的数据,最后计算出这个像素该显示什么样的颜色,并用一个四维向量表示,四个向量分量分别代表RGBA(三原色和透明度)。

js 复制代码
#version 330 core out vec4 FragColor; 
void main() {
FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f); 
}

我们不用那么多复杂的计算,直接指定渲染每个像素都显示成白色。

要想画出三角形,我们暂且配置出这两个着色器就够了!

现在让我们输入三角形的数据: 我们需要将三角形的数据从CPU传到显卡上,这需要用到OpenGL中的顶点缓冲对象VBO。它负责将大量的顶点数据一次性从CPU传到显卡的显存中,提升性能。

定义三角形顶点数组:

js 复制代码
float vertices[] = 
{ -0.5f, -0.5f, 0.0f,
   0.5f, -0.5f, 0.0f, 
   0.0f,  0.5f, 0.0f };

接下来申请一个VBO对象,让我们调用生成它的函数,这个生成函数会返回一个指针,我们接收它,后续就用这个指针管理。我们还要指定这个缓冲与顶点数组缓冲类型绑定,我们试图告诉OpenGL:"从现在开始,我对后续对顶点数据(GL_ARRAY_BUFFER)的操作都用这个VBO进行操作,直到我解绑为止。"

这里我们还用到了另一个顶点数组对象,VAO,我不做过多解释,不想让原本就汗流浃背的大脑过载了,这里VAO的用处就是记录VBO绑定操作,后续我们对大量要绘制的物体进行配置时,不用再重复写这些VBO绑定操作,只要调用相应绑定的VAO即可。

js 复制代码
unsigned int VAO; unsigned int VBO; 
glGenVertexArrays(1, &VAO);

glBindVertexArray(VAO);//VAO要在VBO绑定之前先配置好,才能记录后面的VBO的操作

glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

接下来调用glBufferData函数是用来从cpu到gpu传输数据,它的第一个实参告诉OpenGL我们传输的是顶点数组数据类型,用的是veertex这个数组中的数据,以及传输的数据大小。最后一个参数是指定显卡管理数据的方式,这里我们先填静态方式。

js 复制代码
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); 
glEnableVertexAttribArray(0);

glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);//解绑这些对象

glVertexAttribPointer函数是让我们解释我们传入的数据是什么来头?这里我们只传入了一个位置数据,实际上一个顶点可以包含很多很多数据,包括但不限于位置,法线坐标,颜色,等等。

第一个参数指定了我们传入的数据是在哪个位置,因为前面说了一个顶点数组数据可能包含多个信息。这里我们只有一个数据------位置,所以在第零号位置。第二,三个参数我们告诉他这个数据是由三个数组成。数据类型是浮点型。第四个参数我们告诉他要不要标准化数据(即要把坐标变换到-1到1的范围内,这是OpenGL的规定),但我们的数据都是已经是在-1到1中,所以填false表示不要。

第五个参数是填一个顶点数据有多大。因为我们会传入很多个顶点,OpenGL只有知道每个顶点数据有多大才能知道每次要跨越多少个字节才能从这个顶点到下一个顶点。这里我们一个顶点位置信息就是全部。所以填三个浮点数的字节大小。

第六个参数是让你告诉OpenGL从顶点数组的头部跳到你指定的顶点数据要多少字节。我们的顶点数组里面只有一个位置坐标,默认在最前面,根本不需要跳跃访问。填0.

终于说完了,这里推荐一个网站,可以查询OpenGL的各种函数-> docs.gl

接下来就是构建着色器了! 我们直接在代码中理解:

js 复制代码
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";
const char *fragmentShaderSource = "#version 330 core\n" 
"out vec4 FragColor;\n"
"void main()\n" 
"{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";//注意注意!字符串之间只能有回车键换行,才会被认为是一个拼接字符串!

//创建一个着色器,指定是顶点着色器,用一个整数去接收这个着色器id,理解成就是控制着色器的指针。
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); 
//传入我们写的存放源码字符串,第二个参数代表我们传入一个字符串,第三个是你要传入的字符串指针的指针
//第四个参数我们暂且填null。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
//编译源码
glCompileShader(vertexShader);

//片段着色器分析同理
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);

//创建着色器程序,把先前的两个着色器附加到这个着色器程序,链接,着色器程序构建完成!
unsigned int shaderProgram = glCreateProgram(); 
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader); 
glLinkProgram(shaderProgram);
//着色器已经链接嵌入程序,原来的对象没用了,删除它们,释放内存。
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

//接下来写渲染循环!
while (!glfwWindowShouldClose(window)) { 
glClear(GL_COLOR_BUFFER_BIT);  
glUseProgram(shaderProgram);//启用我们的着色器程序
glBindVertexArray(VAO);//绑定VAO对象
glDrawArrays(GL_TRIANGLES, 0, 3);//指定绘制函数的工作,第一个指定他要画的图元,填三角形;第二个是它开始的索引,我们的顶点位置数据是在数组第一个地方,填0.第三个我们用了多少个顶点,答案是三个。
glBindVertexArray(0);//用完解绑好习惯
glfwSwapBuffers(window); 
glfwPollEvents(); 
}
//善后工作,释放内存
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
glfwTerminate(); //终止函数
return 0; 

我们历经千辛万苦,终于看到了。。。

整个组织好的源码如下

js 复制代码
#include <gl/glew.h>
#include <GLFW/glfw3.h>

#include <iostream>

int main()
{
    // glfw: initialize and configure
  
    glfwInit();

    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
   

   
    if (!glfwInit()) return -1;//easy to make error

    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";
    const char* fragmentShaderSource = "#version 330 core\n"
        "out vec4 FragColor;\n"
        "void main()\n"
        "{\n"
        "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
        "}\n\0";
   
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    
    // fragment shader
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
   

    // link shaders
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
   
   
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

   
    float vertices[] = {
        -0.5f, -0.5f, 0.0f, // left  
         0.5f, -0.5f, 0.0f, // right 
         0.0f,  0.5f, 0.0f  // top   
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &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, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    
    glBindVertexArray(0);
    
    while (!glfwWindowShouldClose(window))
    {
        // input
        
        // render
        
        glClear(GL_COLOR_BUFFER_BIT);

       
        glUseProgram(shaderProgram);
        glBindVertexArray(VBO); 
       //poll events
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
   
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    return 0;
}

你也许会说,太逊了,弄了半天才看到一个三角形!

但是你是否知道: 你电脑中的游戏模型,大部分都是由一个个小的三角形组成的表面!

虚幻五游戏引擎展示

千里之行,始于足下,今天我们画出了一个三角形,谁知道我们几年后又能创造出什么酷炫的效果呢?

加油吧,少年!

相关推荐
闲暇部落1 天前
Android OpenGL ES详解——绘制圆角矩形
opengl·圆形·矩形·圆角矩形
凌云行者2 天前
OpenGL入门008——环境光在片段着色器中的应用
c++·cmake·opengl
闲暇部落6 天前
Android OpenGL ES详解——立方体贴图
opengl·天空盒·立方体贴图·环境映射·动态环境贴图
闲暇部落6 天前
Android OpenGL ES详解——实例化
android·opengl·实例化·实例化数组·小行星带
闲暇部落9 天前
Android OpenGL ES详解——几何着色器
opengl·法线·法向量·几何着色器
刘好念14 天前
[OpenGL]使用OpenGL实现硬阴影效果
c++·计算机图形学·opengl
闲暇部落14 天前
Android OpenGL ES详解——纹理:纹理过滤GL_NEAREST和GL_LINEAR的区别
opengl·texture·linear·纹理过滤·nearest·邻近过滤·线性过滤
凌云行者15 天前
OpenGL入门005——使用Shader类管理着色器
c++·cmake·opengl
凌云行者15 天前
OpenGL入门006——着色器在纹理混合中的应用
c++·cmake·opengl
凌云行者18 天前
OpenGL入门004——使用EBO绘制矩形
c++·cmake·opengl