经纬志
首页 / luma.gl / 教程

着色器与模块化

先看结论

着色器在 v9 里先编译成独立的 Shader 对象,再交给管线。 模块化不是继承,而是把一段 GLSL 与注入点打包成普通对象, 由 shadertools 在组装阶段拼进最终源码。

最后验证 2026-11-03 · luma.gl 9.4 · 更新 2026-10-29 · 引用本文

两个阶段,两个对象

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 / fsGLSL 片段,拼在对应阶段的最前面
sourceWGSL 片段,WebGPU 使用
uniformTypesuniform 字段类型表,顺序与着色器声明一致
defaultUniformsuniform 默认值
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)。