实例说明了如何使用OpenGL ES 2.0和Qt编写鼠标旋转纹理三维立方体。展示了如何有效地处理多边形几何图形,以及如何为可编程图形流水线编写简单的顶点和片段着色器。此外,它还演示了如何使用四元数来表示三维对象的方向。
这个示例是为OpenGLES2.0编写的,但它也适用于桌面OpenGL,因为这个示例非常简单,大多数情况下与桌面OpenGL API都是相同的。它也可以在没有OpenGL支持的情况下进行编译,但是它只显示了一个标签,说明了OpenGL支持是必需的。
该示例由两个类组成:
1,MainWidget类继承扩展了QGLWidget,包含OpenGL ES 2.0初始化、绘图以及鼠标和计时器事件处理
2,GeometryEngine类处理多边形几何图形。将多边形几何转换为顶点缓冲区对象,并从顶点缓冲区对象绘制几何图形。
#include "geometryengine.h"
#include
#include
#include
#include
#include
#include
#include
#include
class GeometryEngine;
class MainWidget : public QOpenGLWidget, protected QOpenGLFunctions
{
Q_OBJECT
public:
explicit MainWidget(QWidget *parent = 0);
~MainWidget();
protected:
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void timerEvent(QTimerEvent *e) override;
void initializeGL() override;
void resizeGL(int w, int h) override;
void paintGL() override;
void initShaders();
void initTextures();
private:
QBasicTimer timer;
QOpenGLShaderProgram program;
GeometryEngine *geometries;
QOpenGLTexture *texture;
QMatrix4x4 projection;
QVector2D mousePressPosition;
QVector3D rotationAxis;
qreal angularSpeed;
QQuaternion rotation;
};
由于OpenGLES2.0不再支持固定的图形管道,所以它必须由我们自己来实现。这使得图形管道非常灵活,但同时也变得更加困难,因为用户必须实现图形管道才能运行最简单的示例。它还使图形管道更高效,因为用户可以决定应用程序需要什么样的管道。
首先,我们必须实现顶点着色。以顶点数据和model-view-projection matrix(MVP)为参数.利用MVP矩阵变换顶点位置到屏幕空间,并将纹理坐标传递给破片着色器。纹理坐标将自动插值在多边形表面。
在此之后,我们需要实现第二部分的图形管线-片段着色器。在这个练习中,我们需要实现处理纹理的片段着色器。它将插值的纹理坐标作为参数,并从给定的纹理中查找片段颜色。
使用QGLShaderProgram,我们可以编译、链接和绑定着色器代码到图形管道。此代码使用Qt资源文件访问着色器源代码。
QGLWidget接口实现了将纹理从QImage加载到GL纹理内存的方法。我们仍然需要使用OpenGL提供的功能来指定GL纹理单元和配置纹理筛选选项。
#include "mainwidget.h"
#include
#include
MainWidget::MainWidget(QWidget *parent) :
QOpenGLWidget(parent),
geometries(0),
texture(0),
angularSpeed(0)
{
}
MainWidget::~MainWidget()
{
//在删除纹理和缓冲区时,确保上下文是当前的。
makeCurrent();
delete texture;
delete geometries;
doneCurrent();
}
//! [0]
void MainWidget::mousePressEvent(QMouseEvent *e)
{
// 保存鼠标按下位置
mousePressPosition = QVector2D(e->localPos());
}
void MainWidget::mouseReleaseEvent(QMouseEvent *e)
{
// 鼠标释放位置-鼠标按下位置
QVector2D diff = QVector2D(e->localPos()) - mousePressPosition;
//旋转轴垂直于鼠标位置差矢量
QVector3D n = QVector3D(diff.y(), diff.x(), 0.0).normalized();
// 加速角速度相对于鼠标扫掠的长度
qreal acc = diff.length() / 100.0;
// 计算新的旋转轴为加权和
rotationAxis = (rotationAxis * angularSpeed + n * acc).normalized();
// 增加角速度
angularSpeed += acc;
}
void MainWidget::timerEvent(QTimerEvent *)
{
// 降低角速度(摩擦)
angularSpeed *= 0.99;
// 当速度低于阈值时停止旋转
if (angularSpeed < 0.01) {
angularSpeed = 0.0;
} else {
// 更新旋转
rotation = QQuaternion::fromAxisAndAngle(rotationAxis, angularSpeed) * rotation;
// 请求一个更新
update();
}
}
void MainWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0, 0, 0, 1);
initShaders();
initTextures();
// 启用深度缓冲
glEnable(GL_DEPTH_TEST);
// 允许后脸剔除
glEnable(GL_CULL_FACE);
geometries = new GeometryEngine;
// 使用QBasicTimer,因为它比QTimer快
timer.start(12, this);
}
//初始化着色器
void MainWidget::initShaders()
{
// 编译顶点着色器
if (!program.addShaderFromSourceFile(QOpenGLShader::Vertex, ":/vshader.glsl"))
close();
// 编译片段着色器
if (!program.addShaderFromSourceFile(QOpenGLShader::Fragment, ":/fshader.glsl"))
close();
// 链接着色器管道
if (!program.link())
close();
// 绑定着色器管道以供使用
if (!program.bind())
close();
}
//! [4]
void MainWidget::initTextures()
{
// 加载cube.png图像
texture = new QOpenGLTexture(QImage(":/cube.png").mirrored());
// 设置最近的过滤模式,以缩小纹理
texture->setMinificationFilter(QOpenGLTexture::Nearest);
// 设置双线性过滤模式的纹理放大
texture->setMagnificationFilter(QOpenGLTexture::Linear);
// 通过重复使用纹理坐标
// f.ex. texture coordinate (1.1, 1.2) is same as (0.1, 0.2)
texture->setWrapMode(QOpenGLTexture::Repeat);
}
void MainWidget::resizeGL(int w, int h)
{
// 计算比例
qreal aspect = qreal(w) / qreal(h ? h : 1);
// 设置近平面到3.0,远平面到7.0,视场45度
const qreal zNear = 3.0, zFar = 7.0, fov = 45.0;
// 重置投影
projection.setToIdentity();
// 设置透视投影
projection.perspective(fov, aspect, zNear, zFar);
}
void MainWidget::paintGL()
{
// 清理颜色和深度缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
texture->bind();
// 计算模型视图转换
QMatrix4x4 matrix;
matrix.translate(0.0, 0.0, -5.0);
matrix.rotate(rotation);
// 设置modelview-projection矩阵
program.setUniformValue("mvp_matrix", projection * matrix);
// 使用包含cube.png的纹理单元0
program.setUniformValue("texture", 0);
// 画立方体几何
geometries->drawCubeGeometry(&program);
}
#ifndef GEOMETRYENGINE_H
#define GEOMETRYENGINE_H
#include
#include
#include
class GeometryEngine : protected QOpenGLFunctions
{
public:
GeometryEngine();
virtual ~GeometryEngine();
void drawCubeGeometry(QOpenGLShaderProgram *program);
private:
void initCubeGeometry();
QOpenGLBuffer arrayBuf;
QOpenGLBuffer indexBuf;
};
#endif // GEOMETRYENGINE_H
OpenGL中呈现多边形的方法有很多种,但最有效的方法是只使用三角形条基元和图形硬件存储器中的顶点。OpenGL有一种机制,可以为这个内存区域创建缓冲区对象,并将顶点数据传输到这些缓冲区。在OpenGL术语中,这些对象被称为顶点缓冲对象(VBO)。
如上图立方体面就是这样分解成三角形的。用三角形条对顶点进行排序,使顶点排序正确。OpenGL基于顶点排序确定三角形前后脸。默认情况下,OpenGL对正面使用逆时针顺序;这一信息被用于后脸剔除,这提高了渲染性能,因为不呈现后脸的三角形。这样,图形管道可以省略不面向屏幕的三角形的渲染边。
使用QOpenGLBuffer创建顶点缓冲区对象且向它们转换数据非常简单。MainWidget确保使用GeometryEngine实例创建和销毁同时创建和销毁当前的OpenGL上下文。通过这种方式,我们可以在GeometryEngine构造函数中使用OpenGL资源,并在其析构函数中执行适当的清理。
#include "geometryengine.h"
#include
#include
struct VertexData
{
QVector3D position;
QVector2D texCoord;
};
//! [0]
GeometryEngine::GeometryEngine()
: indexBuf(QOpenGLBuffer::IndexBuffer)
{
initializeOpenGLFunctions();
// 生成顶点缓冲对象(VBOs)
arrayBuf.create();
indexBuf.create();
// 初始化立方体几何形状并将其转换为顶点缓冲对象(VBOs)
initCubeGeometry();
}
GeometryEngine::~GeometryEngine()
{
arrayBuf.destroy();
indexBuf.destroy();
}
//! [0]
void GeometryEngine::initCubeGeometry()
{
// 对于立方体,我们只需要8个顶点,但我们必须为每个面复制顶点,因为纹理坐标是不同的。
VertexData vertices[] = {
// Vertex data for face 0
{QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.0f, 0.0f)}, // v0
{QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.0f)}, // v1
{QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.0f, 0.5f)}, // v2
{QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v3
// Vertex data for face 1
{QVector3D( 1.0f, -1.0f, 1.0f), QVector2D( 0.0f, 0.5f)}, // v4
{QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.5f)}, // v5
{QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.0f, 1.0f)}, // v6
{QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v7
// Vertex data for face 2
{QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v8
{QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(1.0f, 0.5f)}, // v9
{QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)}, // v10
{QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(1.0f, 1.0f)}, // v11
// Vertex data for face 3
{QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v12
{QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(1.0f, 0.0f)}, // v13
{QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.66f, 0.5f)}, // v14
{QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(1.0f, 0.5f)}, // v15
// Vertex data for face 4
{QVector3D(-1.0f, -1.0f, -1.0f), QVector2D(0.33f, 0.0f)}, // v16
{QVector3D( 1.0f, -1.0f, -1.0f), QVector2D(0.66f, 0.0f)}, // v17
{QVector3D(-1.0f, -1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v18
{QVector3D( 1.0f, -1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v19
// Vertex data for face 5
{QVector3D(-1.0f, 1.0f, 1.0f), QVector2D(0.33f, 0.5f)}, // v20
{QVector3D( 1.0f, 1.0f, 1.0f), QVector2D(0.66f, 0.5f)}, // v21
{QVector3D(-1.0f, 1.0f, -1.0f), QVector2D(0.33f, 1.0f)}, // v22
{QVector3D( 1.0f, 1.0f, -1.0f), QVector2D(0.66f, 1.0f)} // v23
};
// 使用三角形条带绘制立方体面的索引。
// 三角形带之间可以通过复制索引连接。
//如果连接条的顶点顺序相反,则需要复制第一个条的最后一个索引和第二个条的第一个索引。
//如果连接条具有相同的顶点顺序,则只需要复制第一个条的最后一个索引。
GLushort indices[] = {
0, 1, 2, 3, 3, // Face 0 - triangle strip ( v0, v1, v2, v3)
4, 4, 5, 6, 7, 7, // Face 1 - triangle strip ( v4, v5, v6, v7)
8, 8, 9, 10, 11, 11, // Face 2 - triangle strip ( v8, v9, v10, v11)
12, 12, 13, 14, 15, 15, // Face 3 - triangle strip (v12, v13, v14, v15)
16, 16, 17, 18, 19, 19, // Face 4 - triangle strip (v16, v17, v18, v19)
20, 20, 21, 22, 23 // Face 5 - triangle strip (v20, v21, v22, v23)
};
// 将顶点数据传输到VBO 0
arrayBuf.bind();
arrayBuf.allocate(vertices, 24 * sizeof(VertexData));
// 将索引数据传输到VBO 1
indexBuf.bind();
indexBuf.allocate(indices, 34 * sizeof(GLushort));
}
/*从VBOs中绘制原语并告诉可编程图形管道如何定位顶点数据只需要几个步骤。
* 首先,我们需要绑定要使用的VBOs。
* 然后,我们绑定着色程序属性名,并配置绑定VBO中的数据类型。
* 最后,我们将使用其他VBO的索引来绘制三角形带原语。*/
void GeometryEngine::drawCubeGeometry(QOpenGLShaderProgram *program)
{
// 告诉OpenGL使用哪个VBOs
arrayBuf.bind();
indexBuf.bind();
// 偏移的位置
quintptr offset = 0;
// 告诉OpenGL可编程管道如何定位顶点位置数据
int vertexLocation = program->attributeLocation("a_position");
program->enableAttributeArray(vertexLocation);
program->setAttributeBuffer(vertexLocation, GL_FLOAT, offset, 3, sizeof(VertexData));
// 纹理坐标偏移
offset += sizeof(QVector3D);
// 告诉OpenGL可编程管道如何定位顶点纹理坐标数据
int texcoordLocation = program->attributeLocation("a_texcoord");
program->enableAttributeArray(texcoordLocation);
program->setAttributeBuffer(texcoordLocation, GL_FLOAT, offset, 2, sizeof(VertexData));
// 使用VBO 1中的索引绘制立方体几何图形
glDrawElements(GL_TRIANGLE_STRIP, 34, GL_UNSIGNED_SHORT, 0);
}
#include
#include
#include
#ifndef QT_NO_OPENGL
#include "mainwidget.h"
#endif
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QSurfaceFormat format;
format.setDepthBufferSize(24);
QSurfaceFormat::setDefaultFormat(format);
app.setApplicationName("cube");
app.setApplicationVersion("0.1");
#ifndef QT_NO_OPENGL
MainWidget widget;
widget.show();
#else
QLabel note("OpenGL Support required");
note.show();
#endif
return app.exec();
}