Skip to main content

第 6 节:纹理系统——从 Image 到 GPU 纹理对象

纹理(Texture)是 Three.js 中最复杂的 GPU 资源之一。一个 Texture 对象涵盖了从图片加载、参数映射、GPU 上传、Mipmap 生成到纹理单元分配的全链路。


🏗️ Texture 类的双层设计

Texture (JS 侧接口)
├── 用户配置:wrapS/T, magFilter/minFilter, format, type, anisotropy ...
├── 数据源:image / canvas / video / DataTexture (TypedArray)
├── 状态:version, needsUpdate
└── 内部映射:WebGLTextures 负责实际 GPU 操作

Three.js 把"用户友好的 JS API"和"底层 GL 调用"解耦:

  • Texture 类:只描述"纹理想怎样"
  • WebGLTextures 模块:负责"纹理实际上传"

🎨 参数映射:JS → GL

Three.js 的 Texture 字段名和 WebGL 的常量名完全不同。映射关系在 WebGLTextures 模块里:

// 简化源码:src/renderers/webgl/WebGLTextures.js
const wrapToGL = {
[THREE.RepeatWrapping]: gl.REPEAT,
[THREE.ClampToEdgeWrapping]: gl.CLAMP_TO_EDGE,
[THREE.MirroredRepeatWrapping]: gl.MIRRORED_REPEAT
};
const filterToGL = {
[THREE.NearestFilter]: gl.NEAREST,
[THREE.LinearFilter]: gl.LINEAR,
[THREE.NearestMipmapNearestFilter]: gl.NEAREST_MIPMAP_NEAREST,
[THREE.LinearMipmapLinearFilter]: gl.LINEAR_MIPMAP_LINEAR
};

调用 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrapToGL[texture.wrapS])


🖼️ 上传:处理多种数据源

WebGLTextures.uploadTexture() 是入口,内部按数据类型分支:

数据源texImage2D 参数
Image / Canvas / ImageBitmap直接传 image
DataTexture (TypedArray)array
CompressedTexturecompressedTexImage2D
VideoTexture每帧更新(特殊处理)

异步问题:图片加载是异步的,所以 Three.js 设计了"1x1 占位纹理"模式——

// 创建后立即绑定一个 1x1 透明占位
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, transparentPixel);
// 真正图片加载完成后,调用 texImage2D 覆盖

这样可以避免"图片没加载完就画了"的尴尬。


🌀 Mipmap 策略

Three.js 的 Mipmap 生成逻辑在 texture.update() 内部:

// 简化源码
if (textureNeedsPowerOfTwo(texture) && texture.generateMipmaps) {
gl.generateMipmap(gl.TEXTURE_2D);
} else {
// 非 POT 纹理必须降级
texture.minFilter = THREE.LinearFilter; // 不能用 mipmap 系列
}
POT 兼容性

WebGL 1.0 中只有 POT(2 的幂次方)纹理才能使用 Mipmap 和 REPEAT 包裹。WebGL 2.0 放宽了,但 Three.js 仍默认按 WebGL 1.0 行为保守处理。


📦 压缩纹理(CompressedTexture)

直接用 GPU 压缩格式(ASTC、ETC2、S3TC),减少 75% 显存占用:

// 用例
const loader = new KTX2Loader();
loader.load('texture.ktx2', (texture) => {
material.map = texture;
// Three.js 内部会用 gl.compressedTexImage2D 上传
});

压缩格式对照表

格式平台压缩比
S3TC (DXT)Desktop4:1
ETC2Android / WebGL24:1
ASTCiOS / 现代 GPU可变(4:1~36:1)
Basis Universal跨平台自适应

🔌 纹理单元:allocateTextureUnit()

一张 Shader 同时只能采样 N 张纹理,N 取决于 GPU(通常 16-32)。Three.js 用一个计数器分配:

// 简化源码
let textureUnits = 0;
function allocateTextureUnit() {
const unit = textureUnits;
textureUnits = (textureUnits + 1) % maxTextures;
return unit;
}

// 在 Material 编译时,每个 sampler uniform 都会调用这个函数

优化renderer.info.programs[0].samplerUniforms 可以看到当前 Material 用了多少纹理单元。


💾 内存估算

// 简化公式
function estimateTextureMemory(texture) {
const { width, height, format, type, generateMipmaps } = texture;
const bytesPerPixel = getBytesPerPixel(format, type); // RGBA = 4
let memory = width * height * bytesPerPixel;
if (generateMipmaps) {
memory *= 1.333; // Mipmap 大约增加 1/3
}
return memory;
}

示例:1024×1024 RGBA 纹理 = $4\text{MB}$,加 Mipmap = $5.33\text{MB}$。


🎯 Mini 实现

class MiniTexture {
constructor({ image, wrapS = 'REPEAT', wrapS2 = 'REPEAT', minFilter = 'LINEAR' }) {
this.image = image;
this.wrapS = wrapS;
this.minFilter = minFilter;
this.glTexture = null;
}

upload(gl) {
if (!this.glTexture) {
this.glTexture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.glTexture);
// 1x1 占位
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([255, 255, 255, 255]));
}
// 异步加载
const img = new Image();
img.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, this.glTexture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img);
gl.generateMipmap(gl.TEXTURE_2D);
};
img.src = this.image;
}
}

🆚 OGL 对比

OGL 的 Texture 类几乎就是 WebGL 的薄包装

// OGL: src/Texture.js
class Texture {
constructor(gl, { image, wrapS = gl.REPEAT, ... } = {}) {
this.gl = gl;
this.texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, this.texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
// 直接应用参数,不缓存 version
}
}

设计取舍:OGL 没有异步占位机制、版本控制、压缩格式支持。换来的是200 行 vs 1000+ 行的代码量差异。


🎓 深度思考

  1. 为什么 Three.js 要单独维护 WebGLTextures 模块?
    解耦"用户配置"和"实际 GL 调用"。Texture 类只关心"我是什么",WebGLTextures 才关心"怎么上传到 GPU"。这样未来支持 WebGPU 时,只需重写 WebGLTextures,不需要动 Texture 类

  2. flipY 是历史包袱还是合理设计?
    历史包袱。WebGL 纹理坐标系和图片坐标系 Y 轴方向相反,所以默认要翻转。新 API(如 WebGPU)已经统一了方向。

  3. 什么时候应该用 CubeTexture?
    需要环境反射的场景:天空盒、PBR 材质的环境贴图、立方体全景图。它一次性传 6 张 2D 纹理给 GPU。


📚 延伸阅读