本章节建立我们基本上掌握了如何在2D或者3D平面中绘制图形的基础上(当然不了解2D和3D平面绘制图片也可以往下阅读),我们在2D平面中,可以用webgl去绘制我们所需要的图案,但实际中我们用到某个图形时不可能全部一一的去绘制,纹理贴图解决了这个问题,我们可以用已有的图片来填充webgl中的图形。
- 纹理基础
- 2D图形的纹理贴图
- 多纹理单元的纹理贴图
- 不同纹理间实现转场特效
原文的地址来自我的博客:https://github.com/forthealll...
这个系列的源码地址为:源码的地址为: https://github.com/forthealll...
在了解纹理贴图之前,我们先来了解一下预备知识,首先什么是纹理贴图或者说纹理映射呢?
将一张图像贴到一个几何体的表面
这就是纹理贴图的含义,这张图像我们就成为纹理,这个工作我们就称为纹理贴图,其本质就是提取图像中的颜色,然后对应的赋予给几何平面的某个位置,从而将图像在几何体表面完成渲染。
这里从纹理(图像)到几何体表面之间有一个映射,为了了解这个映射是怎么发生的,我们必须了解一下纹理坐标和裁剪坐标。
裁剪坐标
webgl中的裁剪面坐标如下所示:
从上述裁剪坐标系统的示意图中我们可以看出,webgl中整个裁剪平面的中心坐标是(0,0),对于二维的裁剪平面而言其水平方向从(-1,0)到(1,0),其竖直方向从(0,-1)到(0,1).
也就是说其裁剪坐标任何方向的值在区间[-1,1]内。
注意一点:裁剪平面是决定如何映射到画布上,因为画布是二维的,因此裁剪平面也是二维的。webgl中z轴的坐标并不限制在(-1,1),可以为任何值。
纹理坐标
纹理坐标跟裁剪坐标不一样,其值不是从[-1,1]而是从[0,1]:
在贴图过程中,我们需要使裁剪坐标中的顶点坐标,与纹理坐标系统点一一对应。也就是如何截取纹理坐标中的纹理,贴到几何图形中。
需要注意的是,图片本身的坐标与纹理的坐标是左右相等,上下相反的,因此,存在一个上下方向的坐标变换。在webgl中的纹理 中,通过:
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1)
来反转Y轴上的方向。
禁用Chrome的安全性检测
本文的例子中,直接本地通过file文件打开html,如果在这个html文件中有ajax跨域请求,那么因为浏览器的安全性检测,会提示如下信息的错误:
Cross origin requests are only supported for HTTP....
当然如果启动一个本地server就不会有影响。这里有种懒人解决方案,就是通过禁用安全性检测的方法,以mac为例,从命令行窗口中启动chrome,启动命令为
open /Applications/Google\ Chrome.app --args --allow-file-access-from-files
此外,也可以安装http-server,快速在本地启一个server.
下面我们以2D几何图形的纹理贴图来介绍一下,在webgl中如何实现纹理贴图。
function createTexture (gl, filter, data, width, height) { let textureRef = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, textureRef); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter); 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, data); return textureRef; }
上述就是一个创建纹理缓存的例子,通过gl.createTexture()创建一个纹理缓存,并关联系统变量gl.TEXTURE_2D. 此外,因为在纹理的渲染中,在Y轴方向与图片是完全相反的,因此如果需要正相呈现纹理渲染结果,比如进行Y轴方向的反转,即gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1)。
此外我们可以通过gl.texParameteri函数,指定当图片纹理大于渲染区域,或者图片纹理小于渲染区域时,如何去正确的渲染。
最后通过:
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
提取data中的元素保存到纹理缓存中,data可以是image,也可以是video,甚至是另一个canvas的渲染结果。也就是说,webgl的纹理贴图的源,不仅仅可以是图片,还可以是视频的一帧,甚至是另一个cavans。
let image = new Image() image.src = './cubetexture.png' image.onload = function(){ let textureRef = createTexture(gl,gl.LINEAR,image1); gl.activeTexture(gl.TEXTURE0) gl.bindTexture(gl.TEXTURE_2D,textureRef) gl.uniform1i(u_Sampler, 0); // texture unit 0 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }
上述代码加载image图片,在onload的时候,通过之前定义的createTexture创建纹理,然后激活使用0号纹理单元,webgl可以同时支持多个纹理单元,可以同时将多个纹理渲染到同一个可视区。激活纹理单元后,我们将0传递给着色器中的取样器变量u_Sampler.
最后我们在片元着色器中接受纹理单元编号,以及纹理坐标,最后贴图渲染到渲染区:
uniform sampler2D u_Sampler; //取色器变量 varying lowp vec2 v_TexCoord; //纹理坐标 void main() { gl_FragColor = texture2D(u_Sampler,v_TexCoord); }
我们最终得到了在所指定的区域,渲染了正确的一张图片,渲染结果如下所示:
前面说到webgl可以同时支持多个纹理单元,webgl可以同时处理多幅纹理,纹理单元就是为了这个目的而设计的。在上述的例子中,我们只用了一个纹理单元,将一张纹理渲染到了渲染区。接下来我们尝试使用两个纹理单元,将两张图片,渲染到同一个区域。
<div align=center>
<img src="https://user-images.githubusercontent.com/17233651/80330488-df79ab00-8877-11ea-8f2c-8faac87835de.png" width=256 height=256 /><br/>
纹理图片1
</div>
<div align=center>
<img src="https://user-images.githubusercontent.com/17233651/80331074-a6423a80-8879-11ea-9251-07a2b0323f1f.png" width=256 height=256 /><br/>
纹理图片2
</div>
我们使用webgl中国的两个纹理单元,分别为gl.TEXTURE0和gl.TEXTURE1,修改代码如下。
首先是加载和渲染的逻辑:
let textures = [] image.onload = function(){ let textureRef = createTexture(gl,gl.LINEAR,image); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textureRef) textures.push(textureRef) } image.src = './cubetexture.png' let image1 = new Image(); image1.onload = function(){ let textureRef1 = createTexture(gl,gl.LINEAR,image1); gl.activeTexture(gl.TEXTURE1) gl.bindTexture(gl.TEXTURE_2D,textureRef1) textures.push(textureRef1) } image1.src = './img.png' //激活纹理单元并且绘制 gl.uniform1i(u_Sampler, 0); // texture unit 0 gl.uniform1i(u_Sampler1, 1); // texture unit 1 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[0]); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, textures[1]);
最后修改片元着色器:
uniform sampler2D u_Sampler; uniform sampler2D u_Sampler1; void main(){ gl_FragColor = texture2D(u_Sampler,v_TexCoord) + texture2D(u_Sampler1,v_TexCoord); }
最后的渲染结果为:
<div align=center>
<img src="https://user-images.githubusercontent.com/17233651/80331830-ba873700-887b-11ea-9784-d01b9dc2274d.png" width=256 height=256 /><br/>
最后双纹理单元合成渲染结果
</div>
完成的代码 地址为:https://github.com/forthealll...
通过使用多纹理单元,我们可以实现图片间的转场效果,首先我们来看什么是转场效果。
转场效果顾名思义,就是从图片A切换到图片B之间的动画特效,类似与我们在做PPT的时候,一些淡入淡出等等动效。在webgl中,渲染的纹理本质也是图片,因此可以在不同纹理间,按时间顺序控制不同纹理的渲染结果,可以实现转场的效果。
<img src="https://camo.githubusercontent.com/c42ecc6197b0f51a106fb50723f9bc6d2e1f925c/687474703a2f2f692e696d6775722e636f6d2f74573331704a452e676966" /><img src="https://camo.githubusercontent.com/7e34cd12d5a9afa94f470395b04b0914c978ce01/687474703a2f2f692e696d6775722e636f6d2f555a5a727775552e676966" /><img src="https://camo.githubusercontent.com/0456d4ed8753fbce027f1174dc8b22da548eeade/687474703a2f2f692e696d6775722e636f6d2f654974426a33582e676966" />
如上所示的动图就是列出了一些转场特效。
我们以同样前面的两张图片为例,研究一下图片或者说纹理间的转场效果。
直接看片元着色器中的代码:
vec4 transition (vec2 uv) { float time = progress; float stime = sin(time * PI / 2.); float phase = time * PI * bounces; float y = (abs(cos(phase))) * (1.0 - stime); float d = uv.y - y; return mix( mix( getToColor(uv), shadow_colour, step(d, shadow_height) * (1. - mix( ((d / shadow_height) * shadow_colour.a) + (1.0 - shadow_colour.a), 1.0, smoothstep(0.95, 1., progress) // fade-out the shadow at the end )) ), getFromColor(vec2(uv.x, uv.y + (1.0 - y))), step(d, 0.0) ); }
我们看到这个transition函数就是一个转换函数,决定了随着时间,如何切换纹理,从而进行渲染,我们通过getFromColor和getToColor指定了两个不同纹理单元的纹理:
vec4 getToColor(vec2 uv){ return texture2D(u_Sampler,uv); } vec4 getFromColor(vec2 uv){ return texture2D(u_Sampler1,uv); }
最后片元着色器的颜色就是调用transition函数后的结果,在调用transition的时候我们传入了纹理坐标。
void main() { gl_FragColor = transition(v_TexCoord); }
最后要想要渲染结果动起来,就必须写一个定时器,动态改变transition中的参数progress,是其从0到1的变化。
setInterval(()=>{ if(textures.length === 2){ if(i >= 1){ i = 0.01 } gl.uniform1i(u_Sampler, 0); // texture unit 0 gl.uniform1i(u_Sampler1, 1); // texture unit 1 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, textures[0]); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, textures[1]); let progress = gl.getUniformLocation(shaderProgram,'progress') gl.uniform1f(progress,i) i += 0.05 gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); } },100)
最后的渲染结果我们可以看到的动画结果如下:
完成的代码 地址为:https://github.com/forthealll...
此外,在https://github.com/gl-transit... 上收录了各色各样的转场效果,转场不仅仅可以应用于图片,还可以应用于视频的不同帧间,下篇文章将具体讲讲如何实现在webgl中渲染video,以及video的帧间动画。