图形学入门案例(一)

前言

最近开始学习计算机图形学,所以开始收集一些比较基础又非常有意思的案例。

 

 

 

 


使用环境与基本常识

MFC

我们使用MFC(Microsoft Foundation Classes,微软基础类库)来学习图形学

 

项目创建

在VS中创建一个MFC项目,应用程序类型是单个文档、项目样式是MFC standard、使用MFC选择在静态库中使用MFC。

在共享DLL中使用MFC指的是打包时一些MFC的DLL的内容没有被包含在EXE文件中,所以EXE文件较小,但是运行时要求系统中要有相关的DLL文件。

在静态库中使用MFC是将DLL中的相关代码写进EXE文件中,文件较大,但是可以在没有相关DLL的机器上运行。
创建完成。

如何编写

我们主要在MFCXXXView.cpp文件中通过实现OnDraw方法来实现图形的绘制。

 

CDC

Windows使用与设备无关的图形设备环境(DC :Device Context) 进行显示 。MFC基础类库定义了设备环境对象类—-CDC类。
说道CDC类就不能不提一下GdiObject—图形对象类。 在Windows应用程序中,设备环境与图形对象共同工作,协同完成绘图显示工作。就像画家绘画一样,设备环境好比是画家的画布,图形对象好比是画家的画笔。用画笔在画布上绘画,不同的画笔将画出不同的画来。选择合适的图形对象和绘图对象,才能按照要求完成绘图任务。

 

 


中心坐标

映射模式

图形从窗口显示到视区的过程称为映射,根据映射模式的不同可以分为逻辑坐标和设备坐标,逻辑坐标的单位是物理尺度,设备坐标的单位是像素。

“窗口”可以看做一个画布,依赖于逻辑坐标,可以是像素点、毫米或程序员想要的其他尺度。
“视区”可以看做我们的视野,窗口(画布)上的东西映射到视区上才能显示出来,依赖于设备坐标(像素点)。通常,视口和客户区域等同。

方法说明

  • int SetMapMode( int nMapMode ):设置映射模式
    • nMapMode:映射模式
    • 返回值:原映射模式
    • 说明:SetMapMode()函数设置映射模式,定义了将逻辑坐标转换为设备坐标的度量单位,并定义了设备坐标系的x轴和y轴方向。
  • CSize SetWindowExt( int cx, int cy ):设置窗口范围函数
    • cx:窗口x方向宽度的逻辑坐标
    • cy:窗口y方向高度的逻辑坐标
    • 返回值:原窗口范围的CSize对象
  • CSize SetViewportExt( int cx, int cy ):设置视区范围函数
    • cx:视区x方向宽度的设备坐标
    • cy:视区y方向高度的设备坐标
    • 返回值:原视区范围的CSize对象
  • CPoint SetViewportOrg( int x, int y ):设置视区坐标原点函数
    • x,y是视区的新原点坐标
    • 返回值:原视区原点的CPoint对象
  • void  GetClientRect( LPRECT  lpRect )  const:获取客户区矩形信息
    • RECT结构体或CRect对象,用于接收客户区坐标
    • 返回值:无
    • 特别注意:参数可以不使用&地址运算符,但是内部实现的时候其实是自动会处理地址。

函数后加const:只有类的非静态成员函数后可以加const修饰,表示该类的this指针为const类型,不能改变类的成员变量的值,即成员变量为read only(例外情况见2),任何改变成员变量的行为均为非法。此类型的函数可称为只读成员函数

原理说明(思索了一下午的重点)

我理解的原理步骤是这样的:

  1. 先获得客户端矩阵,方便我们得到窗口的长宽
  2. 修改pDC设备上下文对象的映射模式,允许修改逻辑坐标单位方向等信息
  3. 设置窗口范围为刚才得到的客户端的宽高,然后设置视区范围,这里传参要将y轴翻正(原情况y轴向下为正),这里我们可以理解为让窗口设置为客户端长宽,然后窗口对应视区(视口)的映射是上下颠倒的,这样在我们看的视区里,y轴就翻正了
  4. 设置视区的中心为原点坐标

编程实现

//改变原点坐标
void CMFCApplication1View::ChangeOriPos(CDC* pDC) {
	CRect rect;
	//获取客户端矩形信息
	GetClientRect(&rect);
	//设置映射模式(逻辑坐标的单位方向、比例、独立设置)
	pDC->SetMapMode(MM_ANISOTROPIC);

	//下面两个方法设置逻辑坐标和设备坐标的对应关系
	//设置窗口范围
	pDC->SetWindowExt(rect.Width(), rect.Height());
	//设置视区范围(此处即将y轴翻正)
	pDC->SetViewportExt(rect.Width(), -rect.Height());

	//设置客户区中心
	pDC->SetViewportOrg(rect.Width() / 2, rect.Height() / 2);
	//移动客户端的rect
	/*为什么要移动?
		因为我们之前的rect客户端矩形的内容
		大概是{left:0,right:600,top:0,bottom:900},
		我们下面要利用rect.left等属性绘制,故变换原
		点坐标后需要移动一下rect这个矩形,方便我们利用
		left、right等属性。
	*/
	rect.OffsetRect(-rect.Width() / 2, -rect.Height() / 2);

	//绘制坐标轴
	pDC->MoveTo(rect.left,0);
	pDC->LineTo(rect.right, 0);
	pDC->MoveTo(0, rect.top);
	pDC->LineTo(0, rect.bottom);

	pDC->MoveTo(100, 100);
	pDC->LineTo(200, 200);

	pDC->MoveTo(-100, -100);
	pDC->LineTo(-200, -200);
}

效果

注:理解上段代码请不要纠结客户端的rect,这个东西不管怎么变,都不影响我们的原点坐标的设置。

 

 


阴阳鱼

绘制方法

  • BOOL Pie( LPCRECT lpRect, POINT ptStart, POINT ptEnd );
    • lpRect:外接矩形
    • ptStart:圆弧的起点坐标
    • ptEnd:圆弧的终点坐标
    • 返回值:如果成功,返回非0值;否则为0
    • 说明:弧线使用当前画笔绘制,以逆时针方向移动。从每个端点到弧线中心绘制两条额外直线。用当前画刷填充扇形区域。

理论分析

我们绘制一个动态的黑色半圆,需要确定其外接矩阵、起始坐标、终点坐标,故:

代码实践

黑色半圆

在VS中创建一个MFC项目。

void CyinYangFIshView::OnDraw(CDC* pDC)
{
	CyinYangFIshDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: 在此处为本机数据添加绘制代码
	ChangeOriPos(pDC);
	DrawObject(pDC);
}

//改变原点坐标
void CyinYangFIshView::ChangeOriPos(CDC* pDC) {
	CRect rect;
	//获取客户端矩形信息
	GetClientRect(&rect);
	//设置映射模式(逻辑坐标的单位方向、比例、独立设置)
	pDC->SetMapMode(MM_ANISOTROPIC);

	//下面两个方法设置逻辑坐标和设备坐标的对应关系
	//设置窗口范围
	pDC->SetWindowExt(rect.Width(), rect.Height());
	//设置视区范围(此处即将y轴翻正)
	pDC->SetViewportExt(rect.Width(), -rect.Height());

	//设置客户区中心
	pDC->SetViewportOrg(rect.Width() / 2, rect.Height() / 2);
}
void CyinYangFIshView::DrawObject(CDC* pDC) {
	CPen NewPen, *pOldPen;
	CBrush NewBrush, *pOldBrush;
	long R = 300;  //太极图的半径
	CPoint ld, rt, as, ae;
	//ld(left down)左下角点,rt(right top)右上角点
	//as(arc start)圆弧起点,ae(arc end)圆弧终点
	#pragma region blackCircleHalf
	//绘制黑色半圆
	NewBrush.CreateSolidBrush(RGB(0, 0, 0));
	pOldBrush = pDC->SelectObject(&NewBrush);
	//0度角为X轴
	ld = CPoint(-R, -R);
	rt = CPoint(R, R);
	as = CPoint(ROUND(R*cos((Theta - 90)*PI / 180)),ROUND(R*sin((Theta - 90)*PI / 180)));
	ae = CPoint(ROUND(R*cos((Theta + 90)*PI / 180)),ROUND(R*sin((Theta + 90)*PI / 180)));
	pDC->Pie(CRect(ld, rt), as, ae);
	pDC->SelectObject(pOldBrush);
	NewBrush.DeleteObject();
	#pragma endregion
}

这样即可看到绘制半圆的效果。

 

注意,上段代码中:

ROUND是宏定义:#define ROUND(d) int(d+0.5)
PI是宏定义:#define PI 3.1415926

Theta是CXXXView类的float成员变量,在构造函数中被初始化为0。

白色半圆

我们绘制白色半圆同理,修改颜色、起始点坐标即可:

#pragma region whiteCircleHalf
	//改变画刷颜色
	NewBrush.CreateSolidBrush(RGB(255, 255, 255));
	pOldBrush = pDC->SelectObject(&NewBrush);
	ld = CPoint(-R, -R); 
	rt = CPoint(R, R);
	as = CPoint(ROUND(R*cos((Theta + 90)*PI / 180)), ROUND(R*sin((Theta + 90)*PI / 180)));
	ae = CPoint(ROUND(R*cos((Theta - 90)*PI / 180)), ROUND(R*sin((Theta - 90)*PI / 180)));
	pDC->Pie(CRect(ld, rt), as, ae);
	pDC->SelectObject(pOldBrush);
	NewBrush.DeleteObject();
#pragma endregion

黑白鱼头

画鱼头,我们需要利用Ellipse函数,这个函数的参数是一个外接矩形,分析一下左下角和右上角的坐标:

推理思路:

  1. 先推理出鱼头的原点
  2. 再推理出外接矩形的左下角/右上角坐标

例如上图的原点的坐标:

故,原点的X坐标就是 r*cos(Theta-90),故,外接矩形右上角的X坐标就要再加个r;左下角同理;y坐标的分析同理。

 

 

同理,白色鱼头和黑色鱼头的关系就是

黑白鱼眼

鱼眼的分析和鱼头类似:

#pragma region whiteFishEye
	//绘制白色鱼眼
	long r1 = R / 8;//鱼眼半径
	NewPen.CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
	pOldPen = pDC->SelectObject(&NewPen);
	ld = CPoint(ROUND(r*cos((Theta - 90)*PI / 180) - r1), ROUND(r*sin((Theta - 90)*PI / 180) - r1));
	rt = CPoint(ROUND(r*cos((Theta - 90)*PI / 180) + r1), ROUND(r*sin((Theta - 90)*PI / 180) + r1));
	pDC->Ellipse(CRect(ld, rt));
	pDC->SelectObject(pOldPen);
	NewPen.DeleteObject();
#pragma endregion
#pragma region blackFishEye
	//黑色鱼眼
	NewBrush.CreateSolidBrush(RGB(0, 0, 0));
	pOldBrush = pDC->SelectObject(&NewBrush);
	ld = CPoint(ROUND(r*cos((Theta + 90)*PI / 180) - r1), ROUND(r*sin((Theta + 90)*PI / 180) - r1));
	rt = CPoint(ROUND(r*cos((Theta + 90)*PI / 180) + r1), ROUND(r*sin((Theta + 90)*PI / 180) + r1));
	pDC->Ellipse(CRect(ld, rt));
	pDC->SelectObject(pOldBrush);
	NewBrush.DeleteObject();
#pragma endregion

双缓冲绘图

void CyinYangFIshView::OnDraw(CDC* pDC)
{
	CyinYangFIshDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: 在此处为本机数据添加绘制代码
	ChangeOriPos(pDC);

#pragma region doubleBuffer
	//双缓冲绘图
	CRect rect;
	GetClientRect(&rect);
	CDC memDC;//声明内存DC
	CBitmap NewBitmap, *pOldBitmap;
	memDC.CreateCompatibleDC(pDC);//创建一个与显示DC兼容的内存DC 
	NewBitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());//创建兼容内存位图 
	pOldBitmap = memDC.SelectObject(&NewBitmap);//将兼容位图选入内存DC
	memDC.FillSolidRect(rect, pDC->GetBkColor());//按原来背景色填充客户区,否则是黑色
	rect.OffsetRect(-rect.Width() / 2, -rect.Height() / 2);
	memDC.SetMapMode(MM_ANISOTROPIC);//内存DC自定义坐标系
	memDC.SetWindowExt(rect.Width(), rect.Height());
	memDC.SetViewportExt(rect.Width(), -rect.Height());
	memDC.SetViewportOrg(rect.Width() / 2, rect.Height() / 2);
	DrawObject(&memDC);
	pDC->BitBlt(rect.left, rect.top, rect.Width(), rect.Height(), &memDC, -rect.Width() / 2, -rect.Height() / 2, SRCCOPY); //将内存DC中的位图拷贝到设备DC
	memDC.SelectObject(pOldBitmap);
#pragma endregion
}

实现转动

私有成员Theta,我们会定时去控制。

点击类向导,生成WM_TIMER的对应的消息响应函数。

void CyinYangFIshView::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	Theta += 1;
	if (Theta == 360)
		Theta = 0;
	Invalidate(FALSE);

	CView::OnTimer(nIDEvent);
}

修改资源视图,设置图标,给图标ID绑定对应的事件(利用类向导,在View文件中创建响应函数):

//点击事件
void CyinYangFIshView::OnAnimationDo()
{
	// TODO: 在此添加命令处理程序代码
	bPlay = !bPlay;
	if (bPlay) //设置定时器
		SetTimer(1, 30, NULL);
	else
		KillTimer(1);
}
//菜单界面的变化
void CyinYangFIshView::OnUpdateAnimationDo(CCmdUI *pCmdUI)
{
	// TODO: 在此添加命令更新用户界面处理程序代码
	if (bPlay)
		pCmdUI->SetCheck(TRUE);  //菜单界面上的效果
	else
		pCmdUI->SetCheck(FALSE);
}

最终效果

 


二维/三维坐标系

计算机图形学中常用的坐标系

计算机图形学中常用的坐标系有世界坐标系、建模坐标系、观察坐标系、屏幕坐标系、 设备坐标系和规格化设备坐标系等。

世界坐标系

描述三维场景的固定坐标系称为世界坐标系。世界坐标系是一个特殊的坐标系,它建 立了描述其它坐标系所需要的参考框架。世界坐标系也称为全局坐标系,是实数域直角坐 标系。三维世界坐标系可分为右手坐标系与左手坐标系两种。

建模坐标系

描述物体几何模型的坐标系称为建模坐标系或物体坐标系,有时也相对于全局坐标系 而称为局部坐标系。每个物体建模时都有自己独立的坐标系。当物体移动或改变方向时, 与该物体相关联的坐标系将随之移动或改变方向。建模坐标系也是实数域坐标系,建模坐 标系的原点可以放在物体的任意位置上。

在建模坐标系中完成物体的建模后,把物体放入场景中的过程实际上定义了物体从建 模坐标系向世界坐标系的变换。当绘制仅由单物体组成的场景时,建模坐标系常与世界坐 标系重合,用户一般感觉不到世界坐标系的存在。

观察坐标系

观察坐标系是在世界坐标系中定义的直角坐标系。由于视点可以看做人眼,所以观察 坐标系也称为眼睛坐标系。

屏幕坐标系

屏幕坐标系为整数域二维直角坐标系,原点位于屏幕中心,xs 轴水平向右为正,ys 轴 垂直向上为正

设备坐标系

显示器等图形输出设备自身都带有一个二维直角坐标系称为设备坐标系。

 

正方体建模(重点)

新建一个单个文档的MFC项目,然后开始设置:

二维点类

二维点类:

class CP2
{
public:
	CP2(void);
	virtual ~CP2(void);
	CP2(double x, double y);
public:
	double x, y,w;
};
CP2::CP2(void)
{
	x = 0;
	y = 0;
}


CP2::~CP2(void)
{
}
CP2::CP2(double x, double y) {
	this->x = x;
	this->y = y;
}

三维点类

然后是三维点类(继承自二维点类):

class CP3:public CP2
{
public:
	CP3(void);
    virtual ~CP3(void);
	CP3(double x, double y, double z);
public:
	double z;
};
CP3::CP3()
{
	x = 0;
	y = 0;
	z = 0;
}

CP3::CP3(double x, double y, double z) :CP2(x, y) {
	this->z = z;
}

CP3::~CP3()
{
}

下面是面的设计:

class CFacet
{
public:
	CFacet(void);
	virtual ~CFacet();
	void SetPtNumber(int Number);
public:
	int Number; //面中的顶点数
	int Index[4];  //顶点索引下标(默认4个顶点)
};
CFacet::CFacet(void)
{
}


CFacet::~CFacet(void)
{
}

void CFacet::SetPtNumber(int Number) {
	this->Number = Number;
}

如果需要处理任意顶点数(Number>4)的小面,可以将索引号静 态数组设计为动态数组,用 new 运算符来创建,用 delete 运算符来撤销。

立方体

class CCube
{
public:
	CCube(void);
	virtual ~CCube();
	void ReadVertex(void);
	void ReadFacet(void);
	void Draw(CDC* pDC);
public:
	CP3 P[8];
	CFacet F[6];
};
#define ROUND(d) int(d + 0.5)                //四舍五入宏定义 

CCube::CCube(void)
{
}

CCube::~CCube(void)
{
}

void CCube::ReadVertex(void) {
	P[0].x = 0, P[0].y = 0, P[0].z = 0; 
	P[1].x = 1, P[1].y = 0, P[1].z = 0;
	P[2].x = 1, P[2].y = 1, P[2].z = 0;
	P[3].x = 0, P[3].y = 1, P[3].z = 0;
	P[4].x = 0, P[4].y = 0, P[4].z = 1;
	P[5].x = 1, P[5].y = 0, P[5].z = 1; 
	P[6].x = 1, P[6].y = 1, P[6].z = 1; 
	P[7].x = 0, P[7].y = 1, P[7].z = 1;
}
void CCube::ReadFacet(void) {
	F[0].Number=4,F[0].Index[0]=4,F[0].Index[1]=5;
	F[0].Index[2] = 6, F[0].Index[3] = 7;//前面   
	F[1].Number = 4, F[1].Index[0] = 0, F[1].Index[1] = 3;
	F[1].Index[2] = 2, F[1].Index[3] = 1;//后面   
	F[2].Number = 4, F[2].Index[0] = 0, F[2].Index[1] = 4;
	F[2].Index[2] = 7, F[2].Index[3] = 3;//左面   
	F[3].Number = 4, F[3].Index[0] = 1, F[3].Index[1] = 2;
	F[3].Index[2] = 6, F[3].Index[3] = 5;//右面   
	F[4].Number = 4, F[4].Index[0] = 2, F[4].Index[1] = 3;
	F[4].Index[2] = 7, F[4].Index[3] = 6;//顶面 
	F[5].Number = 4, F[5].Index[0] = 0, F[5].Index[1] = 1;
	F[5].Index[2] = 5, F[5].Index[3] = 4;//底面
}
void CCube::Draw(CDC* pDC) {
	CP2 ScreenPoint, temp;
	//访问立方体的6个表面
	for (int nFacet = 0; nFacet < 6; nFacet++) { 
		//访问面内的4个顶点
		for (int nPoint = 0; nPoint < F[nFacet].Number; nPoint++) {
			//正交投影
			ScreenPoint.x = P[F[nFacet].Index[nPoint]].x;
			ScreenPoint.y = P[F[nFacet].Index[nPoint]].y;
			if (0 == nPoint) {
				pDC->MoveTo(ROUND(ScreenPoint.x), ROUND(ScreenPoint.y));
				temp = ScreenPoint;
			}
			else {
				pDC->LineTo(ROUND(ScreenPoint.x), ROUND(ScreenPoint.y));
			}
		}
		//闭合四边形
		pDC->LineTo(ROUND(temp.x), ROUND(temp.y));
	}
}

程序说明:循环访问每个表面内的三维顶点,直接取三维顶点的 x 坐标和 y 坐标进行 绘制。这种投影方式称为正交投影,投影平面为 xOy 面。

三维变换◊

这个CTransform3是我们三维变换的重中之重。

具体的图形学变换原理请参考网上很多公开的资料,我这里就仅将代码贴出。

三维几何变换类

#include"CP3.h"
class CTransform3
{
public:
	CTransform3(void);
	virtual ~CTransform3(void);
	void SetMatrix(CP3* P, int ptNumber); //三维顶点数组初始化
	void Identity(void);              //单位矩阵初始化
	void Translate(double tx, double ty, double tz);//平移变换
	void Scale(double sx, double sy, double sz); //缩放变换
	void Scale(double sx, double sy, double sz, CP3 p); //相对于任意点的缩放变换
	void Scale(double s);  //整体缩放变换
	void Scale(double s, CP3 p); //相对于任意点的整体缩放变换
	void RotateX(double beta); //绕X轴旋转变换
	void RotateY(double beta); //绕Y轴旋转变换
	void RotateZ(double beta); //绕Z轴旋转变换
	void RotateX(double beta,CP3 p); //相对于任意点的绕X轴旋转变换
	void RotateY(double beta,CP3 p); //相对于任意点的绕Y轴旋转变换
	void RotateZ(double beta,CP3 p); //相对于任意点的绕Z轴旋转变换
	void ReflectX(void); //关于X轴反射变换
	void ReflectY(void); //关于Y轴反射变换
	void ReflectZ(void); //关于Z轴反射变换
	void ReflectXOY(void); //关于XOY面反射变换
	void ReflectYOZ(void); //关于YOZ面反射变换
	void ReflectZOX(void); //关于ZOX面反射变换
	void ShearX(double b, double c); //沿X方向错切变换
	void ShearY(double d, double f); //沿Y方向错切变换
	void ShearZ(double g, double h); //沿Z方向错切变换
	void MultiplyMatrix(void); //矩阵相乘
private:
	double M[4][4]; //三维变换矩阵
	CP3* P;  //三维顶点数组名
	int ptNumber;  //三维顶点个数
};
#include "stdafx.h"
#include "CTransform3.h"

#define PI 3.1415926

CTransform3::CTransform3()
{
}

CTransform3::~CTransform3()
{
}
//顶点数组初始化
void CTransform3::SetMatrix(CP3 * P, int ptNumber) 
{
	this->P = P;
	this->ptNumber = ptNumber;
}
//单位矩阵
void CTransform3::Identity(void)
{
	M[0][0] = 1.0, M[0][1] = 0.0, M[0][2] = 0.0, M[0][3] = 0.0;
	M[1][0] = 0.0, M[1][1] = 1.0, M[1][2] = 0.0, M[1][3] = 0.0;
	M[2][0] = 0.0, M[2][1] = 0.0, M[2][2] = 1.0, M[2][3] = 0.0;
	M[3][0] = 0.0, M[3][1] = 0.0, M[3][2] = 0.0, M[3][3] = 1.0;
}
//平移变换
void CTransform3::Translate(double tx, double ty, double tz)
{
	Identity();
	M[0][3] = tx, M[1][3] = ty, M[2][3] = tz;
	MultiplyMatrix();
}
//缩放变换
void CTransform3::Scale(double sx, double sy, double sz)
{
	Identity();
	M[0][0] = sx, M[1][1] = sy, M[2][2] = sz;
	MultiplyMatrix();
}
//相对于任意点的缩放变换
void CTransform3::Scale(double sx, double sy, double sz, CP3 p)
{
	Translate(-p.x, -p.y, -p.z);
	Scale(sx, sy, sz);
	Translate(p.x, p.y, p.z);
}
//整体缩放变换
void CTransform3::Scale(double s)
{
	Identity();
	M[0][0] = s, M[1][1] = s, M[2][2] = s;
	MultiplyMatrix();
}
//相对于任意点的整体缩放变换
void CTransform3::Scale(double s, CP3 p)
{
	Translate(-p.x, -p.y, -p.z);
	Scale(s);
	Translate(p.x, p.y, p.z);
}
//绕X轴的旋转变换
void CTransform3::RotateX(double beta)
{
	Identity();
	beta = beta * PI / 180;
	M[1][1] = cos(beta), M[1][2] = -sin(beta);
	M[2][1] = sin(beta), M[2][2] = cos(beta);
	MultiplyMatrix();
}
//绕Y轴的旋转变换
void CTransform3::RotateY(double beta)
{
	Identity();
	beta = beta * PI / 180;
	M[0][0] = cos(beta), M[0][2] = sin(beta);
	M[2][0] = -sin(beta), M[2][2] = cos(beta);
	MultiplyMatrix();
}
//绕Z轴的旋转变换
void CTransform3::RotateZ(double beta)
{
	Identity();
	beta = beta * PI / 180;
	M[0][0] = cos(beta), M[0][1] = -sin(beta);
	M[1][0] = sin(beta), M[1][1] = cos(beta);
	MultiplyMatrix();
}
//相对于任意点的绕X轴的旋转变换
void CTransform3::RotateX(double beta, CP3 p)
{
	Translate(-p.x, -p.y, -p.z);
	RotateX(beta);
	Translate(p.x, p.y, p.z);
}
//相对于任意点的绕Y轴的旋转变换
void CTransform3::RotateY(double beta, CP3 p)
{
	Translate(-p.x, -p.y, -p.z);
	RotateY(beta);
	Translate(p.x, p.y, p.z);
}
//相对于任意点的绕Z轴的旋转变换
void CTransform3::RotateZ(double beta, CP3 p)
{
	Translate(-p.x, -p.y, -p.z);
	RotateZ(beta);
	Translate(p.x, p.y, p.z);
}
//关于X轴的反射变换
void CTransform3::ReflectX(void)
{
	Identity();
	M[1][1] = -1, M[2][2] = -1;
	MultiplyMatrix();
}
//关于Y轴的反射变换
void CTransform3::ReflectY(void)
{
	Identity();
	M[0][0] = -1, M[2][2] = -1;
	MultiplyMatrix();
}
//关于Z轴的反射变换
void CTransform3::ReflectZ(void)
{
	Identity();
	M[0][0] = -1, M[1][1] = -1;
	MultiplyMatrix();
}
//关于XOY面的反射变换
void CTransform3::ReflectXOY(void)
{
	Identity();
	M[2][2] = -1;
	MultiplyMatrix();
}
//关于YOZ面的反射变换
void CTransform3::ReflectYOZ(void)
{
	Identity();
	M[0][0] = -1;
	MultiplyMatrix();
}
//关于ZOX面的反射变换
void CTransform3::ReflectZOX(void)
{
	Identity();
	M[0][0] = -1;
	MultiplyMatrix();
}
//沿X方向的错切变换
void CTransform3::ShearX(double b, double c)
{
	Identity();
	M[0][1] = b, M[0][2] = c;
	MultiplyMatrix();
}
//沿Y方向的错切变换
void CTransform3::ShearY(double d, double f)
{
	Identity();
	M[1][0] = d, M[1][2] = f;
	MultiplyMatrix();
}
//沿Z方向的错切变换
void CTransform3::ShearZ(double g, double h)
{
	Identity();
	M[2][0] = g, M[2][1] = h;
	MultiplyMatrix();
}

void CTransform3::MultiplyMatrix(void)
{
	CP3* PTemp = new CP3[ptNumber];
	for (int i = 0; i < ptNumber; i++) {
		PTemp[i] = P[i];
	}
	for (int i = 0; i < ptNumber; i++) {
		P[i].x = M[0][0] * PTemp[i].x + M[0][1] * PTemp[i].y
			+ M[0][2] * PTemp[i].z + M[0][3] * PTemp[i].w;
		P[i].y = M[1][0] * PTemp[i].x + M[1][1] * PTemp[i].y
			+ M[1][2] * PTemp[i].z + M[1][3] * PTemp[i].w;
		P[i].z = M[2][0] * PTemp[i].x + M[2][1] * PTemp[i].y
			+ M[2][2] * PTemp[i].z + M[2][3] * PTemp[i].w;
		P[i].w = M[3][0] * PTemp[i].x + M[3][1] * PTemp[i].y
			+ M[3][2] * PTemp[i].z + M[3][3] * PTemp[i].w;
	}
	delete[] PTemp;
}

我们的CTransform3类中,P是我们的某图形的坐标点集,M是我们的变换矩阵,需要变换的时候就M与P矩阵连乘,即可实现变换。

基于齐次坐标的三维顶点使用一维数组P表示,元素类型为CP3,ptNumber是数组元素个数。

投影

Transform类有效的帮助我们去将立方体的顶点的值进行处理改变,下一步就是将世界坐标中的立方体投影到屏幕坐标系中了:

#pragma once
#include "CP3.h"
class CProjection
{
public:
	CProjection(void);
	virtual ~CProjection(void);
	void InitialParameter(void);//透视投影参数初始化
	CP2 PerspectiveProjection(CP3 WorldPoint);//透视投影
	CP2 OrthogonalProjection(CP3 WorldPoint);//正交投影
	CP2 CavalierProjection(CP3 WorldPoint);//斜等测投影
	CP2 CabinetProjection(CP3 WorldPoint);//斜二测投影
public:
	CP3 EyePoint;//视点
private:
	double k[8];//透视投影常数
	double R, Psi, Phi, d;//视点的球坐标
};
#include "StdAfx.h"
#include "Projection.h"
#include "math.h"
#define PI 3.1415926

CProjection::CProjection()
{
}

CProjection::~CProjection()
{
}

#pragma region Orthogonal
//正交投影
CP2 CProjection::OrthogonalProjection(CP3 WorldPoint) {
	CP2 ScreenPoint;
	ScreenPoint.x = WorldPoint.x;
	ScreenPoint.y = WorldPoint.y;
	return ScreenPoint;
}
#pragma endregion

#pragma region Oblique
//斜等测投影
CP2 CProjection::CavalierProjection(CP3 WorldPoint)
{
	CP2 ScreenPoint;
	double Cota = 1;
	double Beta = PI / 4;
	ScreenPoint.x = WorldPoint.x - WorldPoint.z*Cota*cos(Beta);
	ScreenPoint.y = WorldPoint.y - WorldPoint.z*Cota*cos(Beta);
	return ScreenPoint;
}
//斜二测投影
CP2 CProjection::CabinetProjection(CP3 WorldPoint)
{
	CP2 ScreenPoint;
	double Cota = 0.5;
	double Beta = PI / 4;
	ScreenPoint.x = WorldPoint.x - WorldPoint.z*Cota*cos(Beta);
	ScreenPoint.y = WorldPoint.y - WorldPoint.z*Cota*cos(Beta);
	return ScreenPoint;
}
#pragma endregion

#pragma region Perspective 
//透视投影
void CProjection::InitialParameter(void)
{
	k[0] = sin(PI*Phi / 180); 
	k[1] = cos(PI*Phi / 180);
	k[2] = sin(PI*Psi / 180);
	k[3] = cos(PI*Psi / 180);
	k[4] = k[0] * k[2];
	k[5] = k[0] * k[3];
	k[6] = k[1] * k[2];
	k[7] = k[1] * k[3];
	//EyePoint代表视点,R代表视径
	EyePoint.x = k[4] * R;
	EyePoint.y = k[1] * R;
	EyePoint.z = k[5] * R;
}
CP2 CProjection::PerspectiveProjection(CP3 WorldPoint)
{
	//观察坐标系三维点
	CP3 ViewPoint;
	ViewPoint.x = k[3] * WorldPoint.x - k[2] * WorldPoint.z;
	ViewPoint.y = -k[6] * WorldPoint.x + k[0] * WorldPoint.y-k[7]*WorldPoint.z;
	ViewPoint.z = -k[4] * WorldPoint.x - k[1] * WorldPoint.y-k[5]*WorldPoint.z+R;
	//屏幕坐标系二维点
	CP2 ScreenPoint;
	ScreenPoint.x = d * ViewPoint.x / ViewPoint.z;
	ScreenPoint.y = d * ViewPoint.y / ViewPoint.z;
	return ScreenPoint;
}
#pragma endregion

在对应的Cube类的Draw方法中定义好绘制:

void CCube::Draw(CDC* pDC)
{
	CP2 ScreenPoint, temp;
	for (int nFacet = 0; nFacet < 6; nFacet++)//面循环
	{
		for (int nPoint = 0; nPoint < F[nFacet].Number; nPoint++)//顶点循环
		{
			//ScreenPoint = projection.CabinetProjection(P[F[nFacet].Index[nPoint]]);
			//ScreenPoint = projection.CavalierProjection(P[F[nFacet].Index[nPoint]]);
			ScreenPoint = projection.OrthogonalProjection(P[F[nFacet].Index[nPoint]]);
			//ScreenPoint = projection.PerspectiveProjection(P[F[nFacet].Index[nPoint]]);
			if (0 == nPoint)
			{
				pDC->MoveTo(ROUND(ScreenPoint.x), ROUND(ScreenPoint.y));
				temp = ScreenPoint;
			}
			else
			{
				pDC->LineTo(ROUND(ScreenPoint.x), ROUND(ScreenPoint.y));
			}
		}
		pDC->LineTo(ROUND(temp.x), ROUND(temp.y));//闭合四边形
	}
}

MFC绘制

下面就是用一些按钮、消息绑定来响应:

CCubeProjectionView::CCubeProjectionView() noexcept
{
	// TODO: 在此处添加构造代码
	Alpha = 0.0, Beta = 0.0;
	bPlay = FALSE;
	//读入点表面表信息
	cube.ReadVertex();
	cube.ReadFacet();
	/*
	将cube的顶点加载进入transform以便后面的变化
	*/
	transform.SetMatrix(cube.P, 8);
	int nEdge = 200;
	//点坐标进行缩放
	transform.Scale(nEdge, nEdge, nEdge);
	//平移一下,以便位于窗口中心
	transform.Translate(-nEdge / 2, -nEdge / 2, -nEdge / 2);
}
void CCubeProjectionView::OnDraw(CDC* pDC)
{
	CCubeProjectionDoc* pDoc = GetDocument();
	ASSERT_VALID(pDoc);
	if (!pDoc)
		return;

	// TODO: 在此处为本机数据添加绘制代码
	DoubleBuffer(pDC); //绘制图形
}
void CCubeProjectionView::DoubleBuffer(CDC* pDC)//双缓冲绘图
{
	CRect rect;
	GetClientRect(&rect);
	pDC->SetMapMode(MM_ANISOTROPIC);
	pDC->SetWindowExt(rect.Width(), rect.Height());
	pDC->SetViewportExt(rect.Width(), -rect.Height());
	pDC->SetViewportOrg(rect.Width() / 2, rect.Height() / 2);
	CDC memDC;//声明内存DC
	memDC.CreateCompatibleDC(pDC);//创建一个与显示DC兼容的内存DC
	CBitmap NewBitmap, *pOldBitmap;
	NewBitmap.CreateCompatibleBitmap(pDC, rect.Width(), rect.Height());//创建兼容内存位图 
	pOldBitmap = memDC.SelectObject(&NewBitmap);//将兼容位图选入内存DC
	memDC.FillSolidRect(rect, pDC->GetBkColor());//设置客户区背景色
	rect.OffsetRect(-rect.Width() / 2, -rect.Height() / 2);
	memDC.SetMapMode(MM_ANISOTROPIC);//内存DC自定义坐标系
	memDC.SetWindowExt(rect.Width(), rect.Height());
	memDC.SetViewportExt(rect.Width(), -rect.Height());
	memDC.SetViewportOrg(rect.Width() / 2, rect.Height() / 2);
	DrawObject(&memDC);//绘制图形
	pDC->BitBlt(rect.left, rect.top, rect.Width(), rect.Height(), &memDC, -rect.Width() / 2, -rect.Height() / 2, SRCCOPY); //将内存DC中的位图拷贝到显示DC
	memDC.SelectObject(pOldBitmap);
	NewBitmap.DeleteObject();
	memDC.DeleteDC();
}
void CCubeProjectionView::DrawObject(CDC* pDC)//绘制图形
{
	cube.Draw(pDC);
}

以及对应的按钮响应函数以及定时器

void CCubeProjectionView::On32771()
{
	// TODO: 在此添加命令处理程序代码
	bPlay = !bPlay;
	if (bPlay)//设置定时器
		SetTimer(1, 150, NULL);
	else
		KillTimer(1);
}


void CCubeProjectionView::OnUpdate32771(CCmdUI *pCmdUI)
{
	// TODO: 在此添加命令更新用户界面处理程序代码
	if (bPlay)
		pCmdUI->SetCheck(TRUE);
	else
		pCmdUI->SetCheck(FALSE);
}


void CCubeProjectionView::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 在此添加消息处理程序代码和/或调用默认值
	Alpha = 45;
	Beta = 45;
	transform.RotateX(Alpha);
	transform.RotateY(Beta);
	Invalidate(FALSE);
	CView::OnTimer(nIDEvent);
}

效果

 

 

 


 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

 

发表评论