BestAns

路漫漫其修远兮,吾将上下而求索

三维旋转球 源码+注释+简单讲解 银牌收录

前言

关于三维的东西远不止这么点内容,也不是我几句话可以讲完的,需要大家扎扎实实的看图形学。不能好高骛远,要扎实、系统的学习。还要注意相关数学知识的学习。

图形学一般是借助矩阵实现的各种转换。我在这篇文章里尽量不用图形学的术语,也不做优化、不加额外功能,只用最简单的三角函数实现,希望大家能从道理上看的清楚明白。

简单讲解

关于一个点,一定要有一个三维坐标,程序中的结构体 POINT3D 就是。

点的初始化由函数 InitPoint() 实现,该函数产生了 n 个半径为 1 的点。

点的运动,是在三维坐标内运动的,包括平移、缩放、旋转等。这个程序只涉及到了旋转,定义了三个方法:RotateX()、RotateY()、RotateZ(),分别实现绕三个轴旋转;可以使用矩阵运算来实现,也可以直接这样算出来。

最后需要将三维世界呈现出来,这里用到一个术语:投影,就是将三维的画面投影到二维上。投影有多种方法,这个球体用的单点透视,因此还需要一个“观察点”,程序中用 viewZ 定义,观察点坐标是:(0, 0, viewZ)。函数 Projection() 负责实现将三维点投影到二维平面上。

通常都会用矩阵来实现 3D 转换运算,在这里我将矩阵运算展开来写了,用最简单直接的三角函数来计算。

剩下的留待大家自己拓展吧,比如加上缩放、键盘控制、改变点的颜色、优化性能等。只要原理搞明白,这些都不难。

源码

下面是源代码:

// 程序名称:三维旋转球
// 编译环境:Visual C++ 6.0 ~ 2019,EasyX_20210730
// 代码发布:2010-9-14
// 最后更新:2021-8-20
//
#include <graphics.h>
#include <time.h>
#include <math.h>
#include <conio.h>

#define MAXPOINT	5000
#define	PI			3.1415926536


// 定义三维点
struct POINT3D
{
	double x;
	double y;
	double z;
};

POINT3D p3d[MAXPOINT];		// 所有的三维点
double viewZ = 3;			// 视点 z 轴坐标


// 初始化三维点
void InitPoint()
{
	// 产生随机种子
	srand((unsigned)time(NULL));
	
	// 产生球体表面的随机点(根据球体面积与其外切圆柱面积的关系)
	double rxy, a;
	for(int i = 0; i < MAXPOINT; i++)
	{
		p3d[i].z = 2.0 * rand() / RAND_MAX - 1;	// 求随机 z 坐标
		rxy = sqrt(1 - p3d[i].z * p3d[i].z);	// 计算三维矢量在 xoy 平面的投影长度
		a = 2 * PI * rand() / RAND_MAX;			// 产生随机角度
		p3d[i].x = cos(a) * rxy;
		p3d[i].y = sin(a) * rxy;
	}
}


// 使三维点按 x 轴旋转指定角度
void RotateX(POINT3D &p, double angle)
{
	double y = p.y;
	p.y = p.y * cos(angle) + p.z * sin(-angle);
	p.z =   y * sin(angle) + p.z * cos(angle);
}


// 使三维点按 y 轴旋转指定角度
void RotateY(POINT3D &p, double angle)
{
	double x = p.x;
	p.x = p.x * cos(angle) + p.z * sin(-angle);
	p.z =   x * sin(angle) + p.z * cos(angle);
}


// 使三维点按 z 轴旋转指定角度
void RotateZ(POINT3D &p, double angle)
{
	double x = p.x;
	p.x = p.x * cos(angle) + p.y * sin(-angle);
	p.y =   x * sin(angle) + p.y * cos(angle);
}


// 将三维点投影到二维屏幕上(单点透视)
POINT Projection(POINT3D p)
{
	POINT p2d;
	p2d.x = (int)(p.x * ( viewZ / (viewZ - p.z) ) * 200 + 0.5) + 320;
	p2d.y = (int)(p.y * ( viewZ / (viewZ - p.z) ) * 200 + 0.5) + 240;
	return p2d;
}


// 精确延时函数(可以精确到 1ms,精度 ±1ms)
// by yangw80<yw80@qq.com>, 2011-5-4
void HpSleep(int ms)
{
	static clock_t oldclock = clock();		// 静态变量,记录上一次 tick

	oldclock += ms * CLOCKS_PER_SEC / 1000;	// 更新 tick

	if (clock() > oldclock)					// 如果已经超时,无需延时
		oldclock = clock();
	else
		while(clock() < oldclock)			// 延时
			Sleep(1);						// 释放 CPU 控制权,降低 CPU 占用率
//			Sleep(0);						// 更高精度、更高 CPU 占用率
}


// 实现直接操作显示缓冲区的设备对象
class Device
{
private:
	DWORD *m_pbuffer;
	int m_width;
	int m_height;

public:
	Device(int w, int h) : m_width(w), m_height(h)
	{
		initgraph(w, h);
		BeginBatchDraw();
		m_pbuffer = GetImageBuffer();
	}

	~Device()
	{
		EndBatchDraw();
		closegraph();
	}

	// 画点
	void putpixel(int x, int y, COLORREF c)
	{
		if (x >= 0 && x < m_width && y >= 0 && y < m_height)
			m_pbuffer[y * m_width + x] = BGR(c);
	}

	// 更新显示
	void flushdevice()
	{
		FlushBatchDraw();
	}
};


// 主函数
int main()
{
	Device d(640, 480);
	InitPoint();

	int c;
	POINT p2d;
	while(!_kbhit())
	{
		cleardevice();			// 清除屏幕

		for(int i=0; i < MAXPOINT; i++)
		{
			// 使该点围绕三个坐标轴做旋转运动
			RotateX(p3d[i], PI / 800);
			RotateY(p3d[i], PI / 600);
			RotateZ(p3d[i], PI / 400);
			
			// 根据点的深度,产生相应灰度的颜色
			c = (int)(p3d[i].z * 100) + 155;
			
			// 投影该点到屏幕上
			p2d = Projection(p3d[i]);
			
			// 画点
			d.putpixel(p2d.x, p2d.y, RGB(c, c, c));
		}

		d.flushdevice();
		HpSleep(10);			// 延时 10 毫秒
	}

	return 0;
}

更多有趣的形状

InitPoint() 函数负责初始化随机点。前面的程序用随机点构成了一个球体,修改 InitPoint() 函数可以实现不同的形状。注意:请将 x、y、z 的范围生成在 -0.5 ~ 0.5 之间。

以下是各种形状对应的 InitPoint 函数。

立方体

// 初始化三维点
void InitPoint()
{
	// 产生随机种子
	srand((unsigned)time(NULL));
	
	// 产生立方体表面的随机点
	double t;
	for(int i = 0; i < MAXPOINT; i++)
	{
		p3d[i].x = (rand() % 20000 - 10000) / 20000.0;
		p3d[i].y = (rand() % 20000 - 10000) / 20000.0;
		p3d[i].z = (rand() % 2) - 0.5;
		switch(rand() % 3)
		{
			case 1: t = p3d[i].x; p3d[i].x = p3d[i].z; p3d[i].z = t; break;
			case 2: t = p3d[i].y; p3d[i].y = p3d[i].z; p3d[i].z = t; break;
		}
	}
}

正弦曲面

// 初始化三维点
void InitPoint()
{
	// 产生随机种子
	srand((unsigned)time(NULL));
	
	// 产生正弦曲面的随机点
	double u, v;
	for(int i = 0; i < MAXPOINT; i++)
	{
		u = 2 * PI * rand() / RAND_MAX;
		v = 2 * PI * rand() / RAND_MAX;
		p3d[i].x = sin(u) / 2;
		p3d[i].y = sin(v) / 2;
		p3d[i].z = sin(u + v) / 2;
	}
}

圆环

// 初始化三维点
void InitPoint()
{
	// 产生随机种子
	srand((unsigned)time(NULL));
	
	// 产生圆环的随机点
	double a, r;
	for (int i = 0; i < MAXPOINT; i++)
	{
		a = 2 * PI * rand() / RAND_MAX;
		r = cos(a) * 0.25 + 0.6;
		p3d[i].z = sin(a) * 0.25;
		a = 2 * PI * rand() / RAND_MAX;
		p3d[i].x = cos(a) * r;
		p3d[i].y = sin(a) * r;
	}
}

期待你写出来更有趣的形状。

评论 (3) -

  • 万没有想到时至今日我写的代码还是在致敬前人,大佬我写了一个心形的,效果跟你基本一样,我试着把我的生成随机点的方法放进你的里面,但是结果很怪,完全不符合预期,可能大佬的代码还有一些我不知道的设定吧。
    // 初始化三维点
    void InitPoint()
    {
        // 产生随机种子
        srand((unsigned)time(NULL));
        double A_num = 200, B_num = 125, rotate = -PI / 4, beginDegree = PI / 2 - rotate;

        // 产生圆环的随机点
        double a, r;
        for (int i = 0; i < MAXPOINT; i++)
        {
            bool isRight = (rand() % 2);
            double angle = PI / 1000.0 * (rand() % 1000);
            double ver_angle = PI / 1000.0 * (rand() % 1000) - PI / 2;
            double temp_x = A_num * B_num * cos(-PI + beginDegree + angle) /
                sqrt(A_num * A_num * sin(-PI + beginDegree + angle) * sin(-PI + beginDegree + angle) +
                    B_num * B_num * cos(-PI + beginDegree + angle) * cos(-PI + beginDegree + angle));
            double temp_y = A_num * B_num * sin(-PI + beginDegree + angle) /
                sqrt(A_num * A_num * sin(-PI + beginDegree + angle) * sin(-PI + beginDegree + angle) +
                    B_num * B_num * cos(-PI + beginDegree + angle) * cos(-PI + beginDegree + angle));
            double t_x = isRight ? temp_x * cos(rotate) - temp_y * sin(rotate) : -(temp_x * cos(rotate) - temp_y * sin(rotate));
            double t_y = temp_x * sin(rotate) + temp_y * cos(rotate);
            p3d[i].z = B_num * sin(ver_angle);
            p3d[i].x = t_x * cos(ver_angle);
            p3d[i].y = t_y * cos(ver_angle);
        }
    }
  • easyx可以依赖显卡加速吗?这段程序是否用到了显卡加速
    // 实现直接操作显示缓冲区的设备对象
    class Device
    {
    private:
      DWORD *m_pbuffer;
      int m_width;
      int m_height;

    public:
      Device(int w, int h) : m_width(w), m_height(h)
      {
        initgraph(w, h);
        BeginBatchDraw();
        m_pbuffer = GetImageBuffer();
      }

      ~Device()
      {
        EndBatchDraw();
        closegraph();
      }

      // 画点
      void putpixel(int x, int y, COLORREF c)
      {
        if (x >= 0 && x < m_width && y >= 0 && y < m_height)
          m_pbuffer[y * m_width + x] = BGR(c);
      }

      // 更新显示
      void flushdevice()
      {
        FlushBatchDraw();
      }
    };

添加评论