首页 / Three.js 入门教程 / 纹理进阶

Three.js 入门教程

纹理进阶

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

Three.js纹理色彩空间mipmapCanvasTexture视频纹理

本节目标:解决纹理”颜色不对、远处闪烁、斜着看糊”三大问题,学会色彩空间、mipmap、各向异性过滤,并掌握 CanvasTexture、VideoTexture 和加载进度管理。

色彩空间:为什么纹理颜色发灰

计算机图形里有两种颜色空间:

  1. sRGB:显示器用的颜色空间,PNG、JPG 图片基本都是 sRGB;
  2. Linear:光照计算用的线性空间,物理上”正确”的亮度。

three.js 从 r152 起默认开启色彩管理,渲染结果会转换到 sRGB 输出(renderer.outputColorSpace 默认就是 SRGBColorSpace)。但纹理默认按线性空间处理——把一张 sRGB 图片当线性数据用,颜色就会整体偏暗、发灰。

three.js 渲染管线内部用 Linear 计算光照,最后输出时再转回 sRGB 给显示器。这条链路里,凡是”输入的颜色数据”都要标记它原本的空间:图片文件是 sRGB,就标 SRGBColorSpace;数据贴图是线性数据,就保持默认。漏标 sRGB 的常见症状是整体偏暗、颜色发灰,红色尤其明显。

解决办法:给颜色贴图标记 sRGB 空间:

const texture = new THREE.TextureLoader().load('texture.jpg');
texture.colorSpace = THREE.SRGBColorSpace; // 关键一步
Note

只有”给人看的颜色”贴图(map、emissiveMap)需要设 SRGBColorSpace。数据贴图——法线贴图、粗糙度贴图、金属度贴图、AO 贴图——必须保持默认的 NoColorSpace,否则光照计算全乱。

mipmap:为什么远处纹理闪烁

物体远离相机时,一个屏幕像素可能对应纹理里几十个纹素。如果直接抽样,画面会剧烈闪烁,出现摩尔纹和噪点。解决方案是 mipmap:预生成一组逐级缩小一半的纹理金字塔(原图、1/2、1/4、1/8……),远处采样时按比例选合适的层级,相邻层级之间再插值。

three.js 默认开启 mipmap:

const texture = new THREE.TextureLoader().load('texture.jpg');
console.log(texture.generateMipmaps); // true,默认开启
console.log(texture.minFilter);       // LinearMipmapLinearFilter,三线性过滤

minFilter 默认就是 LinearMipmapLinearFilter,效果最好,一般不用改。 mipmap 失效的典型症状:物体近看正常,拉远后表面疯狂闪烁、出现一圈圈波纹。遇到这种情况,先检查 generateMipmaps 是否被误设成 false,或者 minFilter 是否被换成了不带 Mipmap 的选项。

Tip

老教程会说”纹理尺寸必须是 2 的幂(256、512、1024)才能用 mipmap 和重复环绕”。这是 WebGL1 的限制。three.js r163 起只支持 WebGL2,非 2 的幂纹理完全没问题。不过美术资源仍然建议用 2 的幂尺寸,兼容性和压缩效率更好。

各向异性过滤:解决”斜着看糊一片”

mipmap 有个缺点:斜着看地面或墙面时,纹理在某个方向被压得很扁,mipmap 会选很深的层级,导致画面模糊。各向异性过滤(anisotropic filtering)沿视线方向多采样几次,专治这种”斜视糊”:

const texture = new THREE.TextureLoader().load('texture.jpg');
texture.anisotropy = 16; // 或者用显卡支持的最大值

更稳妥的写法是问渲染器要上限:

texture.anisotropy = renderer.capabilities.getMaxAnisotropy();

各向异性值越大效果越好、开销越高,8~16 是常见选择。地面、墙面这类大面积斜视表面,收益立竿见影。

CanvasTexture:程序生成纹理

不想用图片文件?可以用 Canvas 画一张图当纹理。CanvasTexture 把 canvas 包成纹理,特别适合动态内容:数据可视化、动态数字、程序化图案。

// 1. 画一张棋盘格
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#334455';
ctx.fillRect(0, 0, 256, 256);
ctx.fillStyle = '#99aabb';
ctx.fillRect(0, 0, 128, 128);
ctx.fillRect(128, 128, 128, 128);

// 2. 包成纹理
const texture = new THREE.CanvasTexture(canvas);
texture.colorSpace = THREE.SRGBColorSpace; // 是颜色贴图,记得标记

// 3. 像普通纹理一样用
const material = new THREE.MeshStandardMaterial({ map: texture });

Canvas 内容更新后,设置 texture.needsUpdate = true,three.js 会重新上传纹理。

VideoTexture:视频纹理

<video> 元素包成纹理,就是”会动的纹理”:电视屏幕、广告牌、水面特效。

<video id="video" src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
       autoplay muted loop playsinline></video>
const video = document.getElementById('video');
video.play(); // 视频必须处于播放状态,纹理才会更新

const texture = new THREE.VideoTexture(video);
texture.colorSpace = THREE.SRGBColorSpace;

注意三点:

  1. 视频必须 mutedplaysinline,移动端才允许自动播放;
  2. 纹理默认跟随视频帧更新,无需手动刷新;
  3. VideoTexture 本质是每帧把视频画面上传到 GPU,多个大视频会明显吃性能。

加载管理与进度

TextureLoader.load() 有四个参数:地址、完成回调、进度回调、失败回调。注意 TextureLoader 的进度回调拿不到具体百分比:

const loader = new THREE.TextureLoader();
const texture = loader.load(
  'texture.jpg',
  (tex) => console.log('加载完成', tex.image),
  undefined,
  (err) => console.error('加载失败', err)
);

多个文件同时加载、想统一管理,用 LoadingManager

const manager = new THREE.LoadingManager();
manager.onLoad = () => console.log('全部加载完成');
manager.onError = (url) => console.error('加载失败:', url);

const loader = new THREE.TextureLoader(manager);
loader.load('a.jpg');
loader.load('b.jpg');

LoadingManager 可以传给所有 Loader(模型、字体同理),第 31 章会用它做加载进度条。

纹理的内存与细节

纹理数据存在 GPU 显存里,一张 2048x2048 的 RGBA 纹理约占用 16MB 显存,大场景里积少成多。移除物体时,记得释放相关资源:

mesh.geometry.dispose();   // 释放顶点数据
mesh.material.dispose();   // 释放材质与着色器
texture.dispose();         // 释放纹理显存

还有一个细节:flipY。three.js 默认把纹理沿 Y 轴翻转上传(flipY: true),和大多数建模软件一致。个别软件导出的模型纹理上下颠倒,把 texture.flipY = false 即可修正。

其他纹理类型(认识即可)

  1. CubeTexture 立方体纹理:六张纹理拼成环境,用于天空盒和环境反射;
  2. 全景纹理:一张全景图,配合 PMREMGenerator 生成环境光照;
  3. 压缩纹理(KTX2 / Basis):GPU 直接解压的格式,加载快、省内存,大模型必备;加载它需要 KTX2Loader(在 three/addons/loaders/ 下),配合 BasisUniversal 解压器使用;
  4. DataTexture:用数组数据直接构造纹理,程序生成噪声图常用。

这些用到时再展开,现在记住名字,遇到需求知道”有这么个东西”就行。

完整示例:棋盘格地板 + 视频屏幕

一个 CanvasTexture 当棋盘格地板,一个 VideoTexture 当视频屏幕。保存成 HTML 直接运行:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>纹理进阶</title>
  <style>
    html, body { margin: 0; height: 100%; }
    #video { display: none; } /* 视频不显示在页面上,只当纹理用 */
  </style>
</head>
<body>
  <video id="video" src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
         autoplay muted loop playsinline></video>

  <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, 2.5, 5);
    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);

    // 1. CanvasTexture:程序生成的棋盘格地板
    const canvas = document.createElement('canvas');
    canvas.width = 256;
    canvas.height = 256;
    const ctx = canvas.getContext('2d');
    ctx.fillStyle = '#334455';
    ctx.fillRect(0, 0, 256, 256);
    ctx.fillStyle = '#99aabb';
    ctx.fillRect(0, 0, 128, 128);
    ctx.fillRect(128, 128, 128, 128);

    const checkerTexture = new THREE.CanvasTexture(canvas);
    checkerTexture.colorSpace = THREE.SRGBColorSpace;

    const floor = new THREE.Mesh(
      new THREE.PlaneGeometry(5, 5),
      new THREE.MeshStandardMaterial({
        map: checkerTexture,
        side: THREE.DoubleSide,
      })
    );
    floor.rotation.x = -Math.PI / 2;
    scene.add(floor);

    // 2. VideoTexture:视频当屏幕
    const video = document.getElementById('video');
    video.play();

    const videoTexture = new THREE.VideoTexture(video);
    videoTexture.colorSpace = THREE.SRGBColorSpace;

    const screen = new THREE.Mesh(
      new THREE.PlaneGeometry(2.4, 1.35), // 16:9
      new THREE.MeshStandardMaterial({ map: videoTexture })
    );
    screen.position.set(0, 1.6, -1);
    scene.add(screen);

    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    addEventListener('resize', () => {
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(innerWidth, innerHeight);
    });
  </script>
</body>
</html>
Note

视频来自 MDN 的示例资源(cc0 协议),带 CORS 头,跨域加载没问题。用其他视频源时,要确认服务器允许跨域访问,否则纹理加载会被浏览器拦截。

常见问题

  1. 纹理颜色发灰、发暗:忘了设 texture.colorSpace = THREE.SRGBColorSpace。注意只有颜色贴图要设,法线、粗糙度这类数据贴图不能设。
  2. 远处纹理闪烁、出现摩尔纹:mipmap 没生效。检查 texture.generateMipmaps 是否为 true、minFilter 是否带 Mipmap 字样。
  3. 斜着看地面一片糊:各向异性过滤没开,把 texture.anisotropy 调到 8~16。
  4. 视频纹理不显示:视频没在播放(video.play() 没调用或没 muted),或者视频源不支持跨域。
  5. 动态改 Canvas 内容没变化:改完 canvas 要设置 texture.needsUpdate = true,强制重新上传。
  6. 加载失败没有任何提示:给 loader.load() 传 onError 回调,或统一用 LoadingManager 的 onError 收集错误。

小结

本章解决的是纹理的三大质量问题:颜色发灰(色彩空间)、远处闪烁(mipmap)、斜视模糊(各向异性过滤)。CanvasTexture 和 VideoTexture 让纹理不再局限于图片文件,LoadingManager 让加载过程可控。到这里,材质与纹理的基础已经齐了,下一部分开始讲灯光。 给材质配纹理时,可以按这个顺序自查:加载(Loader)→ 标记(colorSpace)→ 摆放(wrap、repeat)→ 质量(过滤、anisotropy)→ 释放(dispose)。每一步都有对应的属性,出了问题逐个排查即可。