【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分別儲存顯示列表和紋理的記憶體地址。最後三個新增的函式宣告就等定義時一起解釋了。
下面,我們開啟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,這樣才能繪製出連續的折線。
現在就可以執行程式檢視效果了!