openGL 概念学习(一)

lutianfei 发表于 2022/05/27 18:33:27 2022/05/27
【摘要】 本文是针对LearnOpenGLCN中一些基础概念的整理与补充。Object 与shader的关系?每个(类)object 对应一个shader?纹理、像素之间的坐标关系VBO、VAO、EBO、FBO等关系和使用方法Fragment为什么要将一些坐标系从-1,1 映射到0-1,纹理坐标系和法线坐标系, 纹理坐标与NDC坐标的平铺对应关系SOIL 是什么意思?Glunit 是什么? 什么时候提...

本文是针对LearnOpenGL中一些基础概念的整理与补充。

一、OpenGL核心模式

1.1 OpenGL内模型数据的本质

openGL只记录每个模型的顶点数据,比如立方体就是八个顶点,每个顶点都有自己的颜色等属性数据。
其他数据都是插值实现计算,因此只要确定顶点,就可以描绘物体了。

image.png

1.2 MVP变换

  • 模型变换:对模型进行平移、旋转、缩放等变换,只针对模型本身

  • 观察变换:计算模型在摄像机坐标系下的位置,摄像机永远看向摄像机坐标系的-z轴
    本质:在摄像机上建立一个坐标系,然后求出来物体相对于摄像机坐标系内的坐标位置。
    image.png

  • 投影变换:将摄像机坐标系中的物体,投影到剪裁平面中,从而进行光栅化。

二、Shader原理

2.1 Shader如何从CPU获得数据

image.png

vertexShader:是并行的针对每个顶点进行处理操作,有多少个Shader调用多少次。
fragmentShader:经过插值后的点进行各种处理,有多少个fragment就会执行多少次。

三、VBO、VAO、EBO基础概念

3.1 VBO

VBO(顶点缓冲对象(Vertex Buffer Objects):在GPU内存中储存的顶点信息。
好处:可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。当数据发送至显卡内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。

使用步骤:

  1. 获取vbo的index
  2. 绑定vbo的index,绑定后任何的操作都是针对该vbo的,直到解绑的那一刻。
  3. 给vbo分配现存空间并且传输数据
  4. 告诉shader数据解析方式
  5. 激活锚点

代码说明:

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就行了。

一个顶点数组对象会储存以下这些内容:

  1. glEnableVertexAttribArrayglDisableVertexAttribArray的调用。
  2. 通过glVertexAttribPointer设置的顶点属性配置。
  3. 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象

image.png

使用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。
image.png

// ..:: 初始化代码 :: ..
// 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 长/宽 来表示图片上面的像素坐标点,从而可以统一的采样到像素数据信息。

image.png

举例说明:
image.png

纹理其实就是贴图系统,遵从UV坐标
image.png

4.2 Texture的filter方式

如果经过u*width和v*height 计算出来的坐标为(12.3, 15.8)那么就要有一种方式进行取整,从而能够采样到图片上面的一个像素点颜色信息。

最邻近插值方式
image.png

线性插值
image.png

4.3 纹理单元(Texture Unit)

OpenGL 给我们提供了很多的纹理Texture锚定点,比如GL_TEXTURE0-GL_TEXTURE15
之所以需要多个锚定点,是因为可能有多个纹理需要贴图到同一个图元上。

image.png

我们可以使用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)的形式结束。

image.png

步骤说明:

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。
  2. 世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。
  3. 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。
  4. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。
  5. 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段

5.1 OpenGL坐标变换全局图

image.png

上图中,OpenGL定义了后三个坐标系(裁剪坐标、NDC坐标、屏幕坐标),前三个坐标(物体坐标、世界坐标、摄像机坐标)是为了用户方便而自定义的坐标。

image.png

OpenGL渲染流水线概览
image.png

六、帧缓存(Framebuffers)

帧缓存不是指一块显存,而是类似于VAO的一个组织结构,他里面包含了很多附件(ColorBuffer,DepthBuffer,StencilBuffer),附件里面会根据不同需求开辟不同的显存空间。

之前的代码,大都是用默认的0号帧缓存来做渲染的。

image.png

一个完整的帧缓冲需要满足以下的条件:

  • 帧缓存需要至少绑定一个附件(颜色、深度或模板缓冲)。
  • 至少绑定一个颜色附件(Attachment)。
  • 每个绑定的附件必须都开辟内存完毕。
  • 每个缓冲都应该有相同的样本标准(N*M个像素数据信息)。

各类附件的实现方法:

  1. 使用Texture作为ColorDepthStencil等各类Buffer,并开辟空间。
    特点:所有渲染操作的结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便地使用它。

  2. 使用RenderBuffer作为ColorDepthStencil等各类Buffer,并开辟空间。
    特点:拥有快速的流水线访问结构,方便拷贝给其他Buffer或者写入数据。

  3. 根据实际情况,混合使用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

image.png

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进一步再做处理的过程。

卷积操作

image.png

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请发送邮件至:cloudbbs@huaweicloud.com;如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。