5月28日 01:36
WebGL 中的纹理(Texture)如何使用?有哪些纹理参数需要配置?
纹理是 WebGL 渲染的核心机制
纹理(Texture)是将 2D 图像数据映射到 3D 几何体表面的技术。在 WebGL 中,几乎所有视觉效果——地板砖纹、角色皮肤、天空背景——都依赖纹理实现。理解纹理的使用流程和参数配置,是掌握 WebGL 的关键一步。
纹理使用的完整流程
1. 创建并绑定纹理对象
javascriptconst texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, texture);
bindTexture 将纹理对象绑定到当前纹理单元,后续所有纹理操作都针对该绑定对象。
2. 翻转 Y 轴(面试高频考点)
WebGL 纹理坐标原点在左下角,而图片坐标原点在左上角。不翻转会导致纹理倒置:
javascriptgl.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(可选)
javascriptgl.generateMipmap(gl.TEXTURE_2D);
Mipmap 会生成一系列逐级减半的纹理副本,在物体远离相机时使用更小的纹理,既提升渲染质量又节省带宽。内存开销仅增加约 30%。
纹理参数详解
纹理环绕方式(Texture Wrapping)
控制纹理坐标超出 [0, 1] 范围时的行为:
| 参数值 | 效果 | 典型场景 |
|---|---|---|
gl.REPEAT | 重复平铺 | 地板砖纹、墙壁图案 |
gl.CLAMP_TO_EDGE | 边缘像素延伸 | 天空盒、非重复贴图 |
gl.MIRRORED_REPEAT | 镜像重复 | 对称图案贴图 |
shellREPEAT: 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_LINEAR | mipmap 间线性过渡 + 最近采样 |
gl.LINEAR_MIPMAP_LINEAR | 三线性过滤,质量最高 |
javascriptgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
着色器中的纹理使用
顶点着色器
glslattribute 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; }
片段着色器
glslprecision mediump float; varying vec2 v_texCoord; uniform sampler2D u_texture; void main() { gl_FragColor = texture2D(u_texture, v_texCoord); }
JavaScript 端绑定
javascriptgl.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_2D | gl.TEXTURE_CUBE_MAP |
| 采样器 | sampler2D | samplerCube |
| 坐标 | 二维 (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]); });
完整纹理加载函数
javascriptfunction 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%,但在远距离渲染时显著提升质量和性能