WebGL从零开始(四)—— 纹理
# 一、基础概念
纹理本质上是一张被“贴”在 3D 模型表面的 2D 图像。
# 1.栅格系统
说的图像,一般指的是点阵图、栅格图、位图;而图形,是矢量图。
纹理是图像,其图像的建立和显示会遵循栅格系统里的规范,如所有图像都是有像素组成的。在WebGL中把像素称为片元,像素是按照相互垂直的行列排列的。
图像中的每个像素都可以通过 行数y 和 列数x 找到,由(x,y) 构成的点位,就是图像的像素坐标。
栅格坐标系的原点在左上角。
栅格坐标系的y轴方向是向下的,x轴是朝右的。
栅格坐标系的坐标基地由两个分量组成,分别是一个像素的宽和一个像素的高。
# 2.UV坐标(纹理坐标)
目的是告诉 WebGL 这张 2D 图片的哪个部分应该贴到模型的哪个角上。
传统的图片坐标系(栅格坐标系)原点在左上角,向下为 Y 轴正方向;而 WebGL 的纹理坐标系原点在左下角,向上为 V 轴正方向
u 代表图片水平方向的位置,v 代表垂直方向的位置。
无论图片的实际像素尺寸是多少,UV 坐标的范围永远是从 (0, 0) 到 (1, 1),使用的是百分比而非绝对像素值。或者说u轴上1个单位是图像的宽,v轴上1个单位是图像的高。
uv坐标系也叫st坐标系。
# 3.采样器
在 WebGL 中,纹理图片并不是像贴纸一样直接“粘”在模型上的,而是由片元着色器在渲染每一个像素时,根据 UV 坐标去图片上“读取”颜色的过程。这个读取动作就叫做采样。
采样需要采样器来处理,在片元着色器中,采样器是使用一种特殊的数据类型 sampler2D,可以通过uniform变量暴露给js,由它指向在 JavaScript 中绑定好的纹理图片。
着色器基于一张图像可以建立一个或多个采样器,不同的采样器可以定义不同的规则去获取图像中的片元。
# 4.纹理对象
纹理对象是存储在显卡(VRAM)中的一块数据结构,它包含了图像的像素数据以及相关的状态设置。
纹理对象是通过js建立的,需要一个图像源,比如Image对象。
纹理对象并不能直接给webgl使用,需要将纹理对象存入一个缓冲区,这个缓冲区可以将纹理对象翻译成着色器可以读懂的数据,并可以用来建立一个采样器。
# 5.纹理单元
纹理单元就是专门用来存放纹理对象的缓冲区。
纹理单元是由webgl提前建立好的,只有固定的几个,如TEXTURE0|1|2|3|4|5|6|7|8,数量有限。WebGL 规范要求至少支持 8 个纹理单元,现代设备通常支持 16 个或更多。
纹理单元无需我们建立但需要我们激活,让其进入使用状态。
# 二、为2D模型贴图基本流程
以这个例子来看看WebGL如何使用纹理。
# 1.准备着色器
1.顶点着色器
<script id="vsSource" type="notjs">
attribute vec4 a_Position;
attribute vec2 a_Pin; // 图钉的位置
varying vec2 v_Pin; // 传给片元着色器的插值
void main() {
gl_Position = a_Position;
v_Pin = a_Pin;
}
</script>
2
3
4
5
6
7
8
9
2.片元着色器
<script id="fsSource" type="notjs">
precision mediump float;
uniform sampler2D u_Sampler; // sampler2D 二维采样器
varying vec2 v_Pin;
void main() {
gl_FragColor = texture2D(u_Sampler, v_Pin); // texture2D 根据插值计算纹理坐标,从纹理中采样颜色
}
</script>
2
3
4
5
6
7
8
# 2.准备数据源
以一个四边形作为贴图的对象,四边形的四个顶点坐标及图钉的坐标作为一个元素。
const vertices = 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 elementBytes = vertices.BYTES_PER_ELEMENT; // 每个元素的字节数
const FSIZE = elementBytes;
// 系列尺寸
const posSize = 2;
const pinSize = 2;
// 类目尺寸
const categorySize = posSize + pinSize;
// 类目字节数
const categoryBytes = categorySize * elementBytes;
// 系列字节索引位置
const posByteIndex = 0;
const pinByteIndex = posSize * elementBytes;
// 顶点总数
const vertexCount = vertices.length / categorySize;
// 创建缓冲区对象
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);
// 获取顶点着色器中的变量
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, posSize, gl.FLOAT, false, categoryBytes, posByteIndex);
// 连接a_Position变量与分配给它的缓冲区对象
gl.enableVertexAttribArray(a_Position);
const a_Pin = gl.getAttribLocation(gl.program, 'a_Pin');
if (a_Pin < 0) {
console.log('获取a_Pin失败');
return -1;
}
gl.vertexAttribPointer(a_Pin, pinSize, gl.FLOAT, false, categoryBytes, pinByteIndex);
gl.enableVertexAttribArray(a_Pin);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# 3.贴图的步骤
1.准备3个对象
- image图像
- 纹理对象
- 纹理单元
// 创建纹理对象
const texture = gl.createTexture();
if (!texture) {
console.log('创建纹理对象失败');
return -1;
}
// 对纹理对象进行配置
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 对纹理图像进行y轴反转
gl.activeTexture(gl.TEXTURE0); // 激活纹理单元0
// 图像源
const image = new Image();
image.src = './img/erha.jpg';
2
3
4
5
6
7
8
9
10
11
12
13
2.将纹理对象装入纹理单元
// 将纹理对象绑定到目标
gl.bindTexture(gl.TEXTURE_2D, texture);
2
3.当图像加载完成后,把图像装进当前纹理单元的纹理对象里。
// 设置纹理图像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
2
texImage2D(target, level, internalformat, format, type, pixels)
- target: 纹理类型
- level:详细级别。0 级是基本图像等级,n 级是第 n 个金字塔简化级。
- internalformat:纹理中的颜色组件,一般就 gl.RGB
- format:纹理数据格式,在 WebGL 1 中,必须和internalformat 一样
- type:纹理数据的数据类型
gl.UNSIGNED_BYTE: 无符号字节。gl.RGBA每个通道 8 位
- pixels:图像源,可以是 ImageData、HTMLImageElment等
4.设置纹理对象的参数
参数会影响uv坐标系和栅格坐标系的映射关系和方式。
// 设置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
2
texParameteri(target, pname, param) i是整数,f是浮点数
texParameterf(target, pname, param)
- target:纹理类型
gl.TEXTURE_2D: 二维纹理。gl.TEXTURE_CUBE_MAP: 立方体纹理。- 当使用 WebGL 2 context 时,还可以使用以下值
gl.TEXTURE_3D: 三维贴图。gl.TEXTURE_2D_ARRAY: 二维数组贴图。
- pname:纹理参数的名称
- gl.TEXTURE_MIN_FILTER 纹理缩小滤波器
- param:与pname相对应的纹理参数值
- gl.LINEAR:线性
5.在js中获取采样器对应的Uniform变量,告诉片元着色器中的采样器,纹理对象在哪个单元里。之后采样器便会根据单元号去单元对象中寻找纹理对象。
// 将0号纹理传递给着色器
const u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
gl.uniform1i(u_Sampler, 0);
2
3
6.渲染

渲染后发现是反的,这是由于之前提到的Image对象遵守栅格坐标系,Y轴是向下的,而UV坐标系的Y轴向上,两者相反,所以画出的图形反了。
7.对图像进行预处理,将图像垂直翻转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 对纹理图像进行y轴反转
pixelStorei(pname, param) 图像预处理
- pname:参数名
- gl.UNPACK_FLIP_Y_WEBGL 是否垂直翻转
- param:参数值

# 三、纹理容器
贴图的图像源尺寸只能是2的n次方,比如2*2、4*4、16*16 ......
如果是非2次幂尺寸,则不会显示,也不会报错提示。
如果要用非2次幂尺寸的图,就需要设置纹理的容器。
在图像上打图钉的时候,形成一个UV区域,这块区域可以理解为纹理容器。
纹理容器可以定义图钉区域的纹理如何显示在webgl图形中。
通过对纹理容器的设置,可以实现:
- 非二次幂图像源的显示
- 纹理的复制
- 纹理的镜像
# 1.非二次幂图像源的显示
// 设置纹理参数
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
gl.LINEAR
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_S, // 纹理坐标水平填充 s , 值包含 gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT
gl.CLAMP_TO_EDGE
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T, // 纹理坐标垂直填充 t , 值包含 gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT
gl.CLAMP_TO_EDGE
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gl.TEXTURE_WRAP_S 和 gl.TEXTURE_WRAP_T 就是纹理容器在 s 方向的尺寸,这里的 s、t就是st坐标系的,也就是 uv 坐标系。
CLAMP_TO_EDGE 边缘夹紧,任意尺寸的图像都可以被宽高为1的uv尺寸夹紧。
注意:只有 CLAMP_TO_EDGE 才能实现非二次幂图像源的显示,其他参数不可以。
# 2.纹理的复制
uv坐标系的基底是1个图片的宽和高,如果设置2个会怎么样?
const vertices = new Float32Array([
-0.5, 0.5, 0.0, 2.0,
-0.5, -0.5, 0.0, 0.0,
0.5, 0.5, 2.0, 2.0,
0.5, -0.5, 2.0, 0.0,
])
2
3
4
5
6
刷新浏览器效果:

看到图片缩小了,其实这里也能看到刚刚 CLAMP_TO_EDGE 的效果,把边缘拉伸至uv的边缘。
好了,如果要实现复制,那么就不用 CLAMP_TO_EDGE, 而是使用 REPEAT
// 设置纹理参数
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
gl.LINEAR
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_S, // 纹理坐标水平填充 s , 值包含 gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT
gl.REPEAT
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T, // 纹理坐标垂直填充 t , 值包含 gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT
gl.REPEAT
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
注意: 这里只能用 二次幂的图像源

# 3.纹理的镜像
通过继续更改texParameteri的第三个参数实现
// 设置纹理参数
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_MIN_FILTER,
gl.LINEAR
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_S, // 纹理坐标水平填充 s , 值包含 gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT
gl.MIRRORED_REPEAT
);
gl.texParameteri(
gl.TEXTURE_2D,
gl.TEXTURE_WRAP_T, // 纹理坐标垂直填充 t , 值包含 gl.REPEAT (默认值),gl.CLAMP_TO_EDGE, gl.MIRRORED_REPEAT
gl.MIRRORED_REPEAT
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 四、分子贴图 mipmap
# 1.概念
分子贴图 mipmap 是一种纹理映射技术。
了解:用于在三维渲染中表示分子的表面和内部结构。这种技术通常结合了分子建模和纹理映射的概念,以实现高质量的分子可视化。分子贴图可以应用于多种科学和工程领域,如药物设计、生物化学、材料科学等。
比如: webgl中有个正方形,它在canvas画布中显示的时候,占据了 2*2 个像素,我们要将一个 8*8 的图像源贴上去。
正方形肯定不能显示图像源的所有像素,因为只有 2*2 = 4 个像素。
这时候会怎么处理呢?在webgl中会将图像源切割成2行2列(因为正方形是 2*2 )的色块,然后将每个色块的均值交个这个正方形。
这个时候就需要进行 分子贴图 了。
分子贴图是一个基于分辨率等比排列的图像集合,集合中每一项的宽高与其前一项宽高的比值都是1/2。
在webgl中,我们可以用 gl.generateMipmap() 方法为图像源创建分子贴图,有了分子贴图后,之前 2*2 的正方形便会从分子集合中寻找与其分辨率最接近的分子图像。
找到分子图像后,就需要基于 webgl 图形的片元尺寸对其分割取色。
取色方法除了均值算法,还有其他的算法。
gl.texParameteri() 方法的
TEXTURE_MAG_FILTER 和 TEXTURE_MIN_FILTER,对应的是纹理在 webgl 图形中的缩放情况。
- TEXTURE_MAG_FILTER 纹理放大滤波器,是纹理在 webgl 图形中被放大的情况。
- TEXTURE_MIN_FILTER 纹理缩小滤波器,是纹理在 webgl 图形中被缩小的情况。
TEXTURE_MAG_FILTER 具备以下参数:
- LINEAR (默认值),线性滤镜,获取纹理坐标点附近 4 个像素的加权平均值,效果平滑
- NEAREST 最近滤镜,获得最靠近纹理坐标点的像素,效果锐利
TEXTURE_MIN_FILTER 具备以下参数:
- LINEAR 线性滤镜,获取纹理坐标点附近 4 个像素的加权平均值,效果平滑
- NEAREST 最近滤镜,获得最靠近纹理坐标点的像素,效果锐利
NEAREST_MIPMAP_NEARESTSelect the nearest mip level and perform nearest neighbor filtering.NEAREST_MIPMAP_LINEAR(默认值) Perform a linear interpolation between mip levels and perform nearest neighbor filtering within each.LINEAR_MIPMAP_NEARESTSelect the nearest mip level and perform linear filtering within it.LINEAR_MIPMAP_LINEARPerform a linear interpolation between mip levels and perform linear filtering : also called trilinear filtering.
注1:后面这 4 个与分子贴图相关的参数适合比较大的贴图,若是比较小的贴图,使用 LINEAR 或 NEAREST 就好。
注2:缩小滤波器的默认值取色方法是 NEAREST_MIPMAP_LINEAR,这个方法会从分子贴图里找分子图像,然后从其中取色,然而当我们没有使用 gl.generateMipmap () 方法建立分子贴图的时候,就得给它一个不需要从分子贴图中去色的方法,如 LINEAR 或 NEAREST。
# 2.实际使用示例——魔方
# 五、纹理模块化
# 1.概念
# 2.使用示例
# 六、纹理合成
# 1.概念
按照某种规则将多张图片合在一起,比如 正片叠底、裁剪……等。
# 2.webgl的多图加载
纹理合成需要多张图像源,所以需要先等多张图像源加载成功后再去合成纹理。
js中使用 Promise.all() 实现多异步等待完成。
- 将图像的加载方法封装进一个Promise中,等图像加载成功后,再 resolve
function imgPromise(img:HTMLImageElement): Promise<HTMLImageElement> {
return new Promise<HTMLImageElement>((resolve, reject)=>{
// 1. 如果图片已经加载好了(比如浏览器缓存),直接 resolve
if (img.complate) {
resolve(img);
return;
}
// 2. 处理加载成功
img.onload = () => resolve(img);
// 3. 处理加载失败,防止 Promise 永远挂起
img.onerror = (err) => reject(new Error(`image load failed: ${img.src}`));
});
}
2
3
4
5
6
7
8
9
10
11
12
13
- 建立多个Image对象
const originImg = new Image();
originImg.src = './images/dress.jpg';
const pattern = new Image();
pattern.src = './images/pattern.jpg';
2
3
4
5
- 等待加载
Promise.all([imgPromise(originImg), imgPromise(pattern)]).then(()=>{
// rect 包装过后的图形对象
rect.maps = {
u_Sampler: { image: originImg },
u_Pattern: { image: pattern }
}
rect.updateMaps();
render();
})
function render() {
gl.clear(gl.COLOR_BUFFER_BIT);
rect.draw();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 3.示例
# 1.在片元着色器中合成纹理
采样一个或多个纹理图像,按特定算法混合其颜色/属性数据,生成最终像素颜色的过程。 它不是简单贴图,而是对多源纹理数据进行实时计算融合。
顶点着色器
attribute vec4 a_Position;
attribute vec2 a_Pin;
varying vec2 v_Pin;
void main() {
gl_Position = a_Position;
v_Pin = a_Pin;
}
2
3
4
5
6
7
片元着色器
precision mediump float;
uniform sampler2D u_Sampler;
uniform sampler2D u_Pattern;
varying vec2 v_Pin;
void main() {
vec4 o = texture2D(u_Sampler, v_Pin);
vec4 p = texture2D(u_Pattern, v_Pin);
gl_FragColor = p*o;
}
2
3
4
5
6
7
8
9
- u_Sampler 是原始图片采样器
- u_Pattern 是纹理图案采样器
通过采样器找到原始图片和纹理图片的片元后,进行相乘运算:
vec4 o = texture2D(u_Sampler, v_Pin);
vec4 p = texture2D(u_Pattern, v_Pin);
gl_FragColor = p*o;
2
3
p*o 分量相乘,这种做法类似 正片叠底 的效果,这种算法会让原始图片的亮度变暗,因为片元分量的值域为[0,1] 所以 p*o 的亮度小于等于 p 和 o
- 当 p=(1,1,1,1) 时, p*o = o
- 当 p=(0,0,0,0) 时, p*o = (0,0,0,0)
# 2.纹理混合
纹理混合就是按照一定比例,将第一张图像合到另一张图像上,类似ps里面的透明度合成。
在片元着色器中的合成方法:
precision mediump float;
uniform sampler2D u_Sampler;
uniform sampler2D u_Pattern;
varying vec2 v_Pin;
void main() {
vec4 o = texture2D(u_Sampler, v_Pin);
vec4 p = texture2D(u_Pattern, v_Pin);
// 将 p 混合到 o,第三个参数是 比例,1的话就是完全显示 p, 0的话就是完全显示 o
gl_FragColor = mix(o, p, 1.0);
}
2
3
4
5
6
7
8
9
10
mix() 方法是按照比例对两个纹理的合成方法,返回数据类型会因其合成对象的不同而不同。
规则如下:
mix(m,n,a) = m+(n-m)*a
如:m=(1,2,3) n=(3,4,5) a=0.5
mix(m,n,a) = (1+(3-1)*0.5, 2+(4-2)*0.5, 3+(5-3)*0.5) = (2, 3, 4)
参考地址:https://www.khronos.org/registry/OpenGL-Refpages/gl4/
# 3.转场动画
在PPT中用到的比较多,就是如场景的淡入、淡出等。
我们可以将一个纹理理解为一个场景,利用转场动画实现淡入、淡出效果。
1.在片元着色器中,将mix()方法中的比值设置为uniform变量。
precision mediump float;
uniform sampler2D u_Sampler;
uniform sampler2D u_Pattern;
uniform float u_Ratio;
varying vec2 v_Pin;
void main() {
vec4 o = texture2D(u_Sampler, v_Pin);
vec4 p = texture2D(u_Pattern, v_Pin);
gl_FragColor = mix(o, p, u_Ratio);
}
2
3
4
5
6
7
8
9
10