Three.js 内置材质定制:ShaderChunk、注入机制与实战

本系列共 48 章、192 道不附答案的练习。围绕 MeshPhongMaterial、MeshStandardMaterial、MeshPhysicalMaterial,讲解怎样在保留内置光照、阴影和颜色管理的前提下,定制顶点变形、材质参数和纹理采样。01–34 章讲手动替换,新增的 35–48 章讲 three-custom-shader-material(CSM)的具体使用与实际替换逻辑。

核心方法是:先确定想修改的数据与计算阶段,再寻找锚点;在正确的作用域和坐标空间中修改它,并管理缓存、状态和配套渲染通道。 replace 只负责字符串操作,不负责保证这些语义正确。

版本、范围与前置知识

推荐阅读路线

初次使用: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 拾取或同步自定义阴影材质。

完整目录与练习导航

理解内置材质与替换机制

  1. 为什么定制内置材质
  2. Three.js 如何组装着色器
  3. 第一次 onBeforeCompile 注入
  4. 如何查找可靠的替换锚点
  5. 字符串替换的四种方式

核心 ShaderChunk 逐项讲解

  1. 全局声明、UV 与顶点颜色
  2. 顶点位置与变形链路
  3. 顶点法线与片元法线
  4. 基础颜色、透明度与自发光
  5. 材质参数与光照计算
  6. 环境、阴影与间接光
  7. Physical 扩展与最终输出

Uniform、类型与程序生命周期

  1. 自定义 Uniform 的声明、注入与更新
  2. Uniform 类型、数组与纹理
  3. 循环次数、defines 与编译期常量
  4. 缓存、重编译与 customProgramCacheKey
  5. 克隆、共享与多对象状态
  6. 补丁组合与升级维护

三种内置材质专题

  1. MeshPhongMaterial 定制
  2. MeshStandardMaterial 定制
  3. MeshPhysicalMaterial 定制

几何变形、法线与阴影一致性

  1. 顶点变形的空间与执行顺序
  2. 变形之后如何修正法线
  3. 让阴影跟随自定义变形
  4. GPU 变形与 CPU 场景数据

完整效果案例

  1. 动态变色与设备状态扫描
  2. 水面波动
  3. 地形与特殊纹理混合
  4. 湿润表面与局部积水
  5. 溶解、裁切与发光边缘
  6. 风吹旗帜或植被

调试与工程落地

  1. 定位 GLSL 编译与视觉错误
  2. 性能与兼容性验证
  3. 构建可复用的定制材质模块

CSM:原理、输出变量与实际案例

  1. CSM 与手动替换:同一条渲染管线,两种控制方式
  2. 从 vanilla 导入到第一个 CSM 材质
  3. csm_Position:修改局部顶点位置
  4. csm_PositionRaw:接管裁剪坐标
  5. csm_Normal 与 csm_FragNormal:几何法线和片元法线
  6. csm_DiffuseColor:基础色、贴图与顶点颜色
  7. csm_FragColor、csm_UnlitFac 与 csm_Emissive
  8. csm_Roughness、csm_Metalness 与 csm_AO
  9. Physical 专用变量:清漆、透射、厚度与虹彩
  10. csm_PointSize:粒子尺寸与透视衰减
  11. CSM 的 Uniform、update、缓存与克隆
  12. patchMap、自定义锚点与组合材质
  13. CSM 阴影同步与 csm_DepthAlpha 的版本边界
  14. 从现有项目迁移:逐项检查一个真实 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 许可证。本教程以安装版本代码为依据;源码引用与兼容性结论不表示已修改第三方依赖。