1.6 Understanding Uniforms
In this tutorial, we'll explore the concept of uniforms in WebGPU shaders. Uniforms provide a mechanism to supply data to shader programs, acting as constants throughout the entire execution of a shader.
Launch Playground - 1_06_uniformsYou might wonder how uniforms differ from attributes, which we've previously used to pass data to shader programs. The distinction lies in their intended use and behavior.
Attributes: Attributes are used to provide per-vertex data. When dealing with attributes, we typically bundle all the attribute data for all vertices into a single buffer. The GPU pipeline then takes care of dissecting this data for each vertex. When a shader program runs on the GPU, it effectively launches numerous instances of the same program, each processing an individual vertex. The GPU's parallel processing capability shines in this aspect, as it can efficiently handle this workload. Think of attributes as the data each vertex requires for its unique processing.
Uniforms: In contrast, uniforms are constants that maintain the same value for all vertices and persist throughout the entire shader program execution. To draw an analogy, consider uniforms as akin to environment variables or command-line arguments when running a command-line program. Once the shader program begins execution, you can think of uniforms as constants that retain their values throughout the process. They don't change or vary per vertex; rather, they provide consistent information to the entire shader.
This distinction is crucial because it enables shaders to handle both per-vertex data (attributes) and shared, unchanging data (uniforms), offering flexibility and efficiency in the rendering process.
In this example, we'll create a uniform called "offset" to shift the positions of our vertices by a consistent amount. Using a uniform for this purpose is logical because we want the same offset applied to all vertices. If we used attributes for the offset, we'd have to duplicate the same value for every vertex, which would be inefficient and wasteful. Uniforms are the ideal choice when you need to pass the same information to the shader for all vertices.
By using a uniform for the offset, we can efficiently apply a global transformation to our geometry, easily update the offset value for dynamic effects, and reduce memory usage and data transfer compared to per-vertex attributes. This example will demonstrate how to declare, set, and use uniforms in WebGPU, illustrating their power in creating flexible and efficient shader programs.
Let's examine the syntax used to create a uniform in this program:
@group(0) @binding(0)
var<uniform> offset: vec3<f32>;
The var<uniform>
declaration indicates that offset
is a uniform variable, signaling to the shader that this variable should be provided from a uniform buffer. The @binding(0)
annotation serves a similar purpose to @location(0)
for vertex attributes. It's an index that identifies the uniform within a uniform buffer. In a typical uniform buffer, you'll pack multiple uniform values, and this index helps the shader locate the correct value efficiently.
The @group(0)
annotation relates to how uniforms are organized. In this simple case, we've placed all uniforms in a single group (group 0). However, for more complex shaders, using multiple groups can be advantageous. For instance, when rendering an animated scene, you might have camera parameters that change frequently and object colors that remain constant. By separating these into different groups, you can update only the data that changes frequently, thereby optimizing performance.
@vertex
fn vs_main(
@location(0) inPos: vec3<f32>,
@location(1) inColor: vec3<f32>
) -> VertexOutput {
var out: VertexOutput;
out.clip_position = vec4<f32>(inPos + offset, 1.0);
out.color = inColor;
return out;
}
After defining the offset
uniform, its usage in the shader becomes straightforward. We simply add the offset
value to the position of each vertex, effectively shifting their positions. This uniform allows us to apply a consistent transformation to all vertices without the need for duplication or redundancy in the shader code.
After modifying the shader, we need to create the uniform buffer and supply the data on the JavaScript side. Uniforms, like attribute data, are provided in a GPU buffer. Let's break down this process:
const uniformData = new Float32Array([
0.1, 0.1, 0.1
]);
let uniformBuffer = createGPUBuffer(device, uniformData, GPUBufferUsage.UNIFORM);
First, we create a uniformData buffer to hold our uniform values. In this example, it contains a 3-element vector representing our offset. We create the uniformBuffer using the GPUBufferUsage.UNIFORM
flag to indicate its purpose. Then, we use our helper function to populate the GPU uniform buffer with the data.
let uniformBindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX,
buffer: {}
}
]
});
Next, we create a uniform binding group layout to describe the format of the uniform group, corresponding to our uniform group definition in the shader code. In this example, the layout has one entry, corresponding to our single uniform value. The binding index matches the one in the shader, while visibility is set to VERTEX as we use this uniform in the vertex shader. Finally, the empty buffer setting object means that we want to use defaults.
let uniformBindGroup = device.createBindGroup({
layout: uniformBindGroupLayout,
entries: [
{
binding: 0,
resource: {
buffer: uniformBuffer
}
}
]
});
We then create the uniform binding group, connecting the layout with the actual data storage. Here, we supply the uniform buffer as the resource for binding 0.
const pipelineLayoutDesc = { bindGroupLayouts: [uniformBindGroupLayout] };
passEncoder.setBindGroup(0, uniformBindGroup);
In the pipeline layout descriptor, we include the uniform binding group layout. Finally, when encoding the render command, we use setBindGroup
to specify the group ID and the corresponding binding group.
With these steps, we've successfully created a uniform buffer, defined its layout, and supplied the uniform data to the shader in the GPU pipeline. The result is the same triangle, but slightly offset based on our uniform values. Experiment with adjusting the offset values in the code sample to see how it affects the triangle's position.