Skip to content
概述
Mesh 由几何体(BufferGeometry)和材质(Material)两部分组成。几何体保存顶点数据,材质决定物体外观。通过纹理(Texture)可以把图像贴到几何体表面。本篇逐一说明这三者的工作机制与基本配置。
几何体数据:BufferGeometry 与 BufferAttribute
BufferGeometry 本身不直接存放数组,而是将类型化数组包装成 BufferAttribute 后挂载到 geometry.attributes 上。每个顶点可以携带多种属性,常用的有三个:
- position — 顶点在局部空间中的坐标,类型
vec3。 - normal — 顶点法线方向,类型
vec3,影响光照计算。 - uv — 纹理坐标,类型
vec2,决定该顶点在纹理图像中对应哪个位置。
以下为手动构造一个三角形几何体并同时设置 position、normal 和 uv 属性的示例:
javascript
const geometry = new THREE.BufferGeometry();
// 三个顶点 (x, y, z)
const positions = new Float32Array([
0, 0, 0,
2, 0, 0,
0, 2, 0
]);
// 法线 (nx, ny, nz),这里所有顶点指向同一方向
const normals = new Float32Array([
0, 0, 1,
0, 0, 1,
0, 0, 1
]);
// UV 坐标 (u, v)
const uvs = new Float32Array([
0, 0,
1, 0,
0, 1
]);
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2));BufferAttribute 构造函数的第二个参数表示每个顶点从缓冲区中取出几个连续数值作为一个属性值:position 和 normal 各占 3 个,uv 占 2 个。Three.js 内部依靠这些数字将缓冲区切分为逐顶点的数据。
内置几何体(如 BoxGeometry、SphereGeometry)在创建时已自动填充这些属性,通常不需要手动构建。如需修改某些顶点数据,可直接访问 geometry.attributes.position 等 BufferAttribute,再操作其底层的 Float32Array。
材质:MeshBasicMaterial 与 MeshStandardMaterial
材质定义 Mesh 对光照的响应方式。两种基础材质的差异如下:
MeshBasicMaterial— 不参与光照计算,直接将设置的颜色或纹理绘制出来。即使场景中没有光源,物体也可见。MeshStandardMaterial— 基于物理的渲染(PBR),依赖场景光源。无光时物体呈黑色;加入环境光、方向光等之后会显示出金属度、粗糙度等效果。
下面的示例用两个立方体对比两种材质:
javascript
const basicMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const standardMat = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube1 = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), basicMat);
const cube2 = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), standardMat);
cube2.position.x = 2;
scene.add(cube1);
scene.add(cube2);若未添加任何光源,cube1 显示红色,cube2 为全黑。加入环境光和方向光后,cube2 会呈现明暗面:
javascript
const ambient = new THREE.AmbientLight(0xffffff, 0.5);
const directional = new THREE.DirectionalLight(0xffffff, 0.5);
directional.position.set(1, 1, 1);
scene.add(ambient);
scene.add(directional);实际项目中若发现带 MeshStandardMaterial 的物体始终全黑,首先应检查是否缺少光源。
纹理加载与基本配置
外部图像通过 TextureLoader 加载,得到 Texture 对象后赋值给材质的 map 属性:
javascript
const loader = new THREE.TextureLoader();
const texture = loader.load('image.jpg');
const material = new THREE.MeshBasicMaterial({ map: texture });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);load 是异步的。首次渲染时纹理可能尚未加载完成,此时物体表面显示材质的颜色(默认为白色),图片到位后自动更新。如果需要在纹理就绪后执行某些操作,可使用回调:
javascript
loader.load(
'image.jpg',
(tex) => {
material.map = tex;
material.needsUpdate = true;
},
undefined,
(err) => console.error('纹理加载失败', err)
);下面介绍纹理对象上影响采样行为和边缘处理的两个常用配置。
过滤模式
纹理缩放时,GPU 需要决定如何取像素。minFilter 控制缩小(纹理分辨率高于屏幕像素),magFilter 控制放大(纹理分辨率低于屏幕像素):
THREE.NearestFilter— 最近邻采样,放大后呈现马赛克效果。THREE.LinearFilter— 双线性插值,放大后模糊但平滑。
为避免默认的 Mipmap 选项带来的不确定行为,可根据需求手动指定:
javascript
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.NearestFilter; // 获得像素风格效果
texture.needsUpdate = true;寻址模式(wrapS / wrapT)
当 UV 坐标超出 [0, 1] 时,wrapS(水平方向)和 wrapT(垂直方向)决定处理方式:
THREE.ClampToEdgeWrapping— 拉伸边缘像素(默认)。THREE.RepeatWrapping— 重复纹理。THREE.MirroredRepeatWrapping— 镜像重复。
使用重复模式时需要同时配置 wrapS、wrapT 以及 texture.repeat:
javascript
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(2, 2); // 水平和垂直方向各重复 2 次上述设置会让纹理在物体表面重复平铺,这与直接修改几何体 UV 不同,但表面效果相似。
UV 坐标与纹理映射
纹理图片是二维像素数组,通过 UV 坐标将其映射到三维表面。每个顶点携带一对标准化值 (u, v),范围 0 到 1。(0, 0) 对应图片左下角,(1, 1) 对应右上角。GPU 在三角形内部对 UV 做线性插值,然后用插值结果从纹理中采样颜色。
内置几何体已生成合适的 UV。例如 BoxGeometry 每个面的四个顶点 UV 恰好为 (0,0)、(0,1)、(1,0)、(1,1),因此图像会被完整映射到一个面上。
修改 UV 可以移动或缩放纹理在面上的位置。例如将某个面的 U 坐标整体偏移 0.5:
javascript
const uvs = geometry.attributes.uv;
for (let i = 0; i < uvs.count; i++) {
const u = uvs.getX(i);
uvs.setX(i, u + 0.5);
}
uvs.needsUpdate = true;如果配合 RepeatWrapping 并设置 repeat,采样器会自动处理 UV 超界,此时无需修改几何体 UV。想要纹理在表面多次平铺时,改 repeat 更简便;若只需要对局部区域进行偏移或缩放,直接调整 UV 更直接。wrapS / wrapT 的效果仅在 UV 实际超出 [0,1] 或 repeat 导致等效 UV 超出时才显现。
综合示例:带纹理的立方体
以下示例依赖前文已创建的 scene、camera 和 renderer,仅展示新增代码。立方体会使用 MeshStandardMaterial,因此需额外添加光源。
javascript
const loader = new THREE.TextureLoader();
const texture = loader.load('crate.jpg');
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ map: texture });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 光源
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambient);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.4);
dirLight.position.set(2, 3, 4);
scene.add(dirLight);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.02;
renderer.render(scene, camera);
}
animate();由于纹理是异步加载的,场景初始渲染时可能会短暂显示材质的基础颜色(白色)。若想完全避免此状态,可将 Mesh 的创建放在 onLoad 回调内,待纹理就绪后再加入场景。这与后文“注意点”中关于加载时序的说明一致。
若要在立方体的六个面贴不同的图片,可使用材质数组。BoxGeometry 内部预设了 6 个渲染组,分别对应各个面:
javascript
const textures = [
loader.load('posx.jpg'),
loader.load('negx.jpg'),
loader.load('posy.jpg'),
loader.load('negy.jpg'),
loader.load('posz.jpg'),
loader.load('negz.jpg')
];
const materials = textures.map(tex => new THREE.MeshStandardMaterial({ map: tex }));
const cube = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), materials);第 i 个组会使用数组中第 i 个材质。如果数组长度不足,缺少材质的组将不渲染。
注意点
- 加载时序:
load为异步操作,第一次渲染时可能尚无纹理。可将Mesh的创建放在onLoad回调中或预先设置占位颜色。 - 跨域:加载外部图片时
TextureLoader会自动为Image设置crossOrigin,但某些 CDN 可能因 CORS 策略拒绝请求。此时需要改用同源图片或配置服务端 CORS 头。 - 内存释放:不再需要的纹理(如换贴图或移除对象时)应调用
texture.dispose()释放 GPU 显存。 - 材质数组长度:必须与几何体的 group 数量一致。
BoxGeometry为 6,CylinderGeometry为 3(底、顶、侧面),ConeGeometry为 2。若传入数组过短,缺失材质的组不会渲染,且不会抛出明显错误。 - StandardMaterial 与光源:忘记添加光源是物体全黑的最常见原因。
本篇覆盖了几何体数据结构、基础材质和纹理的使用方法。后续章节将介绍光照与阴影的配置,进一步发挥 MeshStandardMaterial 的 PBR 特性。
参考链接
- [1] https://developer.mozilla.org/zh-CN/docs/Games/Techniques/3D_on_the_web/Building_up_a_basic_demo_with_Three.js?language=ru
- [3] https://github.com/puxiao/threejs-tutorial/blob/main/10%20Three.js%E5%9F%BA%E7%A1%80%E4%B9%8B%E7%BA%B9%E7%90%86.md
- [6] https://docs.omniverse.nvidia.com/simready/latest/simready-asset-creation/uv-best-practices.html
