首页 / Three.js 入门教程 / 纹理基础

Three.js 入门教程

纹理基础

本教程共 40 篇 · 第 15 篇 · 更新于 2026-08-14 · 约 8 分钟阅读

Three.js纹理TextureLoaderUV贴图Texture

本节目标:学会用 TextureLoader 加载纹理并贴到材质上,掌握 UV 映射概念、三种环绕方式、repeat 与 offset 的用法和最基本的过滤设置。

纹理是什么

材质只靠 color 的话,整个表面是一种颜色,像刚出厂的塑料。现实世界的表面有木纹、砖缝、锈迹。这些细节用几何体做太贵,用”一张图贴在表面”就是纹理(texture)。 打个比方:几何体是雕像的坯子,材质是给坯子上色的颜料,纹理则是印着图案的贴纸。贴纸可以印木纹、印砖缝、印商标,想换图案就换贴纸,不用重新雕坯子。

先分清三个术语:

  1. 图片(image):给人看的 2D 画面;
  2. 纹理(texture):给 3D 渲染用的 2D 数据,可以是颜色,也可以是粗糙度、法线等数据(第 16 章展开);
  3. 纹素(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 servepython -m http.server,然后通过 http://localhost 访问页面。

纹理从哪里来

写示例时最头疼的是”图从哪来”。常用来源:

  1. three.js 官方仓库的 examples/textures 目录,随 CDN 可访问,适合学习和测试;
  2. 免费 PBR 素材站(Poly Haven、freepbr 等),下载整套颜色、法线、粗糙度贴图;
  3. 自己拍照或画图,用 Canvas 程序生成(第 16 章);
  4. 直接引用在线图片 URL,注意对方服务器要允许跨域访问(CORS)。
Note

纹理文件建议单独放在 textures 目录里,用相对路径加载,比如 loader.load('textures/wood.jpg')。前提是页面跑在本地服务器上,而不是 file:// 协议。

三种环绕方式:wrapS 与 wrapT

纹理坐标超出 0~1 范围时怎么处理,由 wrapS(U 方向)和 wrapT(V 方向)决定,默认都是 ClampToEdgeWrapping

  1. ClampToEdgeWrapping:边缘像素无限拉伸,超出部分显示最边缘的颜色;
  2. RepeatWrapping:纹理平铺重复,铺地板、砖墙就靠它;
  3. 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 时,环绕方式必须是 RepeatWrappingMirroredRepeatWrapping,否则平铺不生效。

过滤:纹理缩放时怎么办

纹理被放大(近距离观察)或缩小(远距离观察)时,要用过滤算法决定取什么颜色。两个属性:

  1. magFilter:纹理比屏幕大(放大)时,默认 LinearFilter(双线性插值,平滑);NearestFilter(最近邻,颗粒感)适合像素风游戏;
  2. 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 等参数,不互相影响。

常见问题

  1. 纹理加载不出来,物体是黑的:先看浏览器控制台有没有 404 或 CORS 报错;本地文件必须用本地服务器打开,不能直接双击 HTML。
  2. repeat 设了没反应wrapS / wrapT 必须是 RepeatWrappingMirroredRepeatWrapping,默认的 ClampToEdgeWrapping 不会平铺。
  3. 纹理颜色发灰、发暗:这是色彩空间问题,把 texture.colorSpace 设为 THREE.SRGBColorSpace,下一章会详细讲。
  4. 纹理看着糊:可能是放大过滤用了 LinearFilter 但图片本身很小,换高清图;远处模糊的问题见下一章的各向异性过滤。
  5. 物体两面纹理颜色不一样:平面只有正面贴了图,背面是反面(或看不见),检查 side 设置。

小结

纹理 = 图片 + 一组”怎么贴”的规则。TextureLoader 负责加载,map 槽位负责接收,wrap 管环绕,repeat、offset、rotation 管摆放,filter 管缩放质量。UV 映射是背后的机制,内置几何体都已备好,直接用即可。下一章解决”颜色不对、远处闪烁”这些进阶问题。