如何制作一个涡流形状的粒子效果,星系旋转,星涡 实现与解析

如何制作一个涡流形状的粒子效果,星系旋转,星涡 实现与解析

先来看一下效果

注意,图中由于录制原因,中心部分的像素太小,导致GIF中看不到了

接着说说原理

  1. 在粒子创建的时候,指定一个初始的移动方向(角度)
  2. 每次移动粒子的时候,在原来的角度基础上加上一个旋转偏角
  3. 根据新的角度计算出粒子的位置
  4. 进行绘制粒子即可

好了,这里使用Win32实现,首先我们来说一下关键点

看下结构定义

typedef struct
	{
		int x; //创建粒子时的初始坐标
		int y;
		double direc; //粒子的移动方向,弧度制
		float speed; //粒子的移动速度,或者逃逸速度
		double curDis; //当前粒子距离初始坐标的距离
		bool live; //粒子是否是存活状态
		double curR; //当前粒子的半径
		double addR; //半径的递增大小
		int r;	//粒子的RGB颜色值
		int b;
		int g;
	}Light;

关键操作

1.怎么根据中心点,距离,方向,计算出新坐标
2.新方向怎么计算

新坐标的计算

//Light mLight[200];
//用一个数组来保存粒子,这样我们的最大显示粒子数量就是200,
//在显示的时候,检查粒子的存活状态进行显示
//在满足死亡条件的时候设置为死亡状态,不在进行显示
int px = mLight[i].x + mLight[i].curDis*cos(mLight[i].direc);
int py = mLight[i].y + mLight[i].curDis*sin(mLight[i].direc);
//以上,就是通过初始坐标,半径,方向,计算出目标坐标
//简单来说:新X=源X+距离*cos(方向); 也就是:新X=源X+X增量
//类似的,新的Y坐标也是一样的计算,不过注意,使用的是sin()计算

新方向的计算

const double PI=3.141592653549;
mLight[i].direc = (((rand() % 30) / 180.0*PI) + mLight[i].direc)*0.3 + mLight[i].direc*0.7;
//简单来说:
//新方向=源方向+偏转方向
//看起来不像对吧,这样就像了:
//新方向=(源方向+偏转方向)*新方向比例+源方向*源方向比例
//1.0=新方向比例+源方向比例
//偏转方向=偏转角/180.0*PI; 其实就是等比例计算,偏转方向:PI=偏转角:180
//偏转角=rand()%偏转范围; 使用了随机范围内的一个偏转角度

if (mLight[i].direc > 3.141592653549*2)
	mLight[i].direc -= 3.141592653549*2;
//另外再处理一下,角度保持在2*PI范围内,
//虽然大于2*PI也能进行正常计算,但是总感觉...

怎么角度弧度互转?

//程序中,使用的都是弧度制
//弧度=角度/180.0*PI;
//角度=弧度/PI*180.0;

好了,基础准备好了,直接给出实现代码

使用控制台应用程序+EasyX库实现,主要代码封装在类里面,只需要给定HDC即可进行绘制,也就是说,只要能给一个HDC那就可以绘制,不需要什么EasyX,也不一定是控制台应用程序,也可以是Win32,MFC

代码完整,如果需要,可以直接复制,创建控制台程序即可(前提是安装了EasyX图形库),使用Win32或者MFC的话,直接在合适的地方初始化,在绘制(WM_PAINT/WM_TIMER/OnDraw())里面调用即可

主要类定义:StarVortex

#pragma once

#include

#include
#include
#include
class StarVortex
{
public:
	//这个结构前面介绍过了,就不重复了
	 typedef struct
	{
		int x;
		int y;
		double direc;
		float speed;
		double curDis;
		bool live;
		double curR;//其实这两个域是没用的,因为后面绘制时是直接计算出来的
		double addR;//
		int r;
		int b;
		int g;
	}Light;
	//三个构造,使用本类进行绘制,必须指定绘图的宽度,高度,粒子数量
	//但是,默认宽高度和粒子数量也是有的,但是那样的话,绘制将可能得不到你想要的结果
	 StarVortex();
	 StarVortex(int width, int height, int count);
	 ~StarVortex();
	 //设置完宽高度粒子数量之后,一定要调用的方法
	 //这个方法必须在绘制之前调用,它负责初始化粒子数组的内容
	void InitLight();
	//调用绘制,给定HDC即可
	//每次绘制会自动进行粒子的移动和死亡处理等
	//详细使用步骤请看主函数
	void DrawBackground(HDC hdc);
	//参数设置,和构造一样
	void SetMaxCount(int count);
	void SetWindowSize(int width, int height);
private:
	//前面说了,是有默认参数的,因此通过它统一设置
	void SetDefaultArgument();
	//创建出新的粒子,由于是存放在数组中的,也就是进行了对死亡粒子的重新初始化和复活
	void CreateLight();
	//这个函数是辅助上述的初始化函数和复活函数使用的,负责初始化一个指定下标的粒子
	void SetLightToDefault(int index);

	int mMaxCount; //定义粒子数量,数量越多,占用CPU运算越高,越少则屏幕上将会没什么东西
	Light * mLight;//粒子数组,后面通过malloc动态申请

	int mWinWidth;//绘制的窗口的宽高
	int mWinHeight;
};

主函数文件

#include	//EasyX图形库头文件,Win32或者MFC就不用包含了
#include"StarVortex.h"

int main(int argc, char * argv[])
{
	//获取屏幕的大小,作为我们窗口的大小
	int winWid = GetSystemMetrics(SM_CXSCREEN);
	int winHei = GetSystemMetrics(SM_CYSCREEN);
	//使用EasyX图形库初始化我们的窗口
	HWND hwnd=initgraph(winWid,winHei);
	//移动窗口到0,0位置,这样我们的窗口就铺满了屏幕了
	//参数说明:窗口句柄,Zorder窗口,坐标X,Y,宽高wid,hei,操作掩码
	//由于我们只是移动,因此不排序SWP_NOZORDER ,不调整尺寸SWP_NOSIZE
	//这个时候,Zorder窗口,宽高的值任意值都行
	SetWindowPos(hwnd, NULL, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOSIZE);
	//创建我们的类对象
	StarVortex vortext;
	//设置参数并初始化
	vortext.SetWindowSize(winWid,winHei);
	vortext.SetMaxCount(2000);
	//注意,一定要初始化
	vortext.InitLight();

	while (1)
	{
		//EasyX的缓冲绘制开始
		BeginBatchDraw();
		//EasyX的清空屏幕
		cleardevice();
		
		//调用我们的类方法进行绘制
		//在EasyX中,获取默认窗口的HDC,调用GetImageHDC()方法获取
		//如果是获取EasyX的某个IMAGE对象的HDC,给定参数即可
		//比如:
		//IMAGE img;
		//HDC hdc=GetImageHDC(&img);
		vortext.DrawBackground(GetImageHDC());
		
		//EasyX的缓冲绘制结束
		EndBatchDraw();
		//睡眠一下,控制一下帧率=1000/24
		Sleep(24);
	}
	return 0;
}

类实现代码

//#include "stdafx.h"
#include "StarVortex.h"
//构造函数就不说了,你可以看到默认参数的值
StarVortex::StarVortex()
{
	SetDefaultArgument();
	SetWindowSize(720, 480);
	SetMaxCount(2000);
}

StarVortex::StarVortex(int width, int height, int count)
{
	SetDefaultArgument();
	SetWindowSize(width,height);
	SetMaxCount(count);
}

void StarVortex::SetDefaultArgument()
{
	mMaxCount = 2000;
	mLight = NULL;
	mWinWidth = 720;
	mWinHeight=480;
}
//析构函数中,需要释放申请的内存
StarVortex::~StarVortex()
{
	if (mLight != NULL)
	{
		free(mLight);
	}
}
//设置粒子数量的时候,检查是否已经申请过
void StarVortex::SetMaxCount(int count)
{
	//如果已经申请过了,那么就先释放,再重新申请
	if (mLight != NULL)
	{
		free(mLight);
		mLight = NULL;
	}
	//重新申请
	mMaxCount = count;
	mLight = (Light*)malloc(sizeof(Light)*mMaxCount);
}

void StarVortex::SetWindowSize(int width, int height)
{
	mWinWidth = width;
	mWinHeight = height;
}
void StarVortex::SetLightToDefault(int index)
{
	//上一次生成粒子的方向
	/*
	思路:
	初始化新粒子的时候,根据上一次的角度,加上一个随机角度,
	那么就实现了一周一周的生成,就能够实现螺旋感
	因此使用静态变量
	*/
	static double lastDirect = 0;
	//获取屏幕鼠标的坐标,因为我们的窗口和屏幕一样大
	//因此客户区和屏幕没有什么差别
	//不用调用ScreenToClient函数进行坐标转换
	//我们生成的粒子就从鼠标的坐标下座位出发点
	//就实现了跟随鼠标的效果
	POINT cursor = { 0 };
	GetCursorPos(&cursor);
	
	//设置粒子初始坐标为当前鼠标坐标
	mLight[index].x = cursor.x;
	mLight[index].y = cursor.y;
	//初始状态粒子时死亡状态
	mLight[index].live = false;
	//你可以通过下面的注释语句,实现固定在屏幕中间散开
	/*mLight[index].x = mWinWidth / 2;
	mLight[index].y = mWinHeight / 2;*/
	
	//在上一次的角度基础上加上一个随机的偏转角度
	//注意/180*PI做的是角度转弧度,毕竟人还是比较喜欢角度的
	mLight[index].direc = lastDirect + ((rand() % 15) / 180.0*3.141592653549);
	if (mLight[index].direc > 2 * 3.141592653549)
	{
		mLight[index].direc -= 2 * 3.141592653549;
	}
	//重新保存为上一次角度
	lastDirect = mLight[index].direc;
	//粒子逃逸速度设置为屏幕的1/150-2的大小,这样的1/150就实现了动态根据窗口大小,调整速度
	mLight[index].speed = rand() % (mWinWidth * 1 / 150) + 1;
	if (mLight[index].speed == 1) mLight[index].speed = 2;
	//随机粒子的颜色,采用一个亮色调,背景默认是黑色
	mLight[index].r = rand() % 200 + 54;
	mLight[index].b = rand() % 200 + 54;
	mLight[index].g = rand() % 200 + 54;
	//前面说了,下面这两个域是无效的,所以可以不用管
	mLight[index].curR =1;
	mLight[index].addR = (rand() % 100 + 1)%100;
	//初始的逃逸距离为0,对吧,就在x,y上
	mLight[index].curDis = 0;

}

void StarVortex::InitLight()
{
	//这就是,为什么一定要调用初始化,看到了吧,需要将一些粒子设置为存活状态
	//还有参数设置
	for (int i = 0; i < mMaxCount; i++)
	{
		SetLightToDefault(i);
		if (3 > rand() % 100)	//这里就是97%的粒子都是存活的
			mLight[i].live = true;
		else
			mLight[i].live = false;
	}
}
void StarVortex::CreateLight()
{
	//这里控制一下,每次调用此函数,最多复活多少个粒子
	//如果不进行控制,那么一次会激活一批,导致会产生一圈一圈的效果
	//和预想的有差异
	int maxCount = mMaxCount / 100.0;
	if (maxCount <= 0)
		maxCount = 1;
	int curCount = 0;
	for (int i = 0; i < mMaxCount; i++)
	{
		if (mLight[i].live == false)
		{
			if (rand() % 100 < 21)
			{
				SetLightToDefault(i);
				mLight[i].live = true;
				curCount++;
			}
		}
		if (curCount >= maxCount)
			break;
	}
}
void StarVortex::DrawBackground(HDC hdc)
{
	//我们获取一下,允许粒子走的最大距离,为我们窗口的对角线的距离的0.6
	//距离计算:开方(x^2+y^2) 开方:sqrt,次幂=pow
	double winMaxDis = sqrt(pow(mWinWidth*0.6,2.0)+pow(mWinHeight*0.6,2.0));
	for (int i = 0; i < mMaxCount; i++)
	{
		if (mLight[i].live)//如果粒子存活就进行绘制
		{
			//根据初始坐标,移动距离,方向,计算出绘制的坐标
			int px = mLight[i].x + mLight[i].curDis*cos(mLight[i].direc);
			int py = mLight[i].y + mLight[i].curDis*sin(mLight[i].direc);
			
			//这部分是GDI的内容,设置和粒子颜色一样的画笔和画刷
			//这样就能绘制一个纯净粒子颜色的球形了
			COLORREF cor = RGB(mLight[i].r,mLight[i].g,mLight[i].b);
			HPEN pen = CreatePen(0, 1, cor);
			HGDIOBJ op=SelectObject(hdc, pen);
			HBRUSH brush = CreateSolidBrush(cor);
			HGDIOBJ ob = SelectObject(hdc, brush);
			//计算在当前距离下,球应该有的半径,距离越大,半径越大
			//这也是一个比例运算,应该不难理解,最大半径就是6
			mLight[i].curR = 6.0*mLight[i].curDis / winMaxDis;
			if (mLight[i].curR <= 1)
				mLight[i].curR = 1;
			//绘制圆形,由于GDI是没有绘制圆形的函数的,因此使用坐标点作为圆心,用半径来计算坐标点,使用绘制椭圆的函数进行绘制
			//参数:HDC,左上角X,Y,右下角X,Y
			Ellipse(hdc, px - mLight[i].curR, py - mLight[i].curR, px + mLight[i].curR, py + mLight[i].curR);
			
			//GDI的内容,画笔句柄是需要释放的
			//否则,一旦句柄被用完了,没有释放,那就没墨水绘制了
			//常见的就是,绘制的都是黑白的
			DeleteObject(op);
			DeleteObject(pen);
			DeleteObject(ob);
			DeleteObject(brush);

			/
			//移动,在当前距离的基础上,加上移动速度,就实现了距离的增长
			mLight[i].curDis += mLight[i].speed;
			//如果每次的移动距离都一样,那么距离远近都一个速度,不太好
			//给他加上一个增量,这样,距离越远,速度越快
			mLight[i].speed += 0.1;
			
			//死亡判定,如果移动距离已经大于最大允许距离,那么就该死了
			//但是直接判定死亡,就死的成为一个圆形,没有过度,不好看
			//因此,还要被选中12%确定处以死刑,才能死,
			//否则就能够侥幸存活一段时间,这样就实现了边界模糊
			if (mLight[i].curDis > winMaxDis && rand() % 100 < 12)
				mLight[i].live = false;

			//if (rand() % 100 < 85)
			{
				//看起来有点复杂,简单说一下
				//就是计算偏转角度的,但是是放大了10倍计算的
				//最大偏转30度,距离越远,偏转角度越小
				//这样就明白了吧
				//rfac=(30-28)*距离比例 说了放大了10倍
				//距离比例=粒子移动距离/最大允许距离
				//但是前面说了,粒子可能侥幸逃脱死亡判定,那么也就是说
				//粒子移动距离不一定小于最大距离
				//因此三目运算符处理
				int rfac = (300 - (280.0*(mLight[i].curDis>winMaxDis ? winMaxDis : mLight[i].curDis) / winMaxDis));
				//防止后面取余时发生异常,限定不能小于2
				rfac = rfac<2 ? 2 : rfac;
				//前面说了,放大了10倍,这里还回去(rand() % rfac)/10.0
				//这里除以10
				//这个公式一开始就介绍了,不重复讲了
				// /180.0*3.141592653549 转弧度
				mLight[i].direc = ((((rand() % rfac)/10.0) / 180.0*3.141592653549) + mLight[i].direc)*0.3 + mLight[i].direc*0.7;
				if (mLight[i].direc > 3.141592653549*2)
					mLight[i].direc -= 3.141592653549*2;
			}

			
		}
		

	}
	//每次绘制结束,都去复活一些粒子,这样就能够感觉源源不断的粒子产生和死亡
	//相比较于用链表实现,好多了吧
	CreateLight();
}

你可能感兴趣的:(C++,开发学习)