Learn OpenGL with Qt——坐标系统

如果你是中途开始学习本教程,即使你对OpenGL已经非常熟悉,请至少了解一下以下几个章节,因为Qt中提供了OpenGL的很多便捷操作,熟悉这些操作可以让我们在Qt中高效的使用OpenGL进行绘图。

创建OpenGL窗口

着色器程序已经着色器的创建

创建与加载纹理

使用Qt内置矩阵进行变换

Qt-OpenGL的几个优势:

  1. Qt内嵌了opengl的相关环境,不需要我们自己来搭建,这对小白来说是很友好的。
  2. Qt和opengl都具有优良的跨平台特性,使用Qt做opengl开发可谓是强强联合。
  3. Qt可以轻松的控制窗口的各种处理事件以及窗口属性。
  4. Qt提供了opengl函数的C++封装,使得opengl原来的C风格API可以通过C++的面向对象技术来实现。
  5. Qt提供了十分完善的官方文档,有助于我们掌握QtOpenGL的各种细节。

这个教程将完全使用Qt对openglAPI的C++封装,内容板块尽量与learnopengl保持一致。笔者会逐步的实现教程里的demo,尽可能的说明每一个操作细节。你可以在文章的右上角找到本节的索引目录,如果什么地方操作失败,你可以直接复制代码节点的代码,尝试运行一下,再对比一下自己的代码,看自己是否什么地方出问题了,如果还不能解决问题,可以在下方评论区留言,笔者看到一定第一时间解答。

笔者对openGL了解不是很深,如果什么地方存在问题,希望朋友们能够详细指出。

坐标系统

在上一个教程中,我们学习了如何有效地利用矩阵的变换来对所有顶点进行变换。OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的xyz坐标都应该在-1.01.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:

  • 局部空间(Local Space,或者称为物体空间(Object Space))
  • 世界空间(World Space)
  • 观察空间(View Space,或者称为视觉空间(Eye Space))
  • 裁剪空间(Clip Space)
  • 屏幕空间(Screen Space)

这就是一个顶点在最终被转化为片段之前需要经历的所有不同状态。

你现在可能会对什么是坐标空间,什么是坐标系统感到非常困惑,所以我们将用一种更加通俗的方式来解释它们。下面,我们将显示一个整体的图片,之后我们会讲解每个空间的具体功能。

概述

为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(Model)、观察(View)、投影(Projection)三个矩阵。我们的顶点坐标起始于局部空间(Local Space),在这里它称为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate),观察坐标(View Coordinate),裁剪坐标(Clip Coordinate),并最后以屏幕坐标(Screen Coordinate)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么:

Learn OpenGL with Qt——坐标系统_第1张图片

 

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

你可能已经大致了解了每个坐标空间的作用。我们之所以将顶点变换到各个不同的空间的原因是有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体进行修改的时候,在局部空间中来操作会更说得通;如果要对一个物体做出一个相对于其它物体位置的操作时,在世界坐标系中来做这个才更说得通,等等。如果我们愿意,我们也可以定义一个直接从局部空间变换到裁剪空间的变换矩阵,但那样会失去很多灵活性。

接下来我们将要更仔细地讨论各个坐标系统。

局部空间

局部空间是指物体所在的坐标空间,即对象最开始所在的地方。想象你在一个建模软件(比如说Blender)中创建了一个立方体。你创建的立方体的原点有可能位于(0, 0, 0),即便它有可能最后在程序中处于完全不同的位置。甚至有可能你创建的所有模型都以(0, 0, 0)为初始位置(译注:然而它们会最终出现在世界的不同位置)。所以,你的模型的所有顶点都是在局部空间中:它们相对于你的物体来说都是局部的。

我们一直使用的那个箱子的顶点是被设定在-0.5到0.5的坐标范围中,(0, 0)是它的原点。这些都是局部坐标。

世界空间

如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的。

模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子,你需要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵;我们将箱子的局部坐标变换到场景/世界中的不同位置。

观察空间

观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。在下一节中我们将深入讨论如何创建一个这样的观察矩阵来模拟一个摄像机。

裁剪空间

在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。

因为将所有可见的坐标都指定在-1.0到1.0的范围内不是很直观,所以我们会指定自己的坐标集(Coordinate Set)并将它变换回标准化设备坐标系,就像OpenGL期望的那样。

为了将顶点坐标从观察变换到裁剪空间,我们需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000。投影矩阵接着会将在这个指定的范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0)。所有在范围外的坐标不会被映射到在-1.0到1.0的范围之间,所以会被裁剪掉。在上面这个投影矩阵所指定的范围内,坐标(1250, 500, 750)将是不可见的,这是由于它的x坐标超出了范围,它被转化为一个大于1.0的标准化设备坐标,所以被裁剪掉了。

由投影矩阵创建的观察箱(Viewing Box)被称为平截头体(Frustum),每个出现在平截头体范围内的坐标都会最终出现在用户的屏幕上。将特定范围内的坐标转化到标准化设备坐标系的过程(而且它很容易被映射到2D观察空间坐标)被称之为投影(Projection),因为使用投影矩阵能将3D坐标投影(Project)到很容易映射到2D的标准化设备坐标系中。

一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。

一旦所有顶点被变换到裁剪空间,最终的操作——透视除法(Perspective Division)将会执行,在这个过程中我们将位置向量的x,y,z分量分别除以向量的齐次w分量;透视除法是将4D裁剪空间坐标变换为3D标准化设备坐标的过程。这一步会在每一个顶点着色器运行的最后被自动执行。

在这一阶段之后,最终的坐标将会被映射到屏幕空间中(使用glViewport中的设定),并被变换成片段。

将观察坐标变换为裁剪坐标的投影矩阵可以为两种不同的形式,每种形式都定义了不同的平截头体。我们可以选择创建一个正射投影矩阵(Orthographic Projection Matrix)或一个透视投影矩阵(Perspective Projection Matrix)。

正射投影

正射投影矩阵定义了一个类似立方体的平截头箱,它定义了一个裁剪空间,在这空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。在使用正射投影矩阵变换至裁剪空间之后处于这个平截头体内的所有坐标将不会被裁剪掉。它的平截头体看起来像一个容器:

Learn OpenGL with Qt——坐标系统_第2张图片

上面的平截头体定义了可见的坐标,它由由宽、高、近(Near)平面和远(Far)平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。正射平截头体直接将平截头体内部的所有坐标映射为标准化设备坐标,因为每个向量的w分量都没有进行改变;如果w分量等于1.0,透视除法则不会改变这个坐标。

要创建一个正射投影矩阵,我们可以使用QMatrix4x4的ortho函数:

QMatrix4x4 matrix;
matrix.ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);

前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。

正射投影矩阵直接将坐标映射到2D平面中,即你的屏幕,但实际上一个直接的投影矩阵会产生不真实的结果,因为这个投影没有将透视(Perspective)考虑进去。所以我们需要透视投影矩阵来解决这个问题。

透视投影

如果你曾经体验过实际生活给你带来的景象,你就会注意到离你越远的东西看起来更小。这个奇怪的效果称之为透视(Perspective)。透视的效果在我们看一条无限长的高速公路或铁路时尤其明显,正如下面图片显示的那样:

Learn OpenGL with Qt——坐标系统_第3张图片

正如你看到的那样,由于透视,这两条线在很远的地方看起来会相交。这正是透视投影想要模仿的效果,它是使用透视投影矩阵来完成的。这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,从而使得离观察者越远的顶点坐标w分量越大。被变换到裁剪空间的坐标都会在-w到w的范围之间(任何大于这个范围的坐标都会被裁剪掉)。OpenGL要求所有可见的坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:

 

顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小。这是也是w分量非常重要的另一个原因,它能够帮助我们进行透视投影。最后的结果坐标就是处于标准化设备空间中的。如果你对正射投影矩阵和透视投影矩阵是如何计算的很感兴趣(且不会对数学感到恐惧的话)我推荐这篇由Songho写的文章。

在Qt中可以这样创建一个透视投影矩阵:

QMatrix4x4 matrix;
matrix.perspective(45.0f,width()/(float)height(),0.1f,100.0f);

透视矩阵所做的其实就是创建了一个定义了可视空间的大平截头体,任何在这个平截头体以外的东西最后都不会出现在裁剪空间体积内,并且将会受到裁剪。一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间上的一个点。下面是一张透视平截头体的图片:

perspective_frustum

 它的第一个参数定义了fov的值,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个末日风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。

 当使用正射投影时,每一个顶点坐标都会直接映射到裁剪空间中而不经过任何精细的透视除法(它仍然会进行透视除法,只是w分量没有被改变(它保持为1),因此没有起作用)。因为正射投影没有使用透视,远处的物体不会显得更小,所以产生奇怪的视觉效果。由于这个原因,正射投影主要用于二维渲染以及一些建筑或工程的程序,在这些场景中我们更希望顶点不会被透视所干扰。某些如 Blender 等进行三维建模的软件有时在建模时也会使用正射投影,因为它在各个维度下都更准确地描绘了每个物体。下面你能够看到在Blender里面使用两种投影方式的对比:

Learn OpenGL with Qt——坐标系统_第4张图片

你可以看到,使用透视投影的话,远处的顶点看起来比较小,而在正射投影中每个顶点距离观察者的距离都是一样的。 

把它们都组合到一起

我们为上述的每一个步骤都创建了一个变换矩阵:模型矩阵、观察矩阵和投影矩阵。一个顶点坐标将会根据以下过程被变换到裁剪坐标:

注意矩阵运算的顺序是相反的(记住我们需要从右往左阅读矩阵的乘法)。最后的顶点应该被赋值到顶点着色器中的gl_Position,OpenGL将会自动进行透视除法和裁剪。

Learn OpenGL with Qt——坐标系统_第5张图片

这一章的主题可能会比较难理解,如果你仍然不确定每个空间的作用的话,你也不必太担心。接下来你会看到我们是怎样运用这些坐标空间的,而且之后也会有足够多的例子。

进入3D

既然我们知道了如何将3D坐标变换为2D坐标,我们可以开始使用真正的3D物体,而不是枯燥的2D平面了。

在开始进行3D绘图时,我们首先创建一个模型矩阵。这个模型矩阵包含了位移、缩放与旋转操作,它们会被应用到所有物体的顶点上,以变换它们到全局的世界空间。让我们变换一下我们的平面,将其绕着x轴旋转,使它看起来像放在地上一样。这个模型矩阵看起来是这样的:

QMatrix4x4 model;
model.rotate(-55.0f,QVector3D(1.0f,0.0f,0.0f));

通过将顶点坐标乘以这个模型矩阵,我们将该顶点坐标变换到世界坐标。我们的平面看起来就是在地板上,代表全局世界里的平面。

接下来我们需要创建一个观察矩阵。我们想要在场景里面稍微往后移动,以使得物体变成可见的(当在世界空间时,我们位于原点(0,0,0))。要想在场景里面移动,先仔细想一想下面这个句子:

  • 将摄像机向后移动,和将整个场景向前移动是一样的。

这正是观察矩阵所做的,我们以相反于摄像机移动的方向移动整个场景。因为我们想要往后移动,并且OpenGL是一个右手坐标系(Right-handed System),所以我们需要沿着z轴的正方向移动。我们会通过将场景沿着z轴负方向平移来实现。它会给我们一种我们在往后移动的感觉。

Learn OpenGL with Qt——坐标系统_第6张图片

 在下一个教程中我们将会详细讨论如何在场景中移动。就目前来说,观察矩阵是这样的:

QMatrix4x4 view;
view.translate(0.0f,0.0f,-3.0f);

最后我们需要做的是定义一个投影矩阵。我们希望在场景中使用透视投影,所以像这样声明一个投影矩阵:

QMatrix4x4 projection;
projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);

既然我们已经创建了变换矩阵,我们应该将它们传入着色器。首先,让我们在顶点着色器中声明一个uniform变换矩阵然后将它乘以顶点坐标:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    // 注意乘法要从右向左读
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

我们还应该将矩阵传入着色器(这通常在每次的渲染迭代中进行,因为变换矩阵会经常变动):

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT);       //清除颜色缓存
    shaderProgram.bind();                     //使用shaderProgram着色程序
    {
        QMatrix4x4 model;
        model.rotate(-55.0f,QVector3D(1.0f,0.0f,0.0f));
        shaderProgram.setUniformValue("model",model);

        QMatrix4x4 view;
        view.translate(0.0f,0.0f,-3.0f);
        shaderProgram.setUniformValue("view",view);

        QMatrix4x4 projection;
        projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
        shaderProgram.setUniformValue("projection",projection);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};
        this->glDrawArrays(GL_POLYGON,0,4);     //使用以0开始,长度为4的顶点数据来绘制多边形
    }
}

我们的顶点坐标已经使用模型、观察和投影矩阵进行变换了,最终的物体应该会:

  • 稍微向后倾斜至地板方向。
  • 离我们有一些距离。
  • 有透视效果(顶点越远,变得越小)。

让我们检查一下结果是否满足这些要求:

Learn OpenGL with Qt——坐标系统_第7张图片

 它看起来就像是一个3D的平面,静止在一个虚构的地板上。

代码节点

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;
private:
    QVector vertices;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;
};

#endif // WIDGET_H

widget.cpp 

#include "widget.h"
#include 

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
{
    vertices={
        // 位置              // 颜色               //纹理坐标
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,    1.0f, 0.0f,   // 右下
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,    0.0f, 0.0f,   // 左下
        -0.5f,  0.5f, 0.0f,  0.0f, 1.0f, 1.0f,    0.0f, 1.0f,   // 左下
         0.5f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f,    1.0f, 1.0f,   // 右上
    };
    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast(&Widget::update));
    timer.start();
}

Widget::~Widget()
{
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<glViewport(0,0,w,h);                //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT);       //清除颜色缓存
    shaderProgram.bind();                     //使用shaderProgram着色程序
    {
        QMatrix4x4 model;
        model.rotate(-55.0f,QVector3D(1.0f,0.0f,0.0f));
        shaderProgram.setUniformValue("model",model);

        QMatrix4x4 view;
        view.translate(0.0f,0.0f,-3.0f);
        shaderProgram.setUniformValue("view",view);

        QMatrix4x4 projection;
        projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
        shaderProgram.setUniformValue("projection",projection);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};
        this->glDrawArrays(GL_POLYGON,0,4);     //使用以0开始,长度为4的顶点数据来绘制多边形
    }
}

triangle.vert

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;

out vec3 ourColor;
out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    // 注意乘法要从右向左读
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    ourColor = aColor;
    TexCoord = aTexCoord;
}

triangle.frag

#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ourTexture1;
void main()
{
    FragColor = mix(texture(ourTexture, TexCoord), texture(ourTexture1, TexCoord), 0.4);
}

从2D到3D

因为我们已经使用了纹理贴图,不再需要额外设置顶点颜色了,所以把着色器中关于颜色的变量去掉。

到目前为止,我们一直都在使用一个2D平面,而且甚至是在3D空间里!所以,让我们大胆地拓展我们的2D平面为一个3D立方体。要想渲染一个立方体,我们一共需要36个顶点(6个面 x 每个面有2个三角形组成 x 每个三角形有3个顶点),这是36个顶点的位置:

    vertices = {
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };

由于我们去除了顶点的颜色属性,需要我们重新设置顶点的解析格式:

 //设置顶点解析格式,并启用顶点
    shaderProgram.setAttributeBuffer("aPos", GL_FLOAT, 0, 3, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aPos");
    shaderProgram.setAttributeBuffer("aTexCoord", GL_FLOAT,sizeof(GLfloat) * 3, 2, sizeof(GLfloat) * 5);
    shaderProgram.enableAttributeArray("aTexCoord");

并且调用:

this->glDrawArrays(GL_TRIANGLES, 0, 36);    

你会看到:

Learn OpenGL with Qt——坐标系统_第8张图片

懵了都,这哪是立方体。为什么会这样?

之所以这样是因为OpenGL是一个三角形一个三角形地来绘制你的立方体的,所以即便之前那里有东西它也会覆盖之前的像素。因为这个原因,有些三角形会被绘制在其它三角形上面,虽然它们本不应该是被覆盖的。

笔者注:opengl是把一个3D图形转换成一个2D图形,出现上面的原因是,我们只是单纯的转换,并没有考虑空间的遮挡,没有把被挡住的部分舍弃,而是按顺序的,一个个的绘制图形,覆盖到原来的位置。

幸运的是,OpenGL存储深度信息在一个叫做Z缓冲(Z-buffer)的缓冲中,它允许OpenGL决定何时覆盖一个像素而何时不覆盖。通过使用Z缓冲,我们可以配置OpenGL来进行深度测试。

Z缓冲

OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer),Qt会自动为你生成这样一个缓冲(就像它也有一个颜色缓冲来存储输出图像的颜色)。深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。

然而,如果我们想要确定OpenGL真的执行了深度测试,首先我们要告诉OpenGL我们想要启用深度测试;它默认是关闭的。我们可以通过glEnable函数来开启深度测试。glEnable和glDisable函数允许我们启用或禁用某个OpenGL功能。这个功能会一直保持启用/禁用状态,直到另一个调用来禁用/启用它。现在我们想启用深度测试,需要在initializeGL函数中开启GL_DEPTH_TEST:

this->glEnable(GL_DEPTH_TEST);

 因为我们使用了深度测试,我们也想要在每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中)。就像清除颜色缓冲一样,我们可以通过在paintGL中的glClear函数中指定DEPTH_BUFFER_BIT位来清除深度缓冲:

this->glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);       //清除颜色缓存

 当我们再次运行程序的时候,你会看到下面的图形

Learn OpenGL with Qt——坐标系统_第9张图片

是不是太枯燥了?下面我们让这个立方体随时间旋转。我们将model矩阵的旋转角度与时间进行关联:

        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        QMatrix4x4 model;
        model.rotate(180*time,QVector3D(1.0f,0.5f,0.3f));
        shaderProgram.setUniformValue("model",model);

然后你会看到下面的效果:

代码节点:

 widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;
private:
    QVector vertices;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;
};

#endif // WIDGET_H

 widget.cpp

#include "widget.h"
#include 

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
{
    vertices = {
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };
    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast(&Widget::update));
    timer.start();
}

Widget::~Widget()
{
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<glEnable(GL_DEPTH_TEST);
}

void Widget::resizeGL(int w, int h)
{
    this->glViewport(0,0,w,h);                //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);       //清除颜色缓存

    shaderProgram.bind();                     //使用shaderProgram着色程序
    {
        
        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        QMatrix4x4 model;
        model.rotate(180*time,QVector3D(1.0f,0.5f,0.3f));
        shaderProgram.setUniformValue("model",model);

        QMatrix4x4 view;
        view.translate(0.0f,0.0f,-3.0f);
        shaderProgram.setUniformValue("view",view);

        QMatrix4x4 projection;
        projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
        shaderProgram.setUniformValue("projection",projection);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};
        this->glDrawArrays(GL_TRIANGLES, 0, 36);     //使用以0开始,长度为36的顶点数据来绘制三角形

    }
}

triangle.vert

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    // 注意乘法要从右向左读
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}

triangle.frag

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ourTexture1;
void main()
{
    FragColor = mix(texture(ourTexture, TexCoord), texture(ourTexture1, TexCoord), 0.4);
}

更多的立方体

现在我们想在屏幕上显示10个立方体。每个立方体看起来都是一样的,区别在于它们在世界的位置及旋转角度不同。立方体的图形布局已经定义好了,所以当渲染更多物体的时候我们不需要改变我们的缓冲数组和属性数组,我们唯一需要做的只是改变每个对象的模型矩阵来将立方体变换到世界坐标系中。

首先,让我们为每个立方体定义一个位移向量来指定它在世界空间的位置。我们将在一个QVector3D数组中定义10个立方体位置

在头文件中添加私有成员变量cubePositions

QVector cubePositions;

在构造函数中对数据进行初始化:

    cubePositions = {
      { 0.0f,  0.0f,  0.0f  }, 
      { 2.0f,  5.0f, -15.0f }, 
      {-1.5f, -2.2f, -2.5f  },  
      {-3.8f, -2.0f, -12.3f },  
      { 2.4f, -0.4f, -3.5f  },  
      {-1.7f,  3.0f, -7.5f  },  
      { 1.3f, -2.0f, -2.5f  },  
      { 1.5f,  2.0f, -2.5f  }, 
      { 1.5f,  0.2f, -1.5f  }, 
      {-1.3f,  1.0f, -1.5f  },  
    };

现在,在游戏循环中,我们调用glDrawArrays 10次,但这次在我们渲染之前每次传入一个不同的模型矩阵到顶点着色器中。我们将会在游戏循环中创建一个小的循环用不同的模型矩阵渲染我们的物体10次。注意我们也对每个箱子加了一点旋转:

float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
for(unsigned int i = 0; i < 10; i++){
    QMatrix4x4 model;
    model.translate(cubePositions[i]);
    model.rotate(180*time+i*20.0f,QVector3D(1.0f,0.5f,0.3f));
    shaderProgram.setUniformValue("model",model);
    this->glDrawArrays(GL_TRIANGLES, 0, 36);
}

这段代码将会在每次新立方体绘制出来的时候更新模型矩阵,如此总共重复10次。然后我们应该就能看到一个拥有10个正在奇葩地旋转着的立方体的世界。

 完美!看起来我们的箱子已经找到志同道合的小伙伴了。如果你在这里卡住了,你可以对照一下源代码。

代码节点

widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
class Widget : public QOpenGLWidget,public QOpenGLExtraFunctions
{
    Q_OBJECT

public:
    Widget(QWidget *parent = 0);
    ~Widget();
protected:
    virtual void initializeGL() override;
    virtual void resizeGL(int w,int h) override;
    virtual void paintGL() override;
private:
    QVector vertices;
    QVector cubePositions;
    QOpenGLShaderProgram shaderProgram;
    QOpenGLBuffer VBO;
    QOpenGLVertexArrayObject VAO;
    QOpenGLTexture texture;
    QOpenGLTexture texture1;
    QTimer timer;
};

#endif // WIDGET_H

widget.cpp

#include "widget.h"
#include 

Widget::Widget(QWidget *parent)
    : QOpenGLWidget(parent)
    , VBO(QOpenGLBuffer::VertexBuffer)
    , texture(QOpenGLTexture::Target2D)
    , texture1(QOpenGLTexture::Target2D)
{
    vertices = {
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f, -0.5f,  0.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  0.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,

        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
         0.5f, -0.5f, -0.5f,  1.0f, 0.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
         0.5f, -0.5f,  0.5f,  1.0f, 1.0f,
        -0.5f, -0.5f,  0.5f,  0.0f, 1.0f,
        -0.5f, -0.5f, -0.5f,  0.0f, 0.0f,

        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f,
         0.5f,  0.5f, -0.5f,  1.0f, 1.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
         0.5f,  0.5f,  0.5f,  1.0f, 0.0f,
        -0.5f,  0.5f,  0.5f,  0.0f, 0.0f,
        -0.5f,  0.5f, -0.5f,  0.0f, 1.0f
    };

    cubePositions = {
      { 0.0f,  0.0f,  0.0f  },
      { 2.0f,  5.0f, -15.0f },
      {-1.5f, -2.2f, -2.5f  },
      {-3.8f, -2.0f, -12.3f },
      { 2.4f, -0.4f, -3.5f  },
      {-1.7f,  3.0f, -7.5f  },
      { 1.3f, -2.0f, -2.5f  },
      { 1.5f,  2.0f, -2.5f  },
      { 1.5f,  0.2f, -1.5f  },
      {-1.3f,  1.0f, -1.5f  },
    };

    timer.setInterval(18);
    connect(&timer,&QTimer::timeout,this,static_cast(&Widget::update));
    timer.start();
}

Widget::~Widget()
{
}

void Widget::initializeGL()
{
    this->initializeOpenGLFunctions();        //初始化opengl函数
    if(!shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/triangle.vert")){     //添加并编译顶点着色器
        qDebug()<<"ERROR:"<glEnable(GL_DEPTH_TEST);
}

void Widget::resizeGL(int w, int h)
{
    this->glViewport(0,0,w,h);                //定义视口区域
}

void Widget::paintGL()
{
    this->glClearColor(0.1f,0.5f,0.7f,1.0f);  //设置清屏颜色
    this->glClear(GL_COLOR_BUFFER_BIT| GL_DEPTH_BUFFER_BIT);       //清除颜色缓存

    shaderProgram.bind();                     //使用shaderProgram着色程序
    {



        QMatrix4x4 view;
        view.translate(0.0f,0.0f,-3.0f);
        shaderProgram.setUniformValue("view",view);

        QMatrix4x4 projection;
        projection.perspective(45.0f,width()/(float)height(),0.1f,100.0f);
        shaderProgram.setUniformValue("projection",projection);

        texture.bind(0);                                    //将texture绑定到纹理单元0
        shaderProgram.setUniformValue("ourTexture",0);      //让ourTexture从纹理单元0中获取纹理数据

        texture1.bind(1);                                    //将texture绑定到纹理单元1
        shaderProgram.setUniformValue("ourTexture1",1);      //让ourTexture从纹理单元1中获取纹理数据

        QOpenGLVertexArrayObject::Binder{&VAO};

        float time=QTime::currentTime().msecsSinceStartOfDay()/1000.0;
        for(unsigned int i = 0; i < 10; i++){
            QMatrix4x4 model;
            model.translate(cubePositions[i]);
            model.rotate(180*time+i*20.0f,QVector3D(1.0f,0.5f,0.3f));
            shaderProgram.setUniformValue("model",model);
            this->glDrawArrays(GL_TRIANGLES, 0, 36);
        }
    }
}

triangle.vert 

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    // 注意乘法要从右向左读
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    TexCoord = aTexCoord;
}

triangle.frag

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D ourTexture;
uniform sampler2D ourTexture1;
void main()
{
    FragColor = mix(texture(ourTexture, TexCoord), texture(ourTexture1, TexCoord), 0.4);
}

 

 

你可能感兴趣的:(learn,opengl,with,Qt)