WebGPU快速入门教程

这里写自定义目录标题

WebGPU快速入门教程

下面我会让你在最短的时间内,实现一个WebGPU的小案例,不用你配置复杂的开发环境,你只需要复制下面代码,到一个.html文件中,然后用最新版的谷歌浏览器打开该html文件,就可以看到一个WebGPU的三角形案例。

参了我的个人网站的电子书:threejs中文网:http://www.webgl3d.cn/pages/9bbfa9/

WebGPU免费的部分视频

<!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>Threejs中文网:http://www.webgl3d.cn/</title>
</head>

<body>
    <!-- canvas:用来展示WebGPU渲染的结果 -->
    <canvas id="webgpu" width="500" height="500"></canvas>
    <script type="module">
        // 顶点着色器
        const vertex = /* wgsl */ `
        @vertex
        fn main(@location(1) 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);
        }
        `

        const adapter = await navigator.gpu.requestAdapter();// 浏览器请求GPU适配器
        // 获取GPU设备对象,通过GPU设备对象device的WebGPU API可以控制GPU渲染过程
        const device = await adapter.requestDevice();
        // 配置WebGPU上下文(Canvas元素作为WebGPU的画布)
        const canvas = document.getElementById('webgpu');
        const context = canvas.getContext('webgpu');
        const format = navigator.gpu.getPreferredCanvasFormat();//获取浏览器默认的颜色格式
        context.configure({
            device: device,//WebGPU渲染器使用的GPU设备对象
            format: format,//颜色格式
        });

        // 创建顶点数据的缓冲区
        const vertexArray = new Float32Array([
            // 三角形三个顶点坐标的x、y、z值
            0.0, 0.0, 0.0,//顶点1坐标
            1.0, 0.0, 0.0,//顶点2坐标
            0.0, 1.0, 0.0,//顶点3坐标
        ]);
        //类型化数组Float32Array一个数字元素,占用存储空间4字节,9个浮点数,数据字节长度9*4
        // console.log('类型化数组数据字节长度',vertexArray.byteLength);
        const vertexBuffer = device.createBuffer({
            size: vertexArray.byteLength,//顶点数据的字节长度
            //usage设置该缓冲区的用途
            usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        });
        //把vertexArray里面的顶点数据写入到vertexBuffer对应的GPU显存缓冲区中
        //获取顶点数据的偏移量(单位字节),0表示从vertexArray的数据开始开头读取数据
        device.queue.writeBuffer(vertexBuffer, 0, vertexArray);


        // 创建一个WebGPU渲染管线对象pipeline
        const pipeline = device.createRenderPipeline({
            layout: 'auto',
            vertex: {//顶点相关配置
                // module:设置渲染管线要执行的顶点着色器代码
                module: device.createShaderModule({ code: vertex }),
                entryPoint: "main",//顶点着色器代码入口函数的名字
                buffers: [// 顶点所有的缓冲区模块设置
                    {//其中一个顶点缓冲区设置
                        arrayStride: 3 * 4,//一个顶点数据占用的字节长度,该缓冲区一个顶点包含xyz三个分量,每个数字是4字节浮点数,3*4字节长度
                        // 顶点缓冲区属性
                        attributes: [{
                            shaderLocation: 1,//GPU显存上顶点缓冲区标记存储位置
                            format: "float32x3",//格式:loat32x3表示一个顶点数据包含3个32位浮点数
                            offset: 0//arrayStride表示每组顶点数据间隔字节数,offset表示读取改组的偏差字节数,没特殊需要一般设置0
                        }]
                    }
                ]
            },
            fragment: {
                // module:设置渲染管线要执行的片元着色器代码
                module: device.createShaderModule({ code: fragment }),
                entryPoint: "main",//指定片元着色器入口函数
                targets: [{
                    format: format//和WebGPU上下文配置的颜色格式保持一致
                }]
            },
            primitive: {
                topology: "triangle-list",//绘制三角形
            }
        });

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

</html>


WebGPU学习(开发)环境配置

WebGPU免费的部分视频

咱们的WebGPU学习(开发)环境配置比较简单,不用vite或webpack配置一个复杂的开发环境,直接使用原生.html文件即可。入门学习WebGPU,平时测试代码,都会非常方便,大大节约学习时间。

1. 一个支持WebGPU的浏览器

谷歌浏览器从Chrome 113 Beta测试版开始默认支持WebGPU。如果你的谷歌浏览器低于113版本,请下载新版本浏览器才能正常学习WebGPU。下载安装后,就可以直接预览webGPU的3D案例了。

随着时间的推移,将会越来越多的浏览器支持WebGPU。

谷歌浏览器正式版下载

谷歌浏览器Beta测试版下载

你可以用上面链接下载最新版谷歌浏览器,也可以直接用课件中我下载好的ChromeSetup.exe,运行安装。

2. index.html文件

<script>标签设置type="module",你就可以直接浏览器中实现ES6语法了,不用webpack或vite进行编译处理。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>http://www.webgl3d.cn/</title>
</head>

<body>
    <script type="module">
        // 编写WebGPU代码
    </script>
</body>

</html>

3. vscode编辑器

课程选择的是vscode编辑器,平时开发调试WebGPU案例源码,是借助live-srever插件调用浏览器执行WebGPU代码,非常方便。

4. 测试你的浏览器是否支持WebGPU

<script type="module">
    if(navigator.gpu){
        console.log('你的浏览器支持WebGPU。');
    }else{
        console.log('你的浏览器不支持WebGPU,请更换新版本浏览器。');
    }
</script>

WebGPU API和Canvas画布

WebGPU提供很多相关的API,通过这些WebGPU API可以控制你的显卡GPU渲染3D场景或计算数据。

WebGPU API文档:https://www.w3.org/TR/webgpu/

GPU概念解释

所谓GPU就是图形处理器,再具体点说,就是你电脑上的显卡,如果为了追求更好的性能,一般会在电脑上安装独立显卡。

GPU设备对象

创建GPU设备对象device非常简单,执行navigator.gpu.requestAdapter()adapter.requestDevice()两步操作即可完成。

.requestAdapter().requestDevice()都是异步函数,函数前需要加上es6语法的关键字await

// 浏览器请求GPU适配器
const adapter = await navigator.gpu.requestAdapter();
// 获取GPU设备对象,通过GPU设备对象device的WebGPU API可以控制GPU渲染过程
const device = await adapter.requestDevice();

浏览器控制台测试查看,适配器对象adapter和GPU设备对象device对象的一些属性和方法

console.log('适配器adapter',adapter);
console.log('GPU设备对象device',device);

GPU设备对象device的属性和方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LPUtsJXG-1686833101660)(/imgwegpu/GPU设备对象的属性和方法.svg)]

借助GPU设备对象device提供的很多属性和方法,这些属性和方法都是WebGPU API的一部分。后面课程会给大家逐步讲解,如何通过GPU设备对象device提供的这些WebGPU API渲染3D场景。

device.createRenderPipeline()//创建渲染管线
device.createComputePipeline()//创建计算管线
device.createShaderModule()//创建着色器模块
device.createCommandEncoder()//创建命令对象(绘制和计算命令)
device.createBuffer()//创建缓冲区对象
...

初次接触,可能你还不理解这些WebGPU API,那也没关系,后面会详细讲解,现在你可以随意调用两个API写代码,提前熟悉下。

// 创建渲染管线
const pipeline = device.createRenderPipeline();
// 创建GPU命令对象
const commandEncoder = device.createCommandEncoder();

这也是为什么我们要执行device = await adapter.requestDevice()创建GPU设备对象device,只有通过创建GPU设备对象,我们才可以获得这些API。

Canvas画布

Canvas画布是一个比较特殊的HTML元素,主要用来实现图形绘制的功能,可以进行2D绘图,可以用来实现WebGL,也可以把WebGPU渲染的图像输出到Canvas画布。

<!-- canvas:用来展示WebGPU渲染的结果 -->
<canvas id="webgpu" width="500" height="500"></canvas>

配置WebGPU上下文(Canvas元素作为WebGPU的画布)

获取id名为webgpu的Canvas画布元素。

const canvas = document.getElementById('webgpu');

Canvas画布对象有一个获取上下文的方法.getContext(),参数可以是2d、webgl、webgpu,不同参数用于不同的功能,咱们这里是用于WebGPU渲染,所以参数设置为webgpu。

const context = canvas.getContext('webgpu');

通过方法context.configure()配置从Canvas画布获取的WebGPU上下文对象context

用人话说就是关联Canvas画布和GPU设备对象device,这样就能把Canvas元素作为WebGPU的画布,用来呈现3D渲染效果。

context.configure({
    device: device,//WebGPU渲染器使用的GPU设备对象
});

format属性和颜色格式有关,如果没有特别需要,可以设置为navigator.gpu.getPreferredCanvasFormat()即可,初学可以不用深究。

const format = navigator.gpu.getPreferredCanvasFormat();//获取浏览器默认的颜色格式
context.configure({
    device: device,
    format: format,//颜色格式
});

配置WebGPU上下文代码。

<body>
    <!-- canvas:用来展示WebGPU渲染的结果 -->
    <canvas id="webgpu" width="500" height="500"></canvas>
    <script type="module">
        // 1. 初始化WebGPU
        const adapter = await navigator.gpu.requestAdapter();
        // 获取GPU设备对象,通过GPU设备对象device的WebGPU API可以控制GPU渲染过程
        const device = await adapter.requestDevice();

        //配置WebGPU上下文,把id名为webgpu的Canvas元素作为WebGPU的画布
        const canvas = document.getElementById('webgpu');
        const context = canvas.getContext('webgpu');
        const format = navigator.gpu.getPreferredCanvasFormat();//获取浏览器默认的
        context.configure({
            device: device,//WebGPU渲染器使用的GPU设备对象
            format: format,//WebGPU渲染器使用的颜色格式
        });
    </script>
</body>

创建顶点缓冲区、渲染管线

如果你想渲染一个物体,需要先通过顶点坐标来定义该物体的几何形状,本节课就给大家讲解,怎么通过WebGPU的顶点缓冲区来创建顶点数据。

WebGPU坐标系

WebGPU坐标系在Canvas画布上的坐标原点是Canvas画布的中间位置,x轴水平向y轴竖直向z轴垂直与Canvas画布,朝向屏幕内。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2sAptzo0-1686833114867)(/imgwegpu/canvas画布xy坐标系.png)]

前端开发时候,HTML元素的宽高很多时候是选择以像素为基准定义,比如宽度500px。WebGPU中顶点坐标的表示值采用的是相对值,比如x和y的坐标范围都是[-1,1],z坐标的范围是[0,1]。

在咱们入门的第一个案例中,先不深入谈WebGPU坐标系,你能先用x、y两个分量绘制一个2D平面图就行,后面涉及到3D效果的时候,再详细展开讲解z坐标、投影矩阵、视图矩阵、模型矩阵等深入概念。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KEN3W6Bz-1686833114868)(/imgwegpu/WebGPU坐标系.png)]

JavaScript类型化数组

类型化数组文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Typed_arrays

JavaScript类型化数组不同于普通的数组,类型化数组,就是数组的元素可以设置数字的类型,比如浮点数、无符号整数…

实际开发顶点数据往往都比多,在WebGL、WebGPU、threejs等代码中,会用类型化数组类型化数组表示定义顶点数据。

类型化数组Float32Array表示顶点坐标

一般来说通过WebGPU绘制一个几何图形的时候,比如一个三角形、一个矩形、一个立方体…需要使用顶点先表示几何体的形状。

刚入门,先定义一个简单的几何图形,比如我使用三个顶点的xyz坐标表示一个三角形。实际开发的时候,你可以根据需要,创建任意个顶点坐标数据,来表达一个复杂的几何图案。

类型化数组Float32Array参数数组里面的元素三个为一组表示顶点的xyz坐标。

const vertexArray = new Float32Array([
    // 三角形三个顶点坐标的x、y、z值
    0.0, 0.0, 0.0,//顶点1坐标
    1.0, 0.0, 0.0,//顶点2坐标
    0.0, 1.0, 0.0,//顶点3坐标
]);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ws8vAnfD-1686833114868)(/imgwegpu/WebGPU坐标系定义一个三角形.png)]

创建顶点缓冲区.createBuffer()

通过GPU设备对象的.createBuffer()方法可以创建一个顶点缓冲区。

关于顶点缓冲区,给大家简单解释下。大家都知道数据,会占用电脑的内存,对于顶点数据而言,同样需要占用电脑内存空间,你可以这么理解,当device.createBuffer()执行的时候,会在你的电脑显卡GPU的内存(显存)中开辟一片存储空间,用来存存储顶点数据,你可以把这个开辟的存储空间,称为顶点缓冲区

const vertexBuffer = device.createBuffer();

缓冲区存储字节长度设置size

设置存储空间的size属性,表示存储空间的大小size。

const vertexBuffer = device.createBuffer({
    size: vertexArray.byteLength,//数据字节长度
});
//类型化数组Float32Array一个数字元素,占用存储空间4字节,9个浮点数,数据字节长度9*4
console.log('类型化数组数据字节长度',vertexArray.byteLength);

缓冲区用途定义usage

usage的属性值其他属性值参考文档:https://www.w3.org/TR/webgpu/#typedefdef-gpubufferusageflags

入门案例顶点缓冲区可以像下面一样设置,usage以后还会遇到其他的写法,遇到在专门讲解。

设置usage属性的值为GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,|是JavaScript位运算符

GPUBufferUsage.VERTEX表示用于该缓冲区是顶点缓冲区,就是存储顶点数据的缓冲区。

GPUBufferUsage.COPY_DSTCOPY是复制英文单词,DST是目的地单词destination的缩写,简单说该缓冲区可以写入顶点数据,作为复制顶点数据的目的地。

const vertexBuffer = device.createBuffer({
    size: vertexArray.byteLength,//顶点数据的字节长度
    //usage设置该缓冲区的用途(作为顶点缓冲区|可以写入顶点数据)
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});

顶点数据写入顶点缓冲区

GPU设备对象device队列属性.queue的有一个方法.writeBuffer(),功能是吧类型化数组中的数据写入.createBuffer()创建的顶点缓冲区中。

.writeBuffer(vertexBuffer, 0, vertexArray)表示把vertexArray里面的顶点数据写入到vertexBuffer对应的GPU显存缓冲区中,参数2表示从vertexArray获取顶点数据的偏移量(单位字节),0表示从vertexArray的数据开头读取数据。

//把vertexArray里面的顶点数据写入到vertexBuffer对应的GPU显存缓冲区中
//参数2的0表示从vertexArray的数据开头读取数据。
device.queue.writeBuffer(vertexBuffer, 0, vertexArray)

.createRenderPipeline()创建渲染管线

通过GPU设备对象的方法.createRenderPipeline()可以创建一个WebGPU渲染管线。

// 创建一个WebGPU渲染管线对象pipeline
const pipeline = device.createRenderPipeline();

渲染管线你可以类比生活中的工厂流水线来理解,流水线上不同的功能单元,完成不同的零部件生产,对WebGPU渲染管线类似,WebGPU渲染管线上也提供用于3D渲染的不同功能单元,后面会一一讲解。

你可以把显卡比作一个工厂,工厂里面,你可以开设流水线,同样的道理,你也可以在显卡GPU上开设创建渲染管线,借助GPU设备对象的方法.createRenderPipeline()即可创建WebGPU的渲染管线,你可以根据需要创建多个渲染管线,当然咱们课程入门部分,只需要创建一个用来学习即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEdAaiEz-1686833114869)(/imgwegpu/渲染管线最简单的图案.png)]

.createRenderPipeline()参数

渲染管线方法.createRenderPipeline()的参数是一个对象,对象具有layoutvertexfragmentprimitive等属性,这些属性对应了渲染管线上的不同功能单元。这些属性你现在还不理解,也没有关系,后面会给打逐步讲解。

const pipeline = device.createRenderPipeline({
    layout: 'auto',
    vertex: {
        // 顶点着色器
        module: device.createShaderModule({ code: vertex }),
        entryPoint: "main"
    },
    fragment: {
        // 片元着色器
        module: device.createShaderModule({ code: fragment }),
        entryPoint: "main",
    },
    primitive: {
        topology: "triangle-list",//三角形绘制顶点数据
    }
});

vertex.buffers配置顶点缓冲区

顶点缓冲区负责渲染管线提供顶点数据,所以所以需要通过渲染管线参数的vertex.buffers属性配置,渲染管线如何获取顶点缓冲区中的顶点数据。

const pipeline = device.createRenderPipeline({
    vertex: {//顶点相关配置
        buffers: [// 顶点所有的缓冲区模块设置
            {//其中一个顶点缓冲区设置
                arrayStride: 3*4,//一个顶点数据占用的字节长度,该缓冲区一个顶点包含xyz三个分量,每个数字是4字节浮点数,3*4字节长度
                attributes: [{// 顶点缓冲区属性
                    shaderLocation: 0,//GPU显存上顶点缓冲区标记存储位置
                    format: "float32x3",//格式:loat32x3表示一个顶点数据包含3个32位浮点数
                    offset: 0//arrayStride表示每组顶点数据间隔字节数,offset表示读取改组的偏差字节数,没特殊需要一般设置0
                }]
            }
        ]
    },
});

着色器语言WGSL快速了解

WGSL语言是专门给WebGPU定制的着色器语言,就像WebGL OpenGL中使用的GLSL着色器语言。

如果你有GLSL着色器语言基础,那么学习WGSL还是比较容易的。

如果你从未学过其他着色器语言,第一次接触WGSL着色器语言,要想真正理解WGSL,还需要和后面WebGPU具体案例结合讲解,如果只是把语法给你念一遍,过于抽象,很难理解。

文档资料

WGSL英文文档:https://www.w3.org/TR/WGSL/

WebGPU引擎Orillusion团队翻译:https://www.orillusion.com/zh/wgsl.html

学习基础与难度

如果你有其他着色器语言基础,比如GLSL,那么你学习WGSL将会非常简单。

如果没有学过着色器代码,但是有其它静态类型语言的基础,比如Typescript、C等,理解WGSL基础语言也会相对容易。

如果上面两个基础都没有,只是熟悉JavaScript,相对难度高些。

WGSL学习方法

WGSL虽然类似Typescript、C等语言,但是WGSL主要在GPU上执行,有自身的特殊性,结合WebGPU案例学习WGSL语法,才能更好的理解。

所以本节课不做过多的WGSL语法讲解,随意举几个案例,初步了解一些WGSL最基础的语法,你也不用写代码,跟着视频过一遍即可。

WGSL基础类型

下面简单列举了部分WGSL的数据类型

符号数据类型
bool布尔
u32无符号整数
i32有符号整数
f3232位浮点数
f1616位浮点数

var关键字声明变量

WGSL中可以用var关键字声明变量。

// var关键字声明一个变量a,数据类型是无符号整数
var a:u32;
u32 = 2;
// var关键字声明一个变量a,数据类型是32位浮点数
var a:f32;
a = 2.0;

声明的时候直接赋值

// var关键字声明一个32位浮点数
var a:f32 = 2.0;

有时候你看别人的WGSL代码,声明变量如果赋值了,可能会省略数据类型标注,这时候WGSL会根据变量的值自动推荐变量的数据类型

var a = 2.0;//推断为f32
var a = 2;//推断为i32
var a = false;//推断为布尔值

变量简单运算

两个变量进行运算,需要保持一样数据类型,否则报错。

// 32位浮点数相加
var a:f32 = 2.0;
var b:f32 = 4.0;
var c:f32 = a+b;
// 无符号整数相加
var a:u32 = 2;
var b:u32 = 4;
var c:u32 = a+b;

声明函数的关键字fn

// 这还能混合写,牛逼了
fn 函数名( 参数1:数据类型, 参数2:数据类型...){
    // 代码
}
fn add( x: f32, y:f32){
    var z: f32 =  x + y;
}

如果函数有返回值设置符号->,后面注明返回值的数据类型

// 这还能混合写,牛逼了
fn 函数名( 参数1, 参数2...) -> 返回值数据类型 {
    return 返回值;
}
fn add( x: f32, y:f32) -> f32 {
    return x + y;
}
// 类比JavaScript语言函数
function add(x , y){
    return x + y;
}
// 类比TypeScript语言函数
function add(x: number, y: number): number {
  return x+y
}

if、for等语句

在WGSL中,if、for等语句,和JavaScript逻辑上基本差不多,区别就是注意数据类型即可。

WGSLfor循环语句,基本逻辑

var n:u32 = 10;
var s:f32 = 0.0;
for (var i:u32= 0; i < n; i++) {
    s += 0.05;         
}
var s:bool;
var a:f32 = 2.0;
if(a>1.0){
    s = true;
}else{
    s = false;
}

向量表示颜色

在WGSL中,向量可以表示多种数据,也能进行多种数学运算,咱们这里先不讲解那么多,说些简单的。

// 四维向量有四个分量,可以用来表示颜色的R、G、B、A
var color:vec4<f32> = vec4<f32>(1.0, 0.0, 0.0, 1.0);//红色不透明
// 省略:vec4<f32>数据类型
var color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
// 先声明一个四维向量变量,再赋值
var color:vec4<f32>;
color = vec4<f32>(1.0, 0.0, 0.0, 1.0);

向量表示位置

三维向量vec3<f32>表示具有三个分量,可以用来表示顶点的xyz坐标。

var pos:vec3<f32>;
pos= vec3<f32>(1.0, 2.0, 3.0);

四维向量表示齐次坐标,在WGSL中,表示一个坐标的的时候,如果用四维向量表示,最后一个分量是1.0。改坐标表示xyz的齐次坐标。

var pos:vec4<f32>;
pos= vec4<f32>(1.0, 2.0, 3.0,1.0);

一个三维向量转化为四维向量

var pos:vec3<f32>;
pos = vec3<f32>(1.0, 2.0, 3.0);
//等价于vec4<f32>(1.0, 2.0, 3.0,1.0)
var pos2 = vec4<f32>(pos,1.0);

一个二维向量转化为四维向量

var pos:vec2<f32>;
pos = vec2<f32>(1.0, 2.0);
//等价于vec4<f32>(1.0, 2.0, 3.0,1.0)
var pos2 = vec4<f32>(pos, 3.0,1.0);

结构体

WGSL结构体有点类似JavaScript里面的类

// 定义一个结构体表示点光源
struct pointLight {
    color: vec3<f32>,//光源颜色
    intensity: f32//光源强度
};

通过结构体生成一个光源,类似JavaScript中类执行new实例化一个对象。

var light1:pointLight;
light1.color = vec3<f32>(1.0, 0.0, 0.0);
light1.intensity = 0.6;

WGSL代码注释

WGSL代码注释和JavaScript语言的习惯一样。

  • 单行注释符号//

  • 快级注释符号/* */

WGSL语句结尾分号

在JavaScript中,代码语句结尾的分号可以省略,但是WGSL中分号不能省略。

var a:f32 = 2.0;
var a:f32 = 4.0//分号省略,会报错

顶点着色器

你把渲染管线想象为工厂的一条流水线顶点着色器想象为流水线上一个的工位

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IySHIgr0-1686833138851)(/imgwegpu/渲染管线最简单的图案.png)]

GPU渲染管线上提供的顶点着色器单元的功能就是计算顶点,所谓计算顶点,简单点说,就是对顶点坐标x、y、z的值进行平移、旋转、缩放等等各种操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PNtKZzuY-1686833138852)(/imgwegpu/顶点着色器功能.png)]

顶点着色器代码

GPU渲染管线上的顶点着色器功能单元,可以执行WGSL着色器语言编写的代码。

所有顶点数据经过顶点着色器这个工位时候,都会执行顶点着色器代码中顶点计算的函数,比如平移顶点坐标,比如放大顶点坐标,具体怎么改变顶点数据,看你怎么写的顶点着色器代码。

@vertex
fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
    var pos2 = vec4<f32>(pos,1.0);//pos转齐次坐标
    pos2.x -= 0.2;//偏移所有顶点的x坐标
    return pos2;
}

WGSL着色器代码形式

在JavaScript或Typescript写的WebGPU代码时候,按照语法要求,WGSL着色器的代码,要以字符串的形式存在。

如果你直接在单引号双引号表示的字符串里面写WGSL代码,实现字符串的多行书写,需要用+号码连接,不是很方便的。

const str = '@vetex'
    + 'fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {'
    + '    return vec4<f32>(pos,1.0);'
    + '}'

使用ES6的语法模板字符串``(反引号),实现字符串的多行书写很方便。

// 顶点着色器代码
const vertex = `
@vertex
fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
    return vec4<f32>(pos,1.0);
}
`

反引号里面写顶点着色器代码

Tab键上面的一个按键输入反引号,实现JavaScript模板字符串``语法

const vertex = `以字符串形式写WGSL代码`

@vertex

@vertex表示字符串vertex里面的代码是顶点着色器代码,在GPU渲染管线的顶点着色器单元上执行。

const vertex = `
@vertex
`

为了方便单独管理WGSL着色器代码,你可以创建一个shader.js文件,在里面写着色器代码。

const vertex = `
@vertex
`
export { vertex }

fn关键字声明一个函数

fn关键字声明一个函数,命名为main,作为顶点着色器代码的入口函数。fn关键字类似JavaScript语言的function关键字,用来声明一个函数

@vertex
fn main(){
}

vscode插件 可视化WGSL语法

搜索关键词WGSL,安装插件WGSL和WGSL Literal。

着色器代码之前设置/* wgsl */,可以使用不同颜色来显示WGSL不能的部分,更方便预览学习。

// 顶点着色器代码
const vertex = /* wgsl */`
@vertex
fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
    return vec4<f32>(pos,1.0);
}
`

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aBESFtBK-1686833138852)(/imgwegpu/顶点着色器代码没有彩色显示.png)]

location关键字

location是WGSL语言的一个关键字,通用用来指定顶点缓冲区相关的顶点数据,使用location的时候需要加上@符号前缀,@location()小括号里面设置参数。

main函数的参数@location(0)表示你GPU显存中标记为0的顶点缓冲区中顶点数据。

@vertex
fn main(@location(0)){
}

执行@location(0) pos给main函数参数@location(0)表示的顶点数据设置一个变量名pos。

@vertex
fn main(@location(0) pos){
}

顶点变量的数据类型

可以用三维向量vec3的三个分量表示顶点的x、y、z坐标。

执行@location(0) pos: vec3<f32>给main函数参数pos设置数据类型,vec3表示pos变量的数据类型是三维向量vec3<f32>表示三维向量x、y、z四个属性的值都是32位浮点数。

@vertex
fn main(@location(0) pos: vec3<f32>){
}

注意@location(0)对应WebGPU传过来的顶点是三个为一组,所以顶点着色器代码中pos变量的数据类型,用三维向量表示,如果WebGPU中传过来的顶点数据,两个为一组,比如只有x和y坐标,没有z坐标,书写形式就是@location(0) pos: vec2<f32>

vec3顶点坐标转vec4齐次坐标

在WGSL顶点着色器代码中,很多时候会用四维向量vec4表示顶点的位置坐标,vec4第四个分量默认值一般是1.0,vec4相比vec3多了一个分量,你可以把vec4形式的坐标称为齐次坐标,是WGSL内部一个常用语法形式格式。

@vertex
fn main(@location(0) pos: vec3<f32>){
    var pos2 = vec4<f32>(pos,1.0);//pos转齐次坐标
}

顶点计算后,return返回顶点数据

实际开发,一般会在main函数中,进行顶点计算,具体说就是,对顶点的坐标进行几何变换,比如平移、缩放、旋转等操作。

@vertex
fn main(@location(0) pos: vec3<f32>){
    var pos2 = vec4<f32>(pos,1.0);
    pos2.x -= 0.2;//偏移所有顶点的x坐标
}

渲染管线是一条流水线,顶点着色器处理好的顶点数据,最后需要通过关键字return返回,这样渲染管线的下个环节,就可以使用了。

@vertex
fn main(@location(0) pos: vec3<f32>){
    var pos2 = vec4<f32>(pos,1.0);
    pos2.x -= 0.2;
    return pos2;//返回顶点数据,渲染管线下个环节使用
}

如果你不需要在GPU顶点着色器中对顶点坐标进行变换,可以直接return返回即可

@vertex
fn main(@location(0) pos: vec3<f32>){
    return vec4<f32>(pos,1.0);//返回顶点数据,渲染管线下个环节使用
}

函数返回值数据类型

main函数return返回的变量,需要通过->符号设置函数返回值的数类类型, -> vec4<f32>表示函数返回的变量是浮点数构成的四维向量vec4

@vertex
fn main(@location(0) pos: vec43<f32>) -> vec4<f32>{
    return vec4<f32>(pos,1.0);//返回顶点数据,渲染管线下个环节使用
}

内置变量position@builtin关键字

position是WGSL语言的一个内置变量,所谓内置变量,就是说WGSL默认提供的变量,你不通过关键字var声明就可以使用。WGSL有很多内置变量,不同的内置变量有不同的含义,内置变量position表示顶点数据。

builtin是WGSL语言的一个关键字,使用location的时候需要加上@符号前缀,@location()小括号里面设置参数,参数一般是WGSL的某个内置变量,换句话就是当你使用内置变量的时候,一般需要通过@location()标记。

main函数的返回是顶点数据,这时候除了设置返回值数据类型,还需要设置@builtin(position),表明返回值是顶点位置数据。

@vertex
fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32>{
    return vec4<f32>(pos,1.0);//返回顶点数据,渲染管线下个环节使用
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ujkUJ7xs-1686833138852)(/imgwegpu/图元装配光栅化.png)]

本节课完成的一个最简单顶点着色器代码

后面课程讲解,会经常在此代码基础上增删代码,第一次学习,没有记住顶点着色器全部代码也没关系,初学者会在本代码基础增删代码即可。

const vertex = `
@vertex
fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32>{
    return vec4<f32>(pos,1.0);
}
`

7小节代码体验测试

通过上面学习,你对顶点着色器代码的功能也有了一定了解,你可以在7小节完整代码基础上,改变顶点位置坐标,体验测试,这样印象更加深刻。

着色器代码块方法.createShaderModule()

shader.js文件中顶点着色器代码。

// 顶点着色器代码
const vertex = `
@vertex
fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
    return vec4<f32>(pos,1.0);
}
`
export { vertex }

通过GPU设备对象的.createShaderModule()方法,把顶点着色器代码转化为GPU着色器代码块对象。

// 引入顶点着色器vertex代码对应字符串
import { vertex } from './shader.js'
// 字符串形式的顶点着色器代码作为code属性的值
device.createShaderModule({ code: vertex })

渲染管线参数vertex.module属性

把顶点着色器代码块对象device.createShaderModule({ code: vertex })作为渲染管线参数vertex.module属性的值,这样就可以配置好渲染管线上顶点着色器功能单元,要执行的顶点着色器代码。

import { vertex } from './shader.js'
const pipeline = device.createRenderPipeline({
    vertex: {
        // 设置渲染管线要执行的顶点着色器代码
        module: device.createShaderModule({ code: vertex }),
        entryPoint: "main"
    },
});

entryPoint属性

实际开发中,一般需要通过entryPoint属性指定顶点着色器代码的入口函数,入口函数名字你可以自定义,课程中习惯性设置为main

const pipeline = device.createRenderPipeline({
    vertex: {
        module: device.createShaderModule({ code: vertex }),
        entryPoint: "main"//指定入口函数
    },
});
const vertex =  /* wgsl */`
@vertex
fn main(@location(0) pos: vec3<f32>) -> @builtin(position) vec4<f32> {
    return vec4<f32>(pos,1.0);
}
`

片元着色器、图元装配

上节课给大家讲解了WebGPU渲染管线上的顶点着色器功能单元,下面给大家讲解WebGPU渲染管线其他功能单元(图元装配、光栅化、片元着色器)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xC8nNGdC-1686833150547)(/imgwegpu/图元装配光栅化.png)]

primitive.topology(图元装配)

经过顶点着色器处理过的顶点数据,会进入图元装配环节,简单说就是如何通过顶点数据生成几何图形,比如三个点绘制一个三角形,两点可以绘制一条线段…

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hRjJFId2-1686833150548)(/imgwegpu/图元装配光栅化.png)]

通过渲染管线参数的primitive.topology属性可以设置WebGPU如何绘制顶点数据,下面随便列举即可。

triangle-list表示三个点为一组绘制一个三角形。

const pipeline = device.createRenderPipeline({
    primitive: {
        topology: "triangle-list",//绘制三角形
    }
});

line-strip表示把多个顶点首位相接连接(不闭合),三个坐标点可以绘制两条直线段。

const pipeline = device.createRenderPipeline({
    primitive: {
        topology: "line-strip",//多个定点依次连线
    }
});

point-list表示每个顶点坐标对应位置渲染一个小点

const pipeline = device.createRenderPipeline({
    primitive: {
        topology: "point-list",
    }
});

WebGPU光栅化、片元着色器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QYmkzHaq-1686833150548)(/imgwegpu/图元装配光栅化.png)]

光栅化,就是生成几何图形对应的片元,你可以把片元类比为图像上一个个像素理解,比如绘制绘制一个三角形,光栅化,相当于在三角形返回内,生成一个一个密集排列的片元(像素)。

经过光栅化处理得到的片元,你可以认为是一个没有任何颜色的片元(像素),需要通过渲染管线上片元着色器上色,片元着色器单元就像流水线上一个喷漆的工位一样,给物体设置外观颜色。

片元着色器WGSL操作片元(像素)

片元着色器和的顶点着色器类似,都是渲染管线上的一个着色器功能单元,可以执行WGSL语言编写的着色器代码。

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

@fragment

@fragment表示字符串fragment里面的代码是片元着色器代码,在GPU渲染管线的片元着色器单元上执行。

const fragment = `
@fragment
`

为了方便单独管理WGSL着色器代码,你可以创建一个shader.js文件,在里面写着色器代码。

// 顶点着色器代码
const vertex = /* wgsl */ `
@vertex
`
// 片元着色器代码
const fragment = /* wgsl */ `
@fragment
`
export { vertex, fragment }

fn关键字声明一个函数

fn关键字声明一个函数,命名为main,作为片元着色器代码的入口函数。

@fragment
fn main(){
}

处理片元像素值

顶点着色器代码用来计算顶点的坐标,片元着色器代码用来设置片元像素值。

可以用四维向量四个分量表示像素的RGBA四个分量,比如vec4<f32>(1.0, 0.0, 0.0, 1.0)表示把片元的元素像素值设置为红色,透明度为1.0。

和顶点着色器类似,片元着色器需要通过关键字return,把设置了颜色的片元像素数据传递到渲染管线下一个功能环节。

@fragment
fn main(){
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

片元着色器函数返回值设置@location(0) vec4<f32>

main函数return返回的变量,需要通过->符号设置函数返回值的数类类型, -> vec4<f32>表示函数返回的变量是浮点数构成的四维向量vec4

@fragment
fn main() -> vec4<f32> {
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

片元着色器中的@location(0)和前面顶点着色器中@location(0),虽然符号一样,但不是一回事,片元着色器中的@location(0)和顶点缓冲区中顶点数据也没关系。

通常渲染管线片元着色器输出的片元像素数据,会存储在显卡内存上,@location(0)含义你就简单理解为输出的片元数据存储到显卡内存上,并把存储位置标记为0,用于渲染管线的后续操作和处理。

@fragment
fn main() -> @location(0) vec4<f32> {
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zh7Ffo2V-1686833150548)(/imgwegpu/渲染管线最简单的图案.png)]

渲染管线参数fragment.module属性

把顶点着色器代码块对象device.createShaderModule({ code: fragment })作为渲染管线参数fragment.module属性的值,这样就可以配置好渲染管线上片元着色器功能单元,要执行的片元着色器代码。

import { vertex, fragment } from './shader.js'
const pipeline = device.createRenderPipeline({
    fragment: {//片元相关配置
        // module:设置渲染管线要执行的片元着色器代码
        module: device.createShaderModule({ code: fragment }),
    },
});

entryPoint属性

顶点着色器或片元着色器一般需要通过entryPoint属性指定入口函数,入口函数名字你可以自定义,课程中习惯性设置为main

const pipeline = device.createRenderPipeline({
    vertex: {
        module: device.createShaderModule({ code: vertex }),
        entryPoint: "main"//指定入口函数
    },
    fragment: {
        module: device.createShaderModule({ code: fragment }),
        entryPoint: "main",//指定入口函数
    }
});

fragment.targets的元素的format属性

//获取浏览器默认的颜色格式
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
    device: device,
    format: format,//颜色格式
});
const pipeline = device.createRenderPipeline({
    fragment: {
        module: device.createShaderModule({ code: fragment }),
        entryPoint: "main",
        targets: [{
            format: format//和WebGPU上下文配置的颜色格式保持一致
        }]
    }
});

layout属性

在旧版本WebGPU中,如果你用不到layout特定功能,可以不用设置,不过在新版本WebGPU,是必须设置的,否则报错。入门案例中,咱们不需要对layout属性进行特殊设置,先使用默认值layout: 'auto'就行。

const pipeline = device.createRenderPipeline({
    layout: 'auto',
});

渲染命令(完成第一个案例的渲染

在前面几节课基础上,本节课通过设置一些渲染命令,最终完成第一个WebGPU小案例。本案例虽然非常简单,但是麻雀虽小,五脏俱全,后面的课程都可以在本节课的基础上给大家讲解。

你可以把本节课的小案例,当做一个学习模板,再次基础上增删代码,学习体验WebGPU的各种知识点。

创建命令编码器和渲染通道

首先通过GPU设备对象的方法.createCommandEncoder()创建一个命令编码器对象。

// 创建GPU命令编码器对象
const commandEncoder = device.createCommandEncoder();

通过命令对象的方法.beginRenderPass()可以创建一个渲染通道对象renderPass

const renderPass = commandEncoder.beginRenderPass({
    // 需要配置一些参数
});

通过GPU命令编码器对象commandEncoder可以控制渲染管线pipeline渲染输出像素数据。

前面讲过的一些控制webgpu API,默认不会直接执行,如果想在GPU上执行,还需要配置GPU命令编码器对象commandEncoder实现。

[外链图片转存中…(img-L9hfW19J-1686833162083)]

颜色缓冲区的概念

通过WebGPU渲染管线各个功能处理后,会得到图形的片元数据,或者说像素数据,这些像素数据,会存储到显卡内存颜色缓冲区中。

你可以类比顶点缓冲区和理解颜色缓冲区,顶点缓冲区的功能是存储顶点数据,颜色缓冲区的功能是存储渲染管线输出的像素数据。

颜色缓冲区和顶点缓冲区类似,可以创建,不过有一个比较特殊,就是canvas画布对应一个默认的颜色缓冲区,可以直接使用。

如果你希望webgpu绘制的图形,呈现在canvas画布上,就要把绘制的结果输出到canvas画布对应的颜色缓冲区中。

[外链图片转存中…(img-zxl5RzBY-1686833162084)]

.beginRenderPass的参数对象

.beginRenderPass的参数对象具有多个属性,比如常用的colorAttachments(颜色附件)depthStencilAttachment(深度/模板附件)…本节课先给大家介绍其中一个颜色附近属性colorAttachments

首先大家要知道渲染通道renderPass可以控制渲染管线pipeline渲染输出像素数据,输出的像素数据会存储到GPU设备的颜色缓冲区中。

colorAttachments属性就和颜色缓冲区有关,colorAttachments属性的值是数组,数组里面的元素是对象,可以包含多个对象,每个对象的都和一个颜色缓冲区相关,每个对象具有viewloadOpstoreOpclearValue等属性。

当我们需要把渲染管线的像素数据存储到多个颜色缓冲区时,colorAttachments的属性值才需要设置多个元素对象,一般情况下,colorAttachments的数组元素只需要设置一个即可,这样的话,渲染通道控制渲染管线输出的像素最终就会存储到该数组元素对应颜色缓冲区。

const renderPass = commandEncoder.beginRenderPass({
    // 给渲染通道指定颜色缓冲区,配置指定的缓冲区
    colorAttachments:[{
        // 指向用于Canvas画布的纹理视图对象(Canvas对应的颜色缓冲区)
        // 该渲染通道renderPass输出的像素数据会存储到Canvas画布对应的颜色缓冲区(纹理视图对象)
        view: context.getCurrentTexture().createView(),  
        storeOp: 'store',//像素数据写入颜色缓冲区
        loadOp: 'clear',
        clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, //背景颜色
    }]
});

设置渲染通道的渲染管线

实际开发,可能有一个渲染管线,也可能有多个,你可以根据需要,通过渲染通道renderPass的方法.setPipeline()设置你要控制的渲染管线。

// const pipeline = device.createRenderPipeline()
// 设置该渲染通道控制渲染管线
renderPass.setPipeline(pipeline);

通过GPU命令编码器对象commandEncoder可以根据需要创建多个渲染通道,每个通道都可以控制自己对应的的渲染管线输出图像。不过咱们入门部分案例,比较简单,只是创建一个渲染通道而已。

关联顶点缓冲区数据和渲染管线shaderLocation: 0

顶点缓冲区数据和渲染管线shaderLocation: 0表示存储位置关联起来

renderPass.setVertexBuffer(0, vertexBuffer);

[外链图片转存中…(img-r9lUUN1A-1686833162084)]

补充(后面会讲解):实际开发,可以通过device.createBuffer创建多个顶点缓冲区,第一个案例,只有一个顶点缓冲区.setVertexBuffer()的参数1设置为0即可,如果有多个,可以设置为0、1、2、3等,后面遇到再具体讲解。

绘制命令.draw()

渲染通道对象renderPass提供了一个方法.draw(),英文字面意思就是绘制,你也可以把绘制方法.draw()称为绘制命令。通过绘制命令.draw(),你可以命令渲染通道对应的WebGPU的渲染管线如何绘制你定义的顶点数据。

// renderPass.setPipeline(pipeline);
// 绘制命令.draw()绘制顶点数据
renderPass.draw(3);

注意顺序:调用.draw()之前要设置渲染管线,否则报错。

渲染通道结束命令.end()

渲染通道对象renderPass.end()方法比较简单,就是字面意思结束,不用设置参数,一般你设置好绘制等命令后,需要设置renderPass.end()

// 渲染通道结束命令.end()
renderPass.end();

执行renderPass.end(),系统内部会标记前渲染通道renderPass已经结束。

命令编码器方法.finish()

在前面代码调用的WebGPU API或者说方法,大部分都是用来控制GPU如何运行的,比如device.createRenderPipeline()就是控制GPU创建一个渲染管线,比如.draw方法,控制GPU如何绘制顶点数据,不过这些WebGPU API或方法不能直接控制GPU的运行,需要转化(编码)为GPU指令(命令),才能控制GPU运转。

命令编码器对象commandEncoder执行.finish()方法返回一个命令缓冲区对象,同时会把该编码器相关的WebGL API或方法,编码为GPU指令,存入到返回的命令缓冲区对象中。

// const commandEncoder = device.createCommandEncoder();
// 命令编码器.finish()创建命令缓冲区(生成GPU指令存入缓冲区)
const commandBuffer = commandEncoder.finish();

[外链图片转存中…(img-eHhiTbgB-1686833162084)]

GPU设备命令队列.queue属性

GPU设备命令队列.queue的功能是用来存放控制GPU运转的指令(命令),简单说就是你命令编码器和渲染通道定义的一系列控制GPU运行的命令方法。

.submit()是GPU设备对象device队列属性.queue的一个提交方法。

提交方法.submit()的参数是一个数组,数组的元素是命令编码器执行.finish()生成的GPU命令缓冲区对象commandBuffer,数组元素可以包含多个命令缓冲区对象,入门案例比较简单,只添加了一个。

// const commandEncoder = device.createCommandEncoder();
const commandBuffer = commandEncoder.finish();
// 命令编码器缓冲区中命令传入GPU设备对象的命令队列.queue
device.queue.submit([commandBuffer]);

[外链图片转存中…(img-1yaJBWI9-1686833162084)]

在执行.queue.submit([])方法之前,WebGPU相关命令方法,还不会被GPU硬件执行。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Threejs可视化

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值