WebGL 手擼3d賀卡+小草飄動濾鏡

孔小建發表於2019-04-26

前言

這兩天接到一個專案,是有關全屏視訊的,整個專案中分到我這兒最主要的部分就是結束頁要求3d賀卡展示,正巧和前幾天NingBo童鞋分享的一樣,乾脆點兒,這次搞個webGL版的。哈哈~

實現過程

demo地址

20190426-現在階段就是隻做了個基礎版,曲線動畫啥的都是小事兒。

20190427-現在加上了easebackout曲線方法,用的是d3-ease感覺挺好用的,還有小花的飄動的邏輯,稍後會講解。。太餓了~吃飯去。哈哈(已更新,純文字,不懂得隨時提問)

20190429-設計大改,已經不是這個樣子了,我把這個提出來當demo了,汗~~。不過還好,道理都一樣

手指拖拽旋轉邏輯這個專案用不到,所以沒有新增
複製程式碼

WebGL 手擼3d賀卡+小草飄動濾鏡

ps:有沒有覺得chrome裡devtool不是這個介面啊~哈哈哈,最近在弄一個視覺化的工具
複製程式碼

言歸正傳,我們們接著往下進行:

webGL初始化(常規操作)

  1. 獲取 WebGLRenderingContext
   const gl = canvas.getContext('webgl');
複製程式碼
  1. 編譯shader並把編譯好的shader附加到建立好的program中
    //頂點著色器
    const vertShader = gl.createShader(gl.VERTEX_SHADER); 
    gl.shaderSource(vertShader,vertSource);//vertSource:著色器原始碼
    gl.compileShader(vertShader);
    //片元著色器
    const fragShader = gl.createShader(gl.FRAGMENT_SHADER); 
    gl.shaderSource(fragShader,fragSource);//fragSource:著色器原始碼
    gl.compileShader(fragShader);
    
    //program相關
    const program = gl.createProgram();
    gl.attachShader(program,vertShader); //附加頂點著色器
    gl.attachShader(program,fragShader); //附加片元著色器
    gl.linkProgram(program);
複製程式碼

3.因為賀卡是3d的所以要開啟深度測試

    gl.enable(gl.DEPTH_TEST);
複製程式碼

4.因為元素不是模型而是一個個矩形,只是材質有的是透明的,在元素疊加時會把當前畫素覆蓋到緩衝中,比如顏色值(0,0,0,0)會覆蓋已有顏色(1,0,0,1),導致這個畫素不是你想要的紅色而是透明色。解決辦吧是開啟混合模式。

    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
    gl.enable(gl.BLEND);
複製程式碼

WebGL 手擼3d賀卡+小草飄動濾鏡

當然你如果確保每個元素都是jpg的話 可以不用開啟這個功能。
blendFunc 是定義混合方式,第一個引數是定義源畫素採取怎樣的處理,第二個引數是目標畫素(顏色緩衝區)採取怎樣的處理。
上面寫的函式的意思 最終畫素= 源畫素顏色*源透明度+緩衝區顏色*(1-源透明度)
複製程式碼

基本上初始化工作就完成了,下面來看下怎樣新增元素。

新增賀卡元素

先來附上shader原始碼

//vertSource
uniform mat4 uCameraMatrix;
uniform mat4 uTransformMatrix;
attribute vec3 aPosition;
attribute vec2 aUv;
varying vec2 vUv;
void main(){
    const float scale = 1.0/1.6;
    //這個矩陣不用管 我是懶得寫lookAt了 和lookAt的功能是一樣的
    const mat4 viewAngle = mat4(
        1,0,0,0,
        0,cos(-.1),-sin(-.1),0,
        0,sin(-.1),cos(-.1),0,
        0,-50,-1000,1
    );
    vec3 cPosition = (aPosition)*vec3(scale,scale,-1);
    gl_Position = uCameraMatrix*viewAngle*(uTransformMatrix*vec4(cPosition.xy,0.0,1.0)+vec4(0,0,cPosition.z,0));
    vUv = aUv;//uv傳給片元著色器的,供取樣定位用
} 
複製程式碼
//片元著色器
precision highp float;
uniform sampler2D uImage;
varying vec2 vUv;
void main(){
    vec4 color = texture2D(uImage,vUv);
    if(color.a == 0.0){
        discard;//這個是如果取樣的顏色是透明的則丟棄該顏色,和不加有一點兒區別,看下方圖(需要關閉BLEND)
    }
    gl_FragColor = color;
}
複製程式碼

WebGL 手擼3d賀卡+小草飄動濾鏡

上述頂點著色器主要說下

gl_Position = uCameraMatrix*viewAngle*(uTransformMatrix*vec4(cPosition.xy,0.0,1.0)+vec4(0,0,cPosition.z,0));
複製程式碼

uCameraMatrix:透視矩陣

viewAngle:相當於lookAt,我也想直接在js中把這兩個矩陣整合了,但是看gl-mat4lookAt方法用不對,也沒深究,後來放棄了,直接寫進去了,再就是我這裡面的Z軸取反了,因為gl-mat4裡的透視矩陣給我反過來了,我用不慣。。?,然後正回來了。

uTransformMatrix:變換矩陣,用於變換當前元素用的,心細的童鞋看了應該會問我為什麼不直接寫成

uTransformMatrix*vec4(cPosition,1.0)而是寫成

uTransformMatrix*vec4(cPosition.xy,0.0,1.0)+vec4(0,0,cPosition.z,0)呢?我的做法是用同一個矩陣使每個元素按照自身的底部進行旋轉,如果z軸不是0的話旋轉就不是底部了,所以要先變換,在進行Z軸位移,就是我想要的每個元素以自身的底兒來旋轉。

說完shader接下來就是drawArrays了。

整個3d中我分成了兩類元素,一類是不變的,也就是地面,一類是跟著展開旋轉的,也就是非地面的部分。

地面是相對於其他部分來說只有 uTransformMatrix是個單位矩陣,其他的是隨時間變換而變換,所以我選擇了把他們統一做成了一樣的結構,新增了一個rotateFlag做區分。 每個資料結構如下:

interface attribData{
    buffer:WebGLBuffer; 
    data:Float32Array; //記錄的頂點和UV
    texture?:WebGLTexture; //自身所需的素材
    rotateFlag:boolean; //旋轉開關
}
複製程式碼
  1. 首先要寫入元素資料
    createStandEle(file,[x,y,z]){
        //file:圖片名稱
        //this.option.assets[file]:圖片元素
        const scale = Math.sqrt((600+z)/600); //這個下面會重點說下
        const imgWidth = (<HTMLImageElement>this.option.assets[file]).naturalWidth*scale;
        const imgHeight = (<HTMLImageElement>this.option.assets[file]).naturalHeight*scale;
        const name = file.match(/card\_([^\.]+)/)[1];
        const data = {
            buffer:this.gl.createBuffer(),
            data:new Float32Array([ 
                //頂點資料                            UV資料
                x-imgWidth/2,y,z,                      0,1,
                x+imgWidth/2,y,z,                      1,1,
                x-imgWidth/2,y+imgHeight,z,            0,0,
                x+imgWidth/2,y+imgHeight,z,            1,0
            ]),
            texture:this.gl.createTexture(),
            rotateFlag:true,
        };
        //this.cardData:是我的所有元素的集合
        this.cardData[name] = data; 
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER,data.buffer);
        //給ARRAY_BUFFER寫入資料
        this.gl.bufferData(this.gl.ARRAY_BUFFER, data.data, this.gl.STATIC_DRAW);
        this.gl.activeTexture(this.gl.TEXTURE0);
        this.gl.bindTexture(this.gl.TEXTURE_2D,data.texture);
        let format = this.gl.RGB; //jpg沒必要用alpha
        if(texture.search(/\.png$/)>=0){
            format = this.gl.RGBA;
        }
        //給gl.TEXTURE_2D設定紋理
        this.gl.texImage2D(this.gl.TEXTURE_2D,0,format,format,this.gl.UNSIGNED_BYTE,this.option.assets[texture]);
        //下面是縮放取樣和包裝方式
        this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MIN_FILTER,this.gl.LINEAR);
        this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_MAG_FILTER,this.gl.LINEAR);
        this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_S,this.gl.CLAMP_TO_EDGE);
        this.gl.texParameteri(this.gl.TEXTURE_2D,this.gl.TEXTURE_WRAP_T,this.gl.CLAMP_TO_EDGE);
    }
複製程式碼
基本邏輯就是建立緩衝->繫結緩衝—>給緩衝賦值
複製程式碼

程式碼中的scale有必要說一下,透視矩陣中是符合近大遠小的特徵,但是設計稿件是平面的,沒有遠近的概念,加上遠近之後,psd的前面的元素根據近大遠小的原則是不做處理的話,近處的會大的很離譜,這時有同學會說,我直接縮小圖片就好啦,那麼問題又來了,縮小圖片後近大遠小的原則,其實是近處的元素處於放大的效果 ,小圖片放大會虛大家都知道的吧,所以採取直接縮小圖片的做法是錯的,唯一的做法是修改元素大小,來填充圖片,這樣就不會出現虛的現象了。

WebGL 手擼3d賀卡+小草飄動濾鏡

  1. 執行渲染
    render(timeStamp,offsetTime){
        if(this.rotateX<Math.PI/2){
            this.rotateX+=0.01*offsetTime;
        }else{
            this.rotateX = Math.PI/2;
        }
        Object.keys(this.cardData).forEach(i=>{
            //遍歷並渲染所有元素
            this.renderBuffer(this.cardData[i]);
        });
        super.render(timeStamp,offsetTime);
    }
    renderBuffer(data:attribData){
        this.gl.clear(this.gl.COLOR_BUFFER_BIT|this.gl.DEPTH_BUFFER_BIT);
        this.gl.useProgram(this.cardProgram);
        if(data.rotateFlag){
            const rotate = this.rotateX-Math.PI/2;
            this.gl.uniformMatrix4fv(this.cardParam.uTransformMatrix,false,new Float32Array([
                1,0,0,0,
                0,Math.cos(rotate),-Math.sin(rotate),0,
                0,Math.sin(rotate),Math.cos(rotate),0,
                0,0,0,1,
            ]));
        }else{
            //如果不是旋轉元素則賦值給uTransformMatrix 一個單位矩陣。
            this.gl.uniformMatrix4fv(this.cardParam.uTransformMatrix ,false,this.identityMatrix);
        }
        this.gl.bindBuffer(this.gl.ARRAY_BUFFER,data.buffer);
        this.gl.vertexAttribPointer(<GLint>this.cardParam.aPosition,3,this.gl.FLOAT,false,4*5,0);
        this.gl.vertexAttribPointer(<GLint>this.cardParam.aUv,2,this.gl.FLOAT,false,4*5,4*3);
        this.gl.activeTexture(this.gl.TEXTURE0);
        this.gl.bindTexture(this.gl.TEXTURE_2D,data.texture);
        this.gl.drawArrays(this.gl.TRIANGLE_STRIP,0,4);
    }
複製程式碼

基本邏輯就 繫結緩衝&繫結紋理—>告訴顯示卡從當前繫結的緩衝區中讀取頂點資料->drawArrays

花飄動動效

WebGL 手擼3d賀卡+小草飄動濾鏡
這個效果最主要的操作都在片元著色器中,原理就是獲取uv插值vUV,並對其進行偏移運算,然後讀取計算後uv位置的紋理取樣。很簡單吧~ 附上更新好的片元著色器程式碼;

//片元著色器
precision highp float;
uniform sampler2D uImage;
uniform int uType;//0:非小草 1:小草 這些都是在js中設定的
uniform float uTime;//當前時間戳
varying vec2 vUv;
void main(){
    vec4 color = vec4(0);
    if(uType == 1){
        //小草部分
        float offset = distance(vUv,vec2(0.5,1.0));
        offset = pow(offset,2.)/8.0*sin(uTime);
        mat2 rotate = mat2(
            cos(offset),-sin(offset),
            sin(offset),cos(offset)
        );
        vec2 cUv = vec2(0.5,1.0)+rotate*(vUv-vec2(0.5,1.0));
        if(cUv.x<0.||cUv.y<0.||cUv.x>1.||cUv.y>1.) discard;
        color = texture2D(uImage,cUv);
    }else{
        color = texture2D(uImage,vUv);
    }
    if(color.a == 0.0){
        discard;
    }
    gl_FragColor = color;
}
複製程式碼

上述小草部分 就是對當前uv做偏移處理。

因為小草底部是紮在地上不動的,而且飄動不是線性變化的,越遠離地面飄動幅度越大,所以不能在頂點著色器裡操作斜切啥的運算(類似於css transform的skew操作)。

我這裡選擇的是當前uv到底部中心(小草根部)的距離取二次方,距離底部中心越遠幅度也越明顯。

float offset = distance(vUv,vec2(0.5,1.0));
offset = pow(offset,2.)/8.0;// /8.0是直接用的話幅度太大 而UV值在0-1之間,做一個縮小處理
複製程式碼

再乘以和時間相關的sin值,當sin為0時,因為相乘的關係,所以也就是最後的計算結果和傳入的vUv一樣,也就是說和貼圖元素一樣,當sin為1和-1是就是偏移最大,也就是扭曲後的圖片。

    offset = offset*sin(uTime);
複製程式碼

然後把這個值當作旋轉矩陣的角度,最終生成新的uv座標;

mat2 rotate = mat2(//旋轉矩陣
    cos(offset),-sin(offset),
    sin(offset),cos(offset)
);
vec2 cUv = vec2(0.5,1.0)+rotate*(vUv-vec2(0.5,1.0));//相對於底部中心做旋轉處理
複製程式碼

剩下的部分就是在js中每一幀傳入時間戳還有uType值即可。剩下的就是交給webGL渲染管線處理。

這部分其實應該叫做濾鏡了。像水動效啊、火焰動效啊,還有pixiJs中的filter基本上都是一樣的流程,什麼抖音效果,rgb顏色分離都是在這兒處理。

這部分純文字也不知道能不能講懂。。 後續應該沒啥要加的了。剩下的就是非webGL部分了。過兩天這個專案做好後webGL部分會在結尾處展示哈,想看效果先看完前面的視訊。。。汗。。。

有啥不明白的留言~~~歡迎提問~哈哈哈

相關文章