webgl实现火焰效果

此篇文章我们要实现一个燃烧的火焰效果,其中会包含产生燃烧效果的相关算法,如果不是很理解的话,可以自己动手调节相关参数来进一步理解,先看一下实现的效果。

 

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);函数图片,可供参考。

 

webgl实现火焰效果

如果还是不理解的话可以将函数中的每一步做出图像来理解。

通过上面我们就得到了一个噪音的算法,接下来就是将噪音算法应用起来,并且使用算法勾勒出火焰了。

第三步:修改片元着色器

在第一步中我们初始化了一个简单的顶点着色器和片元着色器,下面我们就要对片元着色器进行修改了。

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>

发表评论