概述
1.坐标系
计算机利用OpenGL可以把三维世界中的三维物体,在二维屏幕上显示出来。如下图(来源于网络):
OpenGL图形渲染管线(Pipeline)学习
一部摄像机放在视椎体的顶部,也就是视椎体四条线交汇的部分。只有视椎体内部的三维物体才会经过一系列的坐标转换被输出到计算机屏幕上。
视椎体是一个矩形底座和顶座被截去顶部的立锥体。视椎体外的红色圆圈和蓝色的部分区域没有显示出来。
因为要把三维的物体映射到二维屏幕上,所以需要坐标转换。
世界坐标:三维物体在现实空间的位置,以XYZ来表示,坐标原点可以自定义;在三维世界的模型坐标基于同一个坐标原点,可以通过平移、旋转、缩放调整三维物体的位置、方位、大小。
模型坐标:是三维模型自己的坐标系,坐标原点一般位于模型的中心位置。每个模型都有一个属于自己的坐标系统,不同的模型之间要想在坐标上发生关系,需要所有的模型统一到世界坐标系下。模型坐标系在处理模型自身的图元之间的关系非常方便。模型同样也可以在自己的坐标系下平移、旋转和缩放。
观察坐标:也可以称为视点坐标、摄像机等,观察坐标主要是把世界坐标经过一系列的平移、旋转换成摄像机的正前方。
坐标计算的过程如下图所示(图片来源于网络)OpenGL渲染管线解析
为了将坐标从一个坐标系转换到另一个坐标系,我们需要用到几个转换矩阵,最重要的几个分别是模型(Model)、视图(View)、投影(Projection)三个矩阵。首先,顶点坐标开始于局部空间(Local Space),称为局部坐标(Local Coordinate),然后经过世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)结束。下面的图示显示了整个流程及各个转换过程做了什么:
局部坐标是对象相对于局部原点的坐标;也是对象开始的坐标。
将局部坐标转换为世界坐标,世界坐标是作为一个更大空间范围的坐标系统。这些坐标是相对于世界的原点的。
接下来我们将世界坐标转换为观察坐标,观察坐标是指以摄像机或观察者的角度观察的坐标。
在将坐标处理到观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标是处理-1.0到1.0范围内并判断哪些顶点将会出现在屏幕上。
最后,我们需要将裁剪坐标转换为屏幕坐标,我们将这一过程成为视口变换(Viewport Transform)。视口变换将位于-1.0到1.0范围的坐标转换到由glViewport函数所定义的坐标范围内。最后转换的坐标将会送到光栅器,由光栅器将其转化为片段。
你可能了解了每个单独的坐标空间的作用。我们之所以将顶点转换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当修改对象时,如果在局部空间中则是有意义的;当对对象做相对于其它对象的位置的操作时,在世界坐标系中则是有意义的;等等这些。如果我们愿意,本可以定义一个直接从局部空间到裁剪空间的转换矩阵,但那样会失去灵活性。接下来我们将要更仔细地讨论各个坐标系。
2. OpenGL 管线
简单来说管线就是一系列过程,三维模型转换成二维图形输出屏幕的过程。这个过程分为多个步骤,每个步骤的输出就是下一个步骤的输入。这些步骤有些实在CPU中运行,有些实在GPU中运行。具体如下:
其中着色器是运行在GPU的小程序,大家不要被它的名字所迷惑,它并不是单单用给像素点上颜色的,它还计算像素点的位置、纹理等信息。
因为GPU相对CPU来说,有更多的计算单元(成百上千计算核心)。CPU最多有两位数的计算单元。所以GPU可以同时使用大量的计算核心利用着色器程序计算每个像素点的位置、颜色、透明度等。因为每个像素单元的显示是独立的,所以每个GPU计算单元互不干扰。
例如:一张1080*900的图片,如果使用CPU计算的话,可能需要计算90多万次,但是使用GPU并行计算的话,一次性的就可以计算出来。所以效率就显而易见区分出来了。
顶点着色器和图元着色器在程序中可以进行编程的,把编程好的着色器放到GPU中运行。
顶点着色器:是GPU渲染管线的第一步,它的数据来源于CPU。CPU把定点坐标、颜色、纹理等数据送入GPU。GPU会使用定点着色器把每个顶点都运行一次。计算每个顶点的坐标、光照、颜色等。顶点着色器输入是坐标顶点输出是经过变换后的顶点。
图元装配:将顶点着色器输出的图元,装配成指定的图元(点、线、三角形),这些图元是构成模型的基本要素。
几何着色器:几何着色器一个可编程的可选阶段。几何着色器的输入是完整的图元,输出是一个或者多个其他的图元或者不输出任何图形。也就是将输入的点或者线扩展成多边形。能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。
光栅化:是把几何图元转换成像素的过程,通俗来讲就是把一个三维图形拍平后的效果,然后在屏幕上显示。确定图元所围成的像素的位置以及图元边界像素的位置。
片段着色器:计算图元中每个像素点的颜色、光照、阴影等(主要是和颜色相关)。每个像素点有4个元素组成(RGBA:红、绿、兰、透明度),每个分量取值范围都是0.0-1.0。片段着色器单独处理每一个片段,并不会影响到周围片元的计算,每个片元的计算都是独立的。正是因为独立性才保证了GPU可以高并发的工作。
3.编程测试
顶点缓存对象:(Vertex Buffer Object)VBO,把内存数据转移到显卡缓存。
顶点数组对象:(Vertex Array Object)VAO。(一个记忆机,记录了绘制一个物体所需要的状态,本身并不存储数据)
为VBO属性配置,记录和哪个VBO绑定的数据;
记忆绑定VBOs,怎么绑定的;
记忆绘制一些顺序的EBO;
一个VAO可以对应多个VBO.一个VBO也可以对应多个VAO.
索引缓存对象:(Element Buffer Object)EBO或(Index Buffer Object)IBO
#pragma once
#include <QOpenGLWindow>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
#include<QOpenGLFunctions_3_3_Core>
class QOpenGLFunctions_3_3_Core;
class MyOpenGLWnd : public QOpenGLWindow {
Q_OBJECT
public:
MyOpenGLWnd();
~MyOpenGLWnd();
private:
void initializeGL()override;
void resizeGL(int w, int h)override;
void paintGL()override;
private:
QOpenGLFunctions_3_3_Core* core;
GLuint VAO;
GLuint VBO;
QOpenGLShaderProgram shaderProgram;//着色器程序,所里系统所有的着色器
};
#include "MyOpenGLWnd.h"
#include <qgl.h>
MyOpenGLWnd::MyOpenGLWnd() {
}
MyOpenGLWnd::~MyOpenGLWnd() {
}
void MyOpenGLWnd::initializeGL() {
//初始化OpenGL的包装类,然后才可以使用OpenGL里面的函数
core = QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_3_3_Core>();
//请主要数值必须在-1 ~ 1之间,如果不在这个范围内,就不会在视口中出现。
GLfloat ver[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f,
};
//GLuint VAO;//在3.3版本及以后使用
/*
** 所有和GL_ARRAY_BUFFER有关系的操作都会被记录下来
** 第一个参数需要创建的缓存数量
** 第二个参数用于存储单一ID或者多个ID
*/
core->glGenVertexArrays(1, &VAO);
/*
** 把VAO绑定到openGL上
*/
core->glBindVertexArray(VAO);
/* 1.创建一个VBO,告诉程序数据要送到显卡哪里(显卡缓存地址)
** 第一个参数:缓存对象的数量;
** 第二个参数:用来保存显卡中显存对象的地址(数组名称),在显卡中名称就代表了地址
** 注:第二个参数不能是指针,如果是指针的话指的是内存中的地址。
** 变量VBO就代表了显卡缓存中的地址
*/
//GLuint VBO;
core->glGenBuffers(1, &VBO);
/*
** 2.把地址根数据绑定
** 第一个参数表明如果以后送的数据也是这个宏GL_ARRAY_BUFFER类型,就表明数据和VBO绑定在一起的。
** 确切的说值指明要绑定数据的数据类型
*/
core->glBindBuffer(GL_ARRAY_BUFFER, VBO);//绑定缓存对象
/*
** 3.把数据送进显卡的缓存
** 第一个参数和glBindBuffer的第一个参数相同,说明就是把数据缓存到GPU的VBO的。
** 第四个参数指定了我们希望显卡如何管理规定的数据,他有三种形式:
** GL_STATIC_DRAW: 数据不会或者不会被改变
** GL_DYNAMIC_DRAW:数据会被改变很多
** GL_STREAM_DRAW: 每次绘制时都会改变
** 三角形的位置数据不会被改变,每次渲染的时候都保持原样,所以它的类型最好是:GL_STATIC_DRAW
** 如果一个缓存中数据会被频繁改变,那么就是用类型GL_DYNAMIC_DRAW或者GL_STREAM_DRAW,
** 这样显卡就会把数据放在高速写入的部分。
*/
//把当前某种类型的缓冲数据从内存传输到GPU的缓存区,GPU缓存地址就是VBO所代表的地址
core->glBufferData(GL_ARRAY_BUFFER, sizeof(ver), ver, GL_STATIC_DRAW);
/*
** 通过以上可以看到,OpenGL是一个状态机,openGL先和显卡缓存地址绑定在一起,
** 然后openGL和数据绑定在一起,最后通过OpenGL把数据放到缓存中。
** 当数据被存到显卡缓存以后,就可以把ver数据清理掉了,因为显卡中已经有数据了。
*/
/*
** 对VBO进行属性配置
** 第一个参数:与着色器的location对应,在一个VAO里面数字是不可以重复的
** 第二个参数:顶点属性的大小,也就是一次性可以读多少个数据 3代表一次读取三个数据
** 第三个参数:读取的数据类型
** 第四个参数:数据是否被标准化,如果数据在-1~1之间,没有必要被标准化,如果数据不在-1~1这个区间,在它们之外,你可以使用GL_TRUE,
** 目的是进行一个强制的标准化,OpenGL认为只要标准化的数据就一定要被画出来。
** 第五个参数:代表读取数据的最大步长
** 第六个参数:读取最大步长后再其中读取的起始位置;
**
** 第二第五和六两个参数共同起作用
** GLfloat ver[] = {
** -0.5f, -0.5f, 0.0f, 1.0, 1.0, 1.0,
** 0.5f, -0.5f, 0.0f, 1.0, 1.0, 1.0,
** 0.0f, 0.5f, 0.0f, 1.0, 1.0, 1.0,
** };
** 如果第五个参数是6,第六个参数是3,说明一次性读取留个参数,每次只取后面3个参数
** glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (void*)(3*sizeof(GLfloat)));
*/
core->glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (void*)0);
//当把上面的数据传入到显卡的缓存以后,显卡并不知道这些数据是做什么用的,是坐标数据还是颜色数据或者纹理数据?
/*
** 以顶点属性作为参数,启动顶点属性,让着色器可以访问这块数据
** 参数:着色器中的location值相对应
** 相当于设一个权限,确定和哪个着色器想关联
*/
core->glEnableVertexAttribArray(0);
//绑定缓存区,这一步其实也可以不需要
core->glBindBuffer(GL_ARRAY_BUFFER, 0);
core->glBindVertexArray(0);
/******************************************************/
/*
** 着色器
** 着色器属于动态编译
*/
QOpenGLShader vertexShager(QOpenGLShader::Vertex);//顶点着色器
vertexShager.compileSourceFile("E:/Projects/QtGuiTest/OPenGLApp/shader/triangle.vert");
QOpenGLShader fragmentShager(QOpenGLShader::Fragment);//片段着色器
fragmentShager.compileSourceFile("E:/Projects/QtGuiTest/OPenGLApp/shader/triangle.frag");
shaderProgram.addShader(&vertexShager);
shaderProgram.addShader(&fragmentShager);
shaderProgram.link();
}
void MyOpenGLWnd::resizeGL(int w, int h) {
}
void MyOpenGLWnd::paintGL() {
//设置清除颜色,使用当前颜色,清除背景
core->glClearColor(0.6f, 0.8f, 0.5f, 1.0f);
core->glClear(GL_COLOR_BUFFER_BIT);
//把着色器送入显卡缓存
shaderProgram.bind();
core->glBindVertexArray(VAO);//会将它记忆的那些状态,相当于那几个函数执行一遍
/*
** 绘制一个三角形,
** 第二个参数:数组的起始位置,
** 第三个参数:绘制的点数
*/
core->glDrawArrays(GL_TRIANGLES, 0, 3);
update();
}
main.cpp
#include "OPenGLApp.h"
#include <QtWidgets/QApplication>
#include <QtOpenGL/QtOpenGL>
#include "MyOpenGLWnd.h"
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
MyOpenGLWnd window;
window.setTitle(QStringLiteral("这是一个OpenGL窗口"));
window.resize(800, 800);
window.show();
return a.exec();
}
两个着色器:
triangle.vert
#version 330 core
layout (location=0) in vec3 aPos;
void main(){
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
triangle.frag
#version 330 core
out vec4 fragColor;
void main(){
fragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
文件目录结构:
运行结果:
aaa
最后
以上就是酷炫百褶裙为你收集整理的OPenGL 基本知识(根据自己理解整理)的全部内容,希望文章能够帮你解决OPenGL 基本知识(根据自己理解整理)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复