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.
One injection selects the GLSL or WGSL pattern implementation for fs:#decl. The base fragment shader calls that implementation without knowing how the GLSL or WGSL source differs.
import type {ShaderPlugin} from '@luma.gl/shaders';
const fillPatternPlugin: ShaderPlugin = {
name: 'fill-pattern-plugin',
injections: [{stage: 'fragment', target: '#decl', glsl: GLSL_FILL_PATTERN_SOURCE, wgsl: 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 injection targets:
- GLSL
- WGSL
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 callable target signatures are registered for the active shader language:
import {GLSLShaderAssembler} from '@luma.gl/shaders/glsl';
const shaderAssembler = new GLSLShaderAssembler();
shaderAssembler.addShaderInjectionTarget({name: 'CLIP_POSITION', stage: 'vertex', glsl: {parameters: 'inout vec4 position, vec2 instanceCoordinates, vec2 geometryCoordinates'}});
shaderAssembler.addShaderInjectionTarget({name: 'CLIP_COLOR', stage: 'fragment', glsl: {parameters: 'inout vec4 color'}});
For WGSL, create a WGSLShaderAssembler instead. Its target 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 injection targets 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.
Keep modules, defines, and shader interfaces at the plugin root. Each injection selects its glsl or wgsl source without merging a second plugin descriptor.
For a plugin that declares an Arrow-backed scalar input and updates only
ShaderInputs as its range changes, see
Arrow ShaderPlugin Filtering.