继续填坑,本文会稍微提一些webgl的基础,不会做过多介绍,看官们请先准备要一定的基础知识。 webgl要实现前面的例子方式有很多,比如 给一个矩形平面添加多个顶点,然后在顶点着色器中,在xy平面上移动顶点位置; 或者移动顶点的z分量,再左乘视图矩阵; 或者只使用四个顶点来创建矩形,然后在片元着色器中对uv进行偏移等等。 我这次只讲第一种,下面几个方法玩个坑以后在填吧~ 这里是html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <!DOCTYPE html > <html lang ="zh-cn" > <head > <meta charset ="UTF-8" > <title > wave flag by webgl</title > <style > * { margin : 0 ; padding : 0 ; } html , body { width : 100% ; height : 100% ; } body { position : relative; background : lightgrey; } #flag-canvas { position : absolute; top : 50% ; left : 50% ; transform-origin : center; transform : translate3d (-50% , -50% , 0 ); } </style > </head > <body > <canvas id ="flag-canvas" > 你的浏览器不支持html5 </canvas > <script src ="./shaders.js" > </script > <script src ="./flag.js" > </script > </body > </html >
flag.js是核心代码,shaders.js 是一个工具类,因为webgl是偏底层的api所以细枝末节比较多,为了核心代码的整洁,所以单独抽取出来了。下面稍微做下解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 var ShaderUtil = { createShader : function (gl, source, type ) { var shader = gl.createShader (type) gl.shaderSource (shader, source) gl.compileShader (shader) if (!gl.getShaderParameter (shader, gl.COMPILE_STATUS )) { console .error ('Compile shader source fail:\n\n' + source, '\n\n=====error log======\n\n' , gl.getShaderInfoLog (shader)) gl.deleteShader (shader) return null } return shader }, createProgram : function (gl, vertexShader, fragmentShader, validate ) { var program = gl.createProgram () gl.attachShader (program, vertexShader) gl.attachShader (program, fragmentShader) gl.linkProgram (program) if (!gl.getProgramParameter (program, gl.LINK_STATUS )) { console .error ('Creating shader program fail:\n' , gl.getProgramInfoLog (program)) gl.deleteProgram (program) return null } if (validate) { gl.validateProgram (program) if (!gl.getProgramParameter (program, gl.VALIDATE_STATUS )) { console .error ('Error validating shader program:\n' , gl.getProgramInfoLog (program)) gl.deleteProgram (program) return null } } gl.detachShader (program, vertexShader) gl.detachShader (program, fragmentShader) gl.deleteShader (vertexShader) gl.deleteShader (fragmentShader) return program }, createProgramFromSrc : function (gl, vertexShaderSrc, fragmentShaderSrc, validate ) { var vShader = ShaderUtil .createShader (gl, vertexShaderSrc, gl.VERTEX_SHADER ) var fShader = ShaderUtil .createShader (gl, fragmentShaderSrc, gl.FRAGMENT_SHADER ) if (!vShader || !fShader) { gl.deleteShader (vShader) gl.deleteShader (fShader) return null } return ShaderUtil .createProgram ( gl, vShader, fShader, validate ) }, getSrcFromUrl : function (url, callback ) { var xhr = new XMLHttpRequest () xhr.open ('GET' , url, true ) xhr.onreadystatechange = function ( ) { if (xhr.readyState === 4 ) { if (xhr.status === 200 ) { callback (xhr.responseText ) } } } xhr.send () } } var Shaders = function (gl, vShaderSrc, fShaderSrc ) { var program = ShaderUtil .createProgramFromSrc (gl, vShaderSrc, fShaderSrc, true ) if (program) { this .program = program this .gl = gl gl.useProgram (this .program ) } this .activate = function ( ) { gl.useProgram (program) return this } this .deactivate = function ( ) { gl.useProgram (null ) return this } this .dispose = function ( ) { if (gl.getParameter (gl.CURRENT_PROGRAM === program)) { this .deactivate () } gl.deleteProgram (program) } }
基本上要运行一断完整的的webgl/opengl着色器代码流程主要包含这些:
glCreateShader(创建着色器) -> glShaderSource(载入着色器代码) -> glCompileShader(编译着色器) -> glCreateProgram(创建程序对象) -> glAttachShader(将着色器附着进来) -> glLinkProgram(把程序对象和所有被附着的着色器链接起来) -> glDetachShader(解除着色器) -> glDeleteShader(删除着色器)
一般来说shader在link完毕后使命就结束了,应该尽早地解除(glDetachShader)并删除(glDeleteShader)来释放内存,如果没有解除shader,即使把它删了它也仍然会附着在 program 上,直到被detach。
每一步骤的细节我都已经在代码里添加了注释,请自行阅读。
下面把 shaders.js 里的代码拆分出来讲解。
首先需要载入着色器的代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ShaderUtil .getSrcFromUrl ('vertexShader.vert' , function (src ) { vShaderSrc = src onAllLoaded () }) ShaderUtil .getSrcFromUrl ('fragmentShader.frag' , function (src ) { fShaderSrc = src onAllLoaded () }) function onAllLoaded ( ) { if (!vShaderSrc || !fShaderSrc) { return false } }
还有一种常用的方式是直接把着色器代码嵌入html或js。总之,着色器代码就是一大段字符串,用什么方式拿到都行,我使用xhr来引入主要是因为一些编辑器提供了shader语法高亮,建议使用这种方式。
下一步在onAllLoaded里创建Image对象来载入我们的纹理图像。
载入完成后,创建shander.js文件里定义的Shader对象,将gl对象和着色器代码作为参数传入,然后通过shader对象的program属性来获取我们着色器里定义的attribute和uniform变量的存储地址。
接着创建顶点缓冲区: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 ... createVerticesBuffer ()gl.vertexAttribPointer (aPosition, 2 , gl.FLOAT , false , eleSize * 2 , 0 ) gl.enableVertexAttribArray (aPosition) ... function createVerticesBuffer ( ) { var vertices = [] var x for (var i = 0 ; i <= imgWidth; i++) { x = -1 + 2 * i / imgWidth vertices.push (x, -1 , x, 1 ) } vertexCount = 2 * (imgWidth + 1 ) vertices = new Float32Array (vertices) eleSize = vertices.BYTES_PER_ELEMENT var buffer = gl.createBuffer () gl.bindBuffer (gl.ARRAY_BUFFER , buffer) gl.bufferData (gl.ARRAY_BUFFER , vertices, gl.STATIC_DRAW ) return buffer }
① 创建 imageWidth + 1个顶点,由于webgl的坐标是从-1~ 1,所以需要把 0 ~ imageWidth投影到 -1 ~ 1。 canvas坐标与webgl坐标的对应关系:
② 基本上,缓冲区创建有一个固定的流程,如下:
1、创建缓冲区对象——gl.createBuffer()
2、绑定缓冲区对象——gl.bindBuffer(target, buffer)
target可以是gl.ARRAY_BUFFER(表示缓冲区中是顶点的数据)或者是ELEMENT_ARRAY_BUFFER(表示缓冲区中是顶点的索引)
buffer是刚刚创建的缓冲区对象的引用
3、向缓冲区中写入数据——gl.bufferData(target, data, usage)
target同bindBuffer时的target,因为只能通过target向缓冲区写入数据,所以必须先绑定缓冲区
data是需要写入的类型化数组
usage是指缓冲类型,可以是GL_STREAM_DRAW , GL_STATIC_DRAW , GL_DYNAMIC_DRAW,该参数作用是帮助webgl优化操作,即使传入错误的值也不会中断程序
4、将缓冲区对象分配给attribute变量——gl.vertexAttribPointer(location, size, type, normalized, stride, offset)
location:变量的存储地址
size:每个顶点分量个数,若个数比变量的数量少,则按照gl.vertexAttrib[1234]f的规则来补全
type:指定数据类型
normalized:boolean类型,表示是否需要将非浮点类型数据归一化到[-1, 1] 区间
stride:相邻两个顶点之间的字节数,默认0
offset:数据的偏移量,即变量开始存储的位置(单位字节),默认0
5、开启attribute变量——gl.enableVertexAttribArray(location)
创建纹理对象: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ... createTexture ()var uSampler = gl.getUniformLocation (shader.program , 'u_Sampler' )gl.uniform1i (uSampler, 0 ) ... function createTexture ( ) { var texture = gl.createTexture () 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.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.RGB , gl.RGB , gl.UNSIGNED_BYTE , image) }
纹理映射比较复杂但是步骤也是比较固定:
1、将纹理坐标写入缓冲区(可选步骤,如果需要,一般是和需要绑定的顶点写入同一个缓冲区)
2、创建纹理对象——gl.createTexture
3、获取片元着色器中声明的取色器变量(uniform类型)的存储位置
4、使用Image对象加载图片
5、在图片加载完成后配置纹理——
①对纹理对象进行Y轴反转 (原因见上图):gl.pixelStorei(pname, param);(pname可以是gl.UNPACK_FLIP_Y_WEBGL或gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL)
②激活纹理单元 :gl.activeTexture(texUnit);(webGL默认至少支持8个纹理单元,可以是gl.TEXTURE0~7)
③开启纹理对象并绑定到target上 :gl.bindTexture(target, texture);(webGL只能通过纹理单元操作纹理对象,所以必须先绑定)
④设置纹理映射到图形上的方式 :gl.texParameteri(target, pname, param);
⑤设置纹理图片 :gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
⑥将纹理图像分配给纹理对象 :gl.texImage2D(target, level, internalformat, format, type, image);()
⑦将纹理单元传递给着色器中的取色器变量 :gl.uniform1i(u_Sampler, 0);
根据顶点绘制图形: gl.drawArrays(mode, first, count) mode:绘制的方式 first:指定从哪个顶点开始绘制 count:指定绘制需要用到的顶点个数(着色器会执行count次,每次处理1个顶点)
顶点着色器: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 uniform float u_Distance; attribute vec2 a_Position; varying vec2 v_UV; varying float v_Slope; float PI = 3.14159 ;float scale = 0.8 ;void main () { float x = a_Position.x; float y = a_Position.y; float amplitude = 1.0 - scale; float period = 2.0 ; float waveLength = 2.0 * scale; v_UV = (mat3 (0.625 ,0 ,0 , 0 ,0.625 ,0 , 0.5 ,0.5 ,1 ) * vec3 (x, y, 1.0 )).xy; y += amplitude * ( (x - (-scale)) / waveLength) * sin (2.0 * PI * (x - u_Distance)); float x2 = x - 0.001 ; float y2 = a_Position.y + amplitude * ( (x2 - (-scale)) / waveLength) * sin (2.0 * PI * (x2 - u_Distance)); v_Slope = y - y2; gl_Position = vec4 (vec2 (x, y), 0.0 , 1.0 ); }
片元着色器: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 precision mediump float ; uniform sampler2D u_Sampler; varying vec2 v_UV; varying float v_Slope; void main () { vec4 color = texture2D ( u_Sampler, v_UV ); if ( v_Slope > 0.0 ) { color = mix ( color, vec4 (0.0 , 0.0 , 0.0 , 1.0 ), v_Slope * 300.0 ); } if ( v_Slope < 0.0 ) { color = mix ( color, vec4 (1.0 ), abs (v_Slope) * 300.0 ); } if (v_UV.x < 0.0 || v_UV.x > 1.0 || v_UV.y < 0.0 || v_UV.y > 1.0 ) { color.a = 0.0 ; } gl_FragColor = color; }
着色器的语法类似c,我简单讲一下里面的逻辑,原理基本上和上一讲canvas2D的实现思路类似,而且逐“像素”的手段更是webgl拿手绝活。
① 由于webgl的缓冲区在每一次玩后都会清空,所以不能像之前那样保留lastY,我的做法是取获取0.001个单位前的x坐标,然后算出斜率v_Slope,传给片元着色器。
② 坐标系统问题:目前我们的代码里面已经涉及了好几套坐标系统,如
窗口、canvas、图片的坐标系统的原点都在左上角且y轴方向向下;
webgl、纹理(也叫uv或st)的坐标系统y轴向上,其中webgl的原点在中间,范围是[-1,1],纹理的原点在左下角,范围是[0, 1]。
各个顶点坐标在创建缓冲区的时候是占满canvas的,假设纹理坐标中有点 P(u, v),经过仿射变换后在webgl坐标中为点Q(x, y),则有
Q = mat3(2,0,0, 0,2,0, 0,0,1) * mat3(1,0,0, 0,1,0, -0.5,-0.5,1) * P
= mat3(2,0,0, 0,2,0, -1,-1,1) * P
可得:
P = (mat3(2,0,0, 0,2,0, -1,-1,1)^-1) * Q
= mat3(0.5,0,0, 0,0.5,0, 0.5,0.5,1) * Q
由于我们需要移动顶点但是不能超出canvas的可视区,所以需要对顶点位置缩放。
假设缩放比率为n,则有
Q = mat3(2n,0,0, 0,2n,0, 0,0,1) * mat3(1,0,0, 0,1,0, -0.5,-0.5,1) * P
可以得到顶点到纹理的变化矩阵为mat3(2n,0,-n, 0,2n,-n, 0,0,1)的逆矩阵,然后去掉齐次坐标。
完整代码戳这里
Demo1
Demo2:See the Pen flag waving by webgl by Kay (@oj8kay ) on CodePen .
目录指引: