1.7.1 坐标系统

一、坐标系统

1、概念

顶点坐标起始于局部空间(Local Space),在这里称它为局部坐标(Local Coordinate),它在之后会变为世界坐标(World Coordinate)、观察坐标(View Coordinate)、裁剪坐标(Clip Coordinate),最后以屏幕坐标(Screen Coordinate)的形式结束。下面这张图展示了整个流程以及各个变换过程做了什么:

之所以将顶点变换到各个不同的空间是因为有些操作在特定的坐标系统中才有意义且更方便。例如,当需要对物体本身进行修改时,应该在局部空间中进行操作;如果要一个物体做出一个相对于其他物体位置的操作时,应该在世界坐标中进行操作。也可以定义一个直接从局部空间变换到裁剪空间的变换矩阵,但是那样会失去很多灵活性。

2、局部空间

局部空间是指物体自身的坐标空间,即物体相对于自己的坐标系(只有它自己)。在局部空间中,物体的位置、旋转和缩放是相对于自身的原点和轴进行定义的。

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

3、世界空间

世界空间是指全局坐标空间,即整个场景的坐标系(包含要渲染的所有物体)。在世界坐标系中,物体的位置、旋转和缩放都是相对于整个场景的原点和轴进行定义的。

物体从局部空间变换到世界空间,该变换是由模型矩阵(Model Matrix)实现的;模型矩阵是一种变换矩阵,它通过对物体进行位移、缩放、旋转将物体从局部坐标放置到世界坐标中。

4、观察空间

观察空间经常被人们称为OpenGL的摄像机(Camera)(有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界坐标转化为用户视野(摄像机)前方的坐标而产生的结果,相当于从摄像机的视角观察到的空间。

物体从世界空间变换到观察空间,该变换是由观察矩阵(View Matrix)实现的。观察矩阵也是一种变换矩阵,通过平移/旋转场景使得特定对象变换到摄像机的前方。

5、裁剪空间

在一个顶点着色器运行的最后,OpenGL期望所有的坐标都落在指定的范围之内,且任何不在这个范围之内的点都应该被裁剪掉,这就是裁剪空间名字的由来。

为了将顶点坐标从观察空间变换到裁剪空间,需要定义一个投影矩阵(Projection Matrix),它指定了一个范围的坐标,比如在每个维度上的-1000到1000,投影矩阵会将在这个范围内的坐标变换为标准化设备坐标的范围(-1.0, 1.0),其他在-1000到1000范围之外的点会被裁剪掉。

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

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

将观察坐标变换为裁剪坐标的投影矩阵有两种形式:正射投影矩阵和透视投影矩阵。

6、正射投影

正射投影矩阵(Orthographic Projection Matrix)定义一个类似立方体的平截头箱,它定义一个裁剪空间,在这个空间之外的顶点都会被裁剪掉。创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度。它的平截头体看起来像一个容器:

上面的平截头体定义了可见的坐标,它是由宽、高、近平面和远平面所指定。任何出现在近平面之前或远平面之后的坐标都会被裁剪掉。

正射投影矩阵直接将坐标映射到2D平面上,即你的屏幕上;这种投影会产生不真实的结果,因为没有考虑透视。

7、透视投影

在实际生活中,离你越远的东西看起来越小,这个效果叫做透视。透视的效果在我们看见一条无限长的高速公路或铁路时尤其明显,如下图所示:

由于透视,这两条平行的直线在很远的地方看起来会相交,这正是透视投影想要模仿的效果,使用透视投影矩阵来完成。这个投影矩阵将给定的平截头体范围映射到裁剪空间,除此之外还修改了每个顶点坐标的w值,离观察者越远的顶点坐标w分量越大,被变换到裁剪空间的坐标都会在-w到w的范围之间。一旦坐标在裁剪空间内,透视除法就会被应用到裁剪空间坐标上:

顶点坐标的每个分量都会除以它的w分量,距离观察者越远顶点坐标就会越小,最后的结果坐标就会处于标准化设备空间中的。

一个透视平截头体可以被看作一个不均匀形状的箱子,在这个箱子内部的每个坐标都会被映射到裁剪空间的一个点。下图是一张透视平截头体的图片:

8、把它们组合到一起

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

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

顶点着色器的输出要求所以的顶点都在裁剪空间内,这是刚才使用变换矩阵所做的。OpenGL对裁剪坐标执行透视除法从而将它们变换到标准化设备坐标。OpenGL会使用glViewport内部的参数来将标准化设备坐标映射到屏幕坐标,每个坐标关联屏幕上的一个点,这个过程叫做视口变换。

二、进入3D

更改顶点着色器的代码,创建三个矩阵并使用

#version 330 core

layout (location = 0) in vec3 aPos; //位置变量的属性位置值为0
layout (location = 1) in vec3 aColor; //颜色变量的属性位置值为1
layout (location = 2) in vec2 aTexture; //纹理变量的属性位置值为2

out vec3 ourColor; //向片段着色器输出一个颜色坐标
out vec2 ourTexture; //向片段着色器输出一个纹理坐标

uniform mat4 model; //模型矩阵
uniform mat4 view; //观察矩阵
uniform mat4 projection; //投影矩阵

void main()
{
    //乘法从右往左读
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    ourColor = aColor;
    ourTexture = aTexture;
}

更改paintGL()函数代码,创建三个矩阵,变换之后把值传给顶点着色器

void MyOpenGLWidget::paintGL()
{
    //设置墨绿色背景
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
    glClear(GL_COLOR_BUFFER_BIT); //状态使用

    //模型矩阵
    QMatrix4x4 model; //创建单位矩阵
    model.rotate(-55.0f, 1.0f, 0.0f, 0.0f); //绕x轴(1.0f, 0.0f, 0.0f))旋转 -55度
    m_shaderProgram.setUniformValue("model", model); //传给顶点着色器

    //观察矩阵
    QMatrix4x4 view; //创建单位矩阵
    view.translate(0.0f, 0.0f, -3.0f); //摄像机的位置是(0.0f, 0.0f, 0.0f),设置物体在摄像机z轴的3.0f处
    m_shaderProgram.setUniformValue("view", view);

    //投影矩阵
    QMatrix4x4 projection; //创建单位矩阵
    projection.perspective(45.0f, (float)width()/height(), 0.1f, 100.0f); //透视投影
    m_shaderProgram.setUniformValue("projection", projection);

    //绘制
    m_shaderProgram.bind(); //激活程序对象
    glBindVertexArray(VAO); //绑定VAO
    m_textureWall->bind(0); //绑定激活纹理单元0
    m_textureSmile->bind(1); //绑定激活纹理单元1
    m_textureSmall->bind(2); //绑定激活纹理单元2
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, NULL); //绘图
}

perspective函数中的第一个参数定义了fov的值,它表示的是视野,是观察空间的大小。如果想要一个真实的观察效果,它的值通常被设置为45.0f,如果想要末日风格可以将其设置为一个更大的值。第二个参数设置了宽高比,由视口的宽(width())除以高(height())所得。第三和第四个参数设置了平截头体的近和远平面,通常设置近平面距离为0.1f,远平面距离为100.0f。

运行结果如下,看起来像3D画面:

三、更加3D

更改顶点着色器代码如下,删除掉有关color的变量,并把aTexture的location设置为1

#version 330 core

layout (location = 0) in vec3 aPos; //位置变量的属性位置值为0
layout (location = 1) in vec2 aTexture; //纹理变量的属性位置值为1

out vec2 ourTexture; //向片段着色器输出一个纹理坐标

uniform mat4 model; //模型矩阵
uniform mat4 view; //观察矩阵
uniform mat4 projection; //投影矩阵

void main()
{
    //乘法从右往左读
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    ourTexture = aTexture;
}

更改片段着色器代码,删除掉有关color的变量

#version 330 core

out vec4 FragColor;

in vec2 ourTexture; //纹理坐标

uniform sampler2D textureWall; //第0个采样器
uniform sampler2D textureSmile; //第1个采样器
uniform sampler2D textureSmall; //第2个采样器

uniform float mixValue;

void main()
{
    FragColor = mix(texture(textureWall, ourTexture),
                   texture(textureSmile, vec2(-ourTexture.x, ourTexture.y)), mixValue);
}

更改myopenglwidget.cpp代码如下:

#include "myopenglwidget.h"
#include <QDebug>
#include <QKeyEvent>
#include <QTime>

unsigned int VBO; //顶点缓冲对象
unsigned int VAO; //顶点数组对象
unsigned int EBO; //元素缓冲对象

MyOpenGLWidget::MyOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{
    setFocusPolicy(Qt::StrongFocus);

    connect(&m_timer, &QTimer::timeout, this, &MyOpenGLWidget::onTimeout);
    m_timer.start(100); //100ms
}

MyOpenGLWidget::~MyOpenGLWidget()
{
    if(m_timer.isActive())
        m_timer.stop();

    makeCurrent();
    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &EBO);
    doneCurrent();
}

void MyOpenGLWidget::onTimeout()
{
    update();
}

void MyOpenGLWidget::initializeGL()
{
    //初始化OpenGL函数
    initializeOpenGLFunctions();

    //创建VBO,并赋予ID
    glGenBuffers(1, &VBO);
    //绑定VBO对象
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //顶点数据
    float 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, 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,

        -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
    };
    //把顶点数据复制到显存中
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //创建VAO对象,并赋予ID
    glGenVertexArrays(1, &VAO);
    //绑定VAO对象
    glBindVertexArray(VAO);

    //创建EBO对象,并赋予ID
    glGenBuffers(1, &EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    unsigned int indices[] = {
                               0, 1, 3, //第一个三角形
                               1, 2, 3 //第二个三角形
                             };
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    //位置属性
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值

    //纹理属性
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1); //开启VAO管理的第三个属性值

    //解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    //解绑VAO
    glBindVertexArray(0);

    //创建一个程序对象
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Vertex,":/shaders/shapes.vert");
    m_shaderProgram.addShaderFromSourceFile(QOpenGLShader::Fragment,":/shaders/shapes.frag");
    bool success = m_shaderProgram.link();
    if(!success)
        qDebug()<<"ERR:" << m_shaderProgram.log();

    m_shaderProgram.bind();
    m_shaderProgram.setUniformValue("vertexColor", 0.0, 1.0, 0.0, 1.0);
    
    m_textureWall = new QOpenGLTexture(QImage(":/images/wall.jpg").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureWall", 0); //把纹理单元传给片段着色器中的采样器

    m_textureSmile = new QOpenGLTexture(QImage(":/images/awesomeface.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmile", 1); //把纹理单元传给片段着色器中的采样器

    m_textureSmall = new QOpenGLTexture(QImage(":/images/small.png").mirrored()); //mirrored消除镜像
    m_shaderProgram.setUniformValue("textureSmall", 2); //把纹理单元传给片段着色器中的采样器

    m_shaderProgram.setUniformValue("mixValue", mixValue);
}

void MyOpenGLWidget::resizeGL(int w, int h)
{
    Q_UNUSED(w);

    Q_UNUSED(h);
    //glViewport(0, 0, w, h);
}

void MyOpenGLWidget::paintGL()
{
    //设置墨绿色背景
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
    glClear(GL_COLOR_BUFFER_BIT); //状态使用

    unsigned int time = QTime::currentTime().msec();
    //模型矩阵
    QMatrix4x4 model; //创建单位矩阵
    model.rotate(time, 1.0f, 0.0f, 0.0f); //绕x轴旋转
    m_shaderProgram.setUniformValue("model", model); //传给顶点着色器

    //观察矩阵
    QMatrix4x4 view; //创建单位矩阵
    view.translate(0.0f, 0.0f, -3.0f); //移动
    m_shaderProgram.setUniformValue("view", view);

    //投影矩阵
    QMatrix4x4 projection; //创建单位矩阵
    projection.perspective(45.0f, (float)width()/height(), 0.1f, 100.0f); //透视投影
    m_shaderProgram.setUniformValue("projection", projection);

    //绘制
    m_shaderProgram.bind(); //激活程序对象
    glBindVertexArray(VAO); //绑定VAO
    m_textureWall->bind(0); //绑定激活纹理单元0
    m_textureSmile->bind(1); //绑定激活纹理单元1
    m_textureSmall->bind(2); //绑定激活纹理单元2
    glDrawArrays(GL_TRIANGLES, 0, 36); //绘图
}

void MyOpenGLWidget::keyPressEvent(QKeyEvent *event)
{
    if(event->key() == Qt::Key_Up)
        mixValue+=0.1;
    else if(event->key() == Qt::Key_Down)
        mixValue-=0.1;
    else
        return;

    if(mixValue > 1.0)
        mixValue = 1.0;

    if(mixValue < 0.0)
        mixValue = 0.0;

    makeCurrent();
    m_shaderProgram.setUniformValue("mixValue", mixValue);
    doneCurrent();
    update();

    QOpenGLWidget::keyPressEvent(event);
}

更改顶点数据vertices,使每行数据存储位置和纹理坐标,共存储6个矩形的数据(一个箱子的六个面)

删除掉颜色属性,更改位置属性和纹理属性的参数

//位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); //开启VAO管理的第一个属性值

//纹理属性
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); //开启VAO管理的第三个属性值

更改模型矩阵数据,让立方体随时间旋转

unsigned int time = QTime::currentTime().msec();
//模型矩阵
QMatrix4x4 model; //创建单位向量
model.rotate(time, 1.0f, 0.0f, 0.0f); //绕x轴旋转
m_shaderProgram.setUniformValue("model", model); //传给顶点着色器

运行结果如下:

看起来像是立方体,但是某些应该被遮挡的面被绘制在这个立方体的其他面上,因为OpenGL是通过绘制一个个的三角形来绘制出立方体,所以即使之前哪里有东西它也会覆盖之前的像素。OpenGL存储深度信息在一个叫做Z缓冲的缓冲中,它允许OpenGL决定何时覆盖一个像素而何时不覆盖。通过使用Z缓冲,可以配置OpenGL来进行深度测试,,值越大越容易被覆盖。

更改paintGL()函数中开头的几行代码如下:

glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //设置墨绿色背景
glEnable(GL_DEPTH_TEST); //打开深度缓冲区
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清空

再运行程序,查看效果:

四、更多立方体

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

首先,为每个立方体定义一个位移向量来指定它在世界空间的位置:

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

在paintGL()函数中调用glDrawArrays()函数10次,但是每次传入不同的模型矩阵到顶点着色器中:

void MyOpenGLWidget::paintGL()
{
    //设置墨绿色背景
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glEnable(GL_DEPTH_TEST);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    unsigned int time = QTime::currentTime().msec();

    //观察矩阵
    QMatrix4x4 view; //创建单位矩阵
    view.translate(0.0f, 0.0f, -3.0f); //移动
    m_shaderProgram.setUniformValue("view", view);

    //投影矩阵
    QMatrix4x4 projection; //创建单位矩阵
    projection.perspective(45.0f, (float)width()/height(), 0.1f, 100.0f); //透视投影
    m_shaderProgram.setUniformValue("projection", projection);

    //绘制
    m_shaderProgram.bind(); //激活程序对象
    glBindVertexArray(VAO); //绑定VAO
    m_textureWall->bind(0); //绑定激活纹理单元0
    m_textureSmile->bind(1); //绑定激活纹理单元1
    m_textureSmall->bind(2); //绑定激活纹理单元2

    foreach(auto item, cubePositions)
    {
        //模型矩阵
        QMatrix4x4 model; //创建单位矩阵
        model.translate(item); //移动
        model.rotate(time, 1.0f, 1.f, 0.0f); //绕向量(1.0f, 1.f, 0.0f)旋转
        m_shaderProgram.setUniformValue("model", model); //传给顶点着色器

        glDrawArrays(GL_TRIANGLES, 0, 36); //绘图
    }
}

运行结果如下:

 

注:观看OpenGL中文官网(https://learnopengl-cn.github.io/)和阿西拜的现代OpenGL入门(https://ke.qq.com/course/3999604#term_id=104150693)学习OpenGL

  • 26
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值