*运动的点(全屏模糊处理的范例)

*运动的点

这个程序并没有太强的艺术效果,只是为了做一个全屏模糊处理的范例。

其中,点的非直线*运动的部分,是用的我在高中时候用 QuickBasic 实现的一种简单方法,看到这部分代码有点让人怀念过去,呵呵。

简单说明一下:

  1. 为了使范例更清晰,全屏模糊处理的代码我做了简化,将第一行和最后一行忽略掉了。通过代码可以清晰地看到,所谓全屏模糊,其实就是对每个点与相邻的几个点的颜色做平均处理(可以自己决定每个点的权重)。

  2. 这个模糊函数只处理了每个点的上、下、左、右共 5 个点。为了获得不同的效果,可以试试将周围八个点一起处理,或者上、下、左、右方向上的两个点一起处理。

  3. 严格来说,每个点在运算时,所使用的左边和上边的点,其实已经不是原来的点了,而是模糊后的点。所以这样的模糊处理对于单个点来说,右边和下边颜色要淡一些。如果需要精确的模糊,可以借助 IMAGE 实现,这里就不再多说了。

  4. 延时没有用常用的 Sleep 实现,而是使用的精确延时,详见文章《精确延时的实现》

下面是运行动画的截图:
*运动的点(全屏模糊处理的范例)

完整代码如下:

///////////////////////////////////////////////////
// 程序名称:*运动的点
//
#include <graphics.h>
#include <conio.h>
#include <math.h>
#include <time.h>
#define AMOUNT 64

// 全屏模糊处理
// (为了简化范例,该函数略去了屏幕第一行和最后一行的处理)
void Blur(DWORD* pMem)
{
	for (int i = 640; i < 640 * 479; i++)
	{
		pMem[i] = RGB(
			(GetRValue(pMem[i]) + GetRValue(pMem[i - 640]) + GetRValue(pMem[i - 1]) + GetRValue(pMem[i + 1]) + GetRValue(pMem[i + 640])) / 5,
			(GetGValue(pMem[i]) + GetGValue(pMem[i - 640]) + GetGValue(pMem[i - 1]) + GetGValue(pMem[i + 1]) + GetGValue(pMem[i + 640])) / 5,
			(GetBValue(pMem[i]) + GetBValue(pMem[i - 640]) + GetBValue(pMem[i - 1]) + GetBValue(pMem[i + 1]) + GetBValue(pMem[i + 640])) / 5);
	}
}

// 点的结构
struct SPOT
{
	int x, y;
	int targetx, targety;
	int dx, dy;
	COLORREF color;
};

// 精确延时函数(可以精确到 1ms,精度 ±1ms)
// (原理在 www.easyx.cn 有文章详细解释)
void HpSleep(int ms)
{
	static clock_t oldclock = clock();		// 静态变量,记录上一次 tick

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

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

// 主函数
void main()
{
	// 初始化
	initgraph(640, 480);			// 创建绘图窗口
	BeginBatchDraw();				// 设置批绘图模式
	srand((unsigned)time(NULL));	// 设置随机种子
	DWORD* pMem = GetImageBuffer();	// 获取显存地址

	// 定义所有点
	SPOT spots[AMOUNT];		//  AMOUNT=64

	// 初始化每个点
	for (int i = 0; i < AMOUNT; i++)
	{
		spots[i].x = spots[i].targetx = rand() % 600 + 20;
		spots[i].y = spots[i].targety = rand() % 440 + 20;
		spots[i].dx = rand() % 40 - 20;
		spots[i].dy = (int)sqrt((double)(400 - spots[i].dx * spots[i].dx) )* ((rand() % 2) * 2 - 1);
		spots[i].color = HSLtoRGB((float)(rand() % 360), 1.0, 0.5);
	}

	while (!_kbhit())
	{
		for (int i = 0; i < AMOUNT; i++)
		{
			setcolor(spots[i].color);
			moveto(spots[i].x, spots[i].y);

			spots[i].targetx += spots[i].dx;
			spots[i].targety += spots[i].dy;

			// 判断是否越界,以及越界处理
			if (spots[i].targetx <= 0)
			{
				spots[i].dx = rand() % 20;
				spots[i].dy = (int)sqrt((double)(400 - spots[i].dx * spots[i].dx)) * ((rand() % 2) * 2 - 1);
			}
			else if (spots[i].targetx >= 639)
			{
				spots[i].dx = -rand() % 20;
				spots[i].dy = (int)sqrt((double)(400 - spots[i].dx * spots[i].dx)) * ((rand() % 2) * 2 - 1);
			}

			if (spots[i].targety <= 0)
			{
				spots[i].dx = rand() % 40 - 20;
				spots[i].dy = (int)sqrt((double)(400 - spots[i].dx * spots[i].dx));
			}
			else if (spots[i].targety >= 479)
			{
				spots[i].dx = rand() % 40 - 20;
				spots[i].dy = -(int)sqrt((double)(400 - spots[i].dx * spots[i].dx));
			}

			// 未越界时,有 10% 的概率改变运行方向
			if (rand() % 10 < 1)
			{
				spots[i].dx = rand() % 40 - 20;
				spots[i].dy = (int)sqrt((double)(400 - spots[i].dx * spots[i].dx) )* ((rand() % 2) * 2 - 1);
			}

			// 计算新点坐标,画线
			spots[i].x += (int)((spots[i].targetx - spots[i].x) * 0.1);
			spots[i].y += (int)((spots[i].targety - spots[i].y) * 0.1);
			lineto(spots[i].x, spots[i].y);
		}

		// 全屏模糊处理
		Blur(pMem);
		FlushBatchDraw();

		// 延时
		HpSleep(33);
	}

	// 按任意键退出
	closegraph();
}

大家平时写练习程序,包括网站上的范例程序,很多延时都直接用的 Sleep() 实现。这个延时有个缺点,那就是无法统计代码执行的时间。请看下图:

*运动的点(全屏模糊处理的范例)

由图可以看到,使用 API 函数 Sleep() 的问题,就是会忽略掉程序的执行时间。很多时候,程序的执行时间是不固定的,所以这就导致使用 Sleep 的延时并不精确,即便 Sleep 使用相同的延时,也可能造成不同电脑上执行速度不同的结果。

图中,理想的延时函数会将程序的执行时间部分考虑进去,这样就可以实现很均匀的延时。下面讨论实现方法。

本次延时要从上次的延时结束开始计算,就必须要记录每次延时执行的具体时刻,而不仅仅是一个时间长度。所以,可以简单的使用 clock() 函数实现,代码如下:

// 精确延时函数(可以精确到 1ms,精度 ±1ms)
// by yangw80<[email protected]>, 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 占用率
}

直接用函数 HpSleep 替换 Sleep 就可以很直观的看到效果和 clock() 函数类似的还有 GetTickCount() 函数,clock() 的精度高一些,其精度取决于常量 CLOCKS_PER_SEC,通常在 1ms。根据微软 MSDN 的描述,GetTickCount() 的精度在 10ms~16ms 之间。

以上代码可以实现微秒级的延时。如果需要更高的精确度,可以使用多媒体定时器。做为范例,以下代码实现微秒级的延时,并封装成类:

// 代码名称:精确到微秒的延时类(基于多媒体定时器)
//
#pragma once
#include <windows.h>

class MMTimer
{
private:
	static LARGE_INTEGER m_clk;			// 保存时钟信息
	static LONGLONG m_oldclk;			// 保存开始时钟和结束时钟
	static int m_freq;					// 时钟频率(时钟时间换算率),时间差

public:
	static void Sleep(int ms);
};

LARGE_INTEGER MMTimer::m_clk;
LONGLONG MMTimer::m_oldclk;
int MMTimer::m_freq = 0;

// 延时
void MMTimer::Sleep(int ms)
{
	if (m_oldclk == 0)
	{
		QueryPerformanceFrequency(&m_clk);
		m_freq = (int)m_clk.QuadPart / 1000;	// 获得计数器的时钟频率

		// 开始计时
		QueryPerformanceCounter(&m_clk);
		m_oldclk = m_clk.QuadPart;				// 获得开始时钟
	}

	unsigned int c = ms * m_freq;

	m_oldclk += c;

	QueryPerformanceCounter(&m_clk);

	if (m_clk.QuadPart > m_oldclk)
		m_oldclk = m_clk.QuadPart;
	else
		do
		{
			::Sleep(1);
			QueryPerformanceCounter(&m_clk);	// 获得终止时钟
		}
		while(m_clk.QuadPart < m_oldclk);
}

看明白了前面的叙述,这个代码应该很容易就能看懂。

使用方法:将以上代码拷贝到新建的 MMTimer.h 中,然后在主程序中加上 #include “MMTimer.h”,在需要 Sleep 的地方执行 MMTimer::Sleep 方法。

为了简单起见,只写了一个 .h 文件。更标准一些的做法,是将前述代码再分离出一个 MMTimer.cpp 文件,甚至改掉 MMTimer 这个名字,或者封装成库等等,这些就不再多说了,本文只想阐述一个方法。

备:转载于YangW