OpenGL 自学总结

前言:

本人是工作后才接触到的OpenGL,大学找工作的时候其实比较着急,就想着尽快有个着落。工作后才发现自己的兴趣点。同时也能感觉到自己当前的工作有一点温水煮青蛙的意思,很担心自己往后能力跟不上年龄的增长。因此想在工作之余多学学自己感兴趣的东西,并记录下来。

本文计划按照模型数据,渲染流水线,顶点着色器,光栅化,片元着色器,其他具体知识点的顺序来梳理自己这段时间自学的内容。

正文:

1、模型数据

什么是模型数据,从本人目前学习的情况来理解,模型就是一组顶点数据的集合,注意,这里的顶点的数据不仅仅是顶点坐标,还包括纹理坐标,法线向量等等。其实每一项顶点数据都可以看作是广义的纹理,可能是二维的(如纹理坐标),也可能是三维的(如RGB颜色)。

代码实验使用的是obj格式的模型文件,其格式可以参考本章下文连接,还是比较好理解的。本人目前只解析了obj文件中的"v"(模型顶点坐标)、"vt"(模型纹理坐标)、"vn"(模型顶点法线坐标)。输入是模型文件目录;输出按照OpenGL的格式,为一段float类型的数据流,逻辑上按行划分,每行为一组顶点数据(顶点坐标,法线数据,纹理坐标)。代码如code 1-1、code 1-2所示:

cpp 复制代码
// objloader.h
#ifndef OBJLOADER_H
#define OBJLOADER_H
#include <QString>
#include "qdebug.h"
#include <iostream>
#include <fstream>
#include <QFile>

struct Vnode {
    float x, y, z;
};
struct Vnormal {
    float x, y, z;
};
struct Vtexture {
    float x, y;
};
struct gldata {
    float vx, vy, vz;
    float vnx, vny, vnz;
    float vtx, vty;
};

class objloader
{
public:
    bool ReadOBJFile(QString &fileName);
    bool GetOBJData(float** data, int* dataLen, int** iddata, int* idlen);
    QList<Vnode> Vlist;
    QList<Vnormal> Vnlist;
    QList<Vtexture> Vtlist;
    QList<gldata> glist;
    QList<int> idlist;
};

code 1-1

cpp 复制代码
// objloader.cpp
#include "objloader.h"
#include "qdebug.h"
#include <iostream>
#include <fstream>
#include <QFile>

bool objloader::ReadOBJFile(QString &fileName)
{
    QFile file(fileName);
    if(!file.open(QIODevice::ReadOnly|QIODevice::Text)){
        qDebug()<<"文件打开失败";
    }
    Vlist.clear();
    Vnlist.clear();
    Vtlist.clear();
    glist.clear();
    idlist.clear();
    int id = 0;
    while(!file.atEnd()) {
        QByteArray line = file.readLine();
        QString str(line);
        str = str.trimmed();
        if (str.length() < 2) {
            continue;
        }
        if (str[0] == 'v'){
            if (str[1] == 't'){ //纹理
                QStringList strlist = str.split(" ");
                Vtexture tmp;
                tmp.x = strlist[1].toFloat();
                tmp.y = strlist[2].toFloat();
                Vtlist.append(tmp);
            } else if (str[1] == 'n') { //法线
                QStringList strlist = str.split(" ");
                Vnormal tmp;
                tmp.x = strlist[1].toFloat();
                tmp.y = strlist[2].toFloat();
                tmp.z = strlist[3].toFloat();
                Vnlist.append(tmp);
            } else {
                QStringList strlist = str.split(" ");
                Vnode tmp;
                tmp.x = strlist[2].toFloat();
                tmp.y = strlist[3].toFloat();
                tmp.z = strlist[4].toFloat();
                Vlist.append(tmp);
            }
        } else if (str[0] == 'f') {
            QStringList strlist = str.split(" ");
            for (int i=1;i<strlist.size();i++) {
                QStringList info = strlist[i].split("/");
                if (info.size() < 3) {
                    qDebug()<<"f decode fail";
                    return false;
                }
                gldata node;
                if (info[0].toInt()-1 >= Vlist.size() ||
                    info[2].toInt()-1 >= Vnlist.size() ||
                    info[1].toInt()-1 >= Vtlist.size()) {
                    qDebug()<<"f overflow";
                    return false;
                }
                node.vx = Vlist.at(info[0].toInt()-1).x;
                node.vy = Vlist.at(info[0].toInt()-1).y;
                node.vz = Vlist.at(info[0].toInt()-1).z;
                node.vnx = Vnlist.at(info[2].toInt()-1).x;
                node.vny = Vnlist.at(info[2].toInt()-1).y;
                node.vnz = Vnlist.at(info[2].toInt()-1).z;
                node.vtx = Vtlist.at(info[1].toInt()-1).x;
                node.vty = Vtlist.at(info[1].toInt()-1).y;
                glist.append(node);
            }
            // push绘制点的下标 123和134,目的是确保绘制方向一致(顺时针)
            idlist.append(id);
            idlist.append(id+1);
            idlist.append(id+2);
            idlist.append(id);
            idlist.append(id+2);
            idlist.append(id+3);
            id = id + 4;
        } else if (str[0] == 'o') {
            qDebug()<<"o 解析失败";
        }
    }
    return true;
}
bool objloader::GetOBJData(float** data, int* dataLen, int** iddata, int* idlen)
{
    *dataLen = (sizeof(gldata)*(glist.size()));
    *data = (float*)malloc(*dataLen);
    *idlen = (sizeof(int)*(idlist.size()));
    *iddata = (int*)malloc(*idlen);
    for (int i=0;i<glist.size();i++) {
        if ((int)(i*sizeof(gldata)) >= *dataLen) {
            qDebug() << "GetOBJData out of mem";
        }
        memcpy((*data) + (i*(sizeof(gldata)/sizeof(float))), &glist.at(i), sizeof(gldata));
    }
    for (int i=0;i<idlist.size();i++) {
        memcpy((*iddata)+i, &idlist.at(i), sizeof(int));
    }
    return true;
}

code 1-2

相关学习:

3D文件格式之OBJ文件格式

2、渲染流水线

模型数据加载进内存中后,计算机只有一堆点的数据,如何绘制出模型的"形"呢?这就需要利用OpenGL的渲染流水线了。一般来说,一个渲染流程会分为三个阶段:应用阶段、几何阶段、光栅化阶段。图2-1是这三个阶段的联系。应用阶段是开发者工作的阶段,开发者需准备好要渲染的各种几何信息(包括模型数据、渲染状态、着色器等),即渲染图元;几何阶段通常在GPU上进行,负责处理应用阶段输入的渲染图元,一般是逐点或者逐多边形地操作(例如对每个顶点做光照处理)。最终几何阶段会将模型的顶点数据变换到屏幕空间中,并交给光栅器处理;光栅化阶段会将几何阶段传递下来的数据进行采样,产生屏幕上的像素,渲染出最终的图像。这一阶段也是在GPU进行的。

图2-1

整个渲染过程中,先是由CPU将数据加载进显存中,并设置渲染状态(例如使用哪些着色器),最后调用渲染命令。之后的工作都在GPU里进行。GPU内部的工作流程如图2-2所示,其中绿色表示该阶段可编程,黄色表示该阶段可配置不可编程,蓝色表示该节点开发者无法控制。实线表示该着色器必须由开发者编程实现,虚线表示该着色器是可选的。本文目前只涉及顶点着色器以及片元着色器。

图2-2

3、顶点着色器

顶点着色器对输入的每一个模型顶点做同样的处理流程,具体处理流程将由开发者编程实现现,一般为一个用GLSL(OpenGL Shading Language)语言编写的txt文件。code 3-1是一段顶点着色器的代码,作用是将传入的模型坐标变换到摄像机的裁剪空间,并设置模型的颜色和纹理并输出给片元着色器。语法和c语言类似,下面介绍代码中的几个关键字:

#version 330 core,指定GLSL的版本和配置。在这个例子中,表示使用OpenGL 3.3版本的核心配置。

layout (location = i) ,这是GLSL接收外部变量的方式之一,其中vec3表示该变量的类型,即3维向量(x,y,z)。后面的aPos则是变量名。外部代码通过code 3-2的方式传入变量,本文用于加载章节1输出的模型数据(顶点坐标、法线坐标、纹理坐标的大数据流)。

out,指定顶点着色器的输出变量,后面跟着变量类型、变量名。顶点着色器的输出将成为片元着色器的输入。

uniform,这是GLSL接受外部变量的另一种方式。mat4表示变量类型,是一个4*4的矩阵,model为变量名。本例子用于传入3个变换矩阵(MVP矩阵),外部代码通过code 3-3的方式传入变量。

cpp 复制代码
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 rPos;
layout (location = 2) in vec2 texCoord;
out vec3 normal;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
   gl_Position = projection*view*model*vec4(aPos.x, aPos.y, aPos.z, 1.0f);
   normal= rPos;
   TexCoord = texCoord;
}

code 3-1

cpp 复制代码
// 加载VAO
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
// 加载VBO,data是模型数据(顶点坐标、法线坐标、纹理坐标的大数据流)
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, datalen, data, GL_STATIC_DRAW);
// 加载EBO,iddata表示绘制模型各个三角形面时,每个三角形顶点坐标的索引,顶点坐标来源与上面的data
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, idlen, iddata, GL_STATIC_DRAW);

/* glVertexAttribPointer说明:
 * 每个顶点属性从一个VBO管理的内存中获得它的数据
 * 具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的
 */
// 绑定顶点
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);

// 绑定法线
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(1);

// 绑定纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT,GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);

/*
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer)
其中:
index:指定要修改的顶点属性的索引,与顶点着色器中的location对应。
size:指定数据的大小,例如顶点坐标是3维(3个数据),纹理是2维(2个数据)
type:指定每个组件的数据类型,可以是GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、GL_INT、GL_UNSIGNED_INT、GL_FLOAT或GL_DOUBLE。
normalized:指定是否应该将非浮点值映射到范围[0,1](如果为GL_TRUE)或[-1,1](如果为GL_FALSE)。
stride:在模型数据流中,两个指定数据之间的步长,本文中每个模型顶点数据由(顶点坐标,法线坐标,纹理坐标)构成,所以每个子数据之间的步长为8个float。
pointer:指定指向第一个顶点属性的指针。如果缓冲区对象绑定到GL_ARRAY_BUFFER,则pointer被解释为首份数据的偏移量;否则,它被解释为指针。
*/
/*
glEnableVertexAttribArray用于激活指定索引的顶点属性数组,使其可以被顶点着色器使用。可以理解为指定一块内存存放中间数据。一般情况下,OpenGL确保至少有16个包含4分量的顶点属性可用。
*/

code 3-2

cpp 复制代码
/*
*void glUniformMatrix4fv (GLint location, GLsizei count, GLboolean transpose, const GLfloat * value)
*location : uniform的位置
*count : 矩阵个数,一般为1
*transpose : 矩阵是列优先矩阵(GL_FALSE)还是行优先矩阵(GL_TRUE)
*value : 指向由count个元素的数组的指针,一般为矩阵的首地址指针
*/
GLint modelLoc = glGetUniformLocation(shaderProgram, "model");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, model.constData());
modelLoc = glGetUniformLocation(shaderProgram, "view");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, view.constData());
modelLoc = glGetUniformLocation(shaderProgram, "projection");
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, projection.constData());

code 3-3

相关学习:

顶点着色器与片元着色器 内置变量

4、光栅化

这部分由OpenGL自身实现,开发者无法控制,但我认为也需要了解其中的过程。光栅化是将内存中的模型(由若干个顶点组成)投影到屏幕空间,并采样到一个一个像素上的过程。之前说过一个模型在内存中表示为若干个顶点,每三个顶点能够组成一个三角形面,称作一个"图元"。光栅化的操作目标就是模型面上的各个图元。每个图元有哪三个顶点组成是之前加载EBO时确定好的。

为什么一个图元是三角形呢?原因有:1、三角形是最基础的多边形,所有的多边形都可以打碎成多个三角形的组合;2、光栅化还有一个很重要的一步------插值,即把顶点的一些属性(坐标、颜色、法线等)通过一定的策略附加到三角形内部的"像素"上,这个过程是线性。因此,只有三角形能够完成插值(4个点不一定在同一个平面,像法线、坐标这样的属性无法通过线性插值给到内部"像素")。

5、片元着色器

经过光栅化后,一个图元内部就有了若干"像素"(也可以叫片元),而片元着色器就是遍历这些"像素"做统一的处理。一般也是一个用GLSL语言编写的txt文件。code 5-1是一段片元着色器的代码,作用是输出当前片元的纹理值,法线暂时没用到(法线一般用于计算光照)。下面介绍下几个关键字:

in,接收顶点着色器的输出,后面跟着分别是数据类型和数据名。

out,片元着色器的输出,一般是颜色数据(RGBA)。

uniform sampler2D ourTexture,这是GLSL供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler2D、sampler3D。该变量能够获取到之前加载的的纹理数据,和输入的纹理坐标TexCoord结合使用就能够得出纹理值(颜色)。纹理的加载方式如code 5-2所示。

cpp 复制代码
#version 330 core
in vec3 normal;
in vec2 TexCoord;
out vec4 Fcolor;

uniform sampler2D ourTexture;

void main()
{
    Fcolor = texture(ourTexture, TexCoord);
}

code 5-1

cpp 复制代码
// 加载纹理
QImage img;
img.load("D:\\IDE\\QTProject\\opgl\\a.png");
// 改变编码格式,不然颜色对不上
img = img.convertToFormat(QImage::Format_RGB888);
int width = img.width();
int height = img.height();
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
// 加载并生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, img.bits());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenerateMipmap(GL_TEXTURE_2D);


// 绘制时需要加上
glBindTexture(GL_TEXTURE_2D, texture);

code 5-2

相关推荐
锦亦之22339 小时前
QT+OSG+OSG-earth如何在窗口显示一个地球
开发语言·qt
柳鲲鹏12 小时前
编译成功!QT/6.7.2/Creator编译Windows64 MySQL驱动(MinGW版)
开发语言·qt·mysql
三玖诶12 小时前
如何在 Qt 的 QListWidget 中逐行添加和显示数据
开发语言·qt
阳光开朗_大男孩儿18 小时前
DBUS属性原理
linux·服务器·前端·数据库·qt
Alphapeople19 小时前
Qt Modbus
开发语言·qt
竹林海中敲代码19 小时前
Qt Creator 集成开发环境 常见问题
qt·qt工具常见问题
竹林海中敲代码1 天前
Qt安卓开发连接手机调试(红米K60为例)
android·qt·智能手机
长沙红胖子Qt1 天前
关于 Qt运行加载内存较大崩溃添加扩大运行内存 的解决方法
开发语言·qt·qt扩大运行内存
gopher95111 天前
qt相关面试题
开发语言·qt·面试
三玖诶1 天前
在 Qt 中使用 QLabel 设置 GIF 动态背景
开发语言·qt·命令模式