残影拖尾效果实现思路分析
今天小菜给大家分享下实现残影、拖尾效果的几种实现思路,或者叫固定套路,保准大家认真看完后,以后再也不怕实现残影、拖尾效果了。
本文字数比较多,且部分内容需要阅读代码加以思考,预计阅读10-15分钟。(画外音:小菜好不容易总结的,客官读完有收获再走呀,?)
啥是残影?小菜直接上图说明。
封面图
游戏人物挥剑动作
游戏人物冲刺动作
李小龙经典镜头
我们经常在影视剧、游戏中,看到残影的镜头。有武器特效方面的,也有人物动作方面的。看到这里想必大家应该了解到了什么是残影了吧。
小菜用白话描述下:
有一个运动的物体,在一段时间内,从这个位置运动到了那个位置,在我们看到的某个画面时间点上,却展示了物体在前一小段时间内的物体运动位置轨迹,这些轨迹往往以半透明的方式展现出来(还有其他表现形势,如上面的游戏中人物冲刺动作的残影),营造出视觉残留效果,起到不错的观赏效果。
拖尾又是啥?顾名思义,拖动尾巴,尾巴跟随的效果,拖尾常常可以和残影一起说,因为残影效果往往伴随着拖尾,就是物体运动着,在之前历史时间点的位置轨迹也会展现出来,不断的消失,不断的跟随。
但拖尾也可以单独拎出来说,不说残影效果,只说尾巴的跟随效果。我们今天的例子也会讲到。
下面我们用 Processing 来实现残影、拖尾效果,分析下如何实现。小菜将套路总结成三个:
1)半透明叠加法
2)生命流逝法
3)中学生班级晨跑法
void setup() {
size(800, 800);
background(0);
noStroke();
}
void draw() {
fill(0, 20);
rect(0, 0, width, height);
fill(30, 255, 255);
circle(mouseX, mouseY, 50);
}
我们运行下看下效果
代码简单的不能再简单了,但却能实现了一种残影拖尾效果。是不是很神奇?
我们来分析下这个残影的实现原理:
1)黑色的画布背景
2)一个跟随鼠标运动的圆,填充色RGB为30,255,255
3)每一次 draw 绘制时,都会在画布上画一层和画布背景颜色的一样,但具有一定透明度的长方形(一般和画布大小一致)
如果去掉了第三步,效果是什么样?
void draw() {
fill(30, 255, 255);
circle(mouseX, mouseY, 50);
}
很明显,我们在画布上不断的画圆,原来的圆会一直停留在画布上。所以随着我们鼠标的运动,会形成一个圆按照鼠标运行轨迹叠加出来的一个画面。
那我们清除下画布呢?每次在 draw 中都填充下背景色,可以将之前画的圆全部擦除掉
void draw() {
background(0); // 每一次绘制,都填充下背景色
fill(30, 255, 255);
circle(mouseX, mouseY, 50);
}
因为每一次绘制都把画布填充了下,会把原来绘制的圆给擦除掉,所以最终呈现的效果如上 gif 图效果。
好了。不清除画布,会导致圆按照轨迹不断叠加,形成一条圆组成的“线条“。填充背景色清除画布,会只看到一个圆跟随鼠标运动。
关键的地方来了,我们每次填充一个半透明画布大小的矩形会怎么样呢?会发生什么神奇的效果?残影 is comming!
一句话讲清原理:不断叠加的半透明矩形会越来越不透明,历史的圆圈轨迹,在半透明矩形叠加的情况下,会慢慢的消失(渐隐),跟着鼠标运动不断新绘制出来的圆,也会被后面叠加的半透明矩形给渐渐的隐藏掉。
我们来看下原理的动态演示
每次 draw 中的半透明矩形的半透明度,目前设置是20(0~255的范围),决定着残影的停留时长,设置的越低,叠加的越慢,半透明叠加到完全不透明需要的时间就越长,残影停留时间就越长。
fill(0, 20); // 20的透明度
rect(0, 0, width, height);
我们把 20 改成 60 看看,效果比较明显:
透明度20
透明度60
小菜再次尝试用一段话来描述原理:生命流逝法使用的是面向对象编程的方式,将运动的圆抽象成一个生命体,这个生命体诞生的时候具有 255 的生命值(刚好和透明度对应),随着时间的推移,这个生命体的生命也在不断流逝,降低到 0 后就会死亡,而生命体的生命值会反映在它的透明度上。
Talk is cheap, show me the code!
ArrayList<Mover> movers = new ArrayList<Mover>();
void setup() {
size(600, 600);
colorMode(RGB);
background(0);
}
void draw() {
background(0);
for (int i = movers.size() - 1; i >= 0; i--) {
Mover mover = movers.get(i);
mover.run();
if (mover.isDead()) {
movers.remove(i);
}
}
}
void mouseDragged() {
movers.add(new Mover(mouseX, mouseY, 50));
}
// Mover是生命体
class Mover {
float x;
float y;
float radius;
float life;
Mover(float x, float y, float radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.life = 255;
}
void run() {
update();
display();
}
void update() {
life -= 12;
life = max(life, 0);
}
boolean isDead() {
if (life <= 0.0) {
return true;
} else {
return false;
}
}
void display() {
fill(30, 255, 255, life);
noStroke();
circle(x, y, radius);
}
}
我们描述下代码思路:
1)我们在鼠标按下的时候,生成一个生命体,生命体诞生于鼠标的位置,生命刚出生255岁,我们将生命体加入到数组中
2)我们在每一帧的绘制中,遍历生命体数组,让生命体的生命流逝,生命流逝会导致透明度逐渐降低到0,变得透明不可见(update函数)
3)我们在每一帧的绘制中,遍历生命体数组,检查生命体是否死亡,死亡的判断依据就是生命值小于等于0,当生命体死亡的时候,我们把生命体从数组中移除,避免数组无限增大,做无谓的遍历与绘制 (isDead函数)
4)我们在每一帧的绘制中,遍历生命体数组,绘制生命体的样子(display函数)
5)记得每一帧用背景色填充,将之前的绘制擦除掉,因为不再需要。在当前帧中,有所有生命体的位置和透明度信息,可以将他们全部绘制出来
我们可以在 display 函数中额外显示下生命体的生命值:
void display() {
fill(30, 255, 255, life);
noStroke();
circle(x, y, radius);
fill(255);
text(life, x, y);
}
运行下
生命体这里都是固定的生命流逝速度,update函数中每次流逝12点生命,调整流逝速度,会直接影响残影的停留时长。先诞生的生命体,先死亡,后诞生的后死亡,于是就有了上图的效果。
这个套路常常用于实现拖尾效果。
小菜想了很久,怎么用通俗易懂的语言来描述这个原理。最终想到了上高中时,班级晨跑锻炼的场景。班级晨跑有以下几个特点:
1)班级的人数固定了,比如是30个同学
2)假设晨跑纵队是一列(为了贴近代码演示,咱们的晨跑是一个纵队,上学的时候一般纵队是2-3列,不然队伍太长了),队首同学不断的绕操场跑圈,队首不断的在更新位置(跟随鼠标)
3)队伍中除了队首同学,每个同学只需要跟着前面一个同学跑就行了,看着前一个同学的后脑勺,下一步将要跑到的位置就是前一个同学的位置
// 中学生班级晨跑法
int num = 100; // 100个同学
int[] x = new int[num];
int[] y = new int[num];
void setup() {
size(600, 600);
noStroke();
fill(255, 100);
}
void draw() {
background(0);
// 从尾巴到头部,每个节点位置更新成上一个节点的位置
// 在此帧绘制中,每一个同学的位置是上一个同学的位置
for (int i = num - 1; i > 0; i--) {
x[i] = x[i - 1];
y[i] = y[i - 1];
}
// 队首同学跑步,跟着鼠标跑
x[0] = mouseX;
y[0] = mouseY;
for (int i = 0; i < num; i++) {
// 越靠前的位置,圆圈越大,越靠后,尾巴越小
ellipse(x[i], y[i], (num - i) / 2, (num - i) / 2);
// 越靠前的位置,圆圈越小,越靠后,尾巴越大
//ellipse(x[i], y[i], i / 2, i / 2);
// 所有圆圈固定大小
//ellipse(x[i], y[i], 30, 30);
}
}
至此,小菜分享了3种实现套路,你经常用哪种呢?不妨留言?