LearnOpenGL Notes->はじめに->変換と座標系

変身

1. マトリックス

行列は、数値、記号、または式の長方形の配列です。マトリックスの各項目はマトリックスの要素と呼ばれます。2×3 行列の例を次に示します。

行列は (i, j) でインデックス付けできます。i は行、j は列です。そのため、上記の行列は 2×3 行列 (3 列 2 行、次元とも呼ばれます) と呼ばれます。マトリックス)。これは、2D 画像にインデックスを付ける場合の (x, y) とは対照的です。4 を取得するインデックスは (2, 1) (2 行目、1 列目) です (注釈: 画像の場合、インデックスは (1) 、2)、最初に列をカウントし、次に行をカウントします)。

行列とは基本的にそれだけであり、数式の長方形の配列です。ベクトルと同様、行列にも非常に美しい数学的特性があります。行列には、行列の加算、減算、乗算などの演算がいくつかあります。

1.1 行列の加算と減算

[行列とスカラーの加算と減算のための数学的演算はありませんが、多くの線形代数ライブラリ (たとえば、私たちが使用する GLM) がそれをサポートしていることに注意してください。numpy を使用したことがある場合は、それをBroadcastingと考えることができます

行列間の加算と減算は、 2 つの行列の対応する要素の加算と減算であるため、同じインデックスの下にある要素のみを演算できる点を除けば、全体的なルールはスカラー演算と似ています。これは、加算と減算が同じ次元の行列に対してのみ定義されることを意味します。3×2行列と2×3行列(または3×3行列と4×4行列)は加減算できません。2 つの 2×2 行列がどのように加算されるかを見てみましょう。

1.2 行列の乗算

行列とスカラーの加算と減算と同様に、行列とスカラー間の乗算は、行列の各要素にスカラーを乗算することです。次の例は、乗算プロセスを示しています。

これで、これらの個々の数値がスカラーと呼ばれる理由が理解できました。簡単に言えば、スカラーは、その値による行列のすべての要素のスケール(Scale) です(注釈: Scalar は Scale + -ar から進化したことに注意してください)。前の例では、すべての要素が 2 倍に拡大されました。

1.3 行列の乗算

矩阵的相乘有一些限制:

  1. 只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。

  1. 矩阵相乘不遵守交换律(Commutative),也就是说A⋅B≠B⋅AA⋅B≠B⋅A。

我们先看一个两个2×2矩阵相乘的例子:

我们首先把左侧矩阵的行和右侧矩阵的列拿出来。这些挑出来行和列将决定我们该计算结果2x2矩阵的哪个输出值。如果取的是左矩阵的第一行,输出值就会出现在结果矩阵的第一行。接下来再取一列,如果我们取的是右矩阵的第一列,最终值则会出现在结果矩阵的第一列。这正是红框里的情况。如果想计算结果矩阵右下角的值,我们要用第一个矩阵的第二行和第二个矩阵的第二列(译注:简单来说就是结果矩阵的元素的行取决于第一个矩阵,列取决于第二个矩阵)。

计算一项的结果值的方式是先计算左侧矩阵对应行和右侧矩阵对应列的第一个元素之积,然后是第二个,第三个,第四个等等,然后把所有的乘积相加,这就是结果了。现在我们就能解释为什么左侧矩阵的列数必须和右侧矩阵的行数相等了,如果不相等这一步的运算就无法完成了!

结果矩阵的维度是(n, m),n等于左侧矩阵的行数,m等于右侧矩阵的列数。

二.矩阵与向量相乘

目前为止,通过这些教程我们已经相当了解向量了。我们用向量来表示位置,表示颜色,甚至是纹理坐标。让我们更深入了解一下向量,它其实就是一个N×1矩阵,N表示向量分量的个数(也叫N维(N-dimensional)向量)。如果你仔细思考一下就会明白。向量和矩阵一样都是一个数字序列,但它只有1列。那么,这个新的定义对我们有什么帮助呢?如果我们有一个M×N矩阵,我们可以用这个矩阵乘以我们的N×1向量,因为这个矩阵的列数等于向量的行数,所以它们就能相乘

但是为什么我们会关心矩阵能否乘以一个向量?正巧,很多有趣的2D/3D变换都可以放在一个矩阵中,用这个矩阵乘以我们的向量将变换(Transform)这个向量。

2.1单位矩阵

在OpenGL中,由于某些原因我们通常使用4×4的变换矩阵,而其中最重要的原因就是大部分的向量都是4分量的。我们能想到的最简单的变换矩阵就是单位矩阵(Identity Matrix)。单位矩阵是一个除了对角线以外都是0的N×N矩阵。在下式中可以看到,这种变换矩阵使一个向量完全不变:

向量看起来完全没变。从乘法法则来看就很容易理解来:第一个结果元素是矩阵的第一行的每个元素乘以向量的每个对应元素。因为每行的元素除了第一个都是0,可得:1⋅1+0⋅2+0⋅3+0⋅4=11⋅1+0⋅2+0⋅3+0⋅4=1,向量的其他3个元素同理。

2.2缩放

对一个向量进行缩放(Scaling)就是对向量的长度进行缩放,而保持它的方向不变。由于我们进行的是2维或3维操作,我们可以分别定义一个有2或3个缩放变量的向量,每个变量缩放一个轴(x、y或z)。

我们先来尝试缩放向量v¯=(3,2)v¯=(3,2)。我们可以把向量沿着x轴缩放0.5,使它的宽度缩小为原来的二分之一;我们将沿着y轴把向量的高度缩放为原来的两倍。我们看看把向量缩放(0.5, 2)倍所获得的s¯是什么样的:

OpenGL通常是在3D空间进行操作的,对于2D的情况我们可以把z轴缩放1倍,这样z轴的值就不变了。

我们刚刚的缩放操作是不均匀(Non-uniform)缩放,因为每个轴的缩放因子(Scaling Factor)都不一样。如果每个轴的缩放因子都一样那么就叫均匀缩放(Uniform Scale)

我们下面会构造一个变换矩阵来为我们提供缩放功能。我们从单位矩阵了解到,每个对角线元素会分别与向量的对应元素相乘。如果我们把1变为3会怎样?这样子的话,我们就把向量的每个元素乘以3了,这事实上就把向量缩放3倍。如果我们把缩放变量表示为(S1,S2,S3)(S1,S2,S3)我们可以为任意向量(x,y,z)(x,y,z)定义一个缩放矩阵:

注意,第四个缩放向量仍然是1,因为在3D空间中缩放w分量是无意义的。w分量另有其他用途,在后面我们会看到;

2.3位移

位移(Translation)是在原始向量的基础上加上另一个向量从而获得一个在不同位置的新向量的过程,从而在位移向量基础上移动了原始向量

和缩放矩阵一样,在4×4矩阵上有几个特别的位置用来执行特定的操作,对于位移来说它们是第四列最上面的3个值。如果我们把位移向量表示为(Tx,Ty,Tz)(Tx,Ty,Tz),我们就能把位移矩阵定义为:

这样是能工作的,因为所有的位移值都要乘以向量的w行,所以位移值会加到向量的原始值上(想想矩阵乘法法则)。而如果你用3x3矩阵我们的位移值就没地方放也没地方乘了,所以是不行的。

[齐次坐标(Homogeneous Coordinates)

向量的w分量也叫齐次坐标。想要从齐次向量得到3D向量,我们可以把x、y和z坐标分别除以w坐标。我们通常不会注意这个问题,因为w分量通常是1.0。使用齐次坐标有几点好处:它允许我们在3D向量上进行位移(如果没有w分量我们是不能位移向量的),而且下一章我们会用w值创建3D视觉效果。

如果一个向量的齐次坐标是0,这个坐标就是方向向量(Direction Vector),因为w坐标是0,这个向量就不能位移(译注:这也就是我们说的不能位移一个方向)。]

有了位移矩阵我们就可以在3个方向(x、y、z)上移动物体,它是我们的变换工具箱中非常有用的一个变换矩阵。

2.4旋转

上面几个的变换内容相对容易理解,在2D或3D空间中也容易表示出来,但旋转(Rotation)稍复杂些。

首先我们来定义一个向量的旋转到底是什么。2D或3D空间中的旋转用角(Angle)来表示。角可以是角度制或弧度制的,周角是360角度或2 PI弧度。

转半圈会旋转360/2 = 180度,向右旋转1/5圈表示向右旋转360/5 = 72度。下图中展示的2D向量v¯是由k¯向右旋转72度所得的:

旋转矩阵在3D空间中每个单位轴都有不同定义,旋转角度用θθ表示:

2.5矩阵的组合

使用矩阵进行变换的真正力量在于,根据矩阵之间的乘法,我们可以把多个变换组合到一个矩阵中。让我们看看我们是否能生成一个变换矩阵,让它组合多个变换。假设我们有一个顶点(x, y, z),我们希望将其缩放2倍,然后位移(1, 2, 3)个单位。我们需要一个位移和缩放矩阵来完成这些变换。结果的变换矩阵看起来像这样:

注意,当矩阵相乘时我们先写位移再写缩放变换的。矩阵乘法是不遵守交换律的,这意味着它们的顺序很重要。当矩阵相乘时,在最右边的矩阵是第一个与向量相乘的,所以你应该从右向左读这个乘法建议您在组合矩阵时,先进行缩放操作,然后是旋转,最后才是位移,否则它们会(消极地)互相影响。比如,如果你先位移再缩放,位移的向量也会同样被缩放(译注:比如向某方向移动2米,2米也许会被缩放成1米)!

用最终的变换矩阵左乘我们的向量会得到以下结果:

不错!向量先缩放2倍,然后位移了(1, 2, 3)个单位。

三.实践

现在我们已经解释了变换背后的所有理论,是时候将这些知识利用起来了。OpenGL没有自带任何的矩阵和向量知识,所以我们必须定义自己的数学类和函数。在教程中我们更希望抽象所有的数学细节,使用已经做好了的数学库。幸运的是,有个易于使用,专门为OpenGL量身定做的数学库,那就是GLM

GLM是OpenGL Mathematics的缩写,它是一个只有头文件的库,也就是说我们只需包含对应的头文件就行了,不用链接和编译。GLM可以在它们的网站上下载。把头文件的根目录复制到你的includes文件夹,然后你就可以使用这个库了。

我们需要的GLM的大多数功能都可以从下面这3个头文件中找到:

#include <E:\OpenGl\glm\glm-master\glm\glm.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\matrix_transform.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\type_ptr.hpp>

先给出一些常用矩阵:

  • vec2 二维向量

  • vec3 三维向量

  • vec4 四维向量

  • mat2 二阶矩阵

  • mat3 三阶矩阵

  • mat4 四阶矩阵

以及一些常用函数:

  • glm::radians() 角度制转弧度制

  • glm::translate() 创建一个平移矩阵,第一个参数是目标矩阵,第二个参数是平移的方向向量

  • glm::rotate() 创建一个将点绕某个轴旋转x弧度的旋转矩阵,第一个参数是弧度,第二个参数是旋转轴

  • glm::scale() 创建一个缩放矩阵,第一个参数是目标矩阵,第二个参数是缩放系数

创建裁剪矩阵的函数,位于glm/ext/matrix_clip_space.hpp,这个文件存放裁剪空间相关的API

  • glm::ortho(float left, float right, float bottom, float top, float zNear, float zFar); 前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。

  • glm::perspective(float fovy, float aspect, float zNear, float zFar); 第一个参数为视锥上下面之间的夹角,第二个参数为宽高比,即视窗的宽/高,第三第四个参数分别为近截面和远界面的深度

我们来看看是否可以利用我们刚学的变换知识把一个向量(1, 0, 0)位移(1, 1, 0)个单位(注意,我们把它定义为一个glm::vec4类型的值,齐次坐标设定为1.0):

glm::vec4 vec(1.0f,0.0f,0.0f,1.0f);
// 译注:下面就是矩阵初始化的一个例子,如果使用的是0.9.9及以上版本
// 下面这行代码就需要改为:
// glm::mat4 trans = glm::mat4(1.0f)
// 之后将不再进行提示
glm::mat4 trans = glm::mat4(1.0f);//这里我用的0.9.9版本+;
trans = glm::translate(trans,glm::vec3(1.0f,1.0f,0.0f));
//上面一句代码中的glm::translate函数是一个平移函数,第一个参数是我们要传入的矩阵,第二个参数是平移的方向向量;
vec = trans * vec;
std::cout<<vec.x<<vec.y<<vec.z<<std::endl;

1.我们先用GLM内建的向量类定义一个叫做vec的向量。

2.接下来定义一个mat4类型的trans,默认是一个4×4单位矩阵。

3.下一步是创建一个变换矩阵,我们是把单位矩阵和一个位移向量传递给glm::translate函数来完成这个工作的(然后用给定的矩阵乘以位移矩阵就能获得最后需要的矩阵)。

4.之后我们把向量乘以位移矩阵并且输出最后的结果。如果你仍记得位移矩阵是如何工作的话,得到的向量应该是(1 + 1, 0 + 1, 0 + 0),也就是(2, 1, 0)。这个代码片段将会输出210,所以这个位移矩阵是正确的。

下面做一个旋转90°并缩放0.5倍的操作:

glm::mat4 trans;
trans = glm::rotate(trans,glm::radians(90°),glm::vec3(0.0f,0.0f,1.0f));
//上述操作会创建一个绕z轴旋转90度的矩阵并于与trans相乘;
trans = glm::scale(trans,glm::vec3(0.5f,0.5f,0.5f));

首先,我们把箱子在每个轴都缩放到0.5倍,然后沿z轴旋转90度。GLM希望它的角度是弧度制的(Radian),所以我们使用glm::radians将角度转化为弧度。注意有纹理的那面矩形是在XY平面上的,所以我们需要把它绕着z轴旋转。因为我们把这个矩阵传递给了GLM的每个函数,GLM会自动将矩阵相乘,返回的结果是一个包括了多个变换的变换矩阵。

下一个大问题是:如何把矩阵传递给着色器?我们在前面简单提到过GLSL里也有一个mat4类型。所以我们将修改顶点着色器让其接收一个mat4的uniform变量,然后再用矩阵uniform乘以位置向量:

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

out vec2 TexCoord;

uniform mat4 transform;

void main()
{
    gl_Position = transform * vec4(aPos, 1.0f);
    TexCoord = vec2(aTexCoord.x, 1.0 - aTexCoord.y);
}

在把位置向量传给gl_Position之前,我们先添加一个uniform,并且将其与变换矩阵相乘。我们的图片现在应该是原来的二分之一大小并(向左)旋转了90度。当然,我们仍需要把变换矩阵传递给着色器:

unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
//首先得到uniform的位置值;
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
//然后将矩阵传给着色器;
glUniformMatrix4fv函数的第一个参数为位置值,第二个参数为传入的矩阵数量,第三个参数为是否转置,第四个参数为实际传入的矩阵,但是要先用glm::value_ptr()函数进行转换,目的是将传入的数据类型转换成OpenGL希望接收的数据类型(因此我们要先用GLM的自带的函数value_ptr来变换这些数据);

我们首先查询uniform变量的地址,然后用有Matrix4fv后缀的glUniform函数把矩阵数据发送给着色器。第一个参数你现在应该很熟悉了,它是uniform的位置值。第二个参数告诉OpenGL我们将要发送多少个矩阵,这里是1。第三个参数询问我们是否希望对我们的矩阵进行转置(Transpose),也就是说交换我们矩阵的行和列。OpenGL开发者通常使用一种内部矩阵布局,叫做列主序(Column-major Ordering)布局。GLM的默认布局就是列主序,所以并不需要转置矩阵,我们填GL_FALSE。最后一个参数是真正的矩阵数据,但是GLM并不是把它们的矩阵储存为OpenGL所希望接受的那种,因此我们要先用GLM的自带的函数value_ptr来变换这些数据。

代码修改部分如下:

//还要指定纹理单元对应的采样器;
    shader.useProgram();
    shader.setInt("Tex1", 0);//采样器1对应纹理单元0;
    shader.setInt("Tex2", 1);
//------------------------以下为修改部分-------------------------------------//
    //创建一个先缩小0.5再旋转90度的变换矩阵:
    glm::mat4 trans = glm::mat4(1.0f);
    glm::vec4 vec(1.0f, 0.0f, 0.0f,1.0f);
    trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
    trans = glm::scale(trans, glm::vec3(0.5f, 0.5f, 0.5f));

    //将矩阵传入uniform中;
    int Location = glGetUniformLocation(shader.ID, "transform");
    glUniformMatrix4fv(Location, 1, GL_FALSE, glm::value_ptr(trans));
//------------------------以上为修改部分-------------------------------------//
    //准备引擎:
    while (!glfwWindowShouldClose(window))
#version 330 core
layout(location = 0) in vec3 aPos;//顶点的位置值,aPos为绑定的每一个顶点;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCrood;

out vec3 OurColor;
out vec2 TexCrood;

uniform mat4 transform;//修改;

void main()
{
    gl_Position = transform*vec4(aPos.x,aPos.y,aPos.z,1.0);
    OurColor = aColor;
    TexCrood = aTexCrood;
}

下面我们再看看可不可以让其随时间旋转:

要记住的是前面的例子中我们可以在任何地方声明变换矩阵,但是现在我们必须在每一次迭代中创建它,从而保证我们能够不断更新旋转角度。这也就意味着我们不得不在每次游戏循环的迭代中重新创建变换矩阵。通常在渲染场景的时候,我们也会有多个需要在每次渲染迭代中都用新值重新创建的变换矩阵;

记住应先进行缩放,旋转,再平移:

    while (!glfwWindowShouldClose(window))
    {
        //准备输入:
        processInput(window);
        //每一次更新缓冲颜色,之后继续画下一帧;
        glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;

        //绑定两个纹理,要先激活没一个纹理单元;
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);//绑定这个纹理到当前激活的纹理单元;
        glActiveTexture(GL_TEXTURE1);//激活纹理单元1;
        glBindTexture(GL_TEXTURE_2D, texture2);
        //启用Shader对象shader的程序启动函数;
//-------------------------------更新代码位置------------------------------------//
        //创建一个随时间旋转的矩阵:
        shader.useProgram();//启用着色器程序;
        glm::mat4 trans = glm::mat4(1.0f);
        trans = glm::translate(trans, glm::vec3(0.1f, -0.1f, 0.0f));
        trans = glm::rotate(trans,(float)glfwGetTime(), glm::vec3(0.0f, 0.0f, 1.0f));
        int Location = glGetUniformLocation(shader.ID, "transform");
        glUniformMatrix4fv(Location, 1, GL_FALSE, glm::value_ptr(trans));
//-------------------------------更新代码结束------------------------------------//
        //shader.useProgram();//启用着色器程序;
        glBindVertexArray(VAO);//绑定VAO;
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        //开始绘制;
        //glDrawArrays(GL_TRIANGLES, 0, 3);//第一个参数是要进行绘制的类型,第二个参数制定了顶点数组的开始索引,
        //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        //注意:6是指一个要绘制六个顶点;!!
        //第三个参数是要进行绘制的顶点的长度;

        //更新缓冲;
        glfwSwapBuffers(window);
        //进行检查;
        glfwPollEvents();
    }

我们可以看到一个旋转的图形。

坐标系统

将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(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)的形式结束。下面的这张图展示了整个流程以及各个变换过程做了什么:

  1. 局部坐标是对象相对于局部原点的坐标,也是物体起始的坐标。

  1. 下一步是将局部坐标变换为世界空间坐标,世界空间坐标是处于一个更大的空间范围的。这些坐标相对于世界的全局原点,它们会和其它物体一起相对于世界的原点进行摆放。

  1. 接下来我们将世界坐标变换为观察空间坐标,使得每个坐标都是从摄像机或者说观察者的角度进行观察的。

  1. 坐标到达观察空间之后,我们需要将其投影到裁剪坐标。裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上。

  1. 最后,我们将裁剪坐标变换为屏幕坐标,我们将使用一个叫做视口变换(Viewport Transform)的过程。视口变换将位于-1.0到1.0范围的坐标变换到由glViewport函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。

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

1.局部空间

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

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

2.世界空间

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

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

3.观察空间

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

4.裁剪空间

在一个顶点着色器运行的最后,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的标准化设备坐标,所以被裁剪掉了。

如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则OpenGL会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。

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

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

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

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

4.1正射投影

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

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

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

glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
//前两个参数为平头截体的左右坐标,中间两个为平头截体的底部和顶部,最后两个参数为近远平面;

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

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

4.2透视投影

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

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

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

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

glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

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

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

当你把透视矩阵的 near 值设置太大时(如10.0f),OpenGL会将靠近摄像机的坐标(在0.0f和10.0f之间)都裁剪掉,这会导致一个你在游戏中很熟悉的视觉效果:在太过靠近一个物体的时候你的视线会直接穿过去(穿模)。

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

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

5.组合在一起

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

Vclip=Mprojection⋅Mview⋅Mmodel⋅Vlocal

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

注意:

{【可以参考Games101的课程,这里就是所谓MVP变换,但不同的是此处的P透视投影变换仅仅会将顶点映射到裁剪空间里面,之后的操作->即标准化和视口会有OpenGL来完成】;}

接下来:

顶点着色器的输出要求所有的顶点都在裁剪空间内,这正是我们刚才使用变换矩阵所做的。OpenGL然后对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL会使用glViewPort内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标都关联了一个屏幕上的点(在我们的例子中是一个800x600的屏幕)。这个过程称为视口变换。

6.进入3D

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

glm::mat4 model;
model = glm::rotate(model,glm::radians(-55.0f),glm::vec3(1.0f,0.0f,0.0f));
//一个模型变换矩阵,它会以x轴为中心进行旋转;

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

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

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

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

【摄像机朝向-z】;

就目前来说,观察矩阵是这样的(做一个平移变换):

glm::mat4 view;
view = glm::translate(view,glm::vec3(0.0f,0.0f,-3.0f));
//这里运用了一个平移变换,将观察空间往+Z轴移动3.0f个单位;
//摄像机往后相当于物体往前移动(这里指摄像机往+Z方向移动相当于物体往-Z移动!!);

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

glm::mat4 projection;
projection = glm::prospective(glm::radians(45.0f),screenwidth/screenheight,0.1f,100.0f);

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

#version 330 core
layout(location = 0) in vec3 aPos;//顶点的位置值,aPos为绑定的每一个顶点;
layout(location = 1) in vec3 aColor;
layout(location = 2) in vec2 aTexCrood;

out vec3 OurColor;
out vec2 TexCrood;


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

void main()
{
    gl_Position = projection * view * model * vec4(aPos.x,aPos.y,aPos.z,1.0);
    OurColor = aColor;
    TexCrood = aTexCrood;
}

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

//创建一个随时间旋转的矩阵:
shader.useProgram();//启用着色器程序;
//创建三个变换矩阵;
glm::mat4 projection = glm::mat4(1.0f);
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
projection = glm::perspective(glm::radians(45.0f), (float)800/ (float)600, 0.1f, 100.0f);

//注意这一段要放在渲染循环中,同时不要忘了在前面先激活shader.useProgram()着色器程序!!!;

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

  • 稍微向后倾斜至地板方向。

  • 离我们有一些距离。

  • 有透视效果(顶点越远,变得越小)。

main.cpp完整代码

#include <glad/glad.h> 
#include <GLFW/glfw3.h>
#include <iostream>
#include<E:\OpenGl\练习1.1\3.3.shader_class\shader s.h>
//以下三行为glm的头文件代码;
#include <E:\OpenGl\glm\glm-master\glm\glm.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\matrix_transform.hpp>
#include <E:\OpenGl\glm\glm-master\glm\gtc\type_ptr.hpp>

#define STB_IMAGE_IMPLEMENTATION
#include <E:/OpenGl/stb_image.h/stb-master/stb_image.h>//这两行代码加入了stb_image库;

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

int main()
{

    //先进行初始化glfw;
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本设置为3;
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本设置为3;
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    GLFWwindow* window = glfwCreateWindow(800, 600, "MY OPENGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Fail to create a window" << std::endl;
        glfwTerminate();//释放资源;
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    //创建完告诉将上下文设置为进程上下文;

    //对glad进行初始化;
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Fail to initnite glad" << std::endl;
        return -1;
    }
    //引入着色器类,着色器被封装到了class Shader里面;
    Shader shader("3.3.shader.vs", "3.3.shader.fs");

    float vertices[] = {
        //     ---- 位置 ----       ---- 颜色 ----     - 纹理坐标 -
             0.5f, 0.5f, 0.0f,   1.0f, 0.0f, 0.0f,   1.0f, 1.0f,   // 右上
             0.5f, -0.5f, 0.0f,   0.0f, 1.0f, 0.0f,   1.0f, 0.0f,   // 右下
            -0.5f, -0.5f, 0.0f,   0.0f, 0.0f, 1.0f,   0.0f, 0.0f,   // 左下
            -0.5f,  0.5f, 0.0f,   1.0f, 1.0f, 0.0f,   0.0f, 1.0f    // 左上
    };
    unsigned int indices[] =
    {
        0,1,3,
        1,2,3
    };

    unsigned int VAO, VBO, EBO;
    glGenVertexArrays(1, &VAO);//创建VAO;
    glGenBuffers(1, &VBO);//创建VBO;
    glGenBuffers(1, &EBO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);


    //链接顶点属性;
    //位置属性;
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);//这里的步长为获得下一个属性值,应该右移六个长度的单位;
    glEnableVertexAttribArray(0);
    //颜色属性;
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);//启用颜色属性;
    //纹理属性;
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
    glEnableVertexAttribArray(2);//启用纹理属性;

    //加入纹理1:
    unsigned int texture1;
    glGenTextures(1, &texture1);
    glBindTexture(GL_TEXTURE_2D, texture1);
    //修改纹理的环绕和过滤方式;
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    //加载入纹理:
    stbi_set_flip_vertically_on_load(true);//防止上下颠倒;
    int width, height, nrchannels;
    unsigned char* data = stbi_load("E:/OpenGl/textures/v2-6e8b14becd4699a1e02421670e25ec74_r.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 << "Fail to load a image" << std::endl;
    }
    stbi_image_free(data);

    //加入纹理2:
    unsigned int texture2;
    glGenTextures(1, &texture2);
    glBindTexture(GL_TEXTURE_2D, texture2);
    //修改纹理的环绕和过滤方式;
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    //加载入纹理:
    stbi_set_flip_vertically_on_load(true);//防止上下颠倒;
    int width1, height1, nrchannels1;
    unsigned char* data2 = stbi_load("E:/OpenGl/textures/2e3c98d3bf204d029be74443398e0c87.jpeg", &width1, &height1, &nrchannels1, 0);
    //生成纹理;
    if (data2)
    {
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width1, height1, 0, GL_RGB, GL_UNSIGNED_BYTE, data2);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Fail to load a image" << std::endl;
    }
    stbi_image_free(data2);

    //还要指定纹理单元对应的采样器;
    shader.useProgram();
    shader.setInt("Tex1", 0);//采样器1对应纹理单元0;
    shader.setInt("Tex2", 1);


    //准备引擎:
    while (!glfwWindowShouldClose(window))
    {
        //准备输入:
        processInput(window);
        //每一次更新缓冲颜色,之后继续画下一帧;
        glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;

        //绑定两个纹理,要先激活没一个纹理单元;
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);//绑定这个纹理到当前激活的纹理单元;
        glActiveTexture(GL_TEXTURE1);//激活纹理单元1;
        glBindTexture(GL_TEXTURE_2D, texture2);
        //启用Shader对象shader的程序启动函数;

        
        //创建一个随时间旋转的矩阵:
        shader.useProgram();//启用着色器程序;
        //创建三个变换矩阵;
        glm::mat4 projection = glm::mat4(1.0f);
        glm::mat4 view = glm::mat4(1.0f);
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
        view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
        projection = glm::perspective(glm::radians(45.0f), (float)800/ (float)600, 0.1f, 100.0f);

        //将变换矩阵传入着色器;注意到由于变换随时发生,要每一次迭代都进行,因此要放在里面;
        int Location1 = glGetUniformLocation(shader.ID, "model");
        glUniformMatrix4fv(Location1, 1, GL_FALSE, glm::value_ptr(model));
        int Location2 = glGetUniformLocation(shader.ID, "view");
        glUniformMatrix4fv(Location2, 1, GL_FALSE, glm::value_ptr(view));
        int Location3 = glGetUniformLocation(shader.ID, "projection");
        glUniformMatrix4fv(Location3, 1, GL_FALSE, glm::value_ptr(projection));


        glBindVertexArray(VAO);//绑定VAO;
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        //开始绘制;
        //glDrawArrays(GL_TRIANGLES, 0, 3);//第一个参数是要进行绘制的类型,第二个参数制定了顶点数组的开始索引,
        //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        //注意:6是指一个要绘制六个顶点;!!
        //第三个参数是要进行绘制的顶点的长度;

        //更新缓冲;
        glfwSwapBuffers(window);
        //进行检查;
        glfwPollEvents();
    }
    //结束要删除VAO,VBO,以及删除着色器程序; 
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    //glDeleteProgram(shaderProgram);//删除着色器程序;
    //结束清楚资源;
    glfwTerminate();
    return 0;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//如果按下的键为回车键;
        glfwSetWindowShouldClose(window, true);
}

7.更加3D

【可以得到一个旋转的六面体,但是会穿模.....;

这是因为没有进行深度测试.】

【这的确有点像是一个立方体,但又有种说不出的奇怪。立方体的某些本应被遮挡住的面被绘制在了这个立方体其他面之上。之所以这样是因为OpenGL是一个三角形一个三角形地来绘制你的立方体的,所以即便之前那里有东西它也会覆盖之前的像素。因为这个原因,有些三角形会被绘制在其它三角形上面,虽然它们本不应该是被覆盖的。

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

Z缓冲

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

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

glEnable(GL_DEPTH_TEST);

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

glClear(DEPTH_BUFFER_BIT);

进行深度缓存之后,我们就可以得到一个开启了深度缓存,并且各个面都为纹理的一个旋转正方体!!;

更多的立方体

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

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

glm::vec3 cubePositions[] = {
  glm::vec3( 0.0f,  0.0f,  0.0f), 
  glm::vec3( 2.0f,  5.0f, -15.0f), 
  glm::vec3(-1.5f, -2.2f, -2.5f),  
  glm::vec3(-3.8f, -2.0f, -12.3f),  
  glm::vec3( 2.4f, -0.4f, -3.5f),  
  glm::vec3(-1.7f,  3.0f, -7.5f),  
  glm::vec3( 1.3f, -2.0f, -2.5f),  
  glm::vec3( 1.5f,  2.0f, -2.5f), 
  glm::vec3( 1.5f,  0.2f, -1.5f), 
  glm::vec3(-1.3f,  1.0f, -1.5f)  
};

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

//准备引擎:
    while (!glfwWindowShouldClose(window))
    {
        //准备输入:
        processInput(window);
        //每一次更新缓冲颜色,之后继续画下一帧;
        glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;
        glClear(GL_DEPTH_BUFFER_BIT);

        //绑定两个纹理,要先激活没一个纹理单元;
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture1);//绑定这个纹理到当前激活的纹理单元;
        glActiveTexture(GL_TEXTURE1);//激活纹理单元1;
        glBindTexture(GL_TEXTURE_2D, texture2);

        glBindVertexArray(VAO);//绑定VAO;
        //进行十次循环;
        for (int i = 0; i < 10; i++)
        {
            //创建一个随时间旋转的矩阵:
            shader.useProgram();//启用着色器程序;
            //创建三个变换矩阵;
            //先初始化三个单位矩阵;
            glm::mat4 projection = glm::mat4(1.0f);
            glm::mat4 view = glm::mat4(1.0f);
            glm::mat4 model = glm::mat4(1.0f);
            model = glm::translate(model, cubePositions[i]);
            float angle = 20.0f;
            model = glm::rotate(model, (float)glfwGetTime() * glm::radians(angle*(i+1)), glm::vec3(0.4f, 0.2f, 0.0f));
            view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
            projection = glm::perspective(glm::radians(45.0f), (float)800 / (float)600, 0.1f, 100.0f);

            //将变换矩阵传入着色器;注意到由于变换随时发生,要每一次迭代都进行,因此要放在里面;
            int Location1 = glGetUniformLocation(shader.ID, "model");
            glUniformMatrix4fv(Location1, 1, GL_FALSE, glm::value_ptr(model));
            int Location2 = glGetUniformLocation(shader.ID, "view");
            glUniformMatrix4fv(Location2, 1, GL_FALSE, glm::value_ptr(view));
            int Location3 = glGetUniformLocation(shader.ID, "projection");
            glUniformMatrix4fv(Location3, 1, GL_FALSE, glm::value_ptr(projection));
            glDrawArrays(GL_TRIANGLES, 0, 36);
        }

        //开始绘制;
        //glDrawArrays(GL_TRIANGLES, 0, 3);//第一个参数是要进行绘制的类型,第二个参数制定了顶点数组的开始索引,
        //glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        //注意:6是指一个要绘制六个顶点;!!
        //第三个参数是要进行绘制的顶点的长度;

        //更新缓冲;
        glfwSwapBuffers(window);
        //进行检查;
        glfwPollEvents();
    }

结果会得到是个十个不同方向速度进行旋转的六边形:

完美!!!!,那就结束啦!!

おすすめ

転載: blog.csdn.net/2201_75303014/article/details/128800473