兔年共赏电子烟花可好?

10,202 阅读8分钟

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

前言

新年到,放鞭炮。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)专栏的小伙伴应该对这一步很熟悉了吧。

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

最终代码如下

总结

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

补充

等等,明明文章开头说的是放兔子烟花,你倒是教教我们怎么放兔子烟花呢? 其实这个就比较简单与繁琐了。

说它繁琐呢 是因为兔子的轮廓需要我们自己想办法表示出来(这一块就留给大家自行解决了哈)

说它简单呢 是因为我们只需要将我们获得的数据 “硬编码” 到我们的程序中就可以了。

比如说这样,下面是我自己录的数据


float Explosion(vec2 uv, float t) {
    float spark = 0.0;
    vec2 points[69];
    points[0] = vec2(-0.109375, 0.13828125000000002);
    points[1] = vec2(-0.11666666666666664, 0.19765624999999998);
    points[2] = vec2(-0.12395833333333334, 0.24765625000000002);
    points[3] = vec2(-0.12395833333333334, 0.30078125);
    points[4] = vec2(-0.11562499999999998, 0.35234374999999996);
    points[5] = vec2(-0.10312500000000002, 0.40234375);
    points[6] = vec2(-0.08229166666666665, 0.43671875000000004);
    points[7] = vec2(-0.052083333333333315, 0.44765625);
    points[8] = vec2(-0.02395833333333336, 0.42578125);
    points[9] = vec2(-0.016666666666666663, 0.38203125000000004);
    points[10] = vec2(-0.00833333333333336, 0.32734375000000004);
    points[11] = vec2(-0.00833333333333336, 0.27109375);
    points[12] = vec2(-0.010416666666666685, 0.22734374999999996);
    points[13] = vec2(-0.013541666666666674, 0.16953125000000002);
    points[14] = vec2(0.008333333333333304, 0.16484374999999996);
    points[15] = vec2(0.009375000000000022, 0.21015625000000004);
    points[16] = vec2(0.012499999999999956, 0.26328125);
    points[17] = vec2(0.018750000000000044, 0.31171875000000004);
    points[18] = vec2(0.028124999999999956, 0.36015624999999996);
    points[19] = vec2(0.03749999999999998, 0.39296875);
    points[20] = vec2(0.055208333333333304, 0.42734375);
    points[21] = vec2(0.08229166666666665, 0.44453125000000004);
    points[22] = vec2(0.10729166666666667, 0.43515625);
    points[23] = vec2(0.125, 0.38828125);
    points[24] = vec2(0.13124999999999998, 0.33359375);
    points[25] = vec2(0.13020833333333337, 0.28046875000000004);
    points[26] = vec2(0.12395833333333328, 0.23671874999999998);
    points[27] = vec2(0.1177083333333333, 0.19609374999999996);
    points[28] = vec2(0.10520833333333335, 0.15546875000000004);
    points[29] = vec2(0.11979166666666663, 0.13359374999999996);
    points[30] = vec2(0.1479166666666667, 0.10234374999999996);
    points[31] = vec2(0.16562500000000002, 0.06640625);
    points[32] = vec2(0.17604166666666665, 0.024218749999999956);
    points[33] = vec2(0.18437499999999996, -0.025781249999999978);
    points[34] = vec2(0.18541666666666667, -0.07265624999999998);
    points[35] = vec2(0.17395833333333333, -0.11953124999999998);
    points[36] = vec2(0.14166666666666672, -0.20703125);
    points[37] = vec2(0.11145833333333333, -0.23203125000000002);
    points[38] = vec2(0.08125000000000004, -0.25390625);
    points[39] = vec2(0.048958333333333326, -0.26328125);
    points[40] = vec2(0.007291666666666696, -0.26640625);
    points[41] = vec2(-0.04583333333333334, -0.26171875);
    points[42] = vec2(-0.08437499999999998, -0.24921875);
    points[43] = vec2(-0.11249999999999999, -0.23203125000000002);
    points[44] = vec2(-0.13854166666666667, -0.20390625);
    points[45] = vec2(-0.16249999999999998, -0.16015625);
    points[46] = vec2(-0.17708333333333331, -0.11328125);
    points[47] = vec2(-0.18333333333333335, -0.05390624999999999);
    points[48] = vec2(-0.17812499999999998, 0.00390625);
    points[49] = vec2(-0.16666666666666669, 0.04921874999999998);
    points[50] = vec2(-0.14895833333333336, 0.08671874999999996);
    points[51] = vec2(-0.12708333333333333, 0.11640625000000004);
    points[52] = vec2(-0.08750000000000002, -0.07265624999999998);
    points[53] = vec2(-0.09791666666666665, -0.08984375);
    points[54] = vec2(-0.08854166666666669, -0.11015625000000001);
    points[55] = vec2(-0.078125, -0.09609374999999998);
    points[56] = vec2(0.08750000000000002, -0.09453125000000001);
    points[57] = vec2(0.09375, -0.07890625000000001);
    points[58] = vec2(0.10312500000000002, -0.09609374999999998);
    points[59] = vec2(0.09583333333333333, -0.11171874999999998);
    points[60] = vec2(-0.020833333333333315, -0.18671875);
    points[61] = vec2(-0.00833333333333336, -0.19609375);
    points[62] = vec2(0.0010416666666667185, -0.20546874999999998);
    points[63] = vec2(0.013541666666666674, -0.21953125);
    points[64] = vec2(0.019791666666666652, -0.18671875);
    points[65] = vec2(0.01041666666666663, -0.19453125);
    points[66] = vec2(-0.011458333333333348, -0.20703125);
    points[67] = vec2(-0.027083333333333348, -0.21640625000000002);
    points[68] = vec2(0.15937500000000004, -0.1679687);
    for(int i = 0; i < 69; i++) {
        points[i].y /= (960. / 640.);
        vec2 dir = points[i] * 0.8;
        float d = length(uv - dir * t);
        float fi = float(i);
        float brightness = mix(0.001, 0.005, smoothstep(0.05, 0.0, t));
        brightness *= (sin(t * 20. + fi * 68.)) * 0.5 + .5;
        brightness *= smoothstep(1.0, 0.75, t);
        spark += brightness / d;

    }
    return spark;
}

一只只可爱的小兔子就出现啦~!

20230112140915_rec_.gif