【Three.js】day107-materials-textures

作者:mario 发布时间: 2026-09-07 阅读量:5 评论数:0

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

  • 内置几何体自带 UVBoxGeometry/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 的标准五件套),按"信息含量"分:

贴图

对应属性

内容

作用

map(颜色贴图)

color

表面颜色

主体观感:颜色/文字/铆钉

normalMap(法线贴图)

normal

表面凹凸细节(蓝紫色图)

用假凹凸骗过光照(很省面数)

roughnessMap

roughness

灰度(白=粗糙/黑=光滑)

锈蚀区粗糙、logo 区光滑

metalnessMap

metalness

灰度(白=金属/黑=非金属)

金属件 vs 塑料件分区

aoMap(环境光遮蔽)

ao

灰度(缝隙更暗)

增加接触阴影的真实感

// 工业设备标准组合(罐体金属 + 底座涂装)
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 行:纹理

环节

手写 WebGL

Three.js

渲染循环

rAF + 手动矩阵 + draw

renderer.render(scene, camera)

光照

手写着色器

new DirectionalLight(...)

几何体

手动顶点数组

new BoxGeometry(...)

几何数据

手动 attribute

构造函数内置

纹理

手动加载图片 → 绑定纹理单元 → 设置采样参数 → 着色器里 sample

TextureLoader.load() + material.map = tex

UV

手动算 UV 插值

内置几何自带 UV

结论:纹理的手写成本集中在"纹理单元管理 + 采样参数 + 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 定"贴图尺寸规范"


十、总结

环节

要点

纹理

材质属性都能用"贴图"替代——贴图 = 逐像素属性表

UV

几何顶点 ↔ 图片像素的映射;内置几何自带,手写几何自己补

贴图类型

map/normalMap/roughnessMap/metalnessMap/aoMap——PBR 五件套

色彩空间

颜色贴图标 sRGB,数据贴图保持线性

环境贴图

scene.environment 救金属发黑——本周质感的胜负手

对照表

第 5 行:纹理封装成本(纹理单元/采样/UV)

罐体有质感了。明天换掉"拼装几何"——导入真实设备模型,让车间真的"像工厂"。


明日预告:Day 108 模型导入——GLTF/GLB 格式是什么、GLTFLoader 怎么用、模型里藏着哪些坑(材质丢失/单位/方向),以及第一个真实设备模型进车间。

评论