WebGL基础学习篇(Lesson 7)

文章推薦指數: 80 %
投票人數:10人

What is texture mapping. 纹理从字面上理解就是使用图片渲染在物体的表面上。

正如下面的图所示:. 使用目前我们已 ... Skiptocontent {{message}} fem-d / webGL Public Notifications Fork 43 Star 166 Code Issues 0 Pullrequests 0 Actions Projects 0 Wiki Security Insights More Code Issues Pullrequests Actions Projects Wiki Security Insights Permalink master Branches Tags Couldnotloadbranches Nothingtoshow {{refName}} default Couldnotloadtags Nothingtoshow {{refName}} default webGL/blog/WebGL基础学习篇(Lesson7).md Gotofile Gotofile T Gotoline L Copypath Copypermalink Thiscommitdoesnotbelongtoanybranchonthisrepository,andmaybelongtoaforkoutsideoftherepository.     Cannotretrievecontributorsatthistime WebGL基础学习篇(Lesson7) 大纲 Whatistexturemapping Creatinganduploadingatexture Usingtexturecoordinates Usingtexturesinshader Texturefiltermodes NEAREST LINEAR Mipmapping NEAREST_MIPMAP_NEAREST LINEAR__MIPMAP_NEAREST NEAREST_MIPMAP_LINEAR LINEAR_MIPMAP_LINEAR Generatingmipmaps Texturewrapping CLAMP_TO_EDGE REPEAT MIRRORED_REPEAT Usingmultipletextures 例子讲解 Cubemaps 例子重点讲解 464lines(249sloc) 22.8KB Raw Blame Editthisfile E OpeninGitHubDesktop OpenwithDesktop Viewraw Viewblame WebGL基础学习篇(Lesson7) 目前我们已经学会在场景中添加位置(geometry),点颜色(vertexcolors)以及光照(lighting);但是这对于我们想达到的还远远不够。

如果能往场景上添加更多的“绘制(paint)”而不需要更多的位置信息岂不是更好?我们可以通过纹理(texturemapping)来实现。

在这章中,我们会使用纹理来使得我们的场景更加生动。

大纲 如何创建纹理 如何在渲染时使用纹理 过滤(filter)和包裹(wrapping)模式以及它们如何影响纹理 多纹理(Multi-texturing) 为正方体加纹理 Whatistexturemapping 纹理从字面上理解就是使用图片渲染在物体的表面上。

正如下面的图所示: 使用目前我们已学到的知识来做这样一个场景会变得相当复杂。

图中WebGL的logo必须通过许多小三角仔细组合才能创建出来。

虽然技术上也是可以的,但是额外的位置信息会随着场景的复杂而急剧上升。

幸运的是,纹理使得上面的工作变得简单。

我们需要的仅仅是一个规定格式的WebGLlogo,一个vertex属性,以及额外的几行渲染器代码。

Creatinganduploadingatexture 首先,由于多方面的原因浏览器在读取纹理时是“由上往下”的顺序,这要是OpenGL中的方式。

因此,在WebGL中我们需要在texture上颠倒Y坐标轴。

我们可以使用以下的代码实现: gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,true); 是否启用由你决定,但是本章中会一直启用。

创建纹理的方式和我们创建vertexbuffer或者indexbuffer的方式非常类似。

我们可以这样创建纹理: vartexture=gl.createTexture(); 纹理,和缓存类似,必须在操作前绑定。

gl.bindTexture(gl.TEXTURE_2D,texture); 第一个参数是我们需要绑定的纹理类型(或者说是纹理绑定地址)。

现在我们主要讲2D纹理,更多的类型我们会在后面讲到。

绑定了纹理后,我们可以加上图片信息。

最简单的方法是将DOM图片传入textImage2D中,如下面所示: varimage=document.getElementById("textureImage"); gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,image); 图片的DOM元素作为最后一个参数传入texImage2D中。

当texImage2D被调用时,WebGL会根据传入的图片尺寸自动判断大小。

其他的参数告诉WebGL图片的信息以及如何存储它。

一般来说你需要关心的只有第三个和第四个参数,当然在没有透明信息是也可以只使用gl.RGB。

对于图片,我们还需要告诉WebGL如何在渲染时过滤纹理。

我们稍后马上会告诉大家什么是过滤以及不同过滤模式的区别,现在先让我们看一个最简单的用例: gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST); 最后,如同buffers一样,我们要养成解绑的操作,我们可以这样: gl.bindTexture(gl.TEXTURE_2D,null); 当然我们不希望太多的图片在页面中显示出来,为此,我们只需要创建image对象就行了。

vartexture=gl.createTexture(); varimage=newImage(); image.onload=function(){ gl.bindTexture(gl.TEXTURE_2D,texture); gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,image); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST); gl.bindTexture(gl.TEXTURE_2D,null); } image.src="textureFile.png"; OK,我们总结一下创建纹理的步骤: 创建一个新的texture对象 将它绑定起来 传递图片信息 设置过滤模式或其他纹理参数 解绑 当我们确定不再需要某个texture对象时,我们可以释放它以及它占用的内存: gl.deleteTexture(texture); Usingtexturecoordinates 现在我们准备好了纹理,接下来需要将它作用到物体上。

接下来的问题是我们怎么控制在不同的面上显示不同的纹理。

这里我们需要用到另外一个叫做texturecoordinates的vertex属性。

纹理坐标(Texturecoordinates)是一个由两个浮点数组成的向量数组,它描述了纹理的坐标。

稍微不同的是,WebGL强制规定了坐标的范围为[0,1],也就是左上角的点为[0,0],右下角的点为[1,1],如图所示: 这样的话,如果你想对应到纹理中心的话,纹理坐标就是[0.5,0.5]。

这个坐标系对于长方形的纹理同样适用。

这种方式虽然一开始会觉得奇怪。

但是以特定的点来定位显然比通过宽高来定位简单多了。

试想你现在适用了一个很高的图片来作为纹理,但是由于图片过大或过高的原因需要改小,适用像素你必须修改对象的像素值,但是使用[0,1]就不需要了。

判断出物体(特别是复杂的物体)对应的纹理坐标是创建3D资源最有技巧的部分之一,幸运的是有许多3D建模工具为woman提供了丰富的功能。

这个过程叫做三维重建(unwrapping)。

和vertexposition用X、Y、Z代表坐标轴一样,texturecoordinates也用符号表示。

不幸的是,在不同的3D软件中这些符号并不一致。

OpenGL使用S和T表示,DirectX使用U和V。

因此你可能会发现有人使用“UVs”来代表texturecoordinates,使用“UVMapping”来表示三维重建(unwrapping)。

这篇文章中我们会使用ST来表示。

Usingtexturesinshader 纹理坐标以相同的方式在渲染器中被使用。

我们需要定义一个二维向量来存储: attributevec2aVertexTextureCoords; 和其他vertexattribute一样,我们需要将前面说到的纹理坐标通过这个属性传递进去。

另外,我们还需要在fragmentshader中添加一个我们此前没有见过的uniform:sampler2D。

这个属性允许我们访问shader中的纹理信息。

uniformsampler2DuSampler; 在之前的例子中,我们总是往unifrom中放入我们想要的值,像光线颜色等。

Samplers则不一样。

现在让我们看看如何将纹理和sampleruniform关联起来: gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,texture); gl.uniform1i(Program.uSampler,0); 首先,我们激活了对应的texture。

WebGL支持多纹理(后面会讲到),因此定义纹理时加上序号是个不错的选择。

然后,我们绑定要使用的texture,这时texture就和TEXTURE0关联起来了。

最后,我们告诉渲染器使用TEXTURE0。

做完这样,我们需要为在fragmentshader中使用纹理做准备。

最简单的方式是将texture作为最终颜色传递过去: gl_FragColor=texture2D(uSampler,vTextureCoords); texture2D将sampleruniform和坐标作为参数,返回一个基于坐标的纹理颜色,即一个四维向量。

即使图片没有alpha通道,它依然会返回一个alpha为1的四维向量。

说了这么多理论,我们来看一个实例: 最简单的纹理 Texturefiltermodes 现在我们已经知道了纹理如何在fragmentshader中使用,但是还仅限于最基本的使用场景,在更复杂的情况下仍然有许多问题。

拿刚才的demo为例,当我们放大时,我们会发现WebGLlogo边缘会出现很多锯齿,当logo很小时也会有这样的问题。

那么这些锯齿是怎么出现的呢? 想想上一章中我们讲到的vertexcolors会做插值(interpolate),因此fragmentshader得到的会是一个平稳的颜色过渡。

纹理坐标也是使用相同的方式,纹理坐标与屏幕像素对应。

在最理想的情况下,它们会是1比1对应上的。

但是,在真实情况下,纹理一般都不在本来的位置上显示。

比如说放大和缩小,这都是纹理比屏幕有更低或更高的分辨率。

当一个纹理被放大或缩小时,texturesampler会返回什么颜色就是有歧义的了。

考虑一下这种情况,纹理被稍稍放大了: 决定左上角或者正中的颜色很容易,但是马赛克(texels)之间的颜色呢?这都是由你的过滤模式决定的。

我们可以通过纹理过滤控制纹理取样的方式以达到我们想要的结果。

设置纹理过滤非常简单,我们在前面的例子里已经看过了。

gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST); 和WebGL中大多数方法一样,texParamteri会操作当前绑定的纹理,而且每个纹理都必须设置。

这就是说不同的纹理可以有不同的过滤方式。

在这个例子中,我们同时将放大过滤器(TEXTURE_MAG_FILTER)和缩小过滤器(TEXTURE_MIN_FILTER)设置为了NEAREST。

这第三个参数可以传递不同的值,最快理解的方法就是实际看一下。

texturefilter属性 现在让我们深入学习下每种过滤方式的工作原理。

NEAREST 使用NEAREST过滤器的纹理始终会返回最接近样本点的马赛克(texel)中心点的颜色。

在这种模式下纹理会呈现出块状像素化,适合“复古(retro)”的图像。

NEAREST在MIN和MAG过滤器中都适用。

LINEAR LINEAR过滤器返回以样本点为中心四个方向上像素的平均值。

这会形成一个逐渐过渡的颜色,这效果通常也是更理想的。

但是这也意味着显卡需要消耗四倍的工作量来完成二个工作,所以它比NEAREST要慢,幸运的是,现在显卡速度很快,我们几乎察觉不到。

LINEAR在MIN和MAG过滤器中都适用。

这个过滤器也被叫做bilinearfiltering。

现在让我们看看两者的差异(放大的情况下): NEARESTfilter LINEARfilter Mipmapping 在讨论其他只适用于TEXTURE_MIN_FILTER过滤方式之前,我们需要先引入一个新概念:mipmapping。

当需要渲染缩小的样本时,我们会遇到这样一个问题:即使是使用LINEAR过滤,样本点也会因为离的太远而损失精度。

下面的图显示了这个问题,右边是将缩小的正方体放大400倍的演示图,这样我们能更清楚的发现问题。

为了避免这种情况,显卡需要实现mipmapchain。

mipmaps是一系列缩小的纹理副本,每一个副本都是前者的一半大小。

如果将这些纹理以一行的形式展示,那么就是: 这样做的好处是,当渲染时显卡会选择最接近当前尺寸的纹理。

但是,mipmapping只在某些纹理过滤时能被使用。

TEXTURE_MIN_FILTER就是其中一种。

NEAREST_MIPMAP_NEAREST 这种过滤方式会选取最接近屏幕上纹理的mipmap,并且使用NEAREST来实现。

LINEAR__MIPMAP_NEAREST 这种过滤方式会选取最接近屏幕上纹理的mipmap,并且使用LINEAR来实现。

NEAREST_MIPMAP_LINEAR 这种过滤方式会选取两个最接近的mipmap,并且使用NEAREST来实现,最后的颜色会是两种样本的平均值。

LINEAR_MIPMAP_LINEAR 这种过滤方式会选取两个最接近的mipmap,并且使用LINEAR来实现,最后的颜色会是两种样本的平均值。

这也叫做三线性过滤(trilinearfiltering)。

在所有的MIPMAP过滤模式中,NEAREST_MIPMAP_NEAREST是最快但质量最低的,而LINEAR_MIPMAP_LINEAR则是最慢但质量最高的,另外两种则徘徊其中。

在大多数情况下,性能的损失并不是那么重要,因此尽量使用LINEAR_MIPMAP_LINEAR。

Generatingmipmaps WebGL并不自动为每个纹理创建mipmap,因此如果你要使用,你需要先创建它们。

幸运的是,这很简单: gl.generateMipmap(gl.TEXTURE_2D); generateMipmap必须在texImage2D之后使用,并且会自动创建完整的mipmap链。

如果你想手动提供mipmaps,你需要在调用texImage2D时主动传入一个非0的参数作为函数的第二个参数。

gl.texImage2D(gl.TEXTURE_2D,1,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,mipmapImage); 这里我们手动的创建了第一级的mipmap,它只有原图的一半大小。

第二级只有四分之一大小,以此类推。

这在一些高级功能下非常有用,或者是处理一些无法在generateMipmap中使用的被压缩图片。

为了使纹理在mipmap中被使用,我们还需要遵守一些尺寸限制。

如字面上说的,纹理的宽高必须是2的幂次方,比如16px,32px,64px等。

还要注意的是,宽高必须要相等,所以512*128的纹理也是可以的。

非2幂次方的纹理依然可以在WebGL中使用,但是只能用于NEAREST和LINEAR过滤器中。

Texturewrapping 在前面的章节中,我们使用了texParameteri来设置纹理的过滤方式,但是正如它的名字所表示的,这并不是它的所有功能。

另外一种我们可以操作的方式叫做纹理包装(texturewrapping)。

纹理包装描述了在0到1范围外的纹理坐标的行为。

这种方式独立于ST坐标系,使用它需要调用两个方法: 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); 这里我们在两个方法中都绑定了CLAMP_TO_EDGE,具体的效果我们将在后面讲到。

直接查看实例总是比单纯的概念描述简单,所以我们直接看例子好了。

不同的包裹模式 CLAMP_TO_EDGE 这种包裹方式将所有不在0到1范围内的纹理坐标都近似到最近的有效点上。

所以我们看到的0到1范围外的纹理都是纹理边缘上的纹理(所以出现上图看到的横线和竖线)。

注意,只有这种包裹方式支持非2次幂纹理。

REPEAT 这是默认的包裹方式,也是最常使用的。

在数学上,这种方式会忽略纹理坐标的整数部分。

因此在0到1范围外就是重复的纹理。

MIRRORED_REPEAT 这种方式的纹理算法更加复杂。

如果纹理坐标的整数部分是偶数,纹理坐标的实现和REPEAT一致。

如果是奇数,那么最后的值是1减去当前纹理坐标。

这样得到的结果就是镜子倒映的展现形式。

如前面说的,这些模式可以是混合的,比如我们这样做: gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.REPEAT); gl.texParameteri(gl.TEXTURE_2D,gl.TEXUTRE_WRAP_T,gl.CLAMP_TO_EDGE); 想知道为什么shaderuniforms被叫做samplers而不是textures?一个texture仅仅是存储在GPU中的图片数据,而sampler包含所有的纹理信息,如过滤和包裹。

Usingmultipletextures 到目前为止,我们每次只加了一个纹理。

但是在很多场景下我们需要使用多纹理来创建更复杂的效果。

为此,我们可以使用WebGL提供的功能在一次draw中操作多纹理,这也被叫做多纹理渲染(multitexturing)。

之前我们已经提到了多纹理的内容,所以让我们再复习下。

我们有这样的代码: gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,texture); 第一行中gl.activeTexture就是实现多纹理的关键代码。

我们使用它来告诉WebGL状态机哪个纹理是我们想操作的。

在这个例子中,我们使用了gl.TEXTURE0,这就是说后续的操作对象都是第一个纹理单元。

如果我们想切换到第二个纹理,使用gl.TEXTURE1就可以了。

不同的设备支持的纹理单元个数也不尽相同,但是WebGL规范规定了必须支持至少两个单元。

我们可以使用以下的方法来获取设备支持的个数: gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); WebGL提供了从gl.TEXTURE0到gl.TEXTURE31。

我们还可以使用gl.TEXTURE0+i来指向gl.TEXTUREi,比如: gl.TEXTURE0+2===gl.TEXTURE2; 在渲染器中操作多纹理也非常简单: uniformsampler2DuSampler; uniformsampler2DuOhterSampler; 在调用draw函数前,你可以动态的告诉渲染器你要使用哪一个。

比如: //Bindthefirsttexture gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,texture); gl.uniform1i(Program.uSampler,0); gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,ohterTexture); gl.uniform1i(Program.uOtherSampler,1); 现在我们有两个纹理了。

问题是我们如何使用它们? 作为例子,我们将实现一个多纹理的场景。

多纹理 例子讲解 在脚本中,我们首先定义了另一个texture变量: vartexture2=null; 在config最后,我们需要添加这个纹理: texture2=newTexture(); texture2.setImage('textures/light.png'); 这个纹理是这样的: 在draw方法中,我们将这个纹理绑定到渲染器: gl.activeTexture(gl.TEXTURE); gl.bindTexture(gl.TEXTURE_2D,texture2.tex); gl.uniform1i(Program.uSampler2,1); 接下来,在渲染器中定义这个uniform: uniformsampler2DuSampler2; 最后,我们将两个纹理混合起来。

在这个例子中,我们期望第二个纹理有光照得效果,因此将两个值相乘: gl_FragColor=texture2D(uSampler,vTextureCoord)*texture2D(uSampler2,vTextureCoord); 注意我们在两个纹理中都使用了相同的纹理坐标。

在这个例子中我们是为了方便而这样做,如果有需要的话也可以设置不同的纹理坐标。

纹理如何混合并不是固定的,我们也可以这样做: gl_FragColor=vec4(texture2D(uSampler2,vTextureCoord).rgb-texture2D(uSampler,vTextureCoord).rgb,1.0); 得到的图就是: Cubemaps 在前面我们说过除了2D纹理外,我们还可以使用立体地图(cubemaps)。

立体地图,如它的字面表达的,是一个立体的纹理。

六个面的纹理都被创建,并且关联到每个面上。

显卡可以使用3D纹理坐标来将它们样本化(sample)。

每个面都根据它们所在的坐标轴和正负值来标记。

到目前为止,我们使用的纹理对象还是TEXTURE_2D。

立体地图引入了新的纹理对象,它和立体的各个面相对应: TEXTURE_CUBE_MAP TEXTURE_CUBE_MAP_POSITIVE_X TEXTURE_CUBE_MAP_NEGATIVE_X TEXTURE_CUBE_MAP_POSITIVE_Y TEXTURE_CUBE_MAP_NEGATIVE_Y TEXTURE_CUBE_MAP_POSITIVE_Z TEXTURE_CUBE_MAP_NEGATIVE_Z 立体地图创建方式和纹理一样,唯一不同的就是纹理对象: gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,positiveXImage); gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,positiveXImage); gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y,0,gl.RGBA,gl.RGBA,gl.UNSIGNED_BYTE,positiveYImage); //Etc. 绑定到渲染器也是相同的方法: gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_CUBE_MAP,cubeTexture); gl.uniform1i(Program.uCubeSampler,0); 渲染器中我们这样定义: uniformsamplerCubeuCubeSampler; 最后,定义fragment的最终颜色: gl_FragColor=textureCube(uCubeSampler,vCubeTextureCoord); 你提供的3D坐标会被显卡规范化为一个向量,它定义了从立体中心指向面的方向。

一个向量会沿着这个方向,与立体面接触的地方就是纹理渲染的地方。

立体地图 例子重点讲解 需要注意的是,在渲染器中我们使用了vertexnormals而不是每个面的纹理坐标,这会给我们镜子一般的感觉。

但是,实际上的normals都是指向外面的。

如果我们要使用它们,我们只能得到一个颜色。

在这个例子中,我们“欺骗”了渲染器,并使用了vertexpostion而不是normal。

(在大多数模型中,使用normals更合适) vVertexNormal=(uNMatrix*vec4(-aVertexPosition,1.0)).xyz; 这个没明白为什么,求有了解的同学告知,附上失败的效果图。

在这章中我们学到了如何使用纹理。

我们测试了不同的过滤效果和包裹效果,以及它们对于纹理有如何的影响。

我们学习了使用多纹理。

最后,也知道了立体地图的使用方式,以及如何做出倒映效果。

在下一章中,我们会学习如何选择和接触场景中的物体。

Go Youcan’tperformthatactionatthistime. Yousignedinwithanothertaborwindow.Reloadtorefreshyoursession. Yousignedoutinanothertaborwindow.Reloadtorefreshyoursession.



請為這篇文章評分?