纹理与贴图
纹理决定表面看起来是什么。颜色偏了、图案重复了、远处糊成一片, 原因通常不在材质颜色,而在纹理的颜色空间与采样设置。
加载一张颜色贴图
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。同一张图既当颜色又当数据时,加载两次并分别设置。
| 设置 | 取值 | 影响 |
|---|---|---|
colorSpace | SRGBColorSpace / NoColorSpace | 颜色贴图用 sRGB,数据贴图保持线性 |
wrapS / wrapT | Repeat / Clamp / Mirrored | 超出 0–1 的 UV 如何取样 |
repeat | Vector2 | 平铺次数,需要配合 RepeatWrapping |
offset | Vector2 | 平移取样起点 |
minFilter | LinearMipmapLinear 等 | 缩小时是否使用 mipmap |
anisotropy | 1 到最大值 | 斜视角下的清晰度 |
用 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)。