閱后即焚的燃盡圖實(shí)現(xiàn)
作者:莫石
https://juejin.cn/post/7176087225245892669
我最開始是在一本書上掠過燃盡效果,當(dāng)時就是覺得很有意思。但是最近才真正動手去實(shí)踐它。我知道這個效果要用噪聲實(shí)現(xiàn),但是實(shí)際做的時候才發(fā)現(xiàn)不知道如何應(yīng)用。于是,去shadertoy上搜索了一番。選取了三個例子,有了一點(diǎn)心得。
一個燃盡效果,簡單一點(diǎn)可分兩部分,第一個就是轉(zhuǎn)場,從燃燒前的圖轉(zhuǎn)變到燃燒后的圖,也就是漸變,淡入淡出, 第二個就是火焰效果了,我們希望在邊緣處有火焰。
第一種實(shí)現(xiàn)方式
參考代碼
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函數(shù)。
先準(zhǔn)備兩張圖,一個要被燃燒的,一個是燃燒后露出來的。這一步所有的效果都一樣,前景和背景。
然后,來了一張高度圖,也可以說是灰度圖,就是坐標(biāo)對應(yīng)一個高度,從0到1。然后前景圖和背景圖的混合系數(shù) a = smoothstep(t-.2,t , height) ; height是不會變的,但是t會越來越大,直到t-.2 > height ,當(dāng)height > t的時候 a為0 ,也就是說這個值會從0 到1 漸變。看到這里我就明白,噪聲該怎么用上去了。我沒有高度圖,但是可以用噪聲來代替 .
然后就是在混合系數(shù)處于(0,1)的閉區(qū)間時添加燃燒的邊緣效果。
轉(zhuǎn)場過渡效果
上面已經(jīng)說了,就是讓兩張圖的混合系數(shù)隨時間變化。這里再啰嗦一下,把高度換成噪聲。先看過渡效果。當(dāng)時我就知道這種過渡應(yīng)該是用噪聲來實(shí)現(xiàn),用二維噪聲,這樣每個坐標(biāo)都對應(yīng)一個隨機(jī)的值,但是連續(xù)的坐標(biāo)對應(yīng)的值又是連續(xù)的,這就是噪聲的特性。
c是混合系數(shù),0的時候顯示前景圖,1的時候顯示背景圖。
我們希望的是每個坐標(biāo)產(chǎn)生的c都能經(jīng)歷從0到1,這里有一個通用的套路,那就是 smoothstep(t,t-.2, c)。
這個公式的意思是c的值不動,讓區(qū)間 [t,t-.2]動起來, 就像一個滑動窗口,c∈[0,1], t>0。因此,隨著t的增加,任意一個c肯定都是從區(qū)間[t,t-.2]的左邊,到區(qū)間內(nèi),到右邊。
在左邊就是0 ,右邊是1,結(jié)果就是從0到1了,這里區(qū)間長度是.2,也可以試試改變這個數(shù),這個區(qū)間的長度越長,過渡效果的中間區(qū)域就越大.
燃燒的總時間就是區(qū)間[t,t-.2] 超過c的最大值所需要的時間,假設(shè)其最大值為1 ,燃燒時間就是1.2,要操控燃燒速度可以直接操控時間。用噪聲實(shí)現(xiàn)轉(zhuǎn)場如下 ,噪聲函數(shù)用的是常規(guī)的雙線性插值。
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)
然后就是燃燒的邊緣效果,只有混合系數(shù)k處于[0,1]時才會有效果。你可以直接用if語句,也可以用兩個smoothstep相減,這樣全0和全1的部分就都是0 了,只有0 ,1之間的。
這一步?jīng)Q定了燃燒邊緣的寬度, 燃燒邊緣的總寬度就是前后兩限制區(qū)間的并集,其實(shí)兩個區(qū)間最好是左邊界一致(因?yàn)檫@里是正序),這樣結(jié)果就沒有負(fù)數(shù)。
float border = smoothstep(0.,.2 ,k ) - smoothstep(.1,1. ,k ) ;
在漸變轉(zhuǎn)場混合之后再加上邊緣,顏色是 (1.5,.5,0.),我看好幾個例子都是用這個顏色,有點(diǎn)不理解。
可以直接用mix,但是大多數(shù)例子都是用加法,把這個顏色加上去,也許是為了突顯火焰的明亮效果,越近(1,1,1)就越亮嘛。還有一個好處就是,用加法不會完全抹去之前的圖形,只是變了色,比如有文字的話還是能辨認(rèn)出來。
vec3 fire = vec3(1.5,.5,.0);
// 燃燒邊緣應(yīng)是 01 大于1的不要 小于零的 自然不會mix上去
color = mix(colorFront, colorback,k);
color += fire *border ;
// color = mix(color, fire ,border) ;
gl_FragColor = vec4(color,1. ) ;
到這里就完成了一個簡單的燃盡效果。
遇到的問題
遇到的就是下面的問題,我使用噪聲之后發(fā)現(xiàn),隨機(jī)性不夠分散,連成一大片了,如下圖所示。
我想要的是上面的那種。后來發(fā)現(xiàn),是噪聲函數(shù)的取值范圍太窄了, 一開始處理uv之后其區(qū)間是[-.5,.5].只要放大傳入噪聲函數(shù)的坐標(biāo),就可以達(dá)到想要的效果。
因?yàn)槲业脑肼暫瘮?shù)實(shí)際上是在整數(shù)點(diǎn)隨機(jī),中間補(bǔ)間,所以區(qū)間范圍越大,結(jié)果的隨機(jī)性就越多。
所以,如果你希望這個轉(zhuǎn)場效果是稀碎的那種,放大坐標(biāo)多倍即可。
我不知道如何在碼上掘金中添加紋理,就直接做成白紙黑底了。可以自行調(diào)節(jié)noise函數(shù)的入?yún)ⅲ^察變化。
如果,你想寫出不一樣的噪聲效果,那么可以去修改噪聲的插值方式或者基礎(chǔ)的隨機(jī)函數(shù)的參數(shù)。
<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。
不過,它真正的特色不在于這個背景,而是先噪聲漸變成黑色(燒黑), 然后再基于這個黑色,又加了一點(diǎn)隨機(jī)效果,漸變到背景(燒穿)。有了黑色和火焰背景之后,確實(shí)更有燃燒的感覺了。
關(guān)鍵代碼如下, paper是前景圖紋理色,n2是噪聲函數(shù)。 非png的圖alpha通道一般就是1。
vec4 c = mix(paper, vec4(0), smoothstep(t2+.1 ,t2-.1 ,n2(st * 400.) ));
// 燃燒邊緣 a < .1說明燒黑了,紋理取色默認(rèn)a應(yīng)該是1 這就進(jìn)一步增加了隨機(jī)性
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));
他所用的噪聲,在普通噪聲的基礎(chǔ)又做了一些處理,這種方式像是fbm。n是噪聲函數(shù)
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;
}
時間差
下面說一下,我領(lǐng)悟到東西,那就是時間差,我看到他的代碼注釋后,以為先燒黑再燒穿,是用時間偏差做出來的。于是就有了下面的代碼 。
float k = smoothstep(t+.2,t ,n2(st*200. ) );// 前景圖和黑色混合系數(shù)
float k2 = smoothstep(t+.1,t-.1 ,n2(st*200. ) );// 上面是變黑 這里是燒穿
color = mix(paper.rgb,vec3(0. ) ,k );
color = mix(color.rgb,bg ,k2 );
他的燃燒背景是依賴了一個紋理,可能那個紋理也是某種函數(shù)生成的,這里暫且以噪聲代替紋理,效果不太好,將就著看一下。
<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>
第三種
這一種的特點(diǎn)是方向可控,原實(shí)例是一條直線,也可以改成圓等幾何圖形。并且他還使用了bfm(布朗分形運(yùn)動),疊加了噪聲的過程中,降低振幅提升頻率。
直線轉(zhuǎn)場
我們先來實(shí)現(xiàn)最簡單的直線轉(zhuǎn)場,下面就是寫了一個直線方程,隨著t的增大,這條直線會按垂直自身方向往上移動。現(xiàn)在就暫定,直線的左邊為前景圖,右邊為背景圖。由于前面處理后的坐標(biāo)范圍是[-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過度
補(bǔ)上變黑和邊緣
嘗試了一下直接偏移邊界,而不是時間,也是可以的。當(dāng)然,用if語句是更好理解的。
color = mix(color , vec3(0), smoothstep(.0, .1,b ));//變黑
// 直接偏移右邊界, 偏移有邊界的話,需要先燒穿再變黑 不然就是現(xiàn)在這樣 b>.1就黑了,但是b要大于.35才燒穿,但是現(xiàn)在是減去截距,所以現(xiàn)在是對的。
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>
結(jié)語
本文介紹了三種燃盡效果的實(shí)現(xiàn)方式。套路都是大同小異,把噪聲的隨機(jī)性加到專場效果中, 判斷邊緣區(qū)域,鑲邊。
這就是噪聲的典型應(yīng)用啊,地形也可以用噪聲的實(shí)現(xiàn),但是法線該如何計(jì)算呢?
推薦閱讀 點(diǎn)擊標(biāo)題可跳轉(zhuǎn)
1、前端加載超大圖片(100M以上)實(shí)現(xiàn)秒開解決方案
3、二十張圖片徹底講明白 Webpack 設(shè)計(jì)理念,以看懂為目的
