纹理基础
本教程共 40 篇 · 第 15 篇 · 更新于 2026-08-14 · 约 8 分钟阅读
本节目标:学会用 TextureLoader 加载纹理并贴到材质上,掌握 UV 映射概念、三种环绕方式、repeat 与 offset 的用法和最基本的过滤设置。
纹理是什么
材质只靠 color 的话,整个表面是一种颜色,像刚出厂的塑料。现实世界的表面有木纹、砖缝、锈迹。这些细节用几何体做太贵,用”一张图贴在表面”就是纹理(texture)。 打个比方:几何体是雕像的坯子,材质是给坯子上色的颜料,纹理则是印着图案的贴纸。贴纸可以印木纹、印砖缝、印商标,想换图案就换贴纸,不用重新雕坯子。
先分清三个术语:
- 图片(image):给人看的 2D 画面;
- 纹理(texture):给 3D 渲染用的 2D 数据,可以是颜色,也可以是粗糙度、法线等数据(第 16 章展开);
- 纹素(texel):纹理里的一个数据点,用来和屏幕上的像素(pixel)区分。
UV 映射:纹理怎么贴上去
把 2D 纹理贴到 3D 表面,需要建立对应关系,这个技术叫 UV 映射。U 和 V 是纹理上的两个坐标轴,范围都是 0~1:(0,0) 在左下角,(1,1) 在右上角,中心是 (0.5,0.5)。
几何体的每个顶点都带一组 UV 坐标,告诉渲染器”这个顶点对应纹理上的哪个点”,顶点之间的点由插值得到。内置几何体(Box、Sphere……)出厂就带好了 UV,所以纹理直接能用;外部建模软件导出的模型一般也带 UV。 以盒子为例:它的六个面各自映射了完整的一张纹理(0~1 的整个范围),所以一张纹理正好铺满六个面,每个面各一张。球体则把纹理像地图一样包裹上去,两极会有收拢变形,这是 UV 展开方式决定的,不是 bug。
Tip想知道一个几何体的 UV 长什么样,加载官方示例纹理
uv_grid_opengl.jpg(带 UV 标尺的网格图)贴上去看。哪里拉伸、哪里接缝,一目了然。
用 TextureLoader 加载纹理
加载纹理用 TextureLoader:
const loader = new THREE.TextureLoader();
const texture = loader.load('https://threejs.org/examples/textures/uv_grid_opengl.jpg');
然后把纹理赋给材质的 map(颜色贴图)槽位:
const material = new THREE.MeshStandardMaterial({ map: texture });
loader.load() 是异步的,但返回值可以立刻使用:three.js 会在图片加载完成后自动更新纹理。如果加载失败,材质保持默认外观(一般是黑色或默认色)。
Note加载本地图片时,不能直接双击 HTML 文件打开(file:// 协议会被浏览器安全策略拦截)。要在项目目录起一个本地服务器,比如运行
npx serve或python -m http.server,然后通过 http://localhost 访问页面。
纹理从哪里来
写示例时最头疼的是”图从哪来”。常用来源:
- three.js 官方仓库的 examples/textures 目录,随 CDN 可访问,适合学习和测试;
- 免费 PBR 素材站(Poly Haven、freepbr 等),下载整套颜色、法线、粗糙度贴图;
- 自己拍照或画图,用 Canvas 程序生成(第 16 章);
- 直接引用在线图片 URL,注意对方服务器要允许跨域访问(CORS)。
Note纹理文件建议单独放在 textures 目录里,用相对路径加载,比如
loader.load('textures/wood.jpg')。前提是页面跑在本地服务器上,而不是 file:// 协议。
三种环绕方式:wrapS 与 wrapT
纹理坐标超出 0~1 范围时怎么处理,由 wrapS(U 方向)和 wrapT(V 方向)决定,默认都是 ClampToEdgeWrapping:
ClampToEdgeWrapping:边缘像素无限拉伸,超出部分显示最边缘的颜色;RepeatWrapping:纹理平铺重复,铺地板、砖墙就靠它;MirroredRepeatWrapping:重复时镜像翻转,接缝处连续,适合左右对称的纹理。
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
repeat / offset / rotation:摆放纹理
配合 RepeatWrapping,用 repeat 控制平铺次数:
texture.repeat.set(4, 4); // U 方向重复 4 次,V 方向重复 4 次
offset 平移纹理(范围 0~1),rotation 旋转纹理(弧度制,绕 center 点旋转):
center 是旋转的支点,默认 (0,0) 在左下角;想绕纹理中心转,设成 (0.5, 0.5)。
texture.offset.set(0.5, 0.25); // 正 offset 使纹理向左、向下移动(与直觉相反):这里向左平移半个纹理、向下平移四分之一
texture.rotation = Math.PI / 4; // 旋转 45 度
Note
repeat大于 1 时,环绕方式必须是RepeatWrapping或MirroredRepeatWrapping,否则平铺不生效。
过滤:纹理缩放时怎么办
纹理被放大(近距离观察)或缩小(远距离观察)时,要用过滤算法决定取什么颜色。两个属性:
magFilter:纹理比屏幕大(放大)时,默认LinearFilter(双线性插值,平滑);NearestFilter(最近邻,颗粒感)适合像素风游戏;minFilter:纹理比屏幕小(缩小)时,默认LinearMipmapLinearFilter(mipmap 三线性过滤,效果最好)。
texture.magFilter = THREE.NearestFilter; // 像素风游戏常用
mipmap 是什么、各向异性过滤怎么调,下一章展开。现在记住:默认过滤设置已经很好了,不用动;要做像素风才改 magFilter。 两个过滤器触发的时机不同:纹理被放大(比如凑近看一堵墙)走 magFilter;被缩小(站远了看)走 minFilter。同一时刻只会有一个生效,所以调参时要先搞清楚”现在是离得近还是离得远”。
完整示例:贴纹理与平铺
一个盒子贴 UV 网格纹理,一块地面用 RepeatWrapping 平铺 4 x 2 次。保存成 HTML 直接运行: 运行后先看盒子:六个面各有一张完整的网格图,边角处能看到 UV 的接缝。再看地面:同一张图被平铺成 4 x 2 的网格。把地面材质的 repeat 改成 (1,1) 再运行,对比平铺与不平铺的差别。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>纹理基础</title>
<style>
html, body { margin: 0; height: 100%; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(3, 3, 6);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 3));
const dirLight = new THREE.DirectionalLight(0xffffff, 3);
dirLight.position.set(5, 8, 6);
scene.add(dirLight);
const controls = new OrbitControls(camera, renderer.domElement);
// 加载纹理(官方示例的 UV 网格图)
const texture = new THREE.TextureLoader().load(
'https://threejs.org/examples/textures/uv_grid_opengl.jpg'
);
// 盒子:一张纹理正好覆盖六个面
const box = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 1.5, 1.5),
new THREE.MeshStandardMaterial({ map: texture })
);
box.position.x = -1.8;
scene.add(box);
// 地面:clone 一份纹理,单独设置平铺
const floorTexture = texture.clone();
floorTexture.wrapS = THREE.RepeatWrapping;
floorTexture.wrapT = THREE.RepeatWrapping;
floorTexture.repeat.set(4, 2); // 平铺 4 x 2 次
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(4, 2),
new THREE.MeshStandardMaterial({
map: floorTexture,
side: THREE.DoubleSide,
})
);
floor.position.x = 2.2;
floor.rotation.y = -0.5;
scene.add(floor);
function animate() {
requestAnimationFrame(animate);
box.rotation.y += 0.01;
controls.update();
renderer.render(scene, camera);
}
animate();
addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
Tip
texture.clone()复制一份纹理,但共享同一张图片数据。这样两个物体可以用同一张图,各自独立设置 repeat、offset 等参数,不互相影响。
常见问题
- 纹理加载不出来,物体是黑的:先看浏览器控制台有没有 404 或 CORS 报错;本地文件必须用本地服务器打开,不能直接双击 HTML。
- repeat 设了没反应:
wrapS/wrapT必须是RepeatWrapping或MirroredRepeatWrapping,默认的ClampToEdgeWrapping不会平铺。 - 纹理颜色发灰、发暗:这是色彩空间问题,把
texture.colorSpace设为THREE.SRGBColorSpace,下一章会详细讲。 - 纹理看着糊:可能是放大过滤用了
LinearFilter但图片本身很小,换高清图;远处模糊的问题见下一章的各向异性过滤。 - 物体两面纹理颜色不一样:平面只有正面贴了图,背面是反面(或看不见),检查
side设置。
小结
纹理 = 图片 + 一组”怎么贴”的规则。TextureLoader 负责加载,map 槽位负责接收,wrap 管环绕,repeat、offset、rotation 管摆放,filter 管缩放质量。UV 映射是背后的机制,内置几何体都已备好,直接用即可。下一章解决”颜色不对、远处闪烁”这些进阶问题。