1. 程式人生 > >OpenGL_Qt學習筆記之_07(閃爍的星星)

OpenGL_Qt學習筆記之_07(閃爍的星星)

     這一小節我們要完成的任務是:將一張背景是黑色,中間是白亮色的星星圖片和五顏六色的顏色進行色彩融合,變成一顆彩色的星星。並且讓這些星星自轉和公轉,可以控制自轉和公轉的速度,另外也能控制所有的星星是否能夠閃爍。

     實驗基礎

  色彩融合知識:

      剛開始提到由黑白的星星變成彩色的星星用到的是色彩融合的知識,關於色彩融合,在上篇文章OpenGL_Qt學習筆記之_06(紋理濾波和光照)已經有了個簡單的介紹,不過該知識點並沒有想象中的那麼簡單。當我們有了一些簡單的色彩混合知識後,可以通過下面這個函式來更一步加深我們的理解。

  void glColor4f(GLfloat red,GLfloat green, GLfloat blue,GLfloat alpha);

  這裡以該引數為例glColor4f(0.1, 0.3, 0.2, 0.5);

  該函式是設定後面出現畫素的顏色資訊的,前面3個引數為rgb分量的比例,加入後面我們繪製某個點時,對應的分量都要乘以該比例值。比如,新來的畫素值pix(0.2, 0.5, 0.3),其實它的畫素值只有(0.02, 0.15, 0.05),因為各自乘了0.1, 0.3, 0.2.

     下面來看看glColor4f()函式alpha引數的作用,如果我們不開啟色彩混合,即沒有使用glEnable(GL_BLEND);語句的話,那麼該引數沒有什麼作用。既然我們這裡討論該引數的作用,當然是假設已經啟用了色彩融合了,另外這裡順便還假設下環境中沒有開啟任何光源。在開啟了色彩融合,且設定色彩融合的源因子為GL_SRC_ALPHA後,這裡假設使用了下面的程式碼:glBlendFunc(GL_SRC_ALPHA, GL_ONE);則我們上面新來的畫素還要考慮它的透明度(該例子為50%),所以pix的實際值是為(0.01, 0.075, 0.025),因為還要都乘以0.5. 並且該畫素的alpha因子為0.5。

     簡單的說,用了glColor4f函式且開啟了色彩混合後,新來的畫素值先轉換為被glColor4f作用過後的值,然後與該位置以前的畫素值進行疊加(即混合),疊加的方式需要參照glBlendFunc()的引數來設定。

     星星閃爍的原理:

     看到的閃爍的星星,其閃爍效果是怎樣形成的呢?NeHe採用的方法是不管是否採用閃爍模式,繪製出來的星星都會自轉,即圍繞自己的中心旋轉,如果啟動了閃爍模式的話,則在自轉的星星上面繼續繪製一個不轉的星星,這2者疊加到一起看起來就像在閃爍了。因為星星的中間亮,周圍暗,因此一個轉一個不轉就會有這種效果(自己可以跑下程式試一試就會明白)。

      星星的旋轉原理:

      該程式中星星繞x軸旋轉是為了方便在螢幕上顯示給使用者看的;繞y軸旋轉完成的是公轉;繞z軸旋轉完成的是自轉.

  注意在opengl中,glRotatef()移動的都是座標系這個整體。我們有必要更一步認識該函式:

  glRotatef(GLfloat angle, GLfloat x, GLfloat y, GLfloat z);

  引數1是旋轉的角度,這個不用多解釋;引數2,3,4組合在空間構成一個三維向量,然後我們用右手法則,握住這個向量並且使大拇指的方向朝向該向量的方向,則另外4指的方向就是旋轉的方向了。如果angle固定不變,那麼不管視窗是否在重新整理,該座標只會旋轉一次(也可以理解為旋轉多次,只不過每次的轉過的角度不變,感覺就像沒有旋轉一樣),要讓物體有旋轉的感覺就必須不斷的增加或者減小angle的值。

  如果我們同時有幾個glRotatef()函式被執行,則後面那個函式旋轉的向量座標系是在前面那個已經旋轉好了的座標系下進行的,並不是一直使用開始固定的那個絕對座標系的。總的來說,該函式旋轉的整個座標系,一旦旋轉過,則後面的步驟都是在這個旋轉過的座標系下進行的。

  在NeHe的程式中,完成星星公轉的程式碼是先繞x軸旋轉,在繞y軸旋轉,然後移動距離,接著又繞y軸反向旋轉相同大小,繼續繞x軸方向旋轉相同角度,最後才開始繪製星星紋理。這樣做的目的就是為了讓星星公轉的時候是在螢幕上進行的。如果我們不採用這種座標抵消的方法,其實根本就不用旋轉座標軸,直接用三角函式計算好星星在螢幕中公轉圓周上的座標值,然後紋理對映的時候應用這些座標值即可。這裡NeHe採用的是空間座標軸旋轉,很巧妙的避開了數學計算這一過程。

  那麼有些朋友可能會問,沿x軸和沿y軸旋轉過後,開沒開始繪畫星星圖樣時又沿它們的反方向旋轉回去,這樣豈不是相當於沒轉一樣,是多餘的。真的是這樣的嗎?顯然不是,大家可以用個物體,比如一張撲克牌動手實驗一下就知道了,這裡也不好畫很多示意圖解釋。反正那些步驟不是多餘的原因是因為在反向旋轉抵消角度之前已經向x軸方向移動了dist的距離(x軸是指相對上一次旋轉過後的座標軸x),所以才會到達公轉的目的。如果中間沒有移動距離的實現,直接方向旋轉抵消x軸和y軸的值,那麼那些程式碼確實是多餘的。

   公轉理解了,自轉就更容易理解了,這裡不再做過多解釋。

  實驗說明:

  按鍵T是用來控制是否開啟星星閃爍功能;按鍵B是用來控制是否開啟色彩融合功能;PageUp鍵用來控制物體走向遠處的距離,相反PageDown用來控制物體走向觀察者的距離;向上的方向鍵和向下的方向鍵用來控制星星公轉螢幕和螢幕的夾角(預設值2者是重合的);當然了F1鍵用來控制全屏切換,Ese鍵是退出程式鍵。

  開發環境:Windows+Qt4.8.2+QtCreator2.5.1

  實驗結果:

  不開啟色彩融合的效果:

  

  開啟色彩融合,不開啟閃爍功能的效果:

  

  開始閃爍功能的效果:

  

  開啟閃爍功能動態效果(qq截圖的,感覺有點失真):

  

實驗主要部分程式碼及註釋(附錄有實驗工程code下載地址):

複製程式碼
#include "glwidget.h"
#include "ui_glwidget.h"

#include <QtGui>
#include <QtCore>
#include <QtOpenGL>
#include <glut>

/*c++中可以在類的外部定義變數*/
GLfloat light_ambient[4]={0.5, 0.5, 0.5, 1.0};
GLfloat light_diffuse[4]={1.0, 1.0, 1.0, 1.0};
GLfloat light_position[4]={0.0, 0.0, 2.0, 0.0};

GLWidget::GLWidget(QGLWidget *parent) :
    QGLWidget(parent),
    ui(new Ui::GLWidget)
{
  //  setCaption("The Opengl for Qt Framework");
    ui->setupUi(this);
    fullscreen = false;
    rotate_angle = 0.0;
    zoom = -15.0;
    title = 90.0;
    spin = 0.1;
    loop = 0;
    twinkle = false;
    blend = false;
 //   timer = new QTimer(this);
  //  connect(timer, SIGNAL(timeout()), this, SLOT(timerEvent()));
    startTimer(5);//開啟5ms定時器
 //   timer->start(50);
}

//這是對虛擬函式,這裡是重寫該函式
void GLWidget::initializeGL()
{
    setGeometry(300, 150, 500, 500);//設定視窗初始位置和大小
    loadTextures();
    glEnable(GL_TEXTURE_2D);//允許採用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);//進行透視校正

    glBlendFunc(GL_SRC_ALPHA, GL_ONE);//源畫素因子採用alpha通道值,目標畫素因子採用1.0
    glEnable(GL_BLEND);

    /*為num個星星的陣列結構體賦初值*/
    for(loop = 0; loop < num; loop++)
        {
            star[loop].angle = 0.0;
            star[loop].dist = (float(loop)/num)*5.0;//星星的離中心的距離越來越遠,最大距離為5,接近螢幕的距離
            star[loop].r = rand()%256;
            star[loop].g = rand()%256;
            star[loop].b = rand()%256;
        }
}

void GLWidget::paintGL()
{
    //glClear()函式在這裡就是對initializeGL()函式中設定的顏色和快取深度等起作用
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glBindTexture(GL_TEXTURE_2D, texture[0]);//繫結紋理目標
    for(loop = 0; loop < num; loop++)
        {
            glLoadIdentity();
            glTranslatef(0.0, 0.0, zoom);//移向螢幕裡面
            glRotatef(title, 1.0, 0.0, 0.0);//沿著x軸旋轉了tilt度
            glRotatef(star[loop].angle, 0.0, 1.0, 0.0);//每個星星沿著y軸旋轉自己的角度
            glTranslatef(star[loop].dist, 0.0, 0.0);
            glRotatef(-star[loop].angle, 0.0, 1.0, 0.0);//將沿著y軸旋轉的角度又轉回去
            glRotatef(-title, 1.0, 0.0, 0.0);//將沿著x軸旋轉過的角度也轉回去
            if(twinkle)//如果星星閃爍的話
                {
                    glColor4ub(star[num-loop-1].r, star[num-loop-1].g, star[num-loop-1].b, 255);//採用的是對稱一頭那邊的星星的顏色
                    //將星星的紋理貼到一個小矩形上
                    glBegin(GL_QUADS);
                    glTexCoord2f(0.0, 0.0);
                    glVertex3f(-1.0, -1.0, 0.0);
                    glTexCoord2f(1.0, 0.0);
                    glVertex3f(1.0, -1.0, 0.0);
                    glTexCoord2f(1.0, 1.0);
                    glVertex3f(1.0, 1.0, 0.0);
                    glTexCoord2f(0.0, 1.0);
                    glVertex3f(-1.0, 1.0, 0.0);
                    glEnd();
                }
            /*不閃爍時*/
            glRotatef(spin, 0.0, 0.0, 1.0);//星星沿z軸自轉spin度
            glColor4ub(star[loop].r, star[loop].g, star[loop].b, 255);//採用的是自己的顏色,完全不透明
            glBegin(GL_QUADS);
            glTexCoord2f(0.0, 0.0);
            glVertex3f(-1.0, -1.0, 0.0);
            glTexCoord2f(1.0, 0.0);
            glVertex3f(1.0, -1.0, 0.0);
            glTexCoord2f(1.0, 1.0);
            glVertex3f(1.0, 1.0, 0.0);
            glTexCoord2f(0.0, 1.0);
            glVertex3f(-1.0, 1.0, 0.0);
            glEnd();

            spin += 0.01f;
            star[loop].angle += float(loop)/num;//角度是在慢慢增大的
            star[loop].dist -= 0.01f;//距離慢慢減小,被吸向了螢幕中心
            if(star[loop].dist < 0)
                {
                    star[loop].dist += 5.0f;
                    star[loop].r = rand()%256;
                    star[loop].g = rand()%256;
                    star[loop].b = rand()%256;
                }
        }
}

//該程式是設定opengl場景透檢視,程式中至少被執行一次(程式啟動時).
void GLWidget::resizeGL(int width, int height)
{
    if(0 == height)
        height = 1;//防止一條邊為0
    glViewport(0, 0, (GLint)width, (GLint)height);//重置當前視口,本身不是重置視窗的,只不過是這裡被Qt給封裝好了
    glMatrixMode(GL_PROJECTION);//選擇投影矩陣
    glLoadIdentity();//重置選擇好的投影矩陣
    gluPerspective(45.0, (GLfloat)width/(GLfloat)height, 0.1, 100.0);//建立透視投影矩陣
    glColor4f(1.0f, 1.0f, 1.0f, 0.5f);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE);
    glMatrixMode(GL_MODELVIEW);//以下2句和上面出現的解釋一樣
    glLoadIdentity();

}
void GLWidget::keyPressEvent(QKeyEvent *e)
{
    switch(e->key())
    {
        /*PageUp鍵為將木箱移到螢幕內部方向*/
        case Qt::Key_T:
        twinkle = !twinkle;
        updateGL();
        break;
        /*B鍵位選擇是否採用色彩融合*/
        case Qt::Key_B:
            blend = !blend;
            if(blend)
                {
                    glEnable(GL_BLEND);//色彩融合和深度快取不能同時開啟
                    glDisable(GL_DEPTH_TEST);
                }
            else
                {
                    glDisable(GL_BLEND);
                    glEnable(GL_DEPTH_TEST);
                }
            updateGL();
            break;
        /*PageUp鍵為將木箱移到螢幕內部方向*/
        case Qt::Key_PageUp:
            zoom -= 0.2;
            updateGL();
            break;
        /*PageDown鍵為將木箱移到螢幕外部方向*/
        case Qt::Key_PageDown:
            zoom += 0.2;
            updateGL();
            break;
        /*Up鍵為加快立方體旋轉的速度*/
        case Qt::Key_Up:
            title += 0.5;
            updateGL();
            break;
        /*Down鍵為減慢立方體旋轉的速度*/
        case Qt::Key_Down:
            title -= 0.5;
            updateGL();
            break;
        /*F1鍵為全屏和普通屏顯示切換鍵*/
        case Qt::Key_F1:
            fullscreen = !fullscreen;
            if(fullscreen)
                showFullScreen();
            else
            {
                setGeometry(300, 150, 500, 500);
                showNormal();
            }
            updateGL();
            break;
        /*Ese為退出程式鍵*/
        case Qt::Key_Escape:
            close();
    }
}

/*裝載紋理*/
void GLWidget::loadTextures()
{
    QImage tex, buf;
    if(!buf.load("./Particle.bmp"))//這個時候因為debug沒有在外面,所以圖片資料夾就是本目錄了
   // if(!buf.load("../opengl_qt_nehe_07/crate.bmp"))
    {
        qWarning("Cannot open the image...");
        QImage dummy(128, 128, QImage::Format_RGB32);//當沒找到所需開啟的圖片時,建立一副128*128大小,深度為32位的點陣圖
        dummy.fill(Qt::green);
        buf = dummy;
    }
    tex = convertToGLFormat(buf);//將Qt圖片的格式buf轉換成opengl的圖片格式tex
    glGenTextures(1, &texture[0]);//開闢3個紋理記憶體,索引指向texture[0]

    /*建立第一個紋理*/
    glBindTexture(GL_TEXTURE_2D, texture[0]);//將建立的紋理記憶體指向的內容繫結到紋理物件GL_TEXTURE_2D上,經過這句程式碼後,以後對
                                            //GL_TEXTURE_2D的操作的任何操作都同時對應與它所繫結的紋理物件
    glTexImage2D(GL_TEXTURE_2D, 0, 3, tex.width(), tex.height(), 0, GL_RGBA, GL_UNSIGNED_BYTE, tex.bits());//開始真正建立紋理資料
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);//當所顯示的紋理比載入進來的紋理小時,採用GL_NEAREST的方法來處理
                                                                      //GL_NEAREST方式速度非常快,因為它不是真正的濾波,所以佔用記憶體非常
                                                                      // 小,速度就快了
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);//當所顯示的紋理比載入進來的紋理大時,採用GL_NEAREST的方法來處理

}

//void GLWidget::timerEvent()
//{
//    spin += 0.01f;
//    for(loop = 0; loop < num; loop++)
//        {
//            star[loop].angle += float(loop)/num;
//            star[loop].dist -= 0.01f;
//            if(star[loop].dist < 0)
//                {
//                    star[loop].dist += 5.0f;
//                    star[loop].r = rand()%256;
//                    star[loop].g = rand()%256;
//                    star[loop].b = rand()%256;
//                }
//        }
//    updateGL();//執行改句就是重新整理視窗了,其實內部呼叫的是paintGL()函式
//}

void GLWidget::timerEvent(QTimerEvent *)
{
    updateGL();
}

GLWidget::~GLWidget()
{
    delete ui;
}
複製程式碼

  總結:本次實驗是對前面知識的一個小小的綜合,需要對色彩融合知識比較深入的理解,特別是深度資訊在對繪圖先後順序的影響;另外理解星星公轉和閃爍的實現方法是本程式的關鍵之處。通過本實驗,可以簡單的實現一些物體在3D空間中的移動。