WebGL从零开始(四)—— 纹理

8/10/2026 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>
1
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>
1
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);
1
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';
1
2
3
4
5
6
7
8
9
10
11
12
13

2.将纹理对象装入纹理单元

// 将纹理对象绑定到目标
gl.bindTexture(gl.TEXTURE_2D, texture);
1
2

3.当图像加载完成后,把图像装进当前纹理单元的纹理对象里。

// 设置纹理图像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
1
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);
1
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);
1
2
3

6.渲染

渲染后发现是反的,这是由于之前提到的Image对象遵守栅格坐标系,Y轴是向下的,而UV坐标系的Y轴向上,两者相反,所以画出的图形反了。

7.对图像进行预处理,将图像垂直翻转

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1); // 对纹理图像进行y轴反转
1

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
);
1
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,
])
1
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
);
1
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
);
1
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_NEAREST Select 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_NEAREST Select the nearest mip level and perform linear filtering within it.
  • LINEAR_MIPMAP_LINEAR Perform 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() 实现多异步等待完成。

  1. 将图像的加载方法封装进一个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}`));
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
  1. 建立多个Image对象
const originImg = new Image();
originImg.src = './images/dress.jpg';

const pattern = new Image();
pattern.src = './images/pattern.jpg';
1
2
3
4
5
  1. 等待加载
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();
}
1
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;
}
1
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;
}
1
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;
1
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);
}
1
2
3
4
5
6
7
8
9
10

mix() 方法是按照比例对两个纹理的合成方法,返回数据类型会因其合成对象的不同而不同。

规则如下:

mix(m,n,a) = m+(n-m)*a
1

如: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);
}
1
2
3
4
5
6
7
8
9
10

# 4.使用蒙版

# 5.视频贴图

Last Updated: 9/16/2026, 9:23:32 AM