1. 程式人生 > >【Qt OpenGL教程】28:貝塞爾曲面

【Qt OpenGL教程】28:貝塞爾曲面

第28課:貝塞爾曲面 (參照NeHe)

這次教程中,我們將介紹貝塞爾曲面,因此這是關於數學運算的一課(這導致很不好講),來吧,相信你能搞定它的!這一課中,我們並不是要做一個完整的貝塞爾曲面庫(庫的話OpenGL已經完成了),而是一個展示概念的程式,來讓你熟悉曲面是怎麼計算實現的。

如果想理解貝塞爾曲面沒有對數學基本的認識是很難的(NeHe原文中對貝塞爾曲線和曲面的介紹並不能讓一個初學者很明白,所以我並不打算照搬過來),所以如果你並不瞭解貝塞爾曲面,我轉載了一篇文章(http://blog.csdn.net/cly116/article/details/47686349)希望能幫到你。我是希望你看完這篇文章,對貝塞爾曲線和曲面有了較系統的瞭解再進入這一課,不然後面涉及數學計算部分你可能會看不懂,而且文章中還介紹了OpenGL中實現的直接用於繪製貝塞爾曲線和曲面的API

。當然,如果你不願意讀這篇文章或者你已經知道了關於它的數學知識你可以不看它,後面我還是多少會解釋一些數學原理的。

程式執行時效果如下:


下面進入教程:

我們這次將在第01課的基礎上修改程式碼,新增程式碼有不少是前面講過的,我就不多解釋了,我們重點要講明白貝塞爾曲面是怎麼繪製的。首先我們增加一個POINT_3D類,來表示一個3D頂點向量,由於比較簡單,我把類宣告和實現直接給大家,不多解釋可以看明白的,具體程式碼如下:

#ifndef POINT_3D_H
#define POINT_3D_H

#include <QWidget>
#include <QGLWidget>

class POINT_3D
{
public:
    POINT_3D();
    POINT_3D(double x, double y, double z);

    double x()const;                                    //x、y、z的access函式
    double y()const;
    double z()const;

    POINT_3D operator +(const POINT_3D &p);             //向量加法
    POINT_3D operator *(double c);                      //向量數乘

private:
    double m_x, m_y ,m_z;                               //3D座標
};

#endif // POINT_3D_H
#include "point_3d.h"

POINT_3D::POINT_3D()
{
    m_x = 0.0;
    m_y = 0.0;
    m_z = 0.0;
}

POINT_3D::POINT_3D(double x, double y, double z)
{
    m_x = x;
    m_y = y;
    m_z = z;
}

double POINT_3D::x() const
{
    return m_x;
}

double POINT_3D::y() const
{
    return m_y;
}

double POINT_3D::z() const
{
    return m_z;
}

POINT_3D POINT_3D::operator +(const POINT_3D &p)        //向量加法
{
    return POINT_3D(m_x + p.m_x, m_y + p.m_y, m_z + p.m_z);
}

POINT_3D POINT_3D::operator *(double c)                 //向量數乘
{
    return POINT_3D(m_x * c, m_y * c, m_z * c);
}

接著我們開啟myglwidget.h檔案,將類宣告更改如下:

#ifndef MYGLWIDGET_H
#define MYGLWIDGET_H

#include "point_3d.h"
#include <QWidget>
#include <QGLWidget>

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

protected:
    //對3個純虛擬函式的重定義
    void initializeGL();
    void resizeGL(int w, int h);
    void paintGL();

    void keyPressEvent(QKeyEvent *event);           //處理鍵盤按下事件

private:
    POINT_3D bernstein(float u, POINT_3D *p);       //計算貝塞爾方程的值
    GLuint genBezier();                             //生成貝塞爾曲面的顯示列表
    void initBezier();                              //初始化貝塞爾曲面

private:
    bool fullscreen;                                //是否全屏顯示
    bool m_ShowCPoints;                             //是否顯示控制點
    GLfloat m_Rot;                                  //旋轉的角度
    int m_Divs;                                     //細分數

    struct BEZIER_PATCH                             //貝塞爾曲面結構體
    {
        POINT_3D anchors[4][4];                     //控制點座標
        GLuint dlBPatch;                            //儲存顯示列表地址
        GLuint texture;                             //儲存繪製的紋理
    } m_Mybezier;                                   //儲存要繪製的貝塞爾曲面資料
};

#endif // MYGLWIDGET_H
我們增加變數m_ShowCPoints來控制是否繪製控制點,m_Rot表示旋轉的角度,m_Divs表示細分數,這裡細分數指的繪製貝塞爾曲面時分多少段來繪製。我們知道,曲線其實是許多段小的連續折線來構成,而我們要繪製曲線也是以這種方式,上面說的細分數也可以說是我們要繪製的折線的段數。當細分數越大時,這曲線就看起來越平滑;細分數越小時,看起來就越曲折,甚至變成直線。

然後我們定義了貝塞爾曲面的結構體,瞭解貝塞爾曲線和曲面的朋友應該知道,只有二次以上的貝塞爾曲線才是我們通常的“曲線”(一次時為直線),但二次的貝塞爾曲線只能向一個方向彎曲(下面有圖),所以我們更喜歡三次的(雖然難度大了,但效果也更好了)。三次的貝塞爾曲線需要4個控制點,所以如果我們要繪製三次的貝塞爾曲面就需要4×4個控制點,因此結構體中anchors為4×4的陣列。還有,dlBPatch和texture分別儲存顯示列表和紋理的記憶體地址。最後三個新增的函式宣告就等定義時一起解釋了。

image

下面,我們開啟myglwidget.cpp,加上宣告#include <QTimer>、#include<QtMath>,我們先來看initBezier()和bernstein()的定義,具體程式碼如下:

void MyGLWidget::initBezier()                           //初始化貝塞爾曲面
{
    //設定貝塞爾曲面的控制點
    m_Mybezier.anchors[0][0] = POINT_3D(-0.75, -0.75, -0.50);
    m_Mybezier.anchors[0][1] = POINT_3D(-0.25, -0.75,  0.00);
    m_Mybezier.anchors[0][2] = POINT_3D( 0.25, -0.75,  0.00);
    m_Mybezier.anchors[0][3] = POINT_3D( 0.75, -0.75, -0.50);
    m_Mybezier.anchors[1][0] = POINT_3D(-0.75, -0.25, -0.75);
    m_Mybezier.anchors[1][1] = POINT_3D(-0.25, -0.25,  0.50);
    m_Mybezier.anchors[1][2] = POINT_3D( 0.25, -0.25,  0.50);
    m_Mybezier.anchors[1][3] = POINT_3D( 0.75, -0.25, -0.75);
    m_Mybezier.anchors[2][0] = POINT_3D(-0.75,  0.25,  0.00);
    m_Mybezier.anchors[2][1] = POINT_3D(-0.25,  0.25, -0.50);
    m_Mybezier.anchors[2][2] = POINT_3D( 0.25,  0.25, -0.50);
    m_Mybezier.anchors[2][3] = POINT_3D( 0.75,  0.25,  0.00);
    m_Mybezier.anchors[3][0] = POINT_3D(-0.75,  0.75, -0.50);
    m_Mybezier.anchors[3][1] = POINT_3D(-0.25,  0.75, -1.00);
    m_Mybezier.anchors[3][2] = POINT_3D( 0.25,  0.75, -1.00);
    m_Mybezier.anchors[3][3] = POINT_3D( 0.75,  0.75, -0.50);

    m_Mybezier.dlBPatch = 0;                            //預設的顯示列表為0
}
POINT_3D MyGLWidget::bernstein(float u, POINT_3D *p)    //計算貝塞爾方程的值
{
    POINT_3D a = p[0] * pow(u, 3);
    POINT_3D b = p[1] * (3*pow(u, 2)*(1-u));
    POINT_3D c = p[2] * (3*u*pow(1-u, 2));
    POINT_3D d = p[3] * pow(1-u, 3);

    POINT_3D r = a + b + c + d;
    return r;
}
先是initBezier()函式,這個函式就是初始化我們定義的貝塞爾曲面結構體的。我們自己挑選一組我們喜歡的控制點,把它們賦值給anchors就行了,最後把dlBPatch賦值為0,表示沒有儲存任何顯示列表的地址。

然後是bernstein函式,這個函式的作用是計算得到當前細分點處的折點座標。對於一次曲線,方程為t + (1-t) = 1,對應的函式為P1*t + P2*(1-t) = P,這裡P1、P2分別為一次曲線(直線)的兩個端點,而P是我們帶入t(細分數)後得到的對應點;而對於三次曲線方程,我們只需要等號兩邊同時立方就可以得到三次曲線的方程了:t^3 + 3*t^2*(1-t) + 3*t*(1-t)^2 + (1-t)^3 = 1,因此對應的函式為P1*t^3 + P2*3*t^2*(1-t) + P3*3*t*(1-t)^2 + P4*(1-t)^3 = P。當然很容易猜到P1、P2、P3和P4就是我們曲線的四個控制點了,P還是我們帶入t(細分數)後得到的對應點。到這裡,想必你能明白bernstein()函式的原理了,引數u 就是我們前面說到的t(要注意必須保證u、t∈[0, 1]),而p就是指向四個控制點的指標了,函式裡面的計算部分就是P1*t^3 + P2*3*t^2*(1-t) + P3*3*t*(1-t)^2 + P4*(1-t)^3 = P的還原了,我就不解釋了。通過這個函式,我們就能把要繪製的貝塞爾曲線,根據細分數來細分成許多段折線,並且得到每個折點的座標了(希望大家理解了)。

下面我們先給出建構函式和解構函式的程式碼,很簡單不解釋了,程式碼如下:

MyGLWidget::MyGLWidget(QWidget *parent) :
    QGLWidget(parent)
{
    fullscreen = false;
    m_ShowCPoints = true;
    m_Rot = 0.0f;
    m_Divs = 7;
    initBezier();

    QTimer *timer = new QTimer(this);                   //建立一個定時器
    //將定時器的計時訊號與updateGL()繫結
    connect(timer, SIGNAL(timeout()), this, SLOT(updateGL()));
    timer->start(10);                                   //以10ms為一個計時週期
}
MyGLWidget::~MyGLWidget()
{
    glDeleteLists(m_Mybezier.dlBPatch, 1);              //刪除顯示列表
}

繼續,我們來定義genBezier()函式,這個函式用於建立繪製貝塞爾曲面的顯示列表,重點所在,具體程式碼如下:

GLuint MyGLWidget::genBezier()                          //生成貝塞爾曲面的顯示列表
{
    GLuint drawlist = glGenLists(1);                    //分配1個顯示列表的空間
    POINT_3D temp[4];
    //根據每一條曲線的細分數,分配相應的記憶體
    POINT_3D *last = (POINT_3D*)malloc(sizeof(POINT_3D)*(m_Divs+1));

    if (m_Mybezier.dlBPatch != 0)                       //如果顯示列表存在,則刪除
    {
        glDeleteLists(m_Mybezier.dlBPatch, 1);
    }

    temp[0] = m_Mybezier.anchors[0][3];                 //獲得u方向的四個控制點
    temp[1] = m_Mybezier.anchors[1][3];
    temp[2] = m_Mybezier.anchors[2][3];
    temp[3] = m_Mybezier.anchors[3][3];

    for (int v=0; v<=m_Divs; v++)                       //根據細分數,建立各個分割點的引數
    {
        float py = ((float)v)/((float)m_Divs);
        last[v] = bernstein(py, temp);                  //使用bernstein函式求得分割點座標
    }

    glNewList(drawlist, GL_COMPILE);                    //繪製一個新的顯示列表
    glBindTexture(GL_TEXTURE_2D, m_Mybezier.texture);   //繫結紋理
    for (int u=1; u<=m_Divs; u++)
    {
        float px = ((float)u)/((float)m_Divs);          //計算v方向上的細分點的引數
        float pxold = ((float)u-1.0f)/((float)m_Divs);  //上一個v方向的細分點的引數

        temp[0] = bernstein(px, m_Mybezier.anchors[0]); //計算每個細分點v方向上貝塞爾曲面的控制點
        temp[1] = bernstein(px, m_Mybezier.anchors[1]);
        temp[2] = bernstein(px, m_Mybezier.anchors[2]);
        temp[3] = bernstein(px, m_Mybezier.anchors[3]);

        glBegin(GL_TRIANGLE_STRIP);                     //開始繪製三角形帶
            for (int v=0; v<=m_Divs; v++)
            {
                float py = ((float)v)/((float)m_Divs);  //沿著u方向順序繪製
                glTexCoord2f(pxold, py);                //設定紋理座標並繪製一個頂點
                glVertex3d(last[v].x(), last[v].y(), last[v].z());

                last[v] = bernstein(py, temp);          //計算下一個頂點
                glTexCoord2f(px, py);                   //設定紋理座標並繪製新的頂點
                glVertex3d(last[v].x(), last[v].y(), last[v].z());
            }
        glEnd();                                        //結束三角形帶的繪製
    }
    glEndList();                                        //顯示列表繪製結束

    free(last);                                         //釋放分配的記憶體
    return drawlist;                                    //返回建立的顯示列表
}

一開始我們分配了一個顯示列表的空間,並讓drawlist儲存了它的地址,並根據細分數,來分配足夠的記憶體空間給last。接著我們檢查dlBPatch,如果不等於0,說明存在顯示列表,要先把它刪除。然後我們給temp賦值,讓它儲存最左側的四個控制點,也就是下面圖中的P0,0~P0,3(我們假定P0,0為貝塞爾曲面的左下角頂點)。緊接著是進入一個迴圈,按照細分數來分割P0,0~P0,3四個控制點繪製的曲線,並把得到的頂點(折點)座標儲存在last指標指向的空間(陣列)中。

下面我們就開始繪製顯示列表了。先繫結紋理,接著我們迴圈u方向(u方向為P0,0~P3,0這個方向),注意迴圈是從1開始的,我們利用u和u-1分別除以m_Divs計算得打當前和上一步的細分數儲存在px和pxold中。然後我們呼叫了四次bernstein()函式,得到當前u方向細分處對應的與P0,0~P0,3平行的四個點儲存於temp中(這四個點只是進行了u方向的細分,可以說是它們那個方向上貝塞爾曲線的虛擬控制點)。

然後我們開始繪製三角形帶(三角形帶之前講過了),我們迴圈v方向(v方向為P0,0~P0,3這個方向),由v除以m_Divs得到紋理座標y,而紋理座標x為前面求得的pxold,於是我們繪製了pxold對應的頂點。下面,我們又呼叫了bernstein(),並以py和temp為引數,這樣我們就得到當前px、py對應的頂點座標(這個點在pxold對應點的右側,因為px對應的t 比pxold對應的t 大1),那麼我就可以繪製下個頂點了。

現在我們假設細分數m_Divs等於4,我們要繪製的點為M0,0~M3,3,點的相對位置如上圖,則繪製順序為M0,0->M1,0->M0,1->M1,1->M0,2->M1,2->M0,3->M1,3->M1,0->M2,0->M1,1->M2,1->M1,2->M2,2->M1,3->M2,3->……(我說一下繪製順序,希望上面看不明白的,看到這可以自己理解)。繪製完三角形帶和顯示列表,函式最後的收尾工作我就不解釋了。

然後,我們把initializeGL()函式和鍵盤控制函式的程式碼一起給大家,改動很小就不解釋了,具體程式碼如下:

void MyGLWidget::initializeGL()                         //此處開始對OpenGL進行所以設定
{
    m_Mybezier.texture = bindTexture(QPixmap("D:/QtOpenGL/QtImage/NeHe.bmp"));
    glEnable(GL_TEXTURE_2D);                            //啟用紋理對映
    m_Mybezier.dlBPatch = genBezier();

    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);               //黑色背景
    glShadeModel(GL_SMOOTH);                            //啟用陰影平滑
    glClearDepth(1.0);                                  //設定深度快取
    glEnable(GL_DEPTH_TEST);                            //啟用深度測試
    glDepthFunc(GL_LEQUAL);                             //所作深度測試的型別
    glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);  //告訴系統對透視進行修正
}
void MyGLWidget::keyPressEvent(QKeyEvent *event)
{
    switch (event->key())
    {
    case Qt::Key_F1:                                    //F1為全屏和普通屏的切換鍵
        fullscreen = !fullscreen;
        if (fullscreen)
        {
            showFullScreen();
        }
        else
        {
            showNormal();
        }
        updateGL();
        break;
    case Qt::Key_Escape:                                //ESC為退出鍵
        close();
        break;

    case Qt::Key_Space:                                 //空格為是否顯示控制點的切換鍵
        m_ShowCPoints = !m_ShowCPoints;
        break;
    case Qt::Key_Left:                                  //Left按下向左旋轉
        m_Rot -= 1.0f;
        break;
    case Qt::Key_Right:                                 //Right按下向右旋轉
        m_Rot += 1.0f;
        break;
    case Qt::Key_Up:                                    //Up按下增加細分數
        m_Divs++;
        m_Mybezier.dlBPatch = genBezier();
        break;
    case Qt::Key_Down:                                  //Down按下減少細分數
        if (m_Divs > 1)
        {
            m_Divs--;
            m_Mybezier.dlBPatch = genBezier();
        }
        break;
    }
}

最後,我們來完成paintGL()函式,具體程式碼如下:

void MyGLWidget::paintGL()                              //從這裡開始進行所以的繪製
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //清除螢幕和深度快取
    glLoadIdentity();                                   //重置模型觀察矩陣
    glTranslatef(0.0f, 0.2f, -3.0f);
    glRotatef(-75.0f, 1.0f, 0.0f, 0.0f);
    glRotatef(m_Rot, 0.0f, 0.0f, 1.0f);                 //繞z軸旋轉

    glCallList(m_Mybezier.dlBPatch);                    //呼叫顯示列表,繪製貝塞爾曲面

    if (m_ShowCPoints)                                  //是否繪製控制點
    {
        glDisable(GL_TEXTURE_2D);                       //禁用紋理貼圖
        glColor3f(1.0f, 0.0f, 0.0f);                    //設定顏色為紅色
        for (int i=0; i<4; i++)                         //繪製水平線
        {
            glBegin(GL_LINE_STRIP);
                for (int j=0; j<4; j++)
                {
                    glVertex3d(m_Mybezier.anchors[i][j].x(),
                               m_Mybezier.anchors[i][j].y(),
                               m_Mybezier.anchors[i][j].z());
                }
            glEnd();
        }
        for (int i=0; i<4; i++)                         //繪製垂直線
        {
            glBegin(GL_LINE_STRIP);
                for (int j=0; j<4; j++)
                {
                    glVertex3d(m_Mybezier.anchors[j][i].x(),
                               m_Mybezier.anchors[j][i].y(),
                               m_Mybezier.anchors[j][i].z());
                }
            glEnd();
        }
        glColor3f(1.0f, 1.0f, 1.0f);                    //恢復OpenGL屬性
        glEnable(GL_TEXTURE_2D);
    }
}
一開始我們還是清空快取,重置矩陣,平移旋轉後,我們呼叫了顯示列表(glCallLists)繪製出貝塞爾曲面。然後我們根據m_ShowCPoints的值來決定是否繪製控制點,如果m_ShowCPoints為true,則進行繪製。繪製時,我們要關閉紋理貼圖,並且要使用GL_LINE_STRIP,這樣才能繪製出連續的折線。

現在就可以執行程式檢視效果了!