Skip to main content

ShaderPlugin

ShaderPlugin groups reusable shader assembly contributions that can be attached to Model and Computation. Prefer plugins as the application-facing optional composition layer when a behavior needs a primary shader module, defines, shader-facing vertex inputs, named injections, portable inter-stage variables, or backend-specific shader source.

For the authoring model, see Shader Injection.

Learn the composition model​

A shader module owns reusable shader code and its data contract: uniform types, bindings, a uniform getter, and dependencies. A shader plugin packages optional assembly contributions around one primary module. A target is a function the host shader calls at a deliberate extension point. An injection supplies statements inside that function, or source at a built-in target.

The host chooses where extension behavior runs. Plugins choose what it does. Declaring a callable target does not insert a call into your shader: the host must call it. With no injections its generated function is a no-op.

Provide a callable target​

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

const colorTargets: ShaderPlugin = {
name: 'color-targets',
injectionTargets: [{
stage: 'fragment',
name: 'FILTER_COLOR',
glsl: {parameters: 'inout vec4 color'},
wgsl: {parameters: 'color: ptr<function, vec4<f32>>'}
}]
};

The signatures describe the same logical function in two languages. GLSL inout and WGSL function pointers let injected statements modify the host's local color. The GLSL host calls FILTER_COLOR(color); the WGSL host calls FILTER_COLOR(&color) after computing its local color and before returning it.

Contribute behavior​

const tint: ShaderPlugin = {
name: 'tint',
injections: [{
stage: 'fragment',
target: 'FILTER_COLOR',
glsl: 'color.rgb *= vec3(1.0, 0.5, 0.5);',
wgsl: '(*color) = vec4<f32>((*color).rgb * vec3<f32>(1.0, 0.5, 0.5), (*color).a);',
order: 0
}]
};

const model = new Model(device, {
vs: glslVertexSource,
fs: glslFragmentSource, // Calls FILTER_COLOR(color)
source: wgslSource, // Calls FILTER_COLOR(&color)
plugins: [colorTargets, tint]
});

Model selects sources for its device. Providers are collected before injections are validated, so [tint, colorTargets] also works. Identical injection target declarations compose and preserve an existing assembler target’s header/footer; conflicting signatures, including conflicts with assembler-registered injection targets, throw. Injection order determines execution order; equal orders preserve plugin declaration order. A missing language variant contributes nothing for that language. An injection into a missing target throws rather than silently disappearing.

Injection string shorthand​

Injection target declarations use ShaderInjectionTarget descriptors with stage, name, and language implementations. Each implementation has parameters, optional returnType, and optional header/footer. Target signature strings are not supported. Injection strings can retain stage and language prefixes:

const shortTint: ShaderPlugin = {
name: 'short-tint',
injectionTargets: [
{name: 'FILTER_COLOR', stage: 'fragment', glsl: {parameters: 'inout vec4 color'}},
{name: 'FILTER_COLOR', stage: 'fragment', wgsl: {parameters: 'color: ptr<function, vec4<f32>>'}}
],
injections: {
'glsl:fs:FILTER_COLOR': 'color.r = 0.5;',
'wgsl:fs:FILTER_COLOR': '(*color).r = 0.5;'
}
};

Unqualified string targets such as fs:FILTER_COLOR remain available when source is intentionally language-independent. Prefer qualified strings for shader code. Use object injections when you need explicit ordering. Compact objects such as {target: 'fs:FILTER_COLOR', glsl: '...'} are not supported; supply stage: 'fragment', target: 'FILTER_COLOR' instead.

Callable and built-in targets​

Callable targets express semantic operations such as filtering a color. Built-in targets such as #decl, #main-start, and #main-end add declarations or statements to an entry point. They use the same injection descriptor, for example {stage: 'vertex', target: '#main-start', wgsl: '...'}. Built-in targets require no declaration; do not declare them as injection targets. Prefer named targets when the placement is part of a host shader's public extension contract.

Configure data and own resources​

Put uniforms, bindings, and helper functions in the plugin's primary module; put supporting modules in that module's dependencies. Update runtime values through model.shaderInputs.setProps({moduleName: {...}}). Changing uniform values does not require replacing the plugin or rebuilding the shader interface.

vertexInputs and interStageVariables declare shader interfaces. They do not allocate buffers or compute attributes. The application still supplies the matching buffer layout, vertex buffers, updates, and cleanup. Luma injects the required stage inputs and transports inter-stage values. This keeps optional features composable without moving application resource ownership into shader assembly.

Injection targets describe render stages. Computation rejects render injection targets, vertex inputs, and inter-stage variables; compute plugins can still contribute modules, defines, and supported structural injections. Plugins do not schedule extra render or compute passes. Use engine and graph APIs for execution.

Usage​

const model = new Model(device, {
source: wgslSource,
vs: glslVertexSource,
fs: glslFragmentSource,
plugins: [tintPlugin]
});

Type​

import type {AttributeShaderType} from '@luma.gl/core';
import type {ShaderInjectionTarget, ShaderInjection, ResolvedShaderInjection, ShaderModule} from '@luma.gl/shaders';

export type ShaderPlugin = {
name: string;
module?: ShaderModule<any, any, any>;
defines?: Record<string, boolean | number>;
injectionTargets?: ShaderInjectionTarget[];
injections?: ShaderInjection[] | Record<string, string>;
vertexInputs?: Record<string, AttributeShaderType>;
interStageVariables?: Record<string, ShaderPluginInterStageVariable>;
};

export type ShaderPluginInterStageVariableInterpolation = 'smooth' | 'flat';

export type ShaderPluginInterStageVariable = {
type: AttributeShaderType;
interpolation?: ShaderPluginInterStageVariableInterpolation;
};

export type ShaderInjection = {
stage: 'vertex' | 'fragment';
target: string;
glsl?: string;
wgsl?: string;
order?: number;
};

export type ResolvedShaderPlugins = {
injectionTargets: ShaderInjectionTarget[];
modules: ShaderModule[];
defines: Record<string, boolean | number>;
injections: Record<string, ResolvedShaderInjection[]>;
vertexInputs: Record<string, AttributeShaderType>;
interStageVariables: Record<string, {
type: AttributeShaderType;
interpolation: ShaderPluginInterStageVariableInterpolation;
}>;
};

Numeric defines support channel counts and other preprocessor specializations, alongside boolean feature flags. WGSL preprocessor conditions support simple numeric comparisons (==, !=, <, <=, >, >=) between defines and numeric literals. This does not add token substitution or general arithmetic expressions. Define precedence matches normal shader assembly.

Primary module​

A plugin optionally supplies one module, including a concretely typed ShaderModule<Props, Uniforms, Bindings> without a cast. That module owns its uniform and binding contract; supporting shader code belongs in module.dependencies. For example, glassTransmissionPlugin.module depends on glassMaterial, which depends on opticalLighting. The assembler and ShaderInputs resolve that dependency graph. Filter and clip each supply their existing module; injection-only plugins omit it.

const filterPlugin: ShaderPlugin = {
name: 'filter',
module: filterShaderModule,
vertexInputs: {filterValues: 'f32'},
injections: [/* host target injections */]
};

Uniform updates remain keyed by module name, not plugin name. Dependencies keep independent uniform and binding namespaces; declaring a primary module does not forward its props to dependencies. Configure each module explicitly through model.shaderInputs.setProps(...). Multiple independent features can be supplied as multiple plugins. Model.modules, Computation.modules, and the resolved assembly module list remain arrays.

Resolution​

FieldResolution
module, defines, vertexInputs, and interStageVariablesShared across shader languages. Later plugins override same-named define keys.
Injection glsl / wgslSelect the source matching the assembly language. An omitted source skips that injection; an empty string remains an explicit injection.
vertexInputsRender-shader inputs merged by input name. Repeated names must use the same shader type; conflicting types throw. Buffer ownership remains with the caller.
interStageVariablesCross-stage values merged by name. Repeated names must use the same type and interpolation. Floating-point values default to smooth; integer values default to and require flat.
Injection entriesPreserve author order within the same order value.

Injection Targets​

ShaderPlugin accepts built-in and declared callable injection targets:

  • vs:#decl
  • vs:#main-start
  • vs:#main-end
  • fs:#decl
  • fs:#main-start
  • fs:#main-end
  • named shader injection targets such as vs:OFFSET_POSITION and fs:FILTER_COLOR declared by a provider plugin or the active shader assembler

Raw regex or arbitrary text-replacement targets from lower-level assembler APIs are intentionally not part of ShaderPlugin.

Use a named target only when a provider plugin or the active shader assembly path declares and calls that target. Use #decl for helper functions and globals, and #main-* for short entry-point statements.

Vertex Inputs​

vertexInputs declares render-shader attribute names and shader types required by a plugin. The plugin does not own buffers: callers still provide matching bufferLayout and attributes to Model.

The assembler adds plugin vertex input declarations before reflection. Repeated plugin declarations must use the same type. Names must be valid non-reserved identifiers and must not conflict with existing shader inputs. Computation rejects plugins with vertexInputs because compute shaders do not have render vertex inputs.

For GLSL, the assembler emits portable vertex in declarations. For WGSL, it inspects the selected vertexEntryPoint, including direct parameters and struct-based inputs, and assigns the first unused @location values in plugin declaration order. Generated entry-point parameters initialize same-invocation private variables before other vs:#main-start injections, so generated target functions can use the public input names.

WGSL reflection maps those generated parameters back to their public names. When an explicit ShaderLayout is supplied, compatible same-name metadata is preserved and newly reflected attributes are appended. Conflicting names, types, or locations throw.

vertexInputs deliberately stops at the shader interface. The application, GPUTable, or a future deck.gl AttributeManager integration still owns the buffer format, stride, step mode, allocation, updates, and lifecycle. Storage resources remain ShaderModule bindings managed through ShaderInputs.

Varyings​

GLSL plugin input and inter-stage variable declarations precede shader module source, so module helpers can read inputs and read or write inter-stage variables without redeclaring them. Ordinary #decl injections retain their existing ordering.

interStageVariables declares values that plugin target source writes during the vertex stage and reads during the fragment stage. The assembler owns the generated shader interface only. It does not own the source coordinates, attributes, buffers, events, resources, or update lifecycle.

Names must be valid non-reserved identifiers and cannot conflict with plugin vertex inputs or existing application stage I/O. Floating-point scalar and vector types default to smooth; integer types default to flat and reject smooth. Explicit flat interpolation is valid for all supported types.

For GLSL, the assembler generates matching vertex out and fragment in declarations, then zero-initializes the vertex value before other main-start injections. For WGSL, the selected vertex entry point must return a named stage-I/O struct. Fragment inputs may use a named struct, individual parameters, or no parameters. The assembler extends the fragment struct when present, or adds individual parameters otherwise. It allocates matching locations unused by either stage, including existing direct parameters, and initializes same-named private variables, copies them to every vertex return, and initializes them from the fragment input before fragment injections. Direct WGSL vertex outputs and ambiguous fragment struct inputs are rejected with an assembly error.

Computation rejects plugins with vertexInputs or interStageVariables because compute pipelines do not have a render vertex-to-fragment interface.

filterShaderPlugin​

filterShaderPlugin is a portable scalar inclusive-range filter. It declares the filterValues: 'f32' vertex input and attaches a filter shader module with enabled, min, and max props. Defaults are {enabled: true, min: 0, max: 1}.

Compatible base shaders register and call the position target:

shaderAssembler.addShaderInjectionTarget({name: 'FILTER_POSITION', stage: 'vertex', glsl: {parameters: 'inout vec4 position'}}); // GLSL
shaderAssembler.addShaderInjectionTarget({name: 'FILTER_POSITION', stage: 'vertex', wgsl: {parameters: 'position: ptr<function, vec4<f32>>'}}); // WGSL
FILTER_POSITION(gl_Position);

The caller supplies the data and updates only shader inputs when the range changes:

const model = new Model(device, {
plugins: [filterShaderPlugin],
shaderAssembler,
bufferLayout: [{name: 'filterValues', format: 'float32', stepMode: 'instance'}],
attributes: {filterValues: filterValueBuffer}
});

model.shaderInputs.setProps({filter: {enabled: true, min: 0.2, max: 0.8}});

Rejected vertices are moved outside clip space. This first version does not support category filters, filtered-row counts, fp64 values, or storage-buffer inputs.

clipShaderPlugin​

clipShaderPlugin provides coordinate-system-neutral rectangular clipping. It declares a smooth clipCoordinates: vec2<f32> inter-stage variable and a clip shader module with these props:

export type ClipShaderPluginProps = {
enabled?: boolean;
bounds?: readonly [number, number, number, number];
mode?: 'instance' | 'geometry';
};

Defaults are {enabled: true, bounds: [0, 0, 1, 1], mode: 'geometry'}. Lower bounds are inclusive and upper bounds are exclusive. Coordinates and bounds must use the same application-defined coordinate system.

Compatible shaders register and call both targets:

vs:CLIP_POSITION(
inout vec4 position,
vec2 instanceCoordinates,
vec2 geometryCoordinates
)
fs:CLIP_COLOR(inout vec4 color)

The vertex target always stores geometryCoordinates in the generated inter-stage variable. In instance mode, an out-of-bounds instanceCoordinates value moves the complete object outside clip space. In geometry mode, the fragment target discards pixels whose interpolated coordinates are outside the bounds.

model.shaderInputs.setProps({
clip: {enabled: true, mode: 'geometry', bounds: [-0.5, -0.5, 0.5, 0.5]}
});

This update changes only the managed clip uniforms. It does not rebuild the model, buffers, shaders, or pipeline.

Example​

const tintPlugin: ShaderPlugin = {
name: 'tint-plugin',
injections: [
{
stage: 'fragment', target: '#decl',
glsl: 'vec4 plugin_getTint() { return vec4(1.0, 0.4, 0.2, 1.0); }',
wgsl: 'fn pluginGetTint() -> vec4<f32> { return vec4<f32>(1.0, 0.4, 0.2, 1.0); }'
}
]
};

For runnable examples, see Shader Plugins and Arrow ShaderPlugin Filtering.

Target diagnostics and ownership​

Unknown named injection targets report the stage-prefixed target and contributing plugin names. Register the target on an application-owned assembler or pass injectionTargets to an assembly call. Per-call targets follow registered targets and override matching stage-prefixed names without changing subsequent assemblies. For WGSL mutable arguments, use pointers such as fs:FILTER_COLOR(color: ptr<function, vec4<f32>>) and write through *color.

Composition diagnostics​

mergeShaderPluginModules(modules, pluginModules, {onConflict}) optionally reports distinct module descriptors sharing a name. The callback receives (name, selected, ignored). Repeated references to one descriptor are compatible; separately created descriptors are reported even if structurally equivalent. Omitting the callback preserves existing behavior. Explicit modules retain precedence and plugin modules use first-seen name selection.

Plugin contributions are collected in plugin declaration order, selecting each injection's source for the active language. Target injections execute in ascending order; equal-order plugin injections retain declaration order.

Legacy application injections append to plugin injections at the same target instead of replacing them. Module injections follow application injections; explicit order values take precedence over collection order.