阅后即焚的燃尽图实现
共 10964字,需浏览 22分钟
·
2024-04-10 18:08
作者:莫石
https://juejin.cn/post/7176087225245892669
我最开始是在一本书上掠过燃尽效果,当时就是觉得很有意思。但是最近才真正动手去实践它。我知道这个效果要用噪声实现,但是实际做的时候才发现不知道如何应用。于是,去shadertoy上搜索了一番。选取了三个例子,有了一点心得。
一个燃尽效果,简单一点可分两部分,第一个就是转场,从燃烧前的图转变到燃烧后的图,也就是渐变,淡入淡出, 第二个就是火焰效果了,我们希望在边缘处有火焰。
第一种实现方式
参考代码
void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
vec2 uv = fragCoord/iResolution.xy;
vec3 col = vec3(0.);
vec3 heightmap = texture(iChannel0, uv).rrr;
vec3 background = texture(iChannel1, uv).rgb;
vec3 foreground = texture(iChannel2, uv).rgb;
float t = fract(-iTime*.2);
vec3 erosion = smoothstep(t-.2, t, heightmap);
vec3 border = smoothstep(0., .1, erosion) - smoothstep(.1, 1., erosion);
col = (1.-erosion)*foreground + erosion*background;
vec3 leadcol = vec3(1., .5, .1);
vec3 trailcol = vec3(0.2, .4, 1.);
vec3 fire = mix(leadcol, trailcol, smoothstep(0.8, 1., border))*2.;
col += border*fire;
fragColor = vec4(col,1.0);
}
这是最简单的,总共不到20行代码,没有用到什么公式,就一个mix函数。
先准备两张图,一个要被燃烧的,一个是燃烧后露出来的。这一步所有的效果都一样,前景和背景。
然后,来了一张高度图,也可以说是灰度图,就是坐标对应一个高度,从0到1。然后前景图和背景图的混合系数 a = smoothstep(t-.2,t , height) ; height是不会变的,但是t会越来越大,直到t-.2 > height ,当height > t的时候 a为0 ,也就是说这个值会从0 到1 渐变。看到这里我就明白,噪声该怎么用上去了。我没有高度图,但是可以用噪声来代替 .
然后就是在混合系数处于(0,1)的闭区间时添加燃烧的边缘效果。
转场过渡效果
上面已经说了,就是让两张图的混合系数随时间变化。这里再啰嗦一下,把高度换成噪声。先看过渡效果。当时我就知道这种过渡应该是用噪声来实现,用二维噪声,这样每个坐标都对应一个随机的值,但是连续的坐标对应的值又是连续的,这就是噪声的特性。
c是混合系数,0的时候显示前景图,1的时候显示背景图。
我们希望的是每个坐标产生的c都能经历从0到1,这里有一个通用的套路,那就是 smoothstep(t,t-.2, c)
。
这个公式的意思是c的值不动,让区间 [t,t-.2]动起来, 就像一个滑动窗口,c∈[0,1], t>0。因此,随着t的增加,任意一个c肯定都是从区间[t,t-.2]的左边,到区间内,到右边。
在左边就是0 ,右边是1,结果就是从0到1了,这里区间长度是.2,也可以试试改变这个数,这个区间的长度越长,过渡效果的中间区域就越大.
燃烧的总时间就是区间[t,t-.2] 超过c的最大值所需要的时间,假设其最大值为1 ,燃烧时间就是1.2,要操控燃烧速度可以直接操控时间。用噪声实现转场如下 ,噪声函数用的是常规的双线性插值。
mian(){
....
float height = noise2d(st*10.) ;
t = mod(t*.2,3. );
float k = 1.-smoothstep(t-.3,t ,height ) ;// 这个范围决定了
color = mix(color, colorHls, m_dist);
vec3 colorFront = texture(iChannel0, fract(st)).rgb;
vec3 colorback = texture(iChannel1, fract(st)).rgb;
color = mix(colorFront, colorback,k);
gl_FragColor = vec4(color,1. ) ;
燃烧边缘效果
参考代码:https://www.shadertoy.com/view/tlfSRS)
然后就是燃烧的边缘效果,只有混合系数k处于[0,1]时才会有效果。你可以直接用if语句,也可以用两个smoothstep相减,这样全0和全1的部分就都是0 了,只有0 ,1之间的。
这一步决定了燃烧边缘的宽度, 燃烧边缘的总宽度就是前后两限制区间的并集,其实两个区间最好是左边界一致(因为这里是正序),这样结果就没有负数。
float border = smoothstep(0.,.2 ,k ) - smoothstep(.1,1. ,k ) ;
在渐变转场混合之后再加上边缘,颜色是 (1.5,.5,0.),我看好几个例子都是用这个颜色,有点不理解。
可以直接用mix,但是大多数例子都是用加法,把这个颜色加上去,也许是为了突显火焰的明亮效果,越近(1,1,1)就越亮嘛。还有一个好处就是,用加法不会完全抹去之前的图形,只是变了色,比如有文字的话还是能辨认出来。
vec3 fire = vec3(1.5,.5,.0);
// 燃烧边缘应是 01 大于1的不要 小于零的 自然不会mix上去
color = mix(colorFront, colorback,k);
color += fire *border ;
// color = mix(color, fire ,border) ;
gl_FragColor = vec4(color,1. ) ;
到这里就完成了一个简单的燃尽效果。
遇到的问题
遇到的就是下面的问题,我使用噪声之后发现,随机性不够分散,连成一大片了,如下图所示。
我想要的是上面的那种。后来发现,是噪声函数的取值范围太窄了, 一开始处理uv之后其区间是[-.5,.5].只要放大传入噪声函数的坐标,就可以达到想要的效果。
因为我的噪声函数实际上是在整数点随机,中间补间,所以区间范围越大,结果的随机性就越多。
所以,如果你希望这个转场效果是稀碎的那种,放大坐标多倍即可。
我不知道如何在码上掘金中添加纹理,就直接做成白纸黑底了。可以自行调节noise函数的入参,观察变化。
如果,你想写出不一样的噪声效果,那么可以去修改噪声的插值方式或者基础的随机函数的参数。
<canvas width="700" height="700"></canvas>
<script>
(async function() {
const canvas = document.querySelector('canvas');
const renderer = new Doodle(canvas, {webgl2: true});
const fragment = await JCode.getCustomCode();
const program = renderer.compileSync(fragment);
renderer.useProgram(program);
renderer.render();
}());
</script>
第二种
参考代码:https://www.shadertoy.com/view/tlfSRS
基本思路和第一种是一样的。前面一张要烧掉的图,烧掉之后露出来的是一个燃烧效果背景,渐变效果用的是一个noise。
不过,它真正的特色不在于这个背景,而是先噪声渐变成黑色(烧黑), 然后再基于这个黑色,又加了一点随机效果,渐变到背景(烧穿)。有了黑色和火焰背景之后,确实更有燃烧的感觉了。
关键代码如下, paper是前景图纹理色,n2是噪声函数。 非png的图alpha通道一般就是1。
vec4 c = mix(paper, vec4(0), smoothstep(t2+.1 ,t2-.1 ,n2(st * 400.) ));
// 燃烧边缘 a < .1说明烧黑了,纹理取色默认a应该是1 这就进一步增加了随机性
c.rgb = clamp( c.rgb + step(c.a, .1)* 1.6 *n2 (1000.*st )* vec3(1.2,.5,.0),.0 ,1. );
// 烧穿了见背景
c.rgb = mix( c.rgb , bg , step(c.a,.01));
他所用的噪声,在普通噪声的基础又做了一些处理,这种方式像是fbm。n是噪声函数
float noise(in vec2 p)
{
return n(p/32.) * 0.58 +
n(p/16.) * 0.2 +
n(p/8.) * 0.1 +
n(p/4.) * 0.05 +
n(p/2.) * 0.02 +
n(p) * 0.0125;
}
时间差
下面说一下,我领悟到东西,那就是时间差,我看到他的代码注释后,以为先烧黑再烧穿,是用时间偏差做出来的。于是就有了下面的代码 。
float k = smoothstep(t+.2,t ,n2(st*200. ) );// 前景图和黑色混合系数
float k2 = smoothstep(t+.1,t-.1 ,n2(st*200. ) );// 上面是变黑 这里是烧穿
color = mix(paper.rgb,vec3(0. ) ,k );
color = mix(color.rgb,bg ,k2 );
他的燃烧背景是依赖了一个纹理,可能那个纹理也是某种函数生成的,这里暂且以噪声代替纹理,效果不太好,将就着看一下。
<canvas width="1000" height="700"></canvas>
<script>
(async function() {
const canvas = document.querySelector('canvas');
const renderer = new Doodle(canvas, {webgl2: true});
const fragment = await JCode.getCustomCode();
const program = renderer.compileSync(fragment);
renderer.useProgram(program);
renderer.render();
}());
</script>
第三种
这一种的特点是方向可控,原实例是一条直线,也可以改成圆等几何图形。并且他还使用了bfm(布朗分形运动),叠加了噪声的过程中,降低振幅提升频率。
直线转场
我们先来实现最简单的直线转场,下面就是写了一个直线方程,随着t的增大,这条直线会按垂直自身方向往上移动。现在就暂定,直线的左边为前景图,右边为背景图。由于前面处理后的坐标范围是[-1., 1.],如果想从左下角开始,需要加上大概1的偏移,用t减截距。
float b = st.x + st.y -2.;
b= t -b;
color = mix(colorFront , c2, smoothstep(.0, .1,b ));
直线过度
加上fbm ,fbm不理解的可以暂且理解为更丝滑的噪声。也就是说这里也可以用噪声。
float fbm20 = fbm(st * 20.);
b+= fbm20;
直线fbm过度
补上变黑和边缘
尝试了一下直接偏移边界,而不是时间,也是可以的。当然,用if语句是更好理解的。
color = mix(color , vec3(0), smoothstep(.0, .1,b ));//变黑
// 直接偏移右边界, 偏移有边界的话,需要先烧穿再变黑 不然就是现在这样 b>.1就黑了,但是b要大于.35才烧穿,但是现在是减去截距,所以现在是对的。
color = mix(color , c2, smoothstep(.1, .35,b ));// 烧穿
vec3 borderCol =(b-.1)* 30. * ( n3(st* 100. + vec2(t) )) * vec3(1.2,.5,0);
color += borderCol * (smoothstep(.2,.3 ,b ) - smoothstep(.29,.3 ,b ));
// if(b> .35){
// color = mix(color, c2, b);
// }
//
// if(b >.1 && b < .3){
// color+=(b-.1)* 30. * ( n3(st* 100. + vec2(t) )) * vec3(1.5,.5,0);
// }
前面说了,这个效果的最大的特色是方向可控,下面的示例就是把直线改成圆圈。
<canvas width="1000" height="500"></canvas>
<script>
(async function() {
const canvas = document.querySelector('canvas');
const renderer = new Doodle(canvas, {webgl2: true});
const fragment = await JCode.getCustomCode();
const program = renderer.compileSync(fragment);
renderer.useProgram(program);
renderer.render();
}());
</script>
结语
本文介绍了三种燃尽效果的实现方式。套路都是大同小异,把噪声的随机性加到专场效果中, 判断边缘区域,镶边。
这就是噪声的典型应用啊,地形也可以用噪声的实现,但是法线该如何计算呢?
推荐阅读 点击标题可跳转
3、二十张图片彻底讲明白 Webpack 设计理念,以看懂为目的