Shader Hooks
The tutorial pages have not yet been updated for luma.gl v9.
In this tutorial, we'll focus on a key feature of the shader module system: the ability to modify the behavior of the shaders that use the shader modules via shader hooks.
Shader Hooks
In the previous tutorial, we used shader modules to insert re-usable functions into the shaders that use them.
A shader hook is simply a function inserted into a vertex or fragment shader. By default, these functions will be no-ops, but they define entry points into which shader modules can inject code.
For high-level API usage, shader hooks are defined on a PipelineFactory (we'll look at low-level shader hooks later):
const pipelineFactory = new PipelineFactory(device);
pipelineFactory.addShaderHook('vs:MY_SHADER_HOOK(inout vec4 position)');
const vs = `
attribute vec4 pos;
void main() {
gl_Position = pos;
MY_SHADER_HOOK(gl_Position);
}
`;
Shader modules can then inject code into the hook via their inject
property:
const myShaderModule = {
name: 'my-shader-module',
inject: 'position.x -= 0.1;'
};
We'll use these features to create a modified version of the previous tutorial, using shader hooks and modules to modify the behavior of a single set of vertex and fragment shaders.
We'll start by setting up our imports and defining our base vertex and fragment shaders:
import {AnimationLoop, Model, PipelineFactory} from '@luma.gl/engine';
const vs = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
OFFSET_POSITION(gl_Position);
}
`;
const fs = `
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
`;
Here we have a shader hook function, OFFSET_POSITION
, called in our vertex shader. Next we'll create two shader modules that insert code into the shader hook:
const offsetLeftModule = {
name: 'offsetLeft',
inject: {
'vs:OFFSET_POSITION': 'position.x -= 0.5;'
}
};
const offsetRightModule = {
name: 'offsetRight',
inject: {
'vs:OFFSET_POSITION': 'position.x += 0.5;'
}
};
These shader modules inject code into the shader hook that will modify the x-coordinate of the position passed in. The inject
property maps shader hook names to the code to be injected into them. The vs
prefix indicates that this is a vertex shader hook.
The onInitialize
method of our AnimationLoop
will be somewhat different from the previous example. To create a shader hook, we need access to a PipelineFactory
instance:
override onInitialize({device}) {
const pipelineFactory = new PipelineFactory(device);
pipelineFactory.addShaderHook('vs:OFFSET_POSITION(inout vec4 position)');
// ...
}
The shader hook definition is the function signature with a prefix indicating whether it is intended for the vertex shader (vs
) or fragment shader (fs
). Shader hooks are always void
funtions so they must return values to the caller via out
or inout
argurments. The rest of onInitialize
is similar to what we've seen before with the exception of using the new shader modules and the PipelineFactory
to create our Model
s:
override onInitialize({gl}) {
const pipelineFactory = new PipelineFactory(device);
pipelineFactory.addShaderHook('vs:OFFSET_POSITION(inout vec4 position)');
const positionBuffer = device.createBuffer(new Float32Array([
-0.3, -0.5,
0.3, -0.5,
0.0, 0.5
]));
const model1 = new Model(device, {
vs,
fs,
pipelineFactory,
modules: [offsetLeftModule],
attributes: {
position: positionBuffer
},
uniforms: {
color: [1.0, 0.0, 0.0]
},
vertexCount: 3
});
const model2 = new Model(device, {
vs,
fs,
pipelineFactory,
modules: [offsetRightModule],
attributes: {
position: positionBuffer
},
uniforms: {
color: [0.0, 0.0, 1.0]
},
vertexCount: 3
});
return {model1, model2};
}
The onRender
method is the same as before. If all went well, a blue trangle and a red triangle should be drawn side-by-side on the canvas. The code injected by the modules into the shader hook is what offsets each triangle to the left or right.
Shader hooks allowed us to define our vertex and fragment shaders once and modify their behavior based on the shader module included.
The entire application should look like the following:
import {AnimationLoop, Model, PipelineFactory} from '@luma.gl/engine';
import {clear} from '@luma.gl/webgl';
const vs = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
OFFSET_POSITION(gl_Position);
}
`;
const fs = `
uniform vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
`;
const offsetLeftModule = {
name: 'offsetLeft',
inject: {
'vs:OFFSET_POSITION': 'position.x -= 0.5;'
}
};
const offsetRightModule = {
name: 'offsetRight',
inject: {
'vs:OFFSET_POSITION': 'position.x += 0.5;'
}
};
const loop = new AnimationLoop({
override onInitialize({device}) {
const pipelineFactory = new PipelineFactory(device);
pipelineFactory.addShaderHook('vs:OFFSET_POSITION(inout vec4 position)');
const positionBuffer = device.createBuffer(new Float32Array([-0.3, -0.5, 0.3, -0.5, 0.0, 0.5]));
const model1 = new Model(device, {
vs,
fs,
pipelineFactory,
modules: [offsetLeftModule],
attributes: {
position: positionBuffer
},
uniforms: {
color: [1.0, 0.0, 0.0]
},
vertexCount: 3
});
const model2 = new Model(device, {
vs,
fs,
pipelineFactory,
modules: [offsetRightModule],
attributes: {
position: positionBuffer
},
uniforms: {
color: [0.0, 0.0, 1.0]
},
vertexCount: 3
});
return {model1, model2};
},
override onRender({device, model}) {
clear(gl, {color: [0, 0, 0, 1]});
model1.draw();
model2.draw();
}
});
loop.start();