WebGL入门教程4 - 使用纹理贴图(Texture Map)

techbrood 发表于 2016-06-06 00:08:07

标签: webgl, texture

- +

3D建模和纹理贴图的关系就好比人体和皮肤(或着装)的关系,3D建模用来处理空间属性,而贴图适合用来处理细腻的表面属性。

如果不使用贴图,而想在表面达到足够的细节感受,会使得建模任务变得异常复杂而得不偿失。


注:本文混用贴图(texture map)、texture和纹理这3个中英文词汇,因为它们代表同样的含义,用来确定物体的表面观感(纹路/光滑度等)。


我们在基础知识教程中已经说明过,顶点的属性除了位置、颜色外,还有纹理。

我们可以把纹理看成是一种特殊的颜色,那么我们就可以使用类似的方式来处理。

总体过程是,加载外部图片,把贴图映射到3D对象的表面即建立贴图的模型化数据,关联缓存并完成绘制。

创建纹理并加载图片

var eyeTexture;
function initTexture() {
    eyeTexture = gl.createTexture();
    eyeTexture.image = new Image();
    eyeTexture.image.onload = function() {
      handleLoadedTexture(eyeTexture)
    }

    eyeTexture.image.src = "eyeball.png";
}

上述代码创建了一个Texture对象,并设置其图像属性的源为eyeball.png图片文件。

纹理使用异步方式加载图像文件,当加载完成时,调用回调函数handleLoadedTexture。

function handleLoadedTexture(texture) {
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, texture.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);
}

第1行代码bindTexture功能和bindBuffer类似,用来把texture设定为当前2D类型的纹理;

第2行用来完成电脑屏幕图像坐标到3D空间坐标的转换,把Y轴翻转;

第3行代码把图像加载到显卡上的纹理空间中去,参数分别是类型(0)、数据存储格式(rgba,重复2次)、数据类型(用来存放rgba的数据类型),以及图像对象本身。

接下来的2行代码,分别设置纹理的放大/缩小参数,也就是当纹理图像和屏幕尺寸不匹配时的处理方式,NEAREST表示保持原样。

最后1行属于整理性质的代码,不是必须的。

纹理图片映射和初始化缓存

接下来我们需要把纹理贴图映射到物体表面,前面提到过,我们把贴图当成特殊的颜色属性和顶点关联起来。

function initBuffers() {
    var latitudeBands = 60;
    var longitudeBands = 60;
    var radius = 2;

    var vertexPositionData = [];
    var normalData = [];
    var textureCoordData = [];
    for (var latNumber = 0; latNumber <= latitudeBands; latNumber++) {
        var theta = latNumber * Math.PI / latitudeBands;
        var sinTheta = Math.sin(theta);
        var cosTheta = Math.cos(theta);

        for (var longNumber = 0; longNumber <= longitudeBands; longNumber++) {
            var phi = longNumber * 2 * Math.PI / longitudeBands;
            var sinPhi = Math.sin(phi);
            var cosPhi = Math.cos(phi);

            var x = cosPhi * sinTheta;
            var y = cosTheta;
            var z = sinPhi * sinTheta;
            var u = x * 0.5 + 0.5;
            var v = y * 0.5 + 0.5;

            normalData.push(x);
            normalData.push(y);
            normalData.push(z);
            textureCoordData.push(u);
            textureCoordData.push(v);
            vertexPositionData.push(radius * x);
            vertexPositionData.push(radius * y);
            vertexPositionData.push(radius * z);
        }
    }
    //......
}

上述代码中xyz是3D球体中某点所在位置的法线,uv是(x,y,z)所映射的贴图上的平面坐标。这里u/v的计算公式为:

faceVertexUvs[ face ][ j ].x = face.vertexNormals[ j ].x * 0.5 + 0.5;
faceVertexUvs[ face ][ j ].y = face.vertexNormals[ j ].y * 0.5 + 0.5;

原因是我们所使用的图片(eyeball.png)是一张用于球面环境投影的MatCap(材质捕捉)图,映射到平面坐标时,需要做如上转换才不会出现变形。先不用深究这一点,总之我们记住要把2D纹理贴到3D模型上,需要做一个UV到XYZ的映射。

eyeballVertexNormalBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, eyeballVertexNormalBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normalData), gl.STATIC_DRAW);
eyeballVertexNormalBuffer.itemSize = 3;
eyeballVertexNormalBuffer.numItems = normalData.length / 3;

eyeballVertexTextureCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, eyeballVertexTextureCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoordData), gl.STATIC_DRAW);
eyeballVertexTextureCoordBuffer.itemSize = 2;
eyeballVertexTextureCoordBuffer.numItems = textureCoordData.length / 2;

eyeballVertexPositionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, eyeballVertexPositionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPositionData), gl.STATIC_DRAW);
eyeballVertexPositionBuffer.itemSize = 3;
eyeballVertexPositionBuffer.numItems = vertexPositionData.length / 3;

上述代码完成顶点和纹理数据缓存方面的处理。

绘制眼球

function drawScene() {
    //......
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, eyeballTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, eyeballVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, eyeballVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, eyeballVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, eyeballVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, eyeballVertexNormalBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexNormalAttribute, eyeballVertexNormalBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, eyeballVertexIndexBuffer);
    setMatrixUniforms();
    gl.drawElements(gl.TRIANGLES, eyeballVertexIndexBuffer.numItems, gl.UNSIGNED_SHORT, 0);    
    //......
}

在绘制元素(调用gl.drawElements接口)之前,我们需要设置当前激活的纹理为TEXTURE0,并告诉着色器程序,我们使用纹理0(WebGL最多可以处理32个纹理)。接着我们把着色器属性(Attribute)和数据缓存关联起来,以便依次读取和绘制顶点数据。

最后,我们还需要在片段着色器程序中添加纹理的处理逻辑:

precision mediump float;

varying vec2 vTextureCoord;
varying vec3 vLightWeighting;

uniform sampler2D uSampler;

void main(void) {
    vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t));
    gl_FragColor = vec4(textureColor.rgb * vLightWeighting, textureColor.a);
}

可以看到,现在球体对象表面的颜色被纹理贴图的颜色所替代了。

我们再加上一些简单的鼠标操作,就完成了一个可以旋转的3D眼球作品。

你可以自己在线试试

possitive(26) views27741 comments0

发送私信

最新评论

请先 登录 再评论.
相关文章
  • CentOS6 Apache2.2多站点HTTPS配置

    可以使用letsencrypt(certbot)免费证书服务。支持多系统、多站点和多目录,支持wildcard(通配符域名),90天生效,可用定时任务自动更新。需要注意一点的是apache2.4以下版本需要在默认的ssl配置中添加如下的指令:NameVirtualHost

  • 2019年开源WebRTC媒体服务器选型比较

    什么是WebRTC服务器?在WebRTC的早期开始,该技术的主要卖点之一是它允许点对点(浏览器到浏览器)通信,几乎没有服务器的干预,服务器通常仅用于信令(比如用于...

  • OpenGL/WebGL顶点坐标变换过程简介

    世界坐标是按照笛卡尔坐标系定义出来的绝对坐标系,下面的各种坐标系都建立在世界坐标的基础上。对象坐标系对象被应用于任何...

  • html5跨平台实战-第一周-水平测验-新闻列表页面

    这是一个DIV+CSS布局页面的一个实例,主要介绍POSITION定位、导航UL LI的制作、利用浮动原理对页面分栏、分列的页面布局。新闻页面的效果图

  • CSS3弹性布局内容对齐(justify-content)属性使用详解

    内容对齐(justify-content)属性应用在弹性容器上,把弹性项沿着弹性容器的主轴线(main axis)对齐。该操作发生在弹性长度以及自动边距被确定后。 它用来在存...

  • 使用HTML5 FileReader和Canvas压缩用户上传的图片

    手机用户拍的照片通常会有2M以上,这对服务器带宽产生较大压力。因此在某些应用下(对图片要求不那么高)我们可以在客户端来压缩图片,然后再提交给服务器。总体...

  • 使用HTML5 Canvas实现的界面元素截屏功能

    如果网站出现问题,常常需要截图来提交反馈,这个功能很实用。使用HTML5的Canvas可以实现这个目标。我们首先引入

  • CSS3图片混合(Blend)效果及其参考计算公式一览表

    在Photoshop软件中,混合是将两个图层的色彩值进行合成,从而创造出大量的效果。在这些效果的背后实际是一些简单的数学公式在起作用。下面所介绍的公式仅适用于R...

  • 如何使用WebGL实现空气高温热变形动画特效

    我们在炎炎夏日,或者在火堆旁,经常会观察到热源周围空气的不稳定波动现象。本文将讲解如何通过WebGL来实现这个特效。该效果可用于热变形、波浪、水面波光等场...

  • IE各版本CSS Hack(兼容性处理)语法速查表

    为了兼容IE各个版本,需要在CSS中添加额外的代码,比如以前常用的_width。之所以工作,是因为浏览器会忽略不能解析的样式规则,因此举个例子来说,把_width写在w...

  • D3.js读取外部json数据

    D3.js是一个很好的数据可视化工具,支持从web服务读取json数据,或者从外部文件如.json, .csv文件中直接读取。由于部分服务比如flickrs上的图文数据服务需要VPN...

  • 如何使用纯CSS3实现一个3D泡沫

    要实现一个逼真的泡沫,涉及到比较复杂的光学/物理学知识。我们这里先简化下问题,实现一个相对简单而足够实用的泡沫元素。我们可以把基础的泡沫元素应用在很多场景中,比如水景、泡咖啡、啤酒甚至火焰特效中。泡沫首先是一个圆形元素.bubble

  • div 、section 、article的区别和使用场景

    div 、section 、article的区别和使用场景
    主要区别,以及适用场合如下:
    1、div在html早期版本就支持了,section和article是html5提出的两个雨衣话标...

  • 如何使用CSS3/SCSS实现逼真的车窗雨滴效果

    在天气预报类的Web移动应用中,常常需要实现屏幕的雨滴效果,表示阴雨天气。感觉上比较神奇,其实想通了,这个效果的实现只需要一点物理知识和CSS3的简单变换。实现一个小雨滴首先雨滴是一个个小的椭圆形元素:.raindrop

  • 更多...