WebGL+Three.js—第三章 WebGL颜色和纹理

443 阅读9分钟

3.1 使用varying变量—绘制彩色三角形

3.1.1 使用varying变量

    1、声明varying变量

        声明varying变量的时候,需要在顶点着色器和片元着色器两边同时声明,而且类型和名称都需要一致。

// 顶点着色器
const VERTEX_SHADER_SOURCE = `
  attribute vec4 aPosition;
  varying vec4 vColor;
  void main() {
    gl_Position = aPosition;
  }
`;

// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  varying vec4 vColor;
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
`;

    2、给varying变量赋值

        varying变量的作用是从顶点着色器向片元着色器传递数据。声明了vColor变量之后,就可以在顶点着色器的main函数里进行赋值,然后在片元着色器使用vColor变量。

// 顶点着色器
const VERTEX_SHADER_SOURCE = `
  attribute vec4 aPosition;
  varying vec4 vColor;
  void main() {
    vColor = aPosition;
    gl_Position = aPosition;
  }
`;

// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  varying vec4 vColor;
  void main() {
    gl_FragColor = vColor;
  }
`;

3.1.2 执行流程

image.png

3.1.3 代码示例

3.2 从顶点到图形—webgl渲染流程介绍

image.png

    1、图元装配

        将独立的顶点坐标装配成几何图形,图形的类别由gl.drawArrays()第一个参数确定。

    2、光栅化

        这一步是将装配好的图形转换为片元。

        (1)剔除

            对于不透明物体,背面对于观察者来说是不可见的。那么在渲染过程中,就会将不可见的部分剔除,不参与绘制。节省渲染开销。

        (2)裁剪

            在可视范围之外的事物是看不到的。图形生成后,有的部分可能位于可视范围之外,这一部分会被剪裁掉,不参与绘制。

    3、渲染流程

        图形所有的片元,它是逐个片元去经过片元着色器和颜色缓冲区的处理,然后绘制到浏览器里。

64829a23-d361-424c-91df-0481b7e5004d.gif

3.3 给图形添加背景图

3.3.1 纹理坐标

    在canvas绘制图片的时候,首先new一个Image对象,然后设置Image实例的onload,再添加它的src属性,在onload函数里做一些绘制操作。

    webgl相比canvas相对复杂一些。canvas的坐标跟图片的坐标是一致的,canvas以向下向右为正方向,图片也一样,它不需要特殊处理图片的坐标信息。在webgl的坐标系统跟canvas的不同,所以在使用之前需要做一定的转换,转换之后的图片坐标称为纹理坐标。

image.png

    纹理坐标也称为st坐标,当前的原点位置在图片的左下角,往上为图片y轴的正方向,向右是图片x轴的正方向。将图片坐标转为纹理坐标之后,需要通过纹理的坐标和图形顶点坐标的映射关系来确定贴图,如下图所示:

image.png

3.3.2 添加背景图流程

    1、实例化Image对象

const img = new Image();
img.onload = function() {
}
img.src = '../aasets/border.png';

    2、创建纹理对象

        通过gl.createTexture()方法创建纹理对象,主要是用于存储纹理图像数据。也可以通过gl.deleteTexture(texture)来删除纹理对象。

img.onload = function() {
  // 创建纹理对象
  const texture = gl.createTexture();
}

    3、翻转图片y轴

        图片原始的坐标是在左上角,而webgl的原点坐标是左下角,因此要让图片坐标跟webgl一致,就需要将它的y轴翻转。

        可以通过gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)来实现y轴的翻转。

image.png

img.onload = function() {
  // 创建纹理对象
  const texture = gl.createTexture();
  // 翻转图片Y轴
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
}

    4、开启(激活)纹理单元

        Webgl是通过纹理单元来管理纹理对象,每个纹理单元管理一张纹理图像。

        可以通过gl.activeTexture(gl.TEXTURE0)方法来实现激活,WebGL默认至少支持8个纹理单元,分别对应TEXTURE0/1/2/3......,这里的gl.TEXTURE0指的就是纹理单元0,要使用它必须先激活它。

img.onload = function() {
  // 创建纹理对象
  const texture = gl.createTexture();
  // 翻转图片Y轴
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
  // 开启一个纹理单元
  gl.activeTexture(gl.TEXTURE0);
}

    5、绑定纹理对象

        可以通过gl.bindTexture(type, texture)来实现绑定。texture是纹理对象,type参数有以下两种:

        (1)gl.TEXTURE_2D:二维纹理

        (2)gl.TEXTURE_CUBE_MAP:立方体纹理

img.onload = function() {
  // 创建纹理对象
  const texture = gl.createTexture();
  // 翻转图片Y轴
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
  // 开启一个纹理单元
  gl.activeTexture(gl.TEXTURE0);
  // 绑定纹理对象
  gl.bindTexture(gl.TEXTURE_2D, texture);
}

    6、设置纹理参数

        这一步是在图形放大缩小的时候,对应获取纹理的方式。可以通过gl.texParameteri(type, pname, param)来处理。

        (1)type:参数同上

        (2)pname:纹理参数

            1.gl.TEXTURE_MAG_FILTER:放大

            2.gl.TEXTURE_MIN_FILTER:缩小

            3.gl.TEXTURE_WRAP_S:横向(水平填充)

            4.gl.TEXTURE_WRAP_T:纵向(垂直填充)

        (3)param

            param参数要根据pname参数的设置而定。例如pname设置了gl.TEXTURE_MAG_FILTER或者gl.TEXTURE_MIN_FILTER(放大或缩小),那么param可以设置gl.NEAREST和gl.LINEAR。

image.png

image.png

img.onload = function() {
  ......

  // 处理放大缩小的逻辑
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  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_T, gl.CLAMP_TO_EDGE);
}

        参考网站:blog.csdn.net/lufy_Legend…

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

        可以通过gl.texImage2D(type, level, internalformat, format, dataType, image)来配置纹理图像。

        (1)type:参数同上

        (2)level:为0即可

        (3)internalformat:图像的内部格式

image.png

        (4)format:纹理的内部格式,必须和internalformat相同

        (5)dataType:纹理数据的数据类型

image.png

            1.gl.UNSIGNED_BYTE:无符号整型,它所代表的是每个颜色分量占据一个字节

            2.gl.UNSIGNED_SHORT_5_6_5:它代表rgb分量,分别占据5、6、5比特

            3.gl.UNSIGNED_SHORT_4_4_4_4:整型:它代表rgba分量,分别占据4、4、4、4比特

            4.gl.UNSIGNED_SHORT_5_5_5_1:整型:它代表rgba分量,分别占据5、5、5、1比特

        (6)image:图片对象

img.onload = function() {
  ......
  // 配置纹理图像
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);
}

    8、片元着色器添加变量接收数据

        声明一个uSampler变量来接收纹理,sampler2D类型代表的是接收的纹理是2D纹理,也可以通过samplerCube来接收立方体纹理。

        声明好变量之后,就可以使用texture2D(uSampler, 纹理坐标)方法来从图像里读片元获取内容。

// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  uniform sampler2D uSampler;
  
  void main() {
    gl_FragColor = texture2D(uSampler, 纹理坐标);
  }
`;

    9、设置纹理坐标

        这个texture2D里的纹理坐标,可以通过数据偏移量的方式来实现。

        关于数据偏移量可参考:2.2 多缓冲区和数据偏移

// 顶点着色器
const VERTEX_SHADER_SOURCE = `
  attribute vec4 aPosition;
  attribute vec4 aTex;
  void main() {
    // 要绘制的点的坐标
    gl_Position = aPosition;    // vec4(0.0,0.0,0.0,1.0)
  }
`;
const aPosition = gl.getAttribLocation(program, 'aPosition');
const aTex = gl.getAttribLocation(program, 'aTex');
const points = 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 BYTES = points.BYTES_PER_ELEMENT;
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 4, 0);
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aTex, 2, gl.FLOAT, false, BYTES * 4, BYTES * 2);
gl.enableVertexAttribArray(aTex);

    10、将纹理坐标传到片元着色器

        由于纹理坐标目前是在顶点着色器接收,它想传到片元着色器,就需要使用varying变量。texture2D纹理坐标接收的数据类型是vec2,因此varying变量的类型也是vec2。

        声明变量之后,还要将顶点着色器的纹理坐标赋值给这个varying变量。

// 顶点着色器
const VERTEX_SHADER_SOURCE = `
  attribute vec4 aPosition;
  attribute vec4 aTex;
  varying vec2 vTex;

  void main() {
    // 要绘制的点的坐标
    gl_Position = aPosition;    // vec4(0.0,0.0,0.0,1.0)
    vTex = vec2(aTex.x, aTex.y);
  }
`;

// 片元着色器
const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  uniform sampler2D uSampler;
  varying vec2 vTex;

  void main() {
    gl_FragColor = texture2D(uSampler, vTex);
  }
`;

    11、纹理单元传递给着色器

        可以通过gl.uniform1i(uSampler, 纹理下标)方法进行传递,第一个参数对应片元着色器接收纹理的uSampler变量(参考第8步),第二个参数是激活纹理的下标,目前使用的是gl.TEXTURE0,即纹理下标为0(参考第4步)。

        gl.uniform1i的i代表的是整型,如果uniform1f的f则代表浮点型。

const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  uniform sampler2D uSampler;
  varying vec2 vTex;

  void main() {
    gl_FragColor = texture2D(uSampler, vTex);
  }
`;
const uSampler = gl.getUniformLocation(program, 'uSampler');
img.onload = function() {
  ......
  gl.uniform1i(uSampler, 0);
}

    注意:图片的大小尽量是2的整数幂,例如1024*1024。因为在光栅化的过程中,需要对纹理采样进行快速地取值,如果不是2的整数幂,它也可以显示出来,在贴图之前会将图片进行纹理的拉伸或者压缩到2的整数幂。

3.3.3 代码示例

3.4 使用多重纹理

    多重纹理指的是将多个图片填充到同一个图形上。

    1、声明多个sampler2D类型变量

        图形是通过声明一个sampler2D类型的变量来接收纹理,如果有多个纹理,则需要声明多个变量来一一对应接收。

const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  uniform sampler2D uSampler;
  uniform sampler2D uSampler1;
  varying vec2 vTex;

  void main() {
    gl_FragColor = texture2D(uSampler, vTex);
  }
`;

    2、接收多个纹理内容

        图形是通过声明一个sampler2D类型的变量来接收纹理,如果有多个纹理,则需要声明多个变量来一一对应接收。

        片元着色器本来是通过texture2D(uSampler, 纹理坐标)方法读取片元内容。现在声明了多个变量之后,则需要先执行多个texture2D方法进行一一读取。然后将它们的结果相乘再赋值给gl_FragColor。

const FRAGMENT_SHADER_SOURCE = `
  precision lowp float;
  uniform sampler2D uSampler;
  uniform sampler2D uSampler1;
  varying vec2 vTex;

  void main() {
    vec4 c1 = texture2D(uSampler, vTex);
    vec4 c2 = texture2D(uSampler1, vTex);
    gl_FragColor = c1 * c2;
  }
`;

    3、新增两个图片纹理方法

        目前将两个纹理图片同时填充到一个图形,需要执行两遍图片纹理加载的流程。新增两个方法分别对应两个流程,然后使用Promise返回纹理加载结果。

const uSampler = gl.getUniformLocation(program, 'uSampler');
const uSampler1 = gl.getUniformLocation(program, 'uSampler1');

function getImage() {
  return new Promise(resolve => {
    const img = new Image();
    img.onload = function() {
      // 创建纹理对象
      const texture = gl.createTexture();
      // 翻转图片Y轴
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
      // 开启一个纹理单元
      gl.activeTexture(gl.TEXTURE0);
      // 绑定纹理对象
      gl.bindTexture(gl.TEXTURE_2D, texture);
      // 处理放大缩小的逻辑
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      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_T, gl.CLAMP_TO_EDGE);
      // 配置纹理图像
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);

      gl.uniform1i(uSampler, 0);
      resolve();
    }
    img.src = '../assets/border.png';
  });
}
function getImage1() {
  return new Promise(resolve => {
    const img = new Image();
    img.onload = function () {
      // 创建纹理对象
      const texture = gl.createTexture();
      // 翻转图片Y轴
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
      // 开启一个纹理单元
      gl.activeTexture(gl.TEXTURE1);
      // 绑定纹理对象
      gl.bindTexture(gl.TEXTURE_2D, texture);
      // 处理放大缩小的逻辑
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      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_T, gl.CLAMP_TO_EDGE);
      // 配置纹理图像
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);

      gl.uniform1i(uSampler1, 1);
      resolve();
    }
    img.src = '../assets/content.png';
  });
}

    4、监听所有纹理流程执行完成

        可以通过Promise.all()方法来监听两张纹理图片流程是否全部执行完成,最后才执行绘制图形方法。

Promise.all([getImage(), getImage1()]).then(() => {
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
});

    5、整合各个纹理加载流程方法

        目前通过两个方法分别执行两段纹理加载流程,流程的内容只有激活纹理单元、传递给着色器的变量、图片路径3个不相同,可以通过传参的方式整合在一起。

/**
 * 加载纹理图片流程
 * @url: 图片路径
 * @location: 着色器接收变量
 * @index: 激活的纹理单元下标
 * */
function getImage(url, location, index) {
  return new Promise(resolve => {
    const img = new Image();
    img.onload = function() {
      // 创建纹理对象
      const texture = gl.createTexture();
      // 翻转图片Y轴
      gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
      // 开启一个纹理单元
      gl.activeTexture(gl[`TEXTURE${index}`]);
      // 绑定纹理对象
      gl.bindTexture(gl.TEXTURE_2D, texture);
      // 处理放大缩小的逻辑
      gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
      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_T, gl.CLAMP_TO_EDGE);
      // 配置纹理图像
      gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);

      gl.uniform1i(location, index);
      resolve();
    }
    img.src = url;
  });
}

Promise.all([getImage('../assets/border.png', uSampler, 0), getImage('../assets/content.png', uSampler1, 1)]).then(() => {
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
});

    6、代码示例