纹理进阶
本教程共 40 篇 · 第 16 篇 · 更新于 2026-08-14 · 约 9 分钟阅读
本节目标:解决纹理”颜色不对、远处闪烁、斜着看糊”三大问题,学会色彩空间、mipmap、各向异性过滤,并掌握 CanvasTexture、VideoTexture 和加载进度管理。
色彩空间:为什么纹理颜色发灰
计算机图形里有两种颜色空间:
- sRGB:显示器用的颜色空间,PNG、JPG 图片基本都是 sRGB;
- 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;
注意三点:
- 视频必须
muted加playsinline,移动端才允许自动播放; - 纹理默认跟随视频帧更新,无需手动刷新;
- 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 即可修正。
其他纹理类型(认识即可)
CubeTexture立方体纹理:六张纹理拼成环境,用于天空盒和环境反射;- 全景纹理:一张全景图,配合
PMREMGenerator生成环境光照; - 压缩纹理(KTX2 / Basis):GPU 直接解压的格式,加载快、省内存,大模型必备;加载它需要
KTX2Loader(在 three/addons/loaders/ 下),配合 BasisUniversal 解压器使用; 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 头,跨域加载没问题。用其他视频源时,要确认服务器允许跨域访问,否则纹理加载会被浏览器拦截。
常见问题
- 纹理颜色发灰、发暗:忘了设
texture.colorSpace = THREE.SRGBColorSpace。注意只有颜色贴图要设,法线、粗糙度这类数据贴图不能设。 - 远处纹理闪烁、出现摩尔纹:mipmap 没生效。检查
texture.generateMipmaps是否为 true、minFilter是否带 Mipmap 字样。 - 斜着看地面一片糊:各向异性过滤没开,把
texture.anisotropy调到 8~16。 - 视频纹理不显示:视频没在播放(
video.play()没调用或没 muted),或者视频源不支持跨域。 - 动态改 Canvas 内容没变化:改完 canvas 要设置
texture.needsUpdate = true,强制重新上传。 - 加载失败没有任何提示:给
loader.load()传 onError 回调,或统一用 LoadingManager 的 onError 收集错误。
小结
本章解决的是纹理的三大质量问题:颜色发灰(色彩空间)、远处闪烁(mipmap)、斜视模糊(各向异性过滤)。CanvasTexture 和 VideoTexture 让纹理不再局限于图片文件,LoadingManager 让加载过程可控。到这里,材质与纹理的基础已经齐了,下一部分开始讲灯光。 给材质配纹理时,可以按这个顺序自查:加载(Loader)→ 标记(colorSpace)→ 摆放(wrap、repeat)→ 质量(过滤、anisotropy)→ 释放(dispose)。每一步都有对应的属性,出了问题逐个排查即可。