注:文章译自http://wgld.org/,原作者杉本雅広(doxas),文章中如果有我的额外说明,我会加上[lufy:],另外,鄙人webgl研究还不够深入,一些专业词语,如果翻译有误,欢迎大家指正。
WebGL与纹理
上次介绍了用点光源的光来进行补色着色的方法。
在片段着色器中对光进行计算,阴影,亮点等效果都非常的漂亮,3D场景的真实度大幅度提升。并且能和顶点颜色一起使用,理解了前面讲解的内容之后,就应该能进行比较高质量的3D渲染了。
这一次,来看高级一点的纹理的使用。所谓纹理,简单一点说,就是可以放到多边形上的图片数据,在WebGL中当然也可以使用。
WebGL和HTML不同,一般的图片类型(gif,jpg,png等)是不可以直接使用的,另外,也可以把canvas转换成纹理,总之,要变换一下方法来进行渲染。
这一次,先来看一下利用纹理的最基本的绘制方法。
WebGL中纹理的限制
上面说了,在HTML中使用的一般类型的图片变换成纹理之后,就可以在WebGL中使用了。意思就是说要把网页中使用的图片数据变成WebGL中可以使用的形式。
但是,WebGL中的纹理需要注意一点,所使用的图片数据的大小必须是2的阶乘,横竖的像素长度大小必须是32x32,128x128等2的阶乘的形式。
当然,做一些处理的话,不是2的阶乘的图片数据也是可以用的,但是基本上作为纹理使用的图像数据的大小必须是2的阶乘。
另外,看一下普通的网页就能感觉到,网页上的图片数据的读取是要花一点时间的,在进行纹理转换的话,必须是在图片读取完之后才行,这里需要做一些特殊的处理,如果对javascript不太熟悉的话可能会无从下手,这个后面会说。
纹理的生成和使用
那么,下面就开始说一说纹理使用的步骤吧。
纹理在WebGL中要使用纹理对象来处理,生成纹理对象需要使用createTexture函数。
>createTexture的使用例子
var tex = gl.createTexture();
这个函数没有参数,只是单纯的返回一个纹理对象,经过上面的代码,变量tex就是一个空的纹理对象了。
生成纹理对象之后,接着要把纹理对象和WebGL进行绑定。
大家想一想,之前在WebGL中使用缓存对象的时候也是需要进行和WebGL的绑定处理,比如使用VBO的时候,这次也和缓存一样,要进行绑定处理。要对纹理数据进行操作的时候,首先必须先进行绑定,然后使用操作纹理的一些函数,被绑定的纹理对象才能适用这些处理。
把纹理数据和WebGL进行绑定的函数是bindTexture。
>bindTexture的使用例子
gl.bindTexture(gl.TEXTURE_2D, tex);
这个函数需要两个参数。第一个参数是纹理的种类,绘制2D的图像类型的话,通常使用gl.TEXTURE_2D作为参数。第二个参数就是要绑定的纹理对象,这里还是挺简单的吧。
虽然将纹理对象和WebGL进行了绑定,但是还没有把最核心的图像数据加进来,将图像数据和纹理进行连接的是texImage2D函数。
>texImage2D的使用例子
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
看了上面的代码,一定会有”这是什么东东?“的感觉吧,这个函数一共接收六个参数,看起来复杂,其实挺简单的。
第一个参数是bindTexture的时候也使用过的纹理的类型,这里也使用gl.TEXTURE_2D就行了。第二个参数纹理映射的等级,暂时不用考虑,设定成0就行了。接着,第三个参数和第四个参数中都指定了gl.RGBA,现阶段先不用考虑,直接这么用就行了。同样,第五个参数也是没什么特别的利用的话,先指定gl.UNSIGNED_BYTE就可以了。主要是,现阶段第一个到第五个参数先像上面这样设置就没问题了,最重要的是第六个参数,这里需要指定图像数据,这个第六个参数中的图像数据在这个时候分配给绑定的纹理。
图片读取时间的考虑
刚才也说了,使用texImage2D函数可以把图片数据接分配给纹理,但是网页中的图片的读取是要花一点时间的。
这里需要注意的是,texImage2D函数被调用的时候,必须是图片已经读取完了之后,如果在图片读取完之前调用texImage2D,则无法正确的将图片数据分配给纹理。。
这里需要在图片读取完后的事件中来调用。
具体流程,就是先用javascript来生成图片对象,图片对象有onload事件可以监测到图片读取完成。在这里把纹理相关的处理做完,最后给图片对象指定图片地址开始读取图片。
这里比较重要的一点是,在图片开始读取之前,要先添加onload事件,在事件中添加纹理相关的处理,这样,图片读取完成之后就可以自动进行纹理相关的这些处理了。
上面说的这些处理,写成函数的话,就是下面这样。
>纹理的生成函数
function create_texture(source){ // イメージオブジェクトの生成 var img = new Image(); // データのオンロードをトリガーにする img.onload = function(){ // テクスチャオブジェクトの生成 var tex = gl.createTexture(); // テクスチャをバインドする gl.bindTexture(gl.TEXTURE_2D, tex); // テクスチャへイメージを適用 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); // ミップマップを生成 gl.generateMipmap(gl.TEXTURE_2D); // テクスチャのバインドを無効化 gl.bindTexture(gl.TEXTURE_2D, null); // 生成したテクスチャをグローバル変数に代入 texture = tex; }; // イメージオブジェクトのソースを指定 img.src = source; }
这个自定义函数create_texture接收一个图片对象的图片地址作为参数。函数中,首先生成图片对象,在图片数据读取之前,先添加了onload事件,在事件中,有纹理的生成,绑定,以及分配图片数据等处理。在函数中,texImage2D函数执行之后,应该会感觉有点奇怪吧,为了生成纹理映射,使用了generateMipmap函数。
纹理映射是一个提前准备一些不同大小的图片数据的组织,不光是在WebGL中,在所有的3D编程中都有这个概念。纹理映射,准备一个纹理使用的场景,在纹理图像需要缩小显示的时候能够发挥很大的作用,因为缩小后的图像数据已经在内部提前准备好,然后进行适当的切换渲染,所以即使把图像缩的再小,也能渲染的很漂亮。
执行了generateMipmap函数之后,就能生成纹理映射了,这个函数的参数和bindTexture一样,也是gl.TEXTURE_2D。
和VBO一样,WebGL中同一时间只能绑定一个纹理,所以最后要解除绑定。在最终生成纹理对象的时候,使用了全局变量,因为onload没有办法返回自己本身。上面的例子,变量texture必须是create_texture函数能够参考到的空间内。
onload的对应结束之后,最后给图片对象指定图片地址,因为已经加了onload事件,图片读取完成之后会自动调用onload事件,执行生成纹理的代码。
纹理坐标和顶点的属性
好了,已经知道了纹理对象的生成的方法了,那么重要的就是如何把纹理放到多边形中了。
要把纹理放到多边形中,在生成多边形的时候,就需要包含将纹理如何放到多边形中这样的信息。所以需要给顶点添加新的顶点属性。
在前面的文章(九,顶点缓存的基础)中已经详细介绍了,向顶点中添加信息需要使用新的VBO。那么这一次新添加的VBO中需要保存顶点的纹理坐标。纹理坐标就是为了表示使用纹理的哪个坐标。纹理坐标使用的范围是0 ~ 1,而且有横竖两个方向。所以,表示纹理坐标的时候类似于(0.0, 0.0)这样,需要两个元素。(lufy:翻译的有点绕嘴,看后面的使用部分就明白了。)
而且,这里有些奇怪的地方,一般,图片数据的坐标系是以左上为原点来考虑的,如下图所示。
从左上角的原点,向右以及向下来对应X和Y的值的大小。
而WebGL中的纹理坐标系则是像下面这样。
坐标系上下颠倒了一下。就是说纹理坐标系中,左下是原点,纵方向上的数值越向上表示越大。但是,对照两个图看一下就知道了,指定纹理坐标的时候也不需要考虑太多。为什么呢?因为图片也是上下翻转的,所以和以左上角为原点的图片一样考虑就行,结果是一致的。
现在的阶段,只需要知道纹理空间上坐标系是上下翻转的就可以了。*以后使用纹理进行一些特殊的处理的时候才需要详细了解相关的知识。
javascript 的修正
接着,为了在程序中使用纹理,来修改一下代码吧。
这次没有任何光照效果,渲染的只是带有图像的多边形而已。虽然用圆环体也可以,但是还需要处理一些细节部分,所以还从多边形模型来开始介绍。
首先,准备模型的顶点数据,刚才也说了,为了保存纹理坐标,要添加新的顶点属性,而因为不需要光照效果,所以法线等情报这次也不使用。
>顶点数据的准备
// attributeLocationを配列に取得 var attLocation = new Array(); attLocation[0] = gl.getAttribLocation(prg, ‘position‘); attLocation[1] = gl.getAttribLocation(prg, ‘color‘); attLocation[2] = gl.getAttribLocation(prg, ‘textureCoord‘); // attributeの要素数を配列に格納 var attStride = new Array(); attStride[0] = 3; attStride[1] = 4; attStride[2] = 2; // 頂点の位置 var position = [ -1.0, 1.0, 0.0, 1.0, 1.0, 0.0, -1.0, -1.0, 0.0, 1.0, -1.0, 0.0 ]; // 頂点色 var color = [ 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 ]; // テクスチャ座標 var textureCoord = [ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0 ]; // 頂点インデックス var index = [ 0, 1, 2, 3, 2, 1 ]; // VBOとIBOの生成 var vPosition = create_vbo(position); var vColor = create_vbo(color); var vTextureCoord = create_vbo(textureCoord); var VBOList = [vPosition, vColor, vTextureCoord]; var iIndex = create_ibo(index); // VBOとIBOの登録 set_attribute(VBOList, attLocation, attStride); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iIndex);
定义了一个四个顶点的四边形。详细看一下保存顶点的位置的部分的话就明白了,是以四边形的中心为原点,顶点顺序和写字母Z的顺序一样,顶点颜色定义成了不透明的白色。
纹理坐标和之前说的一样,定义了包含横竖的两个元素。
顶点数据是用一个数组来表示的,和之前一样生成VBO和IBO。这和之前所作的是完全一样的。这样,顶点相关的处理,也就是说着色器内用attribute变量处理的数据的准备都已经ok了。
这次因为不进行光照处理,所以准备好坐标变换矩阵就可以处理顶点了。逆矩阵和光源的位置等都是不需要的。
但是考虑一下uniform修饰符的变量的话,需要增加一个,uniform修饰符定义的变量是指全部顶点都进行一致处理的数据,所以,作为所有顶点都同样被使用的纹理数据,就必须使用uniform变量来传递数据了。
>uniform相关处理
// uniformLocationを配列に取得 var uniLocation = new Array(); uniLocation[0] = gl.getUniformLocation(prg, ‘mvpMatrix‘); uniLocation[1] = gl.getUniformLocation(prg, ‘texture‘);
这次使用的uniform变量有两个,一个是为了处理坐标变换矩阵,另一个是为了提交纹理数据。
设置纹理有效
纹理中有单位这个概念,是为了给纹理设置编号从而管理纹理的东西,默认是将0号的纹理单位设置为有效。纹理单位在处理多个纹理的时候可以发挥作用,这次只是使用一个纹理,所以就使用默认的0号的单位就可以了。
要将特定的纹理单位设置为有效使用的是activeTexture函数。
>纹理单位有效化
// 有効にするテクスチャユニットを指定 gl.activeTexture(gl.TEXTURE0);
这里作为参数使用的gl.TEXTURE0常量,后面的0就是纹理单位的编号,如果将编号1的纹理设置为有效的话,就需要是gl.TEXTURE1。但是,没有什么特殊的理由的话,使用纹理单位应该按照从小到大的顺序来使用。
>>纹理单位的最大值(上限值) |
多个纹理同时使用的时候,纹理单位是必须用到的,这个最大的单位数是由运行环境决定的。因为运行WebGL的除了电脑,还有手机等,所以纹理单位能使用到多少个判断起来是非常费劲的。 |
因为是受到硬件的性能的制约,所以使用之前先判断一下,然后进行分别处理是可行的。查询执行环境的可使用最大纹理单位数使用getParameter函数。 |
下面是例子 |
gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); |
向getParameter函数中传入这个非常长的常量gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS就可以得到一个整数值,表示可以使用的最大文理单位数,如果返回值是10的话,那么可以使用的纹理单位就是gl.TEXTURE0 ~ gl.TEXTURE9。 |
向着色器中传入纹理数据
将相应的纹理单位设置为有效之后,接着就需要将纹理和WebGL进行绑定处理了,这个在将图片数据转换成纹理数据的时候也做过了,所以简单如下。
>纹理和WebGL绑定
// テクスチャをバインドする gl.bindTexture(gl.TEXTURE_2D, texture);
绑定了纹理情报要传送给着色器,所以需要用uniform变量来处理,这里还使用之前的uniformLocation,处理如下。
>将纹理数据传给着色器
// uniform変数にテクスチャを登録 gl.uniform1i(uniLocation[1], 0);
这里需要注意的是,和向着色器中传入矩阵和向量不同,因为需要传入纹理单位的编号。uniform1i函数是向着色器中传入一个整数的时候使用的。第二个参数就是要向着色器中传入的整数0。也就是说,这里传入的整数是和之前有效化的纹理单位相一致的。
着色器修改
接着,是着色器的修改了,首先是顶点着色器。
>顶点着色器代码
attribute vec3 position; attribute vec4 color; attribute vec2 textureCoord; uniform mat4 mvpMatrix; varying vec4 vColor; varying vec2 vTextureCoord; void main(void){ vColor = color; vTextureCoord = textureCoord; gl_Position = mvpMatrix * vec4(position, 1.0); }
顶点着色器中,顶点的位置,顶点的颜色,还有顶点的纹理坐标都是用attribute修饰符定义的。顶点的颜色和纹理坐标没有做任何处理,都是直接传给片段着色器的。
顶点着色器相关的处理没什么难的地方,接着看片段着色器。
>片段着色器代码
precision mediump float; uniform sampler2D texture; varying vec4 vColor; varying vec2 vTextureCoord; void main(void){ vec4 smpColor = texture2D(texture, vTextureCoord); gl_FragColor = vColor * smpColor; }
片段着色器使用uniform修饰符来接收纹理数据。注意这个sampler2D的变量类型,就是采样的意思,先把它当作纹理数据考虑就行了。
另外,还使用了一个texture2D函数,这个函数有两个参数,第一个参数是采样型的纹理数据,第二个参数是表示纹理坐标的vec型的数据。
这次的例子,顶点着色器中用attribute定义的顶点的纹理坐标,在片段着色器中用varying变量来接收,然后在片段着色器一侧将这个纹理坐标,传入到texture2D函数中。
这样,用texture2D函数获取到纹理的颜色信息之后,再和顶点颜色(varying变量vColor)相乘,就得到最终的颜色了。
总结
纹理的使用方法,用了双倍的时间来讲解了,应该理解了吧。
纹理周边的处理是非常冗长的,但是主要的就是纹理坐标,纹理对象,以及为了处理这些数据的着色器的对应,纹理中单位这个概念,使用合理的操作可以处理多个纹理等。
虽然这次只是最基本的部分的封装,但是变更点非常的多,所以最后会贴出全部代码(lufy:我就不贴了,大家直接用浏览器查看就行了。),另外,最后给出了运行demo。
下次,介绍一下多个纹理的使用。
四边形中使用纹理的demo
转载请注明:转自lufy_legend的博客http://blog.csdn.net/lufy_legend