纹理贴图是Three.js中让3D模型呈现真实外观的核心技术。简单来说,就是把2D图片"贴"到3D几何体表面,替代单一的纯色材质。想象一下给纸箱贴包装纸的过程 - 纹理贴图就是数字世界的"贴包装纸"技术。
为什么我们需要纹理贴图?主要有三大优势:
纹理贴图的实现流程可以分解为四个关键步骤:
javascript复制// 基础示例代码
const loader = new THREE.TextureLoader();
const texture = loader.load('texture.jpg');
const material = new THREE.MeshBasicMaterial({ map: texture });
const mesh = new THREE.Mesh(geometry, material);
TextureLoader是Three.js提供的专门用于加载纹理的工具类。它基于Web的Image API,但提供了更便捷的三维图形集成方式。
javascript复制const loader = new THREE.TextureLoader();
const texture = loader.load(
'path/to/texture.jpg',
onLoad,
onProgress,
onError
);
function onLoad(texture) {
console.log('纹理加载完成', texture);
}
function onProgress(xhr) {
console.log(`${(xhr.loaded / xhr.total) * 100}% 已加载`);
}
function onError(error) {
console.error('加载错误', error);
}
TextureLoader还支持一些配置选项,可以通过set方法设置:
javascript复制const loader = new THREE.TextureLoader();
loader.setCrossOrigin('anonymous'); // 处理跨域
loader.setPath('assets/textures/'); // 设置基础路径
从Three.js r152版本开始,必须显式设置纹理的颜色空间,否则会出现颜色偏差:
javascript复制texture.colorSpace = THREE.SRGBColorSpace; // 适用于大多数彩色纹理
texture.colorSpace = THREE.LinearSRGBColorSpace; // 适用于法线贴图等非彩色纹理
为什么需要设置颜色空间?
显示器显示的颜色是非线性的(gamma校正),而计算机图形计算通常在线性空间进行。SRGBColorSpace告诉Three.js这个纹理已经是gamma校正过的,需要进行反向转换才能在着色器中正确计算。
当从不同域加载纹理时,可能会遇到CORS限制。解决方法包括:
Access-Control-Allow-Origin头UV坐标是连接2D纹理和3D几何体的桥梁。它定义了纹理上的每个点如何映射到几何体表面。
不同的内置几何体有预设的UV映射方式:
| 几何体类型 | UV特点 | 适用场景 |
|---|---|---|
| BoxGeometry | 每个面独立UV,6个面都有完整映射 | 立方体、箱子 |
| SphereGeometry | 类似地球仪展开,两极有扭曲 | 星球、球体 |
| PlaneGeometry | 简单平面映射,无扭曲 | 地面、墙面 |
| CylinderGeometry | 侧面展开为矩形,顶部/底部为圆形 | 柱子、罐子 |
对于自定义的BufferGeometry,需要手动设置UV坐标:
javascript复制const geometry = new THREE.BufferGeometry();
// 设置顶点位置
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// 设置UV坐标
const uvs = new Float32Array([
0, 0, // 顶点0的UV
1, 0, // 顶点1的UV
1, 1, // 顶点2的UV
0, 1 // 顶点3的UV
]);
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));
控制纹理在UV坐标超出0-1范围时的表现:
javascript复制texture.wrapS = THREE.RepeatWrapping; // 水平重复
texture.wrapT = THREE.MirroredRepeatWrapping; // 垂直镜像重复
texture.repeat.set(2, 2); // 在两个方向各重复2次
可选的wrapping模式:
ClampToEdgeWrapping:默认值,边缘像素拉伸RepeatWrapping:平铺重复MirroredRepeatWrapping:镜像重复javascript复制texture.offset.set(0.5, 0); // 水平偏移50%
texture.rotation = Math.PI / 4; // 旋转45度
texture.center.set(0.5, 0.5); // 设置旋转中心
控制纹理在不同距离下的显示质量:
javascript复制texture.magFilter = THREE.LinearFilter; // 放大时使用线性过滤
texture.minFilter = THREE.LinearMipmapLinearFilter; // 缩小时使用mipmap
texture.generateMipmaps = true; // 启用mipmap生成
提升倾斜角度观看纹理时的清晰度:
javascript复制texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
通过修改offset实现流动效果:
javascript复制function animate() {
requestAnimationFrame(animate);
texture.offset.x += 0.01; // 水平流动
renderer.render(scene, camera);
}
PBR材质支持多纹理组合:
javascript复制const material = new THREE.MeshStandardMaterial({
map: albedoTexture, // 基础颜色
normalMap: normalTexture, // 法线贴图
roughnessMap: roughnessTexture, // 粗糙度
metalnessMap: metalnessTexture // 金属度
});
将视频作为动态纹理:
javascript复制const video = document.createElement('video');
video.src = 'video.mp4';
video.loop = true;
video.play();
const texture = new THREE.VideoTexture(video);
const material = new THREE.MeshBasicMaterial({ map: texture });
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 纹理不显示 | 路径错误/跨域限制 | 检查路径/配置CORS/使用本地服务器 |
| 颜色异常 | 未设置colorSpace | 添加texture.colorSpace = THREE.SRGBColorSpace |
| 边缘模糊 | 过滤设置不当 | 设置magFilter/minFilter |
| 性能低下 | 纹理尺寸过大 | 减小尺寸/启用压缩 |
让我们创建一个完整的示例,展示纹理的各种应用:
html复制<!DOCTYPE html>
<html>
<head>
<title>Three.js纹理综合示例</title>
<style>body { margin: 0; }</style>
</head>
<body>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
<script>
// 初始化场景
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 添加光源
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(1, 1, 1);
scene.add(light);
scene.add(new THREE.AmbientLight(0x404040));
// 加载纹理
const loader = new THREE.TextureLoader();
const texture = loader.load('https://threejs.org/examples/textures/brick_diffuse.jpg', () => {
texture.colorSpace = THREE.SRGBColorSpace;
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(2, 2);
// 创建立方体
const cubeGeometry = new THREE.BoxGeometry(2, 2, 2);
const cubeMaterial = new THREE.MeshStandardMaterial({ map: texture });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cube);
// 创建地面
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshStandardMaterial({
map: texture,
side: THREE.DoubleSide
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -1;
scene.add(ground);
});
// 设置相机
camera.position.z = 5;
// 添加控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
// 响应式调整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
这个示例展示了:
素材准备:
预处理:
Three.js集成:
测试优化:
纹理创建:
纹理处理:
性能分析:
掌握了基础纹理贴图后,可以进一步学习:
在实际项目中使用纹理贴图时,我总结了以下几点经验:
命名规范很重要:建立清晰的纹理命名规则(如wall_diffuse.jpg, wall_normal.jpg),这在大型项目中能节省大量时间。
测试不同设备:某些移动设备对纹理尺寸有限制,需要特别测试。我曾遇到iOS设备上2048x2048纹理显示异常的问题,最终发现需要降级到1024x1024。
注意内存管理:纹理是WebGL应用中最消耗内存的资源之一。记得在不再需要时调用texture.dispose()释放资源。
利用调试工具:Three.js的纹理调试工具(如显示UV、法线等)在开发中非常有用,可以快速定位问题。
渐进式加载:对于大纹理,可以考虑先加载低分辨率版本,再逐步替换为高清版本,提升用户体验。
文档习惯:记录每个纹理的原始来源和授权信息,避免后期版权问题。我曾因为忘记记录纹理来源而不得不重新制作一组纹理。