经纬志
首页 / three.js / 教程

纹理与贴图

先看结论

纹理决定表面看起来是什么。颜色偏了、图案重复了、远处糊成一片, 原因通常不在材质颜色,而在纹理的颜色空间与采样设置。

最后验证 2026-11-03 · three.js 0.160.0 · 难度:入门 · 核验 2026-10-29 · 引用本文

加载一张颜色贴图

TextureLoader 返回的纹理默认按线性空间处理。 作为颜色使用的贴图必须显式标成 SRGBColorSpace, 否则渲染结果会偏暗。

const loader = new THREE.TextureLoader();

const colorMap = loader.load('/textures/terrain-color.jpg', function (tex) {
  tex.colorSpace = THREE.SRGBColorSpace;
  tex.wrapS = THREE.RepeatWrapping;
  tex.wrapT = THREE.RepeatWrapping;
  tex.repeat.set(4, 4);
  tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
  tex.needsUpdate = true;
});

const material = new THREE.MeshStandardMaterial({
  map: colorMap,
  roughness: 0.9,
  metalness: 0.0
});

数据类贴图(法线、粗糙度、金属度、位移)要保持线性空间, 不要设成 sRGB。同一张图既当颜色又当数据时,加载两次并分别设置。

设置取值影响
colorSpaceSRGBColorSpace / NoColorSpace颜色贴图用 sRGB,数据贴图保持线性
wrapS / wrapTRepeat / Clamp / Mirrored超出 0–1 的 UV 如何取样
repeatVector2平铺次数,需要配合 RepeatWrapping
offsetVector2平移取样起点
minFilterLinearMipmapLinear 等缩小时是否使用 mipmap
anisotropy1 到最大值斜视角下的清晰度

用 Canvas 生成专题图

按属性着色、加图例这类需求,用 Canvas 现画一张贴图比准备一堆图片更灵活。 CanvasTexture 接收任意 canvas,画完设置 needsUpdate 即可。

function makeGradientTexture(stops) {
  const canvas = document.createElement('canvas');
  canvas.width = 256;
  canvas.height = 1;
  const ctx = canvas.getContext('2d');

  const gradient = ctx.createLinearGradient(0, 0, 256, 0);
  stops.forEach(function (s) {
    gradient.addColorStop(s.at, s.color);
  });
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, 256, 1);

  const texture = new THREE.CanvasTexture(canvas);
  texture.colorSpace = THREE.SRGBColorSpace;
  texture.minFilter = THREE.LinearFilter;
  texture.magFilter = THREE.LinearFilter;
  texture.wrapS = THREE.ClampToEdgeWrapping;
  return texture;
}

const ramp = makeGradientTexture([
  { at: 0.0, color: '#1f5fd0' },
  { at: 0.5, color: '#f0a020' },
  { at: 1.0, color: '#d0453a' }
]);

一维渐变贴图宽度取 256 足够,高度设为 1,并把环绕方式设成 ClampToEdgeWrapping,避免边缘取样时出现颜色回卷。

UV 决定贴到哪

几何体的 uv 属性是一组二维坐标,范围通常为 0 到 1。 自定义几何必须提供 uv,否则纹理无法定位,只会得到一片纯色。

const size = 40;
const geometry = new THREE.PlaneGeometry(size, size, 1, 1);
const uv = geometry.attributes.uv;

// 默认平面 UV:左下 (0,0),右上 (1,1)
for (let i = 0; i < uv.count; i++) {
  uv.setXY(i, uv.getX(i) * 4, uv.getY(i) * 4);   // 平铺 4 次
}
uv.needsUpdate = true;

uv 超出 0 到 1 也能工作,但必须把 wrapS 与 wrapT 设成 RepeatWrapping,否则边缘会按最后一个像素拉伸。

重复次数写在 UV 上更省显存。改 texture.repeat 只是改变采样矩阵,效果相同,但同一张纹理要不同平铺密度时, 改 UV 可以共用一份纹理对象。

注意事项

  • 非 2 的幂尺寸纹理在 WebGL1 下无法使用 mipmap 与 RepeatWrapping,升到 WebGL2 后限制消除但仍建议用 2 的幂。
  • 同一时刻可绑定的纹理单元数量有限,材质里贴图过多会触发重新编译或渲染失败。
  • 修改纹理内容后要设置 needsUpdate = true,否则 GPU 侧仍是旧数据。
  • 纹理与几何一样占用显存,移除对象时调用 texture.dispose()。
  • 大尺寸纹理在移动端会显著增加内存,地形类贴图优先用压缩格式(KTX2)。
引用本文

经纬志(2026)。《纹理与贴图》。https://www.jingweizhi.com/three/tutorial/textures.html(访问日期 2026-11-03)。