36 · 从 vanilla 导入到第一个 CSM 材质

返回目录 · 上一章 · 下一章

本项目使用普通 Vite + Three.js 模块,不是 React Three Fiber。应导入 three-custom-shader-material/vanilla;包的根入口是 React 版本,不能为了复制 React 示例而给这个项目额外引入 React。

1. 构造参数与基础材质属性

import * as THREE from 'three';
import CustomShaderMaterial from 'three-custom-shader-material/vanilla';

const material = new CustomShaderMaterial({
  baseMaterial: THREE.MeshStandardMaterial,
  color: 0x427c96,
  roughness: 0.45,
  metalness: 0.1,
  uniforms: { uDemoStrength: { value: 0.5 } },
  fragmentShader: `
    uniform float uDemoStrength;
    void main() {
      csm_DiffuseColor.rgb *= 1.0 - 0.4 * uDemoStrength;
    }
  `
});
参数 作用
baseMaterial 内置材质构造函数或实例,决定保留的渲染模型
vertexShader / fragmentShader 自定义声明与 main,不是完整替换所有内置源码
uniforms 自定义 GLSL 参数的 { value } 容器表
patchMap 覆盖默认映射不能表达的位置或逻辑,详见第 46 章
cacheKey 补充自定义程序结构身份的函数,详见第 45 章
其他材质参数 传递给基础材质,例如 color、roughness、side

CSM 6.4.0 明确拒绝 ShaderMaterial 和 RawShaderMaterial 作为 baseMaterial。CSM 的目的就是扩展已有材质流程;完全自定义的 ShaderMaterial 应使用它本来的工作方式。

2. 优先传构造函数,谨慎传实例

传 THREE.MeshStandardMaterial 时,CSM 会创建基础实例并传入其他选项。传已有 material 实例时,本版本会在该实例上写入属性、回调和 CSM 元数据,而不是保证复制出一个互不影响的纯包装层。

const original = new THREE.MeshStandardMaterial({ color: 0x999999 });
// 如果 original 还被别的网格使用,先不要直接把它交给 CSM。
const customized = new CustomShaderMaterial({
  baseMaterial: original.clone(),
  fragmentShader: 'void main() { csm_DiffuseColor.rgb *= 0.5; }'
});

这里 clone 只解决普通原生材质实例的隔离;若原实例本来就是 CSM,clone 的 shader 配置和 uniform 共享问题需要按第 45 章处理。不要把“先 clone 一下”当作任何复杂效果的完整深复制方案。

材质仍保留基础类型标记,供 renderer 选择内置流程。不要用 instanceof MeshStandardMaterial 来代替检查 CSM 的明确配置契约;CSM 的继承和代理方式与直接 new 原生类不同。

3. 完整入门场景

将下面代码作为入口,与现有 examples/runtime.js 放在同一目录。它无需外部纹理,以静态球体演示 uniform 到 CSM 输出再到 Standard 光照的完整链路。

import CustomShaderMaterial from 'three-custom-shader-material/vanilla';
import { createDemo } from './runtime.js';

const { THREE, scene, onFrame, dispose } = createDemo();
const uniforms = {
  uDemoTime: { value: 0 },
  uDemoTint: { value: new THREE.Color(0xe48263) }
};
const material = new CustomShaderMaterial({
  baseMaterial: THREE.MeshStandardMaterial,
  color: 0x417c9b,
  roughness: 0.4,
  metalness: 0.1,
  uniforms,
  fragmentShader: `
    uniform float uDemoTime;
    uniform vec3 uDemoTint;
    void main() {
      float demoAmount = 0.5 + 0.5 * sin(uDemoTime);
      csm_DiffuseColor.rgb = mix(csm_DiffuseColor.rgb, uDemoTint, demoAmount);
    }
  `
});
scene.add(new THREE.Mesh(new THREE.SphereGeometry(1.5, 48, 32), material));
scene.add(new THREE.HemisphereLight(0xd8e6ff, 0x40342b, 1.2));
const light = new THREE.DirectionalLight(0xffffff, 3);
light.position.set(3, 5, 4);
scene.add(light);
onFrame((dt, elapsed) => {
  uniforms.uDemoTime.value = elapsed;
});
if (import.meta.hot) import.meta.hot.dispose(dispose);

CSM 在自定义 main 之前把 csm_DiffuseColor 初始化为基础 diffuse、opacity 和可能的 map 采样结果。本例没有 map,因此起始值来自基础材质颜色与不透明度。mix 改的是进入后续光照的颜色,高光仍随灯光变化,不会变成没有体积感的纯色圆片。

从 CPU 看,uniforms.uDemoTime 是稳定对象;从 GPU 看,它是一次 draw call 中共享的 float 输入。每帧只改 .value,不需要 .update()、needsUpdate 或重新 new 材质。

4. main 的组织限制

本版本通过字符串搜索识别 void main() { ... } 并按大括号寻找函数体,不是完整 GLSL 语法树解析器。采用标准、简单的 main 写法,辅助函数和声明放在 main 前面;避免把关键辅助定义放在 main 后面,或期望宏生成的 main 自动被可靠识别。

顶点输出的 varying 需要在两端声明同名同类型;vertex main 负责写入,fragment main 负责读取。不要重新声明 csm_Position、csm_Normal、csm_DiffuseColor,也不要给自己的变量使用库保留的 csm_ 前缀。

CSM 通过去掉注释后的字符串包含关系决定是否启用某个变量补丁。它不是语义分析:仅在表达式里读取变量也可能触发补丁,放进 #ifdef 也不一定能避免库的 JavaScript 侧允许表检查。不同基础材质共用大字符串时尤其要谨慎。

5. TypeScript 如何使用

import * as THREE from 'three';
import CustomShaderMaterial from 'three-custom-shader-material/vanilla';

const material = new CustomShaderMaterial<typeof THREE.MeshPhysicalMaterial>({
  baseMaterial: THREE.MeshPhysicalMaterial,
  roughness: 0.3,
  clearcoat: 1,
  fragmentShader: 'void main() { csm_Roughness = 0.35; }'
});

泛型用于关联基础构造函数参数类型,不会验证 GLSL 字符串中的输出类型、空间、材质允许范围或阴影兼容性。发布包 d.ts 也不能代替浏览器编译检查。

6. 第一轮验证

先测试一个无贴图球体:确认颜色变化、灯光响应、窗口缩放和销毁都正常。再加入 map、vertexColors、normalMap 等功能,检查输出是否仍按预期组合。csm_DiffuseColor 和 csm_FragNormal 等变量会覆盖特定阶段的结果,因此“加入一个输出赋值”可能改变其他贴图的最终影响。

若 .update({ uniforms }) 后效果突然消失,不要先怀疑 GPU;第 45 章会解释本版本 update 参数与保存的 shader 字段之间的差别。日常动画只更新 uniform 值是最简单也最可靠的方式。

练习(不附答案)

  1. 把示例改成 MeshPhongMaterial,仅保留可用的基础颜色输出,比较光照观感。
  2. 增加一个频率 uniform,让输入单位为每秒周期数,并明确 sin 的 2π 因子。
  3. 创建两个独立材质工厂返回不同 uniform 容器,验证改变一个颜色不会影响另一个。
  4. 把一个辅助 GLSL 函数放在 main 前面,再比较非标准 main 写法的识别行为,说明为什么应遵循简单结构。