着色器与模块化
着色器在 v9 里先编译成独立的 Shader 对象,再交给管线。
模块化不是继承,而是把一段 GLSL 与注入点打包成普通对象,
由 shadertools 在组装阶段拼进最终源码。
两个阶段,两个对象
device.createShader 每次只编译一个阶段,stage 取
vertex、fragment 或 compute
(compute 只在 WebGPU 后端可用)。源码是一个完整文件,
不能只写其中一段。
const vs = device.createShader({
stage: 'vertex',
source: [
'#version 300 es',
'in vec3 positions;',
'in vec2 uvs;',
'out vec2 vUV;',
'void main() {',
' vUV = uvs;',
' gl_Position = vec4(positions, 1.0);',
'}'
].join('\n')
});
const fs = device.createShader({
stage: 'fragment',
source: [
'#version 300 es',
'precision highp float;',
'in vec2 vUV;',
'uniform sampler2D uTexture;',
'out vec4 fragColor;',
'void main() { fragColor = texture(uTexture, vUV); }'
].join('\n')
});
console.log(vs.compilationStatus); // 'success' 或 'error'
console.log(vs.getCompilationInfoSync());
getCompilationInfoSync 只在 WebGL 下可用,返回编译器日志;
异步后端用 getCompilationInfo()。日志里给的是行号,
定位时以传入的 source 为准,而不是组装后的源码。
ShaderModule 是类型,不是类
v8 里可以 new ShaderModule({...}),v9 里
luma.ShaderModule 在运行期是 undefined,
按老写法调用会直接抛错。模块就是一个普通对象,字段如下:
| 字段 | 作用 |
|---|---|
name | 模块名,用于依赖解析与调试输出 |
vs / fs | GLSL 片段,拼在对应阶段的最前面 |
source | WGSL 片段,WebGPU 使用 |
uniformTypes | uniform 字段类型表,顺序与着色器声明一致 |
defaultUniforms | uniform 默认值 |
getUniforms | 由 props 生成 uniform 与绑定的函数 |
inject | 注入点与注入代码,键形如 fs:#main-end |
dependencies | 依赖的其它模块,组装时自动带上 |
写一个模块并交给 Model
模块片段里定义函数,注入代码负责调用。注入点必须真实存在, 否则组装阶段不会报错,运行时也看不到任何效果。
const waveModule = {
name: 'wave',
fs: [
'float wave(float x, float freq) {',
' return sin(x * freq) * 0.5 + 0.5;',
'}'
].join('\n'),
inject: {
'fs:#main-end': 'fragColor.rgb *= wave(vUV.x, 6.0);'
}
};
const model = new luma.Model(device, {
vs: vsSource,
fs: fsSource,
modules: [waveModule],
attributes: { positions: positions, uvs: uvs },
bufferLayout: [
{ name: 'positions', stepMode: 'vertex', format: 'float32x3' },
{ name: 'uvs', stepMode: 'vertex', format: 'float32x2' }
],
vertexCount: 6,
topology: 'triangle-list'
});
Model 内部调用组装器,把 vs、fs、
模块片段与宏定义合并成最终源码,再分别交给 device.createShader。
因为注入代码出现在 main 末尾,主着色器必须自己声明
out vec4 fragColor 与 in vec2 vUV。
手动组装
自己管理管线时,需要先拿到组装后的源码。shadertools 提供
ShaderAssembler,GLSL 走
assembleGLSLShaderPair,WebGPU 的 WGSL 走
assembleWGSLShader。
const assembled = luma.ShaderAssembler.assembleGLSLShaderPair({
vs: vsSource,
fs: fsSource,
modules: [waveModule]
});
const vs = device.createShader({ stage: 'vertex', source: assembled.vs });
const fs = device.createShader({ stage: 'fragment', source: assembled.fs });
const pipeline = device.createRenderPipeline({
vs: vs,
fs: fs,
topology: 'triangle-list',
bufferLayout: [
{ name: 'positions', stepMode: 'vertex', format: 'float32x3' }
]
});
返回结果里除源码外还带模块产生的 uniform 取值函数,用它把运行时参数
写进 uniform 缓冲,才能让模块里的 uniform 真正生效。
注意事项
模块片段缺少声明
片段会被拼到主着色器的最前面,用到 precision、
in 变量或自定义宏时,要么由主着色器提供,要么在模块里自己声明。
缺一个 precision highp float; 就可能整条管线链接失败。
同名变量覆盖
多个模块各自定义 float scale 这类通用名字,拼接后是重复定义,
编译直接报错。模块内的函数与变量统一加模块名前缀,是最省心的约定。
用挂钟时间做 uniform
模块的 getUniforms 应把时间、视口等逐帧变化的量算好再写入缓冲。
在着色器里读不到 JavaScript 的时间,
直接把 Date.now() 拼进源码只会得到一次静态的常量。
经纬志(2026)。《着色器与模块化》。https://www.jingweizhi.com/lumagl/tutorial/shaders-modules.html(访问日期 2026-11-03)。