示例可以在这里 查看。此次的示例挺有意思的(个人觉得),推荐看看。效果的图片预览请展开。
我似乎有几篇博文都说“预览请展开”的,那是因为不想浪费大家流量,如果有图片,或者文章过长的话就把一两句描述放前面,感兴趣的同学可以花一点流量阅读细节。
题外话:
前天晚上和村长对话了。村长得知我要考研之后很惊讶:“你居然去考研?堕落了!”然后推荐我去申国外。其他保守看应该没问题,不过先把外语水平考试(I/T)考了,才能投申请啊(所以不趁有时间考 I/T 的我是不是很愚蠢)。
所以之后会很忙(其实已经比正常的准备时间都推后了),不管是哪条线都会很忙。所以尽快将 Bulletproof 写到能运行示例的程度就暂时放在一边,火力全开为生存而斗争吧。jabanny 的邮件还没回复,和 tt 在 xross 上的试验讨论也暂缓(suspended)了。先说声对不起了。
这次我们来反向的是 Pixi 的滤镜,采用帧缓冲(Framebuffer
)作为绘制缓冲区。想当初 Bulletproof 底层换到 Pixi 上就是因为有一个高速滤镜的硬需求。
此次的着色器和 JavaScript 代码都是几乎推倒重来的,因为要考虑迁移到 TypeScript 上时的 OO 特性。
(文中的代码和示例的代码可能有少许不同,以示例为准,因为是之后又经过调试的。)
通过的测试环境:Windows 10, Chrome 45 / Firefox 41 / IE 11 / Edge。
未通过的测试环境(由尝鲜的杨同学提供):OSX El Capitan, Chrome 47-dev / Safari 9。
图片预览:
可以看到,应用了滤镜的矩形被像素化了,而没应用滤镜的三角形前后无变化。
话说玩过 XNA/MonoGame 的同学应该知道,其中有一个继承自 Texture2D
的类 RenderTarget2D
,使用方式大概是这样的(参数什么的从简了):
RenderTarget2D target = new RenderTarget2D(); game.SetRenderTarget(target); SomeDrawableComponents.draw(gameTime); game.SetRenderTarget(null ); DrawTheTextureToASurface(target, someSurface);
这样实现的是一个类似游戏内屏幕的效果(例如 Half Life 2 的 Breencast 在各种屏幕上的投影)。很“碰巧”地,这种方式在 Pixi 中也见到了(详情请见 Pixi 的多重滤镜机制的代码)。
来分析一下。滤镜的基本操作流程是:
绘制原始图形;
处理此图形;
拷贝到输出(可选,如果前两步直接在屏幕缓冲区完成的话)。
WebGL 中的绘图完全是由着色器根据给定的顶点、颜色、索引数据画的。像 BitBlt()
这样的缓冲区数据复制函数是不能用的。那么有没有一种方法,叫做 copyBetweenRenderTargets(from, to)
呢?没有。
这里从 WebGL 的一个着色器类型 sampler2D
,Framebuffer
的一个操作 gl.framebufferTexture2D()
和 XNA/MonoGame 的 API 可以猜想(还用得着猜吗),既然我们看的屏幕是二维的,Texture2D
是二维的,那么就利用 Texture2D
作为真正的缓冲区。(附:GDI+ 中,WinForms 封装了一个 Graphics.FromBitmap()
方法,理由类似。)其实也可以想一下,如果屏幕进入到三维(立体投影),这种绘图习惯应该会改变吧?
首先我们来看一下这次用到的三组着色器。
原始绘图着色器(顶点、片元):
attribute vec3 aVertexPosition;attribute vec4 aColor;uniform mat4 uProjMatrix;varying vec4 vColor;void main() { gl_Position = uProjMatrix * vec4 (aVertexPosition.xyz, 1.0 ); vColor = aColor; }
precision mediump float ;varying vec4 vColor;void main() { gl_FragColor = vColor; }
像素化着色器(顶点、片元):
precision lowp float ;attribute vec3 aVertexPosition;attribute vec2 aTextureCoord;attribute vec4 aColor;uniform mat4 uProjMatrix;varying vec2 vTextureCoord;varying vec4 vColor;void main() { gl_Position = uProjMatrix * vec4 (aVertexPosition.xyz, 1.0 ); vTextureCoord = aTextureCoord; vColor = vec4 (aColor.rgb * aColor.a, aColor.a); }
precision mediump float ;varying vec2 vTextureCoord;uniform vec4 dimensions;uniform vec2 pixelSize;uniform sampler2D uSampler;void main() { vec2 coord = vTextureCoord; vec2 size = dimensions.xy / pixelSize; vec2 color = floor ((vTextureCoord * size)) / size + pixelSize / dimensions.xy * 0.5 ; gl_FragColor = texture2D (uSampler, color); }
输出至屏幕时的着色器(顶点、片元):
attribute vec3 aVertexPosition;attribute vec2 aTextureCoord;uniform mat4 uProjMatrix;varying vec2 vTextureCoord;void main() { gl_Position = uProjMatrix * vec4 (aVertexPosition.xyz, 1.0 ); vTextureCoord = aTextureCoord; }
precision mediump float ;uniform sampler2D uSampler;varying vec2 vTextureCoord;void main() { gl_FragColor = texture2D (uSampler, vTextureCoord); }
可以看到,变量名统一了(aVertexPosition
、aColor
、aTextureCoord
、uSampler
),因为要在多个处理过程中用到。
其中,原始着色器的作用是按照指定的颜色和位置进行输出;像素化着色器处理;屏幕输出着色器则是做了类似拷贝的操作(直接绘制贴图),稍后在代码里可以看到是怎么结合使用的。
接下来从总绘制逻辑开始分解说明。
function render (pixelate ) { if (pixelate === undefined ) { pixelate = true ; } if (pixelate) { rawTarget.use (); rawTarget.renderElements ($_rect_vertices, $_rect_colors, $_rect_indices); pixelateTarget.use (); pixelateTarget.renderRenderTarget (rawTarget); screenTarget.use (); screenTarget.renderRenderTarget (pixelateTarget); } else { rawScreenTarget.use (); rawScreenTarget.renderElements ($_rect_vertices, $_rect_colors, $_rect_indices); } rawScreenTarget.use (); rawScreenTarget.renderElements ($_tri_vertices, $_tri_colors, $_tri_indices, false ); }
先进行最初的绘制,画出一个矩形(renderElements()
),然后由像素化滤镜在自己的缓冲区内处理(renderRenderTarget()
),最后绘制到屏幕上(renderRenderTarget()
)。
在创建上,四个 RenderTarget
指定了不同的着色器和输出方式。
function initRenderTargets ( ) { glc.disable (gl.DEPTH_TEST ); glc.enable (gl.BLEND ); glc.blendFunc (gl.SRC_ALPHA , gl.ONE_MINUS_SRC_ALPHA ); var mat4 = new Matrix4 (); mat4.setOrtho (0 , canvas.width , 0 , canvas.height , -1000 , 0 ); var rawShader = new Shader (glc, Shader .getShaderSourceFromElement (document .getElementById ("vshader-raw" )), Shader .getShaderSourceFromElement (document .getElementById ("fshader-raw" )), { "uProjMatrix" : {type : UniformType .MAT4 , value : mat4.elements } }, ["aVertexPosition" , "aColor" ] ); var screenShader = new Shader (glc, Shader .getShaderSourceFromElement (document .getElementById ("vshader-screen" )), Shader .getShaderSourceFromElement (document .getElementById ("fshader-screen" )), { "uProjMatrix" : {type : UniformType .MAT4 , value : mat4.elements }, "uSampler" : {type : UniformType .SAMPLER2D , value : 0 } }, ["aVertexPosition" , "aTextureCoord" ] ); var pixelateShader = new Shader (glc, Shader .getShaderSourceFromElement (document .getElementById ("vshader-pixelate" )), Shader .getShaderSourceFromElement (document .getElementById ("fshader-pixelate" )), { "uProjMatrix" : {type : UniformType .MAT4 , value : mat4.elements }, "dimensions" : {type : UniformType .VEC4 , value : [canvas.width , canvas.height , 0 , 1 ]}, "pixelSize" : {type : UniformType .VEC2 , value : [50 , 50 ]}, "uSampler" : {type : UniformType .SAMPLER2D , value : 0 } }, ["aVertexPosition" , "aTextureCoord" , "aColor" ] ); rawTarget = new RenderTarget (glc, canvas.width , canvas.height , rawShader); pixelateTarget = new RenderTarget (glc, canvas.width , canvas.height , pixelateShader); screenTarget = new RenderTarget (glc, canvas.width , canvas.height , screenShader, true ); rawScreenTarget = new RenderTarget (glc, canvas.width , canvas.height , rawShader, true ); g_pixelateShader = pixelateShader; }
Shader
的构造函数如下:
function Shader (glc, vshader, fshader, uniforms, attributeNames ) { }
Shader
类负责管理一个着色器实例(关联至 WebGLProgram
),以及提供对应变量的自动更新操作。这种做法学习自 Pixi。
RenderTarget
的构造函数如下:
function RenderTarget (glc, width, height, shader, isRoot ) { }
isRoot
参数控制着 RenderTarget
的行为。为 true
时表示将要直接绘制到屏幕上(缓冲区为 null
),为 false
时表示这个 RenderTarget
是一个缓冲用的 RenderTarget
,绘制操作发生在帧缓冲区(Framebuffer
)内。
其实整个绘制的过程几乎是一致的,除了:
直接绘制到屏幕上的 RenderTarget
中,frameBuffer
为 null
,texture
为 null
;
作为缓冲区的 RenderTarget
中,frameBuffer
和 texture
都不为 null
。
二者的区别在初始化时就已经决定,在绘制时两种 RenderTarget
共享绘制代码。大致如下:
glc.viewport (0 , 0 , this ._width , this ._height );this .use (); shader.use (); shader.updateUniforms ();var FSIZE = Float32Array .BYTES_PER_ELEMENT ; glc.bindBuffer (gl.ARRAY_BUFFER , verticesBuffer); glc.bufferData (gl.ARRAY_BUFFER , verticesTyped, gl.STATIC_DRAW ); glc.vertexAttribPointer (shader.getAttributeLocation ("aVertexPosition" ), 3 , gl.FLOAT , false , FSIZE * 3 , 0 ); glc.enableVertexAttribArray (shader.getAttributeLocation ("aVertexPosition" )); glc.bindBuffer (gl.ARRAY_BUFFER , colorsBuffer); glc.bufferData (gl.ARRAY_BUFFER , colorsTyped, gl.STATIC_DRAW ); glc.vertexAttribPointer (shader.getAttributeLocation ("aColor" ), 4 , gl.FLOAT , false , FSIZE * 4 , 0 ); glc.enableVertexAttribArray (shader.getAttributeLocation ("aColor" )); glc.bindBuffer (gl.ELEMENT_ARRAY_BUFFER , indicesBuffer); glc.bufferData (gl.ELEMENT_ARRAY_BUFFER , indicesTyped, gl.STATIC_DRAW );if (clear) { glc.clearColor (0 , 0 , 0 , 0 ); glc.clearDepth (0 ); glc.clear (gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ); } glc.drawElements (gl.TRIANGLES , indices.length , gl.UNSIGNED_SHORT , 0 );
this .use (); shader.use (); shader.updateUniforms ();var FSIZE = Float32Array .BYTES_PER_ELEMENT ; glc.activeTexture (gl.TEXTURE0 ); glc.bindTexture (gl.TEXTURE_2D , renderTarget.texture ); glc.bindBuffer (gl.ARRAY_BUFFER , glVertexPositionBuffer.buffer ); glc.bufferData (gl.ARRAY_BUFFER , vertexPositionsTyped, gl.STATIC_DRAW ); glc.vertexAttribPointer (shader.getAttributeLocation ("aVertexPosition" ), 3 , gl.FLOAT , false , FSIZE * 3 , 0 ); glc.enableVertexAttribArray (shader.getAttributeLocation ("aVertexPosition" )); glc.bindBuffer (gl.ARRAY_BUFFER , glTextureCoordBuffer.buffer ); glc.bufferData (gl.ARRAY_BUFFER , textureCoordsTyped, gl.STATIC_DRAW ); glc.vertexAttribPointer (shader.getAttributeLocation ("aTextureCoord" ), 2 , gl.FLOAT , false , FSIZE * 2 , 0 ); glc.enableVertexAttribArray (shader.getAttributeLocation ("aTextureCoord" )); glc.bindBuffer (gl.ELEMENT_ARRAY_BUFFER , glTextureIndexBuffer.buffer ); glc.bufferData (gl.ELEMENT_ARRAY_BUFFER , textureIndicesTyped, gl.STATIC_DRAW );if (clear) { glc.clearColor (1 , 0 , 0 , 0 ); glc.clearDepth (0 ); glc.clear (gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ); } glc.drawElements (gl.TRIANGLES , textureIndices.length , gl.UNSIGNED_SHORT , 0 );
RenderTarget.use()
方法很简单:
RenderTarget .prototype .use = function ( ) { var glc = this ._glc ; glc.pixelStorei (gl.UNPACK_FLIP_Y_WEBGL , 0 ); glc.bindFramebuffer (gl.FRAMEBUFFER , this .frameBuffer ); };
核心就在一个 gl.bindFramebuffer()
上。之前说过直接输出时 frameBuffer
为 null
,否则不为 null
。而将渲染目标切回屏幕时 bindFramebuffer()
的参数正好为 null
,于是二者在形式上就统一了。
其他的代码大多数是做绘制数据管理(创建、保存、回收)的,按照规范来就可以了。
最后提醒一下,在创建贴图的时候,要创建一个长宽都为2的整数次方的贴图,否则 WebGL 会报错说无法创建。因此在创建缓冲区(基于贴图)之前要手工调整宽高,使之都为2的整数次方。
Enjoy.