兔年共赏电子烟花可好?


highlight: a11y-light
theme: smartblue

“我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛

前言

新年到,放鞭炮。Wait……根据国家相关法律法规,禁止在市区燃放鞭炮。。。Emmmm,真是破了大防。不过嘛,我在电脑上燃放一下电子鞭炮总没问题吧???

所以,今天我们就来在电脑上完成一个简单的燃放鞭炮的小特效。

我们的运行环境当然是作者喜欢的Shadertoy!!!如果还不知道shadertoy及运行环境的小伙伴,可以查看这篇专栏里的文章。 Shader从入门到放弃 – 鹤云云的专栏 – 掘金 (juejin.cn)

阅读本文你将会收获一幅美丽的兔子🐰烟花🎆图~

20230106-141535.gif

编码

归一化UV坐标

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0.0);

    float d = length(uv);
    col += 0.005 / d;
    fragColor = vec4(col, 1.0);
}

之前学习过Shader从入门到放弃 – 鹤云云的专栏 – 掘金 (juejin.cn)专栏的小伙伴应该对这一步很熟悉了吧。

不过鉴于有新人朋友阅读文章,在这里我还是啰嗦一下,这一步主要是将坐标进行一次重新的映射。
假设我们的画布区域的大小是 640×360,其原点处于左下角。我们的代码是将坐标转化为 -0.5~0.5 之间。

iShot_2023-01-09_18.23.06-tuya.png

现在我们就将坐标转化为了 -0.5 ~ 0.5 之间了。
d = length(uv) 则是计算各个像素点距离画布中心的位置了。

我们可以设置col = d来观察各个像素点距离画布中心的颜色,像素颜色越黑则表示其距离画布中心越近。其距离的范围是 0~0.5 这一点一定要记住。
image.png

为什么我们需要用一个常数来除以这个d 呢?因为我们想要其中心很亮,然后离中心越远的地方就越暗淡。如下面的函数图像所示,我们可以通过调整这个常数项来改变我们的圆点的亮度和大小。常数越大的话我们的圆点就越大、越亮。

image.png

这里我们暂时取值0.005吧。结果如下:
image.png

这个小圆点就是贯穿我们始终的东西了,也就是所谓的“粒子”。我们将使用多个粒子来绘制我们的烟花。

动起来吧

第二步我们就要让这个小家伙动起来了。看过专栏的同学应该知道,如果我们想要实现一些动画,我们肯定是需要用到 iTime 这个参数的,这里我再啰嗦一下。

iTime 是shadertoy 提供的一个内置的变量,它会随着时间不断地变大。所以我们可以利用它来实现一下动画。
首先,第一步要做的依然还是对其进行归一化。否则动画就会拥有无限的时间轴,而不会反复进行播放了。

Wait!可能你又要问,这个iTime 是不断变大的如何让它“归一化”呢? 呃呃呃,这里是作者的措词问题。其实也不能算是归一化吧。就是把 iTime 限制在 0~1的区间。

GLSL为我们提供了一个常用的函数 fract 该函数可以取其小数点后的部分。

Wait Wait Wait,这里有一个极易犯错的点!

fract(x) 函数等价于的是 x - floor(x)。这就意味着:fract(-1.2345)并不等于 -0.2345 也不等于 0.2345,它实际上等价于 -1.2345 - floor(-1.2345) = -1.2345 - (-2) = 0.7655!!!

现在我们修改代码如下:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0.0);

    float t = fract(iTime);
    vec2 dir = vec2(1.0, 1.0) * 0.5;

    vec2 p = uv - dir * t;

    float d = length(p);
    col += 0.008 / d;

    fragColor = vec4(col, 1.0);
}

容我对以上代码稍加解释:

  • t = fract(iTime) 是为了将 iTime 限制在 0~1的范围内。
  • dir = vec2(1.0, 1.0) * 0.5 是为了给我们的粒子一个运动的方向。而 0.5 则是因为我们的画布范围只有 -0.5~0.5 所以我们要限制其范围。
  • p = uv - dir * t,根据当前时间来计算当前粒子的位置

最终结果如下:
20230109184637_rec_.gif

More And More

嘿!如果我说我们的程序已经完成了30%你相信吗。但是事实就是如此,我们的程序的基本结构已经快呼之欲出了。对于单个的烟花粒子来说,它就是从一处运动到另一处的过程,现在我们要增加更多的烟花粒子!!!

从直觉上来说,我们需要增加更多的粒子的话,第一个击中你的思路是什么?没错,就是for 循环。假设我们现在有10个粒子。我们很容易写出下面的代码:

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0.0);

    float t = fract(iTime);
    for(float i = 0.0; i < 10.0; i++) {
        vec2 dir = vec2(1.0, 1.0) * 0.5;
        vec2 p = uv - dir * t;
        float d = length(p);
        col += 0.008 / d;
    }

    fragColor = vec4(col, 1.0);
}

但是,我们现在这个几个粒子都是朝着同一个方向在运动,这可不妙,我们希望每个粒子的运动方向都不一样。所以我们需要一个根据 i 产生不同方向的 “随机函数”。这里我为大家提供一个函数。

vec2 Hash12(float f) {
    float a = fract(sin(f * 3456.12) * 7529.12);
    float b = fract(sin(a + f * 123.789) * 2346.67);
    return vec2(a, b);
}

该函数与其说是一个 “随机函数”,不如说是一个哈希函数,因为它并不是真正意思上的随机,它所起到的作用其实就是只要输入值有一点点的变化,输出的结果就会有很大的差异。

所以该函数的一个接受一个浮点数,输出一个2维向量的哈希函数。

通常的做法就是使用三角函数,再乘以一个很大的值,最后取它的小数部分。这里需要读者好好体会一番。

有了这个函数我们可以改写我们 for 循环中的部分:

for(float i = 0.0; i < 50.0; i++) {
    vec2 dir = Hash12(i + 1.0) - 0.5;
    vec2 p = uv - dir * t;
    float d = length(p);
    col += 0.0005 / d;
}

结果如下:

20230109-212937.gif

看起来不错。。。But,这个烟花的造型似乎是……呃呃呃,有点??Excuse me?(问号❓脸)

这是因为我们使用的是直角坐标系,要修正这个问题,我们需要先随机产生极坐标系的坐标,然后将极坐标系转化为直角坐标系。

我们修改一下我们的哈希函数,并修改for循环中的内容

vec2 Hash12Polar(float f) {
    float rad = fract(sin(f * 3456.12) * 7529.12) * 3.1415926 * 2.0;
    float r = fract(sin((rad + f) * 714.57) * 567.234);

    float x = cos(rad);
    float y = sin(rad);
    return vec2(x, y) * r;
}

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0.0);

    float t = fract(iTime);
    for(float i = 0.0; i < 50.0; i++) {
        vec2 dir = Hash12(i + 1.0) - 0.5;
        vec2 p = uv - dir * t;
        float d = length(p);
        col += 0.0005 / d;
    }

    fragColor = vec4(col, 1.0);
}

结果如下:这下好像不错了诶。
20230109-212548.gif

Boom! 艺术就是爆炸!

现在我们的粒子能够正常的炸开!但是似乎……缺少了一点视觉上的冲击感?

我们期望这个烟花能够爆炸的更加激烈一些。现在的烟花似乎不够亮,众所周知,烟花爆炸的那一瞬间是非常的耀眼,宛如一瞬即逝的艺术。

所以,不如让我们把烟花调亮一点?我们引入一个新的变量brightness来表示亮度

float brightness = 0.005;
col += brightness / d;

这下够亮!

20230109213829_rec_.gif

接下来我们要控制一下这个亮度的时间,我们要让这种艺术消失于一瞬之间。 我们先来看一下代码吧。

float minBrightness = 0.001;
float maxBrightness = 0.005;
float brightness = mix(maxBrightness, minBrightness, smoothstep(0.0, 0.05, t));
col += brightness / d;

我们先声明了两个变量来分别表示最小的亮度和最大的亮度。然后我们想要的效果是在很短的时间内,最大的亮度就衰减到最小值。

从最大值变为最小值(衰减的过程),我们可以使用 mix 函数。此处再稍微啰嗦一点,mix 函数的作用是在两个值之间进行线性插值,它等价于:

function mix(lower, upper, p) {
    return lower * (1 - p) + upper * p;
}

快速的让时间变化,我们需要使用到 smoothstep 函数,该函数可以将值分为三个部分。

smoothstep(a, b, x) 它接受3个参数。分为两种情况:

  1. if a < b:

    x < a时,x = 0; x > b时,x = 1; 否则它在 a, b之间进行线性插值,结果在 0 ~ 1之间

  2. if a > b:
    x > a时,x = 0; x < b时,x = 1; 否则它在 a, b之间进行线性插值,结果在 0 ~ 1之间

这里需要多加体会一下。如果你现在不懂就暂时先接着往下看吧。

通过刚刚的一番操作,我们可以得到这样的结果(此处由于gif图帧率不足无法展示爆炸💥效果,就先不放图了,最后看代码吧。)

现在我们完成了单个爆炸效果,我们可以使用一个函数将其封装起来,以提高代码的可读性。

float Explosion(vec2 uv, float t) {
    float m = 0.0;
    for(float i = 0.0; i < 50.0; i++) {
        vec2 dir = Hash12Polar(i + 1.0) * 0.5;
        vec2 p = uv - dir * t;
        float d = length(p);
        float minBrightness = 0.001;
        float maxBrightness = 0.005;
        float brightness = mix(maxBrightness, minBrightness, smoothstep(0.0, 0.05, t));
        m += brightness / d;
    }
    return m;
}

更多的烟花!

与创建多个粒子来表示烟花类似的,我们可以通过for 循环来创建多个烟花!

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0.0);

    float t = fract(iTime);
    for (float i = 0.0; i < 5.0; i++) {
        vec2 offs = Hash12(i + 1.0) - 0.5;
        col += Explosion(uv - offs, t);
    }
    
    fragColor = vec4(col, 1.0);
}

20230109-215904.gif

现在,我们拥有多个烟花啦~ 但是他们都是一起爆炸的,我们想要他们爆炸的时间有点参差感。

所以,我们需要将时间 t 放到 for 循环里面去。并且,我们也想要让爆炸的位置随着时间变化,所以我们也需要将我们的时间t 传入到哈希函数中。

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0.0);

    
    for (float i = 0.0; i < 5.0; i++) {
        float t = iTime + i / 5.0;
        float ft = floor(t); // 这里的floor表示向下取整,可以理解为是每一个烟花的id
        vec2 offs = Hash12(i + 1.0 + ft * 0.1) - 0.5;
        col += Explosion(uv - offs, fract(t));
    }
    
    fragColor = vec4(col, 1.0);
}

结果如下:

20230109220710_rec_.gif

绽放更加美丽的色彩吧!

我们快要大功告成了,最后只需要修改它们的颜色就完成啦~!


void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord.xy - .5 * iResolution.xy) / iResolution.y;
    vec3 col = vec3(0.0);

    
    for (float i = 0.0; i < 5.0; i++) {
        float t = iTime + i / 5.0;
        float ft = floor(t);
        vec2 offs = Hash12(i + 1.0 + ft * 0.1) - 0.5;
        vec3 color = sin(i + ft * vec3(.34, .56, .78)) * 0.25 + 0.75;
        col += Explosion(uv - offs, fract(t)) * color;
    }
    
    fragColor = vec4(col, 1.0);
}

结果如下啦~

20230109221029_rec_.gif

最终代码如下

jcode

总结

今天我们的烟花盛宴就到此为止啦~ 想必大家都学会了吧。如果没学会也没关系,再仔细的品读上面的文章,相信这个春天你也能开出最绚烂的花~

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容