openGL 概念学习(一)
本文是针对LearnOpenGL中一些基础概念的整理与补充。
一、OpenGL核心模式
1.1 OpenGL内模型数据的本质
openGL只记录每个模型的顶点数据,比如立方体就是八个顶点,每个顶点都有自己的颜色等属性数据。
其他数据都是插值实现计算,因此只要确定顶点,就可以描绘物体了。
1.2 MVP变换
-
模型变换:对模型进行平移、旋转、缩放等变换,只针对模型本身
-
观察变换:计算模型在摄像机坐标系下的位置,摄像机永远看向摄像机坐标系的
-z轴
。
本质:在摄像机上建立一个坐标系,然后求出来物体相对于摄像机坐标系内的坐标位置。
-
投影变换:将摄像机坐标系中的物体,投影到剪裁平面中,从而进行光栅化。
二、Shader原理
2.1 Shader如何从CPU获得数据
vertexShader:是并行的针对每个顶点进行处理操作,有多少个Shader调用多少次。
fragmentShader:经过插值后的点进行各种处理,有多少个fragment就会执行多少次。
三、VBO、VAO、EBO基础概念
3.1 VBO
VBO(顶点缓冲对象(Vertex Buffer Objects)
:在GPU内存中储存的顶点信息。
好处:可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。当数据发送至显卡内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。
使用步骤:
- 获取vbo的index
- 绑定vbo的index,绑定后任何的操作都是针对该vbo的,直到解绑的那一刻。
- 给vbo分配现存空间并且传输数据
- 告诉shader数据解析方式
- 激活锚点
代码说明:
unsigned int VBO;
// 1. 获取vbo的index
glGenBuffers(1, &VBO);
// 2. 绑定vbo的index
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 3. 给vbo分配现存空间并且传输数据
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//以上步骤实现了把顶点数据储存在显卡的内存中,用VBO这个顶点缓冲对象管理。
// 4. 告诉shader数据解析方式
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// 5. 激活锚点 0即对应layout=0锚点
glEnableVertexAttribArray(0);
3.2 VAO
VAO(顶点数组对象 Vertex Array Object)
: 可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。
好处:当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。
一个顶点数组对象会储存以下这些内容:
glEnableVertexAttribArray
和glDisableVertexAttribArray
的调用。- 通过
glVertexAttribPointer
设置的顶点属性配置。 - 通过
glVertexAttribPointer
调用与顶点属性关联的顶点缓冲对象
。
使用VAO时,要做的只是使用glBindVertexArray
绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。
示例如下:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。
3.3 EBO
EBO(索引缓冲对象 Element Buffer Object,也叫Index Buffer Object,IBO)
: 是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。
VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。
// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
//此后代码中的VBO、EBO都会自动绑定在VAO之下。
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
[...]
// ..:: 绘制代码(渲染循环中) :: ..
// 这里必须先指定(激活)好shaderProgram
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);
注意:当目标是GL_ELEMENT_ARRAY_BUFFER
的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。
3.4 片段(元)与像素的区别
图元
:由顶点组成,一个顶点,一条线段,一个三角形或者多边形都可以组成图元。
片段(元)
:图元经过光栅化阶段后,被分割成一个个像素大小的基本单位。片元其实已经很接近像素了,但它还不是像素。片元包含了比RGBA更多的信息,比如深度值,法线,纹理坐标等信息。片元需要在通过一些测试(深度、模板)后才会最终成为像素。同时,可能会有多个片元竞争同一个像素,而这些测试会最终筛选出一个合适的片元,丢弃法线和纹理坐标等不需要的信息后成为像素。
四、纹理
纹理
是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节;可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D的房子上,这样你的房子看起来就像有砖墙外表了。因为我们可以在一张图片上插入非常多的细节,这样就可以让物体非常精细而不用指定额外的顶点。
纹理坐标(Texture Coordinate)
: 用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。之后在图形的其它片段上进行片段插值(Fragment Interpolation)。纹理坐标在x和y轴上,范围为0到1之间。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
4.1 为什么要引入纹理坐标
因为图片的长宽各不相同,需要一个统一的拾取方式来拾取像素。所以定义了0-1的范围内的纹理坐标u/v。
这样可以通过:u/v x 长/宽 来表示图片上面的像素坐标点,从而可以统一的采样到像素数据信息。
举例说明:
纹理其实就是贴图系统,遵从UV坐标
4.2 Texture的filter方式
如果经过u*width和v*height 计算出来的坐标为(12.3, 15.8)那么就要有一种方式进行取整,从而能够采样到图片上面的一个像素点颜色信息。
最邻近插值方式
线性插值
4.3 纹理单元(Texture Unit)
OpenGL 给我们提供了很多的纹理Texture锚定点,比如GL_TEXTURE0-GL_TEXTURE15
之所以需要多个锚定点,是因为可能有多个纹理需要贴图到同一个图元上。
我们可以使用glUniform1i
,给纹理采样器分配一个位置值,这样的话我们能够在一个片段着色器中设置多个纹理。一个纹理的位置值通常称为一个纹理单元(Texture Unit)
。一个纹理的默认纹理单元是0,它是默认的激活纹理单元。
通过使用glUniform1i设置每个采样器的方式告诉OpenGL每个着色器采样器属于哪个纹理单元。
ourShader.use(); // 不要忘记在设置uniform变量之前激活着色器程序!
glUniform1i(glGetUniformLocation(ourShader.ID, "texture1"), 0);
代码示例:
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;
unsigned char *data = stbi_load("container.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);
五、坐标变换
标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。
将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易。
对我们来说比较重要的总共有5个不同的坐标系统:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
我们的顶点坐标起始于局部空间(Local Space)
,在这里它称为局部坐标(Local Coordinate)
,它在之后会变为世界坐标(World Coordinate)
,观察坐标(View Coordinate)
,裁剪坐标(Clip Coordinate)
,并最后以屏幕坐标(Screen Coordinate)
的形式结束。
步骤说明:
局部坐标
是对象相对于局部原点的坐标,也是物体起始的坐标。世界空间坐标
是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。- 接下来我们将世界坐标变换为
观察空间坐标
,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。 - 坐标到达观察空间之后,我们需要将其投影到
裁剪坐标
。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。 - 最后,我们将裁剪坐标变换为
屏幕坐标
,我们将使用一个叫做视口变换(Viewport Transform)的过程
。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport
函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
5.1 OpenGL坐标变换全局图
上图中,OpenGL定义了后三个坐标系(裁剪坐标、NDC坐标、屏幕坐标),前三个坐标(物体坐标、世界坐标、摄像机坐标)是为了用户方便而自定义的坐标。
OpenGL渲染流水线概览
六、帧缓存(Framebuffers)
帧缓存不是指一块显存,而是类似于VAO的一个组织结构,他里面包含了很多附件(ColorBuffer,DepthBuffer,StencilBuffer),附件里面会根据不同需求开辟不同的显存空间。
之前的代码,大都是用默认的0号帧缓存
来做渲染的。
一个完整的帧缓冲需要满足以下的条件:
- 帧缓存需要至少绑定一个附件(颜色、深度或模板缓冲)。
- 至少绑定一个颜色附件(Attachment)。
- 每个绑定的附件必须都开辟内存完毕。
- 每个缓冲都应该有相同的样本标准(N*M个像素数据信息)。
各类附件的实现方法:
-
使用
Texture
作为Color
、Depth
、Stencil
等各类Buffer,并开辟空间。
特点:所有渲染操作的结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便地使用它。 -
使用
RenderBuffer
作为Color
、Depth
、Stencil
等各类Buffer,并开辟空间。
特点:拥有快速的流水线访问结构,方便拷贝给其他Buffer或者写入数据。 -
根据实际情况,混合使用RenderBuffer或者Texture。
6.1 Texture作为Buffer附件
以ColorBuffer为例
//创建一个帧缓冲对象(Framebuffer Object, FBO)
unsigned int fbo;
glGenFramebuffers(1, &fbo);
//绑定帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//创建好一个纹理后将它附加到帧缓存
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
以Depth、StencilBuffer为例
// 将深度和模板缓冲附加为一个纹理需要使用GL_DEPTH_STENCIL_ATTACHMENT类型,并配置纹理的格式,让它包含合并的深度和模板值。
glTexImage2D(
GL_TEXTURE_2D, 0, GL_DEPTH24_STENCIL8, 800, 600, 0,
GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8, NULL
);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, texture, 0);
6.2 RenderBuffer作为Buffer附件
程序示例:
//创建一个渲染缓冲对象
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
//绑定渲染缓冲对象
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
大部分时间我们仅需要深度和模板值用于测试,但不需要对它们进行采样,所以渲染缓冲对象非常适合它们。
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
//附加这个渲染缓冲对象到帧缓存
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
6.3 FrameBuffer渲染流程
Texture作为ColorBuffer, RenderBuffer作为D/S Buffer
pass的概念:相当于一次完整的(离屏)渲染流程。
// Pass1: 离屏渲染阶段
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 我们现在不使用模板缓冲
glEnable(GL_DEPTH_TEST);
DrawScene();
// Pass2:屏幕显示阶段
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 返回默认
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
screenShader.use();
glBindVertexArray(quadVAO);
glDisable(GL_DEPTH_TEST);
glBindTexture(GL_TEXTURE_2D, textureColorbuffer);
glDrawArrays(GL_TRIANGLES, 0, 6);
6.4 后处理(Post-Process)
所谓后处理就是利用FrameBuffer的特性,渲染到ColorBuffer附件后,对渲染的Texture进一步再做处理的过程。
卷积操作
- 点赞
- 收藏
- 关注作者
评论(0)