大家好,我是前端西瓜哥,今天我们来了解 WebGL 的纹理对象(Texture)
纹理对象,是将像素(texels)以数组方式传给 GPU 的对象,常见场景是贴图,就是将图片的数据应用到 3D 物体上。
纹理对象创建和绑定
先创建纹理对象:
const texture = gl.createTexture(); // 创建纹理对象
然后绑定到纹理单元:
gl.bindTexture(gl.TEXTURE_2D, texture); // 将纹理对象绑定上去
填充方式
纹理是要贴到画布的某个区域上的,并不一定刚好设置一下填充方式。
纹理比绘制区域大,就要做缩放;纹理比绘制区域小,就要做放大;纹理没能完全填充绘制区域,就要在水平和垂直方向进行填充。
这些场景都需要对应设置不同的策略。
// 缩小和放大都都使用 “最近点采样”
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
纹理单元
WebGL 支持设置多个纹理单元(Texture Unit),即我们可以将多个图片放到多个单元中,然后进行切换。
就好像手里拿着不同的盖章,想印哪种图案就掏出哪个盖上去。
纹理单元是有上限的,至少要支持 8 个,主流浏览器一般支持 16 个。
具体支持几个,可通过下面代码获得。
gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) // 通常是 16
默认使用 0 号纹理单元,可通过下面这一行代码来切换纹理单元:
gl.activeTexture(gl.TEXTURE1); // 开启 1 号纹理单元
注意这个要 在将纹理对象绑定纹理单元之前 执行。
最后我们需要设置一下我们的纹理采样器选择使用哪个纹理单元:
gl.uniform1i(u_Sampler, 0); // 开启 0 号纹理对象
不主动调用这个方法,默认会使用 0 号纹理单元。
切换纹理单元是有一定的性能代价的,不建议你在短时间内不断地切换纹理单元。简单的渲染场景可忽略不计。
纯色纹理
画个纯纯的红色纹理。
// 红色
const data = new Uint8Array([
255, 0, 0
]);
gl.texImage2D(
gl.TEXTURE_2D, // 纹理目标,这里是二维纹理
0, // 细节级别,0 表示最高级别
gl.RGB, // 纹理内部格式,还支持其他的比如 gl.RGBA、LUMINANCE(流明)
1, // 宽(宽高的单位为像素,且为 2 的 n 次幂)
1, // 高
0, // 是否描边。必须为 0(但 opengl 支持)
gl.RGB, // 源图像数据格式
gl.UNSIGNED_BYTE, // 纹素(单个像素)数据类型
data // 数据数组,一个个像素点
);
主要注意的是,gl.texImage2D() 方法支持函数重载,有多种传入的参数的方式,注意分辨。具体看 官方文档。
这里选择使用 gl.RGB 格式,设置了一个 (255, 0, 0) 的红色颜色值。
最后我们成功画出一个纯红色块。
完整代码:
/** @type {HTMLCanvasElement} */
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl');
const vertexShaderSrc = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
varying vec2 v_TexCoord;
void main() {
gl_Position = a_Position;
v_TexCoord = a_TexCoord;
}
`;
const fragmentShaderSrc = `
precision highp float;
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main() {
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`;
// 创建程序对象
createProgram(gl);
// 顶点坐标,纹理坐标
const verticesTexCoords = new Float32Array([
// 左上点。
// 左边两个是顶点;右边两个是纹理
-0.5, 0.5, 0.0, 1,
// 左下
-0.5, -0.5, 0.0, 0.0,
// 右上
0.5, 0.5, 1, 1,
// 右下
0.5, -0.5, 1, 0.0,
]);
const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
// 创建缓存对象
const verticesTexBuffer = gl.createBuffer();
// 绑定缓存对象到上下文
gl.bindBuffer(gl.ARRAY_BUFFER, verticesTexBuffer);
// 向缓存区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
// 获取 a_Position 变量地址
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
// 允许访问缓存区
gl.enableVertexAttribArray(a_Position);
// 传入纹理坐标位置信息
const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);
/***** 纹理对象 *****/
const texture = gl.createTexture(); // 创建纹理对象
const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler'); // 获取 u_Sampler 地址
// 记载图片
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 翻转纹路图像的 y 轴
gl.activeTexture(gl.TEXTURE0); // 开启 0 号纹理单元
gl.bindTexture(gl.TEXTURE_2D, texture); // 将我们的纹理对象绑定上去
// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
// 【----关键代码---】配置纹理图像
const data = new Uint8Array([255, 0, 0, 0, 255, 255, 0, 255, 0, 0, 255, 0]);
gl.texImage2D(
gl.TEXTURE_2D, // 纹理目标
0, // 细节级别
gl.RGB, // 纹理内部格式
1,
1,
0,
gl.RGB, // 源图像数据格式
gl.UNSIGNED_BYTE, // 纹素数据类型
data // 数据
);
gl.uniform1i(u_Sampler, 0); // 开启 0 号纹理对象
/****** 绘制 ******/
// 清空画布,并指定颜色
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制矩形,这里提供了 4 个点
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
/**** 封装的方法 ****/
function createProgram(gl) {
/**** 渲染器生成处理 ****/
// 创建顶点渲染器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSrc);
gl.compileShader(vertexShader);
// 创建片元渲染器
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentShaderSrc);
gl.compileShader(fragmentShader);
// 程序对象
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
gl.program = program;
}
线上 demo:
https://codesandbox.io/s/1hvp4x?file=/index.js。
多个色块纹理
也可以同时设置多个色块。
const data = new Uint8Array([
255, 0, 0, 255, // 红色
255, 255, 0, 255, // 黄色
0, 0, 255, 255, // 蓝色
0, 255, 0, 255, // 绿色
]);
gl.texImage2D(
gl.TEXTURE_2D, // 纹理目标
0, // 细节级别
gl.RGBA, // 纹理内部格式
2,
2,
0,
gl.RGBA, // 源图像数据格式
gl.UNSIGNED_BYTE, // 纹素数据类型
data // 数据
);
创建了 2x2 4个像素大小的纹理,并制定了这个 4 个像素点的颜色,然后被放大绘制到指定区域上。
线上演示 demo:
https://codesandbox.io/s/7436cs?file=/index.js。
图片纹理
图片纹理,需要加载玩图片,将图片对象绑定到纹理对象上。
// 将纹理图像分配给纹理对象
gl.texImage2D(
gl.TEXTURE_2D,
0, // 细节级别
gl.RGB,
gl.RGB,
gl.UNSIGNED_BYTE,
img // Image 实例
);
结尾
纹理对象是很常用的一个对象,用于指定区域要填充的像素。
常见的是加载图片,把图片贴到三维的一个面上。也可以自己指定像素值。