通过本文的学习,你可以掌握使用WebGL绘制任何一张图片。下图是本文的示例图片:
在绘图之前,一起学习一些理论知识吧。
规范化设备坐标 (Normalized Device Coordinates),直译就是经过归一化处理的坐标系统,这里device指的是WebGL的最大绘制区域,对于前端来说,也就是canvas。 规范化设备坐标x/y轴的范围都是从-1到1,(0,0)坐标在canvas的中心。
编辑切换为居中
规范化设备坐标系统
所以,在上一章节,给顶点着色器设置顶点坐标,实际效果就是绘制一个和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)在图片的左下角。所以,纹理坐标也是一种归一化的坐标系统。
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;
}
`;
本示例希望图片可以铺满整个矩形。因此,顶点的纹理坐标和顶点坐标之间的关系如下图所示:
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绑定纹理单元了?需要经过如下步骤:
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表示把图片上下对称翻转坐标轴。
纹理坐标和图片像素读取关系
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);
};