# 颜色与纹理

这篇文章主要讲三个问题:

  • 将顶点的其他数据,比如颜色等,传入到顶点着色器进行渲染
  • 发生在顶点着色器和片元着色器之间的从图形到片元的转换,又称为图元光栅化
  • 将图形(或称纹理)映射到图形或三维对象的表面上

# 将非坐标数据传入顶点着色器

前面写过绘制三角形,将顶点坐标传入到着色器,需要下面几个步骤:

  1. 创建缓冲区对象
  2. 将缓冲区对象绑定到 target 上
  3. 将顶点坐标数据写入到缓冲区对象
  4. 将缓冲区对象分配给对应的 attribute 变量
  5. 开启 attribute 变量

现在,我们希望把多个顶点相关数据通过缓冲区对象传入顶点着色器,其实只需要每种数据重复上面的步骤即可。

下面代码是绘制一个指定大小的点,需要给顶点着色器传入点的坐标以及点的大小:

关键代码

 // 顶点着色器
var 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';

function main() {
  // 设置顶点信息
  var n = initVertexBuffers(gl);
  // 绘制三个点
  gl.drawArrays(gl.POINTS, 0, n);
}

function initVertexBuffers(gl) {
  var vertices = new Float32Array([
    0.0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ]);
  var n = 3;

  var sizes = new Float32Array([
    10.0, 20.0, 30.0  // 点的尺寸
  ]);

  // 创建缓冲区对象
  var vertexBuffer = gl.createBuffer();  
  var sizeBuffer = gl.createBuffer();
  if (!vertexBuffer || !sizeBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  // 将顶点坐标写入缓冲区对象并开启
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    if(a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_Position);

  // 将顶点尺寸写入缓冲区对象并开启
  gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);
  var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
  if(a_PointSize < 0) {
    console.log('Failed to get the storage location of a_PointSize');
    return -1;
  }
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_PointSize);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

上面代码是创建了两个缓冲区对象,一个是用来传递位置信息,一个是传递点的大小;这种情况比较适合数据量不大的情况,如果数据量大,可以将顶点的坐标和尺寸数据打包到同一个缓冲区对象中,并通过某种机制分别访问缓冲区对象中不同种类的数据。比如可以将顶点的坐标和尺寸数据按照下面的方式交错组织:

  var verticesSizes = new Float32Array([
    // 顶点坐标和点的尺寸
     0.0,  0.5,  10.0,  // 第一个点
    -0.5, -0.5,  20.0,  // 第二个点
     0.5, -0.5,  30.0   // 第三个点
  ]);

然后可以根据 gl.vertexAttribPointer() 函数的第 5 个参数 stride 和第 6 个参数 offset 来读取特定的数据。如下代码:

// 顶点着色器
var 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';

function main() {
  // 设置坐标和尺寸
  var n = initVertexBuffers(gl);
  // ...
  gl.drawArrays(gl.POINTS, 0, n);
}

function initVertexBuffers(gl) {
  var verticesSizes = new Float32Array([
    // 顶点坐标和点的尺寸
     0.0,  0.5,  10.0,  // the 1st point
    -0.5, -0.5,  20.0,  // the 2nd point
     0.5, -0.5,  30.0   // the 3rd point
  ]);
  var n = 3;

  // 创建缓冲区对象
  var vertexSizeBuffer = gl.createBuffer();  
  if (!vertexSizeBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  // 将顶点坐标和尺寸写入缓冲区并开启
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexSizeBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW);

  var FSIZE = verticesSizes.BYTES_PER_ELEMENT;
  // 获取 a_Position 的存储位置,分配缓冲区并开启
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
  gl.enableVertexAttribArray(a_Position);  // 开启分配

  // 获取 a_PointSize 的存储位置,分配缓冲区并开启
  var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
  if(a_PointSize < 0) {
    console.log('Failed to get the storage location of a_PointSize');
    return -1;
  }
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);
  gl.enableVertexAttribArray(a_PointSize);  // 开启缓冲区分配

  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

上面代码将 verticesSizes 数组中每个元素的大小(字节数)存储到 FSIZE 中,用于后续获取坐标点以及点的大小设置步长;类型化数组具有 BYTES_PER_ELEMENT 属性,可以从中获取数组中每个元素所占的字节数。

然后在将缓冲区对象分配给 attribute 变量时,首先需要获取 attribute 变量 a_Position 的存储地址,然后通过 gl.vertexAttribPointer() 函数,将缓冲区对象分配给 a_Position 变量,这里的参数设置跟之前我们写的例子是不一样的,因为在缓冲区对象中存储了两种类型的数据:顶点坐标和顶点尺寸,首先看一下这个函数的一些使用规范:

avatar

参数 stride 表示,在缓冲区对象中,单个顶点的所有数据(这里,就是顶点的坐标和大小)的字节数,也就是相邻两个顶点间的距离,即步进参数。在之前的例子中,因为缓冲区只有顶点坐标,所以这个参数设置为 0 ,在这个例子中,我们需要设置 stride 值:

avatar

如上图,每一个顶点有 3 个数据值(两个坐标数据和一个尺寸数据),因此 stride 应该设置为每项数据大小的三倍。参数 offset 表示当前考虑的数据项距离首个元素的距离,即偏移参数。在 verticesSizes 数组中,顶点的坐标数据是放在最前面的,所以 offset 应当为 0 。我们在获取点的大小的时候,offset 应该设置为 FSIZE * 2;

# 顶点着色器与片元着色器之间通讯

片元着色器可以用来处理颜色之类的属性,我们前面的代码只是向顶点着色器传入顶点坐标,我们需要使用一种新的变量 varying 变量向片元着色器中传入数据,实际上,varying 变量的作用是从顶点着色器向片元着色器传输数据;如下代码,实现将点的颜色传入到顶点着色器,然后顶点着色器与片元着色器之间通讯,获取点颜色:

// 顶点着色器
var 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';

// 片元着色器程序
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +    // 从顶点着色器中接收数据
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  // 设置顶点的坐标和颜色
  var n = initVertexBuffers(gl);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.POINTS, 0, n);
}

function initVertexBuffers(gl) {
  var 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, 
  ]);
  var n = 3; // 顶点数量

  // 创建缓冲区对象
  var vertexColorBuffer = gl.createBuffer();  
  if (!vertexColorBuffer) {
    console.log('Failed to create the buffer object');
    return false;
  }
  // 将顶点坐标和颜色写入缓冲区对象
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  var FSIZE = verticesColors.BYTES_PER_ELEMENT;
  // 获取 a_Position 的存储位置,分配缓冲区并开启
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
  gl.enableVertexAttribArray(a_Position);  // Enable the assignment of the buffer object

  // 获取 a_Color 的存储位置,分配缓冲区并开启
  var a_Color = gl.getAttribLocation(gl.program, 'a_Color');
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
  gl.enableVertexAttribArray(a_Color);

  return n;
}

这里需要注意的是,varying 变量只能是 float (以及相关的 vec2,vec3,vec4,mat2,mat3 和 mat4) 类型的;片元着色器接收变量 v_Color,只需要在片元着色器中也声明同样的变量名即可。

# 彩色三角形——几何形状的装配和光栅化

# 几何形状的装配和光栅化

我们前面给顶点着色器传入顶点坐标后,片元着色器怎样处理每个片元呢;实际上在顶点着色器和片元着色器之间有下面两个步骤:

  • 图形装配过程 这一步的任务是,将孤立的顶点坐标装配成几何图形。几何图形的类别由 gl.drawArrays() 函数的第一个参数决定。
  • 光栅化过程 这一步的任务是,将装配好的几何图形转化为片元

avatar

gl_Position 实际上是几何图形装配阶段的输入数据。几何图形装配过程又称为图元装配过程,因为被装配出的基本图形又被成为图元。我们以绘制一个三角形为例,顶点着色器和片元着色器之间图形装配与光栅化的过程如下:

  • 第一步:执行顶点着色器,缓冲区对象中的第一个坐标(0, 0, 0.5) 被传输给 attribute 变量 a_Position,一旦一个顶点的坐标复制给了 gl_Position,他就进入了图形装配区域,并暂时存储在那里。
  • 第二步:再次执行顶点着色器,类似地将第二个坐标传入并存储在装配区
  • 第三步:第三次执行顶点着色器,将第三个坐标传入并存储在装配区。现在,顶点着色器执行完毕,三个顶点的坐标都已经处于装配区了。
  • 第四步:开始装配图形。使用传入的点坐标,根据 gl.drawArrays() 的第一个参数信息(gl.TRIANGLES)来决定如何装配,这里是使用三个顶点装配出一个三角形。
  • 第五步:显示在屏幕上的三角形是由片元(像素)组成的。所以还需要将图形转化为片元,这个过程被称为光栅化。光栅化之后,我们就得到了组成这个三角形的所有片元。

avatar 上图只显示了 10 个片元,实际上,片元的数目就是这个三角形最终在屏幕上所覆盖的像素数。

# 调用片元着色器

一旦光栅化过程结束后,程序就开始逐片元调用片元着色器。没调用一次,就处理一个片元,对于每个片元,片元着色器计算出该片元的颜色,并写入颜色缓冲区。直到所有的片元被处理完成,浏览器渲染最终结果。

我们可以根据每个片元的位置来确定片元的颜色。光栅化过程中生成的片元都是带有坐标信息的,调用片元着色器时这些坐标信息也随着片元传了进去,可以通过内置变量获取片元的坐标: avatar

修改片元着色器程序:

var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform float u_Width;\n' +
  'uniform float u_Height;\n' +
  'void main() {\n' +
  '  gl_FragColor = vec4(gl_FragCoord.x/u_Width, 0.0, gl_FragCoord.y/u_Height, 1.0);\n' +
  '}\n';

上面每个片元的颜色分量都是通过片元的位置计算得到的。注意,canvas 中的 Y 轴方向跟 webgl 系统的 Y 轴方向是相反的,而且 webgl 中的颜色分量值区间是 0.0 到 1.0 ,所以需要将 Y 轴坐标除以 canvas 的高度来将其处理在 0.0 到 1.0 之间。我们将 gl.drawingBufferWidth (颜色缓冲区的宽度) 和 gl.drawingBufferHeight (颜色缓冲区的高度)的值传给 u_Width 和 u_Height;

# varying 变量的作用和内插过程

其实定义的 varying 变量在顶点着色器与片元着色器之间传递,更准确的说,是顶点着色器的变量在传入片元着色器之前经过了内插过程。所以我们前面说的 v_Colr。顶点着色器中的 v-Color 变量在传入片元着色器之前经过了内插过程。更准确的说,我们在 varying 变量中为三角形的 3 个不同顶点指定了 3 中不同颜色,而三角形表面上这些片元的颜色值都是 webgl 系统用这 3 个顶点的颜色内插出来的。

比如两个端点颜色不同的线段。一个为红色,另一个为蓝色,在顶点着色器中向 varying 传入这两个颜色,然后 webgl 就会自动计算线段上的所有点(片元)的颜色,并赋值给片元着色器中的 varying 变量 v_Color。

avatar

var 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';

// 片元着色器程序
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +    // 内插得到的颜色被赋值给 v_Color
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +  // 再被赋值给 gl_FragColor
  '}\n';

# 矩形表面贴上图像

将图形贴到一个几何图形上,一般叫做纹理映射。纹理映射的作用就是根据纹理图像,为之前光栅化后的每个片元涂上合适的颜色,组成纹理图像的像素又被成为纹素,每一个纹素的颜色都使用 RGB 或 RGBA 格式编码。

在 webgl 中要进行纹理映射,需要下面几个步骤:

  1. 准备好映射到几个图像上的纹理图像
  2. 为几何图形配置纹理映射方式;也就是确定几何图形的某个片元的颜色如何取决于纹理图像中哪个像素;我们利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖,使用纹理坐标来确定纹理图像的哪部分将覆盖到几何图形上。
  3. 加载纹理图像,对其进行一些配置,以便在 webgl 中使用它
  4. 在片元着色器中将相应的纹素从纹理中抽离出来,并将纹素的颜色赋给片元。

第二步指定映射方式,就是确定几何图形的某个片元的颜色如何取决于纹理图像中哪个像素的问题,我们利用图形的顶点坐标来确定屏幕上哪部分将覆盖到几何图形上。下面是纹理坐标。

# 纹理坐标

前面说的纹理坐标是一个新的坐标系统,纹理坐标就是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色;webgl 系统中的纹理坐标是二维的。 不管图像是多大,其右上角纹理坐标始终是(1.0, 1.0) avatar

# 将纹理图像粘贴到几何图形上

我们将纹理坐标(0.0, 1.0)映射到顶点坐标(-0.5, -0.5, 0.0) 上,将纹理坐标 (1.0, 1.0) 映射到顶点坐标 (0.5, 0.5, 0.0) 上,来建立矩形四个顶点与纹理坐标的对应关系,就完成了几何图形的贴纹理操作: avatar

坐标值与图形的尺寸无关。不管多大,其右上角还是 (1.0, 1.0)

# 实现代码

首先在顶点着色器中为每个顶点指定纹理坐标,然后在片元着色器中根据每个片元的纹理坐标从纹理图像中抽取纹素颜色。

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  v_TexCoord = a_TexCoord;\n' +
  '}\n';

// 片元着色器
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'uniform sampler2D u_Sampler;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main() {\n' +
  '  gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  // 设置顶点信息
  var n = initVertexBuffers(gl);
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  // 配置纹理
  if (!initTextures(gl, n)) {
    console.log('Failed to intialize the texture.');
    return;
  }
}

function initVertexBuffers(gl) {
  var 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,
  ]);
  var n = 4; // 顶点数目
  // 创建缓冲区对象
  var vertexTexCoordBuffer = gl.createBuffer();
  if (!vertexTexCoordBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }
  // 将顶点坐标与纹理坐标写入缓冲区对象
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

  var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
  gl.enableVertexAttribArray(a_Position);

  // 获取 a_TexCoord 位置
  var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
  if (a_TexCoord < 0) {
    console.log('Failed to get the storage location of a_TexCoord');
    return -1;
  }
  // 将纹理坐标分配给 a_TexCoord
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
  gl.enableVertexAttribArray(a_TexCoord);
  return n;
}

function initTextures(gl, n) {
  var texture = gl.createTexture();   // 创建纹理对象
  // 获取 u_Sampler 的存储位置
  var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
  var image = new Image();  // 创建一个 image 对象
  // 注册图像加载事件的响应函数
  image.onload = function(){ loadTexture(gl, n, texture, u_Sampler, image); };
  // 浏览器开始加载图像
  image.src = '../resources/sky.jpg';
  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.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
  // 将 0 号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0);
  
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n); // 绘制矩形
}

上面代码主要分五个部分:

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

将纹理坐标传入顶点着色器,与将其他顶点数据(如颜色)传入顶点着色器的方法时相同的。可以将纹理坐标和顶点坐标写在同一个缓冲区中,定义数组 verticesTexCoords 记录顶点坐标和纹理坐标。

然后将顶点坐标和纹理坐标写入缓冲区对象,将其中的顶点坐标分配给 a_Position 变量并开启。然后获取 a_TexCoord 变量的存储位置,将缓冲区中的纹理坐标分配给该变量。

  // 获取 a_TexCoord 位置
  var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
  if (a_TexCoord < 0) {
    console.log('Failed to get the storage location of a_TexCoord');
    return -1;
  }
  // 将纹理坐标分配给 a_TexCoord
  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
  gl.enableVertexAttribArray(a_TexCoord);
  • 配置和加载纹理(initTextures())

initTextures() 函数负责配置加载纹理,首先创建纹理对象 gl.createTexture(),然后调用 gl.getUniformLocation() 从片元着色器中获取 u_Sampler 的存储文职,该变量用来接收纹理图像:

  var texture = gl.createTexture();   // 创建纹理对象
  // 获取 u_Sampler 的存储位置
  var u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');

gl.createTexture() 方法创建纹理对象:

avatar

创建纹理对象后,会有 8 个纹理单元 gl.TEXTURE0 到 gl.TEXTURE7,每一个都与 gl.TEXTURE_2D 相关联,后者就是绑定纹理时的纹理目标。

avatar

可以使用 gl.deleteTexture() 来删除一个纹理对象。如果删除一个已经被删除的纹理对象,不会报错。 avatar

然后下面代码就是请求浏览器加载纹理供 webgl 使用,纹理图像将会映射到矩形上,所以需要使用 Image 对象。然后注册图像异步加载完之后执行 loadTexture() 函数。

  • 为 webgl 配置纹理

loadTexture() 函数主要任务是配置纹理供 webgl 使用,使用纹理对象的方式与使用缓冲区很类似;

  1. 图像 Y 轴反转

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

avatar

下面是 pixelStorei 函数语法:

avatar

  1. 激活纹理单元(gl.activeTexture())

webgl 通过一种称作纹理单元的机制来同时使用多个纹理。每个纹理单元有一个单元有一个单元编号来管理一张纹理图像。即使程序只需要使用一张纹理图像,也得为其指定一个纹理单元。系统支持的纹理单元个数取决于硬件和浏览器的 webgl 实现,默认情况下,webgl 至少支持 8 个纹理单元,内置的变量 l.TEXTURE0 到 gl.TEXTURE7 各表示一个纹理单元。

avatar

在使用纹理单元之前,需要 gl.activeTexture() 方法激活它:

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

avatar

  1. 绑定纹理对象

这一步操作主要是告诉 webgl 系统纹理对象使用的是哪种类型的纹理。在对纹理对象进行操作之前,我们需要绑定纹理对象,这一点跟缓冲区对象很像:在对缓冲区对象进行操作之前,也需要绑定缓冲区对象。webgl 支持两种类型的纹理,如下:

纹理类型 描述
gl.TEXTURE_2D 二维纹理
gl.TEXTURE_CUBE_MAP 立方体纹理
  // 向 target 绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture);

gl.bindTexture 函数语法:

avatar

这个方法完成了两个任务:开启纹理对象,以及将纹理对象绑定到纹理单元上。在这个例子中,因为 0 号纹理单元已经被激活,所以在执行完绑定后,webgl 系统内部状态如下“

avatar

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

gl.texParameteri() 函数设置了如何根据纹理坐标获取纹素颜色、按哪种方式重复填充纹理。 avatar

  // 配置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

通过 pname 可以指定 4 个纹理参数:

  • 放大方法(gl.TEXTURE_MAG_FILTER):这个参数表示,当纹理的绘制范围比纹理本身更大时,如何获取纹素颜色。webgl 需要填充由于放大而造成的像素间的空隙,该参数就表示填充这些空隙的具体方法。
  • 缩小方法(gl.TEXTURE_MIN_FILTER):这个参数表示,当纹理的绘制范围比纹理本身更小时,如何获取纹素颜色。webgl 需要剔除纹理图像中的部分像素,该参数就表示具体的剔除像素的方法。
  • 水平填充方法(gl.TEXTURE_WRAP_S):这个参数表示,如何对纹理图像左侧或右侧的区域进行填充。
  • 垂直填充方法(gl.TEXTURE_WRAP_T):这个参数表示,如何对纹理图像上方和下放的区域进行填充。

avatar

下表是每种纹理参数的默认值:

纹理参数 描述 默认值
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.REPAET

下表是可以赋值给 gl.TEXTURE_MAG_FILTER 和 gl.TEXTURE_MIN_FILTER 的常量: avatar

下表是可以赋值给 gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 的常量: avatar

我们前面代码是设置了 gl.TEXTURE_MIN_FILTER 的参数,设置为 gl.LINEAR,他是一种特殊的、被称为 MIPMAP(也称为金字塔)的纹理类型。MIPMAP 纹理实际上是一系列纹理,或者说是原始纹理图像的一系列不同分辨率的版本。

设置完纹理参数后,webgl 系统的内部状态如下:

avatar

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

使用 gl.texImage2D() 方法将纹理图像分配给纹理对象。 该方法的语法如下:

avatar

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

format 参数表示纹理数据的格式,如下表,需要根据纹理图像的格式来选择这个参数: 这个例子中使用的纹理图片是 JPG 格式的,该格式将每个像素用 RGB 三个分量来表示,所以将参数指定为 gl.RGB,对其他格式的图像,如 PNG 格式的图形,通常使用 gl.RGBA ,BMP 格式的图像通常使用 gl.RGB,而 gl.LUMINANCE 和 gl.LUMINANCE_ALPHA 通常用在灰度图像上。

纹理参数 描述
gl.RGB 红、绿、蓝
gl.RGBA 红、绿、蓝、透明度
gl.ALPHA (0.0, 0.0, 0.0, 透明度)
gl.LUMINANCE L、L、L、1L:流明
gl.LUMINANCE_ALPHA L、L、L、透明度

这里的流明表示我们感知到的物体表面的亮度。通常使用物体表面红、绿、蓝颜色分量值的加权平均来计算流明。

type 参数指定了纹理数据类型,通常我们使用 gl.UNSIGNED_BYTE 数据类型。当然也可以使用其他数据类型,如 gl.UNSIGNED_SHORT_5_6_5 (将 RGB 三分量压缩入 16 比特中)。后面的几种数据格式通常被用来压缩数据,以减少浏览器加载图像的时间。

下面是纹理数据的数据格式

格式 描述
gl.UNSIGNED_BYTE 无符号整型,每个颜色分量占据 1 字节
gl.UNSIGNED_SHORT_5_6_5 RGB:每个分量分别占据 5、6、5 比特
gl.UNSIGNED_SHORT_4_4_4_4 RGBA:每个分量分别占据 4、4、4、4 比特
gl.UNSIGNED_SHORT_5_5_5_1 RGBA:RGB 每个分量分别占据 5 比特,A 分量占据 1 比特
  1. 将纹理单元传递给片元着色器(gl.uniform1i())

一旦将纹理图像传入了 webgl 系统,就必须将其传入片元着色器并映射到图形的表面上去。前面的例子使用 uniform 变量来表示纹理,因为纹理图像不会随着片元变化。

// 片元着色器
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'uniform sampler2D u_Sampler;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main() {\n' +
  '  gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
  '}\n';

必须将着色器中表示纹理对象的 uniform 变量声明为一种特殊的、专用于纹理对象的数据类型,如下表:

类型 描述
sampler2D 绑定到 gl.TEXTURE_2D 上的纹理数据类型
samplerCube 绑定到 gl.TEXTURE_CUBE_MAP 上的纹理数据类型
  1. 从顶点着色器向片元着色器传输纹理坐标

使用 varying 变量 v_TextCoord 将纹理坐标传入到片元着色器中。在片元着色器中,使用功能的是内插后的纹理坐标。

这样就完成了在 webgl 系统中使用纹理的所有准备工作。 剩下的工作就是,根据片元的纹理坐标,从纹理图像上抽出纹素的颜色,然后涂到当前的片元上。

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

片元着色器从纹理图像上获取纹素的颜色;使用 GLSL ES 内置函数 texture2D() 来抽取纹素颜色。

gl_FragColor = texture2D(u_Sampler, v_TexCoord);

该函数的语法如下: avatar

下表是 texture2D() 的返回值

internalformat 返回值
gl.RGB (R,G,B,1.0)
gl.RGBA (R,G,B,A)
gl.ALPHA (0.0,0.0,0.0,0.0,A)
gl.LUMINANCE (L,L,L,1.0)
gl.LUMINANCE_ALPHA (L,L,L,A)

纹理放大和缩小方法的参数将决定 webgl 系统将以何种方式内插出片元。

# 使用多幅纹理

如下代码:

var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'uniform sampler2D u_Sampler0;\n' +
  'uniform sampler2D u_Sampler1;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main() {\n' +
  '  vec4 color0 = texture2D(u_Sampler0, v_TexCoord);\n' +
  '  vec4 color1 = texture2D(u_Sampler1, v_TexCoord);\n' +
  '  gl_FragColor = color0 * color1;\n' +
  '}\n';

function main() {
  // 配置纹理
  if (!initTextures(gl, n)) {
    console.log('Failed to intialize the texture.');
    return;
  }
}

function initVertexBuffers(gl) {
  var 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,
  ]);
  var n = 4; // 顶点数量
  // ...
  return n;
}

function initTextures(gl, n) {
  // 创建纹理对象
  var texture0 = gl.createTexture(); 
  var texture1 = gl.createTexture();

  // 获取 u_Sampler0 和 u_Sampler1 的存储位置
  var u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0');
  var u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1');
  // 创建 image 对象
  var image0 = new Image();
  var image1 = new Image();
  // 注册事件响应函数,在图形加载完成后调用
  image0.onload = function(){ loadTexture(gl, n, texture0, u_Sampler0, image0, 0); };
  image1.onload = function(){ loadTexture(gl, n, texture1, u_Sampler1, image1, 1); };
  image0.src = '../resources/sky.jpg';
  image1.src = '../resources/circle.gif';
  return true;
}
// 标记纹理单元是否已经就绪
var g_texUnit0 = false, g_texUnit1 = false; 
function loadTexture(gl, n, texture, u_Sampler, image, texUnit) {
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
  // 激活纹理
  if (texUnit == 0) {
    gl.activeTexture(gl.TEXTURE0);
    g_texUnit0 = true;
  } else {
    gl.activeTexture(gl.TEXTURE1);
    g_texUnit1 = true;
  }
  // 绑定纹理对象到目标
  gl.bindTexture(gl.TEXTURE_2D, texture);   

  // 设置纹理参数
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  // 设置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  
  gl.uniform1i(u_Sampler, texUnit);   // 将纹理单元编号传递给取样器
  
  // Clear <canvas>
  gl.clear(gl.COLOR_BUFFER_BIT);

  if (g_texUnit0 && g_texUnit1) {
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);   // Draw the rectangle
  }
}

上面是通过两个纹理中取出纹素颜色,来进行渲染的。使用两个纹素来计算最终的片元颜色(gl_FragColor)有很多方法,上面的例子是使用了颜色矢量的分量乘法。也就是两个矢量中对应的分量相乘作为新矢量的分量,如下图;在 GLSL ES 中,只需要将两个 vec4 变量简单相乘一下就可以达到目的:

avatar

评 论:

更新: 11/21/2020, 7:00:56 PM