WebGPU coding and principle (3): a detailed explanation of a complete code

On April 6, 2023, Google announced that Chrome users can enable the new WebGPU graphics API in the 113 Beta version to support hardware graphics acceleration.

This series is a learning record, and it cannot be called a tutorial (so there may be inappropriate, imprecise or wrong in the implementation of the code and the explanation of the principle). This series hopes to explain the implementation methods and principles of related graphics effects in WebGPU through code writing and interpretation of the meaning of the code. If there are any omissions or mistakes, please correct and enlighten me.

1. A detailed explanation of a complete WebGPU rendering code

1. Complete HTML code

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebGPU 渲染</title>
</head>

<body>
    <!-- canvas:用来展示WebGPU渲染的结果 -->
    <canvas id="webgpu" width="500" height="500"></canvas>
    <script type="module">
        // 顶点着色器、片元着色器代码
        // 顶点着色器代码
        const vertex = /* wgsl */ `
            @vertex
            fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
                return vec4<f32>(pos,1.0);
            }
            `

        // 片元着色器代码
        const fragment = /* wgsl */ `
            @fragment
            fn main() -> @location(0) vec4<f32> {
                return vec4<f32>(1.0, 0.0, 0.0, 1.0);
            }
            `

        // 配置WebGPU上下文
        const adapter = await navigator.gpu.requestAdapter();
        const device = await adapter.requestDevice();
        const canvas = document.getElementById('webgpu');
        const context = canvas.getContext('webgpu');
        const format = navigator.gpu.getPreferredCanvasFormat();
        context.configure({
      
      
            device: device,
            format: format,
        });


        //创建顶点数据
        // 一个矩形拆分为两个三角形表示,三角形其中两个顶点坐标是重合的
        // 注意一个面的多个三角形,正反面要保持一致,要么都是正面,要么都是反面,或者说沿着某个方向看过去,要么都是顺时装,要么都是逆时针
        const vertexArray = new Float32Array([
            // 三角形1三个顶点坐标的x、y、z值
            -0.3, -0.5, 0.0,//顶点1坐标
            0.3, -0.5, 0.0,//顶点2坐标
            0.3, 0.5, 0.0,//顶点3坐标
            // 三角形2三个顶点坐标的x、y、z值
            -0.3, -0.5, 0.0,//顶点4坐标 与顶点1重合
            0.3, 0.5, 0.0,//顶点5坐标 与顶点3重合
            -0.3, 0.5, 0.0,//顶点6坐标
        ]);
        const vertexBuffer = device.createBuffer({
      
      // 创建顶点数据的缓冲区
            size: vertexArray.byteLength,
            usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        });
        device.queue.writeBuffer(vertexBuffer, 0, vertexArray);// 顶点数据写入缓冲区


        // 渲染管线
        const pipeline = device.createRenderPipeline({
      
      
            layout: 'auto',
            vertex: {
      
      //顶点相关配置
                module: device.createShaderModule({
      
       code: vertex }),
                entryPoint: "main",
                buffers: [//顶点缓冲区相关设置
                    {
      
      
                        arrayStride: 3 * 4, // 一个顶点数据占用的字节长度,该缓冲区一个顶点包含 xyz 三个分量,每个数字是 4 字节浮点数,3 * 4 字节长度
                        attributes: [{
      
       // 顶点缓冲区属性
                            shaderLocation: 0,// GPU 显存上顶点缓冲区存储位置标记
                            format: "float32x3", // 格式: float 32 * 3 表示一个顶点数据包含 3 个 32 位浮点数
                            offset: 0 // arrayStride 表示每组顶点数据间隔字节数, offset 表示读取该组的偏差字节数,没特殊需要一般设置为 0
                        }]
                    }
                ]
            },
            fragment: {
      
      //片元相关配置
                module: device.createShaderModule({
      
       code: fragment }),
                entryPoint: "main",
                targets: [{
      
      
                    format: format
                }]
            },
            primitive: {
      
      
                topology: "triangle-list",//绘制三角形
            }
        });

        // 命令编码器
        const commandEncoder = device.createCommandEncoder();
        // 渲染通道
        const renderPass = commandEncoder.beginRenderPass({
      
      
            // 给渲染通道指定颜色缓冲区,配置指定的缓冲区
            colorAttachments: [{
      
      
                // 指向用于Canvas画布的纹理视图对象(Canvas对应的颜色缓冲区)
        // 该渲染通道renderPass输出的像素数据会存储到Canvas画布对应的颜色缓冲区(纹理视图对象)
                view: context.getCurrentTexture().createView(),
                storeOp: 'store',
                clearValue: {
      
       r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, //背景颜色
                loadOp: 'clear',
            }]
        });
        renderPass.setPipeline(pipeline);
        // 顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来
        renderPass.setVertexBuffer(0, vertexBuffer);
        renderPass.draw(6);// 绘制顶点数据
        // 渲染通道结束命令.end()
        renderPass.end();
        // 命令编码器.finish()创建命令缓冲区(生成GPU指令存入缓冲区)
        const commandBuffer = commandEncoder.finish();
        device.queue.submit([commandBuffer]);
    </script>
</body>

</html>
2. Analysis of code sub-modules
1. Combining two triangles into a rectangle and its relationship with Shader

1. A 3 * 6 vertex data is defined, and three points (3 * 3) are a triangle, and each triangle is in the order of a direction (here is counterclockwise). (-0.3,-0.5,0.0) -> (0.3,-0.5,0.0) -> (0.3,0.5,0.0) and (-0.3,-0.5,0.0) -> (0.3,0.5,0.0) -> ( -0.3,0.5,0.0)

insert image description here

2. The parameter passed by the renderPass.draw method is 6, which corresponds to the number of vertices in the vertex data array (18 divided by 3, each vertex is represented by 3 numbers, 3 numbers are a group, a total of 6 groups, 6 vertices). That is, the shader code of the vertex shader const vertex = 'wgsl' will be executed 6 times, and the array of vertexArray will be read (because the code of the vertex shader is fn main(@location(0) pos: vec3), corresponding to Float32Array vertexArray, when reading, the vertexArray array with a length of 18 (6 * 3) will be passed in as a group of 3 numbers, such as (-0.3,-0.5,0.0) as the parameter vec3 of the shader fn main function, Then return vec4, this shader code will be executed 6 times, 6 vertices)

Please add a picture description

The format in the buffers set in the vertex in the rendering pipeline: "float32×3" means that 3 numbers form a group.

A float32-bit floating-point number occupies 4 bytes in length.

Please add a picture description

2. How does the vertexArray in JS get from the CPU to the GPU?

Store JS data like vertexArray in GPU memory (in vertex buffer)

  • Create a vertex buffer through device.createBuffer(), that is, open up a storage space in the memory (video memory) of the computer graphics card GPU to store vertex data. This opened up storage space can be understood as a vertex buffer
const vertexBuffer = device.createBuffer({
    
    // 创建顶点数据的缓冲区
            size: vertexArray.byteLength,
            usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        });
  • The data byte length (occupied size) of the memory is defined by vertexArray.byteLength

  • Set the value of the usage attribute to GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, | is a JavaScript bitwise operator.

    GPUBufferUsage.VERTEX indicates that the buffer used is a vertex buffer, that is, a buffer that stores vertex data.

    The COPY of GPUBufferUsage.COPY_DST is to copy the English word, and DST is the abbreviation of the destination word destination. Simply speaking, the buffer can write vertex data as the destination of copying vertex data.

  • Write the JS data on the CPU to the buffer (.writeBuffer (vertexBuffer, 0, vertexArray) means to write the vertex data in the vertexArray to the GPU memory buffer corresponding to the vertexBuffer, and parameter 2 means to get the vertex data from the vertexArray. Shift (unit byte), 0 means read data from the beginning of the vertexArray data.), realize the conversion from CPU data to GPU data.

device.queue.writeBuffer(vertexBuffer, 0, vertexArray);// 顶点数据写入缓冲区
3. How does the vertex data on the GPU correspond to the Shader to be executed on the GPU?
  • ShaderLocation is set to 0 in the buffers in the vertex in the rendering pipeline, that is, the mark of the vertex buffer on the GPU memory is 0. This 0 is the same as the 0 of the rendering channel renderPass.setVertexBuffer(0,vertexBuffer), and the two correspond to each other. This 0 also corresponds to @location(0) in Shader. That is, renderPass.setVertexBuffer(0,vertexBuffer) sets the data value of the vertex buffer marked as 0, and @location(0) in the Shader is to get the data value of the corresponding vertex buffer marked as 0.

    In this way, the GPU vertex data corresponds to the parameters of the code to be run by the Shader.

    Rendering pass: The rendering pass is a rendering process, including light, shadow, highlight and other processes, and the results are added to the final rendered result.

insert image description here
insert image description here

4. What is the connection method between the vertices?

We know that three points can be connected to form a triangle, so is this triangle a line or a face of a triangle? Is it end-to-end closed or not closed?

  • This depends on the properties set by the primitive of the rendering pipeline.

    For example, if it is set to "line-strip", it means that the line of the triangle is drawn, and the value of "triangle-list" means that the surface of the triangle is drawn.

insert image description here

Plotting results for different values:

insert image description here

5. The vertex data is already on the GPU, and the connection method of the vertices has been determined. How to set the color of the graph expressed by the vertex data?
  • The color is set by the fragment shader, and the fragment shader and vertex shader are defined together by the rendering pipeline.

insert image description here

6. What is the rendering pipeline?

The so-called rendering pipeline (render pipeline), that is, the rendering pipeline.

insert image description here

The picture above shows the assembly line in real life, just like the picture on the left. With the assembly line, each step is dedicated to a process, and the production efficiency is greatly improved.

The same is true for the rendering pipeline, which can extend the concept just now to computer image rendering. The working task of the rendering pipeline usually starts from a 3D scene, and finally renders to generate a 2D image, and this work is usually completed by the CPU and GPU together, just like the picture on the right, the CPU is like a incoming truck. The data is thrown to the GPU, and the GPU factory mobilizes computing units like workers to perform simple processing on these data, and finally assembles the product-image.

And the WebGPU rendering pipeline here can be described by the following figure:

insert image description here

In more detail, the above code can correspond to the following rendering pipeline process:

insert image description here

So you can see that the process of the fragment shader has been reached in the rendering pipeline process.

The function of the rendering pipeline is to complete every step and process from vertex data to rendering to canvas. The rendering pipeline will eventually output pixel data (the pixel data will be stored in the color buffer, similar to the vertex buffer used to store vertex data). The pixel data (image) is then drawn onto the Canvas.

7. How is the rendering pipeline created and associated with the canvas (drawing pixel data to the canvas)?
  • 1. Create a command encoder through the .createCommandEncoder method of the obtained GPU device object device. The command encoder can control the rendering pipeline to render and output pixel data.
    All vertex buffers, rendering pipelines, and shader configurations will not be executed. If you want to execute them on the GPU, you also need to configure the GPU command encoder object implementation, and then create a rendering pass through the .beginRenderPass method of the command encoder.

  • 2. Since the GPU can be an independent graphics card with its own memory, it can be controlled through the so-called "instruction buffer" or "instruction queue".

    The instruction queue is a block of memory (display memory) that encodes the instructions to be executed by the GPU. The encoding is closely tied to the GPU itself and is created by the graphics driver. WebGPU exposes a "CommandEncoder" API to interface with this term.

  • 3. WebGPU uses the idea of ​​modern graphics API to encode all the operations and required information of the GPU into a container called "CommandBuffer (command buffer)" in advance, and finally submit it to the GPU uniformly by the CPU, and the GPU starts to execute it.

    The object of the encoding instruction buffer is called GPUCommandEncoder, which is the instruction encoder. Its biggest function is to create two channel encoders (commandEncoder.begin[Render/Compute]Pass()) and issue a submission action (commandEncoder.finish()). All the instructions needed to finally generate this frame.

  • 4. The pipeline object is more of an "executor", which represents the entire behavior of a single calculation process, and it happens on the GPU.

    As for PassEncoder, that is, the channel encoder, it has a series of setXXX methods, and its role is more of a "scheduler".

    After the channel encoder finishes encoding, the entire encoded process represents a Pass (channel) calculation process.

        // 命令编码器
        const commandEncoder = device.createCommandEncoder();
        // 渲染通道
        const renderPass = commandEncoder.beginRenderPass({
    
    
            // 给渲染通道指定颜色缓冲区,配置指定的缓冲区
            colorAttachments: [{
    
    
                // 指向用于Canvas画布的纹理视图对象(Canvas对应的颜色缓冲区)
        // 该渲染通道renderPass输出的像素数据会存储到Canvas画布对应的颜色缓冲区(纹理视图对象)
                view: context.getCurrentTexture().createView(),
                storeOp: 'store',//像素数据写入颜色缓冲区
                clearValue: {
    
     r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, //背景颜色
                loadOp: 'clear',
            }]
        });
8. Which commands are used to determine the drawing operation?
// 设置该渲染通道控制渲染管线        
renderPass.setPipeline(pipeline);
// 顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(6);// 绘制顶点数据
// 渲染通道结束命令.end()
renderPass.end();
// 命令编码器.finish()创建命令缓冲区(生成GPU指令存入缓冲区)
const commandBuffer = commandEncoder.finish();
device.queue.submit([commandBuffer]);
  • 1. Set the current rendering pipeline through the rendering channel created by the instruction encoder.

What is a rendering pass?

本质上就是为了解决一件复杂问题而采用**分而治之**算法在图形渲染领域的一种应用。

打个盖高楼的比方,盖一栋摩天大楼是已经复杂的事情,现实中我们看到的过程从大方向上看至少可以拆成:打地基+盖大楼主体+后期装修,而不会把这盖楼(砌砖)和装修(粉刷、门窗等)的事情在单次操作中同时干了,那样不仅效率极其低下而且肯定把事情搞砸。

GPU 呈现一幅精美的画面也是同样的逻辑,开发者通常把这件事情拆成几个部分,例如先画所有不透明的物体、再画带Mask的不透明物体、再画透明的物体,可能还有后期特效的绘制,而具体怎么拆,就跟你盖楼一样,聪明的人有更聪明的拆法。而这些每个单独的画面部分的绘制工作,GPU都干完后,你就可以提交到屏幕显示了。而**一个Render Pass 就是你拆出来的这些单独的画面绘制工作中的一个,**它包含你提供的原材料(模型、贴图、shader、其他属性设置等等)和操作过程(绘制指令),最终会提交给GPU执行,而本次GPU执行的结果会写入某个 Render Target。


多通道渲染技术:一个物体我们需要多次渲染,每个渲染过程的结果会被累加到最终的呈现结果上。

这些渲染过程一般是:

highlights pass、Global illumination pass、Light Pass、Reflection Pass, Shadow Pass.  Pre-Z、  Post Effect 、

为什么会有多个pass:

多pass是为了实现一个pass实现不了的效果。

pass之间是相互依赖的,后边的pass会用到前面的pass数据(深度、几何信息),最后的pass出来的数据才是帧缓冲中的数据。

一系列drawcall的集合, 这类DrawCall通常有着类似的属性,并且按照一定的顺序执行,绘制结果通常保存在一张RT中,作为输入供后边使用。

一次FBO bind  所有绘制指令  unbind FBO

类似画一幅画 有  草稿构图阶段,明暗调整阶段,上色阶段,以及最后的精修阶段。


insert image description here

  • 2. The rendering channel associates the rendering pipeline with the vertex buffer data
// 设置该渲染通道控制渲染管线        
renderPass.setPipeline(pipeline);
// 顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(6);// 绘制顶点数据
// 渲染通道结束命令.end()
renderPass.end();
// 命令编码器.finish()创建命令缓冲区(生成GPU指令存入缓冲区)
const commandBuffer = commandEncoder.finish();
device.queue.submit([commandBuffer]);
  • 3. The draw command is used to draw vertex data
  • 4. The end command ends the rendering channel command

Through the GPU command encoder object commandEncoder, multiple rendering channels can be created as needed, and each channel can control its corresponding rendering pipeline to output images. But the case here is relatively simple, just create a rendering pass.

  • 5. .finish Create a command buffer (generate GPU instructions and store them in the buffer)
  • 6. Before the device.queue.submit([commandBuffer]); method, WebGPU-related commands will not be actually executed by the hardware.

insert image description here

Finally, through the above steps, the complete process of the rendering pipeline is realized: from CPU to GPU, from data to graphics.

9. Supplement 1: WebGPU coordinate system

insert image description here
insert image description here

The coordinate origin of the WebGPU coordinate system on the Canvas canvas is the middle position of the Canvas canvas, the x-axis is horizontal to the right , and the y-axis is vertically upward . The coordinate ranges of x and y are both [-1,1], and the WebGPU coordinate system z-axis Vertical to the Canvas, facing the screen, the z coordinate range is [0,1].

10. Supplement 2: WebGPU rendering rules

Take a point on each of the x, y, and z axes to create an equilateral triangle.

insert image description here

For a better understanding, suppose that in the 3D space of WebGPU, there is a beam of parallel rays that shine on the XOY plane along the z-axis. At this time, the triangles in the 3D space will produce projections on the XOY plane, just like in life, When people are under the sunlight, they will produce projections on the ground.

At this time, any vertex on the z-axis, after projection, is actually at the origin of the coordinates. In this way, an equilateral triangle above, after three points are projected, is two points on the x and y axes, and the point on the z-axis is projected to the coordinates The origin, such that the three points are connected, the rendered projection result is a direct triangle.

insert image description here

reference:

Rendering pipeline overview

Getting Started with WebGPU (3): Command Encoder, Rendering Channel and Final Drawing

WebGPU computing pipeline, computing shader (general computing) entry case: 2D physical simulation

7. Rendering command (this completes the first case)

4. WebGPU Storage Buffers

Multi-pass rendering RenderPass

Understand the Vulkan rendering pass (RenderPass)

Talking about the Webization of GPU—WebGPU

What does RenderingPass (rendering pass) in computer graphics mean?

Guess you like

Origin blog.csdn.net/yinweimumu/article/details/130958876