纹理映射(贴图)_WebGL笔记8

写在前面

上一篇笔记中,我们利用varying变量把色值传递到片元着色器,实现了渐变效果,而更有用的颜色设置方法就是贴图(纹理映射)

一.原理

贴图就是把图片中的颜色映射到几何图形上去,步骤如下:

  1. 在顶点着色器中为每个顶点指定纹理坐标

  2. 然后在片元着色器中根据每个片元的纹理坐标从纹理图像中抽取纹素颜色

这里仍然是利用varying变量传递纹理坐标的,而且因为有内插过程,图片才能完美地罩在几何图形上

纹理坐标是一种新坐标系,与canvas和WebGL坐标系都不一样,原点在canvas左下角,s轴向右为正,t轴向上为正,canvas右上角坐标为(1.0, 1.0)

二.纹理映射

特别注意:图像格式没有限制,可以使用浏览器支持的任意格式图像,但作为纹理的图片的尺寸必须是2^mx2^n的,否则会报错:

WebGL: drawArrays: texture bound to texture unit 0 is not renderable. It maybe non-power-of-2 and have incompatible texture filtering or is not ‘texture complete’. Or the texture is Float or Half Float type with linear filtering while OES_float_linear or OES_half_float_linear extension is not enabled.

这个叫NPOT问题(Non Power Of Two),是贴图的常识,非2^mx2^n尺寸的图也能贴,但通常都更复杂更耗性能

1.设置纹理坐标

建立纹理坐标与WebGL坐标的联系,设置顶点对应的纹理坐标,然后片元着色器执行时通过gl_FragColor = texture2D(u_Sampler, v_TexCoord);抽取纹素设置片元颜色,图就贴上去了

着色器源程序如下:

// 顶点着色器源程序
var vsSrc = 'attribute vec4 a_Position;' +
    'attribute vec2 a_TexCoord;' +  // 接受纹理坐标
    'varying vec2 v_TexCoord;' +    // 传递纹理坐标
    'void main() {' +
    'gl_Position = a_Position;' +   // 设置坐标
    'v_TexCoord = a_TexCoord;' +    // 设置纹理坐标
'}';
// 片元着色器源程序
//!!! 需要声明浮点数精度,否则报错No precision specified for (float) 
var fsSrc = 'precision mediump float;' +
    'uniform sampler2D u_Sampler;' +    // 取样器
    'varying vec2 v_TexCoord;' +        // 接受纹理坐标
    'void main() {' +
    'gl_FragColor = texture2D(u_Sampler, v_TexCoord);' + // 设置颜色
'}';

其中sampler2D是取样器类型,图片纹理最终存储在该类型对象中

片元着色器中通过texture2D(sampler2D sampler, vec2 coord)来抽取纹素颜色,第一个参数是纹理单元编号,第二个参数是纹理坐标,返回值由给gl.texImage2D传入的internalformat参数决定,如果纹理图像不可用,就返回vec4(0.0, 0.0, 0.0, 1.0)

2.配置和加载纹理

加载图片的方式仍然是new Image,但WebGL不允许使用跨域纹理图像,具体步骤如下:

  1. 创建纹理对象

    // 创建texture
    var texture = gl.createTexture();   // 创建纹理对象
    
  2. 创建image

    var image = new Image();
    
  3. 给image添加load事件处理器,在事件处理器中配置纹理

    image.onload = function() {
        //---加载纹理
        // 1.对纹理图像进行y轴反转
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
        // 2.开开启0号纹理单元
        gl.activeTexture(gl.TEXTURE0);
        // 3.向target绑定纹理对象
        gl.bindTexture(gl.TEXTURE_2D, texture);
        // 4.配置纹理参数
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        // 用图片边缘颜色填充空白区域
        // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        // 镜像填充(轴对称)
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
        // 5.配置纹理图像
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
        // 6.将0号纹理传递给着色器
        gl.uniform1i(u_Sampler, 0);
    
        // 绘制矩形
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, arrVtx.length / 4);
    };
    

    配置纹理的过程比较复杂,待会儿再详细展开

  4. 给image.src赋值加载图像

    image.src = 'miao256x128.png';
    

    注意图片尺寸,错误信息中的non-power-of-2告诉我们必须使用2^mx2^n尺寸的图片作为纹理

3.内部状态

如图:

webgl-texture

webgl-texture

三.配置纹理

1.对纹理图像进行y轴反转

// 1.对纹理图像进行y轴反转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

如果不反转就会发现贴好的图片是倒的,就像贴“福”字一样,因为WebGL纹理坐标系统中的t轴和图像坐标系统的y轴是方向相反。图像坐标系是我们遇到的第4个坐标系,坐标轴/原点与canvas坐标系一样

当然,也可以手动反转y坐标,在片元着色器里让y = 1.0 - y即可,或者,也可以倒着贴。。但为什么不用这么方便的函数呢?

gl.pixelStorei(pname, param)
---
pname
    gl.UNPACK_FLIP_Y_WEBGL 对图像进行y轴反转
    gl.UNPACK_PERMULTIPLY_ALPHA_WEBGL 给图像rgb色值的每一个分量乘以A
param
    0或者非0整数

注意:参数值只能是整数,而不是true/false

2.开启x号纹理单元

// 2.开开启0号纹理单元
gl.activeTexture(gl.TEXTURE0);

WebGL通过纹理单元的机制来同时使用多个纹理,每个纹理单元管理一张纹理图像,gl.TEXTURE0~7一共8个,也就是说最多只能同时管理8张纹理图像

使用纹理单元之前,要通过gl.activeTexture(gl.TEXTURE0);来激活它

3.绑定纹理对象

// 3.向target绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);

告诉WebGL系统纹理对象使用的是哪种类型的纹理,支持2种类型纹理:gl.TEXTURE_2Dgl.TEXTURE_CUBE_MAP,类似于缓冲区操作,同样是指定target,参数个数及含义也类似

同样,在WebGL中也无法直接操作纹理对象,必须通过将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象

4.配置纹理参数

// 4.配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 用图片边缘颜色填充空白区域
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
// 镜像填充(轴对称)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);

设置纹理图像映射到图像上的具体方式,如何根据纹理坐标获取纹素颜色?按哪种方式重复填充纹理?也就是如何内插生成片元

texParameteri(target, pname, param)
---
target
    纹理类型,值为gl.TEXTURE_2D或者gl.TEXTURE_CUBE_MAP
pname
    纹理参数,值为gl.TEXTURE_MAG_FILTER表示纹理尺寸小于图形尺寸时如何放大纹理,默认值是gl.LINEAR
        或者gl.TEXTURE_MIN_FILTER表示纹理尺寸大于图形尺寸时如何缩小纹理,默认值是gl.NEAREST_MIPMAP_LINEAR
        或者gl.TEXTURE_WRAP_S表示如何填充纹理图像左侧或者右侧的区域,默认值是gl.REPEAT
        或者gl.TEXTURE_WRAP_T表示如何填充纹理图像上方和下方的区域,默认值是gl.REPEAT
param
    参数值,gl.TEXTURE_MAG_FILTER和gl.TEXTURE_MIN_FILTER可选值为gl.NEAREST和gl.LINEAR,前者表示使用纹理上距映射后像素中心最近的那个像素的颜色值,后者表示使用距新像素中心最近的4个像素的颜色值的加权平均,效果更好,但开销比较大。这2种是非金字塔纹理,还有金字塔纹理(不常用,不做介绍)
    gl.TEXTURE_WRAP_S和gl.TEXTURE_WRAP_T可选值为gl.REPEAT平铺、gl.MIRRORED_REPEAT镜像平铺、gl.CLAMP_TO_EDGE使用纹理图像边缘值

因为gl.TEXTURE_MIN_FILTER的默认值是特殊的金字塔纹理,所以要做修改gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);,不改的话会报错,且不显示纹理

5.配置纹理图像

// 5.配置纹理图像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);

把纹理图像分配给纹理对象,同时告诉WebGL系统关于纹理图像的一些特性

gl.texImage2D(target, level, internalformat, format, type, image)
---
target
    纹理类型,值为gl.TEXTURE_2D或者gl.TEXTURE_CUBE_MAP
level
    传入0,该参数是为金字塔纹理准备的,一般不用这个参数
internalformat
    图像的内部格式,必须与format取值相同
format
    图像的外部格式,根据图像格式来定jpg/bmp用gl.RGB、png用gl.RGBA、灰度图用gl.LUMINANCE或gl.LUMINANCE_ALPHA,此外还有gl.ALPHA
type
    纹理数据的类型,通常使用gl.UNSIGNED_BYTE,可选值还有gl.UNSIGNED_SHORT_5_6_5、gl.UNSIGNED_SHORT_4_4_4_4、gl.UNSIGNED_SHORT_5_5_5_1
image
    包含纹理图像的image对象

6.把x号纹理单元传递给片元着色器中的取样器变量

// 6.将0号纹理传递给着色器
gl.uniform1i(u_Sampler, 0);

本例中取样器声明为uniform sampler2D类型,uniform是因为纹理图像不随着片元变化,sampler2D对应gl.TEXTURE_2D,而samplerCube用于gl.TEXTURE_CUBE_MAP类型纹理

通过gl.uniform1i(u_Sampler, 0);给sampler2D变量赋值,1表示传入分量个数,i表示分量是int类型,这是WebGL惯用的命名方式,比如gl.uniform4fv(name, value)表示value的值应该是4维向量(有4个元素的类型化数组)

四.DEMO

包含上述代码的完整的例子,请查看:

多图片纹理是指把多张图片贴在同一块区域,比如:

webgl-texture-multi-image-example

webgl-texture-multi-image-example

叠加效果是2个纹素色值分量相乘的结果,vec4(r1, g1, b1, a1) * vec4(r2, g2, b2, a2),结果矢量是vec4(r1 * r2, g1 * g2, b1 * b2, a1 * a2),所以色值左乘右乘效果一样

多图片纹理的内部状态如图:

webgl-texture-multi-image

webgl-texture-multi-image

五.总结

纹理映射的原理很简单:从纹理图片中读取颜色,再赋值给片元,varying变量内插保证了着色均匀

但操作比较复杂,所以我们再次封装了util,接口说明如下:

/**
 * load texture image
 * by default, configure gl.TEXTURE_MIN_FILTER as gl.LINEAR
 *!!! can be overrode at callback
 * @param  {String}   imgPath  texture image path
 * @param  {Function} callback(image, unit) args: image object and texture unit number
 * @return
 * @throws {Error} If all texture unit was active now
 */
function loadTexture(imgPath, callback) {
    //...
}

利用callback保证了灵活性,具体实现细节请查看gl-util.js

至此,WebGL最最基础的东西就结束了,下一篇笔记是关于GLSL ES的详细说明,再然后就是无尽的矩阵漩涡。。

参考资料

  • WebGL编程指南》

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code