在WebGL系列(1)中我们介绍到,如果我们想一次性向着色器传入多个顶点数据,就需要使用到缓冲区对象。前面的示例中,我们都是动态传递顶点的坐标。那如果我们需要动态传入坐标的大小等非坐标数据,该如何去做呢?所以在介绍颜色和纹理之前,我们先来看看如何向顶点着色器传递非坐标数据。
一、非坐标数据传入顶点着色器
之前我们有个例子是通过buffer动态传递多个点的坐标,下面是初始化缓冲区对象的相关代码,具体实现代码可见该系列第一篇文章。
// 初始化顶点缓冲区
function initVertexBuffer(gl) {
const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
const n = 3; // 点的个数
// 创建缓冲区对象
const vertexBuffer =gl.createBuffer();
if (!vertexBuffer) {
console.log('创建缓冲区对象失败!');
return -1;
}
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 获取 attribute 变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 判断是否获取成功
if (a_Position < 0) {
console.log('获取 a_Position 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 连接 a_Position 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
return n;
}
// 顶点着色器程序
const VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' + // 设置顶点坐标
' gl_PointSize = 10.0;\n' + // 设置顶点大小
'}\n';
const FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 0.0);\n' + // 设置颜色
'}\n';
...
那如果我现在需要对不同的点设置不同的大小,最简单的方法,那就是我们再创建一个新的缓冲区对象,用于存储顶点大小,然后传递给着色器。那我们可以对上述代码做些修改:
// 初始化顶点缓冲区
function initVertexBuffer(gl) {
const vertices = new Float32Array([0.0, 0.5, -0.5, -0.5, 0.5, -0.5]);
const sizes = new Float32Array([10.0, 20.0, 30.0])
const n = 3; // 点的个数
// 创建坐标缓冲区对象
const vertexBuffer =gl.createBuffer();
if (!vertexBuffer) {
console.log('创建坐标缓冲区对象失败!');
return -1;
}
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 获取 attribute 变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 判断是否获取成功
if (a_Position < 0) {
console.log('获取 a_Position 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 连接 a_Position 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
// 创建顶点大小缓冲区对象
const sizeBuffer = gl.createBuffer();
if (!sizeBuffer) {
console.log('创建顶点大小缓冲区失败!');
return -1;
}
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);
const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
// 判断是否获取成功
if (a_PointSize < 0) {
console.log('获取 a_PointSize 的存储位置失败!');
return -1;
}
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_PointSize);
return n;
}
// 顶点着色器程序
const VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute float a_PointSize;\n' +
'void main() {\n' +
' gl_Position = a_Position;\n' + // 设置顶点坐标
' gl_PointSize = a_PointSize;\n' + // 设置顶点大小
'}\n';
const FSHADER_SOURCE =
'void main() {\n' +
' gl_FragColor = vec4(1.0, 0.0, 0.0, 0.0);\n' + // 设置颜色
'}\n';
...
如果顶点的每种数据都要使用一个缓冲区对象来维护,当程序中有成千上万个顶点时,那么维护起来是相当有难度的。这样看来,上述创建多个缓冲区的方法显然存在缺陷的。对此,WebGL中允许把顶点的坐标和尺寸数据打包到同一个缓冲区对象中,并通过某种机制分别访问缓冲区中不同种类的数据。
如下程序,我们看看具体是如何实现的。
// 初始化顶点缓冲区
function initVertexBuffer(gl) {
const verticesSizes = new Float32Array([
0.0, 0.5, 10.0,
-0.5, -0.5, 20.0,
0.5, -0.5, 30.0]);
const n = 3; // 点的个数
// 创建坐标缓冲区对象
const vertexBuffer =gl.createBuffer();
if (!vertexBuffer) {
console.log('创建坐标缓冲区对象失败!');
return -1;
}
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW);
const FSIZE = verticesSizes.BYTES_PER_ELEMENT;
// 获取 attribute 变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 判断是否获取成功
if (a_Position < 0) {
console.log('获取 a_Position 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
// 连接 a_Position 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
// 获取 attribute 变量的存储位置
const a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
// 判断是否获取成功
if (a_Position < 0) {
console.log('获取 a_PointSize 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_PointSize 变量
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);
// 连接 a_PointSize 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_PointSize);
return n;
}
上述程序中,将顶点坐标和大小都存放在了同一个数组中,并且传入一个缓冲区对象中。而且在使用gl.vertexAttribPointer方法分配缓冲区对象时有所不同。我们再来回顾一下gl.vertexAttribPointer方法的stride参数 和 offset 参数。
stride:指定相邻两个顶点间的字节数(即:单个顶点所有数据的字节数),默认为0。offset:指定缓冲区对象中的偏移量(以字节为单位),即 attribute 变量从缓冲区中何处开始存储。
那么代码gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);的含义则是:
每个顶点数据占FSIZE * 3字节,a_Position变量从 0 处开始存储,获取的分量个数为2
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2);的含义则是:
每个顶点数据占FSIZE * 3字节,a_PointSize变量从 FSIZE * 2 处开始存储,获取的分量个数为1.
也就是说,一个顶点的数据为0.0, 0.5, 10.0时,0.0, 0.5传递给a_Position变量, 10.0传递给a_PointSize变量。
通过这种方法,我们可以将顶点的多种数据打包在一起,只需要维护一个缓冲区对象。
上述代码实现效果如下:
二、varying 变量
上文中我们实现了为每个顶点设置不同的大小。那如果我们想要为每个顶点设置不同颜色呢?
可能你会想到,创建一个缓冲区对象,填充顶点的位置和颜色数据,然后分配给attribute变量。但是,前面我们介绍到,真正能影响顶点颜色的是片元着色器。
前面设置顶点颜色时,我们使用 uniform 变量将颜色信息传入片元着色器。但是这种方法只能给所有顶点设置同一个颜色。要想使得每个顶点颜色不同,我们需要用到一个新的变量 varying 变量(varying variable)。
varying 变量的作用就是从顶点着色器向片元着色器传输数据。
那么下面我们就看看如何使用 varying 变量:
// 初始化顶点缓冲区
function initVertexBuffer(gl) {
// 顶点坐标和颜色
const verticesColors = new Float32Array([
0.0, 0.5, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 1.0]);
const n = 3; // 点的个数
// 创建坐标缓冲区对象
const vertexColorBuffer =gl.createBuffer();
if (!vertexColorBuffer) {
console.log('创建坐标缓冲区对象失败!');
return -1;
}
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
const FSIZE = verticesColors.BYTES_PER_ELEMENT;
// 获取 attribute 变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 判断是否获取成功
if (a_Position < 0) {
console.log('获取 a_Position 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0);
// 连接 a_Position 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
// 获取 attribute 变量的存储位置a_Color
const a_Color = gl.getAttribLocation(gl.program, 'a_Color');
// 判断是否获取成功
if (a_Color < 0) {
console.log('获取 a_Color 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_PointSize 变量
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2);
// 连接 a_PointSize 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Color);
return n;
}
// 顶点着色器程序
const VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'varying vec4 v_Color;\n' + // varying 变量
'void main() {\n' +
' gl_Position = a_Position;\n' + // 设置顶点坐标
' gl_PointSize = 10.0;\n' + // 设置顶点大小
' v_Color = a_Color;\n' + // 将数据传递给片元着色器
'}\n';
const FSHADER_SOURCE =
'precision mediump float;\n' +
'varying vec4 v_Color;\n' + // varying 变量
'void main() {\n' +
' gl_FragColor = v_Color;\n' + // 从顶点着色器接收数据
'}\n';
上述程序中 varying 变量的使用流程大致如下:
attribute vec4 a_Color;:声明 attribute 变量用于接收颜色数据varying vec4 v_Color;:声明 varying 变量负责将颜色值被传给片元着色器v_Color = a_Color;:将 attribute 变量的值赋给 varying 变量varying vec4 v_Color:在片元着色器中声明一个与顶点着色器中同名的 varying 变量用于接收顶点着色器传来的数据gl_FragColor = v_Color:将 varying 变量的值传递给gl_FragColor修改顶点颜色
注:在 WebGL 中,如果顶点着色器和片元着色器中有类型和命名相同的varying变量,那么顶点着色器赋给该变量的值就会被自动传入片元着色器。
三、几何图形的装配和光栅化
上述绘制不同颜色顶点的例子中,如果我们修改drawArrays()的mode参数为gl.TRIANGLES,会绘制出一个彩色的三角形。如下所示:
gl.drawArrays(gl.TRIANGLES, 0, n);
上述程序中,我们传递了顶点的坐标和颜色,但对于三角形内部颜色的填充,片元着色器又是怎么处理每个片元的呢? 其实在顶点着色器和片元着色器之间,还存在以下两个步骤:
- 图形装配过程:将孤立的顶点坐标装配成几何图形。几何图形的类别由
drawArrays()的mode参数决定。该过程又称为图元装配过程,被装配出的基本图形(点、线、面)又被称为图元(primitives)。 - 光栅化过程:将装配好的几何图形转化为片元
如下图所示,就是绘制一个三角形的大体过程:
- 执行顶点着色器:
gl.drawArrays()的参数n为多少,那么顶点着色器就执行多少次。- 第1次执行顶点着色器,缓冲区中的第一个坐标被赋值给 attribute 变量
a_Position。一旦一个顶点的坐标被赋值给了 gl_Posiiton,它就进入了图形装备区域,并且暂时存在那里。 - 后续操作类似
- 第1次执行顶点着色器,缓冲区中的第一个坐标被赋值给 attribute 变量
- 图形装配:顶点着色器执行完毕后,所有顶点坐标就都处在装备区了。接下来就开始装配图形,使用传入的坐标,根据
drawArrays()的mode参数来决定如何装配。 - 光栅化:将图形转换为片元
- 调用片元着色器:光栅化过程结束后,程序开始逐片元调用片元着色器。对于每个片元,片元着色器计算出该片元颜色,并写入颜色缓冲区
- 浏览器显示:最后一个片元处理完成后,浏览器就会显示最终的结果。
上图皆出自《WebGL编程指南》
内插过程(interpolation process)
前面我们说到,我们把顶点的颜色赋值给顶点着色器的varying变量v_Color,它的值被传递给片元着色器中同名、同类型的变量。但实际上,顶点着色器中的v_Color 变量在传入片元着色器之前经历了内插过程。所以顶点着色器中的v_Color变量和片元着色器中的v_Color变量实际并不相同。
那什么是内插过程呢?我们看如下的例子:
例子中RGBA中的R值从1.0降低到0.0,B值从0.0上升到1.0,线段上的所有片元颜色值都会被恰当的计算出来,这个过程就是内插过程(interpolation process)。具体内插过程是如何计算的,可以参考这篇文章。
上述例子实际效果如下:
四、 纹理映射
上述我们通过自定义顶点颜色以及varying变量的内插过程,绘制出了彩色的三角形。但是如果我们需要绘制一个与某个图片相同的图像怎么办呢?这个时候就要用到纹理映射了。
纹理映射(texture mapping):纹理映射就是将一张图像映射到一个几何图形表面。这张图片可以称为纹理图像(texture image) 或 纹理(texture)。
纹理映射的作用:根据纹理图像,为光栅化后的每个片元涂上合适的颜色。组成纹理图像的像素又被称为纹素(texels),每一个纹素的颜色都使用 RGB 或 RGBA 格式编码。
在 WebGL 中,要进行纹理映射,需要遵循以下四步:
- 准备映射待几何图形上的纹理图像
- 为几何图形配置纹理映射方式
- 加载纹理图像,对其进行一些配置,以在WebGL中使用它
- 在片元着色器中将相应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元。
4.1 纹理坐标
上述第二步配置纹理映射方式其实就是,利用图形的顶点坐标来确定屏幕上哪部分被纹理图像覆盖,使用纹理坐标来确定图像上的哪部分将覆盖到几何图形上。
纹理坐标(texture coordinates) 是纹理图像上的坐标,通过纹理坐标可以在纹理图像上获取纹素颜色。它与WebGl的坐标系统不同,如下图所示(矩形区域就是图像):
4.2 将纹理图像贴到几何图形上
经过上面的介绍,接下来纹理映射的具体代码实现:
// 初始化顶点缓冲区
function initVertexBuffer(gl) {
// 顶点坐标 纹理坐标
const verticesTexCoords = new Float32Array([
-0.5, 0.5, 0.0, 1.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 1.0, 1.0,
0.5, -0.5, 1.0, 0.0
]);
const n = 4; // 点的个数
// 创建坐标缓冲区对象
const vertexTexCoordBuffer = gl.createBuffer();
if (!vertexTexCoordBuffer) {
console.log('创建坐标缓冲区对象失败!');
return -1;
}
// 将缓冲区对象绑定到目标
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
// 向缓冲区写入数据
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);
const FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
// 获取 attribute 变量的存储位置
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 判断是否获取成功
if (a_Position < 0) {
console.log('获取 a_Position 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_Position 变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
// 连接 a_Position 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
// 获取 attribute 变量的存储位置a_TexColor
const a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');
// 判断是否获取成功
if (a_TexCoord < 0) {
console.log('获取 a_TexCoord 的存储位置失败!');
return -1;
}
// 将缓冲区对象分配给 a_PointSize 变量
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
// 连接 a_PointSize 变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_TexCoord);
return n;
}
// 初始化纹理对象
function initTexture(gl, n) {
// 创建纹理对象
const texture = gl.createTexture();
if (!texture) {
console.log('创建纹理对象失败!');
return false;
}
//获取 u_Sampler 的存储位置
const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
// 判断是否获取成功
if (u_Sampler < 0) {
console.log('获取 u_Sampler 的存储位置失败!');
return false;
}
// 创建一个image对象
const image = new Image();
// 注册图形加载事件和响应函数
image.onload = function() { loadTexture(gl, n, texture, u_Sampler, image) };
// 浏览器开始加载图像
image.src = './three/pupu.png';
return true;
}
function loadTexture(gl, n, texture, u_Sampler, image) {
// 对纹理图像进行y轴反转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 开启0号纹理单元
gl.activeTexture(gl.TEXTURE0);
// 向 target 绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture);
// 配置纹理参数
// gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
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.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
// 将0号纹理传递给着色器
gl.uniform1i(u_Sampler, 0);
// 设置canvas背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 情况canvas
gl.clear(gl.COLOR_BUFFER_BIT);
// 绘制矩形
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);
}
const VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec2 a_TexCoord;\n' +
'varying vec2 v_TexCoord;\n' + // varying 变量
'void main() {\n' +
' gl_Position = a_Position;\n' + // 设置顶点坐标
' v_TexCoord = a_TexCoord;\n' + // 将数据传递给片元着色器
'}\n';
const FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform sampler2D u_Sampler;\n' +
'varying vec2 v_TexCoord;\n' + // varying 变量
'void main() {\n' +
' gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' + // 从顶点着色器接收数据
'}\n';
function main() {
// 获取canvas元素
const canvas = document.getElementById('gl');
// 获取WebGL绘图上下文
const gl = canvas.getContext('webgl');
// 确认WebGL支持性
if (!gl) {
console.log('浏览器不支持WebGL');
return;
}
// 初始化着色器
if(!initShader(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('初始化着色器失败!');
return;
}
// 设置顶点位置
const n = initVertexBuffer(gl);
if (n < 0) {
console.log('设置顶点位置失败!');
return;
}
// 配置纹理
if (!initTexture(gl, n)) {
console.log('纹理配置失败!');
return;
}
}
上述程序的main函数主要分为以下五部分:
- 顶点着色器中接收顶点的纹理坐标,光栅化后传递给片元着色器
- 片元着色器根据片元的纹理坐标,从纹理图像中抽取出纹素颜色,赋给当前片元
- 设置顶点的纹理坐标(initVertexBuffers())。需要注意的是纹理坐标是
vec2类型的。 - 准备待加载的纹理图像,令浏览器读取它
- 监听纹理图像的加载事件,一旦加载完成,就在WebGL系统中使用纹理。
下面我们就来看看其中具体细节:
(1)配置和加载纹理
initTexture()函数负责配置和加载纹理:
- 首先调用
gl.createTexture()函数创建纹理对象gl.createTexture():创建纹理对象以存储纹理图像- 返回值:创建的纹理对象,若创建失败,则返回null
gl.deleteTexture(texture):删除纹理对象- 参数:
texture待删除的纹理对象
- 参数:
gl.getUniformLocation(gl.program, 'u_Sampler')获取uniform变量u_Sampler(取样器)的存储位置,该变量用于接收纹理对象。- 创建一个 Image 对象,并注册
onload事件响应函数loadTexture(),图像加载完毕后就会调用该函数。(因为图像是异步加载的,所以需要等待图像加载完毕后才能执行后续的操作) - 最后通知浏览器开始加载图像。
(2)为 WebGL 配置纹理
如上所述,图像加载完毕后就会执行loadTexture()函数,该函数主要负责配置纹理供 WebGL 使用。
图像Y轴反转 gl.pixelStorei()
WebGL纹理坐标系统中的t轴的方向和PNG、BMP、JPG等格式图片的坐标系统的Y轴方向是相反的。因此,只有先将图像Y轴进行反转,才能够正确地将图像映射到图形上。
gl.pixelStorei(pname, param):使用 pname 和 param 指定的方式处理加载得到的图像
- 参数:
pname:gl.UNPACK_FLIP_Y_WEBGL:对图像进行 Y 轴反转。默认值为 falsegl.UNPACK_PREMULTIPLY_ALPHA_WEBGL:将图像RGB颜色值的每一个分量乘以 A 。默认值为false。
param:指定非0(true) 或 0(false)。必须为整数。
- 返回值:无
激活纹理单元 gl.activeTexture()
WebGL 通过纹理单元(texture unit) 的机制来同时使用多个纹理。每个纹理单元有一个单元编号来管理一张纹理图像。
系统支持的纹理单元个数取决于硬件和浏览器的WebGL实现,默认情况下,至少支持8个纹理单元。内置的变量gl.TEXTURE0、gl.TEXTURE1......gl.TEXTURE7个表示一个纹理单元
但是在使用纹理单元之前,需要调用gl.activeTexture()函数激活。
gl.activeTexture(textUnit):激活 textUnit 指定的纹理单元。
- 参数:
textUnit:指定准备激活的纹理单元。gl.TEXTURE0、gl.TEXTURE1......gl.TEXTURE7,最后的数字表示纹理单元的编号。
- 返回值:无
绑定纹理对象 gl.bingTexture()
在使用纹理对象之前,需要使用gl.bingTexture()函数绑定纹理对象,告诉 WebGL 系统纹理对象使用的是哪种类型的纹理。
gl.bingTexture(target, texture):开启 texture 指定的纹理对象,并将其绑定到 target 上。如果在此之前已经通过 gl.activeTexture() 激活了某个纹理单元,则纹理对象也会绑定到这个纹理单元上。
- 参数:
target:纹理类型gl.TEXTURE_2D:二维纹理gl.TEXTURE_CUBE_MAP:立方体纹理
texture:表示绑定的纹理单元
- 返回值:无
在 WebGL 中,必须通过将纹理对象绑定到纹理单元上,然后通过操作纹理单元来操作纹理对象。
配置纹理对象的参数 gl.texParameteri()
我们可以通过gl.texParameteri()配置纹理对象的参数,以此来设置纹理对象映射到图形上的具体方式。
gl.texParameteri(target, pname, param):将 param 的值赋给绑定到目标的纹理对象的 pname 上。
- 参数:
target:gl.TEXTURE_2D或gl.TEXTURE_CUBE_MAPpname:纹理参数param:纹理参数的值
- 返回值:无
具体配置如下表所示:
| 纹理参数 | 描述 | 值 | 默认值 |
|---|---|---|---|
gl.TEXTURE_MAG_FILTER | 放大方法:即当纹理的绘制范围比纹理本身更大时,如何获取纹素颜色 | gl.NEAREST:使用原纹理上距离(曼哈顿距离)映射后像素中心最近的那个像素的颜色值,作为新的像素值gl.LINEAR:使用原纹理上距离(曼哈顿距离)映射后像素中心最近的四个像素的颜色值加权平均,作为新的像素值。gl.NEAREST_MIPMAP_NEARESTgl.NEAREST_MIPMAP_LINEARgl.LINEAR_MIPMAP_LINEARgl.LINEAR_MIPMAP_NEAREST | gl.LINEAR |
gl.TEXTURE_MIN_FILTER | 缩小方法:即当纹理的绘制范围比纹理本身更小时,如何获取纹素颜色 | 同 gl.TEXTURE_MAG_FILTER | gl.NEAREST_MIPMAP_LINEAR |
gl.TEXTURE_WRAP_S | 水平填充方法:即如何对纹理图像左侧或右侧的区域进行填充 | gl.REPEAT:平铺式重复纹理gl.MIRRORED:镜像对称式重复纹理gl.CLAMP_TO_EDGE:使用纹理图像边缘值 | gl.REPEAT |
gl.TEXTURE_WRAP_T | 垂直填充方法:即如何对纹理图像上方或下方的区域进行填充 | 同 gl.TEXTURE_WRAP_S | gl.REPEAT |
注:(x1, y1) 和 (x2, y2) 的曼哈顿距离为 |x1 - x2| + |y1 - y2|
将纹理图像分配给纹理对象 gl.texImage2D()
gl.texImage2D(target, level, interalformat, format, type, image):将 image 指定的图形分配给绑定到目标上的纹理对象。
- 参数:
target:gl.TEXTURE_2D或gl.TEXTURE_CUBE_MAPlevel:传入 0internalformat:图像的内部格式format:纹理数据的格式,必须与internalformat相同type:纹理数据的类型image:包含纹理图像的 Image 对象
- 返回值:无
纹素数据的格式:
| 格式 | 描述 |
|---|---|
gl.RGB | 红、绿、蓝 |
gl.RGBA | 红、绿、蓝、透明度 |
gl.ALPHA | (0.0, 0.0,0.0,透明度) |
gl.LUMINANCE | L、L、L、1L 流明:表示我们感知到的物体表面的亮度,通常使用物体表面红绿蓝颜色分量值来加权平均计算 |
gl.LUMINANCE_ALPHA | L、L、L、透明度 |
通常 JPG、BMP 格式使用gl.RGB,PNG 格式使用gl.RGBA,灰度图像使用gl.LUMINANCE和gl.LUMINANCE_ALPHA
纹理数据的数据格式:
| 格式 | 描述 |
|---|---|
gl.UNSIGNED_BYTE | 无符号整型,每个颜色分量占据1字节 |
gl.UNSIGNED_BYTE_5_6_5 | RGB:每个分量分别占据 5、6、5 比特 |
gl.UNSIGNED_BYTE_4_4_4_4 | RGBA:每个分量分别占据 4、4、4、4 比特 |
gl.UNSIGNED_BYTE_5_5_5_1 | RGBA:每个分量分别占据 5、5、5、1 比特 |
通常使用gl.UNSIGNED_BYTE无符号整型,后面几种类型通常用来压缩数据,以减少浏览器加载图像时间。
将纹理单元传递给片元着色器 gl.uniform1i()
用于接收纹理对象数据的变量必须为以下数据类型其中一种:
| 类型 | 描述 |
|---|---|
sampler2D | 绑定到 gl.TEXTURE_2D 上的纹理数据类型 |
samplerCube | 绑定到 gl.TEXTURE_CUBE_MAP 上的纹理数据类型 |
gl.uniform1i()的第二个参数用于指定纹理单元编号。
在片元着色器中获取纹理像素颜色 texture2D()
texture2D(sampler2D sampler, vec2 coord):从 sampler 指定的纹理上获取 coord 指定的纹理坐标出的像素颜色。
- 参数:
sampler:指定纹理单元编号coord:指定纹理坐标
- 返回值:纹理坐标处的像素的颜色值,格式由
gl.texImage2D的internalformat参数决定。
以上就是纹理映射的大致过程了。
4.3 使用多幅纹理
前面我们说道,在 WebGL 系统中不止一个纹理单元,这样就意味着我们可以同时使用多幅纹理。
大体流程与4.2相同,只不过要创建多个纹理图像,并绑定到不同的纹理单元上。需要注意的是,因为图片是异步加载的,们不知道哪一张图片先加载完毕,所以我们要在响应程序中判断是否图片都加载完毕。等图片都加载完毕之后再绘制。
具体实现这里就不介绍了。
本文到这里也就结束啦!
参考:
[1] 《WebGL 编程指南》