OpenGL(十五)——Qt OpenGL三种不同的纹理滤波方式、光照、物体的移动

20 篇文章 72 订阅
13 篇文章 51 订阅

OpenGL(十五)——Qt OpenGL三种不同的纹理滤波方式、光照、物体的移动

 

一、介绍

本篇分享

1、如何使用三种不同的纹理滤波方式。
2、如何使用键盘来移动场景中的对象,
3、如何在OpenGL场景中应用简单的光照。
这一篇包含了很多内容,如果您对前面的文章程有疑问的话,
先回头复习一下。进入后面的代码之前,很好的理解基础知识十分重要。

二、纹理滤波的方式

从之前的文章中我们知道了,当三维空间里面的多边形经过坐标变换、投影、光栅化等过程,变成二维屏幕上的一组象素的时候,对每个象素需要到相应纹理图像中进行采样,这个过程就称为纹理过滤。

采样:

注:使用纹理坐标获取纹理颜色叫做采样(Sampling)

纹理过滤通常分为2种情况:
1、纹理被缩小(GL_TEXTURE_MIN_FILTER)
比如说一个8 x 8的纹理贴到一个平行于xy平面的正方形上,最后该正方形在屏幕上只占4 x 4的象素矩阵,这种情况下一个象素对应着多个纹理单元。
2、纹理被放大( GL_TEXTURE_MAG_FILTER)
纹理被放大这种情况刚好跟上面相反,假如我们放大该正方形,最后正方形在屏幕上占了一个16 x 16的象素矩阵,这样就变成一个纹理单元对应着多个象素。

纹理滤波一般有四种方式:

1、最近点采样 GL_NEAREST
2、线性纹理过滤(双线性过滤)GL_LINEAR
3、mipmap纹理过滤(三线性过滤) GL_LINEAR_MIPMAP_LINEAR
4、各向异性过滤

三、四种滤波的方式

1、最近点采样 GL_NEAREST (Nearest Point Sampling)

每个像素的纹理坐标和图形的坐标不是刚好对上的时候,就会采取最近采样点的过滤方式。这种方式这个时候效果最好,怎么理解呢,当两者坐标不是严丝合缝的时候,就相当于四舍五入,比如(88.267,89.123)变为(88,89)。

当纹理大小与贴图的大小差不多的时候,这种滤波方式最好。如果差别较大的时候,纹理就需要放大和缩小了。

2、线性纹理过滤(双线性过滤)GL_LINEAR

双线性滤波是进行缩放显示的时候进行纹理平滑的一种纹理过滤方法。在大多数情况下,纹理在屏幕上显示的时候都不会同保存的纹理一模一样,没有任何失真。正是因为这样,所以一些像素要使用纹素(texel,纹理元素)之间的点进行表示,这里假设纹素都是位于各个单元中心或者坐上或者其它位置的点。双线性过滤就是利用这些点在像素所表示的周围四个最近的点之间进行双线性插值。

双线性滤波的局限性:在纹理缩减到一半或者放大一倍的范围内,双线性过滤都能有非常好的精度。也就是说,如果纹理在每个方向都有256个像素,那么将它缩减到128以下或者放大到512以上的时候,由于会丢掉太多的像素或者进行了过多的平滑处理,纹理看起来就会很差。

3、mipmap纹理过滤(三线性过滤)GL_LINEAR_MIPMAP_LINEAR

三线性过滤(Trilinear Filtering)就是用来减轻或消除不同组合等级纹理过渡时出现的组合交叠现象。三线性过滤是以双线性过滤为基础,会对pixel大小与texel大小最接近的两层MipMap level分别进行双线性过滤,也就是必须计算2x4=8个像素的值,然后再对两层得到的结果进行线性插值。

它必须结合双线性过滤和组合式处理映射一并使用。

例如有一张材质影像是512×512个图素,第二张就会是256×256个图素,第三张就会是128×128个图素等等,总之最小的一张是1×1。凭借这些多重解析度的材质影像,当遇到景深极大的场景时(如飞行模拟),就能提供高品质的贴图效果。一个“双线过滤”需要三次混合,而“三线过滤”就得作七次混合处理,所以每个像素就需要多用21/3倍以上的计算时间。还需要两倍大的存储器时钟带宽。但是“三线过滤”可以提供最高的贴图品质,会去除材质的“闪烁”效果。对于需要动态物体或景深很大的场景应用方面而言,只有“三线过滤”才能提供可接受的材质品质。

4、各向异性过滤(Anisotropic Filtering)

各向异性过滤是用来过滤、处理当视角变化导致3D物体表面倾斜时造成的纹理错误。传统的双线性额三线性过滤技术都是指“Isotropy”(各向同性)的,其各方向上矢量值都是一致的,就像正方形和正方体。

原理:三线性过滤原理同双线性过滤一样,都是将相邻像素及彼此之间的相对关系都记忆下来,然后在视角改变的时候绘制出来。只不过三线性过滤的采集范围更大,计算更精确,画面更细腻。
当然占用资源也更多。Anisotropic Filt技术的过滤单元并不是“四四方方”的,其典型单元是矩形,还可以变形为梯形和平行四边形。画面上的一个像素,在一个方向上可以包含不同纹理单元的信息。这就需要一个“非正多边形”的过滤单元,来保证准确的透视关系和透明度。不然,如果在某个轴上的纹理部分有大量信息,或是某个方向上的图象和纹理有个倾角,那么得到的最终纹理就会变得很滑稽,比例也会失调。当视角为90度,或是处理物体边缘纹理时,情况会更糟。

各向异性过滤是最新型的过滤方法(相对各向同性2/3线性过滤),它需要对映射点周围方形8个或更多的像素进行取样,获得平均值后映射到像素点上。对于许多3D加速卡来说,采用8个以上像素取样的各向异性过滤几乎是不可能的,因为它比三线性过滤需要更多的像素填充率。但是对于3D游戏来说,各向异性过滤则是很重要的一个功能,因为它可以使画面更加逼真,自然处理起来也比三线性过滤会更慢。

如下展示了在经典的3D隧道动画中,运用各向异性过滤的前后效果对比。可以看到,没有进行各向异性过滤时,隧道远方比较模糊;添加各向异性过滤后,砖块之间的灰泥等细节清晰可见。

原图,没有进行各向异性过滤:(图片来源于百度百科)

进行了各向异性过滤:(图片来源于百度百科)

 四、光源

光源通常可以分为以下几种:

环境光(Ambient light):环境光看上去来自四面八方,场景中的一切被照亮的程度都一样。这近似于我们从大的、平等的光源获取的光照,比如天空。


方向光(Directional light):方向光看上去似乎来自一个方向,光源好像处于极其远的地方。这与我们从太阳或月亮获取的光照相似。


点光(Point light):点光看上去是从附近某处投射的光亮,而且光的密度随着距离而减少。这适于表示近处的光源,其把它们的光投射到四面八方,像一个灯泡或蜡烛一样。


聚光(Spot light):聚光与点光类似,只是加了一个限制,只能向一个特定的方向投射。这是我们从手电筒或者聚光灯所获得的光照类型。

光线在物体表面反射的方式分为两类:

漫反射(Diffuse reflection):漫反射是指光线平等地向所有方向蔓延,适于表示没有抛光表面的材质,比如地毯或外面的混凝土墙。


镜面反射( Specular reflection):镜面反射在某个特定的方向上反射更加强烈,适于被抛光的或者闪亮的材质,比如光滑的金属或者刚刚打过蜡的汽车。

五、下面开始我们的代码之旅

头文件:

#include <QObject>
#include <QWidget>
#include <qgl.h>
#include <QTimer>
#include <QKeyEvent>
/*
*绘制纹理二.
*/

class NeHe_7_Widget : public QGLWidget
{
    Q_OBJECT
public:
    NeHe_7_Widget(QWidget *parent = 0);
    ~NeHe_7_Widget();

public slots:
    void slotTimer();

protected:
    void initializeGL(); 
    void paintGL();      
    void resizeGL( int width, int height ); 

    void loadGLTextures(); //在这个函数中我们会载入指定的图片并生成相应当纹理。

    void keyPressEvent( QKeyEvent *e );

    GLfloat xRot, yRot, zRot ; //xRot、yRot、zRot来处理立方体在三个方向上的旋转。
    //GLuint texture[6];
    GLfloat zoom;   //zoom是场景深入屏幕的距离。
    GLfloat xSpeed, ySpeed; //xSpeed和ySpeed是立方体在X轴和Y轴上旋转的速度。
    GLuint texture[3]; //texture[3]用来存储三个纹理。
    GLuint filter; //filter表明的是使用哪个纹理。

    bool light;   //light是说明现在是否使用光源。
private:
    QTimer* m_timer;

    GLfloat lightAmbient[4] = { 0.5, 0.5, 0.5, 1.0 };
    GLfloat lightDiffuse[4] = { 1.0, 1.0, 1.0, 1.0 };
    GLfloat lightPosition[4] = { 0.0, 0.0, 2.0, 1.0 };
};

cpp文件:

这一章展示如何使用三种不同的纹理滤波方式。如何使用键盘来移动场景中的对象,
还会在OpenGL场景中应用简单的光照。这一课包含了很多内容,如果您对前面的课程有疑问的话,先回头复习一下。进入后面的代码之前,很好的理解基础知识十分重要。

我们要在第一课的代码上进行改动就可以了。

我们将要增加一个loadGLTextures()函数来处理有关纹理操作的。我们将增加一些变量,稍后我们对这些变量进行解释。

说明:

//GLfloat lightAmbient[4] = { 0.5, 0.5, 0.5, 1.0 };
//GLfloat lightDiffuse[4] = { 1.0, 1.0, 1.0, 1.0 };
//GLfloat lightPosition[4] = { 0.0, 0.0, 2.0, 1.0 };

//这里定义了三个数组,它们描述的是和光源有关的信息。
//我们将使用两种不同的光。第一种称为环境光。环境光来自于四面八方。所有场景中的对象都处于环境光的照射中。


第二种类型的光源叫做漫射光。漫射光由特定的光源产生,并在您的场景中的对象表面上产生反射。处于漫射光直接照。


射下的任何对象表面都变得很亮,而几乎未被照射到的区域就显得要暗一些。这样在我们所创建的木板箱的棱边上就会产生的很不错的阴影效果。

//创建光源的过程和颜色的创建完全一致。前三个参数分别是RGB三色分量,最后一个是alpha通道参数。
//因此,第一行有关lightAmbient的代码使我们得到的是半亮(0.5)的白色环境光。如果没有环境光,未被漫射光照到的地方会变得十分黑暗。


//第二行有关lightDiffuse的代码使我们生成最亮的漫射光。所有的参数值都取成最大值1.0。它将照在我们木板箱的前面,看起来挺好。


//第三行有关lightPosition的代码使我们保存光源的位置。前三个参数和glTranslate中的一样。依次分别是XYZ轴上的位移。由于我们想要光线直接照射在木箱的正面,所以XY轴上的位移都是0.0。第三个值是Z轴上的位移。为了保证光线总在木箱的前面,所以我们将光源的位置朝着观察者(就是您哪。)挪出屏幕。我们通常将屏幕也就是显示器的屏幕玻璃所处的位置称作Z轴的0.0点。所以Z轴上的位移最后定为2.0。假如您能够看见光源的话,它就浮在您显示器的前方。当然,如果木箱不在显示器的屏幕玻璃后面的话,您也无法看见箱子。最后一个参数取为1.0。这将告诉OpenGL这里指定的坐标就是光源的位置,以后的教程中我会多加解释。

NeHe_7_Widget::NeHe_7_Widget(QWidget *parent):QGLWidget(parent)
{
    xRot = yRot = zRot = 0.0;

    zoom = -5.0;
    xSpeed = ySpeed = 0.0;

    filter = 0;

    light = false;

    setGeometry( 100, 200, 640, 480 );
    //setCaption( "NeHe's Texture Mapping Tutorial" );

    m_timer = new QTimer(this);
    m_timer->setInterval(50);

}

NeHe_7_Widget::~NeHe_7_Widget()
{
}

void NeHe_7_Widget::initializeGL()
{

    loadGLTextures();
    //载入纹理.
    glEnable( GL_TEXTURE_2D );
    //启用纹理。如果没有启用的话,你的对象看起来永远都是纯白色

    glShadeModel( GL_SMOOTH );
    glClearColor( 0.0, 0.0, 0.0, 0.5 );
    glClearDepth( 1.0 );
    glEnable( GL_DEPTH_TEST );
    glDepthFunc( GL_LEQUAL );
    //所作深度测试的类型。

    glHint( GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST );
    //真正精细的透视修正。这一行告诉OpenGL我们希望进行最好的透视修正。这会十分轻微的影响性能。但使得透视图看起来好一点。

    glLightfv( GL_LIGHT1, GL_AMBIENT, lightAmbient );
    glLightfv( GL_LIGHT1, GL_DIFFUSE, lightDiffuse );
    glLightfv( GL_LIGHT1, GL_POSITION, lightPosition );
    glEnable( GL_LIGHT1 );

}

 这里开始设置光源。第一行设置环境光的发光量,光源GL_LIGHT1开始发光。这一课的开始处我们我将环境光的发光量存放在lightAmbient数组中。现在我们就使用此数组(半亮度环境光)。

接下来我们设置漫射光的发光量。它存放在lightDiffuse数组中(全亮度白光)。

然后设置光源的位置。位置存放在lightPosition 数组中(正好位于木箱前面的中心,X-0.0,Y-0.0,Z方向移向观察者2个单位,位于屏幕外面)。

最后,我们启用一号光源。我们还没有启用GL_LIGHTING,所以您看不见任何光线。记住:只对光源进行设置、定位、甚至启用,光源都不会工作。除非我们启用GL_LIGHTING。

void NeHe_7_Widget::resizeGL( int width, int height )
{
    if ( height == 0 )
    {
      height = 1;
    }
    //防止height为0。

    glViewport( 0, 0, (GLint)width, (GLint)height );
    //重置当前的视口(Viewport)。

    glMatrixMode( GL_PROJECTION );
    //选择投影矩阵。

    glLoadIdentity();
    //重置投影矩阵。

    gluPerspective( 45.0, (GLfloat)width/(GLfloat)height, 0.1, 100.0 );
    //建立透视投影矩阵。

    glMatrixMode( GL_MODELVIEW );
    //选择模型观察矩阵。

    glLoadIdentity();
    //重置模型观察矩阵。
   
}

void NeHe_7_Widget::paintGL()
{
    glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
    //清楚屏幕和深度缓存。
    glLoadIdentity();
    //重置当前的模型观察矩阵。

    glTranslatef(  0.0,  1.0, zoom);

    glRotatef( xRot,  1.0,  0.0,  0.0 );
    glRotatef( yRot,  0.0,  1.0,  0.0 );
    //glRotatef( zRot,  0.0,  0.0,  1.0 );
    //根据xRot、yRot、zRot的实际值来旋转正方体。

    glBindTexture( GL_TEXTURE_2D, texture[filter] );


    glBegin( GL_QUADS );
      glNormal3f( 0.0, 0.0, 1.0 );
      glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0,  1.0 );
      glTexCoord2f( 1.0, 0.0 ); glVertex3f(  1.0, -1.0,  1.0 );
      glTexCoord2f( 1.0, 1.0 ); glVertex3f(  1.0,  1.0,  1.0 );
      glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0,  1.0,  1.0 );

      glNormal3f( 0.0, 0.0, -1.0 );
      glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0, -1.0 );
      glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0,  1.0, -1.0 );
      glTexCoord2f( 0.0, 1.0 ); glVertex3f(  1.0,  1.0, -1.0 );
      glTexCoord2f( 0.0, 0.0 ); glVertex3f(  1.0, -1.0, -1.0 );

      glNormal3f( 0.0, 1.0, 0.0 );
      glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0,  1.0, -1.0 );
      glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0,  1.0,  1.0 );
      glTexCoord2f( 1.0, 0.0 ); glVertex3f(  1.0,  1.0,  1.0 );
      glTexCoord2f( 1.0, 1.0 ); glVertex3f(  1.0,  1.0, -1.0 );

      glNormal3f( 0.0, -1.0, 0.0 );
      glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0, -1.0, -1.0 );
      glTexCoord2f( 0.0, 1.0 ); glVertex3f(  1.0, -1.0, -1.0 );
      glTexCoord2f( 0.0, 0.0 ); glVertex3f(  1.0, -1.0,  1.0 );
      glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0,  1.0 );

      glNormal3f( 1.0, 0.0, 0.0 );
      glTexCoord2f( 1.0, 0.0 ); glVertex3f(  1.0, -1.0, -1.0 );
      glTexCoord2f( 1.0, 1.0 ); glVertex3f(  1.0,  1.0, -1.0 );
      glTexCoord2f( 0.0, 1.0 ); glVertex3f(  1.0,  1.0,  1.0 );
      glTexCoord2f( 0.0, 0.0 ); glVertex3f(  1.0, -1.0,  1.0 );

      glNormal3f( -1.0, 0.0, 0.0 );
      glTexCoord2f( 0.0, 0.0 ); glVertex3f( -1.0, -1.0, -1.0 );
      glTexCoord2f( 1.0, 0.0 ); glVertex3f( -1.0, -1.0,  1.0 );
      glTexCoord2f( 1.0, 1.0 ); glVertex3f( -1.0,  1.0,  1.0 );
      glTexCoord2f( 0.0, 1.0 ); glVertex3f( -1.0,  1.0, -1.0 );
    glEnd();

    xRot += xSpeed;
    yRot += ySpeed;
}
//loadGLTextures()函数就是用来载入纹理的。
void NeHe_7_Widget::loadGLTextures()
{
    QImage tex,buf;

    if (!buf.load( ":/data/Crate.bmp" ))
    {
        qWarning( "Could not read image file, using single-color instead." );
        QImage dummy( 128, 128, QImage::Format_RGB32 );
        dummy.fill( Qt::green );
        buf = dummy;
        //如果载入不成功,自动生成一个128*128的32位色的绿色图片。
    }

    tex = QGLWidget::convertToGLFormat(buf);

    glGenTextures( 3, &texture[0] );

    glBindTexture( GL_TEXTURE_2D, texture[0] );

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
    glTexImage2D( GL_TEXTURE_2D, 0, 3, tex.width(), tex.height(), 0,
                  GL_RGBA, GL_UNSIGNED_BYTE, tex.bits() );

    //第六课中我们使用了线性滤波的纹理贴图。这需要机器有相当高的处理能力,但它们看起来很不错。\
    这一课中,我们接着要创建的第一种纹理使用GL_NEAREST方式。从原理上讲,这种方式没有真正进行滤波。\
    它只占用很小的处理能力,看起来也很差。唯一的好处是这样我们的工程在很快和很慢的机器上都可以正常运行。\
    您会注意到我们在MIN和MAG时都采用了GL_NEAREST,你可以混合使用GL_NEAREST和GL_LINEAR。纹理看起\
    来效果会好些,但我们更关心速度,所以全采用低质量贴图。MIN_FILTER在图像绘制时小于贴图的原始尺寸时\
    采用。MAG_FILTER在图像绘制时大于贴图的原始尺寸时采用。

    glBindTexture( GL_TEXTURE_2D, texture[1] );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexImage2D( GL_TEXTURE_2D, 0, 3, tex.width(), tex.height(), 0,
                  GL_RGBA, GL_UNSIGNED_BYTE, tex.bits() );
    //这个纹理与第六课的相同,线性滤波。唯一的不同是这次放在了texture[1]中。因为这是第二个纹理。\
    如果放在texture[0]中的话,它将覆盖前面创建的GL_NEAREST纹理。

    glBindTexture( GL_TEXTURE_2D, texture[2] );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST );
    gluBuild2DMipmaps( GL_TEXTURE_2D, GL_RGB, tex.width(), tex.height(), GL_RGBA,\
                       GL_UNSIGNED_BYTE, tex.bits() );
    //这里是创建纹理的新方法。Mipmapping!您可能会注意到当图像在屏幕上变得很小的时候,很多细节将会丢失。\
    刚才还很不错的图案变得很难看。当您告诉OpenGL创建一个 mipmapped的纹理后,OpenGL将尝试创建不同尺寸的\
    高质量纹理。当您向屏幕绘制一个mipmapped纹理的时候,OpenGL将选择它已经创建的外观最佳的纹理(带有更多细节)\
    来绘制,而不仅仅是缩放原先的图像(这将导致细节丢失)。

    //我曾经说过有办法可以绕过OpenGL对纹理宽度和高度所加的限制——64、128、256,等等。办法就是\
    gluBuild2DMipmaps。据我的发现,您可以使用任意的位图来创建纹理。OpenGL将自动将它缩放到正常的大小。

    //因为是第三个纹理,我们将它存到texture[2]。这样本课中的三个纹理全都创建好了。


    //这一行生成 mipmapped 纹理。我们使用三种颜色(红,绿,蓝)来生成一个2D纹理。tex.width()是位图宽度,\
    tex.height()是位图高度,extureImage[0]->sizeY 是位图高度,GL_RGBA意味着我们依次使用RGBA色彩。\
    GL_UNSIGNED_BYTE意味着纹理数据的单位是字节。tex.bits()指向我们创建纹理所用的位图。

}

下面是按键事件的实现:

void NeHe_7_Widget::slotTimer()
{
    xRot += 0.3;
    yRot += 0.2;
    zRot += 0.4;
    update();
}

void NeHe_7_Widget::keyPressEvent( QKeyEvent *e )
{
    switch ( e->key() )
    {
    case Qt::Key_L: //按下了L键,就可以切换是否打开光源。
        light = !light;
        if ( !light ){
            glDisable( GL_LIGHTING );
        }else{
            glEnable( GL_LIGHTING );
        }
        updateGL();
        break;

    case Qt::Key_F: //按下了F键,就可以转换一下所使用的纹理(就是变换了纹理滤波方式的纹理)。
      filter += 1;;
      if ( filter > 2 )
      {
        filter = 0;
      }
      updateGL();
      break;

    case Qt::Key_PageUp: //按下了PageUp键,将木箱移向屏幕内部。
      zoom -= 0.2;
      updateGL();
      break;

    case Qt::Key_PageDown: //按下了PageDown键,将木箱移向屏幕外部。
      zoom += 0.2;
      updateGL();
      break;

    case Qt::Key_W: //按下了Up方向键,减少xSpeed。
      xSpeed -= 0.01;
      updateGL();
      break;

    case Qt::Key_S: //按下了Dowm方向键,增加xSpeed。
      xSpeed += 0.01;
      updateGL();
      break;

    case Qt::Key_A: //按下了Right方向键,增加ySpeed。
      ySpeed += 0.01;
      updateGL();
      break;

    case Qt::Key_D: //按下了Left方向键,减少ySpeed。
      ySpeed -= 0.01;
      updateGL();
      break;


    case Qt::Key_Escape:
      close();

    }

}

以上就是加载纹理和滤波,以及光源的使用的代码。下面我把运行效果贴出来。

六、运行

1、运行程序

 2、打开光源和关闭光源:

 通过上图可以看到,光源打开和关闭的效果。

3、F键切换纹理滤波

 通过纹理滤波,可以看到纹理图片的经过滤波后的效果。

4、木箱的移动,朝里和朝外移动

 5、木箱的旋转效果

 经过上面的演示,我们可以看到本章代码所展示的效果,大家可以动手试一下。

本章所需的图片,我放到了百度网盘,大家可以自取。如果链接失效了,大家可以私信我。

链接:https://pan.baidu.com/s/1hRjo-GVOaToRWhaCtGtwZw 
提取码:4mw4

上一篇:OpenGL(十四)——Qt OpenGL纹理

下一篇:OpenGL(十六)——Qt OpenGL融合(将两张图片叠合成一张图片)

本文原创作者:冯一川(ifeng12358@163.com),未经作者授权同意,请勿转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冯一川

谢谢老板对我的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值