och雪花一分形问题。
前面讲了无穷极数的概念,无穷极数是分型学的理论基础。讲到分型问题,我来给大家介绍一片漂亮的雪花叫Koch雪花,它是1904年由瑞典数学家柯赫构造的。下面一起来看一下这片Koch雪花是怎么构造而成的。
有的同学说这不是一个正三角形吗?对,在正三角形的基础上每条边进行三等分,然后借用中间的边构造一个新的小正三角形,然后把中间的边抽掉。大家看一下这样的形状是不是更像一朵雪花了?但还不是。
继续重复刚才的工作,每条边三等分,借用中间的边构造一个更小的正三角形,然后把中间边抽掉,这个过程其实就是第二次分型。依次延续刚才的过程,每次都是三等分,每次都是用中间这条边进行画正三角形的过程,然后把中间小边抽掉。大家看这就是三次分型的过程,依次类推进行无穷次的分型。
最终就可以得到一片非常美丽的雪花,它叫做Koch雪花。下面的问题是什么?请你用无穷极数所学的知识来判断这片美丽的雪花的周长是多少?面积又是多少?
形结构最大程度地利用了细胞表面积来运输能量
导语
Geoffrey West 等人1999年在Science 发表了一篇文章,用分形几何来解释生物异速标度律,引申出了生命的“第四维度”。集智俱乐部“幂律与规模”读书会上,对生命3/4标度现象如何起源、4是否意味着生命有第四维度、如何从分形的角度理解维度等问题,做了大量探讨,本文是对“生命第四维”问题探讨后的一篇笔记。
该论文题目:
The Fourth Dimension of Life: Fractal Geometry and Allometric Scaling of Organisms
http://biology.unm.edu/jhbrown/Documents/Publications/WestBrown&Enquist1999S.pdf
自然选择使生命的代谢能力最大化,最大限度地扩大了运输资源和能量的表面积。例如我们的代谢能量会通过毛细血管的表面积运向全身上下细胞,从而促进细胞生长,维持生命所需的能量。
举个很直观的例子,尽管你的肺只有一个足球那么大,体积为5~6升,但是,血液中负责氧气和二氧化碳交换的肺泡总表面积,几乎有一个网球场那么大;而所有气流通路的总长度几乎是从伦敦到莫斯科的距离,约2500千米!
这是怎么做到的呢?
Peano曲线和大肠表面的分形褶皱
答案是分形。分形是20世纪系统科学提出的一个重要概念,它可以对d维的几何体施加最大化的褶皱和扭曲而得到d + 1维的几何体。例如褶皱的大脑皮层,以及扭曲缠绕的大肠。生命体内正是充满了神奇的分形结构,才实现了空间填充的最大化。
即便在“不经意间”已经处处运用分形结构解决难题,但或许我们对其并没有系统的认识。理解分形,从认识维度开始。
什么是维度
维度,又称维数,在数学中被定义为独立参数的数目,在物理学和哲学的领域内,是指独立的时空坐标的数目。
0维是一个点,没有长度;
1维是一条线,只有长度;
2维是一个面,由长度和宽度形成的面;
3维是一个体,在2维的基础上加上高度所形成。
1维的线在其维度方向可以截出无穷多个0维的点;
2维的面从任意维度方向可以截出无穷多的1维的线;
3维的体可以截出无穷多的2维的面;
4维则是建立在3维之上又多一个维度,根据上面的概念的引申,4维的东西可以截出无穷多个“体”。
图1 从0-4的维度示意图 | wikipedia
在物理学上往往将时间作为第四维,与空间的3维一起构成4维时空,在任何时间来看(横截观察),就可以得到一个“体”。如果固定空间的一个维度,剩下的两个维度和时间也可以合成一个体,可以形象地展示物体的运动情况,如图2所示。
图2太阳系在银河系中的真实运动轨迹 | wikipedia
维度是这个形体上确定一个位置所需要的独立坐标的个数。对于一个独立的点来说,它没有大小,确定点上的位置不需要任何坐标数据。但对于一条直线来说,就需要用坐标来确定线上点的位置。曲线也是如此。无论直线还是平面上的曲线还是空间上的连续曲线都只有一个维度。
平面和球面都是2维的,因为一个数据无法标定面上的某个具体位置,而是需要并且只需要2个数据。比如地球上的任何一个位置通过精度和维度就可以确定下来。
弯曲的三维形态难以想象,因为其要存在至少4维的空间中。
图3 平面和空间中的曲线,也都是1维的
曲线是一维的,如图3所示,这些曲线,无论是平面上的还是立体中的都是1维的,都可以通过一个数值来确定某个位置。
虽然一般人已经习惯了整数维,但有些时候维度不一定是整数,例如分形,维度可能会是一个非整的有理数或者无理数。
如何理解分形的维度
下面将介绍分形中最著名的几个例子。
A.康托(Cantor)三分集
1883年,德国数学家康托(G.Cantor)提出了如今广为人知的康托三分集。康托三分集的构造过程是:
把闭区间[0,1]平均分为三段,去掉中间的 1/3 部分段,则只剩下两个闭区间[0,1/3]和[2/3,1]。
再将剩下的两个闭区间各自平均分为三段,同样去掉中间的区间段,这时剩下四段闭区间:[0,1/9],[2/9,1/3],[2/3,7/9]和[8/9,1]。
重复删除每个小区间中间的 1/3 段。如此不断的分割下去, 最后剩下的各个小区间段就构成了康托三分集。
康托三分集的维度为 D=log2 / log3=0.631
B.科赫雪花。
瑞典人Koch于1904年提出。给定线段AB,科赫曲线可以由以下步骤生成:
将线段分成三等份,形成3个线段
以中间段为底,向外(内外随意)画一个等边三角形
将这个中间段移去
分别对目前的4个线段,重复1~3
科赫雪花维度为 D=log4 / log3=1.2618
C.门格海绵
1926年被奥地利数学家 Menger 首次描述。门格海绵的结构可以用以下方法形象化:
从一个正方体开始,把正方体的每一个面分成9个正方形。
再将把正方体分成27个小正方体,像魔方一样。
把每一面的中间的正方体去掉,把最中心的正方体也去掉,留下20个正方体(第二个图像)。
把每一个留下的小正方体都重复前面的步骤。
把以上的步骤重复无穷多次以后,得到的图形就是门格海绵。
门格海绵的维度为 D=log20 / log3=2.7268
再次强调,维度是这个形体上确定一个位置所需要的独立坐标的个数。
确定康托集上的某一个位置不需要整个实数域,因为有很多点已经挖去了。所以可以认为康托集要比一个点多,但不如一个曲线多。维度应该在0-1之间。维度应该在0-1之间。
而科赫雪花是在二维平面内,但是没有覆盖整个平面,确定上面的位置一个实数是不够的,例如给定横轴方向的实数而不指定纵轴位置的话无法确定集合上的一个元素,但纵轴并不是连续的,只有某些位置上才会有意义,所以应该大体上维度介于1-2之间。
而门格海绵用不着3个独立的变量来确定一个位置,所以维度应该在2-3之间。从这个意义上讲,能表示在二维平面的分形的维数不能超过2,在立体空间表示出来的分形不会超过3。
所以,West在生命第四维的文章中说V(和体积和质量有关的东西)是4维的,很容易受到的质疑。
严格的分形维度如何定义
严格的分形维度的定义为 Hausdorff 维数,可以通过覆盖来进行解释。
Hausdorff 提出:假设考虑的物体或图形是欧氏空间的有界集合 ,用半径为
的球覆盖其集合时 , 假定是球的个数的最小值,则有分形维数。
我们先说一个长度为L的直线段,如果我们用更小的球去覆盖,则需要的个数会随着球的半径减少而增加,
所以分形维数:
同理很容易说明平面是2维以及立体是3维的。
再来看康托集。假设整个长度为1,用最长的长度为1的直线段即可覆盖,但如果用长度为1/3的线段的话,就不用3个了,两个即可,因为中间是空的。继续进行,若长度为1/9的线段的话,只需要4个线段。
所以当线段的长度变为上一个覆盖的1/3的时候,需要的个数只增加到两倍。
所以分形维数:。
同样,对于科赫雪花,当使用长度为原来1/3的线段进行覆盖时候,覆盖物的个数会增加到4倍,
所以分形维数:。
生命第四维是一种类比
回到 West 关于生命第四维的文章,其目的是解释生物基础代谢率与生物量(质量)之间的非线性关系,更精确地说是3/4的幂律关系。也就是如果生物体体重增加一万倍(10的四次幂),代谢率只增加到原来的一千倍(10的3次幂)。
West 在这个文章中运用Koch雪花那样的“分形可以增加维度”的思想,将血管横截面圆周分形化,从1维变为2维,将整个血管的表面积变为3维,并将其与基础代谢率建立线性关系。而体积和质量相关部分为表面积的维度再增加一个维度变为4维。维度关系如下表所示。
表1 生物体血管的分形维度和传统的欧氏几何维度的关系
和体积有关的东西超过3维,是很难被接受的。集智学友在 West 访华期间向其本人咨询了这个问题, West 本人答复说,这个增加的维度并非实指。
生命的第四维度,在数学上看好像是的,但严格地说——“这是一个类比和拓展”。
参考文献
https://zh.wikipedia.org/wiki/維度
http://v.youku.com/v_show/id_XNTYzNzQ4OTY0.html
https://baike.baidu.com/item/分形理论/1568038?fr=aladdin
https://www.cnblogs.com/WhyEngine/p/3998063.html
https://www.cnblogs.com/WhyEngine/p/3981674.html
https://baike.baidu.com/item/%E9%97%A8%E6%A0%BC%E6%B5%B7%E7%BB%B5/9005082?fr=aladdin
West G B, Brown J H, Enquist B J. The fourth dimension of life: fractal geometry and allometric scaling of organisms[J]. science, 1999, 284(5420): 1677-1679.
朱金兆, 朱清科. 分形维数计算方法研究进展[J]. 北京林业大学学报, 2002, 24(2): 71-78.
http://www.math.ubc.ca/~cass/courses/m308-03b/projects-03b/skinner/ex-dimension-koch_snowflake.htm
编辑:王怡蔺
程 实 验 报 告
实验一:分形图形绘制11
一、实验目的11
二、实验内容11
三、实验心得1111
实验二:三维场景绘制1212
一、实验目的1212
二、实验内容1212
三、实验心得1919
1、实验算法
绘制分形三角形算法思想:求出三角形ABC三条边AB,BC,AC的中点坐标D,E,F,绘制三角形DEF,再对三角形ADF,BDE,CEF重复进行上述操作即可得到分形三角形。
绘制Koch雪花的算法思想:计算得到一条边经过一次分形后的五个点的坐标,然后对分得的四条边反复进行变换,对三角形的三边进行上述变换即可得到Koch雪花。
绘制分形地毯图形思想:计算得到中心正方形的周围8个小正方形的左上顶点坐标与边长,绘制8个小正方形,然后对8个小正方形分别进行上述操作即可得到分形地毯。
2、源程序
源程序如下:
#include <stdio.h>
#include <time.h>
#include <math.h>
#include <GL/glut.h>
#define ROUND(a) ((int )(a+0.5)) // 求某个数的四舍五入值
#define PI 3.141592654 // pi的预定义
int xf=100, yf=200; // 定义测试数据坐标x0,y0
int xl=500, yl=200; // 定义测试数据坐标x1,y1
int xt=xf + (xl - xf) / 2.0; // 画等边三角形时的第三个坐标x0
int yt=yf + (xl - xf) / 2.0 * tan(3.1415926 / 3.0);
// 画等边三角形时的第三个坐标x0
// 窗口初始化函数,初始化背景色,投影矩阵模式和投影方式
void init(void)
{
glClearColor(0.396, 0.655, 0.890, 0.0); //指定窗口的背景色为蓝色
glMatrixMode(GL_PROJECTION); //对投影矩阵进行操作
gluOrtho2D(0.0, 600.0, 0.0, 600.0); //使用正投影
}
//绘制直线的函数
void lineDDA(GLint xa, GLint ya, GLint xb, GLint yb)
{
GLint dx=xb - xa, dy=yb - ya; //计算x,y方向的跨距
int steps, k; //定义绘制直线像素点的步数
float xIcre, yIcre, x=xa, y=ya; //定义步长的增量
//取X,Y方向跨距较大的值为步数
if (abs(dx) > abs(dy))
steps=abs(dx);
else
steps=abs(dy);
//根据步数来求步长增量
xIcre=dx / (float)steps;
yIcre=dy / (float)steps;
//从起点开始绘制像素点
for (k=0;k <=steps; k++)
{
glBegin(GL_POINTS);
glVertex2f(x, y);
glEnd();
x +=xIcre;
y +=yIcre;
}
}
// 中点Bresenham算法绘制直线
void MidBresenhamLine(GLint xa, GLint ya, GLint xb, GLint yb)
{
// 斜率k的四个状态
// K01表示0<k<=1;
// KG1表示k>1;
// K_10表示-1<=k<0;
// KL_1表示k<-1
const int K01=0, KG1=1, K_10=2, KL_1=3;
int flag; // 标识斜率k的状态
GLint dx, dy, d, upIncre, downIncre, x, y;
// 使b的横坐标大于a的横坐标
if (xa > xb)
{
x=xb; xb=xa; xa=x;
y=yb; yb=ya; ya=y;
}
if (yb >=ya && yb - ya < xb - xa)
flag=K01; // K01表示0<k<=1;
else if (yb - ya > xb - xa)
flag=KG1; // KG1表示k>1;
else if (yb <=ya && yb - ya > xa - xb)
flag=K_10; // K_10表示-1<=k<0;
else
flag=KL_1; // KL_1表示k<-1
x=xa;
y=ya;
dx=xb - xa; // 计算增量dx
dy=yb - ya; // 计算增量dy
// 当0<k<=1时
if (flag==K01)
{
d=dx - 2 * dy; // 计算d初值
upIncre=2 * dx - 2 * dy; // 计算步长增量
downIncre=-2 * dy;
}
// 当k>1时
if (flag==KG1)
{
d=2 * dx - dy; // 计算d初值
upIncre=2 * dx; // 计算步长增量
downIncre=2 * dx - 2 * dy;
}
// 当-1<=k<0时
if (flag==K_10)
{
d=-dx - 2 * dy; // 计算d初值
upIncre=-2 * dy; // 计算步长增量
downIncre=-2 * dx - 2 * dy;
}
// 当k<-1时
if (flag==KL_1)
{
d=-2 * dx - dy; // 计算d初值
upIncre=-2 * dx - 2 * dy; // 计算步长增量
downIncre=-2 * dx;
}
// 开始绘制直线
glBegin(GL_POINTS);
// 斜率为无穷大,即为一条竖直线时单独考虑
if (dx==0)
{
// 使B的纵坐标更大
if (ya > yb)
{
y=yb; yb=ya; ya=y;
}
y=ya;
// 从A点向上绘制直线
while (y < yb)
{
glVertex2i(x, y);
y++;
}
}
else
{
// 横向扫描,当扫描到B点横坐标时退出
while (x <=xb)
{
glVertex2i(x, y);
// 如果直线斜率满足(0,1]或[-1,0)时,X方向为最大位移方向且X增加
if (flag==K01 || flag==K_10)
x++;
// 如果直线斜率满足(1, ∞)时,Y方向为最大位移方向且Y增加
if (flag==KG1)
y++;
// 如果直线斜率满足(-∞, -1)时,Y方向为最大位移方向且Y递减
if (flag==KL_1)
y--;
// 当判据d<0时进入
if (d < 0)
{
// 如果直线斜率满足(0,1]时,y增加
if (flag==K01)
y++;
// 如果直线斜率满足(-∞, -1)时,x增加
if (flag==KL_1)
x++;
// 更新判据d
d +=upIncre;
}
// d>0时
else
{
// 如果直线斜率满足[-1,0)时,y递减
if (flag==K_10)
y--;
// 如果直线斜率满足(1, ∞)时,x增加
if (flag==KG1)
x++;
// 更新判据d
d +=downIncre;
}
}
}
glEnd(); // 结束绘制
}
// 递归绘制Koch分形图形的一条边,n为递归次数,inside为Koch图形的朝向
// inside为1表示朝上或朝内,为0表示朝下或朝外
void Koch(float x0, float y0, float x1, float y1, int n, int inside)
{
// n>0时继续递归
if (n > 0)
{
// A,B,C,D,E点为一条线上一次分形后的五个顶点
float xa, ya, xb, yb, xc, yc, xd, yd, xe, ye;
xa=x0; // A点的横坐标与X0相等
ya=y0; // A点的纵坐标与Y0相等
xb=x0 + (x1 - x0) / 3.0; // B点为靠近(X0, Y0)的三等分点
yb=y0 + (y1 - y0) / 3.0;
// 如果朝向为上或内,C点的坐标值计算如下
if (inside)
{
xc=(x1 + x0) / 2.0 + (y0 - y1) * sqrt(3.0) / 6.0;
yc=(y1 + y0) / 2.0 + (x1 - x0) * sqrt(3.0) / 6.0;
}
// 如果朝向为下或外,C点的坐标值计算如下
else
{
xc=(x1 + x0) / 2.0 - (y0 - y1) * sqrt(3.0) / 6.0;
yc=(y1 + y0) / 2.0 - (x1 - x0) * sqrt(3.0) / 6.0;
}
// D点为靠近(X1, Y1)的三等分点
xd=x0 + 2 * (x1 - x0) / 3.0;
yd=y0 + 2 * (y1 - y0) / 3.0;
xe=x1; // E点的横坐标等于X1
ye=y1; // E点的纵坐标等于Y1
Koch(xa, ya, xb, yb, n - 1, inside); // 对边AB进行递归
Koch(xb, yb, xc, yc, n - 1, inside); // 对边BC进行递归
Koch(xc, yc, xd, yd, n - 1, inside); // 对边CD进行递归
Koch(xd, yd, xe, ye, n - 1, inside); // 对边DE进行递归
}
// n=0时递归结束,绘制直线(x0, y0)到(x1, y1)
else
MidBresenhamLine(x0, y0, x1, y1);
}
// 递归绘制分形三角形,n为递归次数
void Triangle(float x0, float y0, float x1, float y1, float x2, float y2, int n)
{
// MID0为边(x1, y1)与(x2, y2)中点
// MID1为边(x0, y0)与(x2, y2)中点
// MID2为边(x0, y0)与(x1, y1)中点
float midx0, midy0, midx1, midy1, midx2, midy2;
// n>0时进入递归
if (n > 0)
{
// 计算MID0的坐标
midx0=(x1 + x2) / 2.0;
midy0=(y1 + y2) / 2.0;
// 计算MID1的坐标
midx1=(x0 + x2) / 2.0;
midy1=(y0 + y2) / 2.0;
// 计算MID2的坐标
midx2=(x0 + x1) / 2.0;
midy2=(y0 + y1) / 2.0;
// 对(x0, y0),MID1,MID2组成的三角形递归
Triangle(x0, y0, midx1, midy1, midx2, midy2, n - 1);
// 对MID0,(x1, y1),MID2组成的三角形递归
Triangle(midx0, midy0, x1, y1, midx2, midy2, n - 1);
// 对MID0,MID1,(x2, y2)组成的三角形递归
Triangle(midx0, midy0, midx1, midy1, x2, y2, n - 1);
}
// n=0时开始绘制三角型的三条边
else
{
MidBresenhamLine(x0, y0, x1, y1);
MidBresenhamLine(x1, y1, x2, y2);
MidBresenhamLine(x0, y0, x2, y2);
}
}
// 绘制内部填充的正方形,(x0, y0)为正方形左上方顶点,side为正方形边长
void DrawSquare(double x0, double y0, double side)
{
glBegin(GL_LINES);
// 从y=y0扫描到y=y0-side,绘制从(x0, y)到(x0+side, y)的直线
for (int y=y0; y > y0 - side; y--)
{
glVertex2d(x0, y);
glVertex2d(x0 + side, y);
}
glEnd();
}
// 绘制分形地毯图形,(x, y)为中心正方形的左上顶点,side为其边长,n为递归次数
void DrawCarpet(double x, double y, double side, int n)
{
// 中心正方形周围的8个小正方形的左上顶点坐标
double square0[2], square1[2], square2[2], square3[2],
square4[2], square5[2], square6[2], square7[2];
double len=side / 3.0; // 8个小正方形的边长
DrawSquare(x, y, side); // 以(x, y)为左上顶点,side为边长绘制实心正方形
// n>0时进入递归
if (n > 0)
{
// 计算第1个小正方形的左上顶点的坐标值
square0[0]=x - 2 * len;
square0[1]=y + 2 * len;
// 计算第2个小正方形的左上顶点的坐标值
square1[0]=x + len;
square1[1]=y + 2 * len;
// 计算第3个小正方形的左上顶点的坐标值
square2[0]=x + 4 * len;
square2[1]=y + 2 * len;
// 计算第4个小正方形的左上顶点的坐标值
square3[0]=x - 2 * len;
square3[1]=y - len;
// 计算第5个小正方形的左上顶点的坐标值
square4[0]=x + 4 * len;
square4[1]=y - len;
// 计算第6个小正方形的左上顶点的坐标值
square5[0]=x - 2 * len;
square5[1]=y - 4 * len;
// 计算第7个小正方形的左上顶点的坐标值
square6[0]=x + len;
square6[1]=y - 4 * len;
// 计算第8个小正方形的左上顶点的坐标值
square7[0]=x + 4 * len;
square7[1]=y - 4 * len;
// 对8个小正方形进行递归处理
DrawCarpet(square0[0], square0[1], len, n - 1);
DrawCarpet(square1[0], square1[1], len, n - 1);
DrawCarpet(square2[0], square2[1], len, n - 1);
DrawCarpet(square3[0], square3[1], len, n - 1);
DrawCarpet(square4[0], square4[1], len, n - 1);
DrawCarpet(square5[0], square5[1], len, n - 1);
DrawCarpet(square6[0], square6[1], len, n - 1);
DrawCarpet(square7[0], square7[1], len, n - 1);
}
}
// 绘制回调函数
void display()
{
glClear(GL_COLOR_BUFFER_BIT); // 设定颜色缓存中的值
glColor3f(1.0, 1.0, 1.0); // 设置直线颜色为白色
//lineDDA(50, 500, 300, 100); // 调用DDA算法函数绘制直线
//MidBresenhamLine(50, 500, 400, 200); // 调用中点Bresenham算法绘制直线
// 对三角形的三条边分别调用Koch分形图形绘制函数
//Koch(xf, yf, xl, yl, 4, 0);
//Koch(xf, yf, xt, yt, 4, 1);
//Koch(xt, yt, xl, yl, 4, 1);
//Triangle(xt, yt, xf, yf, xl, yl, 5); // 调用分形三角形绘制函数绘制图形
DrawCarpet(200, 400, 200, 4); // 调用分形地毯绘制函数绘制图形
glFlush(); //立即执行
}
// 主函数
int main(int argc, char ** argv)
{
glutInit(&argc, argv); // 初始化GLUT库OpenGL窗口的显示模式
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB); // 初始化窗口的显示模式
glutInitWindowSize(600, 600); // 设置窗口的尺寸
glutInitWindowPosition(100, 100); // 设置窗口的位置
glutCreateWindow("Hello OpenGL!"); // 创建窗口标题为“Hello OpenGL!”
init(); // 初始化
glutDisplayFunc(display); // 执行画图程序
glutMainLoop(); // 启动主GLUT事件处理循环
}
3、实验结果
图1-1 中点Bresenham算法绘制直线结果
图1-2 分型三角形绘制结果
图1-3 Koch雪花绘制结果
图1-4 分型地毯绘制结果
通过本次实验,我对图形学有了更深刻的认识,收获很大。
第一次使用OpenGL编程,因为有了C语言的基础,上手并不是特别困难,阅读理解了模板之后,便能自己修改程序完成中点Bresenham算法,对直线的绘制算法有了更全面的认识,把书本上的知识通过代码实现,并且能够看到相当不错的结果,这是很令人欣慰的;分形图形的绘制,是对数学变换以及递归算法的应用,通过自己的观察与计算,能够绘制出较为复杂的图形,分形三角形的绘制相对简单,找出中点即可,Koch图形的绘制需要找出变换后的五个顶点坐标,这需要进一步的计算才能实现,分形地毯的绘制是自己额外绘制的,也是不难实现的,分形图形的绘制让我体会到了数学与图形学的美,对课程有了更加浓厚的兴趣。
总的来说,完成本次实验,我的整体能力得到了提高,对计算机图形学这门课程的理解更加全面。
1、实验算法
三维场景绘制:先绘制出基本的三维图形,再通过简单的平移、缩放、旋转变换,得到一系列图形的组合,最后加入光照、材质等属性,设置合适的观测视角,即可实现简单三维场景的绘制。
本实验绘制三维简单城堡,对城堡的每个部分进行了分解,运用了OpenGL显示列表,再对每个部分进行各种变换,最后绘制出完整的城堡模型。
2、源程序
源程序如下:
#include <stdio.h>
#include <GL/glut.h>
#include <math.h>
#define PI 3.1415265359 // 定义常量π
static GLfloat MatSpec[]={1.0, 1.0, 1.0, 1.0}; // 材料颜色
static GLfloat MatShininess[]={50.0}; // 光泽度
static GLfloat LightPos[]={-2.0, 1.5, 1.0, 0.0}; // 光照位置
static GLfloat ModelAmb[]={0.5, 0.5, 0.5, 0.0}; // 环境光颜色
// 需要画出来的哨塔的数量和墙面的数量
GLint TowerListNum, WallsListNum;
GLint WallsList;
// 哨塔相关参数
int NumOfEdges=30; // 细分度(其值越高,绘制越精细)
GLfloat LowerHeight=3.0; // 哨塔上方倒圆台的下底面高度
GLfloat HigherHeight=3.5; // 哨塔上方倒圆台的上底面高度
GLfloat HR=1.3; // 哨塔上方倒圆台的上底面半径
// 城墙相关参数
GLfloat WallElementSize=0.2; // 城墙上方凸起部分的尺寸
GLfloat WallHeight=2.0; // 城墙上方凸起部分的高度
// 绘制城墙上方凸起部分
void DrawHigherWallPart(int NumElements)
{
glBegin(GL_QUADS); // 开始绘制四边形
// NumElements绘制凸起部分的数目,i小于其值时进入循环
for (int i=0; i < NumElements; i++)
{
glNormal3f(0.0, 0.0, -1.0); // 设置法向量为Z轴负半轴方向
// 绘制四边形的四个顶点
glVertex3f(i * 2.0 * WallElementSize, 0.0, 0.0);
glVertex3f(i * 2.0 * WallElementSize, WallElementSize, 0.0);
glVertex3f((i * 2.0 + 1.0) * WallElementSize, WallElementSize, 0.0);
glVertex3f((i * 2.0 + 1.0) * WallElementSize, 0.0, 0.0);
}
glEnd(); // 结束绘制
}
// 绘制城墙
void DrawWall(GLfloat Length)
{
glBegin(GL_QUADS); // 开始绘制四边形
glNormal3f(0.0, 0.0, -1.0); // 设置法向量为Z轴负半轴方向
// 绘制四边形的四个点
glVertex3f(0.0, 0.0, 0.0);
glVertex3f(0.0, WallHeight, 0.0);
glVertex3f(Length, WallHeight, 0.0);
glVertex3f(Length, 0.0, 0.0);
glEnd(); // 结束绘制
// i为当前绘制的城墙上的凸起的数目
int i=(int)(Length / WallElementSize / 2);
// 保证凸起的总长度小于城墙的长度
if (i * WallElementSize > Length)
i--; // 如果凸起的总长度大于城墙长度,凸起数目减一
glPushMatrix();
glTranslatef(0.0, WallHeight, 0.0); // 平移变换
DrawHigherWallPart(i); // 绘制城墙上方凸起
glPopMatrix();
}
// 初始化函数,绘制哨塔,城墙
void Init(void)
{
/* 绘制哨塔 */
TowerListNum=glGenLists(1); // 生成哨塔显示列表
GLfloat x, z; // 绘制时的法向量x,z分量
GLfloat NVectY; // 哨塔上方部分倒圆台的法向量y分量
glNewList(TowerListNum, GL_COMPILE); // 用于创建和替换一个显示列表函数原型
glBegin(GL_QUADS); // 绘制四边形
// 创建塔身的圆柱体部分
int i=0;
for (i=0; i < NumOfEdges - 1; i++)
{
// 计算前两个点法向量的x,z坐标
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 设置法向量方向
// 以上述法线方向绘制前两个顶点
glVertex3f(x, LowerHeight, z);
glVertex3f(x, 0.0, z);
// 计算后两个点法向量的x,z坐标
x=cos((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
z=sin((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 设置法向量方向
// 以上述法线方向绘制后两个顶点
glVertex3f(x, 0.0, z);
glVertex3f(x, LowerHeight, z);
}
// 计算最后一个四边形的前两个点法向量的x,z坐标
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 设置法向量方向
// 以上述法线方向绘制前两个顶点
glVertex3f(x, LowerHeight, z);
glVertex3f(x, 0.0, z);
// 计算最后一个四边形的后两个点法向量的x,z坐标
x=cos(1.0 / (float)NumOfEdges * PI * 2.0);
z=sin(1.0 / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, 0.0, z); // 设置法向量方向
// 以上述法线方向绘制后两个顶点
glVertex3f(x, 0.0, z);
glVertex3f(x, LowerHeight, z);
// 哨塔下方圆柱体部分绘制完成
// 绘制哨塔上方倒圆台部分
// 计算法向量NVect y分量的值
NVectY=(HR - 1.0) / (LowerHeight - HigherHeight) * (HR - 1.0);
for (i=0; i < NumOfEdges - 1; i++)
{
// 计算四边形前两个顶点的法向量x,z值
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 设置法向量方向
// 绘制四边形前两个顶点
glVertex3f(x * HR, HigherHeight, z * HR);
glVertex3f(x, LowerHeight, z);
// 计算四边形后两个顶点的法向量x,z值
x=cos((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
z=sin((float)(i + 1) / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 设置法向量方向
// 绘制四边形后两个顶点
glVertex3f(x, LowerHeight, z);
glVertex3f(x*HR, HigherHeight, z*HR);
}
// 计算最后一个四边形前两个顶点的法向量x,z坐标
x=cos((float)i / (float)NumOfEdges * PI * 2.0);
z=sin((float)i / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 设置法向量方向
// 绘制最后一个四边形前两个顶点
glVertex3f(x * HR, HigherHeight, z * HR);
glVertex3f(x, LowerHeight, z);
// 计算最后一个四边形后两个顶点的法向量x,z坐标
x=cos(1.0 / (float)NumOfEdges * PI * 2.0);
z=sin(1.0 / (float)NumOfEdges * PI * 2.0);
glNormal3f(x, NVectY, z); // 设置法向量方向
// 绘制最后一个四边形前两个顶点
glVertex3f(x, LowerHeight, z);
glVertex3f(x*HR, HigherHeight, z*HR);
glEnd(); // 绘制结束
glEndList(); // 标志显示列表的结束
/* 绘制哨塔 */
/* 绘制城墙和大门 */
WallsListNum=glGenLists(1); // 创建城墙显示列表
glNewList(WallsListNum, GL_COMPILE); // 说明显示列表的开始
DrawWall(10.0); // 调用城墙绘制函数绘制左侧城墙
glPushMatrix(); // 变换矩阵压栈
glTranslatef(10.0, 0.0, 0.0); // 平移变换
glPushMatrix(); // 变换矩阵压栈
glRotatef(270.0, 0.0, 1.0, 0.0); // 旋转变换
DrawWall(10.0); // 调用城墙绘制函数绘制后侧城墙
glPopMatrix(); // 恢复变换矩阵
glTranslatef(0.0, 0.0, 10.0); // 平移变换
glPushMatrix(); // 变换矩阵压栈
glRotatef(180.0, 0.0, 1.0, 0.0); // 旋转变换
DrawWall(5.0); // 调用城墙绘制函数绘制右侧后方城墙
glRotatef(90.0, 0.0, 1.0, 0.0); // 旋转变换
glTranslatef(0.0, 0.0, 5.0); // 平移变换
DrawWall(5.0); // 调用城墙绘制函数绘制右侧中间城墙
glPopMatrix(); // 恢复变换矩阵
glTranslatef(-5.0, 0.0, 5.0); // 平移变换
glPushMatrix(); // 变换矩阵压栈
glRotatef(180.0, 0.0, 1.0, 0.0); // 旋转变换
DrawWall(5.0); // 调用城墙绘制函数绘制最右侧城墙
glPopMatrix(); // 恢复变换矩阵
glPushMatrix(); // 变换矩阵压栈
glRotatef(90.0, 0.0, 1.0, 0.0); // 旋转变换
glTranslatef(0.0, 0.0, -5.0); // 平移变换
DrawWall(6.0); // 调用城墙绘制函数绘制前方大门右边城墙
// 绘制前方大门
glTranslatef(6.0, 0.0, 0.0); // 平移变换
glBegin(GL_QUADS); // 绘制四边形
glNormal3f(0.0, 0.0, -1.0); // 设置法向量
// 绘制四边形四个顶点
glVertex3f(0.0, WallHeight / 2.0, 0.0);
glVertex3f(0.0, WallHeight, 0.0);
glVertex3f(3.0, WallHeight, 0.0);
glVertex3f(3.0, WallHeight / 2.0, 0.0);
glEnd(); // 绘制结束
i=(int)(3.0 / WallElementSize / 2); // 计算大门上方凸起数目
if (i * WallElementSize > 3.0)
i--; // 如果凸起总长度大于大门长度,凸起数目减一
glPushMatrix(); // 变换矩阵压栈
glTranslatef(0.0, WallHeight, 0.0); // 平移变换
DrawHigherWallPart(i); // 绘制大门上方凸起
glPopMatrix(); // 恢复变换矩阵
glTranslatef(3.0, 0.0, 0.0); // 平移变换
DrawWall(6.0); // 绘制前方大门左侧城墙
glPopMatrix(); // 恢复变换矩阵
glPopMatrix(); // 恢复变换矩阵
glEndList(); // 标识显示列表的结束
/* 绘制城墙和大门 */
}
// 绘制函数
void Display(void)
{
glClearColor(1, 1, 1, 0); // 设置背景色
// 以上面颜色清屏并清除深度缓存
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity(); // 初始化矩阵
glLightfv(GL_LIGHT0, GL_POSITION, LightPos); // 指定光源的位置
glTranslatef(-7.0, -4.0, -20.0); // 平移变换
glRotatef(85.0, 0.0, 1.0, 0.0); // 旋转变换
glRotatef(15.0, 0.0, 0.0, 1.0); // 旋转变换
/* 绘制地面 */
glBegin(GL_POLYGON); // 绘制多边形
glNormal3f(0.0, 1.0, 0.0); // 设置法向量
// 绘制6个顶点
glVertex3f(0.0, 0.0, 0.0);
glVertex3f(10.0, 0.0, 0.0);
glVertex3f(10.0, 0.0, 10.0);
glVertex3f(5.0, 0.0, 15.0);
glVertex3f(0.0, 0.0, 15.0);
glVertex3f(0.0, 0.0, 0.0);
glEnd(); // 绘制结束
/* 绘制地面 */
// 设置全局环境光为双面光
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);
// 执行城墙显示列表
glCallList(WallsListNum);
// 取消设置全局环境光为双面光
glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);
// 执行哨塔显示列表绘制第一个哨塔
glCallList(TowerListNum);
glTranslatef(10.0, 0.0, 0.0); // 平移变换
// 执行哨塔显示列表绘制第二个哨塔
glCallList(TowerListNum);
glTranslatef(0.0, 0.0, 10.0); // 平移变换
// 执行哨塔显示列表绘制第三个哨塔
glCallList(TowerListNum);
glTranslatef(-5.0, 0.0, 5.0); // 平移变换
// 执行哨塔显示列表绘制第四个哨塔
glCallList(TowerListNum);
glTranslatef(-5.0, 0.0, 0.0); // 平移变换
// 执行哨塔显示列表绘制第五个哨塔
glCallList(TowerListNum);
glFlush();// 立即执行
glutSwapBuffers();// 交换缓冲区
}
// 窗口改变函数
void Reshape(int x, int y)
{
glViewport(0, 0, x, y); // 设置视口
glMatrixMode(GL_PROJECTION); // 指定当前操作投影矩阵堆栈
glLoadIdentity(); // 重置矩阵函数,将之前的变换消除
gluPerspective(40.0, (GLdouble)x / (GLdouble)y, 1.0, 200.0); // 投影变换
glMatrixMode(GL_MODELVIEW); // 指定当前操作视景矩阵堆栈
}
// 主函数
int main(int argc, char **argv)
{
glutInit(&argc, argv); // 初始化
// 设置窗口初始显示模式
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowSize(1200, 600); // 显示框大小
glutInitWindowPosition(100, 100); // 确定显示框左上角的位置
glutCreateWindow("Castle"); // 窗口名字
glEnable(GL_DEPTH_TEST); // 打开深度检测
// 设置正反面都为填充方式
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
glutDisplayFunc(Display); // 调用绘图函数
glutReshapeFunc(Reshape); // 调用窗口改变函数
// 定义镜面材料颜色
glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, MatSpec);
// 定义材料的光泽度
glMaterialfv(GL_FRONT_AND_BACK, GL_SHININESS, MatShininess);
glEnable(GL_LIGHTING); // 启用光源
glEnable(GL_LIGHT0); // 使用指定灯光
glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ModelAmb); // 设定全局环境光
Init(); // 调用初始化函数
glutMainLoop(); // 启动主GLUT事件处理循环
return 0;
}
3、实验结果
三维场景绘制结果如图2-1、2-2所示:
图2-1 三维场景绘制结果A
图2-2 三维场景绘制结果B
通过第二次实验,我对三维场景绘制的认识更加全面,对OpenGL的运用也更加熟练。
三维场景的绘制,相比第一次实验较为复杂,对OpenGL的熟练使用要求更多,需要对OpenGL中的平移、缩放和旋转变换有足够的了解,在绘图过程中,运用了OpenGL的显示列表绘制,大大降低了代码的复杂度,绘制简单三维城堡的的过程中,先是绘制城堡的一小部分,如哨塔的上下两个部分,城墙上面的凸起等,在经过一系列变换,绘制出所有的哨塔,城墙以及城墙上方的凸起等等,绘制过程中,对法向量的计算尤为重要,因为绘制圆柱体和圆台时采用的是绘制四边形合成的方法,所以在绘制每个四边形时法向量的设置需要格外注意,对每个部分的定位也要准确,每一步的绘制都需要考虑到下一步的变换,所以在对绘制过程中的坐标变换时也需要注意。
综上,两次实验的完成,让我一点一滴的开始OpenGL编程,其中的收获是不言而喻的,对课程的学习也起到了重要作用。
*请认真填写需求信息,我们会在24小时内与您取得联系。