纹理
纹理是 3D 图形中最重要的元素之一,它可以让简单的几何体呈现丰富的视觉效果。通过将图像贴在物体表面,我们可以模拟木材、金属、织物等各种材质,而不需要复杂的几何结构。本章将详细介绍 Three.js 中纹理的使用方法和各种纹理类型。
纹理基础
什么是纹理
纹理本质上是将 2D 图像"包裹"在 3D 物体表面。这个过程称为纹理映射。想象一下给礼物包装纸包裹礼物的过程:包装纸就是纹理,礼物就是几何体,而 UV 坐标则决定了包装纸如何贴合在礼物上。
UV 坐标系统
UV 坐标是纹理映射的基础。每个顶点除了有 3D 空间坐标 (x, y, z) 外,还有纹理坐标 (u, v):
- U 轴:纹理的水平方向,范围 0 到 1
- V 轴:纹理的垂直方向,范围 0 到 1
左下角是 (0, 0),右上角是 (1, 1)。几何体的每个顶点都有对应的 UV 坐标,告诉 GPU 这个顶点应该对应纹理的哪个位置。
// 查看几何体的 UV 坐标
const geometry = new THREE.BoxGeometry(1, 1, 1);
const uvAttribute = geometry.attributes.uv;
console.log('UV 坐标数量:', uvAttribute.count);
// BoxGeometry 每个面有 4 个顶点,6 个面共 24 个顶点
// 每个顶点有 2 个 UV 分量
Three.js 的内置几何体都已经预设了 UV 坐标,通常不需要手动处理。但当你创建自定义几何体时,需要自己设置 UV 坐标。
加载纹理
TextureLoader
TextureLoader 是最常用的纹理加载器,支持加载普通图片格式:
const textureLoader = new THREE.TextureLoader();
// 异步加载纹理
const texture = textureLoader.load('textures/wood.jpg');
// 带回调的加载方式
textureLoader.load(
'textures/wood.jpg',
(texture) => {
// 加载成功
console.log('纹理加载完成');
texture.colorSpace = THREE.SRGBColorSpace;
},
(progress) => {
// 加载进度
console.log(`加载进度: ${(progress.loaded / progress.total * 100).toFixed(0)}%`);
},
(error) => {
// 加载错误
console.error('纹理加载失败:', error);
}
);
使用 async/await
如果你使用现代 JavaScript,可以用 Promise 方式加载:
async function loadTexture() {
const textureLoader = new THREE.TextureLoader();
try {
const texture = await textureLoader.loadAsync('textures/wood.jpg');
texture.colorSpace = THREE.SRGBColorSpace;
return texture;
} catch (error) {
console.error('纹理加载失败:', error);
}
}
// 使用
const texture = await loadTexture();
const material = new THREE.MeshStandardMaterial({ map: texture });
LoadingManager 管理多个资源
当需要加载多个纹理时,使用 LoadingManager 可以统一管理:
const manager = new THREE.LoadingManager();
manager.onStart = (url, loaded, total) => {
console.log(`开始加载: ${url}`);
};
manager.onProgress = (url, loaded, total) => {
console.log(`进度: ${loaded}/${total}`);
};
manager.onLoad = () => {
console.log('所有资源加载完成');
};
manager.onError = (url) => {
console.error(`加载失败: ${url}`);
};
const textureLoader = new THREE.TextureLoader(manager);
const texture1 = textureLoader.load('texture1.jpg');
const texture2 = textureLoader.load('texture2.jpg');
纹理属性配置
颜色空间
颜色空间决定了纹理的颜色如何被解释和显示:
// sRGB 颜色空间(大多数图片)
texture.colorSpace = THREE.SRGBColorSpace;
// 线性颜色空间(法线贴图、粗糙度贴图等数据纹理)
texture.colorSpace = THREE.LinearSRGBColorSpace;
什么时候用 sRGB?
- 漫反射贴图(颜色贴图)
- 自发光贴图
- 任何包含颜色信息的贴图
什么时候用线性空间?
- 法线贴图
- 粗糙度贴图
- 金属度贴图
- 环境光遮蔽贴图
- 任何存储数值数据而非颜色的贴图
纹理包裹模式
当 UV 坐标超出 0-1 范围时,纹理如何处理:
// 重复(平铺)
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(2, 2); // 水平和垂直各重复 2 次
// 钳制到边缘(边缘像素延伸)
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
// 镜像重复(相邻重复区域镜像翻转)
texture.wrapS = THREE.MirroredRepeatWrapping;
texture.wrapT = THREE.MirroredRepeatWrapping;
wrapS 控制 U 轴(水平),wrapT 控制 V 轴(垂直)。
纹理过滤
纹理过滤决定了当纹理被放大或缩小时如何采样:
// 放大过滤(纹理比原始尺寸大)
texture.magFilter = THREE.NearestFilter; // 最近邻,像素化效果
texture.magFilter = THREE.LinearFilter; // 线性插值,平滑效果(默认)
// 缩小过滤(纹理比原始尺寸小)
texture.minFilter = THREE.NearestFilter; // 最近邻
texture.minFilter = THREE.LinearFilter; // 线性插值
texture.minFilter = THREE.NearestMipmapNearestFilter; // 最近邻 mipmap
texture.minFilter = THREE.NearestMipmapLinearFilter; // 线性 mipmap,最近邻采样
texture.minFilter = THREE.LinearMipmapNearestFilter; // 最近邻 mipmap,线性采样
texture.minFilter = THREE.LinearMipmapLinearFilter; // 三线性过滤(质量最好,默认)
选择建议:
- 像素艺术风格:使用
NearestFilter - 照片写实风格:使用
LinearMipmapLinearFilter - 性能优先:使用
LinearFilter或更简单的过滤方式
Mipmap
Mipmap 是预生成的一系列缩小版本的纹理,可以提高缩小纹理时的质量和性能:
// 手动生成 mipmap(通常不需要,Three.js 自动处理)
texture.generateMipmaps = true;
// 禁用 mipmap(对于某些特殊用途)
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter; // 禁用 mipmap 时必须使用不需要 mipmap 的过滤方式
各向异性过滤
各向异性过滤可以提高斜视角度下的纹理清晰度:
// 获取设备支持的最大各向异性等级
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
// 设置各向异性过滤
texture.anisotropy = maxAnisotropy; // 通常设为最大值或 4、8 等值
这在观察地板、道路等表面时效果明显,可以减少远处的模糊。
纹理变换
可以调整纹理的位置、旋转和中心点:
// 偏移
texture.offset.set(0.5, 0.5); // U 和 V 方向各偏移 50%
// 旋转(弧度)
texture.rotation = Math.PI / 4; // 旋转 45 度
// 旋转中心点(默认左下角 0,0)
texture.center.set(0.5, 0.5); // 以纹理中心为旋转中心
// 重复次数
texture.repeat.set(2, 3); // U 方向 2 次,V 方向 3 次
注意修改纹理属性后,如果纹理已经在使用中,可能需要设置 texture.needsUpdate = true。
纹理类型
普通纹理(Texture)
最常见的纹理类型,用于漫反射贴图、颜色贴图等:
const texture = new THREE.TextureLoader().load('color.jpg');
const material = new THREE.MeshStandardMaterial({
map: texture
});
CubeTexture 立方体纹理
立方体纹理由 6 张图片组成,用于环境映射和天空盒:
const cubeTextureLoader = new THREE.CubeTextureLoader();
const cubeTexture = cubeTextureLoader.load([
'skybox/px.jpg', // 正 X
'skybox/nx.jpg', // 负 X
'skybox/py.jpg', // 正 Y
'skybox/ny.jpg', // 负 Y
'skybox/pz.jpg', // 正 Z
'skybox/nz.jpg' // 负 Z
]);
// 作为场景背景(天空盒)
scene.background = cubeTexture;
// 作为环境贴图(反射)
scene.environment = cubeTexture;
CanvasTexture 画布纹理
将 Canvas 元素作为纹理源,可以实现动态纹理:
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
// 在 canvas 上绘制
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 256, 256);
ctx.fillStyle = '#0000ff';
ctx.fillRect(256, 0, 256, 256);
// 创建纹理
const texture = new THREE.CanvasTexture(canvas);
// 动态更新纹理
function animate() {
requestAnimationFrame(animate);
// 更新 canvas 内容
ctx.clearRect(0, 0, 512, 512);
ctx.fillStyle = `hsl(${Date.now() * 0.1 % 360}, 50%, 50%)`;
ctx.fillRect(0, 0, 512, 512);
// 通知纹理需要更新
texture.needsUpdate = true;
renderer.render(scene, camera);
}
VideoTexture 视频纹理
将视频作为纹理源:
const video = document.createElement('video');
video.src = 'video.mp4';
video.loop = true;
video.muted = true;
video.play();
const texture = new THREE.VideoTexture(video);
texture.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshStandardMaterial({ map: texture });
DataTexture 数据纹理
通过程序化数据创建纹理:
const width = 256;
const height = 256;
const size = width * height;
const data = new Uint8Array(size * 4); // RGBA 4 个通道
// 生成噪声纹理
for (let i = 0; i < size; i++) {
const stride = i * 4;
const value = Math.floor(Math.random() * 255);
data[stride] = value; // R
data[stride + 1] = value; // G
data[stride + 2] = value; // B
data[stride + 3] = 255; // A
}
const texture = new THREE.DataTexture(data, width, height);
texture.needsUpdate = true;
DepthTexture 深度纹理
存储深度信息的纹理,用于后期处理:
const depthTexture = new THREE.DepthTexture();
depthTexture.format = THREE.DepthFormat;
depthTexture.type = THREE.UnsignedShortType;
const renderTarget = new THREE.WebGLRenderTarget(width, height, {
depthTexture: depthTexture
});
材质中的纹理贴图
不同的材质支持不同类型的贴图:
标准材质贴图
const material = new THREE.MeshStandardMaterial({
map: colorTexture, // 漫反射/颜色贴图
normalMap: normalTexture, // 法线贴图
roughnessMap: roughnessTexture, // 粗糙度贴图
metalnessMap: metalnessTexture, // 金属度贴图
aoMap: aoTexture, // 环境光遮蔽贴图
emissiveMap: emissiveTexture, // 自发光贴图
alphaMap: alphaTexture, // 透明度贴图
lightMap: lightTexture // 光照贴图
});
法线贴图
法线贴图存储表面的法线方向,可以在不增加几何体复杂度的情况下模拟表面细节:
const normalTexture = new THREE.TextureLoader().load('normal.jpg');
normalTexture.colorSpace = THREE.LinearSRGBColorSpace;
const material = new THREE.MeshStandardMaterial({
map: colorTexture,
normalMap: normalTexture,
normalScale: new THREE.Vector2(1, 1) // 法线强度
});
法线贴图使用紫色作为基准色,因为这种颜色对应 Z 轴正方向的法线。
环境光遮蔽贴图
AO 贴图在角落和缝隙处添加阴影,增加立体感:
const aoTexture = new THREE.TextureLoader().load('ao.jpg');
aoTexture.colorSpace = THREE.LinearSRGBColorSpace;
const material = new THREE.MeshStandardMaterial({
map: colorTexture,
aoMap: aoTexture,
aoMapIntensity: 1.0 // AO 强度
});
// 注意:AO 贴图需要第二组 UV 坐标
geometry.setAttribute('uv2', geometry.attributes.uv);
粗糙度和金属度贴图
这些贴图控制表面的物理属性:
const material = new THREE.MeshStandardMaterial({
// 单独的贴图
roughnessMap: roughnessTexture,
metalnessMap: metalnessTexture,
// 也可以用组合贴图(ORM 贴图)
// R 通道 = 环境光遮蔽
// G 通道 = 粗糙度
// B 通道 = 金属度
aoMap: ormTexture,
roughnessMap: ormTexture,
metalnessMap: ormTexture
});
// 使用 roughness 和 metalness 贴图时,可以通过属性调整强度
material.roughness = 1.0; // 贴图值会乘以这个值
material.metalness = 1.0;
环境贴图
环境贴图用于模拟反射和折射效果:
HDR 环境贴图
使用 HDR 格式的环境贴图可以获得更真实的效果:
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const rgbeLoader = new RGBELoader();
rgbeLoader.load('environment.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
scene.background = texture;
});
PMREMGenerator
PMREMGenerator 可以将任何环境贴图预处理为更适合实时渲染的格式:
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();
// 从 HDR 加载
const envTexture = await new RGBELoader().loadAsync('environment.hdr');
const envMap = pmremGenerator.fromEquirectangular(envTexture).texture;
scene.environment = envMap;
// 释放临时资源
envTexture.dispose();
pmremGenerator.dispose();
程序化环境贴图
可以使用 Scene 为环境贴图生成内容:
const pmremGenerator = new THREE.PMREMGenerator(renderer);
// 创建一个场景用于生成环境贴图
const envScene = new THREE.Scene();
envScene.background = new THREE.Color(0x888888);
// 添加一些光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1);
envScene.add(light);
// 生成环境贴图
const envMap = pmremGenerator.fromScene(envScene).texture;
scene.environment = envMap;
纹理优化
纹理尺寸
纹理尺寸应该是 2 的幂次方(如 512, 1024, 2048),这样 GPU 可以更高效地处理:
// 推荐的尺寸
512 x 512
1024 x 1024
2048 x 2048
4096 x 4096
// 非二次幂尺寸会被自动调整或可能导致问题
// 虽然现代 GPU 支持非二次幂纹理,但性能会受影响
纹理压缩
使用压缩纹理格式可以减少内存占用:
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader(manager);
ktx2Loader.setTranscoderPath('jsm/libs/basis/');
ktx2Loader.detectSupport(renderer);
ktx2Loader.load('texture.ktx2', (texture) => {
material.map = texture;
});
纹理释放
当不再需要纹理时,应该释放其占用的 GPU 内存:
// 释放纹理
texture.dispose();
// 释放材质(会同时释放相关纹理)
material.dispose();
// 如果手动创建了图片元素
image.src = '';
完整示例
下面是一个展示多种纹理用法的完整示例:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 场景设置
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(3, 3, 3);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 创建 Canvas 纹理(棋盘格)
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
const tileSize = 32;
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
ctx.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#333333';
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
}
}
const checkerTexture = new THREE.CanvasTexture(canvas);
checkerTexture.wrapS = THREE.RepeatWrapping;
checkerTexture.wrapT = THREE.RepeatWrapping;
checkerTexture.repeat.set(2, 2);
// 创建 Data 纹理(噪声)
const size = 128;
const data = new Uint8Array(size * size * 4);
for (let i = 0; i < size * size; i++) {
const stride = i * 4;
const noise = Math.random();
data[stride] = noise * 255;
data[stride + 1] = noise * 200;
data[stride + 2] = noise * 150;
data[stride + 3] = 255;
}
const noiseTexture = new THREE.DataTexture(data, size, size);
noiseTexture.needsUpdate = true;
// 创建物体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 使用棋盘格纹理的立方体
const material1 = new THREE.MeshStandardMaterial({
map: checkerTexture,
roughness: 0.8,
metalness: 0.2
});
const cube1 = new THREE.Mesh(geometry, material1);
cube1.position.x = -1.5;
scene.add(cube1);
// 使用噪声纹理的立方体
const material2 = new THREE.MeshStandardMaterial({
map: noiseTexture,
roughness: 0.5,
metalness: 0.5
});
const cube2 = new THREE.Mesh(geometry, material2);
cube2.position.x = 1.5;
scene.add(cube2);
// 地面
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshStandardMaterial({
map: checkerTexture,
roughness: 0.8
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -1;
ground.receiveShadow = true;
scene.add(ground);
// 光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
scene.add(directionalLight);
// 动画
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
cube1.rotation.y = elapsed * 0.5;
cube2.rotation.y = -elapsed * 0.5;
controls.update();
renderer.render(scene, camera);
}
animate();
// 响应窗口变化
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
小结
纹理是 3D 图形中不可或缺的元素:
- UV 坐标定义了纹理如何映射到几何体表面
- TextureLoader 用于加载图片纹理,支持多种格式
- 纹理属性如包裹模式、过滤方式、颜色空间需要正确配置
- 多种纹理类型适用于不同场景:CanvasTexture、VideoTexture、DataTexture 等
- 材质贴图如法线贴图、粗糙度贴图可以增强表面细节
- 环境贴图实现反射和折射效果
合理使用纹理可以大幅提升 3D 场景的视觉效果,同时需要注意纹理尺寸和内存管理以保证性能。