检测浏览器是否支持webgpu
chrome 113版本以上已经完全支持
//包含该属性就证明支持
console.log(navigator.gpu);
渲染流程
常见api
device.createRenderPipeline();//创建渲染管线
device.createComputePipeline();//创建计算管线
device.createShaderModule();//创建着色器模块
device.createCommandEncoder();//创建命令对象(绘制和计算命令)
device.createBuffer();//创建缓冲区
通过顶点缓冲区创建顶点数据
- usage的属性值可以是下面两种
- GPUBufferUsage.VERTEXT : 表示用于该缓冲区是顶点缓冲区,就算存储顶点数据的缓冲区
- GPUBufferUsage.COPY_DST : 表示该缓冲区可以写入顶点数据,作为复制顶点数据的目的地
//渲染一个物体,需要先通过顶点坐标来定义该物体的几何形状
//通过顶点缓冲区创建顶点数据
const vertexBuffer = device.createBuffer({
size: vertextArray.byteLength,//根据字节长度,此时是4*9=36字节
//缓冲区用途:作为顶点缓冲区|可以写入顶点数据
usage: GPUBufferUsage.VERTEXT | GPUBufferUsage.COPY_DST
});
webgpu的着色器语言是wgsl
wgsl基础类型
bool 布尔
u32 无符号整数
i32 有符号整数
f32 32位浮点数
f16 16位浮点数
wgsl可以用var声明变量
var a:u32;
a=2;
var b:f16=1.0;
wgsl类型推断
var c=20;
两个变量进行运算,需要保持一样的数据类型
函数
fn add(x:f32,y:32)->f32{
return x+y;
}
if语句
var a:f32=2.0;
if(a>10.0){
}
for语句
var n:u32=10;
for(var i:u32=0;i<n;i++){
s+=0.05;
}
向量表示颜色
四维向量有四个分量,可以用来表示颜色的RGBA
var color:vec4<f32>=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);
等价于vec4<f32>(1.0,2.0,3.0,1.0);
三维向量转四维向量
var pos2=vec4<f32>(pos,1.0)
二维向量转四维向量
var pos2:vec2<f32>(1.0,2.0);
var pos4:vec4<f32>(pos2,3.0,4.0);
结构体
struct pointLight{
color:vec3<f32>,
intensity:f32
}
var light1:pointLight;
light1.color=vec3<f32>(1.0,0.0,0.0);
light1.intensity=0.6;
颜色缓冲区
通过WebGPU渲染管线各个功能处理后,会得到图形的片元数据,或者说像素数据,这些像素数据,会存储到显卡内存颜色缓冲区中。
你可以类比顶点缓冲区和理解颜色缓冲区,顶点缓冲区的功能是存储顶点数据,颜色缓冲区 的功能是存储渲染管线输出的像素数据。
颜色缓冲区和顶点缓冲区类似,可以创建,不过有一个比较特殊,就是canvas画布对应一个默认的颜色缓冲区,可以直接使用。
如果你希望webgpu绘制的图形,呈现在canvas画布上,就要把绘制的结果输出到canvas画布对应的颜色缓冲区中。
重点:其实存储就是片元着色器处理生成的一个个片元,只不过这个缓冲区不需要创建,有默认的
如果不想让颜色显示出来,则可以手动创建一个颜色缓冲区,把片元着色器生成的片元数据存储在自己创建的颜色缓冲区中
webgpu坐标系
坐标原点在canvas画布的中间位置,x轴向右,y轴向上,z轴垂直画布向屏幕内;x和y的范围是[-1,1],z是[0,1]
对于webgpu坐标系,图形学中称为标准化设备坐标系(简称NDC),因为坐标范围是
-1~1
或0~1
的相对值,所以把NDC
称为归一化设备坐标系
也行。
webgpu默认(注意是默认)的渲染规律是,渲染的xyz如果超出了范围(x和y的范围是[-1,1],z是[0,1]),会被裁剪
那么默认情况下,WebGPU会如何渲染上面顶点坐标定义的三角形?
为了更好理解,假设在WebGPU的3D空间中,存在一束平行光线,沿着z轴照射到XOY平面(实际上就是x0y)上,这时候3D空间中的三角形会在XOY平面上产生投影,就像生活中,人在太阳光 下,会地面上产生投影。
这时候,z轴上的任何顶点,投影后,其实都在坐标原点,这样上面一个等边三角形,三个点投影后,就是两个点在x和y轴, z轴上的点投影到坐标原点,这样三个点连接起来,渲染的投影结果就是一个直角三角形。
矩阵补充
上图中坐标平移的实现;内部的一些过程可以说明,为什么有齐次坐标
而这些变换(平移旋转缩放)又为什么用矩阵计算
旋转矩阵
模型矩阵
在图形学中经常会提到模型矩阵的概念,其实模型矩阵就是平移/缩放/旋转矩阵的统称;或者说是平移/缩放/旋转矩阵相乘得到的复合矩阵
几何变换顺序对结果的影响?
假设一个顶点原始坐标(2,0,0)。
先平移2、后缩放10:如果先沿着x轴平移2,变为(4,0,0),再x轴方向缩放10倍,最终坐标是(40,0,0)。
先缩放10、后平移2:如果先x轴方向缩放10倍,变为(20,0,0),再沿着x轴平移2,最终坐标 是(22,0,0)。
可以发现上面同样的平移和缩放,顺序不同,变换后的顶点坐标也不相同。
以矩阵乘法不符合一般乘法的(交换律)
模型矩阵:先计算所有几何变化对应的矩阵的乘积,得到一个模型矩阵,再对顶点坐标(转换为齐次坐标)进行变换。
一般不推荐每次变换都和顶点坐标进行计算;但是这种计算的话,矩阵和齐次坐标的计算靠近顺序参考图9
单位矩阵
计算开源库gl-matrix
webgpu计算理论上不需要自己编写算法,可以借助于开源库
gl-matrix
,提供了矩阵,向量,四元数等等与图形学相关的数学计算函数
[补充视频]https://www.bilibili.com/video/BV11M41137UH?p=12&vd_source=1fe868f5f3b4b37f6561498b82c13364
基础案例
<!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>Document</title>
</head>
<body>
<canvas id="webgpu" width="500" height="500"></canvas>
</body>
</html>
<script type="module">
import { vertex, fragment } from "./shader.js";
//浏览器请求gpu适配器
const adapter = await navigator.gpu.requestAdapter();
//获取gpu设备对象,通过gpu设备对象device的webgpu api可以控制gpu渲染过程
const device = await adapter.requestDevice();
const canvas = document.getElementById('webgpu');
const ctx = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();//获取浏览器默认的颜色格式
ctx.configure({
device, //webgpu渲染器使用的gpu设备对象
format
});
//类数组
const vertextArray = new Float32Array([
0.0, 0.0, 0.0,//顶点1坐标
1.0, 0.0, 0.0,
0.0, 1.0, 0.0
])
//渲染一个物体,需要先通过顶点坐标来定义该物体的几何形状
//通过顶点缓冲区创建顶点数据
const vertexBuffer = device.createBuffer({
size: vertextArray.byteLength,//根据字节长度,此时是4*9=36字节
//缓冲区用途:作为顶点缓冲区|可以写入顶点数据
usage: GPUBufferUsage.VERTEXT | GPUBufferUsage.COPY_DST
});
/**
* usage的属性值可以是下面两种
* GPUBufferUsage.VERTEXT : 表示用于该缓冲区是顶点缓冲区,就算存储顶点数据的缓冲区
* GPUBufferUsage.COPY_DST : 表示该缓冲区可以写入顶点数据,作为复制顶点数据的目的地
**/
//顶点数据写入顶点缓冲区:0 表示从vertextArray的数据开头开始读取数据
device.queue.writeBuffer(vertexBuffer, 0, vertextArray)
//创建渲染管线
//渲染管线类似于工厂流水线,流水线上不同的功能单元,完成不同的零部件生产,渲染管线上面提供用于3D渲染的不同功能单元
//根据需要可以创建多个渲染管线
const pipeline = device.createRenderPipeline({
layout: 'auto',//默认这么写即可
vertex: {
//顶点着色器
buffer: [//顶点所有的缓冲区模块设置
{//其中一个顶点缓冲区的设置
arrayStride: 3 * 4,//一个顶点的字节长度
attributes: [{//顶点缓冲区属性
shaderLocation: 0,//GPU显存上顶点缓冲区标记存储位置,例如,当前buffer内部可以是多个缓冲区,此处是标记
format: "float32x3",//表示一个顶点包含3个浮点数
offset: 0,//表示arrayStride每组顶点数据间隔字节数
}]
}
],
module: device.createShaderModule({ code: vertex }),//顶点着色器代码
entryPoint: "main",//指定着色器代码的入口函数
},
fragment: {
//片元着色器
module: device.createShaderModule({ code: fragment }),
entryPoint: "main",
targets: [
{
format,//和webgpu上下文配置的颜色格式保持一致
}
]
},
//这个和图元装配相关,指定绘制的形式
primitive: {
//绘制三角形,线条,点
topology: "triangle-list",//三角形绘制顶点数据,如果顶点数超过三个,则三个为一组画一个三角形
//line-strip :多个定点依次连线(不闭合)
//point-list : 每个顶点坐标对应位置渲染一个小点
}
});
//创建命令编码器对象:commandEncoder可以控制渲染管线pipeline渲染输出像素数据
const commandEncoder = device.createCommandEncoder();
//创建渲染通道对象
//之前的webgpu api默认不会执行,还需要配置GPU命令编码器对象commandEncoder实现
//beginRenderPass中参数很多,例如colorAttachments(颜色附件),depthStencilAttachment(深度/模板附件)
//colorAttachments属性就和颜色缓冲区有关,属性值是数组,数组中元素是对象,每个对象和一个颜色缓冲区相关,每个对象具有view,loadOp,storeOp,clearValue等属性
//需要把渲染管线的像素数据存储到多个颜色缓冲区时候才需要colorAttachments设置多个元素对象,一般设置一个即可
const renderPass = commandEncoder.beginRenderPass({
colorAttachments:[
{
//指向用于canvas画布的纹理视图对象(canvas对应的颜色缓冲区)
//该渲染通道renderPass输出的像素数据会存储到canvas画布对应的颜色缓冲区中
view:ctx.getCurrentTexture().createView(),
storeOp:'store',//像素数据写入颜色缓冲区
loadOp:'clear',
clearValue:{r:0.5,g:0.5,b:0.5,a:1.0},//背景色
}
]
});
//设置该渲染通道对应的渲染管线
//可以根据需要创建多个渲染通道,每个渲染通道都可以控制自己对应的渲染管线输出对象
renderPass.setPipeline(pipeline);
//顶点缓冲区数据和渲染管线shaderLocation:0 bioassay存储位置关联起来
renderPass.setVertexBuffer(0,vertexBuffer);
//绘制顶点数据
renderPass.draw(3);//3是因为上面只有三个点
renderPass.end();//结束命令
//darw,createRenderPipeline等webgpu的api不能直接在GPU上面运行,需要转换为GPU指令(命令),才能控制GPU运转
//命令编码器.finish()创建命令缓冲区(生成GPU指令存入GPU命令缓冲区)
const commandBuffer=commandEncoder.finish();//返回一个命令缓冲区对象,同时编码api为GPU指令存入缓冲区
//命令编码器缓冲区中命令传入GPU设置对象的命令队列queue
device.queue.submit([commandBuffer]);//之所以是数组,因为命令缓冲区可能是多组
</script>
引入的wgsl代码文件,此处使用js导出字符串形式
//@vertex表示字符串里面的代码是顶点着色器代码,在GPU渲染管线的顶点着色器单元上执行
//fn声明一个函数,命令为main,作为顶点着色器代码的入口函数
//location是wgsl语言的一个关键字,通用用来指定顶点缓冲区相关的顶点数据,使用location时候需要加上@前缀,@location()
//小括号里面设置参数
//@location(0) 表示GPU显存中标记为0(shaderLocation)的顶点缓冲区中的顶点数据
//wgsl顶点着色器代码中,很多时候用四维向量vec4表示顶点的位置坐标,vec4第四个分量一般默认1.0
const vertex =/*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坐标,而不只是一个点,区别去js的语法
//return返回之后,渲染管线的下一个环节就可以使用了
return pos2;
}
`
//内置关键字
//position是wgsl语言的一个内置变量,表示顶点数据
//builtin,表示和其配合使用的是内置变量(例如position)
//使用时候前面要加@ 例如:@builtin(position);一般用于返回值类型,表示通知下一个环节,返回值是顶点位置坐标数据
//通常片元着色器输出的片元像素数据,会存储在显卡内存上,location(0)含义简单理解为输出的片元数据存储到显卡内存上
//并把存储位置标记为0(存储到0,然后供后续使用),用于渲染管线的后续操作和处理;也就是说此处的location(0)和顶点着色器中的location(0)不相干
const fragment=/*wgsl*/`
@fragment
fn main()->location(0) vec4<f32>{
return vec4<f32>(1.0,0.0,0.0,1.0);//片元设置为红色
}
`;
export {vertex,fragment}
为什么webgpu使用矩阵?
首先,3D的旋转平移缩放操作可以通过矩阵的乘法实现计算,不需要特殊的处理;齐次,gpu可以进行矩阵的并行计算,虽然单个计算速度小于cpu,但是总体速度更快。
比较 GPU 和 CPU ,就是比较它们两者如何处理任务。CPU 使用几个核心处理单元去优化串行顺序任务,而 GPU 的大规模并行架构拥有数以千计的更小、更高效的处理单元,用于处理多个并行小任务。
CPU 拥有复杂的系统指令,能够进行复杂的任务操作和调度,两者是互补关系,而不能相互代替。
GPU 是大规模并行架构,处理并行任务毫无疑问是非常快的,深度学习需要高效的矩阵操作和大量的卷积操作, GPU 的并行架构再适合不过。简单来说,确实如此,但是为什么 GPU 进行矩阵操作和卷积操作会比 CPU 要快呢?
真正原因是 GPU具有如下特性 :
(1) 高带宽
(2) 高速的缓存性能
(3) 并行单元多
在执行多任务时, CPU 需要等待带宽,而 GPU 能够优化带宽。
举个简单的例子,我们可以把 CPU 看作跑车, GPU 是大卡车,任务就是要把一堆货物从北京搬运到广州。 CPU(跑车〉可以快速地把数据(货物〉从内存读入 RAM 中,然而 GPU (大卡车〉装货的速度就好慢了。不过后面才是重点, CPU (跑车)把这堆数据(货物)从北京搬运到广州|需要来回操作很多次,也就是往返京广线很多次,而 GPU (大卡车)只需要一 次就可以完成搬运(一次可以装载大量数据进入内存)。换言之, CPU 擅长操作小的内存块,而 GPU 则擅长操作大的内存块 。 CPU 集群大概可以达到 50GB/s 的带宽总量,而等量的 GPU 集群可以达到 750GB/s 的带宽量。
如果让一辆大卡车去装载很多堆货物,就要等待很长的时间了,因为要等待大卡车从北京运到广州,然后再回来装货物。设想一下,我们现在拥有了跑车车队和卡车车队(线程并行〉,运载一堆货物(非常大块的内存数据需要读入缓存,如大型矩阵)。我们会等待第一辆卡车,但是后面就不需要等待的时间了,因为在广州会有一队伍的大卡车正在排队输送货物(数据),这时处理器就可以直接从缓存中读取数据了。在线性并行的情况下, GPU 可以提供高带宽,从而隐藏延迟时间。这也就是GPU 比 CPU 更适合处理深度学习的原因。