luma.gl/docs/tutorials/shader-hooks.mdx
2023-11-10 07:04:16 -05:00

235 lines
6.2 KiB
Plaintext

import {DeviceTabs} from '@site/src/react-luma';
import {ShaderHooksExample} from '@site';
# Shader Hooks
<DeviceTabs />
<ShaderHooksExample />
In the [previous tutorial](/docs/tutorials/shader-modules), we used shader modules
to insert re-usable functions into the shaders that use them.
In this tutorial, we'll focus on another feature of shader modules:
the ability to modify the behavior of the shaders that use the shader modules via **shader hooks**.
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](/docs/api-reference/engine/pipeline-factory)
(we'll look at low-level shader hooks later):
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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();
```