Understanding Bindings
luma.gl offers support for setting up ("binding") data required by the GPU during shader execution, including:
- attribute buffers
- bindings (uniform buffers, textures, samplers, ...)
- uniforms
Background
A key responsibility of any GPU framework is to make enable the application to set up (or "bind") data so that it can be accessed by shader code running on the GPU.
Shaders contain declarations of external inputs such as attributes, uniform blocks, samplers etc. Collectively, these inputs define data that the CPU (the application) needs to be provide to the GPU (typically by "binding" data to the right "locations").
Shader Layout
luma.gl needs a certain amount of metadata describing what bindings a specific shader (or pair of vertex and fragment shaders) expects.
luma.gl expects this metadata to be conform to the ShaderLayout
type, and a ShaderLayout
-conforming object
is required when creating a RenderPipeline
or ComputePipeline
.
Note that while ShaderLayout
s must be created manually for WebGPU devices,
luma.gl can generate them automatically on WebGL devices (using WebGL program introspection APIs).
Shaders expose numeric bindings, however in applications, named bindings tend to be more convenient, and the ShaderLayout does include information on both names, locations and formats.
type ShaderLayout = {
attributes: {
{name: 'instancePositions', location: 0, format: 'float32x2', stepMode: 'instance'},
{name: 'instanceVelocities', location: 1, format: 'float32x2', stepMode: 'instance'},
{name: 'vertexPositions', location: 2, format: 'float32x2', stepMode: 'vertex'}
},
bindings: {
{name: 'projectionUniforms', location: 0, type: 'uniforms'},
{name: 'textureSampler', location: 1, type: 'sampler'},
{name: 'texture', location: 2, type: 'texture'}
}
}
device.createRenderPipeline({
layout,
attributes,
bindings
});
Attribute Layout
const shaderLayout: ShaderLayout = {
attributes: [
{name: 'instancePositions', location: 0, format: 'float32x2', stepMode: 'instance'},
{name: 'instanceVelocities', location: 1, format: 'float32x2', stepMode: 'instance'},
{name: 'vertexPositions', location: 2, format: 'float32x2', stepMode: 'vertex'}
],
...
};
Buffer Mapping
For many use cases, supplying a single, "canonically" formatted buffer per attribute is sufficient. However, sometimes an application may want to use more sophisticated GPU buffer layouts, controlling GPU buffer offsets, strides, interleaving etc.
Buffer mapping is an optional feature enabling custom buffer layouts and buffer interleaving.
Note that buffer mappings need to be defined when a pipeline is created, and all buffers subsequently supplied to that pipeline need to conform to the buffer mapping.
Example: The bufferLayout
field in the example below specifies that both the
instancePositions
and instanceVelocities
attributes should be read from a single,
interleaved buffer. In this example, since no strides are provided, supplied buffers are assumed to be "packed",
with alternating "position" and "velocity" values with no padding in between.
device.createRenderPipeline({
shaderLayout: {
attributes: [
{name: 'instancePositions', location: 0, format: 'float32x2', stepMode: 'instance'},
{name: 'instanceVelocities', location: 1, format: 'float32x2', stepMode: 'instance'},
{name: 'vertexPositions', location: 2, format: 'float32x2', stepMode: 'vertex'}
],
...
},
// We want to use "non-standard" buffers: two attributes interleaved in same buffer
bufferLayout: [
{name: 'particles', attributes: [
{name: 'instancePositions'},
{name: 'instanceVelocities'}
]
],
attributes: {
particles: device.createBuffer(...)
},
bindings: {}
});
Model usage
new Model(device, {
shaderLayout: {
attributes: {
instancePositions: {location: 0, format: 'float32x2', stepMode: 'instance'},
instanceVelocities: {location: 1, format: 'float32x2', stepMode: 'instance'},
vertexPositions: {location: 2, format: 'float32x2', stepMode: 'vertex'}
}
}
});
Types
ShaderLayout
export type ShaderLayout = {
attributes: AttributeLayout[];
bindings: BindingLayout[];
}
AttributeLayout
- name =
string
- location -
number
- format -
VertexFormat
- stepMode -
'vertex' \| 'instance'
Advanced Example
WGSL vertex shader
struct Uniforms {
modelViewProjectionMatrix : mat4x4<f32>;
};
[[binding(0), group(0)]] var<uniform> uniforms : Uniforms; // BINDING 0
struct VertexOutput {
[[builtin(position)]] Position : vec4<f32>;
[[location(0)]] fragUV : vec2<f32>;
[[location(1)]] fragPosition: vec4<f32>;
};
[[stage(vertex)]]
fn main([[location(0)]] position : vec4<f32>,
[[location(1)]] uv : vec2<f32>) -> VertexOutput {
var output : VertexOutput;
output.Position = uniforms.modelViewProjectionMatrix * position;
output.fragUV = uv;
output.fragPosition = 0.5 * (position + vec4<f32>(1.0, 1.0, 1.0, 1.0));
return output;
}
WGSL Fragment Shader
[[group(0), binding(1)]] var mySampler: sampler; // BINDING 1
[[group(0), binding(2)]] var myTexture: texture_2d<f32>; // BINDING 2
[[stage(fragment)]]
fn main([[location(0)]] fragUV: vec2<f32>,
[[location(1)]] fragPosition: vec4<f32>) -> [[location(0)]] vec4<f32> {
return textureSample(myTexture, mySampler, fragUV) * fragPosition;
}