Three.js 内置材质定制:ShaderChunk、注入机制与实战
本系列共 48 章、192 道不附答案的练习。围绕 MeshPhongMaterial、MeshStandardMaterial、MeshPhysicalMaterial,讲解怎样在保留内置光照、阴影和颜色管理的前提下,定制顶点变形、材质参数和纹理采样。01–34 章讲手动替换,新增的 35–48 章讲 three-custom-shader-material(CSM)的具体使用与实际替换逻辑。
核心方法是:先确定想修改的数据与计算阶段,再寻找锚点;在正确的作用域和坐标空间中修改它,并管理缓存、状态和配套渲染通道。 replace 只负责字符串操作,不负责保证这些语义正确。
版本、范围与前置知识
- 以本仓库安装的 Three.js 0.185.1 / r185 源码为准,面向 WebGLRenderer / WebGL2。onBeforeCompile 字符串注入不能直接照搬到 WebGPURenderer 的节点材质体系。
- CSM 章节以项目安装的 three-custom-shader-material 6.4.0 为准,使用
/vanilla入口。根据发布代码核对输出类型、默认值、替换顺序和生命周期,不把 README 中的简化表述当作所有版本的保证。 - ShaderChunk 是内部实现模块。升级 Three.js 后,要检查锚点、宏条件、变量和阴影路径,不能只验证 JavaScript 没有报错。
- 默认使用内置模板兼容的 GLSL 写法;不要自行重复添加 renderer 已提供的
#version、矩阵、attribute 等前缀。 - 多数案例以未蒙皮、未实例化的普通 Mesh 为基础。支持范围和替代方案在各章明确说明,不因普通球体成功渲染就承诺支持所有几何类型。
- 数学和空间基础可参阅 Three.js 日常开发中的向量与矩阵,尤其是坐标转换、法线矩阵、Shader 数据流章节。
- 每章四道练习,暂不提供答案。完整案例用于演示正文知识,不替代练习中的扩展要求。
推荐阅读路线
初次使用:01–05 理解机制与替换方式 → 13–17 掌握状态、类型和缓存 → 19–21 选择目标材质 → 26–31 运行案例。遇到具体锚点,再查 06–12。
几何变形:07、08 → 22、23 → 24、25 → 27 或 31。不要只复制 begin_vertex 的位置修改而跳过法线、阴影和 CPU 边界。
准备复用:18 → 32、33 → 34。把效果封装为拥有清晰状态和资源边界的模块,再用于复杂项目。
使用 CSM:35–36 入门 → 按需求阅读 37–44 的变量专题 → 45–47 管理状态、扩展补丁与阴影 → 48 检查项目中的真实迁移案例。CSM 自动连接常用输出,但不会自动推导法线、修复 CPU 拾取或同步自定义阴影材质。
完整目录与练习导航
理解内置材质与替换机制
核心 ShaderChunk 逐项讲解
Uniform、类型与程序生命周期
- 自定义 Uniform 的声明、注入与更新
- Uniform 类型、数组与纹理
- 循环次数、defines 与编译期常量
- 缓存、重编译与 customProgramCacheKey
- 克隆、共享与多对象状态
- 补丁组合与升级维护
三种内置材质专题
几何变形、法线与阴影一致性
完整效果案例
调试与工程落地
CSM:原理、输出变量与实际案例
- CSM 与手动替换:同一条渲染管线,两种控制方式
- 从 vanilla 导入到第一个 CSM 材质
- csm_Position:修改局部顶点位置
- csm_PositionRaw:接管裁剪坐标
- csm_Normal 与 csm_FragNormal:几何法线和片元法线
- csm_DiffuseColor:基础色、贴图与顶点颜色
- csm_FragColor、csm_UnlitFac 与 csm_Emissive
- csm_Roughness、csm_Metalness 与 csm_AO
- Physical 专用变量:清漆、透射、厚度与虹彩
- csm_PointSize:粒子尺寸与透视衰减
- CSM 的 Uniform、update、缓存与克隆
- patchMap、自定义锚点与组合材质
- CSM 阴影同步与 csm_DepthAlpha 的版本边界
- 从现有项目迁移:逐项检查一个真实 CSM 示例
每章末尾的四道练习覆盖理解、编码、预测、排错或扩展,可直接从上述章节进入。
CSM 输出变量 → 实际替换逻辑
下表覆盖本地 6.4.0 定义的 19 个输出变量。它们是 CSM 提供的 GLSL 可写变量,不是需要添加到 JavaScript uniforms 中的字段。具体支持材质、默认值和特殊情况请进入对应章节。
| 变量 | 类型 | 实际接入位置与主要影响 | 章节 |
|---|---|---|---|
| csm_Position | vec3 | begin_vertex 中初始化 transformed;后续仍处理常规变换 | 37 |
| csm_PositionRaw | vec4 | project_vertex 执行后覆盖 gl_Position;其他几何派生数据不会自动跟着重算 | 38 |
| csm_Normal | vec3 | beginnormal_vertex 的 objectNormal;再走后续法线变换 | 39 |
| csm_FragNormal | vec3 | normal_fragment_maps 后覆盖 normal;需要正确的观察空间单位方向 | 39 |
| csm_DiffuseColor | vec4 | color_fragment 后整体写入 diffuseColor;默认值与后续覆盖会影响 map、顶点色组合 | 40 |
| csm_FragColor | vec4 | opaque_fragment 后参与混合,后续仍有输出转换等处理 | 41 |
| csm_UnlitFac | float | 控制内置输出与 csm_FragColor 的混合;单独使用不自动启用混合补丁 | 41 |
| csm_Emissive | vec3 | totalEmissiveRadiance 初始化时替换 emissive;emissiveMap 后续仍可调制 | 41 |
| csm_Roughness | float | roughnessmap_fragment 后覆盖 roughnessFactor;不会自动保留粗糙度贴图结果 | 42 |
| csm_Metalness | float | metalnessmap_fragment 后覆盖 metalnessFactor | 42 |
| csm_AO | float | aomap_fragment 后给间接漫反射额外乘 1 - csm_AO;0 表示不增加遮蔽 |
42 |
| csm_Clearcoat | float | 物理材料结构初始化的清漆基础值,随后仍乘清漆贴图 | 43 |
| csm_ClearcoatRoughness | float | 清漆粗糙度基础值,后续仍有贴图及粗糙度修正 | 43 |
| csm_ClearcoatNormal | vec3 | 自定义投影扰动算法,不是简单赋值;无有效默认初始化,不宜按标准法线接口理解 | 43 |
| csm_Transmission | float | transmission_fragment 内透射基础值;会触发对应特性启用逻辑 | 43 |
| csm_Thickness | float | 介质厚度基础值;单独使用不自动开启 transmission | 43 |
| csm_Iridescence | float | 虹彩基础值;本系列使用 Physical 路径 | 43 |
| csm_PointSize | float | Points 的 size 赋值处;后续仍可能按距离衰减 | 44 |
| csm_DepthAlpha | float | 6.4.0 的深度字符串补丁与 r185 存在具体问题;不要当成通用阴影透明开关 | 47 |
手动替换与 CSM 不能只按变量名称机械翻译。例如“保留贴图后乘色”与“用 csm_DiffuseColor 覆盖基础色”可能不同;csm_PositionRaw 也不等于更新整个场景的世界位置。各变量章节都按输入、默认值、替换点、下游行为和实际案例展开。
效果 → 替换位置
| 需求 | 优先调查的位置 | 主要注意点 |
|---|---|---|
| 染色且保留光照 | map_fragment / color_fragment 之后 | 修改 diffuseColor.rgb;区分乘色与覆盖 |
| 增加不依赖普通入射光的亮带 | emissivemap_fragment 之后 | 修改 totalEmissiveRadiance;不自动照亮其他物体或产生 bloom |
| 替换颜色纹理采样 | map_fragment | 保留或接管原采样、alpha 和特殊颜色解码分支 |
| 改粗糙度与金属度 | roughnessmap_fragment / metalnessmap_fragment 之后 | 在 lights_physical_fragment 消费前修改 factor |
| Phong 局部高光 | lights_phong_fragment 之后 | 改局部 material 结构体,不给只读 uniform 赋值 |
| 调整最终表面法线 | normal_fragment_maps 之后 | 法线空间、归一化、双面及 clearcoat 法线 |
| 顶点波浪、弯曲 | begin_vertex 至 project_vertex 之间 | 先决定 morph、蒙皮、位移贴图的相对顺序 |
| 法线随变形更新 | beginnormal_vertex 附近或专门法线路径 | 普通模板法线阶段早于 begin_vertex |
| 阈值溶解 | 主材质与深度/距离材质的裁切阶段 | 相同坐标、公式、阈值和遮罩 |
| 对光照汇总结果调色 | opaque_fragment 之前 | 保留色调映射、输出颜色空间等后续步骤 |
| Physical 厚度、透射等 | 对应参数实际被消费的位置 | 有些逻辑在 Chunk 内部或主模板内,没有通用同名锚点 |
ShaderChunk → 变量与阶段
下面列出日常最常查的变量。完整核心模块说明见 06–12,每个模块的前提和相邻阶段都在正文展开。
| 阶段 / 模块 | 典型变量 | 说明 |
|---|---|---|
common 与 *_pars_* |
函数、结构体、uniform、varying 声明 | 通常在 main 外,不能随意放执行语句 |
| uv_vertex | vMapUv 等条件 varying | 与具体贴图宏和 UV 通道有关;自定义采样可用独立 varying |
| begin_vertex | transformed | 局部可变位置的起点,原始 position 不可当作可写变量 |
| beginnormal_vertex | objectNormal,条件 objectTangent | 位置和法线必须按同一几何变形解释 |
| defaultnormal_vertex / normal_vertex | transformedNormal / vNormal | 包含模型到观察空间法线处理及相关几何特性 |
| project_vertex | mvPosition、gl_Position | 投影之后再改 transformed 不会自动重算投影 |
| worldpos_vertex | worldPosition | 受宏条件约束,不是任何配置下都存在 |
| map_fragment / color_fragment | diffuseColor | 基础颜色与 alpha,并非最终屏幕 RGB |
| roughnessmap_fragment | roughnessFactor | Standard/Physical 的局部参数 |
| metalnessmap_fragment | metalnessFactor | Standard/Physical 的局部参数 |
| normal_fragment_begin/maps | normal、geometryNormal 等 | 几何法线与扰动法线有不同职责 |
| lights_phong_fragment / lights_physical_fragment | material | GLSL 局部材料结构体,不是 JS Material 对象 |
| lights_fragment_begin/maps/end | reflectedLight 等 | 光照累积与间接照明,不同模型有不同附加量 |
| emissivemap_fragment | totalEmissiveRadiance | 加入最终辐射的自发光项 |
| opaque_fragment 之前 | outgoingLight | 已汇总光照,尚需保留后续输出处理 |
变化 → 更新方式
| 变化 | 通常需要做什么 | 不应误用 |
|---|---|---|
| 时间、阈值、颜色等已有 uniform 值 | 更新稳定容器的 .value 或对象分量 |
每帧 needsUpdate 或每帧更换缓存键 |
将一个已有容器换成全新 { value } |
重新考虑所有绑定引用,优先避免这样做 | 以为旧 shader 自动追踪变量重新赋值 |
| 已声明 sampler 更换兼容纹理 | 更新其 value,配置正确的纹理数据和颜色空间 | 把 sampler2D 随意换成另一种 sampler 类型 |
| 改纹理像素或部分采样配置 | 按 Texture API 标记 needsUpdate 等 | 用 material.needsUpdate 代替纹理上传 |
| 改 GLSL 声明、数组容量、结构模式 | 保证缓存键区分结构,并设置 material.needsUpdate | 只改闭包值、保持相同键 |
| 首次编译后安装新补丁 | 安装回调与正确键,设置 needsUpdate | 等待普通 uniform 刷新来执行源码替换 |
| 克隆一套效果 | 通过工厂重建回调、独立状态和配套材质 | 只 clone 主 Material 就当作完整复制 |
| 顶点形状或裁切变化 | 同步阴影通道,检查 CPU 边界和拾取 | 以为主画面变化会自动修改其他通道 |
运行配套代码
六个完整入口在 examples 下,均使用程序化几何与纹理,不依赖远程模型或图片:
| 案例 | 入口文件 |
|---|---|
| 动态扫描 | color-scan.js |
| 水面波浪 | water-waves.js |
| 纹理混合 | texture-blending.js |
| 湿润表面 | wet-surfaces.js |
| 溶解 | dissolve.js |
| 风吹变形 | wind-deformation.js |
公共代码为 runtime.js、patch-utils.js、deformation.js;第 34 章另提供 create-dissolve-bundle.js。
新增八个 CSM 完整场景沿用相同 runtime,项目已安装 CSM,无需另装 React 或修改依赖:
| CSM 案例 | 入口文件 | 重点 |
|---|---|---|
| 波面位置与几何法线 | csm-wave.js | csm_Position + csm_Normal |
| 裁剪空间标记 / 广告牌 | csm-position-raw.js | csm_PositionRaw、屏幕尺寸和边界 |
| 粒子尺寸 | csm-points.js | csm_PointSize 与距离衰减 |
| 着色与未受光颜色对比 | csm-color-comparison.js | DiffuseColor、FragColor、UnlitFac |
| 材质纹理与 PBR 参数混合 | csm-pbr-blend.js | 颜色、粗糙度、金属度、AO |
| 湿润清漆层 | csm-physical-coat.js | Clearcoat 与 ClearcoatRoughness |
| 主表面与两类阴影共同溶解 | csm-shadow-dissolve.js | 用共享 discard 处理覆盖,不依赖 DepthAlpha |
| 项目环面结变形的改进版本 | csm-migration.js | 雅可比逆转置、时间、阴影、CPU 边界 |
第 36 章还提供完整入门代码。原有 pages/three-custom-shader/ 在第 48 章作为源码阅读案例,现有网页没有被这些文档示例替换。
在现有 Vite 练习入口旁复制 examples 的这些 JS 文件,选择一个案例作为 main.js 或由入口 import。一次只运行一个完整场景入口,避免重复创建 renderer 和渲染循环。第 03、34 章的完整代码也可以保存为 main.js,材质工厂章节则需要挂到已有 Mesh 上并提供灯光。
默认 runtime 使用 document.body 作为容器,需要确定尺寸,例如练习入口样式 html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; }。也可以 createDemo({ parent: 容器元素 }),由调用者设置容器高度。新建正式展示页时,仍需遵守仓库界面规范和入口登记要求。本系列本身不新增网页入口。
从项目根目录运行 yarn dev,打开练习页,不通过 file 协议直接加载模块。现有 Vite 构建能运行,不代表系统旧 Node 可以直接解析 Three.js r185 的全部模块语法;终端数值验证应使用兼容该依赖的 Node 版本。
公共运行外壳的契约
createDemo() 返回 THREE、scene、camera、renderer、onFrame、onDispose、dispose。默认空场景,不自动添加灯光和地面;相机为透视 60°,初始位置 (6,5,8) 看向原点,near=0.1、far=1000。
onFrame((dt, elapsed) => {}) 在渲染前执行,单位为秒,dt 上限为 0.05,elapsed 是截断后累计的模拟时间。onFrame 返回退订函数,onDispose 注册自定义清理。窗口和容器变化时会更新画布尺寸与投影,DPR 上限为 2。
dispose() 先收集场景资源,再运行注册的清理,最后释放常见几何体、主材质、自定义 Depth/Distance 材质、可发现纹理及灯光阴影资源,停止循环并移除画布。内置材质通过 onBeforeCompile 注入的自定义 uniform 纹理不一定能从 material 属性中发现,示例会显式注册纹理释放。
这个外壳适用于资源由独立场景拥有的教学示例。外部共享纹理、已移出场景的对象、额外 render target 和复杂缓存,应由所属模块明确管理。不要同时让两个资源管理器无条件 dispose 同一资源。
验证记录
01–34 章的初次验证
已核对 34 章、136 道练习、本地链接、56 个 JavaScript 代码块及 10 个配套 JS 文件的语法。核心锚点与 r185 源码交叉审查,材质工厂在真实 ShaderLib 上进行注入、uniform 引用和缓存结构检查;水波与风变形的解析导数通过数值差分对照。
在本机 Edge 无头浏览器中,对六个场景、入门与模块示例、四个材质工厂及四个点光源阴影变体,分别以 1280×720、390×844 运行,共 32 组基本检查。未出现 JavaScript 或 GLSL 编译错误,画布尺寸和销毁检查通过。截图审查后修正了水面自阴影条纹,并复测受影响的方向光、点光源和两种视口组合。
项目 yarn build 通过,仍有现有空入口 chunk 与大 chunk 提示。Markdown 代码块并非 Vite 构建入口,所以构建与浏览器检查分别进行。这些检查不等于所有设备、交互、灯光模式和性能场景均已验证;各章练习中的扩展功能仍由学习者完成。
35–48 章的 CSM 扩展验证
基于实际安装的 Three.js r185 与 CSM 6.4.0,逐项核对输出变量的注入位置、执行顺序和版本限制。全系列静态检查覆盖 48 章、192 道练习、78 个 JavaScript 代码块、18 个配套 JS 文件和本地链接,未发现语法或链接问题。
在本机 Edge 无头浏览器中,八个 CSM 场景、第 36 章入门代码和迁移案例的点光源变体,分别以 1280×720、390×844 运行,共 20 组场景检查通过;另有 18 项输出变量着色器检查通过。检查包含程序编译、画布尺寸、适用场景的阴影资源和销毁行为,并对八个场景截图做了视觉审查。
另行执行的一项反例测试复现了 csm_DepthAlpha 配合 RGBADepthPacking 时的 dist: undeclared identifier 编译错误。这是第 47 章记录的版本兼容问题;配套溶解案例采用共享 discard,主表面、方向光阴影和点光源阴影均通过检查。上述结果不代表所有设备、材质组合或性能场景均已覆盖。
本次扩展后再次运行 yarn build,构建通过;仍有现有空入口 chunk 与超过 500 KiB 的 chunk 提示。文档示例的运行结果以以上独立浏览器检查为准。
核心源码与参考
本地源码在安装依赖后可访问:Material、WebGLRenderer、WebGLPrograms、WebGLProgram、WebGLUniforms、WebGLShadowMap。
材质模板:Phong、Standard/Physical、Depth、Distance。
在线参考:Material、MeshPhongMaterial、MeshStandardMaterial、MeshPhysicalMaterial。在线文档可能随版本变化,具体替换行为以本系列标注的 r185 为准。
正文中的内置 Shader 片段摘录来自 Three.js,遵循其 MIT 许可证。示例纹理由 Canvas 程序生成,不引入额外第三方图片资源。
CSM 参考:项目主页、本地 6.4.0 README、vanilla 类型定义、实际发布实现与 patchMap。CSM 由 Faraz Shaikh 等贡献者维护,使用 MIT 许可证。本教程以安装版本代码为依据;源码引用与兼容性结论不表示已修改第三方依赖。