一、实验项目要求
任务1:设计立方体类,八个顶点的颜色定义为白、红、绿、黄、蓝、品红、青、黑;
任务2:使用重心坐标算法,基于顶点颜色填充立方体的四边形表面。
任务3:使用背面剔除算法对立方体进行消隐。
任务4:使用鼠标或键盘方向键,与图形进行交互。
二、理论分析或算法分析
任务1的实现
构建一个立方体类,里面包括读点、读面以及对点的初始化,包含三角形的填充类,在这里采用斜投影来绘制线框图,再构建一个点类,用来初始化顶点颜色的变化,在立方体类中,直接初始化,顶点坐标时赋予颜色,在绘制函数中,定义一个三角形填充类的指针并开辟空间;
任务2的实现
用三角形表示法,有两种方式:第一种是平面着色模式,使用任意顶点的颜色填充三角形内部,颜色具有单一性,会出现马赫带效应,导致颜色不真实;光滑着色模式,三角形内部任意一点的颜色由三个顶点的颜色进行双线性插值得到,采用“左闭右开,下闭上开”的原则,在这次实验中,我们采用第二种着色模式,颜色是渐变性的。取一个三角形的最高点P0和最低点P1,P2位于右侧为右三角形,P2位于左侧为左三角形;设计三角形填充类Triangle,包含带颜色的浮点数二维点类和带颜色的二维整数类,填充时,将四边形分为上三角形和下三角形,最后释放内存。
任务3的实现
背面剔除就是把那些需要看见的能看见,看不见的给它弄的看不见,让你可以更直观,更有立体感,建立一个Vector来建立背面剔除类,包含三维点类,在n*v>=0的时候,是可以看见的,反之,是不可以看见的。在类中,计算向量的点积和计算向量的叉积(v0*v1=(y0*z1+z0*y1,z0*x1+x0*z1,x0*y1+y0*x1)以及重载运算符。构造向量的方法有两种,一种是坐标,另一种是.p来构造,用末点减初点,在绘制平面函数中进行消影,将四边形平分为两个三角形,采用三角形来两个面向量相加,进行背面剔除n*v>=0。
任务4的实现
使用键盘方向键:当按动左键时,可以转动5,按动右键时,又可以转动5;
使用鼠标键:设置一个定时器,当按动动画按钮(OnGraphAnimation)的时候,可以进行旋转,当再次按动动画按钮(OnUpdateGraphAnimation)的时候,可以停下来。
实现了动画的交互。
三、实现方法
主代码:
投影:
#pragma once
#include "P3.h"
class CProjection
{
public:
CProjection(void);
virtual ~CProjection(void);
void SetEye(double R);//设置视点
CP3 GetEye(void);//读取视点
CP2 ObliqueProjection(CP3 WorldPoint);//斜投影
CP2 OrthogonalProjection(CP3 WorldPoint);//正交投影
CP2 PerspectiveProjection(CP3 WorldPoint);//透视投影
private:
CP3 EyePoint;//视点
double R, d;//视径和视距
};
#include "pch.h"
#include "Projection.h"
CProjection::CProjection(void)
{
R = 1200, d = 800;
EyePoint.x = 0, EyePoint.y = 0, EyePoint.z = R;//视点位于屏幕正前方
}
CProjection::~CProjection(void)
{
}
void CProjection::SetEye(double R)//设置视径
{
EyePoint.z = R;
}
CP3 CProjection::GetEye(void)//读取视点
{
return EyePoint;
}
CP2 CProjection::ObliqueProjection(CP3 WorldPoint)//斜二测投影
{
CP2 ScreenPoint;//屏幕坐标系二维点
ScreenPoint.x = WorldPoint.x - 0.3536 * WorldPoint.z;
ScreenPoint.y = WorldPoint.y - 0.3536 * WorldPoint.z;
return ScreenPoint;
}
CP2 CProjection::OrthogonalProjection(CP3 WorldPoint)//正交投影
{
CP2 ScreenPoint;//屏幕坐标系二维点
ScreenPoint.x = WorldPoint.x;
ScreenPoint.y = WorldPoint.y;
return ScreenPoint;
}
CP2 CProjection::PerspectiveProjection(CP3 WorldPoint)
{
CP3 ViewPoint;//观察坐标系三维点
ViewPoint.x = WorldPoint.x;
ViewPoint.y = WorldPoint.y;
ViewPoint.z = EyePoint.z - WorldPoint.z;
ViewPoint.c = WorldPoint.c;
CP2 ScreenPoint;//屏幕坐标系二维点
ScreenPoint.x = d * ViewPoint.x / ViewPoint.z;
ScreenPoint.y = d * ViewPoint.y / ViewPoint.z;
ScreenPoint.c = ViewPoint.c;
return ScreenPoint;
}
背面剔除:
#pragma once
#include "P3.h"
class CVector3
{
public:
CVector3(void);
virtual ~CVector3(void);
CVector3(double x, double y, double z);//绝对向量
CVector3(const CP3 &p);
CVector3(const CP3 &p0, const CP3 &p1);//相对向量
double Magnitude(void);//计算向量的模
CVector3 Normalize(void);//归一化向量
friend CVector3 operator + (const CVector3 &v0, const CVector3 &v1);//运算符重载
friend CVector3 operator - (const CVector3 &v0, const CVector3 &v1);
friend CVector3 operator * (const CVector3 &v, double scalar);
friend CVector3 operator * (double scalar, const CVector3 &v);
friend CVector3 operator / (const CVector3 &v, double scalar);
friend double DotProduct(const CVector3 &v0, const CVector3 &v1);//计算向量的点积
friend CVector3 CrossProduct(const CVector3 &v0, const CVector3 &v1);//计算向量的叉积
private:
double x,y,z;
};
#include "pch.h"
#include "Vector3.h"
CVector3::CVector3(void)
{
x = 0.0,y = 0.0, z = 1.0;//指向z轴正向
}
CVector3::~CVector3(void)
{
}
CVector3::CVector3(double x, double y, double z)//绝对向量
{
this->x = x;
this->y = y;
this->z = z;
}
CVector3::CVector3(const CP3 &p)
{
x = p.x;
y = p.y;
z = p.z;
}
CVector3::CVector3(const CP3 &p0, const CP3 &p1)//相对向量
{
x = p1.x - p0.x;
y = p1.y - p0.y;
z = p1.z - p0.z;
}
double CVector3::Magnitude(void)//向量的模
{
return sqrt(x * x + y * y + z * z);
}
CVector3 CVector3::Normalize(void)//归一化为单位向量
{
CVector3 vector;
double magnitude = sqrt(x * x + y * y + z * z);
if(fabs(magnitude) < 1e-4)
magnitude = 1.0;
vector.x = x / magnitude;
vector.y = y / magnitude;
vector.z = z / magnitude;
return vector;
}
CVector3 operator + (const CVector3 &v0, const CVector3 &v1)//向量的和
{
CVector3 vector;
vector.x = v0.x + v1.x;
vector.y = v0.y + v1.y;
vector.z = v0.z + v1.z;
return vector;
}
CVector3 operator - (const CVector3 &v0, const CVector3 &v1)//向量的差
{
CVector3 vector;
vector.x = v0.x - v1.x;
vector.y = v0.y - v1.y;
vector.z = v0.z - v1.z;
return vector;
}
CVector3 operator * (const CVector3 &v, double scalar)//向量与常量的积
{
CVector3 vector;
vector.x = v.x * scalar;
vector.y = v.y * scalar;
vector.z = v.z * scalar;
return vector;
}
CVector3 operator * (double scalar, const CVector3 &v)//常量与向量的积
{
CVector3 vector;
vector.x = v.x * scalar;
vector.y = v.y * scalar;
vector.z = v.z * scalar;
return vector;
}
CVector3 operator / (const CVector3 &v, double scalar)//向量数除
{
if(fabs(scalar) < 1e-4)
scalar = 1.0;
CVector3 vector;
vector.x = v.x / scalar;
vector.y = v.y / scalar;
vector.z = v.z / scalar;
return vector;
}
double DotProduct(const CVector3 &v0, const CVector3 &v1)//向量的点积
{
return(v0.x * v1.x + v0.y * v1.y + v0.z * v1.z);
}
CVector3 CrossProduct(const CVector3 &v0, const CVector3 &v1)//向量的叉积
{
CVector3 vector;
vector.x = v0.y * v1.z - v0.z * v1.y;
vector.y = v0.z * v1.x - v0.x * v1.z;
vector.z = v0.x * v1.y - v0.y * v1.x;
return vector;
}
填充三角形:
#pragma once
#include "P2.h"//带颜色的浮点数二维点类
#include "Point2.h"//带颜色的整数二维点类
class CTriangle//三角形填充类
{
public:
CTriangle(void);
virtual ~CTriangle(void);
void SetPoint(CP2 P0, CP2 P1, CP2 P2);//浮点数顶点构造三角形
void GouraudShader(CDC* pDC);//填充三角形
private:
void EdgeFlag(CPoint2 PStart, CPoint2 PEnd, BOOL bFeature);//边标记
CRGB LinearInterp(double t, double tStart, double tEnd, CRGB cStart, CRGB cEnd);//颜色线性插值
void SortVertex(void);//三角形顶点排序
private:
CPoint2 point0, point1, point2;//三角形的整数顶点
CPoint2* SpanLeft; //跨度的起点数组标志
CPoint2* SpanRight;//跨度的终点数组标志
int nIndex;//扫描线索引
};
#include "pch.h"
#include "Triangle.h"
#define ROUND(d) int(d + 0.5)
CTriangle::CTriangle(void)
{
}
CTriangle::~CTriangle(void)
{
}
void CTriangle::SetPoint(CP2 P0, CP2 P1, CP2 P2)
{
point0.x = ROUND(P0.x);
point0.y = ROUND(P0.y);
point0.c = P0.c;
point1.x = ROUND(P1.x);
point1.y = ROUND(P1.y);
point1.c = P1.c;
point2.x = ROUND(P2.x);
point2.y = ROUND(P2.y);
point2.c = P2.c;
}
void CTriangle::GouraudShader(CDC* pDC)
{
SortVertex();//point0点为y坐标最小的点,point1点为y坐标最大的点,point2点的y坐标位于二者之间。如果y值相同,取x最小的点
//定义三角形覆盖的扫描线条数
int nTotalScanLine = point1.y - point0.y + 1;
//定义span的起点与终点数组
SpanLeft = new CPoint2[nTotalScanLine];
SpanRight = new CPoint2[nTotalScanLine];
//判断三角形与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;
for (int x = SpanLeft[n].x; x < SpanRight[n].x; x++)//左闭右开
{
CRGB clr = LinearInterp(x, SpanLeft[n].x, SpanRight[n].x, SpanLeft[n].c, SpanRight[n].c);
pDC->SetPixelV(x, y, RGB(clr.red * 255, clr.green * 255, clr.blue * 255));
}
}
if (SpanLeft)
{
delete[]SpanLeft;
SpanLeft = NULL;
}
if (SpanRight)
{
delete[]SpanRight;
SpanRight = NULL;
}
}
void CTriangle::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++)
{
CRGB crColor = LinearInterp(y, PStart.y, PEnd.y, PStart.c, PEnd.c);
if(bFeature)
SpanLeft[nIndex++] = CPoint2(ROUND(x), y, crColor);
else
SpanRight[nIndex++] = CPoint2(ROUND(x), y, crColor);
x += m;
}
}
void CTriangle::SortVertex(void)
{
CPoint2 pt[3];
pt[0] = point0;
pt[1] = point1;
pt[2] = point2;
for (int i = 0; i < 2; i++)
{
for (int j = i + 1; j < 3; j++)
{
int k = i;
if (pt[k].y >= pt[j].y)
k = j;
if (k == j)
{
CPoint2 ptTemp = pt[i];
pt[i] = pt[k];
pt[k] = ptTemp;
}
}
}
point0 = pt[0];
point1 = pt[2];
point2 = pt[1];
}
CRGB CTriangle::LinearInterp(double t, double tStart, double tEnd, CRGB cStart, CRGB cEnd)//颜色线性插值
{
CRGB color;
color = (tEnd - t) / (tEnd - tStart) * cStart + (t - tStart) / (tEnd - tStart) * cEnd;
return color;
}
绘制立方体:
#pragma once
#include"P3.h"
#include "Facet.h"
#include"Vector3.h"
#include"Projection.h"
#include"Triangle.h"
class CCube
{
public:
CCube(void);
virtual ~CCube(void);
void ReadPoint(void);//读入点表
void ReadFacet(void);//读入面表
CP3* GetVertexArrayName(void);//得到顶点数组名
void Draw(CDC* pDC);//绘制立方体线框
private:
CP3 P[6];//点表
CFacet F[8];//面表
CProjection projection; //投影
};
#include "pch.h"
#include "Cube.h"
#define ROUND(d) int(d + 0.5)
CCube::CCube(void)
{
}
CCube::~CCube(void)
{
}
CP3* CCube::GetVertexArrayName(void)
{
return P;
}
void CCube::ReadPoint(void)//点表
{
//顶点的三维坐标(x,y,z)
P[0].x = 0, P[0].y = 1, P[0].z = 0; P[0].c = CRGB(0.0, 0.0, 0.0);
P[1].x = -1, P[1].y = 0, P[1].z = 0; P[1].c = CRGB(1.0, 0.0, 0.0);
P[2].x = 0, P[2].y = 0, P[2].z = 1; P[2].c = CRGB(1.0, 1.0, 0.0);
P[3].x = 1, P[3].y = 0, P[3].z = 0; P[3].c = CRGB(0.0, 1.0, 0.0);
P[4].x = 0, P[4].y = 0, P[4].z = -1; P[4].c = CRGB(0.0, 0.0, 1.0);
P[5].x = 0, P[5].y = -1, P[5].z = 0; P[5].c = CRGB(1.0, 0.0, 1.0);
}
void CCube::ReadFacet(void)//面表
{
//面的顶点序号
F[0].Index[0] = 0; F[0].Index[1] = 2; F[0].Index[2] = 3; //上面的右前面
F[1].Index[0] = 0; F[1].Index[1] = 3; F[1].Index[2] = 4; //上面的右后面
F[2].Index[0] = 0; F[2].Index[1] = 1; F[2].Index[2] = 2; //上面的左前面
F[3].Index[0] = 0; F[3].Index[1] = 4; F[3].Index[2] = 1; //上面的右后面
F[4].Index[0] = 2; F[4].Index[1] = 5; F[4].Index[2] = 3; //下面的右前面
F[5].Index[0] = 3; F[5].Index[1] = 5; F[5].Index[2] = 4; //下面的右后面
F[6].Index[0] = 1; F[6].Index[1] = 5; F[6].Index[2] = 2; //下面的左前面
F[7].Index[0] = 1; F[7].Index[1] = 4; F[7].Index[2] = 5; //下面的左后面
}
void CCube::Draw(CDC* pDC)
{
CP2 ScreenPoint[4];//屏幕二维点
CP3 ViewPoint = projection.GetEye();//视点
CTriangle* pFill = new CTriangle;//申请内存
for (int nFacet = 0; nFacet < 8; nFacet++)//面循环
{
CP3 ViewPoint = projection.GetEye();//视点
CVector3 ViewVector(P[F[nFacet].Index[0]], ViewPoint);// 面的视向量
ViewVector = ViewVector.Normalize();//视向量单位化
CVector3 Vector01(P[F[nFacet].Index[0]], P[F[nFacet].Index[1]]);//边向量
CVector3 Vector02(P[F[nFacet].Index[0]], P[F[nFacet].Index[2]]);
CVector3 FacetNormal = CrossProduct(Vector01, Vector02);//面法向量
FacetNormal = FacetNormal.Normalize();
if (DotProduct(ViewVector, FacetNormal) >= 0)//背面剔除算法
{
for (int nPoint = 0; nPoint < 3; nPoint++)
{
ScreenPoint[nPoint] = projection.PerspectiveProjection(P[F[nFacet].Index[nPoint]]);
}
pFill->SetPoint(ScreenPoint[0], ScreenPoint[1], ScreenPoint[2]);//下三角形
pFill->GouraudShader(pDC);
}
}
delete pFill;//释放内存
}
使用键盘方向键或者鼠标进行交互:
键盘方向键:
void CTestView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
switch (nChar)
{
case VK_UP:
Alpha = -5;
transform.RotateX(Alpha);
break;
case VK_DOWN:
Alpha = +5;
transform.RotateX(Alpha);
break;
case VK_LEFT:
Beta = -5;
transform.RotateY(Beta);
break;
case VK_RIGHT:
Beta = +5;
transform.RotateY(Beta);
break;
default:
break;
}
Invalidate(FALSE);
CView::OnKeyDown(nChar, nRepCnt, nFlags);
}
鼠标按钮:
void CTestView::OnTimer(UINT_PTR nIDEvent)
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
Alpha = 10, Beta = 10;
transform.RotateX(Alpha);
transform.RotateY(Beta);
Invalidate(FALSE);
CView::OnTimer(nIDEvent);
}
void CTestView::OnGraphAnimation()
{
// TODO: 在此添加命令处理程序代码
bPlay = !bPlay;
if (bPlay)//设置定时器
SetTimer(1, 300, NULL);
else
KillTimer(1);
}
void CTestView::OnUpdateGraphAnimation(CCmdUI *pCmdUI)
{
// TODO: 在此添加命令更新用户界面处理程序代码
if (bPlay)
pCmdUI->SetCheck(TRUE);
else
pCmdUI->SetCheck(FALSE);
}
四、实验结果分析
实验结果
实验总结
在这次实验中,刚开始出现了线连不起来,然后在仔细观察后,是绘制颜色那块出了错误,所以在之后的实验中要仔细,这样就不会出错。