中,我介绍了一种opengl显示地球的办法。那种办法的短处是,大量的计算由CPU完成。假如我们在做一个功能很强、CPU负担较重的界面,较为明智的办法是尽量让GPU分担CPU的工作。本篇博客演示的就是通过GLSL,让GPU分担CPU的一部分工作。
先分析《不使用gluSphere显示地球仪》一文中哪些地方可以交给GPU处理:CPU要计算每个Vertex在世界坐标系的位置
void MainWindow::vInitVerticesTextures(int iVerticalAngleStep, int iHorizontalAngleStep, int iR,
GLfloat * & pVertData, GLfloat * & pTexData,
int & iVerticalPntQuantity, int & iHorizontalPntQuantity)
{
iVerticalPntQuantity = 180 / iVerticalAngleStep;
iHorizontalPntQuantity = 360 / iHorizontalAngleStep;
pVertData = new GLfloat[iVerticalPntQuantity * iHorizontalPntQuantity * 18];
pTexData = new GLfloat[iVerticalPntQuantity * iHorizontalPntQuantity * 12];
int iVertexNodeCount = 0, iTexNodeCount = 0;
for(int vAngle = 0; vAngle < 180; vAngle = vAngle + iVerticalAngleStep)// 垂直方向angleSpan度一份
{
for(int hAngle = 0; hAngle < 360; hAngle = hAngle + iHorizontalAngleStep)// 水平方向angleSpan度一份
{
// 纵向横向各到一个角度后计算对应的此点在球面上的坐标
float x0 = (float) (iR * sin(vAngle * DEG2RAD) * cos(hAngle * DEG2RAD));
float y0 = (float) (iR * sin(vAngle * DEG2RAD) * sin(hAngle * DEG2RAD));
float z0 = (float) (iR * cos(vAngle * DEG2RAD));
// Log.w("x0 y0 z0","" + x0 + " "+y0+ " " +z0);
float x1 = (float) (iR * sin(vAngle * DEG2RAD) * cos((hAngle + iHorizontalAngleStep) * DEG2RAD));
float y1 = (float) (iR * sin(vAngle * DEG2RAD) * sin((hAngle + iHorizontalAngleStep) * DEG2RAD));
float z1 = (float) (iR * cos(vAngle * DEG2RAD));
float x2 = (float) (iR * sin((vAngle + iVerticalAngleStep) * DEG2RAD) *
cos((hAngle + iHorizontalAngleStep) * DEG2RAD));
float y2 = (float) (iR * sin((vAngle + iVerticalAngleStep) * DEG2RAD) *
sin((hAngle + iHorizontalAngleStep) * DEG2RAD));
float z2 = (float) (iR * cos((vAngle + iVerticalAngleStep) * DEG2RAD));
// Log.w("x2 y2 z2","" + x2 + " "+y2+ " " +z2);
float x3 = (float) (iR * sin((vAngle + iVerticalAngleStep) * DEG2RAD) * cos(hAngle * DEG2RAD));
float y3 = (float) (iR * sin((vAngle + iVerticalAngleStep) * DEG2RAD) * sin(hAngle * DEG2RAD));
float z3 = (float) (iR * cos((vAngle + iVerticalAngleStep) * DEG2RAD));
// Log.w("x3 y3 z3","" + x3 + " "+y3+ " " +z3);
// 将计算出来的XYZ坐标加入存放顶点坐标的ArrayList
pVertData[iVertexNodeCount++] = x1;
//alVertix.add(x1);
pVertData[iVertexNodeCount++] = y1;
//alVertix.add(y1);
pVertData[iVertexNodeCount++] = z1;
//alVertix.add(z1);
pVertData[iVertexNodeCount++] = x3;
//alVertix.add(x3);
pVertData[iVertexNodeCount++] = y3;
//alVertix.add(y3);
pVertData[iVertexNodeCount++] = z3;
//alVertix.add(z3);
pVertData[iVertexNodeCount++] = x0;
//alVertix.add(x0);
pVertData[iVertexNodeCount++] = y0;
//alVertix.add(y0);
pVertData[iVertexNodeCount++] = z0;
//alVertix.add(z0);
// *****************************************************************
float s0 = hAngle / 360.0f;
float s1 = (hAngle + iHorizontalAngleStep)/360.0f ;
float t0 = 1 - vAngle / 180.0f;
float t1 = 1 - (vAngle + iVerticalAngleStep) / 180.0f;
/*t0 = t0 - 0.1;
t1 = t1 - 0.1;
if(t0 < 0)
t0 = t0 + 1;
if(t1 < 0)
t1 = t1 + 1;*/
pTexData[iTexNodeCount++] = s1;
//textureVertix.add(s1);// x1 y1对应纹理坐标
pTexData[iTexNodeCount++] = t0;
//textureVertix.add(t0);
pTexData[iTexNodeCount++] = s0;
//textureVertix.add(s0);// x3 y3对应纹理坐标
pTexData[iTexNodeCount++] = t1;
//textureVertix.add(t1);
pTexData[iTexNodeCount++] = s0;
//textureVertix.add(s0);// x0 y0对应纹理坐标
pTexData[iTexNodeCount++] = t0;
//textureVertix.add(t0);
// *****************************************************************
pVertData[iVertexNodeCount++] = x1;
//alVertix.add(x1);
pVertData[iVertexNodeCount++] = y1;
//alVertix.add(y1);
pVertData[iVertexNodeCount++] = z1;
//alVertix.add(z1);
pVertData[iVertexNodeCount++] = x2;
//alVertix.add(x2);
pVertData[iVertexNodeCount++] = y2;
//alVertix.add(y2);
pVertData[iVertexNodeCount++] = z2;
//alVertix.add(z2);
pVertData[iVertexNodeCount++] = x3;
//alVertix.add(x3);
pVertData[iVertexNodeCount++] = y3;
//alVertix.add(y3);
pVertData[iVertexNodeCount++] = z3;
//alVertix.add(z3);
// *****************************************************************
pTexData[iTexNodeCount++] = s1;
//textureVertix.add(s1);// x1 y1对应纹理坐标
pTexData[iTexNodeCount++] = t0;
//textureVertix.add(t0);
pTexData[iTexNodeCount++] = s1;
//textureVertix.add(s1);// x2 y3对应纹理坐标
pTexData[iTexNodeCount++] = t1;
//textureVertix.add(t1);
pTexData[iTexNodeCount++] = s0;
//textureVertix.add(s0);// x3 y3对应纹理坐标
pTexData[iTexNodeCount++] = t1;
//textureVertix.add(t1);
// *****************************************************************
}
}
}
其中涉及大量三角函数运算。而三角函数运算是比较耗时的。这部分是我们的优化目标。
在着手优化之前,不妨先分析一下绘制地球仪的原理。
1)地球仪是一个圆球,但是opengl是无法绘制理想的球形的。它所能做的是用多面体来近似完美球形。在本例子里,我把地球按照经度每10度一格,纬度每10度一格来分割。因此地球表面被划分为180 / 10 * 360 / 10 个格子。
2)地球表面的形貌记录在一个矩形 的图片里。这就是我们要用的纹理。
假如把这张图也用网格分割,那么纵向的线对应经线,而横向线代表纬线。这与球面的网格构成对应关系。
我们要做的就是把分割的纹理填充到对应的球面的网格里去。
这通过GLSL的texture()函数实现。
不难看出,纹理的横纵坐标对应地球仪的经纬度。但是GLSL对纹理的坐标取值有规定:0-1.左边=0,最右边=1;最上=1,最下=0。因此在vertex shader里有这一句:
texCoord = vec2(pos.x / 360.0, 1.0 - pos.y / 180.0);
opengl通过setAttributeArray()函数把vertex的坐标传入GPU。shader的数目与vertex的数目相等,每一个shader对应一个vertex。在《不使用gluSphere显示地球仪》一文中,我们让CPU计算了各个vertex的世界坐标。为了减少CPU的负担,在本例中,传入vertex shader的坐标不是各个vertex在世界坐标系的坐标,而是vertex 在球面的经纬度坐标。
" float x = pos[2] * sin(pos[1] * DEG2RAD) * cos(pos[0] * DEG2RAD);\n"
" float y = pos[2] * sin(pos[1] * DEG2RAD) * sin(pos[0] * DEG2RAD);\n"
" float z = pos[2] * cos(pos[1] * DEG2RAD);\n"
" gl_Position = mat4MVP * vec4(x, y, z, 1.0);\n"
经纬度转世界坐标的工作在vertex shader里完成。最后用mat4MVP * vec4(x,y,z,1.0)来完成世界坐标到观察者坐标的变换。
texCoord随后传入fragment shader。利用texture(Tex, texCoord)获取对应的gl_Position的颜色。相邻的两个vertex之间的点的颜色按如下方法得出:texCoord 与 texCoord之间进行插值。插值的结果也带入texture(Tex, texCoord),得出该点颜色。这一步不需要程序员显式的写入代码,GLSL内部已经做了这样的处理。
最后给出代码:
h文件:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLTexture>
#include <QOpenGLShader>
#include <QOpenGLShaderProgram>
class MainWindow : public QOpenGLWidget, protected QOpenGLFunctions
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
GLuint m_uiVertLoc;
GLuint m_uiTexNum;
QOpenGLTexture * m_pTextures;
QOpenGLShaderProgram * m_pProgram;
GLfloat * m_pVertices;
int m_iCount;
protected:
void initializeGL();
void paintGL();
void resizeGL(int w, int h);
};
#endif // MAINWINDOW_H
cpp文件:
#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QOpenGLWidget(parent)
{
}
MainWindow::~MainWindow()
{
m_pTextures->release();
delete m_pTextures;
delete m_pProgram;
delete [] m_pVertices;
}
void MainWindow::initializeGL()
{
initializeOpenGLFunctions();
m_iCount = 0;
m_uiTexNum = 0;
QImage img(QString("earth.bmp"));
//给顶点赋值
int iAngleStep = 10, iR = 1;
m_pVertices = new GLfloat[180 / iAngleStep * 360 / iAngleStep * 6 * 3];
for(int iLongitude = 0; iLongitude < 360; iLongitude += iAngleStep)
{
for(int iLatitude = 0; iLatitude < 180; iLatitude += iAngleStep)
{
m_pVertices[m_iCount++] = iLongitude;
m_pVertices[m_iCount++] = iLatitude;
m_pVertices[m_iCount++] = iR;
m_pVertices[m_iCount++] = iLongitude;
m_pVertices[m_iCount++] = iLatitude + iAngleStep;
m_pVertices[m_iCount++] = iR;
m_pVertices[m_iCount++] = iLongitude + iAngleStep;
m_pVertices[m_iCount++] = iLatitude + iAngleStep;
m_pVertices[m_iCount++] = iR;
m_pVertices[m_iCount++] = iLongitude + iAngleStep;
m_pVertices[m_iCount++] = iLatitude + iAngleStep;
m_pVertices[m_iCount++] = iR;
m_pVertices[m_iCount++] = iLongitude + iAngleStep;
m_pVertices[m_iCount++] = iLatitude;
m_pVertices[m_iCount++] = iR;
m_pVertices[m_iCount++] = iLongitude;
m_pVertices[m_iCount++] = iLatitude;
m_pVertices[m_iCount++] = iR;
}
}
QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
const char *vsrc =
"#version 330\n"
"in vec3 pos;\n"
"out vec2 texCoord;\n"
"uniform mat4 mat4MVP;\n"
"void main()\n"
"{\n"
" float DEG2RAD = 3.1415926 / 180.0;\n"
" float x = pos[2] * sin(pos[1] * DEG2RAD) * cos(pos[0] * DEG2RAD);\n"
" float y = pos[2] * sin(pos[1] * DEG2RAD) * sin(pos[0] * DEG2RAD);\n"
" float z = pos[2] * cos(pos[1] * DEG2RAD);\n"
" gl_Position = mat4MVP * vec4(x, y, z, 1.0);\n"
" texCoord = vec2(pos.x / 360.0, 1.0 - pos.y / 180.0);\n"//纹理的Y方向是从下向上的,而pos.y的正方向是从上向下,所以是1.0 - pos.y / 180.0
"}\n";
vshader->compileSourceCode(vsrc);
QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
const char *fsrc =
"#version 330\n"
"out vec4 color;\n"
"in vec2 texCoord;\n"
"uniform sampler2D Tex\n;"
"void main()\n"
"{\n"
" color = texture(Tex, texCoord);\n"//注意,texCoord的值域在0-1之间
"}\n";
fshader->compileSourceCode(fsrc);
m_pProgram = new QOpenGLShaderProgram;
m_pProgram->addShader(vshader);
m_pProgram->addShader(fshader);
m_pProgram->link();
m_pProgram->bind();
m_uiVertLoc = m_pProgram->attributeLocation("pos");
m_pProgram->enableAttributeArray(m_uiVertLoc);
m_pProgram->setAttributeArray(m_uiVertLoc, m_pVertices, 3, 0);
m_pTextures = new QOpenGLTexture(img.mirrored());
m_pTextures->setMinificationFilter(QOpenGLTexture::Nearest);
m_pTextures->setMagnificationFilter(QOpenGLTexture::Linear);
m_pTextures->setWrapMode(QOpenGLTexture::Repeat);
m_pProgram->setUniformValue("Tex", m_uiTexNum);
qDebug()<<m_iCount;
glEnable(GL_DEPTH_TEST);
glClearColor(0,0,0,1);
}
void MainWindow::paintGL()
{
//QMatrix4x4在声明时被默认为单位矩阵
QMatrix4x4 m1, m2, m3, m;
m1.ortho(-1.0f, +1.0f, -1.0f, 1.0f, -50.0f, 50.0f);//right//generate projection matrix
m2.lookAt(QVector3D(2,0,0), QVector3D(0,0,0), QVector3D(0,0,10));//generate view matrix, right
qDebug()<<m2;
//m3.translate(0,-0.707,0.0);//right, generate model matrices
//m3.rotate(45, 0.0f, 0.0f, 1.0f);//right, generate model matrices
m = m1 * m2 * m3;
m_pProgram->setUniformValue("mat4MVP", m);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
m_pTextures->bind(m_uiTexNum);
glDrawArrays(GL_TRIANGLES, 0, m_iCount / 3);
m_pTextures->release();
}
void MainWindow::resizeGL(int w, int h)
{
glViewport(0,0,w,h);
}
效果: