您的位置:首页 > 编程语言 > Qt开发

【Qt OpenGL教程】28:贝塞尔曲面

2015-08-15 18:18 471 查看
第28课:贝塞尔曲面 (参照NeHe)
这次教程中,我们将介绍贝塞尔曲面,因此这是关于数学运算的一课(这导致很不好讲),来吧,相信你能搞定它的!这一课中,我们并不是要做一个完整的贝塞尔曲面库(库的话OpenGL已经完成了),而是一个展示概念的程序,来让你熟悉曲面是怎么计算实现的。

如果想理解贝塞尔曲面没有对数学基本的认识是很难的(NeHe原文中对贝塞尔曲线和曲面的介绍并不能让一个初学者很明白,所以我并不打算照搬过来),所以如果你并不了解贝塞尔曲面,我转载了一篇文章(/article/10634882.html)希望能帮到你。我是希望你看完这篇文章,对贝塞尔曲线和曲面有了较系统的了解再进入这一课,不然后面涉及数学计算部分你可能会看不懂,而且文章中还介绍了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,这样才能绘制出连续的折线。
现在就可以运行程序查看效果了!

全部教程中需要的资源文件点此下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: