残影拖尾实现思路分析

1178 / 2025-08-28 15:33:35 2018世界杯球队

残影拖尾效果实现思路分析

今天小菜给大家分享下实现残影、拖尾效果的几种实现思路,或者叫固定套路,保准大家认真看完后,以后再也不怕实现残影、拖尾效果了。

本文字数比较多,且部分内容需要阅读代码加以思考,预计阅读10-15分钟。(画外音:小菜好不容易总结的,客官读完有收获再走呀,?)

残影啥是残影?小菜直接上图说明。

封面图

游戏人物挥剑动作

游戏人物冲刺动作

李小龙经典镜头

我们经常在影视剧、游戏中,看到残影的镜头。有武器特效方面的,也有人物动作方面的。看到这里想必大家应该了解到了什么是残影了吧。

小菜用白话描述下:

有一个运动的物体,在一段时间内,从这个位置运动到了那个位置,在我们看到的某个画面时间点上,却展示了物体在前一小段时间内的物体运动位置轨迹,这些轨迹往往以半透明的方式展现出来(还有其他表现形势,如上面的游戏中人物冲刺动作的残影),营造出视觉残留效果,起到不错的观赏效果。

拖尾拖尾又是啥?顾名思义,拖动尾巴,尾巴跟随的效果,拖尾常常可以和残影一起说,因为残影效果往往伴随着拖尾,就是物体运动着,在之前历史时间点的位置轨迹也会展现出来,不断的消失,不断的跟随。

但拖尾也可以单独拎出来说,不说残影效果,只说尾巴的跟随效果。我们今天的例子也会讲到。

常用套路下面我们用 Processing 来实现残影、拖尾效果,分析下如何实现。小菜将套路总结成三个:

1)半透明叠加法

2)生命流逝法

3)中学生班级晨跑法

套路1-半透明叠加法代码语言:javascript代码运行次数:0运行复制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 绘制时,都会在画布上画一层和画布背景颜色的一样,但具有一定透明度的长方形(一般和画布大小一致)

如果去掉了第三步,效果是什么样?

代码语言:javascript代码运行次数:0运行复制void draw() {

fill(30, 255, 255);

circle(mouseX, mouseY, 50);

}很明显,我们在画布上不断的画圆,原来的圆会一直停留在画布上。所以随着我们鼠标的运动,会形成一个圆按照鼠标运行轨迹叠加出来的一个画面。

那我们清除下画布呢?每次在 draw 中都填充下背景色,可以将之前画的圆全部擦除掉

代码语言:javascript代码运行次数:0运行复制void draw() {

background(0); // 每一次绘制,都填充下背景色

fill(30, 255, 255);

circle(mouseX, mouseY, 50);

}因为每一次绘制都把画布填充了下,会把原来绘制的圆给擦除掉,所以最终呈现的效果如上 gif 图效果。

好了。不清除画布,会导致圆按照轨迹不断叠加,形成一条圆组成的“线条“。填充背景色清除画布,会只看到一个圆跟随鼠标运动。

关键的地方来了,我们每次填充一个半透明画布大小的矩形会怎么样呢?会发生什么神奇的效果?残影 is comming!

一句话讲清原理:不断叠加的半透明矩形会越来越不透明,历史的圆圈轨迹,在半透明矩形叠加的情况下,会慢慢的消失(渐隐),跟着鼠标运动不断新绘制出来的圆,也会被后面叠加的半透明矩形给渐渐的隐藏掉。

我们来看下原理的动态演示

每次 draw 中的半透明矩形的半透明度,目前设置是20(0~255的范围),决定着残影的停留时长,设置的越低,叠加的越慢,半透明叠加到完全不透明需要的时间就越长,残影停留时间就越长。

代码语言:javascript代码运行次数:0运行复制 fill(0, 20); // 20的透明度

rect(0, 0, width, height);我们把 20 改成 60 看看,效果比较明显:

透明度20

透明度60

套路2-生命流逝法小菜再次尝试用一段话来描述原理:生命流逝法使用的是面向对象编程的方式,将运动的圆抽象成一个生命体,这个生命体诞生的时候具有 255 的生命值(刚好和透明度对应),随着时间的推移,这个生命体的生命也在不断流逝,降低到 0 后就会死亡,而生命体的生命值会反映在它的透明度上。

Talk is cheap, show me the code!

代码语言:javascript代码运行次数:0运行复制ArrayList movers = new ArrayList();

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 函数中额外显示下生命体的生命值:

代码语言:javascript代码运行次数:0运行复制 void display() {

fill(30, 255, 255, life);

noStroke();

circle(x, y, radius);

fill(255);

text(life, x, y);

}运行下

生命体这里都是固定的生命流逝速度,update函数中每次流逝12点生命,调整流逝速度,会直接影响残影的停留时长。先诞生的生命体,先死亡,后诞生的后死亡,于是就有了上图的效果。

套路3-中学生班级晨跑法这个套路常常用于实现拖尾效果。

小菜想了很久,怎么用通俗易懂的语言来描述这个原理。最终想到了上高中时,班级晨跑锻炼的场景。班级晨跑有以下几个特点:

1)班级的人数固定了,比如是30个同学

2)假设晨跑纵队是一列(为了贴近代码演示,咱们的晨跑是一个纵队,上学的时候一般纵队是2-3列,不然队伍太长了),队首同学不断的绕操场跑圈,队首不断的在更新位置(跟随鼠标)

3)队伍中除了队首同学,每个同学只需要跟着前面一个同学跑就行了,看着前一个同学的后脑勺,下一步将要跑到的位置就是前一个同学的位置

代码语言:javascript代码运行次数:0运行复制// 中学生班级晨跑法

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种实现套路,你经常用哪种呢?不妨留言?