Linux环境QT5.9+OpenGL绘制图形与渲染(11)详解系列
这是一个系列的博客,OpenGL是基于计算机图形学为基础发展出来的一个分支,必须理解清楚的是“向量”,由数学向量与C语言结合发展出了shader language,shader封装再结合C++就是UE4和UE5图形引擎了。向量vector(不是C++里面的vector容器啊,不要搞混了),最重要的就是引入数学矩阵Matrix,一个矩阵就是一个向量,即将向量计算变成矩阵变换,同时一个向量vector可以等于两个矩阵计算的结果。


这是向量A,换算成矩阵的样子

矩阵的乘法满足于以下运算律:
结合律:(AB)C = A(BC)
左分配律:(A + B)C = AC + BC
右分配律:C(A + B) = CA + CB
矩阵乘法不满足交换律:
由此我们明白了,矩阵Matrix与向量Vector的关系,但是还没搞明白OpenGL与shader的关系。
OpenGL有vertex shader 和 fragment shader等过程,这些就是封装过的shader在OpenGL里面使用。
关于纹理滤波的问题:
线性插值滤波(GL_LINEAR)==的纹理贴图,这需要机器有相当高的处理能力,但是看起来效果会很好;
最临*值滤波(GL_NEAREST),它只占用很小的处理能力,看起来效果会比较差,但是使用它因为不占用资源,工程在很快和很慢的机器上都可以正常运行;也可以混合使用线性插值滤波和最临*值滤波,纹理看起来效果会好一些;
Mipmap,这是一种创建纹理的新方法;您可能会注意到当图像在屏幕上变得很小的时候,很多细节将会丢失,刚才还很不错的图案变得很难看;当您告诉OPenGL创建一个mipmaped纹理时,OPenGL将选择它已经创建的外观最佳的纹理(带有很多细节)来绘制,而不仅仅是缩放原先的图像(这将导致细节丢失)。
关于光照
(1)当不开启光照时,使用顶点颜色来产生整个表面的颜色。
用glShadeModel可以设置表面内部像素颜色产生的方式。GL_FLAT/GL_SMOOTH.
(2)一般而言,开启光照后,在场景中至少需要有一个光源(GL_LIGHT0.。.GL_LIGHT7)
通过glEnable(GL_LIGHT0) glDisable(GL_LIGHT0) 来开启和关闭指定的光源。
— 全局环境光 —
GLfloat gAmbient[] = {0.6, 0,6, 0,6, 1.0};
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, gAmbient);
(3)设置光源的光分量 – 环境光/漫色光/镜面光
默认情况下,GL_LIGHT0.。.GL_LIGHT7 的GL_AMBIENT值为(0.0, 0.0, 0.0, 1.0);
GL_LIGHT0的GL_DIFFUSE和GL_SPECULAR值为(1.0, 1.0, 1.0, 1.0),
GL_LIGHT1.。.GL_LIGHT7 的GL_DIFFUSE和GL_SPECULAR值为(0.0, 0.0, 0.0, 0.0)。
GLfloat lightAmbient[] = {1.0, 1.0, 1.0, 1.0};
GLfloat lightDiffuse[] = {1.0, 1.0, 1.0, 1.0};
GLfloat lightSpecular[] = {0.5, 0.5, 0.5, 1.0};
glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, lightDiffuse);
glLightfv(GL_LIGHT0, GL_SPECULAR, lightSpecular);
(4)设置光源的位置和方向
– *行光 – 没有位置只有方向
GLfloat lightPosiTIon[] = {8.5, 5.0, -2.0, 0.0}; // w=0.0
glLightfv(GL_LIGHT0, GL_POSITION, lightPosiTIon);
– 点光源 – 有位置没有方向
GLfloat lightPosiTIon[] = {8.5, 5.0, -2.0, 1.0}; // w不为0
glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);
– 聚光灯 – 有位置有方向
GLfloat lightPosition[] = {-6.0, 1.0, 3.0, 1.0}; // w不为0
glLightfv(GL_LIGHT0, GL_POSITION, lightPosition);
GLfloat lightDirection[] = {1.0, 1.0, 0.0};
glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, lightDirection); // 聚光灯主轴方向 spot direction
glLightf(GL_LIGHT0, GL_SPOT_CUTOFF, 45.0); // cutoff角度 spot cutoff
** *行光不会随着距离d增加而衰减,但点光源和聚光灯会发生衰减。
attenuation为衰变系数,系数值越大,衰变越快。
默认情况下,c=1.0, l=0.0, q=0.0
glLightf(GL_LIGHT0, GL_CONSTANT_ATTENUATION, 2.0); // c 系数
glLightf(GL_LIGHT0, GL_LINEAR_ATTENUATION, 1.0); // l 系数
glLightf(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, 0.5); // q 系数


.h文件
ifndef MYGLWIDGET_H
define MYGLWIDGET_H
include
include
include
include
include
include <GL/glu.h>
include
include
include <math.h>
class MyGLWidget : public QOpenGLWidget
{
Q_OBJECT
public:
MyGLWidget(QWidget *parent = nullptr);
~MyGLWidget();
protected:
void resizeGL(int w, int h);
void initializeGL();
void paintGL();
void keyPressEvent(QKeyEvent *event);
void timerEvent(QTimerEvent *event);
private:
void loadGLTexture();
private:
bool m_show_full_screen;
GLfloat m_x_rotate;
GLfloat m_y_rotate;
GLfloat m_z_rotate;
GLuint m_texture[1];
//将使用points数组来存放网格各顶点独立的x,y,z坐标。这里网格由45×45点形成,
//换句话说也就是由44格×44格的小方格子依次组成了。
float m_points[45][45][3]; // Points网格顶点数组
};
endif // MYGLWIDGET_H
.cpp文件
include “myglwidget.h”
//创建一个飘动的旗帜。我相信在这课结束的时候,你可以掌握纹理映射和混合操作。
//实现一个以正弦波方式运动的图象。这一课基于NeHe的教程第六课,当然您至少也应该学会了一至六课的知识。
//您需要下载源码压缩包,并将压缩包内带的data目录连其下的位图一起释放至您的代码目录下。或者使用您自己的位图,
//当然它的尺寸必须适合OpenGL纹理的要求。
MyGLWidget::MyGLWidget(QWidget *parent) : QOpenGLWidget(parent), m_show_full_screen(false),
m_x_rotate(0.0f), m_y_rotate(0.0f), m_z_rotate(0.0f)
{
showNormal();
startTimer(100);
}
MyGLWidget::~MyGLWidget()
{
glDeleteTextures(1, &m_texture[0]);
}
//这段代码用来加载位图文件。如果文件不存在,返回 NULL 告知程序无法加载位图。
//关于用作纹理的图像我想有几点十分重要,此图像的宽和高必须是2的n次方;
//宽度和高度最小必须是64象素;并且出于兼容性的原因,图像的宽度和高度不应超过256象素。
//如果您的原始素材的宽度和高度不是64,128,256象素的话,使用图像处理软件重新改变图像的大小。
//肯定有办法能绕过这些限制,但现在我们只需要用标准的纹理尺寸。
void MyGLWidget::resizeGL(int w, int h)
{
if(h == 0)
{
h = 1;
}
glViewport(0, 0, w, h); //重置当前的视口
//调用glLoadIdentity()之后我们为场景设置透视图。
//modelview matrix(模型观察矩阵)。
glMatrixMode(GL_PROJECTION); // 选择投影矩阵
glLoadIdentity(); // 重置投影矩阵
//设置视口的大小
gluPerspective(45.0f, (GLfloat)w/(GLfloat)h, 0.1f, 100.0f);
glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵
glLoadIdentity(); // 重置模型观察矩阵
}
//loadGLTexture()这行代码调用载入位图并生成纹理。纹理创建好了,我们启用2D纹理映射。
//如果您忘记启用的话,您的对象看起来永远都是纯白色
void MyGLWidget::initializeGL()
{
loadGLTexture();
glEnable(GL_TEXTURE_2D); // 启用纹理映射
//下一行启用smooth shading(阴影*滑)。阴影*滑通过多边形精细的混合色彩,并对外部光进行*滑。
glShadeModel(GL_SMOOTH); // 启用阴影*滑
//因此,当您使用glClearColor(0.0f,0.0f,1.0f,0.0f),您将用亮蓝色来清除屏幕。
glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // 黑色背景
//接下来的三行必须做的是关于depth buffer(深度缓存)的。将深度缓存设想为屏幕后面的层。
//深度缓存不断的对物体进入屏幕内部有多深进行跟踪。
//程序其实没有真正使用深度缓存,但几乎所有在屏幕上显示3D场景OpenGL程序都使用深度缓存。
//它的排序决定那个物体先画。这样您就不会将一个圆形后面的正方形画到圆形上来。
glClearDepth(1.0f); // 设置深度缓存
glEnable(GL_DEPTH_TEST); // 启用深度测试
glDepthFunc(GL_LEQUAL); // 所作深度测试的类型
//接着告诉OpenGL我们希望进行最好的透视修正。但使得透视图看起来好一点。
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 告诉系统对透视进行修正
glPolygonMode(GL_BACK, GL_FILL); // 后表面完全填充
glPolygonMode(GL_FRONT, GL_LINE); // 前表面使用线条绘制
//上面的代码指定使用完全填充模式来填充多边形区域的背面
// X*面循环
for(int x=0; x<45; x++)
{
// Y*面循环
for(int y=0; y<45; y++)
{
// 向表面添加波浪效果
m_points[x][y][0]=float((x/5.0f)-4.5f);
m_points[x][y][1]=float((y/5.0f)-4.5f);
m_points[x][y][2]=float(sin((((x/5.0f)40.0f)/360.0f)3.141592654*2.0f));
}
}
//上面的两个循环初始化网格上的点。使用整数循环可以消除由于浮点运算取整造成的脉冲锯齿的出现。
//我们将x和y变量都除以5,再减去4.5,这样使得我们的波浪可以“居中”。
//点[x][y][2]最后的值就是一个sine函数计算的结果。Sin()函数需要一个弧度参变量。
//将float_x乘以40.0f,得到角度值。然后除以360.0f再乘以PI,乘以2,就转换为弧度了。
}
//开始两行代码 glClear() 和 glLoadIdentity() 是第一课中就有的代码。
//glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 清除屏幕并设为我们在 InitGL() 中选定的颜色,本例中是黑色。
//深度缓存也被清除。模型观察矩阵也使用glLoadIdentity()重置。
void MyGLWidget::paintGL()
{
float float_x, float_y, float_xb, float_yb; // 用来将旗形的波浪分割成很小的四边形
//下面的代码中大多数变量除了用来控制循环和存储临时变量之外并没有什么别的用处。
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清除屏幕和深度缓存
glLoadIdentity(); // 重置当前的模型观察矩阵
glTranslatef(0.0f, 0.0f, -12.0f); // 移入屏幕12个单位
glRotatef(m_x_rotate, 1.0f, 0.0f, 0.0f);
glRotatef(m_y_rotate,0.0f,1.0f,0.0f);
glRotatef(m_z_rotate,0.0f,0.0f,1.0f);
glBindTexture(GL_TEXTURE_2D, m_texture[0]); // 选择纹理
glBegin(GL_QUADS); // 四边形绘制开始
for(int x=0; x<44;x++) // 沿 X *面 0-44 循环(45点)
{
for(int y=0;y<44;y++) // 沿 Y *面 0-44 循环(45点)
{
//接着开始使用循环进行多边形绘制。这里使用整型可以避免我以前所用的int()强制类型转换。
float_x = float(x)/44.0f; // 生成X浮点值
float_y = float(y)/44.0f; // 生成Y浮点值
float_xb = float(x+1)/44.0f; // X浮点值+0.0227f
float_yb = float(y+1)/44.0f; // Y浮点值+0.0227f
//上面我们使用4个变量来存放纹理坐标。每个多边形(网格之间的四边形)分别映射了纹理的1/44×1/44部分。
//循环首先确定左下顶点的值,然后我们据此得到其他三点的值。
glTexCoord2f(float_x, float_y); // 第一个纹理坐标 (左下角)
glVertex3f(m_points[x][y][0], m_points[x][y][1], m_points[x][y][2]);
glTexCoord2f(float_x, float_yb); // 第二个纹理坐标 (左上角)
glVertex3f(m_points[x][y+1][0], m_points[x][y+1][1], m_points[x][y+1][2]);
glTexCoord2f( float_xb, float_yb ); // 第三个纹理坐标 (右上角)
glVertex3f( m_points[x+1][y+1][0], m_points[x+1][y+1][1], m_points[x+1][y+1][2] );
glTexCoord2f( float_xb, float_y ); // 第四个纹理坐标 (右下角)
glVertex3f( m_points[x+1][y][0], m_points[x+1][y][1], m_points[x+1][y][2] );
}
}
glEnd(); // 四边形绘制结束
//上面几行使用glTexCoord2f()和glVertex3f()载入数据。提醒一点:四边形是逆时针绘制的。
//如果您按顺时针顺序绘制的话,您初始时见到的可能是前表面。也就是说您将看到网格型的纹理效果而不是完全填充的。
}
void MyGLWidget::keyPressEvent(QKeyEvent *event)
{
switch(event->key())
{
case Qt::Key_F2:
{
m_show_full_screen = !m_show_full_screen;
if(m_show_full_screen){
showFullScreen();
}
else
{
showNormal();
}
update();
break;
}
case Qt::Key_Escape:
{
qApp->exit();
break;
}
}
}
void MyGLWidget::timerEvent(QTimerEvent *event)
{
for(int y=0; y<45; y++)
{
GLfloat hold = m_points[0][y][2]; // 存储当前左侧波浪值
for(int x=0; x<44; x++) // 沿X*面循环
{
// 当前波浪值等于其右侧的波浪值
m_points[x][y][2] = m_points[x+1][y][2];
}
m_points[44][y][2] = hold; // 刚才的值成为最左侧的波浪值
}
//上面所作的事情是先存储每一行的第一个值,然后将波浪左移一下,是图象产生波浪。
//存储的数值挪到末端以产生一个永无尽头的波浪纹理效果。然后重置计数器wiggle_count以保持动画的进行。
//上面的代码由NeHe(2000年2月)修改过,以消除波浪间出现的细小锯齿。
//现在增加 xrot , yrot 和 zrot 的值。
m_x_rotate += 0.3f;
m_y_rotate += 0.2f;
m_z_rotate += 0.4f;
update();
QOpenGLWidget::timerEvent(event);
}
void MyGLWidget::loadGLTexture()
{
//现在载入图像,并将其转换为纹理。
QImage image(":/image/Tim.bmp");
image = image.convertToFormat(QImage::Format_RGB888);
image = image.mirrored();
glGenTextures(1, &m_texture[0]); // 创建纹理
// 使用来自位图数据生成 的典型纹理
glBindTexture(GL_TEXTURE_2D, m_texture[0]);
//下来我们创建真正的纹理。下面一行告诉OpenGL此纹理是一个2D纹理 ( GL_TEXTURE_2D )。
//因为图像是由红色数据,绿色数据,蓝色数据三种组分组成。 TextureImage[0]->sizeX 是纹理的宽度。
//TextureImage[0]->sizey 是纹理的高度。参数零是边框的值,一般就是“0”。
//GL_RGB 告诉OpenGL图像数据由红、绿、蓝三色数据组成。
//GL_UNSIGNED_BYTE 意味着组成图像的数据是无符号字节类型的。
//最后TextureImage[0]->data 告诉OpenGL纹理数据的来源。指向存放在 TextureImage[0] 记录中的数据。
glTexImage2D(GL_TEXTURE_2D, 0, 3,
image.width(), image.height(), 0, GL_RGB, GL_UNSIGNED_BYTE,
image.bits());
//下面的两行告诉OpenGL在显示图像时,
//当它比放大得原始的纹理大 ( GL_TEXTURE_MAG_FILTER )或缩小得比原始得纹理小( GL_TEXTURE_MIN_FILTER )时
//OpenGL采用的滤波方式。通常这两种情况下我都采用 GL_LINEAR 。
//这使得纹理从很远处到离屏幕很*时都*滑显示。使用 GL_LINEAR 需要CPU和显卡做更多的运算。
//如果您的机器很慢,您也许应该采用 GL_NEAREST 。过滤的纹理在放大的时候
//您也可以结合这两种滤波方式。在*处时使用 GL_LINEAR ,远处时 GL_NEAREST 。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 线形滤波
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 线形滤波
}
//标准的NeHe旋转增量。现在编译并运行程序,您将看到一个漂亮的位图波浪。

浙公网安备 33010602011771号