WebGL 纹理颜色原理

本文由云+社区发表web

做者:ivweb qcyhustcanvas

导语

WebGL绘制图像时,往着色器中传入颜色信息就能够给图形绘制出相应的颜色,如今已经知道顶点着色器和片断着色器一块儿决定着向颜色缓冲区写入颜色信息并最终呈现出来,那么这个过程是什么样,若是图形的颜色须要用现有图片来渲染那么又该如何操做?数组

img

颜色缓冲区

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

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

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

img

图形装配

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

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

光栅化

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

光栅化结束后,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的像素,将图片的信息读取后存放在纹理对象或者说纹理图像中,纹理图像有本身的坐标系,坐标中每个单元格就存放的纹理图像的像素信息,也被称做纹素。

img

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

img

用一个案例来实现纹理贴图,如今要作的是:

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

在这个例子中我选择提早加载图片。在这里要注意有的浏览器不容许访问本地文件,能够考虑本身搭建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完成,传入参数纹理单元编号和纹理图像坐标。

img

多纹理实现

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

img

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

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

相关文章
相关标签/搜索