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:
fillPatternTypeselects the procedural maskfillPatternSizecontrols stripe or dot spacingfillPatternUvgives the fragment shader local coordinates inside each triangletriangleCentersupplies 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.