前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >阅后即焚的燃尽图实现

阅后即焚的燃尽图实现

作者头像
winty
发布2024-01-03 15:12:50
2050
发布2024-01-03 15:12:50
举报
文章被收录于专栏:前端Q前端Q

前言

我最开始是在一本书上掠过燃尽效果,当时就是觉得很有意思。但是最近才真正动手去实践它。我知道这个效果要用噪声实现,但是实际做的时候才发现不知道如何应用。于是,去shadertoy上搜索了一番。选取了三个例子,有了一点心得。

一个燃尽效果,简单一点可分两部分,第一个就是转场,从燃烧前的图转变到燃烧后的图,也就是渐变,淡入淡出, 第二个就是火焰效果了,我们希望在边缘处有火焰。

下面将分别介绍三种实现方式,具体如下。

第一种

参考代码:https://www.shadertoy.com/view/tlfSRS

代码语言:javascript
复制
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);
}
代码语言:javascript
复制
html, body {
  width: 100%;
  height: 100%;
  background: #ddd;
}
canvas {
  width: 90%;
  aspect-ratio: 2;
}

这是最简单的,总共不到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,要操控燃烧速度可以直接操控时间。用噪声实现转场如下 ,噪声函数用的是常规的双线性插值。

代码语言:javascript
复制
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之间的。

这一步决定了燃烧边缘的宽度, 燃烧边缘的总宽度就是前后两限制区间的并集,其实两个区间最好是左边界一致(因为这里是正序),这样结果就没有负数。

代码语言:javascript
复制
  float border = smoothstep(0.,.2 ,k ) - smoothstep(.1,1. ,k ) ;

在渐变转场混合之后再加上边缘,颜色是 (1.5,.5,0.),我看好几个例子都是用这个颜色,有点不理解。

可以直接用mix,但是大多数例子都是用加法,把这个颜色加上去,也许是为了突显火焰的明亮效果,越近(1,1,1)就越亮嘛。还有一个好处就是,用加法不会完全抹去之前的图形,只是变了色,比如有文字的话还是能辨认出来。

代码语言:javascript
复制
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函数的入参,观察变化。

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

代码语言:javascript
复制
<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>

<style>
html, body {
  width: 100%;
  height: 100%;
  background: #ddd;
}
canvas {
  width: 350px;
  height: 350px;
}
</style>
代码语言:javascript
复制
#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;


uniform vec2 dd_resolution;
uniform float dd_time;
out vec4 fragColor;

float random2d(vec2 coord){
    return fract(sin(dot(coord.xy, vec2(12.9898, 78.233))) * 43758.5453);
}

float  noise2d(vec2 p){ 
    vec2 f = fract(p) ;
    vec2 i = floor(p);
     float x1  = mix(random2d(vec2(i)), random2d(vec2(i.x+1. ,i.y)), f.x);// 只在整数部分随机 小数部分补间,f已经是小数
    float x2  = mix(random2d(vec2(vec2(i.x,i.y+1.))), random2d(vec2(i.x+1. ,i.y+1.)), f.x);// 只在整数部分随机 小数部分补间,f已经是小数
    float d = mix(x1,x2,f.y) ; // 这就是双线性插值吗 ,二维有四个点,先在x方向上插值,得到两个值,这两个值再在y上插值
   
    return d ;

}

void main() {
    vec2 st =  gl_FragCoord.xy/dd_resolution.xx;
    float height = noise2d(st*10.) ;
    // float height  = texture(iChannel3, st*.1).r ;
    float t = mod(dd_time*.2,3. );

    float k = 1.-smoothstep(t-.3,t ,height ) ;// 这个范围决定了
    // border 的宽度不能太窄 ,否则没有火焰的那种感觉
    float border = smoothstep(0.1,.2 ,k ) - smoothstep(.1,1. ,k ) ;

    
    vec3 colorFront = vec3(1);
    vec3 colorback = vec3(0);

    vec3 fire = vec3(1.5,.5,0.);
    // 燃烧边缘应是 01 大于1的不要 小于零的 自然不会mix上去

    vec3  color = mix(colorFront, colorback,k);
        
    color +=  fire *border ;
    // color =  mix(color, fire ,border) ;
    fragColor = vec4(color,1. ) ;
}

第二种

参考代码:https://www.shadertoy.com/view/tlfSRS

基本思路和第一种是一样的。前面一张要烧掉的图,烧掉之后露出来的是一个燃烧效果背景,渐变效果用的是一个noise。

不过,它真正的特色不在于这个背景,而是先噪声渐变成黑色(烧黑), 然后再基于这个黑色,又加了一点随机效果,渐变到背景(烧穿)。有了黑色和火焰背景之后,确实更有燃烧的感觉了。

关键代码如下, paper是前景图纹理色,n2是噪声函数。非png的图alpha通道一般就是1。

代码语言:javascript
复制
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是噪声函数

代码语言:javascript
复制
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;
}

时间差

下面说一下,我领悟到东西,那就是时间差,我看到他的代码注释后,以为先烧黑再烧穿,是用时间偏差做出来的。于是就有了下面的代码 。

代码语言:javascript
复制
    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 );

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

代码语言:javascript
复制
<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>

<style>
html, body {
  width: 100%;
  height: 100%;
  background: #ddd;
}
canvas {
  width: 90%;
  aspect-ratio: 2;
}
</style>
代码语言:javascript
复制
#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;


uniform vec2 dd_resolution;
uniform float dd_time;
out vec4 fragColor;

float random2d(vec2 coord){
    return fract(sin(dot(coord.xy, vec2(12.9898, 78.233))) * 43758.5453);
}

float  noise2d(vec2 p){ 
    vec2 f = fract(p) ;
    vec2 i = floor(p);
    float xk =f.x*f.x*.8  + f.x * .3; 
     float x1  = mix(random2d(vec2(i)), random2d(vec2(i.x+1. ,i.y)), xk);// 只在整数部分随机 小数部分补间,f已经是小数
    float x2  = mix(random2d(vec2(vec2(i.x,i.y+1.))), random2d(vec2(i.x+1. ,i.y+1.)), xk);// 只在整数部分随机 小数部分补间,f已经是小数
    float d = mix(x1,x2,f.y) ; // 这就是双线性插值吗 ,二维有四个点,先在x方向上插值,得到两个值,这两个值再在y上插值
   
    return d ;

}
float n2(in vec2 p)
{
    return noise2d(p/32.) * 0.58 +
           noise2d(p/16.) * 0.2  +
           noise2d(p/8.)  * 0.1  +
           noise2d(p/4.)  * 0.05 +
           noise2d(p/2.)  * 0.02 +
           noise2d(p)     * 0.0125;
}

vec3 fireBg(vec2 p){ 
    vec2 offset = vec2(0., dd_time*2.) ;
    vec3 color = vec3(1.);
// 它这个燃烧背景要依赖纹理 不同的纹理效果不一样
    for( int i =0; i< 3; i++){
         color += mix(n2(  p -.25 * offset + .5), 
                      noise2d( p-offset), 
                      abs(mod(float(i) * .666, 2.) -1.) ) * color * color ;

    }

    return color * vec3(.0666, .0266, .00333);
}

void main() {
    vec2 st =  gl_FragCoord.xy/dd_resolution.xx;
    float height = noise2d(st*10.) ;
    // float height  = texture(iChannel3, st*.1).r ;
    float t = mod(dd_time*.2,3. );


   float k = smoothstep(t+.3,t ,n2(st*200. ) );// 这噪声函数只要放大了效果就有点是我想要的了
    float k2 = smoothstep(t+.15,t-.15 ,n2(st*200. ) );// 上面是变黑 这里是烧穿

    // border 的宽度不能太窄 ,否则没有火焰的那种感觉
    float border = smoothstep(0.1,.1 ,k2 ) - smoothstep(.1,1. ,k2 ) ;

    
    vec3 colorFront = vec3(.7,.5,.1)* (.7 + .3* noise2d(st*100.));
    vec3 colorback = fireBg(st * 100.);

    vec3 fire = vec3(1.5,.5,0.);
    // 燃烧边缘应是 01 大于1的不要 小于零的 自然不会mix上去

    vec3 color = colorFront ;  
    color = mix(color, vec3(0.),k);
      color = mix(color, colorback,k2);
        
    color +=  fire *border ;
    // color =  mix(color, fire ,border) ;
    fragColor = vec4(color,1. ) ;
}

第三种

这一种的特点是方向可控,原实例是一条直线,也可以改成圆等几何图形。并且他还使用了bfm(布朗分形运动),叠加了噪声的过程中,降低振幅提升频率。

直线转场

我们先来实现最简单的直线转场,下面就是写了一个直线方程,随着t的增大,这条直线会按垂直自身方向往上移动。现在就暂定,直线的左边为前景图,右边为背景图。由于前面处理后的坐标范围是[-1., 1.],如果想从左下角开始,需要加上大概1的偏移,用t减截距。

代码语言:javascript
复制
    float b = st.x + st.y  -2.;
    b= t -b; 
    color = mix(colorFront , c2, smoothstep(.0, .1,b ));

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

代码语言:javascript
复制
 float fbm20 = fbm(st * 20.);
  b+= fbm20;

补上变黑和边缘

尝试了一下直接偏移边界,而不是时间,也是可以的。当然,用if语句是更好理解的。

代码语言:javascript
复制
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);
    // }

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

代码语言:javascript
复制
<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>

<style>
html, body {
  width: 100%;
  height: 100%;
  background: #ddd;
}
canvas {
  width: 90%;
  aspect-ratio: 2;
}
</style>
代码语言:javascript
复制
#!/jcode/lang/glsl https://xitu.github.io/jcode-languages/dist/lang-glsl.json

#version 300 es
precision highp float;


uniform vec2 dd_resolution;
uniform float dd_time;
out vec4 fragColor;

float random2d(vec2 coord){
    return fract(sin(dot(coord.xy, vec2(12.9898, 78.233))) * 43758.5453);
}

float  noise2d(vec2 p){ 
    vec2 f = fract(p) ;
    vec2 i = floor(p);
    float xk =f.x*f.x*.8  + f.x * .3; 
     float x1  = mix(random2d(vec2(i)), random2d(vec2(i.x+1. ,i.y)), xk);// 只在整数部分随机 小数部分补间,f已经是小数
    float x2  = mix(random2d(vec2(vec2(i.x,i.y+1.))), random2d(vec2(i.x+1. ,i.y+1.)), xk);// 只在整数部分随机 小数部分补间,f已经是小数
    float d = mix(x1,x2,f.y) ; // 这就是双线性插值吗 ,二维有四个点,先在x方向上插值,得到两个值,这两个值再在y上插值
   
    return d ;

}
float n2(in vec2 p)
{
    return noise2d(p/32.) * 0.58 +
           noise2d(p/16.) * 0.2  +
           noise2d(p/8.)  * 0.1  +
           noise2d(p/4.)  * 0.05 +
           noise2d(p/2.)  * 0.02 +
           noise2d(p)     * 0.0125;
}
float fbm (vec2 p2){ 
  float d = n2(p2*10.);
  d+=.5*n2(p2 * 20.);
  d+=.25*n2(p2 * 40.);
  d+=.125*n2(p2 * 80.);
  return d;
}
vec3 fireBg(vec2 p){ 
    vec2 offset = vec2(0., dd_time*2.) ;
    vec3 color = vec3(1.);
// 它这个燃烧背景要依赖纹理 不同的纹理效果不一样
    for( int i =0; i< 3; i++){
         color += mix(n2(  p -.25 * offset + .5), 
                      fbm( p-offset), 
                      abs(mod(float(i) * .666, 2.) -1.) ) * color * color ;

    }

    return color * vec3(.0666, .0266, .00333);
}

void main() {
    vec2 st =  gl_FragCoord.xy/dd_resolution.yy;
    // float height  = texture(iChannel3, st*.1).r ;
    float t = mod(dd_time*.5,4. );
    st-=.5;
    st*=5.;
    vec3 colorFront = vec3(.7,.5,.1)* (.7 + .3* fbm(st*100.));

    vec3 color = colorFront ;  

    float fbm20 = fbm(st * 20.);
    vec3 c2 = vec3(0.8902, 0.3373, 0.0627) *(.5 + .5* fbm20) ;

    // 从demo角度学习,我直接扭曲这条线,好像就可以了,就是效果比较一般
    float d2 = st.x + st.y  +2. ;
    float b =  (st.y+ 0.)*(st.y +0.) +  st.x * st.x  + fbm20*.3 +.5;

    b = t - b ;
    b+= fbm20;
// 
    color = mix(color , vec3(0), smoothstep(.0, .1,b ));//变黑

    
    color = mix(color , c2, smoothstep(.1, .35,b ));// 烧穿

    vec3 borderCol =(b-.1)* 30. * ( n2(st* 100. + vec2(t) )) * vec3(1.2,.5,0);

    color += borderCol * (smoothstep(.2,.3 ,b ) - smoothstep(.29,.3 ,b )); 
    
  
    fragColor = vec4(color,1. ) ;
}

结语

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

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

作者:莫石 链接:https://juejin.cn/post/7176087225245892669

点个在看支持我吧

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端Q 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 第一种
    • 转场过渡效果
      • 燃烧边缘效果
        • 遇到的问题
        • 第二种
          • 时间差
          • 第三种
            • 直线转场
              • 补上变黑和边缘
          • 结语
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档