第五章:颜色与纹理

衔接上一篇:第四章:高级变换与动画基础

收获

当你学习完下面内容后你会有如下收获:

  • 如何将顶点的其他坐标(非坐标)数据传入顶点着色器中(如:颜色等)。
  • 装配、光栅化、内插过程
  • 如何将图像(纹理)映射到图形或三维对象的表面上。

1. 颜色

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

在我们之前的案例中,我们都是创建一个缓冲区对象,在其中存储顶点的坐标数据。然后将缓冲区对象传入给顶点着色器,但是我们每一个点的尺寸都是固定的统一的。这样并不好,所以我们接下来将实现给每个点不同的尺寸。

1.1.1 创建多个缓冲区

还记得,之前我们是如何将多个顶点坐标一次性传入着色器中的吗?需要遵循的步骤如下:

  • 创建缓冲区对象。
  • 将缓冲区对象绑定到target上。
  • 将顶点坐标数据写入缓冲区对象中。
  • 将缓冲区对象分配给对应的attribute变量。
  • 开启attribute变量。

其实我们想实现给每个点不同的尺寸的话,只需要重复上面的操作即可。创建一个缓冲区对象用来传顶点坐标,创建一个缓冲区对象用来传顶点尺寸。 具体实现代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
    <script src="./utils.js"></script>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute float a_PointSize;
      void main () {
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      void main () {
        gl_FragColor = vec4(1.0, 1.0, 0, 1.0);
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');
      initShader(gl, vsSource, fsSource);

      // 准备顶点坐标和顶点尺寸
      const vertexPositions = new Float32Array([0, 0.3, -0.1, 0, 0.1, 0]);
      const vertexSizes = new Float32Array([30.0, 60.0, 90.0]);

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');

      // 顶点坐标缓冲区
      const vertexPositionsBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexPositionsBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertexPositions, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
      gl.enableVertexAttribArray(a_Position);

      // 顶点尺寸缓冲区
      const vertexSizeBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexSizeBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertexSizes, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
      gl.enableVertexAttribArray(a_PointSize);

      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.POINTS, 0, 3);
    </script>
  </body>
</html>

这里的utils.js第二章:WebGL入门这篇文章中有写到,暂时不需要我们理解里面的内容,用就好了。

上述步骤在WebGL系统的内部状态如下图所示:

1.1.2 gl.vertexAttribPointer()的步进和偏移参数

上面使用多个缓冲区对象向着色器传递多种数据,比较适合数据量不大的情况。如果顶点的个数比较多的话就难以维护,所以WebGL允许我们把顶点的坐标和尺寸数据打包到同一个缓冲区对象中。如下:

js 复制代码
// 顶点坐标和点的尺寸
const verticesSize = new Float32Array([
    0, 0.3, 30.0, // 第一个点
    -0.1, 0, 60.0 // 第二个点
    0.1, 0, 90.0 // 第三个点
])

我们可以将顶点坐标和点的尺寸放在一块并写入一个缓冲区对象中,但是WebGL是无法智能的区分哪些是坐标数据,哪些是尺寸数据的。所以需要我们告诉它该如何区分,我们可以使用vertexAttribPointer()方法的第5和第6个参数来实现。先回忆一下其方法的参数:

我们可以通过第5个参数stride告诉WebGL我们的点是如何划分的,也就是相邻顶点之间的间隔字节数。

js 复制代码
// 顶点坐标和点的尺寸
const verticesSize = new Float32Array([
    0, 0.3, 30.0, // 第一个点
    -0.1, 0, 60.0, // 第二个点
    0.1, 0, 90.0 // 第三个点
])
// 获取数组中元素字节数
const FSIZE = verticesSize.BYTES_PER_ELEMENT;

...
// 告诉WebGL我们相邻顶点之间的字节数为:FSIZE * 3
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0)
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, 0)

然后通过第6个参数offset告诉WebGL我们的每个顶点之间的数据类型(顶点坐标或顶点尺寸)是如何划分的。也就是每种数据类型的偏移字符大小。

js 复制代码
// 顶点坐标和点的尺寸
const verticesSize = new Float32Array([
    0, 0.3, 30.0, // 第一个点
    -0.1, 0, 60.0, // 第二个点
    0.1, 0, 90.0 // 第三个点
]);
// 获取数组中元素字节数
const FSIZE = verticesSize.BYTES_PER_ELEMENT;

...
// 告诉WebGL不同数据类型的偏移量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);

至此就能够实现只创建一个缓冲区对象,并将各个顶点的数据放到一个数组中传入缓冲区中。也能实现上面的效果。

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
    <script src="./utils.js"></script>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute float a_PointSize;
      void main () {
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      void main () {
        gl_FragColor = vec4(1.0, 1.0, 0, 1.0);
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');
      initShader(gl, vsSource, fsSource);

      // 顶点坐标和点的尺寸
      const verticesSize = new Float32Array([
        0, 0.3, 30.0, // 第一个点
        -0.1, 0, 60.0, // 第二个点
        0.1, 0, 90.0 // 第三个点
      ]);

      // 获取数组中元素字节数
      const FSIZE = verticesSize.BYTES_PER_ELEMENT;

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');

      // 顶点缓冲区
      const verticesSizeBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, verticesSizeBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, verticesSize, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
      gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_PointSize);

      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.POINTS, 0, 3);
    </script>
  </body>
</html>

WebGL系统会根据strideoffset参数,从缓冲区中正确地抽取出数据,依次赋值给着色器中的各个attribute变量,并进行绘制。WebGL系统内部行为如下:

1.1.3 修改颜色

至此,我们已经了解了将多种顶点数据信息传入顶点着色器的技术,下面我们将尝试修改各顶点的颜色。具体方法和步骤与之前相同,但是颜色是由片元着色器处理的属性,数据却在顶点着色器中,所以接下来我们将要了解顶点着色器与片元着色器之间如何传值

我们可以使用一个新的变量varying变量向片元着色器中传入数据,varying变量的作用是从顶点着色器向片元着色器传输数据。

js 复制代码
<script id="vertexShader" type="x-shader/x-vertex">
  attribute vec4 a_Color;
  varying vec4 v_Color;
  void main () {
    v_Color = a_Color;
  }
</script>
<script id="fragmentShader" type="x-shader/x-fragment">
  precision mediump float;
  varying vec4 v_Color;
  void main () {
    gl_FragColor = v_Color;
  }
</script>

设置不同位置大小颜色的顶点,代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
    <script src="./utils.js"></script>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec4 a_Color;
      attribute float a_PointSize;
      varying vec4 v_Color;
      void main () {
        gl_Position = a_Position;
        gl_PointSize = a_PointSize;
        v_Color = a_Color;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      varying vec4 v_Color;
      void main () {
        gl_FragColor = v_Color;
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const vsSource = document.getElementById('vertexShader').innerHTML;
      const fsSource = document.getElementById('fragmentShader').innerHTML;
      const gl = canvas.getContext('webgl');
      initShader(gl, vsSource, fsSource);

      // 顶点坐标、尺寸、颜色
      const vertices = new Float32Array([
        0.0, 0.5, 30.0, 1.0, 0.0, 0.0,
        -0.5, -0.5, 60.0, 0.0, 1.0, 0.0,
        0.5, -0.5, 90.0, 0.0, 0.0, 1.0,
      ]);

      // 获取数组中元素字节数
      const FSIZE = vertices.BYTES_PER_ELEMENT;

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
      const a_Color = gl.getAttribLocation(gl.program, 'a_Color');

      // 顶点缓冲区
      const verticesBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, verticesBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 6, 0);
      gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 6, FSIZE * 2);
      gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_PointSize);
      gl.enableVertexAttribArray(a_Color);

      gl.clearColor(0, 0, 0, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.drawArrays(gl.POINTS, 0, 3);
    </script>
  </body>
</html>

如果顶点着色器和片元着色器中有类型和变量名都相同的varying变量,那么顶点着色器赋予该变量的值会自动传入给片元着色器。如下图:

当我们修改drawArrays方法的一个参数类型为TRIANGLES时,效果如下:

1.2 顶点着色器与片元着色器数据传输细节

我们通过上面的代码了解了可以通过varying变量将颜色数据从顶点着色器传递到片元着色器中。但是我们并不了解这一传递过程中的细节,下面我们将好好了解一下这一过程。

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

在顶点着色器和片元着色器之间,有这样两个步骤

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

宏观上:

微观上:

gl.drawArrays()的参数n为3,顶点着色器将被执行3次。

第1步 :执行顶点着色器,缓冲区对象的第一个坐标(0.0,0.5)被传递给attribute变量a_Position。一旦一个顶点的坐标被赋值给了gl_Position,它就进入了图形装配区域,并暂时储存在那里。我们显示的赋予了a_Positionx分量和y分量,所以z与w分量都是默认值。实际上进入图形装配区域的坐标其实是(0.0,0.5,0.0,0.1)。

第2步:再次执行顶点着色器,类似地,将第2个坐标(-0.5,-0.5,0.0,1.0)传入并储存在装配区。

第3步:第3次执行顶点着色器,将第3个坐标(0.5,-0.5,0.0,1.0)传人并储存在装配区。现在,顶点着色器执行完毕,三个顶点坐标都已经处在装配区了。

第4步:开始装配图形。使用传入的点坐标,根据gl.drawArrays()的第一个参数信息(gl.TRIANGLES)来决定如何装配。

第5步 :显示在屏幕上的三角形是由片元(像素)组成的,所以还需要将图形转化为片元,这个过程被称为光栅化。光栅化之后,你可以看到光栅化后得到的组成三角形的片元。

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

1.2.2 调用片元着色器

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

1.2.3 varying变量的作用与内插过程

前面程序中指定的每个顶点的颜色不同,可最后绘制出来的是一个具有渐变色彩效果的三角形呢?

事实上,我们把顶点的颜色赋值给了顶点着色器中的varying变量v_Color,它的值被传给片元着色器中的同名、同类型变量如下图所示。但是,更准确地说,顶点着色器中的v_Color变量在传入片元着色器之前经过了内插过程 。所以,片元着色器中的v_Color变量和顶点着色器中的v_Color变量实际上并不是一回事,这也正是我们将这种变量称为"varying"(变化的)变量的原因。

我们在varying变量中为三角形的3个不同顶点指定了3种不同颜色,而三角形表面上这些片元的颜色值都是WebGL系统用这3个顶点的颜色内插出来的。

当两个顶点的的颜色不同时,他们之间的就会进行颜色值的内插。如下图所示:

在这个例子中RGBA中的R值从1.0降低为0.0,而B值则从0.0上升至1.0,线段上的所有片元的颜色值都会被恰当地计算出来------这个过程就被称为内插过程

2.纹理

至此,我们了解了如何绘制彩色的图形,以及颜色的内插过程。虽然这些方法强大,但是更复杂的情况下仍然不够用。例如我们想要弄一个逼真的砌墙(如下),如果你试图创建很多个三角形并指定它们的位置和颜色来模拟墙面上的坑坑洼洼,那将使你陷入苦海。

在三维图形学中,有一项很重要的技术可以解决这个问题,那就是纹理映射

纹理映射:所谓纹理映射就是将一张图像映射到几何图形的表面上去。

纹理:映射的图像又称为纹理图像或纹理。

纹素 :组成纹理图像的像素又称为纹素,每个纹素的颜色都是使用RGBRGBA格式编码。

2.1 WebGL中如何做纹理映射

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

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

第1步很简单,就是准备一张图片即可。第2步的映射方式是指用那块纹理像素覆盖几何图形的那块片元,这里我们需要使用纹理坐标来确定纹理图像的哪部分将覆盖到几何图形上。

2.1.1 纹理坐标

纹理坐标是一套新的坐标体系,纹理坐标系是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。WebGL系统中的纹理坐标系统是二维的,如图下图所示。为了将纹理坐标和广泛使用的x坐标和y坐标区分开来,WebGL使用st命名纹理坐标(st坐标系统)。

:纹理坐标与纹理图像的大小无关,不管是128 × 128还是128 × 258的图像,右上角的坐标始终都是(1.0,1.0)

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

如前所述,在WebGL中,我们通过纹理图像的纹理坐标与几何形体顶点坐标间的映射关系,来确认怎样将纹理图像贴上去,如下左图所示。

我们通过建立矩形四个顶点与纹理坐标对应关系,就获得了右图所示结果。

2.2 实现代码

现在,你应该已经大致了解纹理映射的原理了,接下来我们将用程序来实现。

2.2.1 设置纹理坐标

将纹理坐标传入顶点着色器,与将其他顶点数据(如颜色)传入顶点着色器的方法是相同的。

html 复制代码
<!-- 顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
  attribute vec4 a_Position;
  attribute vec2 a_TexCoord;
  varying vec2 v_TexCoord;
  void main () {
    gl_Position = a_Position;
    v_TexCoord = a_TexCoord;
  }
</script>
<!-- 片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
  // 这里只需要关注v_TexCoord的传递,其他的后面会有讲
  uniform sampler2D u_Sampler;
  varying vec2 v_TexCoord;
  void main () {
    gl_FragColor = texture2D(u_Sampler, v_TexCoord);
  }
</script>

上面示例中v_TexCoord是纹理的坐标,需要从顶点着色器中通过varying传递给片元着色器。和前面传递顶点尺寸步骤一样。sampler2D u_Sampler是定义了一个2D的纹理采样器,texture2D方法的作用是在片元着色器中获取纹理像素颜色。

html 复制代码
<script>
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 a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);
</script>

上示例就是将程序中的纹理坐标传入缓冲区中。

2.2.2 配置和加载纹理

js 复制代码
/**
 * 配置和加载纹理
 * @param {object} gl WebGL上下文对象
 * @param {number} n 顶点个数
 */
const initTextures = (gl, n) => {
  const texture = gl.createTexture();
  const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
}

上面示例gl.createTexture()方法可以创建纹理对象

调用该函数将会在WebGL中创建一个纹理对象,如下图所示。其中gl.TEXTURE0gl.TEXTURE7是管理纹理图像的8个纹理单元,每一个都与gl.TEXTURE_2D相关联,而后者也是绑定纹理时的纹理目标。

接着,请求浏览器加载纹理图像供WebGL使用,该纹理图像将会映射到矩形上。为此,我们需要使用Image对象。

js 复制代码
// utils.js
/**
 * 配置和加载纹理
 * @param {object} gl WebGL上下文对象
 * @param {number} n 顶点个数
 */
const initTextures = (gl, n) => {
  const texture = gl.createTexture();
  const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
  const image = new Image();
  // 注册图像加载事件的响应函数
  image.onload = () => {
    loadTexture(gl, texture, n, u_Sampler, image);
  }
  image.src = './images.png';
}

当浏览器加载图像完成之后就会调用loadTexture()方法,为WebGL配置纹理。

2.2.3 为WebGL配置纹理

js 复制代码
// utils.js
/**
 * 为WebGL配置纹理
 * @param {object} gl WebGL上下文对象
 * @param {object} texture 纹理对象
 * @param {number} n 顶点个数
 * @param {object} u_Sampler 取样器 
 * @param {object} image image对象
 */
const loadTexture = (gl, texture, n, 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.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  // 将0号纹理传递给着色器
  gl.uniform1i(u_Sampler, 0);

  gl.clearColor(0, 0, 0, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
}

图像Y反转

在使用图像之前,你必须先进行Y轴上的反转,因为WebGL坐标的Y轴与纹理坐标的Y轴刚好相反。如下图,

我们可以使用函数pixelStorei()将纹理图像进行反转,函数介绍如下:

激活纹理单元

WebGL通过一种称作纹理单元的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像。即使你的程序只需要使用一张纹理图像,也得为其指定一个纹理单元。

系统纹理单元的个数取决于硬件和浏览器的WebGL实现,默认情况下WebGL至少支持8个纹理单元,内置的gl.TEXTRUE0gl.TEXTRUE1...各代表一个纹理单元。

我们可以通过gl.activeTexture()方法来激活指定单元,函数介绍如下:

上面的loadTexture方法中的gl.activeTexture(gl.TEXTURE0)就是激活0号单元,WebGL内部如下图:

绑定纹理对象

我们还需要指定纹理对象使用哪种类型的纹理,并将这种类型的纹理绑定到纹理对象上。在WebGL中纹理分为两种类型:

  • gl.TEXTURE_2D:二维纹理。
  • gl.TEXTURE_CUBE_MAP:立方体纹理。

因为我们的示例是一张二维图像纹理,所以指定的类型也就是gl.TEXTURE_2D了。然后通过gl.bindTexture()进行绑定。

js 复制代码
gl.bindTexture(gl.TEXTURE_2D, textrue);

WebGl系统内部状态如下:

配置纹理对象的参数

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

方法 描述
方法方法 gl.TEXTURE_MAG_FILTER 当纹理的绘制范围比纹理本身更大时,会造成像素间的间隙,该参数就表示填充这些空隙的具体方法
缩小方法 gl.TEXTURE_MIN_FILTER 当纹理的绘制范围比纹理本身更小时,会造成部分像素需要剔除,该参数就表示具体的剔除像素的方法
水平填充方法 gl.TEXTURE_WRAP_S 这个参数表示,如何对纹理图像左侧或右侧的区域进行填充。
垂直填充方法 gl.TEXTURE_WRAP_T 这个参数表示,如何对纹理图像上方或下方的区域进行填充。

下面是四种纹理参数产生的效果:

以下是可以赋值给纹理参数纹理参数值

可以赋值给gl.TEXTURE_MAG_FILTERgl.TEXTURE_MIN_FILTER的纹理参数值:

可以赋值给gl.TEXTURE_WRAP_Sgl.TEXTURE_WRAP_T的纹理参数值:

js 复制代码
 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

配置好纹理参数后,WebGL系统内部状态如下:

将纹理图像分配给纹理对象

我们通过方法gl.texImage2D()方法将纹理图像分配给纹理对象。

这时,Image对象中的图像就从JS传入WebGL系统中,并存储在纹理对象中,如图所示

将纹理单元传递给片元着色器

我们通过指定纹理单元编号 将纹理对象传给u_Sampler

js 复制代码
gl.uniform1i(u_Sampler, 0);

WebGL系统内部状态图,如下所示:

完整代码如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body {
        margin: 0;
      }
    </style>
    <script src="./utils.js"></script>
  </head>
  <body>
    <canvas id="canvas"></canvas>
    <script id="vertexShader" type="x-shader/x-vertex">
      attribute vec4 a_Position;
      attribute vec2 a_TexCoord;
      varying vec2 v_TexCoord;
      void main () {
        gl_Position = a_Position;
        v_TexCoord = a_TexCoord;
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      precision mediump float;
      uniform sampler2D u_Sampler;
      varying vec2 v_TexCoord;
      void main () {
        gl_FragColor = texture2D(u_Sampler, v_TexCoord);
      }
    </script>
    <script>
      const canvas = document.getElementById('canvas');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const gl = canvas.getContext('webgl');
      const vertexSource = document.getElementById('vertexShader').innerHTML;
      const fragmentSource = document.getElementById('fragmentShader').innerHTML;
      initShader(gl, vertexSource, fragmentSource);

      // prettier-ignore
      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 FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;

      const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
      const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');

      const vertexTexCoordBuffer = gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
      gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
      gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
      gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
      gl.enableVertexAttribArray(a_Position);
      gl.enableVertexAttribArray(a_TexCoord);

      initTextures(gl, 4);
    </script>
  </body>
</html>

参考

WebGL编程指南

相关推荐
灵犀学长2 分钟前
解锁HTML5页面生命周期API:前端开发的新视角
前端·html·html5
江号软件分享11 分钟前
轻松解决Office版本冲突问题:卸载是关键
前端
致博软件F2BPM18 分钟前
Element Plus和Ant Design Vue深度对比分析与选型指南
前端·javascript·vue.js
慧一居士1 小时前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead1 小时前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码7 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子7 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年7 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子7 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina7 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试