Shader Modules
This tutorial shows how to build reusable shader functionality with luma.gl's shader modules. The example below defines a custom color module that injects an HSV-to-RGB function into two different models.
Shader Modules
It is assumed you've set up your development environment as described in Setup.
A shader module bundles reusable WGSL and GLSL snippets, plus any uniforms or
functions it needs. The color module defined here converts HSV values to RGB
and exposes a setColor helper. Two separate models import the module and supply
different HSV values, demonstrating how module code can be shared across
programs.
The complete source for this example is shown below:
import {NumberArray3} from '@math.gl/types';
import {Buffer} from '@luma.gl/core';
import {AnimationLoopTemplate, AnimationProps, Model, ShaderInputs} from '@luma.gl/engine';
import {ShaderModule} from '@luma.gl/shadertools';
import {webgl2Adapter} from '@luma.gl/webgl';
import {webgpuAdapter} from '@luma.gl/webgpu';
// Base vertex and fragment shader code pairs
const source1 = /* wgsl */ `\
struct VertexOutput {
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertexMain(@location(0) position: vec2<f32>) -> VertexOutput {
var output: VertexOutput;
output.position = vec4<f32>(position - vec2<f32>(0.5, 0.0), 0.0, 1.0);
return output;
}
struct ColorUniforms {
hsv: vec3<f32>,
};
@group(0) @binding(0) var<uniform> color: ColorUniforms;
@fragment
fn fragmentMain() -> @location(0) vec4<f32> {
return vec4<f32>(color_hsv2rgb(color.hsv), 1.0);
}
`;
const source2 = /* wgsl */ `\
struct VertexOutput {
@builtin(position) position: vec4<f32>,
};
@vertex
fn vertexMain(@location(0) position: vec2<f32>) -> VertexOutput {
var output: VertexOutput;
output.position = vec4<f32>(position + vec2<f32>(0.5, 0.0), 0.0, 1.0);
return output;
}
struct ColorUniforms {
hsv: vec3<f32>,
};
@group(0) @binding(0) var<uniform> color: ColorUniforms;
@fragment
fn fragmentMain() -> @location(0) vec4<f32> {
return vec4<f32>(color_hsv2rgb(color.hsv) - vec3<f32>(0.3), 1.0);
}
`;
const vs1 = `\
#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position - vec2(0.5, 0.0), 0.0, 1.0);
}
`;
const fs1 = `\
#version 300 es
precision highp float;
uniform colorUniforms {
vec3 hsv;
} color;
out vec4 fragColor;
void main() {
fragColor = vec4(color_hsv2rgb(color.hsv), 1.0);
}
`;
const vs2 = `\
#version 300 es
in vec2 position;
void main() {
gl_Position = vec4(position + vec2(0.5, 0.0), 0.0, 1.0);
}
`;
const fs2 = `\
#version 300 es
precision highp float;
uniform colorUniforms {
vec3 hsv;
} color;
out vec4 fragColor;
void main() {
fragColor = vec4(color_hsv2rgb(color.hsv) - 0.3, 1.0);
}
`;
type ColorModuleProps = {
hsv: NumberArray3;
};
// We define a small customer shader module that injects a function into the fragment shader
// to convert from HSV to RGB colorspace
// From http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl
const color: ShaderModule<ColorModuleProps> = {
name: 'color',
source: /* wgsl */ `\
fn color_hsv2rgb(hsv: vec3<f32>) -> vec3<f32> {
let K = vec4<f32>(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
let p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www);
let rgb = hsv.z * mix(K.xxx, clamp(p - K.xxx, vec3<f32>(0.0), vec3<f32>(1.0)), hsv.y);
return rgb;
}
`,
fs: /* glsl */ `\
vec3 color_hsv2rgb(vec3 hsv) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(hsv.xxx + K.xyz) * 6.0 - K.www);
vec3 rgb = hsv.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), hsv.y);
return rgb;
}
`,
uniformTypes: {
hsv: 'vec3<f32>'
}
};
class AppAnimationLoopTemplate extends AnimationLoopTemplate {
model1: Model;
shaderInputs1 = new ShaderInputs<{color: ColorModuleProps}>({color});
model2: Model;
shaderInputs2 = new ShaderInputs<{color: ColorModuleProps}>({color});
positionBuffer: Buffer;
constructor({device}: AnimationProps) {
super();
this.positionBuffer = device.createBuffer(new Float32Array([-0.3, -0.5, 0.3, -0.5, 0.0, 0.5]));
this.shaderInputs1.setProps({color: {hsv: [0.7, 1.0, 1.0]}});
this.shaderInputs2.setProps({color: {hsv: [1.0, 1.0, 1.0]}});
this.model1 = new Model(device, {
id: 'model1',
source: source1,
vs: vs1,
fs: fs1,
shaderInputs: this.shaderInputs1,
bufferLayout: [{name: 'position', format: 'float32x2'}],
attributes: {
position: this.positionBuffer
},
vertexCount: 3,
parameters: {
// TODO(ibgreen): Remove, hack to ensure WebGPU depth target is used.
depthWriteEnabled: true,
depthCompare: 'less'
}
});
this.model2 = new Model(device, {
id: 'model2',
source: source2,
vs: vs2,
fs: fs2,
shaderInputs: this.shaderInputs2,
bufferLayout: [{name: 'position', format: 'float32x2'}],
attributes: {
position: this.positionBuffer
},
vertexCount: 3,
parameters: {
// TODO(ibgreen): Remove, hack to ensure WebGPU depth target is used.
depthWriteEnabled: true,
depthCompare: 'less'
}
});
}
onFinalize() {
this.model1.destroy();
this.model2.destroy();
this.positionBuffer.destroy();
}
onRender({device}) {
const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1]});
this.model1.draw(renderPass);
this.model2.draw(renderPass);
renderPass.end();
}
}
const animationLoop = makeAnimationLoop(AnimationLoopTemplate, {adapters: [webgpuAdapter, webgl2Adapter]})
animationLoop.start();
Both triangles share the same base shaders while the color module handles HSV
conversion and color passing. Modules keep shader logic organized and allow
applications to compose effects from small, reusable pieces.