5月28日 01:36

WebGL 中的纹理(Texture)如何使用?有哪些纹理参数需要配置?

纹理是 WebGL 渲染的核心机制

纹理(Texture)是将 2D 图像数据映射到 3D 几何体表面的技术。在 WebGL 中,几乎所有视觉效果——地板砖纹、角色皮肤、天空背景——都依赖纹理实现。理解纹理的使用流程和参数配置,是掌握 WebGL 的关键一步。

纹理使用的完整流程

1. 创建并绑定纹理对象

javascript
const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture);

bindTexture 将纹理对象绑定到当前纹理单元,后续所有纹理操作都针对该绑定对象。

2. 翻转 Y 轴(面试高频考点)

WebGL 纹理坐标原点在左下角,而图片坐标原点在左上角。不翻转会导致纹理倒置:

javascript
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);

这行代码必须写在 texImage2D 之前。面试中经常追问:为什么 WebGL 纹理坐标系与图片坐标系方向相反?答案在于 OpenGL 传统——纹理坐标沿用数学坐标系(Y 向上),而图片格式遵循扫描线顺序(Y 向下)。

3. 上传纹理数据

javascript
// 从 Image 对象加载 gl.texImage2D( gl.TEXTURE_2D, // 目标 0, // mipmap 级别 gl.RGBA, // 内部格式 gl.RGBA, // 源格式 gl.UNSIGNED_BYTE, // 数据类型 image // Image 对象 ); // 直接上传像素数据 gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, // 宽、高、边框(必须为0) gl.RGBA, gl.UNSIGNED_BYTE, pixels // Uint8Array );

4. 配置纹理参数

javascript
// 环绕方式 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);

5. 生成 Mipmap(可选)

javascript
gl.generateMipmap(gl.TEXTURE_2D);

Mipmap 会生成一系列逐级减半的纹理副本,在物体远离相机时使用更小的纹理,既提升渲染质量又节省带宽。内存开销仅增加约 30%。

纹理参数详解

纹理环绕方式(Texture Wrapping)

控制纹理坐标超出 [0, 1] 范围时的行为:

参数值效果典型场景
gl.REPEAT重复平铺地板砖纹、墙壁图案
gl.CLAMP_TO_EDGE边缘像素延伸天空盒、非重复贴图
gl.MIRRORED_REPEAT镜像重复对称图案贴图
shell
REPEAT: CLAMP_TO_EDGE: MIRRORED_REPEAT: |ABCD|ABCD| |AAAA|ABCD|DDDD| |ABCD|DCBA|ABCD| |ABCD|ABCD| |AAAA|ABCD|DDDD| |ABCD|DCBA|ABCD|

面试追问:非 2 的幂次(NPOT)纹理只能使用 CLAMP_TO_EDGE,不能使用 REPEAT,也不能生成 Mipmap。这是 WebGL1 的重要限制,WebGL2 已解除。

纹理过滤方式(Texture Filtering)

放大过滤(MAG_FILTER)

纹理被放大时(纹理像素 < 屏幕像素):

参数值效果
gl.NEAREST最近邻采样,像素化效果,速度快
gl.LINEAR双线性插值,平滑效果(推荐)

缩小过滤(MIN_FILTER)

纹理被缩小时(纹理像素 > 屏幕像素):

参数值效果
gl.NEAREST最近邻采样
gl.LINEAR双线性插值
gl.NEAREST_MIPMAP_NEAREST选最近 mipmap 级别 + 最近采样
gl.LINEAR_MIPMAP_NEAREST选最近 mipmap 级别 + 线性插值
gl.NEAREST_MIPMAP_LINEARmipmap 间线性过渡 + 最近采样
gl.LINEAR_MIPMAP_LINEAR三线性过滤,质量最高
javascript
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);

着色器中的纹理使用

顶点着色器

glsl
attribute vec3 a_position; attribute vec2 a_texCoord; uniform mat4 u_mvpMatrix; varying vec2 v_texCoord; void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); v_texCoord = a_texCoord; }

片段着色器

glsl
precision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); }

JavaScript 端绑定

javascript
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.uniform1i(textureLocation, 0); // 对应 TEXTURE0

面试追问gl.uniform1i 传入的 0 代表什么?它指定纹理单元的索引,0 对应 gl.TEXTURE0,1 对应 gl.TEXTURE1,以此类推。

纹理坐标系

shell
(0, 1) ──────── (1, 1) │ │ │ 纹理图像 │ │ │ (0, 0) ──────── (1, 0)
  • 原点在左下角(Y 向上),与图片坐标(左上角,Y 向下)相反
  • 坐标范围 [0, 1],与纹理实际像素尺寸无关
  • 超出 [0, 1] 范围的行为由环绕方式决定

多纹理混合

WebGL 支持同时使用多个纹理,通过纹理单元(Texture Unit)管理:

javascript
// 纹理1 绑定到 TEXTURE0 gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture1); gl.uniform1i(texture1Location, 0); // 纹理2 绑定到 TEXTURE1 gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D, texture2); gl.uniform1i(texture2Location, 1);
glsl
// 片段着色器 uniform sampler2D u_texture1; uniform sampler2D u_texture2; varying vec2 v_texCoord; void main() { vec4 color1 = texture2D(u_texture1, v_texCoord); vec4 color2 = texture2D(u_texture2, v_texCoord); gl_FragColor = mix(color1, color2, 0.5); }

立方体纹理(CubeMap)

CubeMap 由 6 个正方形纹理组成,分别对应立方体的 6 个面。与 2D 纹理的关键区别:

特性2D 纹理CubeMap
目标gl.TEXTURE_2Dgl.TEXTURE_CUBE_MAP
采样器sampler2DsamplerCube
坐标二维 (s, t)三维方向向量 (x, y, z)
采样函数texture2D()textureCube()
典型用途表面贴图环境反射、天空盒
javascript
// 创建 CubeMap const cubeTexture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_CUBE_MAP, cubeTexture); // 为六个面分别设置纹理 const faces = [ gl.TEXTURE_CUBE_MAP_POSITIVE_X, gl.TEXTURE_CUBE_MAP_NEGATIVE_X, gl.TEXTURE_CUBE_MAP_POSITIVE_Y, gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, gl.TEXTURE_CUBE_MAP_POSITIVE_Z, gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, ]; faces.forEach((face, i) => { gl.texImage2D(face, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, images[i]); });

完整纹理加载函数

javascript
function loadTexture(gl, url) { const texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture); // 临时 1x1 蓝色像素,图片加载完成前使用 gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]) ); const image = new Image(); image.onload = function () { gl.bindTexture(gl.TEXTURE_2D, texture); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image ); if (isPowerOf2(image.width) && isPowerOf2(image.height)) { gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR); } else { 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); }; image.src = url; return texture; } function isPowerOf2(value) { return (value & (value - 1)) === 0; }

性能优化要点

  • 纹理图集(Texture Atlas):将多个小纹理合并为一张大图,用不同 UV 坐标区分,减少纹理切换和绘制调用
  • 优先使用 2 的幂次尺寸:128、256、512、1024、2048,才能启用 Mipmap 和 REPEAT 环绕
  • 纹理尺寸上限:移动端保证支持 2048x2048,通过 gl.getParameter(gl.MAX_TEXTURE_SIZE) 查询实际限制
  • 压缩纹理格式:DXT(桌面端)、ETC(Android)、PVRTC(iOS),减少显存占用和加载时间
  • 及时释放纹理gl.deleteTexture(texture) 回收 GPU 资源,避免内存泄漏
  • Mipmap 内存开销:仅增加约 30%,但在远距离渲染时显著提升质量和性能
标签:WebGL