webgl實現火焰效果
此篇文章我們要實現一個燃燒的火焰效果,其中會包含產生燃燒效果的相關算法,如果不是很理解的話,可以自己動手調節相關參數來進一步理解,先看一下實現的效果。
第一步: 構建基礎
爲了實現上面的效果我們先來構建出canvas的DOM,以及相關的shader代碼。
這裏的步驟和這篇文章中第一步是相同的,你可以移步[https://juejin.im/post/5dcba9aaf265da4d556d0164]這裏查看,或者在最後我也會附上全部的代碼(不要忘記引入相關的js文件)。
需要注意的是爲了讓火焰的位置在中間我這邊canvas的寬度設置爲了375,高度設置爲了667,這兩個值與後面片元着色器中的參數有關,如果你實現效果時發現火焰位置不對可以適當調整。
第二步: 一個合適的噪音算法
要實現火焰的外焰燃燒效果,我們需要使用一個噪音算法,webgl有梯度噪音,細胞噪音等等的算法,但是模擬出來的外焰燃燒效果個人感覺不是很生動,在此我這邊使用了另一種噪音算法:
float noise(vec3 p){
vec3 i = floor(p);
vec4 a = dot(i, vec3(1., 57., 21.)) + vec4(0., 57., 21., 78.);
vec3 f = cos((p-i)*acos(-1.))*(-.5)+.5;
a = mix(sin(cos(a)*a),sin(cos(1.+a)*(1.+a)), f.x);
a.xy = mix(a.xz, a.yw, f.y);
return mix(a.x, a.y, f.z);
}
在算法中我們首先使用floor函數返回p的最小整數部分,然後使用dot函數對i和另一個vec3數值取正。
然後定義一個使用cos和acos函數計算出來的f,再對a做混合的sin與cos函數操作,最後進行mix的線性混合。
爲了便於理解這裏有一個a = mix(sin(cos(a) a),sin(cos(1.+a) (1.+a)), f.x);函數圖片,可供參考。
如果還是不理解的話可以將函數中的每一步做出圖像來理解。
通過上面我們就得到了一個噪音的算法,接下來就是將噪音算法應用起來,並且使用算法勾勒出火焰了。
第三步: 修改片元着色器
在第一步中我們初始化了一個簡單的頂點着色器和片元着色器,下面我們就要對片元着色器進行修改了。
void main() {
vec2 v = -1.5 + 3. * v_TexCoord;
vec3 org = vec3(0., -2., 4.);
vec3 dir = normalize(vec3(v.x*1.6, -v.y, -1.5));
vec4 p = raymarch(org, dir);
float glow = p.w;
vec4 col = mix(vec4(1.,.5,.1,1.), vec4(0.1,.5,1.,1.), p.y*.02+.4);
gl_FragColor = mix(vec4(0.), col, pow(glow*2.,4.));
}
v_TexCoord是我們傳入的紋理座標值,對其進行了乘與加處理,還記得第一步中說過的火焰位置嗎?這兩個參數就是用來調節的。
後面我們定義了一個vec3的變量,同時定義了一個歸一化後的變量dir。
接着我們定義了一個變量p,對其使用了自定義函數raymarch進行處理,後面介紹這個函數,最後就是對位置的一個線性混合了,最後是賦值。
接下來就是raymarch函數以及其使用到的自定義函數了。
float sphere(vec3 p, vec4 spr){
return length(spr.xyz-p) - spr.w;
}
float flame(vec3 p){
float d = sphere(p*vec3(1.,.5,1.), vec4(.0,-1.,.0,1.));
return d + (noise(p+vec3(.0,time*2.,.0)) + noise(p*3.)*.5)*.25*(p.y) ;
}
float scene(vec3 p){
return min(100.-length(p) , abs(flame(p)) );
}
vec4 raymarch(vec3 org, vec3 dir){
float d = 0.0, glow = 0.0, eps = 0.02;
vec3 p = org;
bool glowed = false;
for(int i=0; i<64; i++)
{
d = scene(p) + eps;
p += d * dir;
if( d>eps )
{
if(flame(p) < .0)
glowed=true;
if(glowed)
glow = float(i)/64.;
}
}
return vec4(p,glow);
}
raymarch函數的主要作用就是配合noise勾勒出火焰整體的外觀,包括大小,顏色值,以及效果的位置等等。
勾勒出火焰的核心就是讓片元着色器指定的位置渲染出指定的顏色,然後將這些片元線性連接起來就是火焰了,例如外焰要渲染成線性的黃色,內焰渲染成藍色。
在上面的raymarch函數中我們在for循環中使用了64,並且在後面除了64,如果你感興趣的話,可以去調節這兩個數值,你會發現火焰的明亮發生了變化。
上面的代碼中我們使用flame函數定義了火焰燃燒的速度以及外焰燃燒的高度等信息,使用scene函數定義了整體的效果,可以試着將其中的100修改爲10,你會發現火焰的顏色完全變了。
全部的代碼
上面我們一步步的實現了全部的效果,需要注意的是我這邊爲了火焰的效果將canvas背景設置爲了黑色,而不是片元着色器中導致的,下面就是所有的代碼,你可以放在本地去試一下,同時理解一下算法有趣的內容。
<template> <div> <canvas id="glcanvas" ref="webgl" width="375" height="667"></canvas> </div> </template> <script> /* eslint-disable */ import testImg from './static/img/img1.jpeg' export default { props: { msg: String }, mounted() { let VSHADER_SOURCE = ` attribute vec4 a_Position; attribute vec2 a_TexCoord; varying vec2 v_TexCoord; void main() { gl_Position = a_Position; v_TexCoord = a_TexCoord; }` let FSHADER_SOURCE = ` precision mediump float; uniform sampler2D u_Sampler; uniform float time; varying vec2 v_TexCoord; float noise(vec3 p){ vec3 i = floor(p); vec4 a = dot(i, vec3(1., 57., 21.)) + vec4(0., 57., 21., 78.); vec3 f = cos((p-i)*acos(-1.))*(-.5)+.5; a = mix(sin(cos(a)*a),sin(cos(1.+a)*(1.+a)), f.x); a.xy = mix(a.xz, a.yw, f.y); return mix(a.x, a.y, f.z); } float sphere(vec3 p, vec4 spr){ return length(spr.xyz-p) - spr.w; } float flame(vec3 p){ float d = sphere(p*vec3(1.,.5,1.), vec4(.0,-1.,.0,1.)); return d + (noise(p+vec3(.0,time*2.,.0)) + noise(p*3.)*.5)*.25*(p.y) ; } float scene(vec3 p){ return min(100.-length(p) , abs(flame(p)) ); } vec4 raymarch(vec3 org, vec3 dir){ float d = 0.0, glow = 0.0, eps = 0.02; vec3 p = org; bool glowed = false; for(int i=0; i<64; i++){ d = scene(p) + eps; p += d * dir; if( d>eps ){ if(flame(p) < .0) glowed=true; if(glowed) glow = float(i)/64.; } } return vec4(p,glow); } void main() { vec2 v = -1.5 + 3. * v_TexCoord; vec3 org = vec3(0., -2., 4.); vec3 dir = normalize(vec3(v.x*1.6, -v.y, -1.5)); vec4 p = raymarch(org, dir); float glow = p.w; vec4 col = mix(vec4(1.,.5,.1,1.), vec4(0.1,.5,1.,1.), p.y*.02+.4); gl_FragColor = mix(vec4(0.), col, pow(glow*2.,4.)); }` let canvas = this.$refs.webgl let gl = getWebGLContext(canvas); initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE); let n = this.initVertexBuffers(gl); this.inirTextures(gl, n); let u_time = gl.getUniformLocation(gl.program, "time"); let newTime = 0.1; let draw = function(){ newTime = newTime + 0.05; gl.uniform1f(u_time, newTime); gl.clearColor(0.0, 0.0, 0.0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); requestAnimationFrame(draw); } draw() }, methods: { initVertexBuffers(gl){ var verticesTexCoords = new Float32Array([ -1.0, 1.0, 0.0, 1.0, -1.0, -1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 0.0, ]); var n = 4; var vertexCoordBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, vertexCoordBuffer); gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW); var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT; var a_Position = gl.getAttribLocation(gl.program, 'a_Position'); gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0); gl.enableVertexAttribArray(a_Position); var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord'); gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2); gl.enableVertexAttribArray(a_TexCoord) return n; }, inirTextures(gl, n){ var texture = gl.createTexture(); var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler'); var image = new Image(); image.onload = ()=>{this.loadTexture(gl, n, texture, u_Sampler, image);}; image.crossOrigin = "anonymous"; image.src = testImg return true; }, loadTexture(gl, n, texture, u_Sampler, image){ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); gl.uniform1i(u_Sampler, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); } } } </script> <style lang="scss"> #glcanvas{ background-color: #000; } </style>