阅后即焚的燃尽图实现

尼伯特

共 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. ) ;
d0bed46dbb7dc2a12a78f7d0800ff922.webp

燃烧边缘效果

参考代码: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].只要放大传入噪声函数的坐标,就可以达到想要的效果。

因为我的噪声函数实际上是在整数点随机,中间补间,所以区间范围越大,结果的随机性就越多。

所以,如果你希望这个转场效果是稀碎的那种,放大坐标多倍即可。b77313ea51402975837fd58f2296a87d.webp

我不知道如何在码上掘金中添加纹理,就直接做成白纸黑底了。可以自行调节noise函数的入参,观察变化。

如果,你想写出不一样的噪声效果,那么可以去修改噪声的插值方式或者基础的随机函数的参数。

cbd965bf649734347f850334751f24ee.webp
      
      <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 );

他的燃烧背景是依赖了一个纹理,可能那个纹理也是某种函数生成的,这里暂且以噪声代替纹理,效果不太好,将就着看一下。

16a25761095d62f2e0a8de4d9539f900.webp
      
      <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 ));
36a943b08cb66e617c04b8f90bab2d49.webp

直线过度

加上fbm ,fbm不理解的可以暂且理解为更丝滑的噪声。也就是说这里也可以用噪声。

      
       float fbm20 = fbm(st * 20.);
  b+= fbm20;
28ddbdba75796a2164de591c04040b97.webp直线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);
    // }
   

前面说了,这个效果的最大的特色是方向可控,下面的示例就是把直线改成圆圈。

343f4677a2e34b2f7dfd2353428b0fe8.webp
      
      <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>

结语

本文介绍了三种燃尽效果的实现方式。套路都是大同小异,把噪声的随机性加到专场效果中, 判断边缘区域,镶边。

这就是噪声的典型应用啊,地形也可以用噪声的实现,但是法线该如何计算呢?

推荐阅读  点击标题可跳转

1、前端加载超大图片(100M以上)实现秒开解决方案

2、2024 年 7 个 Web 前端开发趋势

3、二十张图片彻底讲明白 Webpack 设计理念,以看懂为目的

浏览 20
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报