webgl實現火焰效果

騰訊DeepOcean發表於2019-12-03

此篇文章我們要實現一個燃燒的火焰效果,其中會包含產生燃燒效果的相關演算法,如果不是很理解的話,可以自己動手調節相關引數來進一步理解,先看一下實現的效果。

webgl實現火焰效果

第一步:構建基礎

為了實現上面的效果我們先來構建出canvas的DOM,以及相關的shader程式碼。

這裡的步驟和這篇文章中第一步是相同的,你可以移步這裡檢視,或者在最後我也會附上全部的程式碼(不要忘記引入相關的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>

複製程式碼

相關文章