這兩天接到一個項目,是有關全屏視頻的,整個項目中分到我這兒最主要的部分就是結束頁要求3d賀卡展現,正巧和前幾天NingBo童鞋分享的同樣,乾脆點兒,此次搞個webGL版的。哈哈~css
demo地址html
20190426-如今階段就是隻作了個基礎版,曲線動畫啥的都是小事兒。web
20190427-如今加上了easebackout曲線方法,用的是d3-ease
感受挺好用的,還有小花的飄動的邏輯,稍後會講解。。太餓了~吃飯去。哈哈(已更新,純文字,不懂得隨時提問)chrome
20190429-設計大改,已經不是這個樣子了,我把這個提出來當demo
了,汗~~。不過還好,道理都同樣npm
手指拖拽旋轉邏輯這個項目用不到,因此沒有添加
複製代碼
ps:有沒有以爲chrome裏devtool不是這個界面啊~哈哈哈,最近在弄一個可視化的工具
複製代碼
言歸正傳,我們接着往下進行:canvas
const gl = canvas.getContext('webgl'); 複製代碼
//頂點着色器
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的因此要打開深度測試bash
gl.enable(gl.DEPTH_TEST);
複製代碼
4.由於元素不是模型而是一個個矩形,只是材質有的是透明的,在元素疊加時會把當前像素覆蓋到緩衝中,好比顏色值(0,0,0,0)會覆蓋已有顏色(1,0,0,1),致使這個像素不是你想要的紅色而是透明色。解決辦吧是開啓混合模式。markdown
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
複製代碼
固然你若是確保每一個元素都是jpg的話 能夠不用開啓這個功能。
blendFunc 是定義混合方式,第一個參數是定義源像素採起怎樣的處理,第二個參數是目標像素(顏色緩衝區)採起怎樣的處理。
上面寫的函數的意思 最終像素= 源像素顏色*源透明度+緩衝區顏色*(1-源透明度)
複製代碼
基本上初始化工做就完成了,下面來看下怎樣添加元素。數據結構
先來附上shader源碼ide
//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; } 複製代碼
上述頂點着色器主要說下
gl_Position = uCameraMatrix*viewAngle*(uTransformMatrix*vec4(cPosition.xy,0.0,1.0)+vec4(0,0,cPosition.z,0));
複製代碼
uCameraMatrix
:透視矩陣
viewAngle
:至關於lookAt,我也想直接在js中把這兩個矩陣整合了,可是看gl-mat4
的lookAt
方法用不對,也沒深究,後來放棄了,直接寫進去了,再就是我這裏面的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; //旋轉開關
}
複製代碼
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的前面的元素根據近大遠小的原則是不作處理的話,近處的會大的很離譜,這時有同窗會說,我直接縮小圖片就好啦,那麼問題又來了,縮小圖片後近大遠小的原則,實際上是近處的元素處於放大的效果 ,小圖片放大會虛你們都知道的吧,因此採起直接縮小圖片的作法是錯的,惟一的作法是修改元素大小,來填充圖片,這樣就不會出現虛的現象了。
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
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部分會在結尾處展現哈,想看效果先看完前面的視頻。。。汗。。。
有啥不明白的留言~~~歡迎提問~哈哈哈