Skip to main content

Shader Plugins

ShaderPlugin packages reusable shader behavior that can be attached to a Model or Computation. This example renders a grid of triangles from one base shader pair. One plugin adds procedural fill patterns, while clipShaderPlugin adds portable whole-instance and per-fragment clipping.

It is assumed you've set up your development environment as described in Setup.

The base shaders keep the vertex interface explicit:

  • fillPatternType selects the procedural mask
  • fillPatternSize controls stripe or dot spacing
  • fillPatternUv gives the fragment shader local coordinates inside each triangle
  • triangleCenter supplies the application-owned coordinate used by instance clipping

The fill-pattern plugin does not add inputs, so the base shader declares its application-owned attributes explicitly. Plugins that need optional per-vertex data may declare the shader-facing name and type through vertexInputs; the model or table still owns the matching bufferLayout, buffer, and update lifecycle.

Each backend-specific plugin variant injects the pattern implementation into fs:#decl. The base fragment shader calls that implementation without knowing how the GLSL or WGSL source differs.

import type {ShaderPlugin} from '@luma.gl/shadertools';

const fillPatternPlugin: ShaderPlugin = {
name: 'fill-pattern-plugin',
glsl: {
injections: [
{
target: 'fs:#decl',
injection: GLSL_FILL_PATTERN_SOURCE
}
]
},
wgsl: {
injections: [
{
target: 'fs:#decl',
injection: WGSL_FILL_PATTERN_SOURCE
}
]
}
};

The injected shader source can expose a stable callable surface while still using language-specific syntax internally:

const GLSL_FILL_PATTERN_SOURCE = /* glsl */ `
float plugin_fillPatternStripeMask(float position, vec2 size) {
float stepLength = max(size.x + size.y, 0.0001);
float wrappedPosition = fract(position / stepLength) * stepLength;
float edgeWidth = 0.0025;
return 1.0 - smoothstep(size.x - edgeWidth, size.x + edgeWidth, wrappedPosition);
}

vec4 plugin_applyFillPattern(vec4 color, float fillPatternType, vec2 uv, vec2 size) {
float opacity =
fillPatternType < 0.5 ? 1.0 : plugin_fillPatternStripeMask(uv.x + uv.y, size);
return vec4(color.rgb, color.a * opacity);
}
`;

const WGSL_FILL_PATTERN_SOURCE = /* wgsl */ `
fn pluginFillPatternStripeMask(position: f32, size: vec2<f32>) -> f32 {
let stepLength = max(size.x + size.y, 0.0001);
let wrappedPosition = fract(position / stepLength) * stepLength;
let edgeWidth = 0.0025;
return 1.0 - smoothstep(size.x - edgeWidth, size.x + edgeWidth, wrappedPosition);
}

fn pluginApplyFillPattern(
color: vec4<f32>,
fillPatternType: f32,
uv: vec2<f32>,
size: vec2<f32>
) -> vec4<f32> {
let opacity =
select(pluginFillPatternStripeMask(uv.x + uv.y, size), 1.0, fillPatternType < 0.5);
return vec4<f32>(color.rgb, color.a * opacity);
}
`;

The model opts into the behavior through plugins, while the explicit attributes stay on the model:

const model = new Model(device, {
source: WGSL_SOURCE,
vs: GLSL_VERTEX_SHADER,
fs: GLSL_FRAGMENT_SHADER,
shaderAssembler,
plugins: [fillPatternPlugin, clipShaderPlugin],
bufferLayout: [
{name: 'position', format: 'float32x2'},
{name: 'fillPatternType', format: 'float32'},
{name: 'fillPatternSize', format: 'float32x2'},
{name: 'fillPatternUv', format: 'float32x2'},
{name: 'triangleCenter', format: 'float32x2'}
],
attributes: {
position: positionBuffer,
fillPatternType: fillPatternTypeBuffer,
fillPatternSize: fillPatternSizeBuffer,
fillPatternUv: fillPatternUvBuffer,
triangleCenter: triangleCenterBuffer
},
vertexCount
});

Portable Varyings and Clipping

clipShaderPlugin declares clipCoordinates: vec2<f32> as a smooth plugin varying. The application supplies each triangle's center and each vertex's position to a pair of named hooks:

CLIP_POSITION(gl_Position, triangleCenter, position);
// ...in the fragment shader, after creating fragColor:
CLIP_COLOR(fragColor);
CLIP_POSITION(&output.position, triangleCenter, position);
// ...in the fragment shader, after creating a mutable color:
CLIP_COLOR(&color);

The hook signatures are registered for the active shader language:

const shaderAssembler = new ShaderAssembler();

shaderAssembler.addShaderHook(
'vs:CLIP_POSITION(inout vec4 position, vec2 instanceCoordinates, vec2 geometryCoordinates)'
);
shaderAssembler.addShaderHook('fs:CLIP_COLOR(inout vec4 color)');

The WGSL signatures use pointers for the mutable position and color. WGSL varying synthesis requires the selected vertex entry point to return a named stage-I/O struct and the fragment entry point to consume one named stage-I/O struct. The structs may be the same or separate. The assembler assigns unused locations and copies plugin values across the generated interface.

The clipping controls update only the plugin's shader inputs:

model.shaderInputs.setProps({
clip: {
enabled,
mode,
bounds: [left, bottom, right, top]
}
});

No buffers, models, shaders, or pipelines are recreated. In instance mode, the triangle center retains or rejects the whole triangle. In geometry mode, the interpolated vertex positions slice triangles that cross the bounds. Bounds use inclusive lower edges and exclusive upper edges.

The plugin owns only shader-interface generation and clipping shader behavior. The application still owns coordinates and buffers. A future deck.gl adapter can connect these declarations to AttributeManager without moving deck.gl's events, resources, or layer lifecycle into ShaderPlugin.

Prefer ShaderPlugin as the public composition layer. Shader hooks are the lower-level contract a base shader can expose when it wants a named injection point. A plugin may target a registered hook such as vs:OFFSET_POSITION or fs:FILTER_COLOR instead of a #decl or #main-* anchor, but applications should normally attach plugins rather than coordinate hook injection directly.

The runnable example includes all nine pattern variants: none, hash0, hash45, hash90, hash135, checker0, checker45, dotgrid, and dotgrid45.

Use shared plugin fields when a contribution is backend-neutral. Use glsl or wgsl blocks when the shader language needs different code, defines, or modules.

For a plugin that declares an Arrow-backed scalar input and updates only ShaderInputs as its range changes, see Arrow ShaderPlugin Filtering.