Linux环境QT5.9+OpenGL绘制图形与渲染(12)详解系列

这是一个系列的博客,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 <GL/glu.h>

include

include

include

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);
private:
void buildLists();
void loadTexture();
private:
bool m_show_full_screen;
//首先是存储纹理的变量,然后两个新的变量用于显示列表。这些变量是指向内存中显示列表的指针。命名为box和top。
//然后用两个变量xloop,yloop表示屏幕上立方体的位置,两个变量xrot,yrot表示立方体的旋转。
GLuint m_box; // 保存盒子的显示列表
GLuint m_top; // 保存盒子顶部的显示列表
GLfloat m_yrot;
GLfloat m_xrot;
GLuint m_texture[1];

};

endif // MYGLWIDGET_H

.cpp文件

include “myglwidget.h”

//在制作游戏里的小行星场景时,每一层上至少需要两个行星,你可以用OpenGL中的多边形来构造每一个行星。
//聪明点的做法是做一个循环,每个循环画出行星的一个面,最终你用几十条语句画出了一个行星。
//每次把行星画到屏幕上都是很困难的。当你面临更复杂的物体时你就会明白了。
//给现实列表一个名字,比如给小行星的显示列表命名为“asteroid”。现在,任何时候我想在屏幕上画出行星,
//只需要调用glCallList(asteroid)。之前做好的小行星就会立刻显示在屏幕上了。
//因为小行星已经在显示列表里建造好了,OpenGL不会再计算如何构造它。
//称这个DEMO为Q-Bert显示列表。最终这个DEMO将在屏幕上画出15个立方体。
//每个立方体都由一个盒子和一个顶部构成,顶部是一个单独的显示列表,盒子没有顶。
//接下来建立两个颜色数组
static GLfloat boxcol[5][3]= // 盒子的颜色数组
{
// 亮:红,橙,黄,绿,蓝
{1.0f,0.0f,0.0f},{1.0f,0.5f,0.0f},{1.0f,1.0f,0.0f},{0.0f,1.0f,0.0f},{0.0f,1.0f,1.0f}
};

static GLfloat topcol[5][3]= // 顶部的颜色数组
{
// 暗:红,橙,黄,绿,蓝

};

MyGLWidget::MyGLWidget(QWidget *parent) : QOpenGLWidget(parent), m_show_full_screen(false),
m_yrot(0.0f), m_xrot(0.0f)
{
showNormal();
}

MyGLWidget::~MyGLWidget()
{
glDeleteTextures(1, &m_texture[0]);
glDeleteLists(m_box, 2);
}

//下面的代码的作用是重新设置OpenGL场景的大小,而不管窗口的大小是否已经改变
//甚至您无法改变窗口的大小时(例如您在全屏模式下),在程序开始时设置我们的透视图。
void MyGLWidget::resizeGL(int w, int h)
{
if(h == 0)
{
h = 1;
}
glViewport(0, 0, w, h); //重置当前的视口
//下面几行为透视图设置屏幕。意味着越远的东西看起来越小。这么做创建了一个现实外观的场景。
//此处透视按照基于窗口宽度和高度的45度视角来计算。0.1f,100.0f是我们在场景中所能绘制深度的起点和终点。
//glMatrixMode(GL_PROJECTION)指明接下来的两行代码将影响projection matrix(投影矩阵)。
// glLoadIdentity()近似于重置。它将所选的矩阵状态恢复成其原始状态。
//调用glLoadIdentity()之后我们为场景设置透视图。
//glMatrixMode(GL_MODELVIEW)指明任何新的变换将会影响 modelview matrix(模型观察矩阵)。
//最后我们重置模型观察矩阵。
//只要知道如果您想获得一个精彩的透视场景的话,必须这么做。
glMatrixMode(GL_PROJECTION); // 选择投影矩阵
glLoadIdentity(); // 重置投影矩阵
//设置视口的大小
gluPerspective(45.0f, (GLfloat)w/(GLfloat)h, 0.1f, 100.0f);

glMatrixMode(GL_MODELVIEW); //选择模型观察矩阵
glLoadIdentity(); // 重置模型观察矩阵
}

//初始化的代码,加入了一行BuildList()。先读入纹理,然后建立显示列表,
//这样当我们建立显示列表的时候就可以将纹理贴到立方体上了。
void MyGLWidget::initializeGL()
{
loadTexture();
buildLists();
glEnable(GL_TEXTURE_2D); // 启用纹理映射
//启用smooth shading(阴影平滑)。阴影平滑通过多边形精细的混合色彩,并对外部光进行平滑。
glShadeModel(GL_SMOOTH); // 启用阴影平滑

//最大值也是1.0f,代表特定颜色分量的最亮情况。最后一个参数是Alpha值。
//因此,当您使用glClearColor(0.0f,0.0f,1.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); // 告诉系统对透视进行修正
//接下来的三行使灯光有效。Light0一般来说是在显卡中预先定义过的,如果Light0不工作
//最后一行的GL_COLOR_MATERIAL使我们可以用颜色来贴纹理。
//如果没有这行代码,纹理将始终保持原来的颜色,glColor3f(r,g,b)就没有用了。
glEnable(GL_LIGHT0); // 使用默认的0号灯
glEnable(GL_LIGHTING); // 使用灯光
glEnable(GL_COLOR_MATERIAL); // 使用颜色材质
}

void MyGLWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除屏幕和深度缓存
glBindTexture(GL_TEXTURE_2D, m_texture[0]); // 选择纹理
//现在到了真正有趣的地方了。用一个循环,循环变量用于改变Y轴位置,在Y轴上画5个立方体,所以用从1到5的循环。
for (int yloop=1;yloop<6;yloop++) // 沿Y轴循环
{//另外用一个循环,循环变量用于改变X轴位置。每行上的立方体数目取决于行数,所以循环方式如下。
for (int xloop=0;xloop<yloop;xloop++) // 沿X轴循环
{

  //重置模型变化矩阵
  glLoadIdentity();       // 重置模型变化矩阵
  //下边的代码是移动和旋转当前坐标系到需要画出立方体的位置。
  // 设置盒子的位置
  glTranslatef(1.4f+(float(xloop)*2.8f)-(float(yloop)*1.4f),((6.0f-float(yloop))*2.4f)-7.0f,-20.0f);
  glRotatef(45.0f-(2.0f*yloop)+m_xrot,1.0f,0.0f,0.0f);
  glRotatef(45.0f+m_yrot,0.0f,1.0f,0.0f);
  //然后在正式画盒子之前设置颜色。每个盒子用不同的颜色。
  glColor3fv(boxcol[yloop-1]);
  //颜色设置好了。现在需要做的就是画出盒子。
  //不用写出画多边形的代码,只需要用glCallList(box)命令调用显示列表。
  //盒子将会用glColor3fv()所设置的颜色画出来。
  glCallList(m_box);     // 绘制盒子
  //然后用另外的颜色画顶部.
  glColor3fv(topcol[yloop-1]);   // 选择顶部颜色
  glCallList(m_top);             // 绘制顶部
}

}
}

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;
}
case Qt::Key_Up:
{
m_xrot-=0.2f;
update();
break;
}
case Qt::Key_Down:
{
m_xrot+=0.2f;
update();
break;
}
case Qt::Key_Left:
{
m_yrot-=0.2f;
update();
break;
}
case Qt::Key_Right:
{
m_yrot+=0.2f;
update();
break;
}
}
}

//所有创造盒子的代码都在第一个显示列表里,所有创造顶部的代码都在另一个列表里。
void MyGLWidget::buildLists()
{
//开始的时候我们告诉OpenGL我们要建立两个显示列表。
//glGenLists(2)建立了两个显示列表的空间,并返回第一个显示列表的指针。
//“box”指向第一个显示列表,任何时候调用“box”第一个显示列表就会显示出来。
m_box = glGenLists(2); // 创建两个显示列表的名称

//现在开始构造第一个显示列表。已经申请了两个显示列表的空间了,并且有box指针指向第一个显示列表。
//所以现在我们应该告诉OpenGL要建立什么类型的显示列表。
//我们用glNewList()命令来做这个事情。你一定注意到了box是第一个参数,这表示OpenGL将把列表存储到box所指向的内存空间。
//第二个参数GL_COMPILE告诉OpenGL我们想预先在内存中构造这个列表,这样每次画的时候就不必重新计算怎么构造物体了。
//GL_COMPILE类似于编程。在你写程序的时候,把它装载到编译器里,你每次运行程序都需要重新编译。
//当OpenGL编译过显示列表后,就不需要再每次显示的时候重新编译它了。这就是为什么用显示列表可以加快速度。
glNewList(m_box, GL_COMPILE); // 创建第一个显示列表

//下面这部分的代码画出一个没有顶部的盒子,它不会出现在屏幕上,只会存储在显示列表里。
//你可以在glNewList()和glEngList()中间加上任何你想加上的代码。可以设置颜色,贴图等等。
//唯一不能加进去的代码就是会改变显示列表的代码。显示列表一旦建立,你就不能改变它。
//比如你想加上glColor3ub(rand()%255,rand()%255,rand()%255),使得每一次画物体时都会有不同的颜色。
//但因为显示列表只会建立一次,所以每次画物体的时候颜色都不会改变。物体将会保持第一次建立显示列表时的颜色。
//如果你想改变显示列表的颜色,你只有在调用显示列表之前改变颜色。后面将详细解释这一点。
    glBegin(GL_QUADS);							// 开始绘制四边形
        // 底面
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f);
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f);
        // 前面
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f);
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f);
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f);
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f);
        // 后面
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f);
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f);
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
        // 右面
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f);
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f);
        glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f);
        glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f,  1.0f);
        // 左面
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
        glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f);
        glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f,  1.0f,  1.0f);
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f);
    glEnd();								// 四边形绘制结束
//用glEngList()命令,告诉OpenGL已经完成了一个显示列表。在glNewList()和glEngList()之间的任何东西就是显示列表的一部分。
glEndList();									// 第一个显示列表结束
//建立第二个显示列表。在上一个显示列表的指针上加1,就得到了第二个显示列表的指针。第二个显示列表的指针命名为“top”。
m_top = m_box+1;								// 第二个显示列表的名称
//现在我们知道了第二个显示列表的指针,我们可以建立它了。
glNewList(m_top,GL_COMPILE);					// 盒子顶部的显示列表
    //下面的代码画出盒子的顶部。
    glBegin(GL_QUADS);							// 开始绘制四边形
        // 上面
        glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f,  1.0f, -1.0f);
        glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,  1.0f,  1.0f);
        glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f,  1.0f,  1.0f);
        glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f,  1.0f, -1.0f);
    glEnd();								// 结束绘制四边形
//然后告诉OpenGL第二个显示列表建立完毕。
glEndList();					            // 第二个显示列表创建完毕

}

//贴图纹理的代码和之前教程里的代码是一样的。需要一个可以贴在立方体上的纹理。
//使用mipmapping处理让纹理看上去光滑,因为我讨厌看见像素点。纹理的文件名是“cube.bmp”。
void MyGLWidget::loadTexture()
{
QImage image(":/image/Cube.bmp");
image = image.convertToFormat(QImage::Format_RGB888);
image = image.mirrored();
glGenTextures(1, &m_texture[0]); // 创建纹理
glBindTexture(GL_TEXTURE_2D, m_texture[0]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
image.width(), image.height(), 0, GL_RGB, GL_UNSIGNED_BYTE,
image.bits());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

}

posted @ 2023-08-24 11:33  铁木2023  阅读(48)  评论(0)    收藏  举报