首页 > 代码库 > [GEiv]第六章:粒子特效 绚丽的火焰与爆炸

[GEiv]第六章:粒子特效 绚丽的火焰与爆炸

第六章:粒子特效

绚丽的火焰与爆炸

        本章节主要介绍粒子特效设计的方法论,其中有相当的知识量是平台无关的;在本文中会以“爆炸”这个实际的例子为线索,进行详细的设计讲解,并最终使用GEiv实现它。


[为什么要使用”粒子”]

        实现粒子特效的首要目的,是对一些环境效果进行模拟仿真,常见的环境效果,例如火焰、爆炸、雨、雪、雾等,都是无数微小的粒子以某些规律共同作用的结果。而对于计算机来讲,虽然没有足够的运算能力对每一个自然粒子进行抽象,但我们可以借鉴其原理,使用相对更少的粒子对这些自然现象进行模拟和仿真,以达到近似的效果。


[需要设计哪些内容]

        [粒子属性]

        首先需要设计的是单个粒子的属性,这里我们以粒子个体作为考虑的焦点,考虑的内容往往是粒子的共有属性,属性的内容可以是图形样式、大小、颜色等等。

        [投射规律]

        投射规律考虑粒子以何种方式投射到屏幕上,这里以粒子群为考虑的焦点,考虑的内容会涉及到实际的物理规律,例如粒子在空间中的角度分布、速度分布以及颗粒大小分布等情况。

        [演变规律]

        演变规律是抛射后的粒子随着时间变化的规律,它同样会涉及到物理规律的模拟,只不过这次是针对单个粒子的设计,例如速度、自旋角度、颜色等属性的变化规律。


[实例-模拟爆炸]

        爆炸特效在游戏中的使用相当广泛,属于经典的粒子系统。现在我们从零开始,设计一个爆炸的粒子特效。

        在想要模拟爆炸前先来观察一个实际的爆炸例子:

        

        从图片中我们能够概况一些基本的物理规律:

        首先,在一个爆炸中,粒子的大小显然是不同的,而且,简单的想,粒子的大小与其质量成正比,所以粒子速度应该与其大小负相关,你可以看到颗粒状的小型碎片已经飞到了火焰之外的区域,这是动量守恒定律所确定的。

        其次,在爆炸的中心,能量较高,呈现出亮白色;而在爆炸的外围,与空气接触后热量明显下降,火焰呈现出暗红色,在这个过程中,颜色也呈现出了明显的变化规律:亮白-》黄色-》红色-》暗红。

        还有,速度的变化规律:在爆炸发生后,粒子的速度并不会一直不变,它还要受到空气阻力的作用,根据流体力学的相关内容,空气阻力与速度的平方成正比,与物体在运动方向的正投影面积成正比,所以其速度变化应该表现为某种受到阻尼的运动状态。

        最后,在能量耗尽的暗红色区域,粒子逐渐消失,也就是说其颜色通道系数应该以某种非线性(先慢后快)的方式衰减。

        [属性设计]

        粒子图元:首先需要确定的问题,我们如何选择粒子的图形呢,使用点?圆形?方块?还是使用某种贴图呢……其实设计粒子的基本形态很值得一说,我们暂且使用圆形来设计,在最后您可以看到更改粒子形态对整体特效的影响。

        粒子的颜色:由白到红,初始值使用白色。

        粒子的大小:为了较为明确的产生大小两种粒子,我将使用一定的概率分布策略随机产生大小(详见投射设计部分)。

        自旋角度:在圆周上均匀分布,由于一开始我们使用圆形作为图元,所以这个自旋这个属性不会显露出来。

        通道:Alph初始值设置为1.0。

        [投射设计]

        产生:在我们给定爆炸点之后,假定粒子围绕着给定点进行+/-5位置浮动的随机的产生。

        大小分布:以50%的概率产生6~35大小的粒子,否则产生6~24大小的粒子,这里只是一个简单方案,你也可以考虑使用高斯分布等。

        速度方向分布:以产生点进行360度均匀分布。

        速度大小分布:为了简化选择了恒定值,但是,空气阻力模型在演变中起到作用,故仍可观察到非常近似地模拟结果。

        [演变设计]

        速度衰减:

        

        对于每一帧:v -= a*w^2*v;其中,w是粒子大小,a是衰减系数,v是当前速度,也就是说,速度进行阻尼衰减,并且大碎片的速度衰减的更快。

        颜色衰减:

        

        ↑衰减时间图

        

        ↑衰减过程均匀抽样

        首先,RGB中的红色分量是不变的。

        假如把时间t变量规格化到0~1之间。

        那么,蓝色分量应该最快衰减,因为爆炸主色调至少应该是一个暖色调。所以蓝色线使用的是t^16。

        绿色分量暂时设置为伴随t的线性衰减,其实,G分量衰减速度可以依据大小而定以获得更逼真的效果。

        通道衰减:

        

        ↑衰减时间图

        

        ↑衰减过程均匀抽样

        通道衰减过程先慢后快,这样,在特效开始的一段时间内,我们不会感到通道的变化,直到粒子快要消亡时才会有直观的视觉感受。

        [编码实现]

        接下来就是编码阶段了,我们也明确的看到,其实整个粒子特效的实现过程中,设计占了相当大的比例,在最后的阶段,只不过是要我们使用擅长的平台去实现罢了,其实很多软件开发都是这样的,编码只是个实现过程,不是什么高科技。

        您可以到GitHub上找到本章中的例子。这里进入

        在Geiv下,我们的粒子仅需要实现Individual接口,并使用个体的集群管理器进行管理即可(参阅第五章)。

//ExpIndividual.java:
package com.geiv.test;

import engineextend.crowdcontroller.Individual;
import geivcore.UESI;
import geivcore.enginedata.obj.Obj;
import java.awt.Color;
import com.thrblock.util.REPR;
import com.thrblock.util.RandomSet;

public class ExpIndividual implements Individual {
	UESI UES;
	Obj disp;

	float sTallms = 500;//这里设置了粒子从产生到消亡的总经历时间
	float allms = sTallms;
	float Dms = 17;//这里设置了每一帧的时间,你也可以用1000/UES.getFPS这中方法在构造器里填充

	float V = 4.5f;//运动的初始速度被固定为4.5像素每帧
	float ax, ay;
	float vx, vy;
	float Theta;//自选角度,本例中暂时使用圆形,所以是看不出的

	public ExpIndividual(UESI UES) {
		this.UES = UES;
		disp = UES.creatObj(UESI.XRIndex);//这里把图元产生在了XR层,前面的章节中介绍了该层次混合模式的特点。
		disp.addGLOval("FFFFFF",0,0,12,12,12);//画一个圆形
		disp.setGLFill(true);
		disp.setColor("FFFFFF");
		disp.setAlph(disp.getTopDivIndex(), 1.0f);

		allms = sTallms;
	}

	@Override
	public boolean isAvalible() {
		return !disp.isPrintable();//关于Individual请参考第五章的介绍
	}

	@Override
	public void getUse(Object[] ARGS, float... FARGS) {
		int Rad;//我们使用一定的分布方法产生Rad大小,RandomSet是内置的随机数发生器,其静态方法名称都比较好理解,就不在这里细细讲解了。
		if (RandomSet.getRate(50)) {//以50%的概率返回布尔值true
			Rad = RandomSet.getRandomNum(6, 35);//返回6~35随机数,均匀分布。
		} else {
			Rad = RandomSet.getRandomNum(6, 24);
		}
		disp.setWidth(Rad);
		disp.setHeight(Rad);
		//初始位置具有+/-5的浮动区域
		disp.setCentralX(FARGS[0] + RandomSet.getRandomNum(-5, 5));
		disp.setCentralY(FARGS[1] + RandomSet.getRandomNum(-5, 5));
		//初始自选角度,0~360均匀分布。
		disp.setAngle(RandomSet.getRandomFloatIn_1() * 360);
                //速度角,0~2PI均匀分布,使用弧度是为了方便调用Math下的三角函数。
		Theta = (float) Math.PI * 2 * RandomSet.getRandomFloatIn_1();
		vx = V * (float) Math.sin(Theta);//计算横纵向速度
		vy = -V * (float) Math.cos(Theta);
		ax = -0.0003f * (disp.getWidth() * disp.getWidth()) * vx;//计算加速度
		ay = -0.0003f * (disp.getHeight() * disp.getHeight()) * vy;
		disp.show();//显示到屏幕上(投射完成)
	}

	@Override
	public void doStp(int clock) {
		if (this.allms > Dms) {
			allms -= Dms;//allms记录当前剩余存活期,使用这个变量是为了将存活期规格化到0~1之间。
                        //REPR是内置的变换工具,可以将一个规格化后的线性量转化为自定义的常用非线性量
			//颜色变化
			disp.setColor(new Color(1.0f, REPR.Rep_POW_1_F(allms / sTallms, disp.getWidth() / 24), REPR.Rep_POW_F(allms / sTallms, 16)));
			//通道变化
			disp.setAlph(REPR.Rep_POW_1_F(allms / sTallms, disp.getWidth() / 12));
			//运算加速度
			ax = -0.0003f * (disp.getWidth() * disp.getWidth()) * vx;
			ay = -0.0003f * (disp.getHeight() * disp.getHeight()) * vy;
			//运算速度
			vx += ax;
			vy += ay;
			//运算位置
			disp.setDx(disp.getDx() + vx);
			disp.setDy(disp.getDy() + vy);
		} else {
                        //生命周期结束后,将粒子资源回收
			finish(Individual.SRC_INNER);
		}
	}

	@Override
	public void finish(int src) {
		disp.hide();
		//重置颜色与通道
		disp.setColor("FFFFFF");
		disp.setAlph(disp.getTopDivIndex(), 1.0f);
		//重置大小
		disp.setWidth(12);
		disp.setHeight(12);
		//重置存活时间。
		allms = sTallms;
	}

	@Override
	public void destroy() {
		disp.destroy();
	}
}

//Explosion.java:
package com.geiv.test;

import engineextend.crowdcontroller.CrowdController;
import geivcore.UESI;

public class Explosion{

	UESI UES;
	CrowdController cc;
	public Explosion(UESI UES)
	{
		this.UES = UES;
		cc = new CrowdController(UES, true);
		for(int i = 0;i < 512;i++)//装入了512个粒子资源
		{
			cc.addIndividual(new ExpIndividual(UES));
		}
	}

	public void doEffect(float dx,float dy) {
		for(int i = 0;i < 128;i++)//当每次调用时,分配128个粒子资源,同时也意味着,您可以同时在屏幕上产生4个异步的爆炸特效。
		{
			cc.getAvailible().getUse(null,dx,dy);
		}
	}

	public void forceClose() {
		cc.finishAllInd();
	}
}

//Main.java:
package com.geiv.test;

import geivcore.R;
import geivcore.UESI;

public class Main{
	public static void main(String[] args) {
		UESI UES = new R();
		Explosion exp = new Explosion(UES);
		for(;;){
			exp.doEffect(400,300); //产生一个爆炸
			UES.wait(3,1000); //延时1秒
		}
	}
}


执行效果:


[粒子特效的改进]

        “一堆圆形一点儿也不像嘛”这是我同学看到程序后的第一句评价,的确,从粒子的行为模式上来讲,是有类似爆炸的性质了,不过一个爆炸也不能只让圆形组成不是吗?

        

        ↑给一个大点的图,可以更突出的发现这个问题



        在属性设计时,我提到了关于粒子图元选择的问题,对于爆炸这个特效,显然均匀的圆形(或者其他图形)不是一种好的图元构成,我们需要一个形状并不均匀,甚至伴有随机性的图形来替换这个圆,于是笔者想到了“云”这个东西。

        

        ↑由于云是白色的,所以为了展示,把PS的衬底一起截下来了。

        云本来是与爆炸毫不相干的东西,选择它是由它的图形性质决定的:边缘渐变、具有随机性、在颜色通道上也不均匀。而且,加上我们之前定义的自旋随机分布,加入自旋角的云看起来和彼此具有更大的差异。

为了使用云这个素材,先把它放在项目目录里:

         

        之后找到ExpIndividual类,找到它的图元绘制部分:

        我们把

        disp.addGLOval("FFFFFF",0,0,12,12,12);

        disp.setGLFill(true);

        这两行改为:

        disp.addGLImage(0, 0, 12, 12,".\\Effect\\PT_CLOUD1_POINT.png");

经过改进的特效:

        

        ↑可以跟圆形做一下对比,是不是好多了呢

[总结]

        本章介绍了粒子特效设计的基本步骤,即属性、投射、演化三部分。

        粒子特效是对自然的模拟,因此在设计时要充分地考虑到物理因素,这样会得到更好的仿真结果。

        最后,恰当地选择粒子图元可以得到更好的结果,而粒子图元的选择与图形的性质有关。


[GEiv]第六章:粒子特效 绚丽的火焰与爆炸