WebGL 纹理颜色原理

巧了我就是萌 提交于 2020-03-26 06:03:12

本文由云+社区发表

作者:ivweb qcyhust

导语

WebGL绘制图像时,往着色器中传入颜色信息就可以给图形绘制出相应的颜色,现在已经知道顶点着色器和片段着色器一起决定着向颜色缓冲区写入颜色信息并最终呈现出来,那么这个过程是什么样,如果图形的颜色需要用现有图片来渲染那么又该如何操作?

颜色缓冲区

在绘制开始前,经常见到调用函数清空画布的代码gl.clear(gl.COLOR_BUFFER_BIT),清空画布的绘图区实际上就是用之前定义好的背景颜色将颜色缓冲的的颜色清除。颜色缓冲区中存放着需要显示到画布上的像素的颜色数据,它属于帧缓存的一部分,与深度缓存、模板缓存等一起决定着最终画布上图像的显示信息。

可以将颜色缓存区看成图像颜色存储器,在缓存区中以RGB或RGBA的格式存储着画布上每一个像素的颜色信息,各个像素点组合起来就构成了颜色缓存的矩形阵列。这个定义看起来与图片存储器是很相似的,颜色缓存为RGB或是RGBA每一个通道分配存放位数,其中RGB就是颜色数据,A表示alpha也就是该像素的透明度信息,颜色占用的位数值就是颜色深度,比如颜色深度为24位,表示每一个像素24位,一般24位的分配方案就是红色、蓝色、绿色各占8位,如果需要透明效果的话,可以采用32位颜色深度为alpha通道分配8位。

这里可以总结得出,画布上各个像素点呈现的颜色就是存放在颜色缓冲区的颜色信息所决定的,而绘制图形的颜色缓冲区的信息又是由顶点着色器决定。要知道颜色如何渲染就要深入分析着色器的工作过程。

图形装配

要绘制一个三角形,我们是这样定义着色器的:

// 顶点着色器
const VSHADER_SOURCE =
 `attribute vec4 a_Position;
  void main() {
    gl_Position = a_Position;
  }`;

// 片段着色器
const FSHADER_SOURCE =
 `void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }`;

之后通过gl.program将顶点position坐标传入顶点着色器,这就相当于在画布上确定了几个点的坐标信息,这些点需要用线条连接起来才能构成图形,这个由顶点坐标装配成几何图形的过程就叫做图形装配。

被装配的基本图形被称作图元,它包含点、线、面等基本几何图形。在调用WebGL的drawArrays或drawElements方法时作为参数传入,从而指定图元类型。

一个三角形的绘制过程拆分来看就是执行三次顶点着色器,将三个点坐标都传入装配区,根据绘制函数的图元参数gl.TRIANGLES将三个点装配成三角形,然后进入下一个过程——光栅化。

光栅化

简单来说,光栅化就是将图形转化成片元,可以理解成一个个像素。只有将图形转化成像素后才能交由片段着色器处理。

光栅化结束后,WebGL执行片段着色器。每执行一次片段着色器就处理一个片元,将该片元的颜色写入颜色缓冲区中,等到图形中所有的片元处理完毕画布上就得到了最后的图像。

如上面的例子,每一个片元都会被执行成红色,由这一个个红色像素组成的三角形也就是红色的。

如果要绘制一个多颜色三角图形又是一个什么过程呢?首先需要修改着色器的定义,也许可以这样:

// 顶点着色器
const VSHADER_SOURCE =
 `attribute vec4 a_Position;
  attribute vec4 a_Color;
  varying vec4 v_Color;
  void main() {
    gl_Position = a_Position;
    v_Color = a_Color;
  }`;

// 片段着色器
const FSHADER_SOURCE =
 `varying vec4 v_Color;
  void main() {
    gl_FragColor = v_Color;
  }`;

向顶点着色器传入顶点坐标和颜色两个数据,执行三次后得到三角形三个顶点的坐标和颜色,接下来通过图元装配得到一个三角形的图元,到了关键的光栅化这一步,该如何定义片元的颜色呢?WebGL采用一个叫做内插的过程来计算颜色的值。

以一条线为例来解释内插,两个端点分别为(1.0,0.0,0.0)和(0.0,1.0,0.0),从一端到另一端,R的值从1.0降到0.0,G的值由0.0升到1.0,线上的所有点颜色值都这样计算出来,实现了平滑的颜色渐变,这就是内插。

经过内插,图形的每一个片元都指定了自己的颜色,写入颜色缓冲区后呈现出来。

纹理贴图

如果要为WebGL创建更加复杂更加自然的现实效果,就需要采用贴图来将现成的图片贴到图形上。

图片容器中存放的也是一个个RGB或RGBA的像素,将图片的信息读取后存放在纹理对象或者说纹理图像中,纹理图像有自己的坐标系,坐标中每一个单元格就存放的纹理图像的像素信息,也被称作纹素。

将纹理图像的坐标转换到画布上图形的坐标的映射过程就是纹理映射,这个过程中,为图形顶点指定了纹理坐标,剩下的颜色由内插计算得出,写入颜色缓冲区后,图形的表面就被贴上了图像的颜色。

用一个案例来实现纹理贴图,现在要做的是:

  • 加载好需要的纹理图像
  • 设置纹理坐标
  • 对纹理进行配置
  • 片段着色器抽出纹素并赋值给片元

在这个例子中我选择提前加载图片。在这里要注意有的浏览器不允许访问本地文件,可以考虑自己搭建server或是开启浏览器访问本地文件。

function main() {
  const canvas = document.getElementById('webgl');

  const webgl = getWebGLContext(canvas);
  webgl.images = {};

  // 初始化之前先加载图片
  loadImage([
    `src/images/0.jpeg`,
  ], webgl).then((gl) => {
    if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
      console.log('Failed to intialize shaders.');
      return;
    }

    gl.clearColor(0, 0, 0, 1);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    const n = initVertexBuffers(gl);
    initTextures(gl, n, 0);
  });
}

loadImage的实现很简单,用一个promise来处理异步加载图片,传入数组为了之后支持多张图片。在initVertexBuffers中创建数据buffer,将图形顶点和纹理图像坐标一起传入着色器。

function initVertexBuffers(gl) {
  // 顶点坐标和纹理图像坐标
  const vertices = new Float32Array([
    -0.3, 0.7, 0.0, 0.0, 1.0,
   -0.3, -0.7, 0.0, 0.0, 0.0,
    0.3, 0.7, 0.0, 1.0, 1.0,
    0.3, -0.7, 0.0, 1.0, 0.0,
  ]);

  const FSIZE = vertices.BYTES_PER_ELEMENT;

  const vertexBuffer = gl.createBuffer();

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

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

  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 5, 0);
  gl.enableVertexAttribArray(a_Position);

  gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 5, FSIZE * 3);
  gl.enableVertexAttribArray(a_TexCoord);

  return 4;
}

然后看看最主要的initTextures,在这里配置纹理:

function initTextures(gl, n, index) {
  // 创建纹理对象
  const texture = gl.createTexture();
  const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
  const image = gl.images[index];

  // WebGL纹理坐标中的纵轴方向和PNG,JPG等图片容器的Y轴方向是反的,所以先反转Y轴
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

  // 激活纹理单元,开启index号纹理单元
  gl.activeTexture(gl[`TEXTURE${index}`]);

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

  // 配置纹理对象的参数
  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.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, index);

  // 绘制
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
}

这里又遇到两个概念:

纹理对象配置参数 texParameteri方法用来配置纹理对象参数,函数第二个参数传入配置参数名,第三个参数传入配置参数值,可以配置的参数有:

  • 伸展(gl.TEXTURE_MAX_FILTER): 绘制图形比纹理图像大的时候怎么取纹素,默认值gl.LINEAR
  • 收缩(gl.TEXTURE_MIN_FILTER): 绘制图形比纹理图像小的时候怎么取纹素, 默认值gl.NEAREST_MIP_LINEAR
  • 水平填充(gl.TEXTURE_WRAP_S): 定义绘制图形水平方向如何填充,默认值gl.REPEAT
  • 垂直填充(gl.TEXTURE_WRAP_T): 定义绘制图形垂直方向如何填充,默认值gl.REPEAT

详细参考texParameteri

纹理单元 如果需要使用多张图片就要管理多个纹理图片,WebGL为了使用多个纹理,用纹理单元来处理纹理图像。WebGL的实现至少支持8个纹理单元,分别用gl.TEXRTRUE0,gl.TEXRTRUE1,...,gl.TEXRTRUE7来表示。

最后是着色器代码,在调用gl.drawArrays传入图元类型TRIANGLE_STRIP后执行:

const VSHADER_SOURCE =
 `attribute vec4 a_Position;
  attribute vec2 a_TexCoord;
  varying vec2 v_TexCoord;
  void main() {
    gl_Position = a_Position;
    v_TexCoord = a_TexCoord;
  }`;

const FSHADER_SOURCE =
 `precision mediump float;
  uniform sampler2D u_Sampler;
  varying vec2 v_TexCoord;
  void main() {
    gl_FragColor = texture2D(u_Sampler, v_TexCoord);
  }`;

顶点着色器中传入纹理图像的顶点坐标,将它传递给片段着色器,在片段着色器中声明了一个专用于纹理对象的数据类型sampler2D,指向一个纹理单元编号(接下来解释),着色器获取纹素由函数texture2D完成,传入参数纹理单元编号和纹理图像坐标。

多纹理实现

要使用多个纹理就要用到更多的纹理单元,多个纹理可以组合也可以单独渲染,利用前面的代码,可以很容易扩展成一起多纹理的案例,加上一些3D效果和动画,就可以组合成一个轮播图片。

此文已由腾讯云+社区在各渠道发布

获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!