Skip to main content

Writing Portable Shaders

luma.gl does not transpile an application shader from WGSL to GLSL or from GLSL to WGSL. A rendering feature that must run on WebGPU and WebGL 2 normally keeps matching shader implementations for both languages and gives them the same application-facing inputs.

Backend Shape

BackendSource passed to luma.glEntry-point shapeBinding guidance
WebGPUOne WGSL source string.The source contains the entry points needed by the render or compute pipeline.For WGSL assembled through Model, Computation, or ShaderAssembler, prefer named @binding(auto) declarations.
WebGL 2GLSL ES 3.00 vs and fs strings.Vertex and fragment stages are separate source strings.Use luma.gl binding and shader-layout metadata; WebGL has no native bind groups.

Model accepts both render-path forms. Computation is WebGPU-only and accepts WGSL compute source.

Keep The Public Shader Contract Stable

A portable shader pair is easier to maintain when both backend sources expose the same logical contract:

ContractKeep aligned
AttributesAttribute names, formats, and buffer layout expected by the model.
VaryingsValues passed from vertex to fragment stage.
Module propsShaderModule.uniformTypes, defaultUniforms, and getUniforms() output.
BindingsResource names passed from JavaScript, even when WGSL and GLSL syntax differs.
PluginsShared plugin behavior and shader-facing vertex input names, with glsl and wgsl variants only where syntax differs.

Do not make the shader language difference leak into application props unless the feature is genuinely backend-specific. Prefer one JavaScript prop model and two shader implementations.

WGSL Binding Rule

WGSL used through shadertools should usually declare named resources with @binding(auto):

@group(0) @binding(auto) var<uniform> app: AppUniforms;
@group(0) @binding(auto) var colorTexture: texture_2d<f32>;
@group(0) @binding(auto) var colorTextureSampler: sampler;

Then bind by resource name:

model.setBindings({
app: uniformBuffer,
colorTexture: texture
});

The assembler assigns concrete WGSL binding numbers before WebGPU reflects and compiles the shader. Raw low-level WebGPU shader creation that bypasses shadertools still needs ordinary numeric WGSL bindings.

Portable Modules And Plugins

A portable ShaderModule may provide source for WGSL, vs and fs for GLSL, or all three. A portable ShaderPlugin can keep backend-neutral modules and defines at the top level while placing language-specific injection source under glsl and wgsl. If a plugin declares vertexInputs, keep those shader-facing names and types aligned with the caller-owned buffer layout and attribute data.

Use defines for real source variants, not as a substitute for two clear shader implementations. WGSL conditionals are preprocessed by luma.gl before binding assignment, so inactive resource declarations do not consume binding slots.

Verification Checklist

  • Run the feature through the WebGPU and WebGL 2 device tabs when both are supported.
  • Keep assembled WGSL binding debug output readable when bindings are involved.
  • Keep module uniform descriptors aligned with the shader declarations.
  • Use shader plugins when a reusable behavior needs different GLSL and WGSL source but one application-facing attachment point.

For exact WGSL rules, see WGSL Support. For render model props, see Model. For a working dual-backend example, see the Shader Plugins tutorial.