WebGL Shader 環境搭建

_Hahn_發表於2018-09-26

正如《WebGL 程式設計指南》中所說的:

傳統的三維圖形程式通常使用 C 或 C++ 等語言開發,併為特定的平臺被編譯成二進位制的可執行檔案。這意味著程式不能跨平臺執行。相比之下,WebGL 程式由 HTML 和 JavaScript 組成,只需要放在 Web 上即可執行。WebGL 由 OpenGL ES 衍生而來。

WebGL Shader 環境搭建

WebGL Shader 環境搭建

OpenGL 是底層的驅動級的圖形介面,但是這種底層介面是瀏覽器中的 JavaScript 無法呼叫的。2010 年 WebGL 被推出來了之後,它允許工程師使用 JavaScript 去呼叫部分封裝過的 OpenGL ES2.0 標準介面去提供硬體級別的 3D 圖形加速功能。所以 GLSL 程式真正執行的時候還是跑在 OpenGL 驅動上的。

一、DOM

介紹了背景之後,下面說下怎麼在網頁上跑起 shader 程式碼來,先看 DOM 怎麼寫:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>webgl</title>
    <style>
        body {margin: 0; overflow: hidden;}
    </style>
</head>
<body onload="main()">
    <canvas id="album" width="300" height="300">
        Please use a browser that supports "canvas"
    </canvas>

    <script src="./js/lib/webgl-utils.js"></script>
    <script src="./js/lib/webgl-debug.js"></script>
    <script src="./js/lib/cuon-utils.js"></script>
    <script src="./js/lib/cuon-matrix.js"></script>
    <script src="./index.js"></script>
</body>
</html>

複製程式碼

Ok,簡單的不能再簡單的 DOM 結構了,整個頁面只需要一個 <canvas>的節點即可。Canvas 是 HTML5 提供的一個特性,你可以把它當做一個載體,簡單的說就是一張白紙。而 Canvas 2D 相當於獲取了內建的二維圖形介面,也就是二維畫筆。Canvas 3D 是獲取基於 WebGL 的圖形介面,相當於三維畫筆(當然你也可以畫二維的東西)。

其中還引用了這麼幾個檔案,這幾個檔案是來自《WebGL 程式設計指南》裡提供的工具庫:

<script src="./js/lib/webgl-utils.js"></script>
<script src="./js/lib/webgl-debug.js"></script>
<script src="./js/lib/cuon-utils.js"></script>
<script src="./js/lib/cuon-matrix.js"></script>
複製程式碼

下面分別把他們的程式碼列出來,它們的作用就是封裝程式碼、相容版本、提供工具函式等,這裡先不細究:

// webgl-utils.js
WebGLUtils=function(){var o=function(e,n){for(var t=["webgl","experimental-webgl","webkit-3d","moz-webgl"],i=null,o=0;o<t.length;++o){try{i=e.getContext(t[o],n)}catch(e){}if(i)break}return i};return{create3DContext:o,setupWebGL:function(e,n,t){t=t||function(e){var n=document.getElementsByTagName("body")[0];if(n){var t=window.WebGLRenderingContext?'It doesn\'t appear your computer can support WebGL.<br/><a href="http://get.webgl.org">Click here for more information.</a>':'This page requires a browser that supports WebGL.<br/><a href="http://get.webgl.org">Click here to upgrade your browser.</a>';e&&(t+="<br/><br/>Status: "+e),n.innerHTML='<div style="margin: auto; width:500px;z-index:10000;margin-top:20em;text-align:center;">'+t+"</div>"}},e.addEventListener&&e.addEventListener("webglcontextcreationerror",function(e){t(e.statusMessage)},!1);var i=o(e,n);return i||(window.WebGLRenderingContext,t("")),i}}}(),window.requestAnimationFrame||(window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e,n){window.setTimeout(e,1e3/60)}),window.cancelAnimationFrame||(window.cancelAnimationFrame=window.cancelRequestAnimationFrame||window.webkitCancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.mozCancelAnimationFrame||window.mozCancelRequestAnimationFrame||window.msCancelAnimationFrame||window.msCancelRequestAnimationFrame||window.oCancelAnimationFrame||window.oCancelRequestAnimationFrame||window.clearTimeout);
複製程式碼
// webgl-debug.js
WebGLDebugUtils=function(){var a={enable:{0:!0},disable:{0:!0},getParameter:{0:!0},drawArrays:{0:!0},drawElements:{0:!0,2:!0},createShader:{0:!0},getShaderParameter:{1:!0},getProgramParameter:{1:!0},getVertexAttrib:{1:!0},vertexAttribPointer:{2:!0},bindTexture:{0:!0},activeTexture:{0:!0},getTexParameter:{0:!0,1:!0},texParameterf:{0:!0,1:!0},texParameteri:{0:!0,1:!0,2:!0},texImage2D:{0:!0,2:!0,6:!0,7:!0},texSubImage2D:{0:!0,6:!0,7:!0},copyTexImage2D:{0:!0,2:!0},copyTexSubImage2D:{0:!0},generateMipmap:{0:!0},bindBuffer:{0:!0},bufferData:{0:!0,2:!0},bufferSubData:{0:!0},getBufferParameter:{0:!0,1:!0},pixelStorei:{0:!0,1:!0},readPixels:{4:!0,5:!0},bindRenderbuffer:{0:!0},bindFramebuffer:{0:!0},checkFramebufferStatus:{0:!0},framebufferRenderbuffer:{0:!0,1:!0,2:!0},framebufferTexture2D:{0:!0,1:!0,2:!0},getFramebufferAttachmentParameter:{0:!0,1:!0,2:!0},getRenderbufferParameter:{0:!0,1:!0},renderbufferStorage:{0:!0,1:!0},clear:{0:!0},depthFunc:{0:!0},blendFunc:{0:!0,1:!0},blendFuncSeparate:{0:!0,1:!0,2:!0,3:!0},blendEquation:{0:!0},blendEquationSeparate:{0:!0,1:!0},stencilFunc:{0:!0},stencilFuncSeparate:{0:!0,1:!0},stencilMaskSeparate:{0:!0},stencilOp:{0:!0,1:!0,2:!0},stencilOpSeparate:{0:!0,1:!0,2:!0,3:!0},cullFace:{0:!0},frontFace:{0:!0}},r=null;function o(e){if(null==r)for(var t in r={},e)"number"==typeof e[t]&&(r[e[t]]=t)}function n(){if(null==r)throw"WebGLDebugUtils.init(ctx) not called"}function f(e){n();var t=r[e];return void 0!==t?t:"*UNKNOWN WebGL ENUM (0x"+e.toString(16)+")"}function u(e,t,r){var n=a[e];return void 0!==n&&n[t]?f(r):r.toString()}function S(e){var t=e.getParameter(e.MAX_VERTEX_ATTRIBS),r=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,r);for(var n=0;n<t;++n)e.disableVertexAttribArray(n),e.vertexAttribPointer(n,4,e.FLOAT,!1,0,0),e.vertexAttrib1f(n,0);e.deleteBuffer(r);var a=e.getParameter(e.MAX_TEXTURE_IMAGE_UNITS);for(n=0;n<a;++n)e.activeTexture(e.TEXTURE0+n),e.bindTexture(e.TEXTURE_CUBE_MAP,null),e.bindTexture(e.TEXTURE_2D,null);for(e.activeTexture(e.TEXTURE0),e.useProgram(null),e.bindBuffer(e.ARRAY_BUFFER,null),e.bindBuffer(e.ELEMENT_ARRAY_BUFFER,null),e.bindFramebuffer(e.FRAMEBUFFER,null),e.bindRenderbuffer(e.RENDERBUFFER,null),e.disable(e.BLEND),e.disable(e.CULL_FACE),e.disable(e.DEPTH_TEST),e.disable(e.DITHER),e.disable(e.SCISSOR_TEST),e.blendColor(0,0,0,0),e.blendEquation(e.FUNC_ADD),e.blendFunc(e.ONE,e.ZERO),e.clearColor(0,0,0,0),e.clearDepth(1),e.clearStencil(-1),e.colorMask(!0,!0,!0,!0),e.cullFace(e.BACK),e.depthFunc(e.LESS),e.depthMask(!0),e.depthRange(0,1),e.frontFace(e.CCW),e.hint(e.GENERATE_MIPMAP_HINT,e.DONT_CARE),e.lineWidth(1),e.pixelStorei(e.PACK_ALIGNMENT,4),e.pixelStorei(e.UNPACK_ALIGNMENT,4),e.pixelStorei(e.UNPACK_FLIP_Y_WEBGL,!1),e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!1),e.UNPACK_COLORSPACE_CONVERSION_WEBGL&&e.pixelStorei(e.UNPACK_COLORSPACE_CONVERSION_WEBGL,e.BROWSER_DEFAULT_WEBGL),e.polygonOffset(0,0),e.sampleCoverage(1,!1),e.scissor(0,0,e.canvas.width,e.canvas.height),e.stencilFunc(e.ALWAYS,0,4294967295),e.stencilMask(4294967295),e.stencilOp(e.KEEP,e.KEEP,e.KEEP),e.viewport(0,0,e.canvas.clientWidth,e.canvas.clientHeight),e.clear(e.COLOR_BUFFER_BIT|e.DEPTH_BUFFER_BIT|e.STENCIL_BUFFER_BIT);e.getError(););}return{init:o,mightBeEnum:function(e){return n(),void 0!==r[e]},glEnumToString:f,glFunctionArgToString:u,makeDebugContext:function(t,a){o(t),a=a||function(e,t,r){for(var n,a="",i=0;i<r.length;++i)a+=(0==i?"":", ")+u(t,i,r[i]);n="WebGL error "+f(e)+" in "+t+"("+a+")",window.console&&window.console.log&&window.console.log(n)};var i={};function e(r,n){return function(){var e=r[n].apply(r,arguments),t=r.getError();return 0!=t&&(i[t]=!0,a(t,n,arguments)),e}}var r={};for(var n in t)"function"==typeof t[n]?r[n]=e(t,n):r[n]=t[n];return r.getError=function(){for(var e in i)if(i[e])return i[e]=!1,e;return t.NO_ERROR},r},makeLostContextSimulatingContext:function(r){var e={},a=1,n=!1,i=[],t=void 0,o=void 0,f=void 0,u={};function c(e,t){var r=e[t];return function(){if(!n)return function(e){for(var t=0;t<e.length;++t){var r=e[t];if((n=r)instanceof WebGLBuffer||n instanceof WebGLFramebuffer||n instanceof WebGLProgram||n instanceof WebGLRenderbuffer||n instanceof WebGLShader||n instanceof WebGLTexture)return r.__webglDebugContextLostId__==a}var n;return!0}(arguments)?r.apply(e,arguments):void(u[e.INVALID_OPERATION]=!0)}}for(var l in r)"function"==typeof r[l]?e[l]=c(r,l):e[l]=r[l];function b(e){return{statusMessage:e}}e.loseContext=function(){if(!n){for(n=!0,++a;r.getError(););!function(){for(var e=Object.keys(u),t=0;t<e.length;++t)delete glErrorShdow_[e]}(),u[r.CONTEXT_LOST_WEBGL]=!0,setTimeout(function(){t&&t(b("context lost"))},0)}},e.restoreContext=function(){if(n){if(!o)throw"You can not restore the context without a listener";setTimeout(function(){if(function(){for(var e=0;e<i.length;++e){var t=i[e];t instanceof WebGLBuffer?r.deleteBuffer(t):t instanceof WebctxFramebuffer?r.deleteFramebuffer(t):t instanceof WebctxProgram?r.deleteProgram(t):t instanceof WebctxRenderbuffer?r.deleteRenderbuffer(t):t instanceof WebctxShader?r.deleteShader(t):t instanceof WebctxTexture&&r.deleteTexture(t)}}(),S(r),n=!1,o){var e=o;o=f,f=void 0,e(b("context restored"))}},0)}},e.getError=function(){if(!n)for(;e=r.getError();)u[e]=!0;for(var e in u)if(u[e])return delete u[e],e;return r.NO_ERROR};for(var s=["createBuffer","createFramebuffer","createProgram","createRenderbuffer","createShader","createTexture"],g=0;g<s.length;++g)e[A=s[g]]=function(t){return function(){if(n)return null;var e=t.apply(r,arguments);return e.__webglDebugContextLostId__=a,i.push(e),e}}(r[A]);var E=["getActiveAttrib","getActiveUniform","getBufferParameter","getContextAttributes","getAttachedShaders","getFramebufferAttachmentParameter","getParameter","getProgramParameter","getProgramInfoLog","getRenderbufferParameter","getShaderParameter","getShaderInfoLog","getShaderSource","getTexParameter","getUniform","getUniformLocation","getVertexAttrib"];for(g=0;g<E.length;++g)e[A=E[g]]=function(e){return function(){return n?null:e.apply(r,arguments)}}(e[A]);var d,m,T,R=["isBuffer","isEnabled","isFramebuffer","isProgram","isRenderbuffer","isShader","isTexture"];for(g=0;g<R.length;++g){var A;e[A=R[g]]=function(e){return function(){return!n&&e.apply(r,arguments)}}(e[A])}function x(t){return"function"==typeof t?t:function(e){t.handleEvent(e)}}return e.checkFramebufferStatus=(d=e.checkFramebufferStatus,function(){return n?r.FRAMEBUFFER_UNSUPPORTED:d.apply(r,arguments)}),e.getAttribLocation=(m=e.getAttribLocation,function(){return n?-1:m.apply(r,arguments)}),e.getVertexAttribOffset=(T=e.getVertexAttribOffset,function(){return n?0:T.apply(r,arguments)}),e.isContextLost=function(){return n},e.registerOnContextLostListener=function(e){t=x(e)},e.registerOnContextRestoredListener=function(e){n?f=x(e):o=x(e)},e},resetToInitialState:S}}();
複製程式碼
// cuon-utils.js
function initShaders(e,r,a){var t=createProgram(e,r,a);return t?(e.useProgram(t),e.program=t,!0):(console.log("Failed to create program"),!1)}function createProgram(e,r,a){var t=loadShader(e,e.VERTEX_SHADER,r),o=loadShader(e,e.FRAGMENT_SHADER,a);if(!t||!o)return null;var l=e.createProgram();if(!l)return null;if(e.attachShader(l,t),e.attachShader(l,o),e.linkProgram(l),!e.getProgramParameter(l,e.LINK_STATUS)){var n=e.getProgramInfoLog(l);return console.log("Failed to link program: "+n),e.deleteProgram(l),e.deleteShader(o),e.deleteShader(t),null}return l}function loadShader(e,r,a){var t=e.createShader(r);if(null==t)return console.log("unable to create shader"),null;if(e.shaderSource(t,a),e.compileShader(t),!e.getShaderParameter(t,e.COMPILE_STATUS)){var o=e.getShaderInfoLog(t);return console.log("Failed to compile shader: "+o),e.deleteShader(t),null}return t}function getWebGLContext(e,r){var a=WebGLUtils.setupWebGL(e);return a?((arguments.length<2||r)&&(a=WebGLDebugUtils.makeDebugContext(a)),a):null}
複製程式碼
// cuon-matrix.js
var Matrix4=function(t){var e,r,n;if(t&&"object"==typeof t&&t.hasOwnProperty("elements")){for(r=t.elements,n=new Float32Array(16),e=0;e<16;++e)n[e]=r[e];this.elements=n}else this.elements=new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1])};Matrix4.prototype.setIdentity=function(){var t=this.elements;return t[0]=1,t[4]=0,t[8]=0,t[12]=0,t[1]=0,t[5]=1,t[9]=0,t[13]=0,t[2]=0,t[6]=0,t[10]=1,t[14]=0,t[3]=0,t[7]=0,t[11]=0,t[15]=1,this},Matrix4.prototype.set=function(t){var e,r,n;if((r=t.elements)!==(n=this.elements)){for(e=0;e<16;++e)n[e]=r[e];return this}},Matrix4.prototype.concat=function(t){var e,r,n,o,i,s,a,u;if(r=this.elements,n=this.elements,r===(o=t.elements))for(o=new Float32Array(16),e=0;e<16;++e)o[e]=r[e];for(e=0;e<4;e++)i=n[e],s=n[e+4],a=n[e+8],u=n[e+12],r[e]=i*o[0]+s*o[1]+a*o[2]+u*o[3],r[e+4]=i*o[4]+s*o[5]+a*o[6]+u*o[7],r[e+8]=i*o[8]+s*o[9]+a*o[10]+u*o[11],r[e+12]=i*o[12]+s*o[13]+a*o[14]+u*o[15];return this},Matrix4.prototype.multiply=Matrix4.prototype.concat,Matrix4.prototype.multiplyVector3=function(t){var e=this.elements,r=t.elements,n=new Vector3,o=n.elements;return o[0]=r[0]*e[0]+r[1]*e[4]+r[2]*e[8]+e[12],o[1]=r[0]*e[1]+r[1]*e[5]+r[2]*e[9]+e[13],o[2]=r[0]*e[2]+r[1]*e[6]+r[2]*e[10]+e[14],n},Matrix4.prototype.multiplyVector4=function(t){var e=this.elements,r=t.elements,n=new Vector4,o=n.elements;return o[0]=r[0]*e[0]+r[1]*e[4]+r[2]*e[8]+r[3]*e[12],o[1]=r[0]*e[1]+r[1]*e[5]+r[2]*e[9]+r[3]*e[13],o[2]=r[0]*e[2]+r[1]*e[6]+r[2]*e[10]+r[3]*e[14],o[3]=r[0]*e[3]+r[1]*e[7]+r[2]*e[11]+r[3]*e[15],n},Matrix4.prototype.transpose=function(){var t,e;return e=(t=this.elements)[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this},Matrix4.prototype.setInverseOf=function(t){var e,r,n,o,i;if(r=t.elements,n=this.elements,(o=new Float32Array(16))[0]=r[5]*r[10]*r[15]-r[5]*r[11]*r[14]-r[9]*r[6]*r[15]+r[9]*r[7]*r[14]+r[13]*r[6]*r[11]-r[13]*r[7]*r[10],o[4]=-r[4]*r[10]*r[15]+r[4]*r[11]*r[14]+r[8]*r[6]*r[15]-r[8]*r[7]*r[14]-r[12]*r[6]*r[11]+r[12]*r[7]*r[10],o[8]=r[4]*r[9]*r[15]-r[4]*r[11]*r[13]-r[8]*r[5]*r[15]+r[8]*r[7]*r[13]+r[12]*r[5]*r[11]-r[12]*r[7]*r[9],o[12]=-r[4]*r[9]*r[14]+r[4]*r[10]*r[13]+r[8]*r[5]*r[14]-r[8]*r[6]*r[13]-r[12]*r[5]*r[10]+r[12]*r[6]*r[9],o[1]=-r[1]*r[10]*r[15]+r[1]*r[11]*r[14]+r[9]*r[2]*r[15]-r[9]*r[3]*r[14]-r[13]*r[2]*r[11]+r[13]*r[3]*r[10],o[5]=r[0]*r[10]*r[15]-r[0]*r[11]*r[14]-r[8]*r[2]*r[15]+r[8]*r[3]*r[14]+r[12]*r[2]*r[11]-r[12]*r[3]*r[10],o[9]=-r[0]*r[9]*r[15]+r[0]*r[11]*r[13]+r[8]*r[1]*r[15]-r[8]*r[3]*r[13]-r[12]*r[1]*r[11]+r[12]*r[3]*r[9],o[13]=r[0]*r[9]*r[14]-r[0]*r[10]*r[13]-r[8]*r[1]*r[14]+r[8]*r[2]*r[13]+r[12]*r[1]*r[10]-r[12]*r[2]*r[9],o[2]=r[1]*r[6]*r[15]-r[1]*r[7]*r[14]-r[5]*r[2]*r[15]+r[5]*r[3]*r[14]+r[13]*r[2]*r[7]-r[13]*r[3]*r[6],o[6]=-r[0]*r[6]*r[15]+r[0]*r[7]*r[14]+r[4]*r[2]*r[15]-r[4]*r[3]*r[14]-r[12]*r[2]*r[7]+r[12]*r[3]*r[6],o[10]=r[0]*r[5]*r[15]-r[0]*r[7]*r[13]-r[4]*r[1]*r[15]+r[4]*r[3]*r[13]+r[12]*r[1]*r[7]-r[12]*r[3]*r[5],o[14]=-r[0]*r[5]*r[14]+r[0]*r[6]*r[13]+r[4]*r[1]*r[14]-r[4]*r[2]*r[13]-r[12]*r[1]*r[6]+r[12]*r[2]*r[5],o[3]=-r[1]*r[6]*r[11]+r[1]*r[7]*r[10]+r[5]*r[2]*r[11]-r[5]*r[3]*r[10]-r[9]*r[2]*r[7]+r[9]*r[3]*r[6],o[7]=r[0]*r[6]*r[11]-r[0]*r[7]*r[10]-r[4]*r[2]*r[11]+r[4]*r[3]*r[10]+r[8]*r[2]*r[7]-r[8]*r[3]*r[6],o[11]=-r[0]*r[5]*r[11]+r[0]*r[7]*r[9]+r[4]*r[1]*r[11]-r[4]*r[3]*r[9]-r[8]*r[1]*r[7]+r[8]*r[3]*r[5],o[15]=r[0]*r[5]*r[10]-r[0]*r[6]*r[9]-r[4]*r[1]*r[10]+r[4]*r[2]*r[9]+r[8]*r[1]*r[6]-r[8]*r[2]*r[5],0===(i=r[0]*o[0]+r[1]*o[4]+r[2]*o[8]+r[3]*o[12]))return this;for(i=1/i,e=0;e<16;e++)n[e]=o[e]*i;return this},Matrix4.prototype.invert=function(){return this.setInverseOf(this)},Matrix4.prototype.setOrtho=function(t,e,r,n,o,i){var s,a,u,h;if(t===e||r===n||o===i)throw"null frustum";return a=1/(e-t),u=1/(n-r),h=1/(i-o),(s=this.elements)[0]=2*a,s[1]=0,s[2]=0,s[3]=0,s[4]=0,s[5]=2*u,s[6]=0,s[7]=0,s[8]=0,s[9]=0,s[10]=-2*h,s[11]=0,s[12]=-(e+t)*a,s[13]=-(n+r)*u,s[14]=-(i+o)*h,s[15]=1,this},Matrix4.prototype.ortho=function(t,e,r,n,o,i){return this.concat((new Matrix4).setOrtho(t,e,r,n,o,i))},Matrix4.prototype.setFrustum=function(t,e,r,n,o,i){var s,a,u,h;if(t===e||n===r||o===i)throw"null frustum";if(o<=0)throw"near <= 0";if(i<=0)throw"far <= 0";return a=1/(e-t),u=1/(n-r),h=1/(i-o),(s=this.elements)[0]=2*o*a,s[1]=0,s[2]=0,s[3]=0,s[4]=0,s[5]=2*o*u,s[6]=0,s[7]=0,s[8]=(e+t)*a,s[9]=(n+r)*u,s[10]=-(i+o)*h,s[11]=-1,s[12]=0,s[13]=0,s[14]=-2*o*i*h,s[15]=0,this},Matrix4.prototype.frustum=function(t,e,r,n,o,i){return this.concat((new Matrix4).setFrustum(t,e,r,n,o,i))},Matrix4.prototype.setPerspective=function(t,e,r,n){var o,i,s,a;if(r===n||0===e)throw"null frustum";if(r<=0)throw"near <= 0";if(n<=0)throw"far <= 0";if(t=Math.PI*t/180/2,0===(s=Math.sin(t)))throw"null frustum";return i=1/(n-r),a=Math.cos(t)/s,(o=this.elements)[0]=a/e,o[1]=0,o[2]=0,o[3]=0,o[4]=0,o[5]=a,o[6]=0,o[7]=0,o[8]=0,o[9]=0,o[10]=-(n+r)*i,o[11]=-1,o[12]=0,o[13]=0,o[14]=-2*r*n*i,o[15]=0,this},Matrix4.prototype.perspective=function(t,e,r,n){return this.concat((new Matrix4).setPerspective(t,e,r,n))},Matrix4.prototype.setScale=function(t,e,r){var n=this.elements;return n[0]=t,n[4]=0,n[8]=0,n[12]=0,n[1]=0,n[5]=e,n[9]=0,n[13]=0,n[2]=0,n[6]=0,n[10]=r,n[14]=0,n[3]=0,n[7]=0,n[11]=0,n[15]=1,this},Matrix4.prototype.scale=function(t,e,r){var n=this.elements;return n[0]*=t,n[4]*=e,n[8]*=r,n[1]*=t,n[5]*=e,n[9]*=r,n[2]*=t,n[6]*=e,n[10]*=r,n[3]*=t,n[7]*=e,n[11]*=r,this},Matrix4.prototype.setTranslate=function(t,e,r){var n=this.elements;return n[0]=1,n[4]=0,n[8]=0,n[12]=t,n[1]=0,n[5]=1,n[9]=0,n[13]=e,n[2]=0,n[6]=0,n[10]=1,n[14]=r,n[3]=0,n[7]=0,n[11]=0,n[15]=1,this},Matrix4.prototype.translate=function(t,e,r){var n=this.elements;return n[12]+=n[0]*t+n[4]*e+n[8]*r,n[13]+=n[1]*t+n[5]*e+n[9]*r,n[14]+=n[2]*t+n[6]*e+n[10]*r,n[15]+=n[3]*t+n[7]*e+n[11]*r,this},Matrix4.prototype.setRotate=function(t,e,r,n){var o,i,s,a,u,h,p,c,l,f,m,M;return t=Math.PI*t/180,o=this.elements,i=Math.sin(t),s=Math.cos(t),0!==e&&0===r&&0===n?(e<0&&(i=-i),o[0]=1,o[4]=0,o[8]=0,o[12]=0,o[1]=0,o[5]=s,o[9]=-i,o[13]=0,o[2]=0,o[6]=i,o[10]=s,o[14]=0,o[3]=0,o[7]=0,o[11]=0):0===e&&0!==r&&0===n?(r<0&&(i=-i),o[0]=s,o[4]=0,o[8]=i,o[12]=0,o[1]=0,o[5]=1,o[9]=0,o[13]=0,o[2]=-i,o[6]=0,o[10]=s,o[14]=0,o[3]=0,o[7]=0,o[11]=0):0===e&&0===r&&0!==n?(n<0&&(i=-i),o[0]=s,o[4]=-i,o[8]=0,o[12]=0,o[1]=i,o[5]=s,o[9]=0,o[13]=0,o[2]=0,o[6]=0,o[10]=1,o[14]=0,o[3]=0,o[7]=0,o[11]=0):(1!==(a=Math.sqrt(e*e+r*r+n*n))&&(e*=u=1/a,r*=u,n*=u),h=1-s,p=e*r,c=r*n,l=n*e,f=e*i,m=r*i,M=n*i,o[0]=e*e*h+s,o[1]=p*h+M,o[2]=l*h-m,o[3]=0,o[4]=p*h-M,o[5]=r*r*h+s,o[6]=c*h+f,o[7]=0,o[8]=l*h+m,o[9]=c*h-f,o[10]=n*n*h+s,o[11]=0,o[12]=0,o[13]=0,o[14]=0),o[15]=1,this},Matrix4.prototype.rotate=function(t,e,r,n){return this.concat((new Matrix4).setRotate(t,e,r,n))},Matrix4.prototype.setLookAt=function(t,e,r,n,o,i,s,a,u){var h,p,c,l,f,m,M,y,x,v,w,A;return p=n-t,c=o-e,l=i-r,m=(c*=f=1/Math.sqrt(p*p+c*c+l*l))*u-(l*=f)*a,M=l*s-(p*=f)*u,y=p*a-c*s,v=(M*=x=1/Math.sqrt(m*m+M*M+y*y))*l-(y*=x)*c,w=y*p-(m*=x)*l,A=m*c-M*p,(h=this.elements)[0]=m,h[1]=v,h[2]=-p,h[3]=0,h[4]=M,h[5]=w,h[6]=-c,h[7]=0,h[8]=y,h[9]=A,h[10]=-l,h[11]=0,h[12]=0,h[13]=0,h[14]=0,h[15]=1,this.translate(-t,-e,-r)},Matrix4.prototype.lookAt=function(t,e,r,n,o,i,s,a,u){return this.concat((new Matrix4).setLookAt(t,e,r,n,o,i,s,a,u))},Matrix4.prototype.dropShadow=function(t,e){var r=new Matrix4,n=r.elements,o=t[0]*e[0]+t[1]*e[1]+t[2]*e[2]+t[3]*e[3];return n[0]=o-e[0]*t[0],n[1]=-e[1]*t[0],n[2]=-e[2]*t[0],n[3]=-e[3]*t[0],n[4]=-e[0]*t[1],n[5]=o-e[1]*t[1],n[6]=-e[2]*t[1],n[7]=-e[3]*t[1],n[8]=-e[0]*t[2],n[9]=-e[1]*t[2],n[10]=o-e[2]*t[2],n[11]=-e[3]*t[2],n[12]=-e[0]*t[3],n[13]=-e[1]*t[3],n[14]=-e[2]*t[3],n[15]=o-e[3]*t[3],this.concat(r)},Matrix4.prototype.dropShadowDirectionally=function(t,e,r,n,o,i,s,a,u){var h=n*t+o*e+i*r;return this.dropShadow([t,e,r,-h],[s,a,u,0])};var Vector3=function(t){var e=new Float32Array(3);t&&"object"==typeof t&&(e[0]=t[0],e[1]=t[1],e[2]=t[2]),this.elements=e};Vector3.prototype.normalize=function(){var t=this.elements,e=t[0],r=t[1],n=t[2],o=Math.sqrt(e*e+r*r+n*n);return o?1==o||(o=1/o,t[0]=e*o,t[1]=r*o,t[2]=n*o):(t[0]=0,t[1]=0,t[2]=0),this};var Vector4=function(t){var e=new Float32Array(4);t&&"object"==typeof t&&(e[0]=t[0],e[1]=t[1],e[2]=t[2],e[3]=t[3]),this.elements=e};
複製程式碼

二、JavaScript

接下來就是寫 JS 了,是的就是這麼快。接下來我們在 index.js 寫進我們的程式碼,大家可以留意到 DOM 中繫結了一個事件<body onload="main()">

// index.js

// 頂點著色器程式碼
var VSHADER_SOURCE =`
    attribute vec4 a_Position;
    varying vec2 uv;

    void main() {
        gl_Position = vec4(vec2(a_Position), 0.0, 1.0);
        uv = vec2(0.5, 0.5) * (vec2(a_Position) + vec2(1.0, 1.0));
    }
`
// 片元著色器程式碼
var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;

    void main() {
        gl_FragColor = vec4(0.,0.,0.,1.);
    }
`;


function main() {
    var canvas = document.getElementById('album');
    // 這裡的寬高按實際情況設定
    canvas.width = 375;
    canvas.height = 667;
    
    // 獲取 webgl 上下文(getWebGLContext 是前面引入的工具庫預設的)
    var gl = getWebGLContext(canvas);
    if (!gl) {
        console.log('Failed to get the rendering context for WebGL');
        return;
    }
    
    // 初始化著色器(initShaders 是工具庫定義的函式,傳入上下文,頂點/片元著色器程式碼)
    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('Failed to intialize shaders.');
        return;
    }
    
    // 設定頂點資料(initVertexBuffers 函式詳見下面)
    var n = initVertexBuffers(gl);
    if (n < 0) {
        console.log('Failed to set the vertex information');
        return;
    }
    
    // 清空畫布
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    
    // 繪製頂點(三個點決定一個矩形,四個點可以繪製兩個三角形,組成為矩形,也就是我們的畫布)
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
}

// 初始化頂點緩衝區
function initVertexBuffers(gl) {
    // 頂點座標(畫布的四個點)
    var verticesTexCoords = new Float32Array([
        1.0, -1.0,
        1.0, 1.0,
        -1.0, 1.0,
        -1.0, -1.0
    ]);

    // 頂點數量(4個點決定一個矩形)
    var n = 4;

    // 建立頂點緩衝區
    var vertexTexCoordBuffer = gl.createBuffer();
    if (!vertexTexCoordBuffer) {
        console.log('Failed to create the buffer object');
        return -1;
    }

    // 繫結緩衝區
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

    // 獲取陣列每個元素的大小
    var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;

    // 獲取 a_Position 的儲存位置並設定緩衝區
    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
        console.log('Failed to get the storage location of a_Position');
        return -1;
    }
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 2, 0);
    gl.enableVertexAttribArray(a_Position);

    return n;
}
複製程式碼

有了這些程式碼之後,我們就可以通過修改片元著色器程式碼新增我們的效果了。

WebGL Shader 環境搭建

通過修改片元著色器程式碼(把顏色修改為紅色),重新整理後確實看到有所改變。那假設我們想要做動畫呢?

很簡單,我們在片元著色器程式碼中新增一個關於時間的變數:

var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;
    uniform float time;     // 變化時間

    void main() {
        gl_FragColor = vec4(0.,0.,0.,1.);
    }
`;
複製程式碼

然後在 main() 函式最後新增以下程式碼:

var t = 0;
var time = gl.getUniformLocation(gl.program, 'time');
if (time < 0) {
    console.log('Failed to get the storage location of time');
    return -1;
}
gl.uniform1f(time, .0);

render(gl, time, t);
複製程式碼

在 index.js 中新增一個render 函式:

function render(gl, time, t) {
    t += 0.01;
    gl.uniform1f(time, t);  // 每次渲染都把最新的時間傳入片元著色器中
    
    // 每次都需要清空畫布再繪製
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

    requestAnimationFrame(render.bind(this, gl, time, t));
}
複製程式碼

新增完了之後,我們修改一下片元著色器(引入時間變化),看看是否有效果:

var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;
    uniform float time;     // 變化時間

    void main() {
        float r = uv.x;
        float g = uv.y;
        float b = abs(sin(time));
        gl_FragColor = vec4(r,g,b,1.);
    }
`;
複製程式碼

PS:漸變錄成 GIF 真是個災難:

WebGL Shader 環境搭建


三、載入材質

當然我們不能一直跟畫素打交道,視訊或者圖片都可以以貼圖的形式放在畫布中進行加工。下面講講如何把貼圖展示在畫布中。首先簡單的預載入我們想要的貼圖資源:

function main() {
    var canvas = document.getElementById('album');
    // 這裡的寬高按實際情況設定
    canvas.width = 375;
    canvas.height = 667;
    
    // 在這裡把貼圖資源預載入了
    var imgList = [];
    preload(imgList, [
        './images/img1.jpg',
        './images/img2.jpg'
    ], function() {
        // 載入完成後再開始初始化 WebGL
        
        // 獲取 webgl 上下文
        var gl = getWebGLContext(canvas);
        if (!gl) {
            console.log('Failed to get the rendering context for WebGL');
            return;
        }
    // ...
    })
}

// 圖片預載入
function preload(imgList, arrayOfImages, callback) {
    var sum = arrayOfImages.length;
    var count = 0;
    arrayOfImages.forEach(function(value){
        var image = new Image()
        image.src = value;
        image.onload = function() {
            imgList.push(image)
            if (++count == sum) {
                callback && callback()
            }
        }
    })
}
複製程式碼

接下來就是初始化材質內容,在資源預載入的回撥函式底部增加我們之前寫好的 WebGL 程式碼:

function main() {
    var canvas = document.getElementById('album');
    // 這裡的寬高按實際情況設定
    canvas.width = 375;
    canvas.height = 667;

    // 在這裡把貼圖資源預載入了(這裡載入兩張圖,方便做轉場)
    var imgList = [];
    preload(imgList, [
        './images/img8.jpg',
        './images/img9.jpg'
    ], function() {

        // ...
        // 繪製頂點(這一步留到後面)
        // gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);


        // 新增材質初始化函式
        if (!initTextures(gl, imgList)) {
            console.log('Failed to intialize the texture.');
            return;
        }

        var t = 0;
        var time = gl.getUniformLocation(gl.program, 'time');
        if (time < 0) {
            console.log('Failed to get the storage location of time');
            return -1;
        }
        gl.uniform1f(time, .0);

        render(gl, time, t);
    })
}

// 初始化材質
function initTextures(gl, imgList) {

    // 有兩張圖,所以建立兩個材質
    var texture0 = gl.createTexture();
    var texture1 = gl.createTexture();

    if (!texture0 && !texture1) {
        console.log('Failed to create the texture object');
        return false;
    }
    var u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0');
    if (!u_Sampler0) {
        console.log('Failed to get the storage location of u_Sampler0');
        return false;
    }
    var u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1');
    if (!u_Sampler1) {
        console.log('Failed to get the storage location of u_Sampler1');
        return false;
    }

    // 載入我們的材質並初始化(函式在下面有定義)
    loadTexture(gl, texture0, u_Sampler0, imgList[0], 0);
    loadTexture(gl, texture1, u_Sampler1, imgList[1], 1);

    return true;
}


// 載入材質
function loadTexture(gl, texture, u_Sampler, image, index) {
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
    gl.activeTexture(gl['TEXTURE'+index])
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    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, index);
    return true;
}
複製程式碼

如果這個時候執行程式碼你會發現報錯,因為我們新增了兩個變數u_Sampler0u_Sampler1 用於存放兩張材質圖,所以需要在片元著色器中加上:

// 片元著色器程式碼
var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;
    uniform float time;     // 變化時間
    uniform sampler2D u_Sampler0; 
    uniform sampler2D u_Sampler1;

    void main() {
        // 同時使用內建的 mix() 函式做兩張圖的線性插值漸變效果
        vec4 color = mix(texture2D(u_Sampler0, uv), texture2D(u_Sampler1, uv), abs(sin(time)));
        gl_FragColor = vec4(color);
    }
`;
複製程式碼

WebGL Shader 環境搭建

其實這就是普通的轉場效果,我們還可以通過一些內建函式做更多轉場效果:

// 片元著色器程式碼
var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;
    uniform float time;     // 變化時間
    uniform sampler2D u_Sampler0; 
    uniform sampler2D u_Sampler1;

    void main() {
        // 同時使用內建的 mix() 函式做兩張圖的線性插值漸變效果
        vec4 color = mix(texture2D(u_Sampler0, uv), texture2D(u_Sampler1, uv), abs(sin(time)));
        gl_FragColor = vec4(color);
    }
`;
複製程式碼

WebGL Shader 環境搭建

再來個複雜的:

// 片元著色器程式碼
var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;
    uniform float time;     // 變化時間
    uniform sampler2D u_Sampler0; 
    uniform sampler2D u_Sampler1;

    void main() {
        float m = smoothstep(-0.5, 0., uv.x - abs(sin(time))*1.5);
        gl_FragColor = mix(texture2D(u_Sampler0, (uv - 0.5) * (1.0 - m) + 0.5), texture2D(u_Sampler1, (uv - 0.5) * m + 0.5), m);
    }
`;
複製程式碼

WebGL Shader 環境搭建

再來個縮放的:

// 片元著色器程式碼
var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;
    uniform float time;     // 變化時間
    uniform sampler2D u_Sampler0; 
    uniform sampler2D u_Sampler1;

    void main() {
        st -= .5;
        st *= 1.-smoothstep(0., 1., abs(sin(time)));
        st += .5;

        vec4 color = mix(texture2D(u_Sampler0, st), texture2D(u_Sampler1, uv), abs(sin(time)));
    }
`;
複製程式碼

WebGL Shader 環境搭建

這裡附上 indexjs 所有程式碼:

// 頂點著色器程式碼
var VSHADER_SOURCE =`
    attribute vec4 a_Position;
    varying vec2 uv;

    void main() {
        gl_Position = vec4(vec2(a_Position), 0.0, 1.0);
        uv = vec2(0.5, 0.5) * (vec2(a_Position) + vec2(1.0, 1.0));
    }
`
// 片元著色器程式碼
var FSHADER_SOURCE =`
    precision mediump float;
    varying vec2 uv;
    uniform float time;     // 變化時間
    uniform sampler2D u_Sampler0; 
    uniform sampler2D u_Sampler1;

    void main() {
        float m = smoothstep(-0.5, 0., uv.x - abs(sin(time))*1.5);
        gl_FragColor = mix(texture2D(u_Sampler0, (uv - 0.5) * (1.0 - m) + 0.5), texture2D(u_Sampler1, (uv - 0.5) * m + 0.5), m);
    }
`;


function main() {
    var canvas = document.getElementById('album');
    // 這裡的寬高按實際情況設定
    canvas.width = 375;
    canvas.height = 667;

    // 在這裡把貼圖資源預載入了
    var imgList = [];
    preload(imgList, [
        './images/img8.jpg',
        './images/img9.jpg'
    ], function() {

        // 獲取 webgl 上下文(getWebGLContext 是前面引入的工具庫預設的)
        var gl = getWebGLContext(canvas);
        if (!gl) {
            console.log('Failed to get the rendering context for WebGL');
            return;
        }
        
        // 初始化著色器(initShaders 是工具庫定義的函式,傳入上下文,頂點/片元著色器程式碼)
        if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
            console.log('Failed to intialize shaders.');
            return;
        }
        
        // 設定頂點資料(initVertexBuffers 函式詳見下面)
        var n = initVertexBuffers(gl);
        if (n < 0) {
            console.log('Failed to set the vertex information');
            return;
        }
        
        // 清空畫布
        gl.clearColor(0.0, 0.0, 0.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);

        // 繪製頂點
        // gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);

        // 設定貼圖
        if (!initTextures(gl, imgList)) {
            console.log('Failed to intialize the texture.');
            return;
        }

        var t = 0;
        var time = gl.getUniformLocation(gl.program, 'time');
        if (time < 0) {
            console.log('Failed to get the storage location of time');
            return -1;
        }
        gl.uniform1f(time, .0);

        render(gl, time, t);
    }) 
}

// 初始化頂點緩衝區
function initVertexBuffers(gl) {
    // 頂點座標
    var verticesTexCoords = new Float32Array([
        1.0, -1.0,
        1.0, 1.0,
        -1.0, 1.0,
        -1.0, -1.0
    ]);

    // 頂點數量
    var n = 4;

    // 建立頂點緩衝區
    var vertexTexCoordBuffer = gl.createBuffer();
    if (!vertexTexCoordBuffer) {
        console.log('Failed to create the buffer object');
        return -1;
    }

    // 繫結緩衝區
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

    // 獲取陣列每個元素的大小
    var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;

    // 獲取 a_Position 的儲存位置並設定緩衝區
    var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if (a_Position < 0) {
        console.log('Failed to get the storage location of a_Position');
        return -1;
    }
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 2, 0);
    gl.enableVertexAttribArray(a_Position);

    return n;
}

function render(gl, time, t) {
    t += 0.01;
    gl.uniform1f(time, t);
    
    // 每次都需要清空畫布再繪製
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 4);
    
    requestAnimationFrame(render.bind(this, gl, time, t));
}

// 圖片預載入
function preload(imgList, arrayOfImages, callback) {
    var sum = arrayOfImages.length;
    var count = 0;
    arrayOfImages.forEach(function(value){
        var image = new Image()
        image.src = value;
        image.onload = function() {
            imgList.push(image)
            if (++count == sum) {
                callback && callback()
            }
        }
    })
}

// 初始化材質
function initTextures(gl, imgList) {
    var texture0 = gl.createTexture();
    var texture1 = gl.createTexture();

    if (!texture0 && !texture1) {
        console.log('Failed to create the texture object');
        return false;
    }
    var u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0');
    if (!u_Sampler0) {
        console.log('Failed to get the storage location of u_Sampler0');
        return false;
    }
    var u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1');
    if (!u_Sampler1) {
        console.log('Failed to get the storage location of u_Sampler1');
        return false;
    }

    loadTexture(gl, texture0, u_Sampler0, imgList[0], 0);
    loadTexture(gl, texture1, u_Sampler1, imgList[1], 1);

    return true;
}

// 載入材質
function loadTexture(gl, texture, u_Sampler, image, index) {
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
    gl.activeTexture(gl['TEXTURE'+index])
    gl.bindTexture(gl.TEXTURE_2D, texture)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    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, index);
    return true;
}
複製程式碼