专栏:EGE专栏
专栏:EGE示例程序
示例程序下载
花火闪烁的夜晚
站点 | 链接 |
---|---|
百度网盘 | 示例一 花火闪烁的夜晚 |
CSDN | 示例一 花火闪烁的夜晚 (无需积分) |
一、烟花
在做烟花特效前,先来看看烟花的样子。
烟花各式各样,并不是只有一种烟花。而接下来要做的是下面这种比较简单的。烟花在发射后,会拖着一条尾巴快速上升,到达一定高度后快速散开,最后逐渐熄灭。
二、烟花效果的设计
1. 烟花发射过程
我们将烟花发射过程分为三个阶段,分别是准备阶段、上升阶段和爆炸阶段。
在每个阶段,我们都会给烟花设定一个在某个范围内的随机时间,这样就能够将烟花的发射时间错开,不至于过于整齐而显得比较刻意。
在准备阶段,烟花尚未点燃,不进行显示。准备阶段结束后进入发射阶段,烟花以一定的初速度上升,并且由于重力和空气阻力等因素速度减小,上升一定时间后爆炸,分成多个粒子向四周散开。粒子散开后会受到重力和空气阻力等因素影响会往下坠,一段时间后熄灭。
2. 烟花的构成
烟花可以看做由很多个有颜色的粒子组成,每一个粒子都带有各自的速度(矢量)。同一个烟花可以是五颜六色,也可以只有一种颜色。
上升过程中,粒子基本都堆在一起,炸开时分散向四面八方,会形成一个球状,受重力和空气阻力影响,粒子向外发射后会逐渐向下方坠落。受粒子本身重量、形状及大小等因素影响,不同种类的烟花在爆炸时,发射出的粒子的运动轨迹并不相同。
烟花在上升阶段是一个整体,上升到最高点后爆炸分散成大量粒子。粒子具有一定的初速度,在重力、空气阻力等影响下位置和速度都在不断地变化。
由于粒子的位置和速度并不相同,所以需要分别记录每个粒子的位置和速度信息。
在本例中,同一个烟花的粒子具有同样大小和颜色,因此并没有单独为每个粒子记录大小和颜色信息。
实际上也可以将所有粒子分成几部分,每一个部分具有相同的颜色,这样在不必记录过多颜色信息的同时也能达到较好的效果。
粒子按如下定义,记录位置和速度信息。
//速度
struct Speed
{
double x, y;
};
//位置
struct Pos
{
double x, y;
};
//粒子
struct Particle
{
Pos pos;
Speed speed;
};
烟花包含多个粒子,还有发射位置、发射速度以及各阶段的持续时间。粒子的初始位置和射出速度则在爆炸时给出。
由此我们定义的烟花类包含以下成员变量:
//烟花
class Fireworks
{
private:
static const int NUM_PARTICLE = 200; //烟花包含的粒子数
static const float particleSpeed; //爆炸时粒子射出的速率
private:
Particle p[NUM_PARTICLE]; //粒子
color_t color; //烟花颜色
int waitingTime; //等待时间
int riseTime; //上升时间
int bloomTime; //爆炸时间
Pos pos; //位置(上升阶段)
Speed speed; //速度(上升阶段)
......
};
3. 烟花和粒子的位置、速度计算
本例中粒子只在二维平面内运动,不考虑三维透视。
烟花在初始化时,会给出一个初始位置和初始速度,后面就需要根据这个位置和速度,配合重力等因素来计算出烟花和粒子的位置。
3.1 位置的计算
在每一帧中,烟花或粒子都会朝着当前方向移动一段距离。这里的速度是以帧为时间单位的,因此每一帧中,位移都和速度相等:
Δ
x
=
v
t
=
v
\Delta x=vt = v
Δx=vt=v 下一帧的位置为
x
′
=
x
+
Δ
x
x' = x + \Delta x
x′=x+Δx
烟花发射升空时是一个整体,爆炸时会分散成多个粒子。因此粒子的初始位置可以取烟花爆炸时的位置。
3.1 速度的计算
烟花发射升空受到重力和空气阻力的影响,会有向下的加速度, v ′ = v + a t v'=v+at v′=v+at 每帧的速度变化量 a a a 调整至合适的数值即可。
烟花爆炸后,随着粒子逐渐燃烧殆尽,空气阻力对粒子速度的影响也越来越大。因此,每一帧会对粒子速度进行衰减 v ′ = k v v' = kv v′=kv 系数 k k k 调整至合适值即可 ( 0 < k ⩽ 1 ) (0 < k \leqslant 1) (0<k⩽1)。
4. 烟花及粒子的绘制
爆炸时散射出去的粒子较小,直接在当前位置上绘制一个像素点即可。烟花升空时的轨迹较粗,可以绘制稍大一点的圆或矩形。
烟花升空和粒子向四周散射时都会留下一条长长的轨迹,可以通过对图像进行模糊操作来达到这种拖尾效果。粒子移动后,原来位置上绘制的粒子会随着模糊操作的不断进行而逐渐淡化,最后消失。
相关函数:圆( fillellipse ), 矩形(bar),模糊滤镜函数 (imagefilter_blurring)。
三、 实现
1. 三个阶段的时间
整个烟花的过程分为三个阶段:准备阶段、发射阶段和绽放阶段。分别由下面的三个参数来控制时间长短。
- delayTime :延迟时间, 烟花发射的时刻是不同的,设置 随机的延迟时间,延迟时间过后烟花发射。
- riseTime :上升时间,上升时间决定烟花发射多久后会爆炸,越大上升得越高, 设置随机时间来让烟花错开。
- bloomTime :绽放时间, 绽放后多长时间会消失。
参数是通过不断调整得来的,最后得到一个比较合适的设置。程序中初始化时这三个时间如下所示:
delayTime = rand() % 300 + 20;
riseTime = rand() % 80 + 160;
bloomTime = 160;
2. 发射速度和方向
下面就是背景图,需要让烟花在地面上释放,而且在地面的不同地方发射,并且控制在一定范围内,发射的方向也需要进行控制。地面高度的是相同的,即y坐标相同,不同的是x坐标。
pos :烟花的位置, 要让烟花在不同的地点发射,可以设置不同的初始值。
Speed speed :烟花上升时的速度。
pos.x = rand() % 450 + 300.0f;
pos.y = GROUND_Y; //地面位置是580
speed.y = myrand(1.0f) - 3.0f; //上升速度,根据坐标系需要是负的
speed.x = myrand(0.4f) - 0.2f; //可稍微倾斜
3. 烟花绘制的流程
3.1 需要考虑的内容
- 烟花包含多个粒子,粒子数,爆炸时粒子速度大小,粒子颜色。
- 设置一些时间变量,用来控制烟花上升的时刻,时长和绽放的时间。
- 上升速度和位置,需要注意坐标系里y是向下为正。烟花初始随机设定好本次的发射位置,和时间,速度等相关属性,绽放完后重新开始。每次都会先更新烟花位置再绘制。
- 考虑一下重力因素(有往下的加速度),空气阻力因素(速度衰减),爆炸时的球状散开(速度大小相等,方向不同),可以求x, y分速度。
- 稍微考虑一下水平动量守恒,做一对速度相反的粒子,因为是随机的,怕有时太随机了偏向一边。
3.2 烟花流程
① 创建烟花。
② 对烟花进行 初始化 ,设置烟花从准备到发射,再到绽放的一整个流程的参数,比如三个时间,爆炸时每个粒子的速度。
其中粒子爆炸速度比较难计算,因为要设置爆炸成球状,速度大小都是相等的,只是速度的方向不同。随机得到方向后,就 将速度投影到xOy平面上,然后再分别投影到x轴和y轴。 可以参考空间向量如何获得其两个x, y方向的分量。
void Fireworks::init()
{
delayTime = rand() % 300 + 20;
riseTime = rand() % 80 + 160;
bloomTime = 160;
risePos.x = rand() % 450 + 300.0f;
risePos.y = GROUND;
riseSpeed.y = myrand(1.0f) - 3.0f; //上升速度,根据坐标系需要是负的
riseSpeed.x = myrand(0.4f) - 0.2f; //可稍微倾斜
//随机颜色
color = HSVtoRGB(myrand(360.0f), 1.0f, 1.0f);
//给每一个粒子设置初始速度
for (int i = 0; i < NUM_PARTICLE - 1; i += 2)
{
//为了球状散开,设初始速度大小相等
//初始随机速度水平角度和垂直角度,因为看到是平面的,所以求x, y分速度
float levelAngle = randomf() * 360;
float verticalAngle = randomf() * 360;
//速度投影到xOy平面
float xySpeed = particleSpeed * cos(verticalAngle);
//求x, y分速度
p[i].speed.x = xySpeed * cos(levelAngle);
p[i].speed.y = xySpeed * sin(levelAngle);
//动量守恒,每对速度反向
if (i + 1 < NUM_PARTICLE) {
p[i + 1].speed.x = -p[i].speed.x;
p[i + 1].speed.y = -p[i].speed.y;
}
}
}
③ 每一帧里对烟花的位置以及粒子的位置进行更新。
上升时只更新烟花的位置就行了,因为粒子都在一起,爆炸时就要分别计算粒子的位置了。从上升到爆炸的过渡阶段,要给每一个粒子设置位置,因为之前更新的都是烟花的位置,粒子的位置没有单独给出,此时直接将粒子位置设置为烟花当前位置。每次对位置进行更新,都将对应的时间值减1。
阶段的判断:
准备阶段 :延迟时间 大于0, 不更新位置
上升阶段 :延迟时间 为 0,并且 上升时间 大于0, 更新烟花位置,如果到达爆炸阶段,就给粒子当前位置。
爆炸阶段 :前面两个时间都不大于0,爆炸时间 大于0, 更新粒子位置
结束阶段 :前面三个时间都为0,这时就对烟花重新初始化
//更新位置等相关属性
void Fireworks::update()
{
if (delayTime > 0) {
delayTime--;
return;
}
//处于上升阶段,只更新烟花位置
else if (riseTime > 0) {
risePos.x += riseSpeed.x;
risePos.y += riseSpeed.y;
//重力作用
riseSpeed.y += 0.005;
//上升完毕,到达爆炸阶段
if (--riseTime <= 0) {
//设粒子初始位置为烟花当前位置
for (int i = 0; i < NUM_PARTICLE; i++) {
p[i].pos.x = risePos.x;
p[i].pos.y = risePos.y;
}
}
}
//烟花绽放阶段
else if (bloomTime > 0) {
bloomTime--;
//粒子散开,更新粒子位置
for (int i = 0; i < NUM_PARTICLE; i++) {
p[i].pos.x += p[i].speed.x;
p[i].pos.y += p[i].speed.y;
//重力作用
p[i].speed.y += 0.005;
//速度减慢
p[i].speed.x *= 0.982;
p[i].speed.y *= 0.982;
}
}
else {
//烟花重新开始
init();
}
}
④ 每一帧里,如果是准备阶段,不绘制,如果是上升阶段,就根据烟花的位置绘制,如果是爆炸阶段,就根据每一个粒子的位置进行绘制。
void Fireworks::draw(PIMAGE pimg)
{
//未开始
if (delayTime > 0)
return;
//烟花上升阶段
else if (riseTime > 0) {
setfillcolor(color, pimg);
//画四个点,这样大一些
bar(risePos.x, risePos.y, risePos.x + 2, risePos.y + 2, pimg);
}
//烟花绽放阶段
else {
setfillcolor(color, pimg);
for (int i = 0; i < NUM_PARTICLE; i++) {
bar(p[i].pos.x, p[i].pos.y, p[i].pos.x + 2, p[i].pos.y + 2, pimg);
}
}
}
最后需要对烟花进行加模糊滤镜,这个是为了加拖尾效果,否则粒子都只是一个小点,不带拖尾。可以通过调整模糊滤镜的参数来改变不同的效果。
4. 程序流程
① 创建烟花,播放背景音乐
② 帧循环中,不断绘制背景于窗口,更新烟花位置, 绘制烟花于缓存,对缓存加模糊滤镜,绘制缓存到窗口。
③ 结束时释放资源
注意事项
因为涉及模糊滤镜,所以不可能在有背景的窗口加模糊滤镜,这样背景会糊掉,所以只能另外设个图像缓存,在里面做图像模糊操作。
将图像缓存绘制到窗口时,要特别注意,我们需要的是光叠加的效果,所以绘制时需要到三元光栅操作码 SRCPAINT, 即 目标图像和源图像颜色位或,这个请参考官网源文档,这个码还挺难找的,因为列表里操作码很多。
5. 程序源码
下面是项目的一些文件:
其中包括:
- 源文件:fireworks.cpp, fireworks.h 和 main.cpp
- 资源文件:花火bg.jpg(图片文件), 羽肿 - 花火が瞬く夜に.mp3(音乐文件)
5.1 fireworks.h
下面是 fireworks.h 头文件的内容:
#pragma once
#ifndef FIREWORKS_H_
#define FIREWORKS_H_
#include <graphics.h>
//随机数, 0.0~1.0
#define myrand(m) ((float)rand() * m / 36565)
struct Speed {
double x, y;
};
struct Pos {
double x, y;
};
struct Particle
{
Pos pos;
Speed speed;
};
#define GROUND 580 //地面位置y坐标
class Fireworks//烟花类
{
private:
static const int NUM_PARTICLE = 200;
static const double particleSpeed;
Particle p[NUM_PARTICLE];
color_t color;
int delayTime; //延迟时间
int riseTime; //上升时间
int bloomTime; //爆炸时间
Pos risePos; //上升阶段位置
Speed riseSpeed; //上升速度
public:
//初始化
Fireworks();
void init();
//更新位置等相关属性
void update();
//根据属性值绘画
void draw(PIMAGE pimg = NULL);
};
#endif // ! FIREWORKS_H_
5.2 fireworks.cpp
下面是 fireworks.cpp 文件的内容:
#include <cmath>
#define SHOW_CONSOLE
#include "fireworks.h"
const double Fireworks::particleSpeed = 3.0f;
Fireworks::Fireworks()
{
init();
}
void Fireworks::init()
{
delayTime = rand() % 300 + 20;
riseTime = rand() % 80 + 160;
bloomTime = 160;
risePos.x = rand() % 450 + 300.0f;
risePos.y = GROUND;
riseSpeed.y = myrand(1.0f) - 3.0f; //上升速度,根据坐标系需要是负的
riseSpeed.x = myrand(0.4f) - 0.2f; //可稍微倾斜
//随机颜色
color = HSVtoRGB(myrand(360.0f), 1.0f, 1.0f);
//给每一个粒子设置初始速度
for (int i = 0; i < NUM_PARTICLE - 1; i += 2)
{
//为了球状散开,设初始速度大小相等
//初始随机速度水平角度和垂直角度,因为看到是平面的,所以求x, y分速度
double levelAngle = randomf() * 360;
double verticalAngle = randomf() * 360;
//速度投影到xOy平面
double xySpeed = particleSpeed * cos(verticalAngle);
//求x, y分速度
p[i].speed.x = xySpeed * cos(levelAngle);
p[i].speed.y = xySpeed * sin(levelAngle);
//动量守恒,每对速度反向
if (i + 1 < NUM_PARTICLE) {
p[i + 1].speed.x = -p[i].speed.x;
p[i + 1].speed.y = -p[i].speed.y;
}
}
}
void Fireworks::draw(PIMAGE pimg)
{
//未开始
if (delayTime > 0)
return;
//烟花上升阶段
else if (riseTime > 0) {
setfillcolor(color, pimg);
//画四个点,这样大一些
bar(risePos.x, risePos.y, risePos.x + 2, risePos.y + 2, pimg);
}
//烟花绽放阶段
else {
setfillcolor(color, pimg);
for (int i = 0; i < NUM_PARTICLE; i++) {
bar(p[i].pos.x, p[i].pos.y, p[i].pos.x + 2, p[i].pos.y + 2, pimg);
}
}
}
//更新位置等相关属性
void Fireworks::update()
{
if (delayTime-- > 0)
return;
//处于上升阶段,只更新烟花位置
else if (riseTime > 0) {
risePos.x += riseSpeed.x;
risePos.y += riseSpeed.y;
//重力作用
riseSpeed.y += 0.005;
//上升完毕,到达爆炸阶段
if (--riseTime <= 0) {
//设粒子初始位置为烟花当前位置
for (int i = 0; i < NUM_PARTICLE; i++) {
p[i].pos.x = risePos.x;
p[i].pos.y = risePos.y;
}
}
}
//烟花绽放阶段
else if (bloomTime-- > 0) {
//粒子散开,更新粒子位置
for (int i = 0; i < NUM_PARTICLE; i++) {
p[i].pos.x += p[i].speed.x;
p[i].pos.y += p[i].speed.y;
//重力作用
p[i].speed.y += 0.005;
//速度减慢
p[i].speed.x *= 0.982;
p[i].speed.y *= 0.982;
}
}
else {
//烟花重新开始
init();
}
}
5.3 main.cpp
下面是 main.cpp 文件的内容:
用到了两个文件,一张背景图片和一首背景音乐,要放在main.cpp文件旁边。代码中用的是相对路径,直接默认文件位于当前目录下。
#include <time.h>
#include <graphics.h>
#include "fireworks.h"
#define NUM_FIREWORKS 12 //烟花数量,12左右比较好,多了太密集
int main()
{
initgraph(800, 800, INIT_RENDERMANUAL);
srand((unsigned)time(NULL));
//烟花
Fireworks* fireworks = new Fireworks[NUM_FIREWORKS];
//背景图(800 x 800)
PIMAGE bgPimg = newimage();
getimage(bgPimg, "花火bg.jpg");
//先绘制一下,不然前面有空白期
putimage(0, 0, bgPimg);
delay_ms(0);
//背景音乐
MUSIC bgMusic;
bgMusic.OpenFile("羽肿 - 花火が瞬く夜に.mp3");
bgMusic.SetVolume(1.0f);
if (bgMusic.IsOpen()) {
bgMusic.Play(0);
}
//图像缓存, 因为要加背景图,直接加模糊滤镜会把背景图模糊掉
//所以另设一个图像缓存来绘制烟花并加模糊滤镜,再绘制到窗口
PIMAGE cachePimg = newimage(800, 800);
//计时用,主要用来定时检查音乐播放
int timeCount = 0;
for (; is_run(); delay_fps(60))
{
//隔1秒检查一下,如果播放完了,重新播放
if ((++timeCount % 60 == 0) && (bgMusic.GetPlayStatus() == MUSIC_MODE_STOP)) {
bgMusic.Play(0);
}
//更新位置
for (int i = 0; i < NUM_FIREWORKS; i++) {
fireworks[i].update();
}
//清屏
cleardevice();
//绘制背景
putimage(0, 0, bgPimg);
//绘制烟花到图像缓存中
for (int i = 0; i < NUM_FIREWORKS; i++) {
fireworks[i].draw(cachePimg);
}
//模糊滤镜,拖尾效果
//第二个参数,模糊度,越大越模糊,粒子也就越粗
//第三个参数,亮度,越大拖尾越长
//可以试试一下其它参数搭配,例如以下几组:
//0x03, 0xff
//0x0b, 0xe0
//0xff, 0xff
imagefilter_blurring(cachePimg, 0x0a, 0xff);
//缓存绘制到窗口,模式为(最终颜色 = 窗口像素颜色 Or 图像像素颜色), 这样颜色会叠加起来
putimage(0, 0, cachePimg, SRCPAINT);
}
//释放动态分配的内存
delete[] fireworks;
//删除图像,释放资源
delimage(bgPimg);
delimage(cachePimg);
//释放音频资源
bgMusic.Close();
closegraph();
return 0;
}
6. 运行过程截图
背景图原来是有烟花的,PS抠掉了,不会抠图,有些没抠掉。本来想把灯笼单独抠出来做张图像,也不会抠。背景图里有水,水里少了倒影,这个是可以计算出倒影的位置的,粒子根据地面位置对称一下就完事了。但是水边有草,这个应该在上面,应该要单独抠出来,最后放上去,不然一弄下来草上是有烟花的。不会抠,懒得弄。
(背景图和背景歌曲都来自羽肿的轻音乐 《花火が瞬く夜に》, 挺喜欢的 ٩(๑>◡<๑)۶ )
EGE专栏:EGE专栏