“我正在参加「兔了个兔」创意投稿大赛,详情请看:「兔了个兔」创意投稿大赛”
前言
新年到,放鞭炮。Wait......根据国家相关法律法规,禁止在市区燃放鞭炮。。。Emmmm,真是破了大防。不过嘛,我在电脑上燃放一下电子鞭炮总没问题吧???
所以,今天我们就来在电脑上完成一个简单的燃放鞭炮的小特效。
我们的运行环境当然是作者喜欢的Shadertoy!!!如果还不知道shadertoy及运行环境的小伙伴,可以查看这篇专栏里的文章。 Shader从入门到放弃 - 鹤云云的专栏 - 掘金 (juejin.cn)
阅读本文你将会收获一幅美丽的兔子🐰烟花🎆图~
编码
归一化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 之间。
现在我们就将坐标转化为了 -0.5 ~ 0.5 之间了。
而 d = length(uv) 则是计算各个像素点距离画布中心的位置了。
我们可以设置col = d来观察各个像素点距离画布中心的颜色,像素颜色越黑则表示其距离画布中心越近。其距离的范围是 0~0.5 这一点一定要记住。
为什么我们需要用一个常数来除以这个d 呢?因为我们想要其中心很亮,然后离中心越远的地方就越暗淡。如下面的函数图像所示,我们可以通过调整这个常数项来改变我们的圆点的亮度和大小。常数越大的话我们的圆点就越大、越亮。
这里我们暂时取值0.005吧。结果如下:
这个小圆点就是贯穿我们始终的东西了,也就是所谓的“粒子”。我们将使用多个粒子来绘制我们的烟花。
动起来吧
第二步我们就要让这个小家伙动起来了。看过专栏的同学应该知道,如果我们想要实现一些动画,我们肯定是需要用到 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,根据当前时间来计算当前粒子的位置
最终结果如下:
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;
}
结果如下:
看起来不错。。。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);
}
结果如下:这下好像不错了诶。
Boom! 艺术就是爆炸!
现在我们的粒子能够正常的炸开!但是似乎……缺少了一点视觉上的冲击感?
我们期望这个烟花能够爆炸的更加激烈一些。现在的烟花似乎不够亮,众所周知,烟花爆炸的那一瞬间是非常的耀眼,宛如一瞬即逝的艺术。
所以,不如让我们把烟花调亮一点?我们引入一个新的变量brightness来表示亮度
float brightness = 0.005;
col += brightness / d;
这下够亮!
接下来我们要控制一下这个亮度的时间,我们要让这种艺术消失于一瞬之间。 我们先来看一下代码吧。
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个参数。分为两种情况:
-
if a < b:
x < a时,x = 0; x > b时,x = 1; 否则它在 a, b之间进行线性插值,结果在 0 ~ 1之间
-
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);
}
现在,我们拥有多个烟花啦~ 但是他们都是一起爆炸的,我们想要他们爆炸的时间有点参差感。
所以,我们需要将时间 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);
}
结果如下:
绽放更加美丽的色彩吧!
我们快要大功告成了,最后只需要修改它们的颜色就完成啦~!
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);
}
结果如下啦~
最终代码如下
总结
今天我们的烟花盛宴就到此为止啦~ 想必大家都学会了吧。如果没学会也没关系,再仔细的品读上面的文章,相信这个春天你也能开出最绚烂的花~
补充
等等,明明文章开头说的是放兔子烟花,你倒是教教我们怎么放兔子烟花呢? 其实这个就比较简单与繁琐了。
说它繁琐呢 是因为兔子的轮廓需要我们自己想办法表示出来(这一块就留给大家自行解决了哈)
说它简单呢 是因为我们只需要将我们获得的数据 “硬编码” 到我们的程序中就可以了。
比如说这样,下面是我自己录的数据
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;
}
一只只可爱的小兔子就出现啦~!