Comparison between WebGL and WebGPU [4] - Uniform

As we all know, when GPU runs programmable pipeline, shaders run in parallel, and each shader entry function will be executed in parallel in GPU. Each shader charges a large piece of data in a unified format, reflecting the advantages of GPU multi-core, which can process data with small cores at the same time; However, some data is the same for each shader. The type of data is "uniform", also known as uniform value.

This article lists the uniform materials in native WebGL 1/2 and WebGPU. There are some examples for reference to compare the differences between them.

1. WebGL 1.0 Uniform

1.1. Addressing with WebGLUniformLocation

In WebGL 1.0, webgl uniformlocation is usually saved on the JavaScript side to pass the uniform value to the shader program.

Use GL The getuniformlocation () method obtains this location in the following ways

  • Full name: GL getUniformLocation(program, 'u_someUniformVar')
  • Component: usually part of a vector, such as GL Getuniformlocation (program, 'u_somevec3 [0]') is the location of the 0th element (element type is vec3)
  • Structure member: GL getUniformLocation(program, 'u_someStruct.someMember')

Shader codes corresponding to the above three cases:

// full name
uniform float u_someUniformVar;

// weight
uniform vec3 u_someVec3[3]; // Notice that here are three VECs

// Structure member
struct SomeStruct {
  bool someMember;
};
uniform SomeStruct u_someStruct;  

There are three types of value transfer: scalar / vector, matrix and sampling texture. See below.

1.2. uniformMatrix[234]fv for matrix assignment

For matrices, use GL Uniformmatrix [234] Fv () method can be passed, where f represents float and v represents vector, that is, the incoming parameter is a vector (i.e. array);

To pass a 4 × 4 matrix as an example:

// Get location (at initialization)
const matrixLocation = gl.getUniformLocation(program, "u_matrix")

// Create or update the column main order transformation matrix (at render time)
const matrix = [/* ... */]

// Transfer values (at render time)
gl.uniformMatrix4fv(matrixLocation, false, matrix)

1.3. Scalar and vector use uniform[1234][fi][v]

For ordinary scalars and vectors, use GL Uniform [1234] [fi] [v] () method can be passed, where 1, 2, 3 and 4 represent the dimension of scalar or vector (1 is scalar), f/i represents float or int, and V represents vector (that is, the data you pass will be parsed into vector array in the shader).

give an example:

  • Statement 1, GL uniform1fv(someFloatLocation, [4.5, 7.1])
  • Statement 2, GL uniform4i(someIVec4Location, 5, 2, 1, 3)
  • Statement 3, GL uniform4iv(someIVec4Location, [5, 2, 1, 3, 2, 12, 0, 6])
  • Statement 4, GL uniform3f (someVec3Location, 7.1, -0.8, 2.1)

The code in the shader corresponding to the above four assignment statements is:

// Statement 1 can be adapted to 1~N floating point numbers
// When only flyer element arrays are passed, uniform float u can be declared directly_ someFloat;
uniform float u_someFloat[2];

// Statement 2 adapts to an ivec4
uniform ivec4 u_someIVec4;

// Statement 3 adapts 1~N ivec4
// When only flyer element arrays are passed, uniform float u can be declared directly_ someIVec4;
uniform ivec4 u_someIVec4[2];

// Statement 4 adapts to a vec3
uniform vec3 u_someVec3;

In WebGL 2.0, there will be some extensions in the component value type. Please refer to the relevant documents by yourself.

1.4. Transfer texture

In the vertex shader phase, you can sample the texture using the texture coordinates of the vertices:

attribute vec3 a_pos;
attribute vec2 a_uv;
uniform sampler2D u_texture;
varying vec4 v_color;

void main() {
  v_color = texture2D(u_texture, a_uv);
  gl_Position = a_pos; // Assume that vertices do not need to be transformed
}

Then, on the JavaScript side, you can use GL Uniform1i() to tell the shader which texture pit I just passed the texture to:

const texture = gl.createTexture()
const samplerLocation = gl.getUniformLocation(/* ... */)

// ...  Set texture data

gl.activeTexture(gl[`TEXTURE${5}`]) // Tell WebGL to use the texture on the fifth pit
gl.bindTexture(gl.TEXTURE_2D, texture)

gl.uniform1i(samplerLocation, 5) // Tell the shader to go to the 5th pit to read the texture later

2. WebGL 2.0 Uniform

2.1. Extension of scalar / vector / matrix value transfer method

The Uniform system of WebGL 2.0 supports matrices of non square matrix type, such as

const mat2x3 = [
  1, 2, 3,
  4, 5, 6,
]
gl.uniformMatrix2x3fv(loc, false, mat2x3)

The above method passes 4 × 3 matrix.

For single values and vectors, the method of unsigned values is additionally provided, that is, from uniform[1234][fi][v] to uniform[1234][f/ui][v], that is, the following eight new methods:

gl.uniform1ui(/* ... */) // Transfer data to 1 uint
gl.uniform2ui(/* ... */) // Transfer data to 1 uvec2
gl.uniform3ui(/* ... */) // Transfer data to 1 uvec3
gl.uniform4ui(/* ... */) // Transfer data to 1 uvec4

gl.uniform1uiv(/* ... */) // Pass data to uint array
gl.uniform2uiv(/* ... */) // Pass data to uvec2 array
gl.uniform3uiv(/* ... */) // Pass data to uvec3 array
gl.uniform4uiv(/* ... */) // Pass data to uvec4 array

The corresponding uniform in GLSL300 is:

#version 300 es
#define N ? // N depending on your needs, the number of JavaScript passes should also match
  
uniform uint u_someUint;
uniform uvec2 u_someUVec2;
uniform uvec3 u_someUVec3;
uniform uvec4 u_someUVec4;

uniform uint u_someUintArr[N];
uniform uvec2 u_someUVec2Arr[N];
uniform uvec3 u_someUVec3Arr[N];
uniform uvec4 u_someUVec4Arr[N];

It should be noted that uint/uvec234 can only be used in higher versions of glsl, that is, it is not downward compatible with WebGL 1.0 and GLSL100

However, WebGL 2.0 brings more than just these minor repairs. The most important thing is UBO. Start right away.

2.1. What is the creation of UniformBlock and UniformBuffer

In WebGL 1.0, only one uniform value of any kind can be set at a time. If there are more updates to the uniform within a frame, it is not a good thing for the state machine WebGL, which will bring additional transmission overhead from CPU to GPU.

In WebGL 2.0, it is allowed to send one pile of uniform at a time. The aggregate of this pile of uniform is called UniformBuffer, which is specific to the code:

First GLSL 300

uniform Light {
  highp vec3 lightWorldPos;
  mediump vec4 lightColor;
};

Then JavaScript

const lightUniformBlockBuffer = gl.createBuffer()
const lightUniformBlockData = new Float32Array([
  0, 10, 30, 0,    // vec3, light source position, fill a tail 0 for 8 Byte alignment
  1, 1, 1, 1,     // vec4, color of light
])
gl.bindBuffer(gl.UNIFORM_BUFFER, lightUniformBlockBuffer);
gl.bufferData(gl.UNIFORM_BUFFER, lightUniformBlockData, gl.STATIC_DRAW);

gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, lightUniformBlockBuffer)

Don't rush to ask why, step by step.

First of all, you can see that GLSL300 allows multiple Uniform variables to be declared using block syntax similar to the structure. Here, the coordinates and color of the light source are used, and different precision and data types (vec3 and vec4) are used respectively.

Then, on the JavaScript side, you see the new method GL Bindbufferbase() to bind a WebGLBuffer to position 0. This lightUniformBlockBuffer is actually a UniformBufferObject (UBO) that collects two Uniform variables. The curly bracket area named Light in the shader is called UniformBlock

In fact, creating a UBO is the same as creating an ordinary VBO, and the binding and assignment operations are almost the same (the first parameter is different). In the design of UBO, for example, it is possible to use the same data type in the design of UBO.

2.2. State binding

In WebGL 2.0, the JavaScript side allows you to bind the UniformBlock position in the shader program to a variable:

const viewUniformBufferIndex = 0;
const materialUniformBufferIndex = 1;
const modelUniformBufferIndex = 2;
const lightUniformBufferIndex = 3;
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'View'), viewUniformBufferIndex);
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Model'), modelUniformBufferIndex);
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Material'), materialUniformBufferIndex);
gl.uniformBlockBinding(prg, gl.getUniformBlockIndex(prg, 'Light'), lightUniformBufferIndex);

Here, we use GL Getuniformblockindex () gets the location of UniformBlock in the shader program, and GL binds this location to your favorite number Uniformblockbinding() method.

This has the advantage that you can artificially specify the order of each UniformBlock in your program, and then use these index es to update different ubos

// Update the UniformBlock pointed to by materialUniformBufferIndex (=1) with different ubos
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, redMaterialUBO)
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, greenMaterialUBO)
gl.bindBufferBase(gl.UNIFORM_BUFFER, 1, blueMaterialUBO)

Of course, WebGL 2.0 has other extensions to Uniform, which will not be listed here.

The function of bindBufferBase is similar to enableVertexAttribArray, which tells WebGL which pit I will use soon.

2.3. Uniform in shader

The shader uses GLSL300 syntax to use UniformBlock and new data types, which is no different from GLSL100. Of course, GLSL300 has many new grammars. Here we only pick up some about Uniform.

There are already examples of uint/uvec234 types in Section 2.1, which will not be repeated here.

On UniformBlock, there is another point that needs to be added, that is, the "naming" problem.

The syntax of UniformBlock is as follows:

uniform <BlockType> {
  <BlockBody>
} ?<blockName>;

// Example: named definition
uniform Model {
  mat4 world;
  mat4 worldInverseTranspose;
} model;

// Example: unnamed definition
uniform Light {
  highp vec3 lightWorldPos;
  mediump vec4 lightColor;
};

If you use a named definition, you need to use its name to access the members in the Block, such as model world,model. Worldinvertetranspose et al.

A complete example is as follows:

#version 300 es
precision highp float;
precision highp int;

// Layout control of uniform block
layout(std140, column_major) uniform;

// Declare a uniform block: transform, named transform, for use by the main program
// You can also use mvpMatrix without naming
uniform Transform
{
  mat4 mvpMatrix;
} transform;

layout(location = 0) in vec2 pos;

void main() {
  gl_Position = transform.mvpMatrix * vec4(pos, 0.0, 1.0);
}

Note that even if UniformBlock is named transform, the mvpMatrix of the elevation cannot share the same name with the members in other blocks, and transform has no function of namespace.

Look at JavaScript again:

//#region gets the uniform position in the shader program and binds it
const uniformTransformLocation = gl.getUniformBlockIndex(program, 'Transform')
gl.uniformBlockBinding(program, uniformTransformLocation, 0)
//endregion

//#region create ubo
const uniformTransformBuffer = gl.createBuffer()
//#endregion

//#region the ArrayBufferView required to create the matrix, and the main order of the column
const transformsMatrix = new Float32Array([
  1.0, 0.0, 0.0, 0.0,
  0.0, 1.0, 0.0, 0.0,
  0.0, 0.0, 1.0, 0.0,
  0.0, 0.0, 0.0, 1.0
])
//#endregion

//#region passes data to WebGLBuffer
gl.bindBuffer(gl.UNIFORM_BUFFER, uniformTransformBuffer)
gl.bufferData(gl.UNIFORM_BUFFER, transformsMatrix, gl.DYNAMIC_DRAW);
gl.bindBuffer(gl.UNIFORM_BUFFER, null)
//#endregion

// ----------When you need to draw----------
//#region is bound ubo to uniformLocation on index 0 for use by shaders
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniformTransformBuffer)
// ...  Render
// -------------

2.4. Transfer texture

The texture is consistent with WebGL 1.0, but the texture function of GLSL300 has changed. Please find the data and compare by yourself.

3. WebGPU Uniform

WebGPU has three types of Uniform resources: scalar / vector / matrix, texture and sampler.

Each has its own container. The first one uses gpubbuffer uniformly, which is the so-called UBO; The second and third use GPUTexture and GPUSampler

3.1. Creation and group transmission of three types of resources

The above three types of resources are grouped into a group, that is, GPUBindGroup, which I call resource binding group, and then passed to various pipelines (GPURenderPipeline, GPUComputePipeline) that organize the shader module.

Unified and easy to handle. In order to save space, data transmission will not be described in detail here. Focus on the code of their binding group:

const someUbo = device.createBuffer({ /* Note that usage should have UNIFORM */ })
const texture = device.createTexture({ /* Create a general texture */ })
const sampler = device.createSampler({ /* Create general sampler */ })

// Layout objects relate to the pipeline layout and the binding group itself
const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0, // < - bind to resource 0
      visibility: GPUShaderStage.FRAGMENT,
      sampler: {
        type: 'filtering'
      }
    },
    {
      binding: 1, // < - bound to resource 1
      visibility: GPUShaderStage.FRAGMENT,
      texture: {
        sampleType: 'float'
      }
    },
    {
      binding: 2,
      visibility: GPUShaderStage.FRAGMENT,
      buffer: {
        type: 'uniform'
      }
    }
  ]
})
const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  entries: [
    {
      binding: 0,
      resource: sampler, // < - incoming sampler object
    },
    {
      binding: 1,
      resource: texture.createView() // < - view of incoming texture object
    },
    {
      binding: 2,
      resource: {
        buffer: someUbo // < - incoming UBO
      }
    }
  ]
})

// Pipeline
const pipelineLayout = device.createPipelineLayout({
  bindGroupLayouts: [bindGroupLayout]
})
const renderingPipeline = device.createRenderPipeline({
  layout: pipelineLayout
  // ...  Other configurations
})

// ...  renderPass switches between pipeline and bindGroup to draw

3.2. Meaning of updating Uniform and binding groups

Updating Uniform resources is actually very simple.

In case of UBO, the light, material, time frame parameters and single frame change matrix modified by the front end will be updated, and device queue. Writebuffer:

device.queue.writeBuffer(
  someUbo, // To whom
  0, 
  buffer, // Pass ArrayBuffer, that is, the new data in the current frame
  byteOffset, // Where to start
  byteLength // How long
)

Using writeBuffer can ensure that the gpubbuffer is still used, and its binding relationship with binding group and pipeline is still in use; Transferring values without mapping and demapping is to reduce the cost of CPU/GPU dual terminal communication

If it's texture, use it Image copy operation Several methods are used to update texture objects;

Generally, the sampler and texture are not updated directly, but different binding groups are switched on the encoder to switch the resources required by the pipeline. Especially for texture, if the data is updated frequently, the cost of CPU/GPU dual terminal communication will increase.

For delayed rendering, off screen rendering and other color attachments that need to be updated, in fact, we only need to create a new colorAttachments object to realize "I can use the next frame drawn from the previous frame", and there is no need to directly brush the data from the CPU memory into the GPU.

Updating Uniform requires reasonable grouping of almost unchanged resources that need to be changed in each frame and divided into different binding groups, so that targeted updates can be made without resetting the pipeline and binding group once, and only switching on the channel encoder.

3.3. Uniform in shader

There is not much WGSL syntax involved here.

Similar to UniformBlock, you need to specify "one thing", which is the structure directly used by WGSL.

First, UBO:

// --Vertex shader--

// Declare a struct type
struct Uniforms {
  modelViewProjectionMatrix: mat4x4<f32>;
};

// The declaration specifies that its binding ID is 0 and the binding group sequence number is 0
@binding(2)
@group(0)
var<uniform> myUniforms: Uniforms;

// Then the myUniforms variable can be called in the function.

Then texture and Sampler:

@group(0)
@binding(1)
var mySampler: sampler;

@group(0)
@binding(2)
var myTexture: texture_2d<f32>;

// ...  Texture sampling in the main function of the slice shader
textureSample(myTexture, mySampler, fragUV);

4. Comparison and summary

WebGL takes 2 as the comparison benchmark. Compared with WebGPU, it has no resource binding group and no sampler object (the sampling parameters are set by another method).

Compared with the description method of WebGPU, using one method to switch UniformBlock, texture and other resources may be omitted, which is one of the characteristics of the global state writing method. Of course, the upper encapsulation library will help us shield these problems.

Compared with the syntax style, in fact, WebGPU improves more on the load of CPU to GPU when these uniform are updated every frame. It is encoded into instruction buffer by encoder in advance and sent at one time. It is better than sending one by one by WebGL. In places such as graphics rendering and GPU operation, a little makes a lot, and the performance is higher.

I am not proficient in WebGL 2.0 Uniform and GLSL300. Please point out any errors.

5. References

Keywords: webgl gpu

Added by lynx2003 on Sat, 19 Feb 2022 13:50:26 +0200