(本文是LearnOpenGL的学习笔记, 教程中文翻译地址https://learnopengl-cn.github.io/(备用地址https://learnopengl-cn.readthedocs.io/zh/latest/),写于 2020-2-1 ,并在 2021-8-15 进行了更新)
0.前言
上一篇笔记记录了利用矩阵变换来对顶点数据进行变换(Transform):https://blog.csdn.net/gongjianbo1992/article/details/104121687,本文将学习OpenGL坐标系统。
OpenGL希望在每次顶点着色器运行后,我们可见的所有顶点都为标准化设备坐标(Normalized Device Coordinate, NDC)。也就是说,每个顶点的x,y,z坐标都应该在-1.0到1.0之间,超出这个坐标范围的顶点都将不可见。我们通常会自己设定一个坐标的范围,之后再在顶点着色器中将这些坐标变换为标准化设备坐标。然后将这些标准化设备坐标传入光栅器(Rasterizer),将它们变换为屏幕上的二维坐标或像素。
将坐标变换为标准化设备坐标,接着再转化为屏幕坐标的过程通常是分步进行的,也就是类似于流水线那样子。在流水线中,物体的顶点在最终转化为屏幕坐标之前还会被变换到多个坐标系统(Coordinate System)。将物体的坐标变换到几个过渡坐标系(Intermediate Coordinate System)的优点在于,在这些特定的坐标系统中,一些操作或运算更加方便和容易,这一点很快就会变得很明显。对我们来说比较重要的总共有5个不同的坐标系统:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
1.如何实现
这里面主要的几个知识点:变换的流程、透视投影、右手坐标系、Z缓冲。
为了将坐标从一个坐标系变换到另一个坐标系,我们需要用到几个变换矩阵,最重要的几个分别是模型(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函数所定义的坐标范围内。最后变换出来的坐标将会送到光栅器,将其转化为片段。
个人理解:model矩阵把单个物体的坐标映射到整体坐标系里,view矩阵从一个视点再来转换,最后perspective透视矩阵截取一个可见范围的空间。
对于透视投影,可以参考网上的资料,一般可以使用工具类,在Qt中,view和perspective矩阵可以这样设置:
QMatrix4x4 view; //观察矩阵,后退一点
view.translate(QVector3D(0.0f, 0.0f, -3.0f));
_shaderProgram.setUniformValue("view", view);
QMatrix4x4 projection; //透视投影
//坐标到达观察空间之后,我们需要将其投影到裁剪坐标。
//裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上
//参数1:指定视景体的视野的角度
//参数2:指定你的视景体的宽高比
//参数3:指定观察者到视景体的最近的裁剪面的距离(正数)
//参数4:指定观察者到视景体最远的裁剪面距离(正数)
projection.perspective(45.0f, 1.0f * width() / height(), 0.1f, 100.0f);
_shaderProgram.setUniformValue("projection", projection);
上面代码view观察矩阵把场景向(-)移动了3 ,相当于我们视角摄像机向(+)后退了3,这样才能看到整个场景,移动的方向复合OpenGL的右手坐标系,简单来说,就是正x轴在你的右手边,正y轴朝上,而正z轴是朝向后方的。想象你的屏幕处于三个轴的中心,则正z轴穿过你的屏幕朝向你。坐标系画起来如下:
OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer)。现在我们想启用深度测试(默认关闭),需要开启GL_DEPTH_TEST,并且每次渲染迭代之前清除深度缓冲(否则前一帧的深度信息仍然保存在缓冲中):
glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
2.实现代码
(项目git链接:https://github.com/gongjianbo/OpenGLwithQtWidgets.git)
我的GLCoordinate类实现效果:
GLCoordinate类代码:
#pragma once
#include <QOpenGLWidget>
#include <QOpenGLFunctions_3_3_Core>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLTexture>
#include <QTimer>
//坐标系统
//QOpenGLWidget窗口上下文
//QOpenGLFunctions访问OpenGL接口,可以不继承作为成员变量使用
class GLCoordinate
: public QOpenGLWidget
, protected QOpenGLFunctions_3_3_Core
{
public:
explicit GLCoordinate(QWidget *parent = nullptr);
~GLCoordinate();
protected:
//【】继承QOpenGLWidget后重写这三个虚函数
//设置OpenGL资源和状态。在第一次调用resizeGL或paintGL之前被调用一次
void initializeGL() override;
//渲染OpenGL场景,每当需要更新小部件时使用
void paintGL() override;
//设置OpenGL视口、投影等,每当尺寸大小改变时调用
void resizeGL(int width, int height) override;
private:
//着色器程序
QOpenGLShaderProgram shaderProgram;
//顶点数组对象
QOpenGLVertexArrayObject vao;
//顶点缓冲
QOpenGLBuffer vbo;
//纹理(因为不能赋值,所以只能声明为指针)
QOpenGLTexture *texture1{ nullptr };
QOpenGLTexture *texture2{ nullptr };
//旋转
QTimer timer;
int rotate{ 0 };
};
#include "GLCoordinate.h"
#include <QMatrix4x4>
#include <QDebug>
GLCoordinate::GLCoordinate(QWidget *parent)
: QOpenGLWidget(parent)
{
connect(&timer,&QTimer::timeout,this,[this](){
rotate+=2;
if(isVisible()){
update();
}
});
timer.setInterval(50);
}
GLCoordinate::~GLCoordinate()
{
//initializeGL在显示时才调用,释放未初始化的会异常
if(!isValid())
return;
//QOpenGLWidget
//三个虚函数不需要makeCurrent,对应的操作已由框架完成
//但是释放时需要设置当前上下文
makeCurrent();
vbo.destroy();
vao.destroy();
delete texture1;
delete texture2;
doneCurrent();
}
void GLCoordinate::initializeGL()
{
//为当前上下文初始化OpenGL函数解析
initializeOpenGLFunctions();
//着色器代码
//in输入,out输出,uniform从cpu向gpu发送
//因为OpenGL纹理颠倒过来的,所以取反vec2(aTexCoord.x, 1-aTexCoord.y);
const char *vertex_str=R"(#version 330 core
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inTexCoord;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
out vec2 texCoord;
void main()
{
gl_Position = projection * view * model * vec4(inPos, 1.0);
texCoord = vec2(inTexCoord.x, 1-inTexCoord.y);
})";
const char *fragment_str=R"(#version 330 core
in vec2 texCoord;
uniform sampler2D texture1;
uniform sampler2D texture2;
out vec4 fragColor;
void main()
{
fragColor = mix(texture(texture1, texCoord), texture(texture2, texCoord), 0.2);
})";
//将source编译为指定类型的着色器,并添加到此着色器程序
if(!shaderProgram.addCacheableShaderFromSourceCode(
QOpenGLShader::Vertex,vertex_str)){
qDebug()<<"compiler vertex error"<<shaderProgram.log();
}
if(!shaderProgram.addCacheableShaderFromSourceCode(
QOpenGLShader::Fragment,fragment_str)){
qDebug()<<"compiler fragment error"<<shaderProgram.log();
}
//使用addShader()将添加到该程序的着色器链接在一起。
if(!shaderProgram.link()){
qDebug()<<"link shaderprogram error"<<shaderProgram.log();
}
//顶点数据(盒子六个面,一个面两个三角)
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
};
vao.create();
vao.bind();
//QOpenGLVertexArrayObject::Binder vaoBind(&vao);
vbo=QOpenGLBuffer(QOpenGLBuffer::VertexBuffer);
vbo.create();
vbo.bind();
vbo.allocate(vertices,sizeof(vertices));
//参数1对应layout
// position attribute
shaderProgram.setAttributeBuffer(0, GL_FLOAT, 0, 3, sizeof(GLfloat) * 5);
shaderProgram.enableAttributeArray(0);
// texture coord attribute
shaderProgram.setAttributeBuffer(1, GL_FLOAT, sizeof(GLfloat) * 3, 2, sizeof(GLfloat) * 5);
shaderProgram.enableAttributeArray(1);
// texture 1
//直接生成绑定一个2d纹理, 并生成多级纹理MipMaps
texture1 = new QOpenGLTexture(QImage(":/container.jpg"), QOpenGLTexture::GenerateMipMaps);
if(!texture1->isCreated()){
qDebug() << "Failed to load texture";
}
// set the texture wrapping parameters
// 等于glTexParameteri(GLtexture_2D, GLtexture_WRAP_S, GL_REPEAT);
texture1->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
texture1->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);
// set texture filtering parameters
//等价于glTexParameteri(GLtexture_2D, GLtexture_MIN_FILTER, GL_LINEAR);
texture1->setMinificationFilter(QOpenGLTexture::Linear);
texture1->setMagnificationFilter(QOpenGLTexture::Linear);
// texture 2
//直接生成绑定一个2d纹理, 并生成多级纹理MipMaps
texture2 = new QOpenGLTexture(QImage(":/awesomeface.png"), QOpenGLTexture::GenerateMipMaps);
if(!texture2->isCreated()){
qDebug() << "Failed to load texture";
}
// set the texture wrapping parameters
// 等于glTexParameteri(GLtexture_2D, GLtexture_WRAP_S, GL_REPEAT);
texture2->setWrapMode(QOpenGLTexture::DirectionS, QOpenGLTexture::Repeat);
texture2->setWrapMode(QOpenGLTexture::DirectionT, QOpenGLTexture::Repeat);//
// set texture filtering parameters
//等价于glTexParameteri(GLtexture_2D, GLtexture_MIN_FILTER, GL_LINEAR);
texture2->setMinificationFilter(QOpenGLTexture::Linear);
texture2->setMagnificationFilter(QOpenGLTexture::Linear);
shaderProgram.bind();
shaderProgram.setUniformValue("texture1", 0);
shaderProgram.setUniformValue("texture2", 1);
QMatrix4x4 view; //观察矩阵,后退一点
view.translate(QVector3D(0.0f, 0.0f, -3.0f));
shaderProgram.setUniformValue("view", view);
QMatrix4x4 projection; //透视投影
//坐标到达观察空间之后,我们需要将其投影到裁剪坐标。
//裁剪坐标会被处理至-1.0到1.0的范围内,并判断哪些顶点将会出现在屏幕上
//参数1:指定视景体的视野的角度
//参数2:指定你的视景体的宽高比
//参数3:指定观察者到视景体的最近的裁剪面的距离(正数)
//参数4:指定观察者到视景体最远的裁剪面距离(正数)
projection.perspective(45.0f, 1.0f * width() / height(), 0.1f, 100.0f);
shaderProgram.setUniformValue("projection", projection);
shaderProgram.release();
timer.start();
}
//绘制多个盒子
static 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)
};
void GLCoordinate::paintGL()
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
//因为我们使用了深度测试,我们也想要在每次渲染迭代之前清除深度缓冲
//(否则前一帧的深度信息仍然保存在缓冲中)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//OpenGL存储它的所有深度信息于一个Z缓冲(Z-buffer)中,也被称为深度缓冲(Depth Buffer)。
//深度值存储在每个片段里面(作为片段的z值),当片段想要输出它的颜色时,OpenGL会将它的深度值和z缓冲进行比较,
//如果当前的片段在其它片段之后,它将会被丢弃,否则将会覆盖。
//这个过程称为深度测试(Depth Testing),它是由OpenGL自动完成的。
//(不开启深度缓冲的话,盒子的纹理堆叠顺序就是乱的)
glEnable(GL_DEPTH_TEST); //默认关闭的
//纹理
glActiveTexture(GL_TEXTURE0);
texture1->bind();
glActiveTexture(GL_TEXTURE1);
texture2->bind();
shaderProgram.bind();
vao.bind();
for (unsigned int i = 0; i < 10; i++) {
//计算模型矩阵
QMatrix4x4 model;
//平移
model.translate(cubePositions[i]);
//这样每个箱子旋转的速度就不一样
float angle = (i + 1.0f) * rotate;
//旋转
model.rotate(angle, QVector3D(1.0f, 0.3f, 0.5f));
//传入着色器并绘制
shaderProgram.setUniformValue("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
vao.release();
texture1->release();
texture2->release();
shaderProgram.release();
}
void GLCoordinate::resizeGL(int width, int height)
{
glViewport(0, 0, width, height);
}
3.参考
LearnOpenGL:https://learnopengl-cn.github.io/01%20Getting%20started/08%20Coordinate%20Systems/
博客(Qt+OpenGL):https://www.jianshu.com/p/45122bdaae77
博客(透视投影):https://blog.csdn.net/weixin_39662196/article/details/81545695