WebGL系列(3):颜色与纹理

201 阅读8分钟

WebGL系列(1)中我们介绍到,如果我们想一次性向着色器传入多个顶点数据,就需要使用到缓冲区对象。前面的示例中,我们都是动态传递顶点的坐标。那如果我们需要动态传入坐标的大小等非坐标数据,该如何去做呢?所以在介绍颜色和纹理之前,我们先来看看如何向顶点着色器传递非坐标数据。

一、非坐标数据传入顶点着色器

之前我们有个例子是通过buffer动态传递多个点的坐标,下面是初始化缓冲区对象的相关代码,具体实现代码可见该系列第一篇文章。

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
    const n = 3;  // 点的个数
    
    // 创建缓冲区对象
    const vertexBuffer  =gl.createBuffer();
    if (!vertexBuffer) {
        console.log('创建缓冲区对象失败!');
        return -1;
    }

    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    // 获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_Position 的存储位置失败!');
        return -1;
    }

    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

    // 连接 a_Position 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position);
    
    return n;

}

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'void main() {\n' +
    '  gl_Position = a_Position;\n' +   // 设置顶点坐标
    '  gl_PointSize = 10.0;\n' +  // 设置顶点大小
    '}\n';

const FSHADER_SOURCE = 
    'void main() {\n' +
    '  gl_FragColor = vec4(1.0, 0.0, 0.0, 0.0);\n' +  // 设置颜色
    '}\n';

...

那如果我现在需要对不同的点设置不同的大小,最简单的方法,那就是我们再创建一个新的缓冲区对象,用于存储顶点大小,然后传递给着色器。那我们可以对上述代码做些修改:

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
    const sizes = new Float32Array([10.0, 20.0, 30.0])
    const n = 3;  // 点的个数
    
    // 创建坐标缓冲区对象
    const vertexBuffer  =gl.createBuffer();
    if (!vertexBuffer) {
        console.log('创建坐标缓冲区对象失败!');
        return -1;
    }
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
    // 获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_Position 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    // 连接 a_Position 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position);


    // 创建顶点大小缓冲区对象
    const sizeBuffer = gl.createBuffer();
    if (!sizeBuffer) {
        console.log('创建顶点大小缓冲区失败!');
        return -1;
    }
    gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);
    const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
    // 判断是否获取成功
    if (a_PointSize < 0) {
        console.log('获取 a_PointSize 的存储位置失败!');
        return -1;
    }
    gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(a_PointSize);
    
    return n;
}

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute float a_PointSize;\n' +
    'void main() {\n' +
    '  gl_Position = a_Position;\n' +   // 设置顶点坐标
    '  gl_PointSize = a_PointSize;\n' +  // 设置顶点大小
    '}\n';

const FSHADER_SOURCE = 
    'void main() {\n' +
    '  gl_FragColor = vec4(1.0, 0.0, 0.0, 0.0);\n' +  // 设置颜色
    '}\n';
...

如果顶点的每种数据都要使用一个缓冲区对象来维护,当程序中有成千上万个顶点时,那么维护起来是相当有难度的。这样看来,上述创建多个缓冲区的方法显然存在缺陷的。对此,WebGL中允许把顶点的坐标和尺寸数据打包到同一个缓冲区对象中,并通过某种机制分别访问缓冲区中不同种类的数据。

如下程序,我们看看具体是如何实现的。

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    const verticesSizes = new Float32Array([
        0.0, 0.5, 10.0,
        -0.5, -0.5, 20.0, 
        0.5, -0.5, 30.0]);
    const n = 3;  // 点的个数
    
    // 创建坐标缓冲区对象
    const vertexBuffer  =gl.createBuffer();
    if (!vertexBuffer) {
        console.log('创建坐标缓冲区对象失败!');
        return -1;
    }
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW);
    const FSIZE = verticesSizes.BYTES_PER_ELEMENT;
    // 获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_Position 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
    // 连接 a_Position 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position);

    // 获取 attribute 变量的存储位置
    const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_PointSize 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_PointSize 变量
    gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);
    // 连接 a_PointSize 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_PointSize);
    
    return n;
}

上述程序中,将顶点坐标和大小都存放在了同一个数组中,并且传入一个缓冲区对象中。而且在使用gl.vertexAttribPointer方法分配缓冲区对象时有所不同。我们再来回顾一下gl.vertexAttribPointer方法的stride参数 和 offset 参数。

  • stride:指定相邻两个顶点间的字节数(即:单个顶点所有数据的字节数),默认为0。
  • offset:指定缓冲区对象中的偏移量(以字节为单位),即 attribute 变量从缓冲区中何处开始存储。

那么代码gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);的含义则是: 每个顶点数据占FSIZE * 3字节,a_Position变量从 0 处开始存储,获取的分量个数为2

gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);的含义则是: 每个顶点数据占FSIZE * 3字节,a_PointSize变量从 FSIZE * 2 处开始存储,获取的分量个数为1.

也就是说,一个顶点的数据为0.0, 0.5, 10.0时,0.0, 0.5传递给a_Position变量, 10.0传递给a_PointSize变量。

通过这种方法,我们可以将顶点的多种数据打包在一起,只需要维护一个缓冲区对象

上述代码实现效果如下:

image.png

二、varying 变量

上文中我们实现了为每个顶点设置不同的大小。那如果我们想要为每个顶点设置不同颜色呢?

可能你会想到,创建一个缓冲区对象,填充顶点的位置和颜色数据,然后分配给attribute变量。但是,前面我们介绍到,真正能影响顶点颜色的是片元着色器

前面设置顶点颜色时,我们使用 uniform 变量将颜色信息传入片元着色器。但是这种方法只能给所有顶点设置同一个颜色。要想使得每个顶点颜色不同,我们需要用到一个新的变量 varying 变量(varying variable)

varying 变量的作用就是从顶点着色器向片元着色器传输数据。

那么下面我们就看看如何使用 varying 变量:

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    // 顶点坐标和颜色
    const verticesColors = new Float32Array([
        0.0, 0.5, 1.0, 0.0, 0.0,
        -0.5, -0.5, 0.0, 1.0, 0.0, 
        0.5, -0.5, 0.0, 0.0, 1.0]);
    const n = 3;  // 点的个数
    
    // 创建坐标缓冲区对象
    const vertexColorBuffer  =gl.createBuffer();
    if (!vertexColorBuffer) {
        console.log('创建坐标缓冲区对象失败!');
        return -1;
    }
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
    const FSIZE = verticesColors.BYTES_PER_ELEMENT;
    // 获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_Position 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
    // 连接 a_Position 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position);

    // 获取 attribute 变量的存储位置a_Color
    const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
    // 判断是否获取成功
    if (a_Color < 0) {
        console.log('获取 a_Color 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_PointSize 变量
    gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
    // 连接 a_PointSize 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Color);

    return n;

}

// 顶点着色器程序
const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec4 a_Color;\n' +
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_Position = a_Position;\n' +   // 设置顶点坐标
    '  gl_PointSize = 10.0;\n' +  // 设置顶点大小
    '  v_Color = a_Color;\n' +  // 将数据传递给片元着色器
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'varying vec4 v_Color;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = v_Color;\n' +  // 从顶点着色器接收数据
    '}\n';

image.png

上述程序中 varying 变量的使用流程大致如下:

  1. attribute vec4 a_Color;:声明 attribute 变量用于接收颜色数据
  2. varying vec4 v_Color;:声明 varying 变量负责将颜色值被传给片元着色器
  3. v_Color = a_Color;:将 attribute 变量的值赋给 varying 变量
  4. varying vec4 v_Color:在片元着色器中声明一个与顶点着色器中同名的 varying 变量用于接收顶点着色器传来的数据
  5. gl_FragColor = v_Color:将 varying 变量的值传递给 gl_FragColor 修改顶点颜色

:在 WebGL 中,如果顶点着色器片元着色器中有类型和命名相同varying变量,那么顶点着色器赋给该变量的值就会被自动传入片元着色器。

三、几何图形的装配和光栅化

上述绘制不同颜色顶点的例子中,如果我们修改drawArrays()mode参数为gl.TRIANGLES,会绘制出一个彩色的三角形。如下所示:

gl.drawArrays(gl.TRIANGLES, 0, n);

image.png

上述程序中,我们传递了顶点的坐标和颜色,但对于三角形内部颜色的填充,片元着色器又是怎么处理每个片元的呢? 其实在顶点着色器和片元着色器之间,还存在以下两个步骤:

  • 图形装配过程:将孤立的顶点坐标装配成几何图形。几何图形的类别由drawArrays()mode参数决定。该过程又称为图元装配过程,被装配出的基本图形(点、线、面)又被称为图元(primitives)
  • 光栅化过程:将装配好的几何图形转化为片元

如下图所示,就是绘制一个三角形的大体过程:

5e05a82a935486e71aedc0所示979d97551.jpg

  1. 执行顶点着色器gl.drawArrays()的参数n为多少,那么顶点着色器就执行多少次。
    • 第1次执行顶点着色器,缓冲区中的第一个坐标被赋值给 attribute 变量 a_Position。一旦一个顶点的坐标被赋值给了 gl_Posiiton,它就进入了图形装备区域,并且暂时存在那里。
    • 后续操作类似
  2. 图形装配:顶点着色器执行完毕后,所有顶点坐标就都处在装备区了。接下来就开始装配图形,使用传入的坐标,根据drawArrays()mode参数来决定如何装配。
  3. 光栅化:将图形转换为片元
  4. 调用片元着色器:光栅化过程结束后,程序开始逐片元调用片元着色器。对于每个片元,片元着色器计算出该片元颜色,并写入颜色缓冲区
  5. 浏览器显示:最后一个片元处理完成后,浏览器就会显示最终的结果。

cddcf38a815ec0b1510f73c374614a7.jpg

5e05a82a935486e71aedc0979d97551.jpg 上图皆出自《WebGL编程指南》

内插过程(interpolation process)

前面我们说到,我们把顶点的颜色赋值给顶点着色器的varying变量v_Color,它的值被传递给片元着色器中同名、同类型的变量。但实际上,顶点着色器中的v_Color 变量在传入片元着色器之前经历了内插过程。所以顶点着色器中的v_Color变量和片元着色器中的v_Color变量实际并不相同。

78e449bb8c0dee53b4edc5c9ff25bb3.jpg

那什么是内插过程呢?我们看如下的例子:

884e8e469ff8ccaf6ec61a532ccef03.jpg

例子中RGBA中的R值从1.0降低到0.0,B值从0.0上升到1.0,线段上的所有片元颜色值都会被恰当的计算出来,这个过程就是内插过程(interpolation process)。具体内插过程是如何计算的,可以参考这篇文章

上述例子实际效果如下:

image.png

四、 纹理映射

上述我们通过自定义顶点颜色以及varying变量的内插过程,绘制出了彩色的三角形。但是如果我们需要绘制一个与某个图片相同的图像怎么办呢?这个时候就要用到纹理映射了。

纹理映射(texture mapping):纹理映射就是将一张图像映射到一个几何图形表面。这张图片可以称为纹理图像(texture image)纹理(texture)

纹理映射的作用:根据纹理图像,为光栅化后的每个片元涂上合适的颜色。组成纹理图像的像素又被称为纹素(texels),每一个纹素的颜色都使用 RGB 或 RGBA 格式编码。

在 WebGL 中,要进行纹理映射,需要遵循以下四步:

  1. 准备映射待几何图形上的纹理图像
  2. 为几何图形配置纹理映射方式
  3. 加载纹理图像,对其进行一些配置,以在WebGL中使用它
  4. 在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元。

4.1 纹理坐标

上述第二步配置纹理映射方式其实就是,利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖,使用纹理坐标来确定图像上的哪部分将覆盖到几何图形上。

纹理坐标(texture coordinates) 是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。它与WebGl的坐标系统不同,如下图所示(矩形区域就是图像):

image.png

4.2 将纹理图像贴到几何图形上

经过上面的介绍,接下来纹理映射的具体代码实现:

// 初始化顶点缓冲区
function initVertexBuffer(gl) {
    // 顶点坐标 纹理坐标
    const verticesTexCoords = new Float32Array([
        -0.5, 0.5, 0.0, 1.0,
        -0.5, -0.5, 0.0, 0.0, 
        0.5, 0.5, 1.0, 1.0,
        0.5, -0.5, 1.0, 0.0
    ]);
    const n = 4;  // 点的个数
    
    // 创建坐标缓冲区对象
    const vertexTexCoordBuffer = gl.createBuffer();
    if (!vertexTexCoordBuffer) {
        console.log('创建坐标缓冲区对象失败!');
        return -1;
    }
    // 将缓冲区对象绑定到目标
    gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
    // 向缓冲区写入数据
    gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
    const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
    // 获取 attribute 变量的存储位置
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    // 判断是否获取成功
    if (a_Position < 0) {
        console.log('获取 a_Position 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_Position 变量
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
    // 连接 a_Position 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_Position);

    // 获取 attribute 变量的存储位置a_TexColor
    const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
    // 判断是否获取成功
    if (a_TexCoord < 0) {
        console.log('获取 a_TexCoord 的存储位置失败!');
        return -1;
    }
    // 将缓冲区对象分配给 a_PointSize 变量
    gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
    // 连接 a_PointSize 变量与分配给它的缓冲区对象
    gl.enableVertexAttribArray(a_TexCoord);

    return n;
}

// 初始化纹理对象
function initTexture(gl, n) {
    // 创建纹理对象
    const texture = gl.createTexture();
    if (!texture) {
        console.log('创建纹理对象失败!');
        return false;
    }  
    //获取 u_Sampler 的存储位置
    const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
    // 判断是否获取成功
    if (u_Sampler < 0) {
        console.log('获取 u_Sampler 的存储位置失败!');
        return false;
    }
    // 创建一个image对象
    const image = new Image();
    // 注册图形加载事件和响应函数
    image.onload = function() { loadTexture(gl, n, texture, u_Sampler, image) };
    // 浏览器开始加载图像
    image.src = './three/pupu.png';

    return true;
}

function loadTexture(gl, n, texture, u_Sampler, image) {
    // 对纹理图像进行y轴反转
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
    // 开启0号纹理单元
    gl.activeTexture(gl.TEXTURE0);
    // 向 target 绑定纹理对象
    gl.bindTexture(gl.TEXTURE_2D, texture);
    // 配置纹理参数
    // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
    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.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
    // 将0号纹理传递给着色器
    gl.uniform1i(u_Sampler, 0);
    // 设置canvas背景色
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 情况canvas
    gl.clear(gl.COLOR_BUFFER_BIT);
    // 绘制矩形
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); 

}

const VSHADER_SOURCE = 
    'attribute vec4 a_Position;\n' +
    'attribute vec2 a_TexCoord;\n' +
    'varying vec2 v_TexCoord;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_Position = a_Position;\n' +   // 设置顶点坐标
    '  v_TexCoord = a_TexCoord;\n' +  // 将数据传递给片元着色器
    '}\n';

const FSHADER_SOURCE = 
    'precision mediump float;\n' + 
    'uniform sampler2D u_Sampler;\n' + 
    'varying vec2 v_TexCoord;\n' +  // varying 变量
    'void main() {\n' +
    '  gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +  // 从顶点着色器接收数据
    '}\n';

function main() {
    // 获取canvas元素
    const canvas = document.getElementById('gl');
    // 获取WebGL绘图上下文
    const gl = canvas.getContext('webgl');
    // 确认WebGL支持性
    if (!gl) {
        console.log('浏览器不支持WebGL');
        return;
    }
    // 初始化着色器
    if(!initShader(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
        console.log('初始化着色器失败!');
        return;
    }

    // 设置顶点位置
    const n = initVertexBuffer(gl);
    if (n < 0) {
        console.log('设置顶点位置失败!');
        return;
    } 

    // 配置纹理
    if (!initTexture(gl, n)) {
        console.log('纹理配置失败!');
        return;
    }
}

image.png

上述程序的main函数主要分为以下五部分:

  1. 顶点着色器中接收顶点的纹理坐标,光栅化后传递给片元着色器
  2. 片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元
  3. 设置顶点的纹理坐标(initVertexBuffers())。需要注意的是纹理坐标是 vec2 类型的。
  4. 准备待加载的纹理图像,令浏览器读取它
  5. 监听纹理图像的加载事件,一旦加载完成,就在WebGL系统中使用纹理。

下面我们就来看看其中具体细节:

(1)配置和加载纹理

initTexture()函数负责配置和加载纹理:

  • 首先调用 gl.createTexture() 函数创建纹理对象
    • gl.createTexture():创建纹理对象以存储纹理图像
      • 返回值:创建的纹理对象,若创建失败,则返回null
    • gl.deleteTexture(texture):删除纹理对象
      • 参数:texture 待删除的纹理对象
  • gl.getUniformLocation(gl.program, 'u_Sampler')获取uniform变量u_Sampler(取样器)的存储位置,该变量用于接收纹理对象。
  • 创建一个 Image 对象,并注册 onload 事件响应函数 loadTexture()图像加载完毕后就会调用该函数。(因为图像是异步加载的,所以需要等待图像加载完毕后才能执行后续的操作)
  • 最后通知浏览器开始加载图像。

(2)为 WebGL 配置纹理

如上所述,图像加载完毕后就会执行loadTexture()函数,该函数主要负责配置纹理供 WebGL 使用。

图像Y轴反转 gl.pixelStorei()

WebGL纹理坐标系统中的t轴的方向和PNG、BMP、JPG等格式图片的坐标系统的Y轴方向是相反的。因此,只有先将图像Y轴进行反转,才能够正确地将图像映射到图形上。

gl.pixelStorei(pname, param):使用 pname 和 param 指定的方式处理加载得到的图像

  • 参数
    • pname:
      • gl.UNPACK_FLIP_Y_WEBGL:对图像进行 Y 轴反转。默认值为 false
      • gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL:将图像RGB颜色值的每一个分量乘以 A 。默认值为false。
    • param:指定非0(true) 或 0(false)。必须为整数。
  • 返回值:无

激活纹理单元 gl.activeTexture()

WebGL 通过纹理单元(texture unit) 的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像。

系统支持的纹理单元个数取决于硬件和浏览器的WebGL实现,默认情况下,至少支持8个纹理单元。内置的变量gl.TEXTURE0gl.TEXTURE1......gl.TEXTURE7个表示一个纹理单元

但是在使用纹理单元之前,需要调用gl.activeTexture()函数激活。

gl.activeTexture(textUnit):激活 textUnit 指定的纹理单元。

  • 参数
    • textUnit:指定准备激活的纹理单元。gl.TEXTURE0gl.TEXTURE1......gl.TEXTURE7,最后的数字表示纹理单元的编号。
  • 返回值:无

绑定纹理对象 gl.bingTexture()

在使用纹理对象之前,需要使用gl.bingTexture()函数绑定纹理对象,告诉 WebGL 系统纹理对象使用的是哪种类型的纹理。

gl.bingTexture(target, texture):开启 texture 指定的纹理对象,并将其绑定到 target 上。如果在此之前已经通过 gl.activeTexture() 激活了某个纹理单元,则纹理对象也会绑定到这个纹理单元上。

  • 参数
    • target:纹理类型
      • gl.TEXTURE_2D:二维纹理
      • gl.TEXTURE_CUBE_MAP:立方体纹理
    • texture:表示绑定的纹理单元
  • 返回值:无

在 WebGL 中,必须通过将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象。

配置纹理对象的参数 gl.texParameteri()

我们可以通过gl.texParameteri()配置纹理对象的参数,以此来设置纹理对象映射到图形上的具体方式。

gl.texParameteri(target, pname, param):将 param 的值赋给绑定到目标的纹理对象的 pname 上。

  • 参数
    • targetgl.TEXTURE_2Dgl.TEXTURE_CUBE_MAP
    • pname:纹理参数
    • param:纹理参数的值
  • 返回值:无

具体配置如下表所示:

纹理参数描述默认值
gl.TEXTURE_MAG_FILTER放大方法:即当纹理的绘制范围比纹理本身更大时,如何获取纹素颜色gl.NEAREST:使用原纹理上距离(曼哈顿距离)映射后像素中心最近的那个像素的颜色值,作为新的像素值
gl.LINEAR:使用原纹理上距离(曼哈顿距离)映射后像素中心最近四个像素的颜色值加权平均,作为新的像素值。
gl.NEAREST_MIPMAP_NEAREST
gl.NEAREST_MIPMAP_LINEAR
gl.LINEAR_MIPMAP_LINEAR
gl.LINEAR_MIPMAP_NEAREST
gl.LINEAR
gl.TEXTURE_MIN_FILTER缩小方法:即当纹理的绘制范围比纹理本身更小时,如何获取纹素颜色gl.TEXTURE_MAG_FILTERgl.NEAREST_MIPMAP_LINEAR
gl.TEXTURE_WRAP_S水平填充方法:即如何对纹理图像左侧或右侧的区域进行填充gl.REPEAT:平铺式重复纹理
gl.MIRRORED:镜像对称式重复纹理
gl.CLAMP_TO_EDGE:使用纹理图像边缘值
gl.REPEAT
gl.TEXTURE_WRAP_T垂直填充方法:即如何对纹理图像上方或下方的区域进行填充gl.TEXTURE_WRAP_Sgl.REPEAT

注:(x1, y1) 和 (x2, y2) 的曼哈顿距离为 |x1 - x2| + |y1 - y2|

将纹理图像分配给纹理对象 gl.texImage2D()

gl.texImage2D(target, level, interalformat, format, type, image):将 image 指定的图形分配给绑定到目标上的纹理对象。

  • 参数
    • targetgl.TEXTURE_2Dgl.TEXTURE_CUBE_MAP
    • level:传入 0
    • internalformat:图像的内部格式
    • format:纹理数据的格式,必须与internalformat相同
    • type:纹理数据的类型
    • image:包含纹理图像的 Image 对象
  • 返回值:无

纹素数据的格式

格式描述
gl.RGB红、绿、蓝
gl.RGBA红、绿、蓝、透明度
gl.ALPHA(0.0, 0.0,0.0,透明度)
gl.LUMINANCEL、L、L、1L
流明:表示我们感知到的物体表面的亮度,通常使用物体表面红绿蓝颜色分量值来加权平均计算
gl.LUMINANCE_ALPHAL、L、L、透明度

通常 JPG、BMP 格式使用gl.RGBPNG 格式使用gl.RGBA灰度图像使用gl.LUMINANCEgl.LUMINANCE_ALPHA

纹理数据的数据格式:

格式描述
gl.UNSIGNED_BYTE无符号整型,每个颜色分量占据1字节
gl.UNSIGNED_BYTE_5_6_5RGB:每个分量分别占据 5、6、5 比特
gl.UNSIGNED_BYTE_4_4_4_4RGBA:每个分量分别占据 4、4、4、4 比特
gl.UNSIGNED_BYTE_5_5_5_1RGBA:每个分量分别占据 5、5、5、1 比特

通常使用gl.UNSIGNED_BYTE无符号整型,后面几种类型通常用来压缩数据,以减少浏览器加载图像时间。

将纹理单元传递给片元着色器 gl.uniform1i()

用于接收纹理对象数据的变量必须为以下数据类型其中一种:

类型描述
sampler2D绑定到 gl.TEXTURE_2D 上的纹理数据类型
samplerCube绑定到 gl.TEXTURE_CUBE_MAP 上的纹理数据类型

gl.uniform1i()的第二个参数用于指定纹理单元编号

7e8becd0d0928efd0a7db030d1fabe3.jpg

在片元着色器中获取纹理像素颜色 texture2D()

texture2D(sampler2D sampler, vec2 coord):从 sampler 指定的纹理上获取 coord 指定的纹理坐标出的像素颜色。

  • 参数
    • sampler:指定纹理单元编号
    • coord:指定纹理坐标
  • 返回值:纹理坐标处的像素的颜色值,格式由gl.texImage2Dinternalformat参数决定。

以上就是纹理映射的大致过程了。

4.3 使用多幅纹理

前面我们说道,在 WebGL 系统中不止一个纹理单元,这样就意味着我们可以同时使用多幅纹理。

大体流程与4.2相同,只不过要创建多个纹理图像,并绑定到不同的纹理单元上。需要注意的是,因为图片是异步加载的,们不知道哪一张图片先加载完毕,所以我们要在响应程序中判断是否图片都加载完毕。等图片都加载完毕之后再绘制。

具体实现这里就不介绍了。

本文到这里也就结束啦!

参考:

[1] 《WebGL 编程指南》