Day 107 · 材质与贴图 — 从"纯色"到"真实质感"
Day 101 你学会了材质类型,但材质只有
color——整个世界像"塑料玩具"。今天的主题是让材质有真实质感:贴图(Texture)把一张图片"贴"到几何表面,UV 决定贴图怎么铺,环境贴图(Environment)让金属真正反光——顺便彻底解决 Day 101 遗留的"金属发黑"。做完,你的罐体会从"蓝色塑料柱"变成"车间里的不锈钢罐体"。
目录
一、纹理是什么:材质从"纯色"到"贴图"
1.1 材质参数的本质
Day 101 的材质只用了 color 一个参数。其实材质每个"属性"都可以用贴图替代——贴图就是"逐像素的属性值":
color: 0x00c6ff → map(颜色贴图):每个像素的颜色
metalness: 0.7 → metalnessMap:每个像素的金属度
roughness: 0.4 → roughnessMap:每个像素的粗糙度
一张贴图 = 一张图片 = 一个"二维属性表"。材质从"一个常量"升级成"一张查找表"。
1.2 加载纹理的最简方式
import * as THREE from "three";
// 用 TextureLoader 加载贴图(异步加载,用回调/await 拿结果)
const loader = new THREE.TextureLoader();
const texture = loader.load("/assets/textures/metal-panel.jpg");
// 贴到材质上
const material = new THREE.MeshStandardMaterial({
map: texture, // 颜色贴图
metalness: 0.7,
roughness: 0.4,
});
注意:贴图加载是异步的(Day 111 专门讲加载管理)。上面代码在资源加载完成前,物体会先以默认值渲染——这就是"为什么一开始没贴图,过一会突然有了"的原因。
二、UV 坐标:贴图怎么"贴"上去
2.1 UV 是"几何表面 ↔ 图片像素"的映射
Day 106 提到 geometry 有 uv attribute——它就是每个顶点在图片上的位置:
图片:u 轴(0~1 从左到右)、v 轴(0~1 从下到上)
几何:每个顶点的 uv 告诉 GPU "这个顶点对应图片的哪个像素"
中间的像素由 GPU 插值计算(自动平滑过渡)
图片(1×1 归一化) 立方体的一个面
┌─────────┐ (1,1) ┌─────────┐
│ │ │ │
│ 贴图 │ → │ 贴到面上│
│ │ │ │
(0,0)└─────────┘ └─────────┘
2.2 为什么你会"遇到"UV
内置几何体自带 UV:
BoxGeometry/CylinderGeometry都已算好 uv——你用map直接就有正确贴图自定义几何要手设 UV:Day 106 手写的三角形没有 uv,贴图会"错乱/没贴图"
模型自带 UV:Day 108 的 GLTF 模型在建模软件里就烘焙好了 UV
// 自定义几何补 UV 的姿势(Day 106 三角形为例)
geometry.setAttribute("uv", new THREE.BufferAttribute(
new Float32Array([0, 0, 1, 0, 0, 1]), // 三个顶点的 (u,v)
2
));
记忆钩子:UV 是"缝纫图样"——把平面图"裁"到立体表面。建模软件负责裁(Day 108 模型自带),手写几何时你自己裁。
三、贴图类型:不只是颜色
工业材质常用的贴图组合(PBR 的标准五件套),按"信息含量"分:
// 工业设备标准组合(罐体金属 + 底座涂装)
const material = new THREE.MeshStandardMaterial({
map: tex.colorTex, // 颜色贴图
normalMap: tex.normalTex, // 凹凸细节
roughnessMap: tex.roughTex, // 分区粗糙度
metalnessMap: tex.metalTex, // 分区金属度
roughness: 0.5, // 贴图没覆盖时的默认值
metalness: 0.5,
});
对大多数场景:
map+normalMap就能显著提升质感;roughnessMap/metalnessMap是"细化",可以逐步加。别一次上五件套把性能拖垮(每张贴图都要传 GPU 采样)。
四、色彩空间:为什么纹理"发灰"
4.1 一个高频坑:贴图颜色偏灰/偏暗
Three.js 从 r152 起对色彩空间很严格:纹理默认按 sRGB 解释,需显式标注,否则颜色贴图会"发灰":
import * as THREE from "three";
const loader = new THREE.TextureLoader();
const texture = loader.load("/assets/textures/metal-panel.jpg");
// ✅ 关键一行:颜色贴图要标注 sRGB 色彩空间(否则发灰/发暗)
texture.colorSpace = THREE.SRGBColorSpace;
记忆钩子:颜色贴图标 sRGB,数据贴图(法线/粗糙/金属)标默认(线性)。法线贴图千万别标 sRGB——会出"假凹凸完全错乱"的诡异效果。
4.2 其他常用纹理设置
texture.wrapS = THREE.RepeatWrapping; // 水平重复(大墙面铺砖)
texture.wrapT = THREE.RepeatWrapping; // 垂直重复
texture.repeat.set(2, 2); // 重复 2×2 次(缩小贴图面积)
texture.anisotropy = 8; // 各向异性过滤:斜视角更清晰(性能换清晰度)
五、环境贴图:金属不发黑的关键
5.1 问题回顾(Day 101 坑 5)
metalness=1 的物体发黑——因为金属几乎不吸收光、只反射环境。场景里只有点光源,没有"环境"可反射,金属就像照镜子但镜子里是黑的。
5.2 解法:给场景一个 Environment
import * as THREE from "three";
import { RoomEnvironment } from "three/examples/jsm/environments/RoomEnvironment.js";
// 生成一个"室内环境光场"(自带 HDR 氛围,无需下载贴图)
const pmrem = new THREE.PMREMGenerator(renderer); // PMREM:把环境转成 GPU 可用的光场
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
scene.environmentIntensity = 1.0; // 环境光强度(调大补光,调小更暗)
// 或者用外部 HDR 环境贴图(更真实,Week 17 深入):
// const rgbe = new RGBELoader();
// scene.environment = (await rgbe.loadAsync("/assets/hdr/industrial.hdr")).texture;
5.3 效果与原理
加了 environment 之后:
金属 → 反射周围环境(发亮、有立体反光)——"金属发黑"消失
塑料 → 环境光兜底补光,暗面不再死黑
原理:environment = 一个"无限远的光源 + 反射贴图",
让材质有了"周围有什么可反射"的信息
这一步是本周"质感"的分水岭:加一行
scene.environment,整个场景从"打光的塑料玩具"变成"有环境的真实空间"。Day 112 验收的"对比截图"就是它。
六、实战:有质感的罐体
把今天内容合成一个有真实质感的罐体(Day 106 实例化的材质升级版):
// src/core/three/create-tank-material.ts
import * as THREE from "three";
/**
* 创建罐体材质:贴图 + 环境光场
* 资源约定:贴图放 /public/assets/textures,HDR 放 /public/assets/hdr
*/
export async function createTankMaterial(
renderer: THREE.WebGLRenderer
): Promise<{ material: THREE.MeshStandardMaterial; dispose(): void }> {
// —— 环境光场(解决金属发黑,一次生成全局复用)——
const pmrem = new THREE.PMREMGenerator(renderer);
const envMap = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
// —— 贴图加载(异步)——
const loader = new THREE.TextureLoader();
const [map, normalMap] = await Promise.all([
loader.loadAsync("/assets/textures/metal-panel.jpg"),
loader.loadAsync("/assets/textures/metal-panel-normal.jpg"),
]);
// 颜色贴图标 sRGB(坑 1);法线贴图保持默认
map.colorSpace = THREE.SRGBColorSpace;
const material = new THREE.MeshStandardMaterial({
map,
normalMap,
metalness: 0.8,
roughness: 0.35,
envMap, // 材质级环境(或 scene.environment 全局)
envMapIntensity: 1.0,
});
return {
material,
dispose() {
map.dispose(); normalMap.dispose(); envMap.dispose(); material.dispose();
},
};
}
注意返回了
dispose()——纹理、环境、材质都是 GPU 资源,加载了就必须成对释放(Day 69/100 成对纪律的资产版,Day 111 会系统化)。
七、对照表第 5 行:纹理
结论:纹理的手写成本集中在"纹理单元管理 + 采样参数 + UV 绑定"——Three.js 一行 map: texture。你只需记住"标 sRGB""环境贴图救金属"两个关键决策。
八、常见坑点
坑 1:贴图发灰/发暗
原因:颜色贴图没标 sRGB。修法:texture.colorSpace = THREE.SRGBColorSpace(第四节)。
坑 2:法线贴图也标了 sRGB
症状:表面凹凸错乱、光怪陆离。修法:法线/粗糙度/金属度贴图保持默认线性空间。
坑 3:金属全黑
原因:没环境贴图,金属无处反射。修法:scene.environment 或材质 envMap(第五节)。
坑 4:贴图没出来(一开始是纯色)
原因:纹理异步加载,渲染先跑了。修法:加载完成后再创建材质/渲染,或先给默认色再替换(Day 111 的加载态)。
坑 5:大贴图卡顿/显存爆
原因:单张贴图 4096×4096 甚至更大,还加载多张。修法:贴图尺寸按需(256~1024 够用);anisotropy 适度;加载后释放 loader 缓存。
九、自测挑战
T1 · 贴图罐体(60 分钟)
完成第六节:罐体贴上 metal-panel 颜色贴图 + 法线贴图(无素材可用代码生成简单渐变/噪声图)。对比 Day 101 的纯色罐体截图——差异就是"质感"。
T2 · 环境贴图实验(40 分钟)
给场景加 scene.environment(RoomEnvironment),做三张对比:无环境金属 / 有环境金属 / 环境 + 贴图。亲眼看"金属发黑"怎么被救活。
T3 · 色彩空间实验(20 分钟)
同一张贴图,分别"标 sRGB"和"不标",截图对比发灰程度。把这个坑钉死在笔记里(面试高频追问)。
T4 · 贴图性能(20 分钟)
用 Performance 面板观察加贴图后的内存/帧率,记录 256 / 1024 / 4096 三种贴图尺寸的差异——为 Day 112 定"贴图尺寸规范"。
十、总结
罐体有质感了。明天换掉"拼装几何"——导入真实设备模型,让车间真的"像工厂"。
明日预告:Day 108 模型导入——GLTF/GLB 格式是什么、GLTFLoader 怎么用、模型里藏着哪些坑(材质丢失/单位/方向),以及第一个真实设备模型进车间。