TIT 计算机图形学 期末课设-青花瓷坛的实体模型

前言

  • 实验中用到了很多知识点,这里只讲解其中的一部分,仅供参考.详细讲解可以看孔老师的视频课,具体学习程序中的类
  • 参考视频计算机图形学全套算法讲解和C++编码实现(共23讲配套源码)计算机图形学案例视频讲解以及主页相关算法。孔老师是我的代课老师,孔教授有十多年教学经验,视频课很不错,所有的源程序都基于他写的函数,并非小张写的。所有源程序都基于C+编译
  • 参考教材《计算机图形学-理论与实践项目化教程》 孔令德著,大家多多支持哇
  • 实验源码很多,没有传CSDN因为小张认为源码并不是我开的,只是拿老师的程序做了一些东西,拿这个赚积分和马内未免有点!这里直接放了期末课设青花瓷坛源程序,不方便访问Github的可以评论邮箱。孔老师的视频课讲的很清楚,大家去B站就可以看啦!
  • 第四章贝塞尔曲线和回转体画出线框图 P106(两个人)丁兆国 刘元华
  • 第五章使用projection类投影 P94,第三章图形的几何变换 transform类 P37(两个人)周立波
  • 第五章消隐(ZBuffer) P94 刘元华
  • 第七章材质(Material)、光照(Lighting、LightSource) P139(材质少,光照内容多,两个人合作一下)俞文巧 雷康哲
  • 第八章球体映射纹理 P171(一个人)张帆

一、项目描述

使用双三次贝塞尔曲线绘制青花瓷坛的线框模型,通过增加光照、材质、纹理,实现线框模型变为实体模型,达到模拟三维青花瓷坛的效果

image-20211222004717597

image-20211222004728058

二、项目设计

1.原理

映射原理:双三次Bezier曲面的各处都具有了(u,v)值。将纹理uv与曲面uv同时规范化到[0,1]区间内,当查找到曲面纹理时,先将曲面uv放大到和纹理位图一样大小的尺寸,然后获取对应纹理图像上的颜色作为曲面上该点的漫反射率系数

image-20211222004836832

image-20211222004913153

2.背面剔除算法原理:

视矢量与小平面法矢量的点积大于等于零(n·v>=0),cosα>0,表面可见

1.使用四边形网格的顶点计算面矢量并归一化

2.使用四边形网格的第一顶点与视点,计算矢量并归一化

3.计算法矢量与视矢量的点积,如果其值大于等于零,则绘制该网格,否则剔除该网格

3.球体图像纹理映射原理

3.1读入位图

纹理来自于位图,将位图作为资源导入,位图由DIB格式自动转换为DDB格式,每行存储4个字节的颜色数据,将位图转储到一维颜色数组中,就可以根据曲面网格点与位图去查询位图像素点的颜色,并将其作为材质的漫反射率和环境反射率,从而实现了将位图绑定到物体表面上

image-20211215193043390

4.算法步骤

1.将图像纹理加载到资源标签页中,将位图数据保存到一维数组中

2.定义revolution回转类,读入顶点表和表面表,圆环面全部细分为四边形网格

3.将位图绑定到回转体的顶点上

4.根据光源的位置,数量,视点的位置,材质属性,构造三维光照场景

5.使用画家算法,剔除回转体的不可见网格

6.使用三维透视投影算法,将回转体三维多边形投影为屏幕三维四边形

7.根据网格点的法矢量,插值计算小面内每一点的法矢量,将纹理作为该点的材质,调用光照模型计算小面内该点的光强

8.使用定时器改变圆环的转角生成旋转动画

2.相关算法

PhongShader算法:对顶点的法矢量进行插值,得到小面内每一点的法矢量。对小面内的每一点都调用光照模型计算光强。计算光强需要四个参数,视点位置、当前点三维坐标、当前点法矢量、当前点的材质属性。如果当前点的材质的漫反射率取自一幅图像的相应点,这就为该物体添加了纹理。

向量线性插值计算:

v(t)=(tEnd-t)/(tEnd-tStart)*vStart+(t-tStart)/(tEnd-tStart)*vEnd;

t表示第一个向量的x(y)方向单位向量,tStart表示第一个点的x(y)方向单位向量,tEnd表示第二个点的x(y)方向单位向量,vStart起点的法向量,vEnd表示第二个点的法向量。

计算光强公式:

I=Ie +Id+Is =kaIa+f(d)[KdIpmax(N·L,0)+ksIpmax(N·H)^n^]

计算物体表面上任意一点P的光强I时,必须确定物体表面的单位矢量L、单位中分矢量H、单位法矢量N以及材质的漫反射率kd。当光源位置和视点位置不变时,光矢量L和中分矢量H是一个定值。影响光强的只有漫反射率kd和单位法矢量N。

Zbuffer算法:通过使用处理深度缓冲器,若当前点的深度值小于缓冲器中的深度值,则说明当前点离视点近,使用SetPixelV函数绘制当前点,否则放弃当前点。

当前像素点的深度值:z(x,y)=-$\frac{Ax+By+D}{C}$,C≠0;

A,B,C代表法向量的坐标

3.模型

image-20220103234134907

三、部分代码

1.导入位图,格式为bmp

2.Texture类

class CTexture
{
public:
CTexture(void);
virtual~CTexture(void);
void PrepareBitmap(UINT nIDResource);//准备位图
void DeleteObject(void);//释放位图
public:
BYTE* image;//存储位图一维数组
BITMAP bmp;//BITMAP结构体变量
};

CTexture::CTexture(void)//数组初始化为空
{
image = NULL;
}

CTexture::~CTexture(void)
{
}

void CTexture::PrepareBitmap(UINT nIDResource)//准备位图
{
CBitmap NewBitmap;//DDB
NewBitmap.LoadBitmap(nIDResource);//从资源视图中读入位图存储到Newbitmap中
NewBitmap.GetBitmap(&bmp);//将CBitmap的信息保存到Bitmap结构体中,目的是为了得到图像的高度和宽度
int nbytesize = bmp.bmWidthBytes * bmp.bmHeight;//计算位图字节数
image = new BYTE[nbytesize];
NewBitmap.GetBitmapBits(nbytesize, (LPVOID)image);//将位图数据赋值到一维数组image中
}

void CTexture::DeleteObject(void)//释放位图
{
if(NULL != image)
delete []image;
}

3.在贝塞尔曲线类中绑定纹理对象

void CBezierPatch::Tessellation(CMesh Mesh)//细分曲面函数
{
double M[4][4];//系数矩阵M
M[0][0] = -1, M[0][1] = 3, M[0][2] = -3, M[0][3] = 1;
M[1][0] = 3, M[1][1] = -6, M[1][2] = 3, M[1][3] = 0;
M[2][0] = -3, M[2][1] = 3, M[2][2] = 0, M[2][3] = 0;
M[3][0] = 1, M[3][1] = 0, M[3][2] = 0, M[3][3] = 0;
CP3 P3[4][4];//曲线计算用控制点数组
for (int i = 0; i < 4; i++)
for (int j = 0; j < 4; j++)
P3[i][j] = CtrPt[i][j];
LeftMultiplyMatrix(M, P3);//系数矩阵左乘三维点矩阵
TransposeMatrix(M);//计算转置矩阵
RightMultiplyMatrix(P3, M);//系数矩阵右乘三维点矩阵
double u0, u1, u2, u3, v0, v1, v2, v3;//u,v参数的幂
double u[4] = { Mesh.BL.u,Mesh.BR.u ,Mesh.TR.u ,Mesh.TL.u };
double v[4] = { Mesh.BL.v,Mesh.BR.v ,Mesh.TR.v ,Mesh.TL.v };
for (int i = 0; i < 4; i++)
{
u3 = pow(u[i], 3.0), u2 = pow(u[i], 2.0), u1 = u[i], u0 = 1;
v3 = pow(v[i], 3.0), v2 = pow(v[i], 2.0), v1 = v[i], v0 = 1;
quadrP[i] = (u3 * P3[0][0] + u2 * P3[1][0] + u1 * P3[2][0] + u0 * P3[3][0]) * v3
+ (u3 * P3[0][1] + u2 * P3[1][1] + u1 * P3[2][1] + u0 * P3[3][1]) * v2
+ (u3 * P3[0][2] + u2 * P3[1][2] + u1 * P3[2][2] + u0 * P3[3][2]) * v1
+ (u3 * P3[0][3] + u2 * P3[1][3] + u1 * P3[2][3] + u0 * P3[3][3]) * v0;
quadrT[i].u = (pTexture->bmp.bmWidth - 1) * u[i];
quadrT[i].v = (pTexture->bmp.bmHeight - 1) * v[i];
}
}

22~27绑定纹理到四边形网格上,u、v数组是曲面四边形网格的顶点,quadrTu、quadrTv数组是相应纹理的顶点(没有计算四边形网格细分点的法向量,后续将使用球体的位置向量代替法向量)

4.ZBuffer

ZBuffer类中的PhongShader函数中,读取纹理位图的颜色数据,并将其设置为材质的漫反射率和环境反射率

//读入三角形每个顶点的坐标、点法向量和纹理坐标
void SetPoint
//纹理坐标插值函数
void LinearInterp
//顶点排序
void SortVertex
//定义纹理读取函数,该函数使用纹理坐标读出纹素的颜色值
/*从位图左下角开始读取,从而保证位图正向绘制*/
void GetTexture
class CZBuffer
{
public:
CZBuffer(void);
virtual ~CZBuffer(void);
void InitialDepthBuffer(int nWidth, int nHeight, double zDepth);//初始化深度缓冲区
void SetPoint(CP3 P0, CP3 P1, CP3 P2, CVector3 N0, CVector3 N1, CVector3 N2, CT2 T0, CT2 T1, CT2 T2);
void PhongShader(CDC* pDC, CP3 ViewPoint, CLighting* pLight, CMaterial* pMaterial, CTexture* pTexture);
private:
void SortVertex(void);//顶点排序
void EdgeFlag(CPoint2 PStart, CPoint2 PEnd, BOOL bFeature);//边标记
CVector3 LinearInterp(double t, double tStart, double tEnd, CVector3 vStart, CVector3 vEnd);//向量线性插值
CT2 LinearInterp(double t, double tStart, double tEnd, CT2 texStart, CT2 texEnd);//纹理坐标线性插值
CRGB GetTexture(int u, int v, CTexture* pTexture);//读取纹理
protected:
CP3 P0, P1, P2;//三角形的浮点数顶点
CPoint3 point0, point1, point2;//三角形的整数顶点坐标
CPoint2* SpanLeft; //跨度的起点数组标志
CPoint2* SpanRight;//跨度的终点数组标志
int nIndex;//记录扫描线条数
double** zBuffer;//深度缓冲区
int nWidth, nHeight;//缓冲区宽度与高度
};

CZBuffer::CZBuffer(void)
{
zBuffer = NULL;
}

CZBuffer::~CZBuffer(void)
{
for (int i = 0; i < nWidth; i++)
{
delete[] zBuffer[i];
zBuffer[i] = NULL;
}
if (zBuffer != NULL)
{
delete zBuffer;
zBuffer = NULL;
}
}

//读入三角形每个顶点的坐标、点法向量和纹理坐标
void CZBuffer::SetPoint(CP3 P0, CP3 P1, CP3 P2, CVector3 N0, CVector3 N1, CVector3 N2, CT2 T0, CT2 T1, CT2 T2)
{
this->P0 = P0, this->P1 = P1, this->P2 = P2;
//P0点对应的纹理坐标
point0.x = ROUND(P0.x);
point0.y = ROUND(P0.y);
point0.z = P0.z;
point0.c = P0.c;
point0.n = N0;
point0.t = T0;
//P1点对应的纹理坐标
point1.x = ROUND(P1.x);
point1.y = ROUND(P1.y);
point1.z = P1.z;
point1.c = P1.c;
point1.n = N1;
point1.t = T1;
//P2点对应的纹理坐标
point2.x = ROUND(P2.x);
point2.y = ROUND(P2.y);
point2.z = P2.z;
point2.c = P2.c;
point2.n = N2;
point2.t = T2;
}

void CZBuffer::PhongShader(CDC* pDC, CP3 ViewPoint, CLighting* pLight, CMaterial* pMaterial, CTexture* pTexture)
{
double CurrentDepth = 0.0;//当前扫描线的深度
CVector3 Vector01(P0, P1), Vector02(P0, P2);
CVector3 fNormal = CrossProduct(Vector01, Vector02);
double A = fNormal.x, B = fNormal.y, C = fNormal.z;//平面方程Ax+By+Cz+D=0的系数
double D = -A * P0.x - B * P0.y - C * P0.z;//当前扫描线随着x增长的深度步长
if (fabs(C) < 1e-4)
C = 1.0;
double DepthStep = -A / C;//计算扫描线深度步长增量
SortVertex();
//定义三角形覆盖的扫描线条数
int nTotalLine = point1.y - point0.y + 1;
//定义span的起点与终点数组
SpanLeft = new CPoint2[nTotalLine];
SpanRight = new CPoint2[nTotalLine];
//判断三角形与P0P1边的位置关系,0-1-2为右手系
int nDeltz = (point1.x - point0.x) * (point2.y - point1.y) - (point1.y - point0.y) * (point2.x - point1.x);//点向量叉积的z坐标
if (nDeltz > 0)//三角形位于P0P1边的左侧
{
nIndex = 0;
EdgeFlag(point0, point2, TRUE);
EdgeFlag(point2, point1, TRUE);
nIndex = 0;
EdgeFlag(point0, point1, FALSE);
}
else//三角形位于P0P1边的右侧
{
nIndex = 0;
EdgeFlag(point0, point1, TRUE);
nIndex = 0;
EdgeFlag(point0, point2, FALSE);
EdgeFlag(point2, point1, FALSE);
}
for (int y = point0.y; y < point1.y; y++)//下闭上开
{
int n = y - point0.y;
BOOL bInFlag = FALSE;//跨度内外测试标志,初始值为假表示三角形外部
for (int x = SpanLeft[n].x; x < SpanRight[n].x; x++)//左闭右开
{
if (bInFlag == FALSE)
{
CurrentDepth = -(A * x + B * y + D) / C;//z=-(Ax+By+D)/C
bInFlag = TRUE;
x -= 1;
}
else
{
/*
通过对三角形跨度端点的法向量进行线性插值,得到的当前点的归一化法向量(ptNormal)
*/
CVector3 ptNormal = LinearInterp(x, SpanLeft[n].x, SpanRight[n].x, SpanLeft[n].n, SpanRight[n].n);
ptNormal = ptNormal.Normalize();

/*
通过对三角形跨度两端点的纹理坐标进行线性插值,得到当前点的纹理坐标
*/

CT2 Texture = LinearInterp(x, SpanLeft[n].x, SpanRight[n].x, SpanLeft[n].t, SpanRight[n].t);//纹理坐标线性插值
Texture.c = GetTexture(ROUND(Texture.u), ROUND(Texture.v), pTexture);//读取纹理颜色
pMaterial->SetDiffuse(Texture.c);//纹理作为材质漫反射率
pMaterial->SetAmbient(Texture.c);//纹理作为材质环境反射率
CRGB Intensity = pLight->Illuminate(ViewPoint, CP3(x, y, CurrentDepth), ptNormal, pMaterial);//Intensity计算当前点的光强

/*
处理深度缓冲器,若当前点的深度值小于缓冲器中的深度值,则说明当前点离视点近,使用SetPixelV函数绘制当前 点,否则放弃当前点
*/
if (CurrentDepth <= zBuffer[x + nWidth / 2][y + nHeight / 2])//ZBuffer算法
{
zBuffer[x + nWidth / 2][y + nHeight / 2] = CurrentDepth;
pDC->SetPixelV(x, y, COLOR(Intensity));
}
CurrentDepth += DepthStep;//更新当前点的深度值
}
}
}
if (SpanLeft)
{
delete[]SpanLeft;
SpanLeft = NULL;
}
if (SpanRight)
{
delete[]SpanRight;
SpanRight = NULL;
}
}

void CZBuffer::EdgeFlag(CPoint2 PStart, CPoint2 PEnd, BOOL bFeature)
{
int dx = PEnd.x - PStart.x;
int dy = PEnd.y - PStart.y;
double m = double(dx) / dy;
double x = PStart.x;
/*
对跨度两边的纹理坐标进行线性插值,并将纹理值加入跨度左右数组
*/
for (int y = PStart.y; y < PEnd.y; y++)
{
CVector3 ptNormal = LinearInterp(y, PStart.y, PEnd.y, PStart.n, PEnd.n);
CT2 Texture = LinearInterp(y, PStart.y, PEnd.y, PStart.t, PEnd.t);
if (bFeature)
SpanLeft[nIndex++] = CPoint2(ROUND(x), y, ptNormal, Texture);
else
SpanRight[nIndex++] = CPoint2(ROUND(x), y, ptNormal, Texture);
x += m;
}
}

void CZBuffer::SortVertex(void)
{
CPoint3 pt[3];
pt[0] = point0;
pt[1] = point1;
pt[2] = point2;
for (int i = 0; i < 2; i++)
{
int min = i;
for (int j = i + 1; j < 3; j++)
if (pt[j].y < pt[min].y)
min = j;
CPoint3 pTemp = pt[i];
pt[i] = pt[min];
pt[min] = pTemp;
}
point0 = pt[0];
point1 = pt[2];
point2 = pt[1];
}
/*
纹理坐标插值函数
*/
CVector3 CZBuffer::LinearInterp(double t, double tStart, double tEnd, CVector3 vStart, CVector3 vEnd)//向量线性插值
{
CVector3 vector;
vector = (tEnd - t) / (tEnd - tStart) * vStart + (t - tStart) / (tEnd - tStart) * vEnd;
return vector;
}

CT2 CZBuffer::LinearInterp(double t, double tStart, double tEnd, CT2 texStart, CT2 texEnd)//纹理坐标线性插值
{
CT2 texture;
texture = (t - tEnd) / (tStart - tEnd) * texStart + (t - tStart) / (tEnd - tStart) * texEnd;
return texture;
}

void CZBuffer::InitialDepthBuffer(int nWidth, int nHeight, double zDepth)//初始化深度缓冲
{
this->nWidth = nWidth, this->nHeight = nHeight;
zBuffer = new double *[nWidth];
for (int i = 0; i < nWidth; i++)
zBuffer[i] = new double[nHeight];
for (int i = 0; i < nWidth; i++)//初始化深度缓冲
for (int j = 0; j < nHeight; j++)
zBuffer[i][j] = zDepth;
}
/*
定义纹理读取函数,该函数使用纹理坐标读出纹素的颜色值
*/
CRGB CZBuffer::GetTexture(int u, int v, CTexture* pTexture)
{
v = pTexture->bmp.bmHeight - 1 - v;/*从位图左下角开始读取,从而保证位图正向绘制*/
/*检测图片的边界,防止越界*/
if (u < 0) u = 0;/*左*/ if (v < 0) v = 0;/*下*/
if (u > pTexture->bmp.bmWidth - 1) u = pTexture->bmp.bmWidth - 1;/*右*/
if (v > pTexture->bmp.bmHeight - 1) v = pTexture->bmp.bmHeight - 1;/*上*/
/*查找对应纹理空间的颜色值*/
int position = v * pTexture->bmp.bmWidthBytes + 4 * u;//循环每一列,每行读四个字节,计算纹理坐标的位置
/*
颜色在内存中排列方式是BGR,所以image数组的索引position代表B,+1代表G,+2代表R
*/
return CRGB(pTexture->image[position + 2] / 255.0, pTexture->image[position + 1] / 255.0, pTexture->image[position] / 255.0);
}

四、整体设计步骤

回转体模型主要分为四个回转体对象,十二个控制点,分别是revoBody1坛身主体1, revoBody2坛身主体2, revoBottom坛底,和revoLid坛盖

image-20211215213034410

十二个控制点模拟四段三次贝塞尔曲线,每个回转体读入控制点(ReadCubicBezierControlPoint)分别调用回转类(CRevolution::DrawRevolutionSurface),DrawRevolutionSurface完成曲面绘制

image-20211215215301486

在线框模型的基础上,添加了光照与材质以及纹理来实现将线框模型变为实体模型,这里是光照与材质

image-20211215215323176

这里初始化光照,使用了一个光源

image-20211215215337177

这里读入位图,为每个回转体设置纹理

image-20211216001646721

这里使用的是球体图像纹理映射实现回转体的实体模型,将两张位图映射到四个对象上

image-20211215215651936

CBezierPatch::Tessellation,这里是顶点的纹理绑定,一个回转体上贴四张图片

image-20211215234711188

(往下滑看下PhongShader)调用ZBuffer算法进行填充

SetPoint读入三角形每个顶点的坐标、点法向量和纹理坐标

SortVertex对顶点排序

这里应用了PhongShader算法(ZBuffer类),法向量线性插值和纹理坐标线性插值算法,将纹理颜色作为材质的漫反射率和材质的环境反射率

LinearInterp纹理坐标线性插值计算,(LinearInterp)得到的当前点的归一化法向量(ptNormal),(LinearInterp) 通过对三角形跨度两端点的纹理坐标进行线性插值,得到当前点的纹理坐标

根据纹理地址使用使用GetTexture,定义纹理读取函数,使用纹理坐标读出纹素的颜色值

pMaterial->SetDiffuse(Texture.c);//纹理作为材质漫反射率
pMaterial->SetAmbient(Texture.c);//纹理作为材质环境反射率
CRGB Intensity = pLight->Illuminate(ViewPoint, CP3(x, y, CurrentDepth), ptNormal, pMaterial);
//用设置为纹理颜色的材质作为这一点的参数计算他的光强
image-20211216211153505

五、总结

通过本次课程设计,了解了微软是以从左上读入位图数据。而Ctexture类中从位图左下角开始读取,目的是为了保证位图正向绘制。

PhongShader算法中,利用了向量线性插值计算,通过对纹理坐标线性插值计算得到归一化法向量。通过对三角形跨度两端点的纹理坐标进行线性插值,得到当前点的纹理坐标。

Zbuffer算法中,利用了深度缓冲器,通过计算该点的深度值来判断是否绘制该像素点