【零基础学WebGL】绘制图片

前言

通过本文的学习,你可以掌握使用WebGL绘制任何一张图片。下图是本文的示例图片:

【零基础学WebGL】绘制图片_第1张图片

理论知识

在绘图之前,一起学习一些理论知识吧。

规范化设备坐标

规范化设备坐标 (Normalized Device Coordinates),直译就是经过归一化处理的坐标系统,这里device指的是WebGL的最大绘制区域,对于前端来说,也就是canvas。 规范化设备坐标x/y轴的范围都是从-1到1,(0,0)坐标在canvas的中心。

【零基础学WebGL】绘制图片_第2张图片

编辑切换为居中

规范化设备坐标系统

所以,在上一章节,给顶点着色器设置顶点坐标,实际效果就是绘制一个和canvas一样大的矩形。

const vertexPostion = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]; 
setAttribute(gl, program, vertexPostion, "a_position"); 

纹理坐标

纹理坐标(Texture Coordinate),横轴用s表示,纵轴用t表示,两者的范围都是从0到1,(0,0)在图片的左下角。所以,纹理坐标也是一种归一化的坐标系统。

【零基础学WebGL】绘制图片_第3张图片

存储限定符:uniform、attribute、varying

uniform变量,用来表示一致不变的数据(每个顶点中该值都一样),可以在顶点着色器中使用,也可以在片元着色器中使用。可以通过js程序给着色器的uniform变量传值。比如下面:

// fragment 
const fragmentSource = ` 
  ... 
  // u_image表示一个纹理图片,在所有的片元着色器,u_image值都是相同的。 
  uniform sampler2D u_image; 
 
  void main() { 
    ... 
  } 
`; 
 
// js 
const sampler = gl.getUniformLocation(program, "u_image"); 
gl.uniform1i(sampler, 0); 

attribute变量,用来表示和顶点相关的数据,只可以在顶点着色器中使用。可以通过js程序给顶点着色器传递一个数组缓存对象,然后顶点着色器会从数组缓存对象中逐一获取顶点位置,赋值给attribute变量,所以attribute变量的值在不同的顶点中是不同的。

// vertex 
const vertextSource = ` 
  attribute vec2 a_position; 
  void main(void) { 
    gl_Position = vec4(a_position, 0, 1.0); 
  } 
`; 
 
// js 
const buffer = gl.createBuffer(); 
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); 
const vertexPostion = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]; 
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPostion), gl.STATIC_DRAW); 
// 获取顶点属性的在着色器中的索引,并激活它 
const aVertexPositionLocation = gl.getAttribLocation(program, attribute); 
gl.enableVertexAttribArray(aVertexPositionLocation); 
// 设置顶点属性如何从顶点缓冲对象中取值。每次从数组缓冲对象中读取2个值 
gl.vertexAttribPointer(aVertexPositionLocation, 2, gl.FLOAT, false, 0, 0); 

varying变量,作用是从顶点着色器向片元着色器传值,只要在片元着色器中也声明同名varying变量,顶点着色器赋给该变量的值就会自动传入片元着色器。 在顶点着色器把varying变量传给片元着色器之前,会进行内插处理。我们可以使用varying的特点,设置顶点的纹理坐标,然后通过内插,得到顶点之间的片元像素的纹理坐标。

// vertex 
const vertextSource = ` 
  attribute vec2 a_texCoord; 
  varying vec2 v_texCoord; 
  void main(void) { 
    v_texCoord = a_texCoord; 
  } 
`; 
 
// fragment 
const fragmentSource = ` 
  precision mediump float; 
  varying vec2 v_texCoord; 
  uniform sampler2D u_image; 
 
  void main() { 
    gl_FragColor = texture2D(u_image, v_texCoord); 
  } 
`; 
 
// js 
// js不能直接给varying变量赋值,只能先给attribute变量赋值, 
// 然后在顶点着色器代码中,把attribute赋值给varying 
const txtCoordData = [ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0 ]; 
setAttribute(gl, program, txtCoordData, "a_texCoord"); 

准备工作

下载图像

学习前面的理论知识之后,就可以开始写代码了。 首先,你需要得到一个HTMLImageElement对象。值得一提的是:图片域名和H5应用域名必须同源。

var image = new Image(); 
image.src = "./bcy.png"; // MUST BE SAME DOMAIN!!! 
image.onload = function () { 
  render(image); // 绘制图片 
}; 

绘制矩形

接下来,仍然需要创建一个WebGL程序,然后绘制一个矩形(大小和canvas一样)。和【零基础学WebGL】绘制一个矩形区别在于,我们需要给矩形表皮贴一张图片,而不是一个固定颜色。如下代码和上一章节绘制矩形相同。

const render = (image: HTMLImageElement) => { 
  // 环境准备 
  const gl = createContext("container", 300, 300); 
  .... 
 
  // 设置矩形顶点坐标 
  const vertexPostion = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]; 
  setAttribute(gl, program, vertexPostion, "a_position"); 
  ... 
 
  // 设置纹理 
  ... 
 
  // 绘制 
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 
}; 

设置纹理

纹理坐标

首先,需要改造之前的顶点着色器代码。

const vertextSource = ` 
  attribute vec2 a_position; 
  attribute vec2 a_texCoord; 
  varying vec2 v_texCoord; 
  void main(void) { 
    gl_Position = vec4(a_position, 0, 1.0); 
    v_texCoord = a_texCoord; 
  } 
`; 
  • 新增attribute变量a_texCoord,表示顶点的纹理坐标;
  • 新增varying变量v_texCoord,对顶点纹理坐标进行插值,然后传递给片段着色器,用来获取纹理像素。

本示例希望图片可以铺满整个矩形。因此,顶点的纹理坐标和顶点坐标之间的关系如下图所示:

【零基础学WebGL】绘制图片_第4张图片

  • 顶点(-1,-1)对应的纹理坐标是(0,0);
  • 顶点(1,-1)对应的纹理坐标是(1,0);
  • 顶点(-1,1)对应的纹理坐标是(0,1);
  • 顶点(1,1)对应的纹理坐标是(1,1).
const render = (image: HTMLImageElement) => { 
  ... 
  // 设置顶点坐标 
  const vertexPostion = [-1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0]; 
  setAttribute(gl, program, vertexPostion, "a_position"); 
 
  // 设置纹理 
  // 设置纹理坐标 
  const txtCoordData = [ 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0 ]; 
  setAttribute(gl, program, vertexPostion, "a_texCoord"); 
 
  ... 
}; 

纹理单元

接下来,需要修改片元着色器,使得它知道使用哪个图片作为纹理。

const fragmentSource = ` 
  precision mediump float; 
  varying vec2 v_texCoord; 
  uniform sampler2D u_image; 
 
  void main() { 
    gl_FragColor = texture2D(u_image, v_texCoord); 
  } 
`; 

在片元着色器中,定义了sampler2D类型的变量u_image,sampler2D直译为纹理采样器,实际上sampler2D绑定的值是纹理单元,一个数字,表示纹理的编号。 WebGL 要求实现支持至少 8 个纹理单元,从0开始。你可以通过下面代码查询支持的数量:

const maxTextureUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS); 

那如何给sampler2D绑定纹理单元了?需要经过如下步骤:

  • 激活你想用的纹理单元,我这里使用0号纹理单元:gl.TEXTURE0;
  • 创建纹理对象,并绑定到目标点gl.TEXTURE_2D;
  • 获取uniform的索引位置,将其绑定到纹理单元。
const render = (image: HTMLImageElement) => { 
  ... 
  // 设置纹理 
  gl.activeTexture(gl.TEXTURE0); // 激活 
 
  var texture = gl.createTexture(); // 创建纹理对象 
  gl.bindTexture(gl.TEXTURE_2D, texture); 
 
  const sampler = gl.getUniformLocation(program, "u_image"); // 获取纹理采样器索引 
  gl.uniform1i(sampler, 0); // 绑定纹理单元 
 
  ... 
}; 

纹理参数

下面介绍一些常用的纹理参数:

参数名 描述 参数值
gl.TEXTURE_MAG_FILTER 纹理放大滤波器 gl.LINEAR (默认值):表示线性插值 gl.NEAREST:表示取最近的纹理像素值作为屏幕像素的值
gl.TEXTURE_MIN_FILTER 纹理缩小滤波器 gl.LINEAR,gl.NEAREST, gl.NEAREST_MIPMAP_NEAREST, gl.LINEAR_MIPMAP_NEAREST, gl.NEAREST_MIPMAP_LINEAR (默认值), gl.LINEAR_MIPMAP_LINEAR.
gl.TEXTURE_WRAP_S 纹理坐标水平填充方式 gl.REPEAT (默认值):复制0-1对应位置的纹理像素进行填充 gl.CLAMP_TO_EDGE:使用边缘的纹理像素值填充 gl.MIRRORED_REPEAT:镜像复制0-1对应位置的纹理像素进行填充
gl.TEXTURE_WRAP_T 纹理坐标垂直填充方式 gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT.

在webgl中,使用gl.texParameteri进行纹理参数设置。各位可以自行修改参数,感官上理解每个参数的实际意义。

const render = (image: HTMLImageElement) => { 
  ... 
  // 设置纹理 
  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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); 
 
  ... 
}; 

纹理图像

最后,需要调用gl.texImage2D把image绑定到纹理上。在绑定之前,你需要调用gl.pixelStorei对图像进行预处理,值gl.UNPACK_FLIP_Y_WEBGL表示把图片上下对称翻转坐标轴。

【零基础学WebGL】绘制图片_第5张图片

纹理坐标和图片像素读取关系

const render = (image: HTMLImageElement) => { 
  ... 
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); 
 
  // Upload the image into the texture. 
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); 
 
  // 绘制 
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); 
}; 

你可能感兴趣的:(前端)