背景
WebGL的问题
WebGPU 出现之前,浏览器中主流的3D渲染解决方案是WebGL。WebGL 基于 OpenGL ES,由非盈利技术联盟 Khronos Group 负责维护。由于 OpenGL 是上世纪90年代提出的图形API,很多设计理念已经不再适用于现代显卡。后面微软提出了自己的图形 API Direct3D 12,苹果提出了 Metal,Khronos 组织也提出了新的图形 API Vulkan。在此基础上 W3C 组织从2017年开始,将上述三个现代图形 API 的 web 版本合并,并命名为 WebGPU。
WebGPU的提出
WebGPU与WebGL 一样为前端提供了3D 图形渲染能力。不同的是 WebGPU 提出了专门用于并行计算的 Compute Shader,这使得WebGPU 能够更好地支持显卡的通用并行计算能力。除此之外得益于 WebGPU 能更好地发挥现代显卡的性能,WebGPU 对复杂场景的渲染性能也是优于 WebGL 的。下图是工程师郝稼力曾在 GMTC 全球大前端技术大会上分享的通过 WebGL 和 WebGPU 在2070显卡上渲染1000颗树时的性能对比
可以看到无论是渲染帧率还是渲染每帧的 CPU 耗时,WebGPU 在复杂场景下都远优于 WebGL。但笔者在个人电脑上测试发现对于 drawcall 次数多的场景,WebGPU 的优势并不大。
目前Chrome@114 版本以及 Chrome Canary 已经支持了 WebGPU。
WebGPU 示例
WebGPU 渲染管线和 WebGL 一样分为应用阶段(CPU)、几何阶段和光栅化阶段。开发者主要通过几何阶段的顶点着色器和光栅化阶段的片元着色器来完成渲染的实现。由于目前一些流行的开源库(babylon.js/three)已经实现或者正在实现对 WebGPU 的 API 封装,所以后面是用开源的3D 渲染库后可能对 WebGPU 的 API 接触不会太多。所以就用下面一个简单的 例子介绍一下 WebGPU。
javascript
//为了不阻塞浏览器主线程,WebGPU的 API 主要都是异步实现。为了代码可读性,是用 async/await
async function main(canvas) {
//获取 WebGPU 的 adapter,adapter 不可直接操作,一个 adapter 是一个 GPU 的逻辑抽象。
const adapter = await navigator.gpu?.requestAdapter()
//获取 device,一个 device 是 GPU 功能的逻辑抽象集合。同一页面中可以创建多个 device,device 管理了其创建的 buffer、texture 等对象,从这一点上看 device 类似于 WebGLRenderingContext 对象。但不同的是,device 并不依赖于canvas。实际上 WebGPU 渲染中,canvas 并不是必须的,只是充当了用于输出的纹理。
const device = await adapter?.requestDevice()
if (!device) {
fail('need a browser that supports WebGPU')
return
}
// Get a WebGPU context from the canvas and configure it
const context = canvas.getContext('webgpu')
const presentationFormat = navigator.gpu.getPreferredCanvasFormat()
console.log(presentationFormat)
context.configure({
device,
format: presentationFormat
})
//shaderModule 对象中可以包含一个或多个 shader 函数
const module = device.createShaderModule({
//label 用于 debug 时的方便开发者定位问题来源
label: 'our hardcoded red triangle shaders',
code: `
//顶点着色器代码,是用 WGSL 语言编写。方法 vs()参数为内建变量vertex_index,表示当前顶点着色器运行的顶点索引
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
//着色器代码中写死的顶点坐标信息。坐标(0,0)位于 canvas 中心,(1,1)位于 canvas 右上角
var pos = array<vec2f, 3>(
vec2f( 0.0, 0.8), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
//顶点着色器返回一个由四位浮点数组成的向量。该向量会被赋值给内建变量 position。在默认的绘制模式'triangle-list'下。GPU 会将相邻的三个 position 连起来形成一个三角形
return vec4f(pos[vertexIndex], 0.0, 1.0);
}
//片元着色器代码,@location(0)表示片元着色器将输出写入第一个 render target
@fragment fn fs() -> @location(0) vec4f {
return vec4f(1, 0, 0, 0.3);
}
`
})
//创建 WebGPU 的渲染管线,也可以创建计算管线。pipeline 描述了我们使用哪些着色器来完成渲染或者计算任务。
const pipeline = device.createRenderPipeline({
label: 'our hardcoded red triangle pipeline',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs'
},
fragment: {
module,
entryPoint: 'fs',
targets: [{format: presentationFormat}]
}
})
//renderPassDescriptor.colorAttachments 对象描述了 WebGPU 的输出纹理。
const renderPassDescriptor = {
label: 'our basic canvas renderPass',
colorAttachments: [
{
// view: <- to be filled out when we render
clearValue: [0.3, 0.3, 0.3, 1],
loadOp: 'clear',//绘制前使用 clearValue清空纹理
storeOp: 'store'
}
]
}
function render() {
// Get the current texture from the canvas context and
// 将 canvas 作为第一个输出纹理,对应了片元着色器函数的输出对象@location(0)
renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView()
// make a command encoder to start encoding commands
const encoder = device.createCommandEncoder({label: 'our encoder'})
// make a render pass encoder to encode render specific commands
const pass = encoder.beginRenderPass(renderPassDescriptor)
pass.setPipeline(pipeline)
pass.draw(3) // call our vertex shader 3 times.
pass.end()
const commandBuffer = encoder.finish()
device.queue.submit([commandBuffer])
}
render()
}
export default main
上面的代码是从 webgpu.fundamentals.org 上拷贝的。展示了 webgpu 绘制一个三角形的示例。由于三角形的坐标数据写死在里顶点着色器中,所以该示例没有用到 vertexBuffer。
WGSL 介绍
由于 WebGPU 的着色器要通过 WGSL 来实现,后面就算使用开源的3D 引擎后,也需要开发者自己实现大部分的 shader。所有 WGSL 语言是必须要熟悉的。
WGSL 全称为 WebGPU Shading Language,为专门给 WebGPU 定制的在 GPU 中执行的着色器语言,相当于 GLSL 之于 WebGL。与前端 Javascript 语言最大的不同在于,WGSL 是一门静态语言,所有变量都需要先定义变量的类型再使用。大家可以在 google.github.io/tour-of-wgs... 上测试 WGSL 的语法。
变量类型
-
基础类型
-
i32 - 四字节长度的有符号整数.
-
u32 - 四字节长度的无符号正数.
-
f32 - 四字节长度的IEEE754浮点数.
-
bool
-
f16 - 两字节长度的浮点数,optional feature
-
向量类型
WGSL 支持二维vec2、三维 vec3和四维 vec4向量。向量类型的变量定义时类似 TypeScript 中的泛型变量的定义。
let a = vec2(1, -2); //a 为有符号整数的二维向量
let b = vec3(3.4, 5.6, 7.8);//b 为浮点数的三维向量
let c = vec4(9, 10); //c 为无符号整数的四维向量
各个类型向量的缩写为
-
vec2 -> vec2i
-
vec2 -> vec2u
-
vec2 -> vec2f
-
vec2 -> vec2h
-
矩阵类型
WGSL 支持从 mat2x2到mat4x4之间所有维度的矩阵定义。矩阵为列主序。通过语法 matx<> 定义矩阵
rust
const zero_init = mat3x4f();//定义了一个4行3列的0矩阵
const column_wise = mat3x2f(vec2f(1, 2), vec2f(3, 4), vec2f(5, 6));//定义了一个由三个列向量组成的矩阵
const scalar_wise = mat2x3f(1, 2, 3, 4, 5, 6);//定义了一个2行3列的矩阵
- 数组
WGSL 中通过语法 array<type, numElements> 来定义数组
- 结构体
语法和 C 语言的 struct 类似。
rust
struct A{
a: u32,
b: f32
}
const a = A(); // a.a:0, a.b: 0
const b = A(10, 11.5); //b.a:10, b.b: 11.5
函数
函数定义
函数定义语法为 fn name(parameters) -> returnType { ..body... }
rust
fn do_nothing() {
}
fn eat_an_i32(my_param : i32) {
}
fn give_me_a_number() -> i32 {
return 42;
}
fn average(a : f32, b : f32) -> f32 {
return (a + b) / 2;
}
函数调用
-
WGSL 函数可以在函数声明前调用
-
WGSL 函数不能递归调用
entry points
每个着色器代码都需要指定entry point,意思是该着色器的入口函数。用@vertex,@fragment 或者@compute在函数开头指定。着色器代码中可以指定多个函数为 entry point,但在创建 pipeline 时需指定一个 entry point
@must_use
用@must_use 修饰的方法,其返回值再调用时必须被使用,否则会编译报错
变量声明关键字 let var const
虽然名称和 JavaScript 中的关键字一样。但 WGSL 中的 let var const 作用和 JS 的差别很多
-
var 用于定义可变变量
-
let 用于定义不可变变量,即常量
-
const 用于定义编译时常量,与用 let 定义的变量不同的是,const 定义的常量值不能再运行时确定,只能在编译时确定。作用类似于C 语言和 GLSL 里的宏
rust
const one = 1; // ok
const two = one * 2; // ok
const PI = radians(180.0); // ok
fn add(a: f32, b: f32) -> f32 {
const result = a + b; // ERROR! const can only be used with compile time expressions
return result;
}
控制流
for 循环
rust
for (var i = 0; i < 10; i += 1) {
}
if else
rust
if (i < 5) {
...
} else if (i > 7) {
..
} else {
...
}
while
c
var j = 0;
while (j < 5) {
...
j++;
}
loop
c
var k = 0;
loop {
k++;
if (k >= 5) {
break;
}
}
break
c
var k = 0;
loop {
k++;
if (k >= 5) {
break;
}
}
var k = 0;
loop {
k++;
break if (k >= 5);
}
continue
c
for (var i = 0; i < 10; ++i) {
if (i % 2 == 1) {
continue;
}
...
continuing {
// continue goes here
...
}
}
switch
c
var a : i32;
let x : i32 = generateValue();
//x 和 case 的判断条件必须是 i32或者 u32 case 的判断条件必须是 constant
switch x {
case 0: {
a = 1;
}
default { //default 必需且唯一
a = 2;
}
case 1, 2, {
a = 3;
}
case 3, {
a = 4;
}
case 4 {
a = 5;
}
}
inter-stage 变量
inter-stage 变量用于顶点着色器的输出和片元着色器的输入
c
//定义一个结构体描述 vs 输出和 fs 输出变量
struct OurVSOutput {
@builtin(position) position: vec4f, //@builtin 修饰的变量为 wgsl 内置变量
@location(0) color: vec4f //@location(0)暂时可以理解为编号为0的通道。inter-stage 变量需要靠 location(index)标识。变量的名称在不同着色器之间可以不同,但变量类型和@location(index)必须一致
}
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> OurVSOutput {
var pos = array<vec2f, 3>(
vec2f( 0.0, 0.8), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
let color = array<vec4f, 3>(
vec4f(1, 0, 0, 1),
vec4f(0, 1, 0, 1),
vec4f(0, 0, 1, 1)
);
var vsOutput: OurVSOutput;
vsOutput.position = vec4f(pos[vertexIndex], 0.0, 1.0);
vsOutput.color = color[vertexIndex];
return vsOutput;
}
//fs 的输入设置为 OurVSOutput 类型,其实也可以定义为
//fs(@location(0) diffuse: vec4f) -> @location(0)
//OurVSOutput.position 在 fs 中代表的是像素坐标,和 vs 中的@builtin(position)含义不一样
@fragment fn fs(fsInput: OurVSOutput) -> @location(0) vec4f {
return fsInput.color;
}
渲染效果为:
Uniforms
shader 的全局变量
着色器代码
rust
struct OurStruct {
color: vec4f,
scale: vec2f,
offset: vec2f,
}
//let 声明符只能在函数作用域使用 var<uniform> 表示 ourStruct 变量为 uniform buffer 变量
//可共享,可读不可写
@group(0) @binding(0) var<uniform> ourStruct: OurStruct;
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
var pos = array<vec2f, 3>(
vec2f( 0.0, 0.8), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
return vec4f(pos[vertexIndex] * ourStruct.scale + ourStruct.offset, 0, 1);
}
@fragment fn fs() -> @location(0) vec4f {
return ourStruct.color;
}
-
上面shader 代码中创建了OurStruct 结构体,用于定义 uniform 变量的类型
-
顶点着色器中通过读取 OurStruct 中的 scale 和 offset 设置三角形的缩放和位移。像素着色器中读取 color 设置三角形的颜色。
-
uniform 变量 ourStruct 虽然定义为了一个可变变量,但在着色器中是不允许修改 uniform 变量的值的。
-
@group(0) @binding(0) 修饰符表示着色器需要从第0个 bindingGroup 里的第0个 binding 处读取这个 uniform 变量。关于group和 binding
上图可以看到,着色器主要通过 bindGroup 对象访问uniform buffer、storage buffer、纹理以及 sampler等外部资源。一个 bindGroup 可以通过绑定多个 buffer,而每次渲染可以创建多个 bindGroup 对象。
- 和 webgl 的着色器代码不同的是,顶点着色器和像素着色器可以不用单独声明同一个 uniform 变量
js 代码(重点部分)
js
//color 16字节 + scale 8字节 + offset 8字节,一共需要一个32字节长度的 buffer 来存放 uniform
const uniformBufferSize = 4 * 4 + 2 * 4 + 2 * 4
//uniformBuffer 是一个 GPUBuffer,用于存放color,scale,offset的值
const uniformBuffer = device.createBuffer({
label: 'uniforms for triangle',
size: uniformBufferSize,
// eslint-disable-next-line no-undef
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
//uniformValue 是一个 typedArray,用于写入uniform
const uniformValue = new Float32Array(uniformBufferSize / 4)
//根据 color,scale,offset 在 OurStruct 中的顺序, 将三个值通过uniformValue 写入 buffer
uniformValue.set([0, 1, 0, 1], 0) //写入color, 在 typedArray 中的偏移量为0
uniformValue.set([1, 0.5], 4) //写入scale, 在 typedArray 中的偏移量为color 的长度,即4
uniformValue.set([-0.25, -0.3], 6) //写入scale, 在 typedArray 中的偏移量为color和 scale的长度,即6
//创建 bindGroup 对象,将 buffer 绑定到@binding(0)位置
const bindGroup = device.createBindGroup({
label: 'triangle bind group',
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0, resource: {buffer: uniformBuffer}}]
})
function render() {
if (!device) return
//将 uniform 的值从 js 的 typedArray 读入到 GPUBuffer 中
device.queue.writeBuffer(uniformBuffer, 0, uniformValue)
renderPassDescriptor.colorAttachments[0].view = context.getCurrentTexture().createView()
// make a command encoder to start encoding commands
const encoder = device.createCommandEncoder({label: 'our encoder'})
// make a render pass encoder to encode render specific commands
const pass = encoder.beginRenderPass(renderPassDescriptor)
pass.setPipeline(pipeline)
//将 bindGroup 对象设置为 @group(0)
pass.setBindGroup(0, bindGroup)
pass.draw(3) // call our vertex shader 3 times.
pass.end()
const commandBuffer = encoder.finish()
device.queue.submit([commandBuffer])
}
上述例子其实是将 color,scale,offset 三个uniform 变量压缩到一个 buffer 中。我们也可以创建三个 buffer 分别存放这三个 uniform 变量。并分别绑定到 @group(0) @binding(0|1|2) 中。但我所看到 fundamental 教程中没有建议过这样做,应该是为了提升传输效率。但使用 struct 也引入了一个 C 语言中常见的内存对齐的问题,简单来说为了提高 CPU 访问内存的效率。
-
64位 CPU 的数据总线宽度为64bit,也就是一次能读取8字节的数据。CPU 通过指令只能读取8的整数位置的内存数据。
-
struct 变量中各个分量在内存中是连续存放的。CPU在读取struct 结构中的每个分量时,要尽量少地减少访问内存的次数,因为读取内存数据这个操作对CPU 来说很耗时
-
所以struct 中各个分量并不总是在内存中连续排放的,获取 struct 变量值时为了减少 CPU 读取的次数,需要调整 struct 各个分量在内存中的位置,中间留出必要的空隙。
c
struct A{
chart a,
long b
}
参考结构体 A。如果 A.a 和 A.b 在内存中是连续存放的。分量 a 位于内存地址为0的地方占据1字节的内存空间,分量b占据地址为1到8的内存空间。那么当 CPU 要读取分量b时,需要先读取地址0到7的内存空间,取1到7字节,然后读取地址8到15的内存空间,取地址8和地址1到7拼接起来,得到分量 b 的值。这样 CPU 就需要访问两次内存。如果分量 a 位于地址0,分量 b 位于地址8到15。那么 CPU 只用访问一次内存就能拿到地址8到15中的分量 b 的值了。
- struct 中各个分量在内存中的排布并不需要开发者来设置,编译器会在编译时进行内存对齐。但棘手的是,因为uniform 需要我们在 js 代码中通过 typedArray 设置好然后写入 buffer。TypedArray 在内存中也是一块连续的地址,这就要求我们在写入 typedArray 时,需要根据 uniform 变量结构体在内存中的实际分布,设置 typedArray 的大小,以及各个分量因为内存对齐,在 typedArray 中的偏移量。所以有必要说一下内存对齐的原则,主要有一下两点
-
结构体每个成员相对于起始地址的偏移能够被其min(自身大小, 8)整除,如果不能则在前一个成员后面补充字节
-
结构体总体大小能够被min(最宽的成员的大小, 8)整除,如不能则在后面补充字节
具体例子可以查看连接链接中的alignments 即类型的对齐长度就是前文中的min(自身大小, 8)
-
假设 uniform 的类型为 struct OurStruct {f32 scale, vec3f color}。那么着色器代码的编译器会在 scale 和 color 之间插入12个字节,在 color 后面插入4个字节。那么 uniform 变量实际要占据当内存空间位32字节,虽然结构体中的分量实际内存大小加起来位16字节。
-
如果js 中操作 typedArra 时不考虑内存对齐,
js
//color 16字节 + scale 8字节 + offset 8字节,一共需要一个32字节长度的 buffer 来存放 uniform
const uniformBufferSize = 4 + 12
//uniformBuffer 是一个 GPUBuffer,用于存放color,scale,offset的值
const uniformBuffer = device.createBuffer({
label: 'uniforms for triangle',
size: uniformBufferSize,
// eslint-disable-next-line no-undef
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
//uniformValue 是一个 typedArray,用于写入uniform
const uniformValue = new Float32Array(uniformBufferSize / 4)
//根据 color,scale,offset 在 OurStruct 中的顺序, 将三个值通过uniformValue 写入 buffer
uniformValue.set([0.5], 0) //写入scale, 在 typedArray 中的偏移量为color 的长度,即4
uniformValue.set([0, 1, 0], 1) //写入color, 在 typedArray 中的偏移量为0
浏览器报错为: 从错误信息中可以看出js 中创建的 buffer 太小,不能小雨内存对齐后端大小32字节
- js 操作 typedArray 时考虑内存对齐
js
//color 16字节 + scale 8字节 + offset 8字节,一共需要一个32字节长度的 buffer 来存放 uniform
const uniformBufferSize = 4 + 12 + 12 + 4
//uniformBuffer 是一个 GPUBuffer,用于存放color,scale,offset的值
const uniformBuffer = device.createBuffer({
label: 'uniforms for triangle',
size: uniformBufferSize,
// eslint-disable-next-line no-undef
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
})
//uniformValue 是一个 typedArray,用于写入uniform
const uniformValue = new Float32Array(uniformBufferSize / 4)
//根据 color,scale,offset 在 OurStruct 中的顺序, 将三个值通过uniformValue 写入 buffer
uniformValue.set([0.5], 0) //写入scale, 在 typedArray 中的偏移量为color 的长度,即4
uniformValue.set([1, 0, 0], 4) //写入color, 在 typedArray 中的偏移量为4*4
这样就能输出正确的缩小一倍的红色三角形
storage buffer
在 uniform buffer 介绍 bindGroup 时的那种WebGPU 渲染管线流程图中提到了 storage buffer。storage buffer 和 uniform buffer 类似,都是着色器可共享的 buffer。在着色器代码中通过@group(0) @binding(0) var<storage, read> test: Array<vec4f>;
定义一个用于访问 storage buffer 的变量 test。storage buffer 和 uniform buffer 的区别主要在于
-
uniform buffer 访问速度更快,对于需要被高频次使用的数据比如场景中需要多次渲染的模型的材质以及变换矩阵等数据,使用 uniform buffer 能够带来更好的性能
-
storage buffer 的存储空间更大。一个 uniform buffer 大小最大位 64KB。但一个 storage buffer 的最大空间位 128MB
-
storage buffer 可读可写,但 uniform buffer 是只读的。在着色器代码中定义 storage 变量时,<storage, read>的第二个参数 read 表示该 storage 变量是只读的。因为大容量且可读可写的特性,storage buffer 可以用来作为计算着色器的输入和输出载具。
-
虽然 storage buffer 和 uniform buffer 类似主要作为着色器之间的共享变量。但依靠共享和大空间的特性,我们也可以将顶点数据比如 position 放在 storage buffer 中,然后在顶点着色器中通过 vertex_index 索引获取顶点对应的顶点数据。这样顶点着色器也可以访问其他顶点的数据
vertex buffer
每个顶点特有的数据称为 attributes,WebGPU 中每种 attribute 都需要通过 vertex buffer 来存放。我们在定义 attribute 时会告诉 GPU 这种 attribute 的数据类型、在 vertex buffer 中的位置、访问步幅以及 shaderLocation等,WebGPU 会根据顶点索引值将各个顶点的各个 attribute 从 vertex buffer 中取出然后传给顶点着色器。
着色器代码
rust
struct VSOutput { //定义顶点着色器输出,color 作为 inter stage 变量,将顶点颜色通过 location(0) 传给片元着色器
@builtin(position) position: vec4f,
@location(0) color: vec3f,
}
struct OurStruct {
scale: f32,
}
//顶点 attribute 变量结构体,此处用于定义顶点着色器的输入参数
struct Vertex {
@location(0) position: vec2f, //position attribute,此处的 location(0)和 VSOutput.color 的 location(0) 是不一样的。一个是顶点着色器 attribute 的通道,一个是顶点着色器到片元着色器的 inter-stage 变量通道
@location(1) color: vec3f, //color attribute
}
@group(0) @binding(0) var<uniform> ourStruct: OurStruct;
@vertex fn vs(vert: Vertex) -> VSOutput {
var output: VSOutput;
output.position = vec4f(vert.position * vec2f(ourStruct.scale), 0, 1);
output.color = vert.color;
return output;
}
@fragment fn fs(@location(0) color: vec3f) -> @location(0) vec4f {
return vec4f(color, 1);
}
js 代码
javascript
//三角形三个顶点,每个顶点 包含一个长度为2*fload32的 position attribute 和一个长度为3*float32的 color attribute,
const vertexData = new Float32Array(3 * 2 + 3 * 3)
vertexData[0] = 0
vertexData[1] = 0.7
vertexData[2] = 1 //红色
vertexData[3] = 0
vertexData[4] = 0
vertexData[5] = -0.5
vertexData[6] = -0.5
vertexData[7] = 0 //绿色
vertexData[8] = 1
vertexData[9] = 0
vertexData[10] = 0.5
vertexData[11] = -0.5
vertexData[12] = 0 //蓝色
vertexData[13] = 0
vertexData[14] = 1
//定义 vertext buffer,该 buffer 存放了position 和 color 两个 attribute
const vertexBuffer = device.createBuffer({
label: 'vertex buffer vertices',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
})
device.queue.writeBuffer(vertexBuffer, 0, vertexData)
const pipeline = device.createRenderPipeline({
label: 'our hardcoded red triangle pipeline',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
{
//描述第一个 buffer layout,此处并不创建实际的 buffer
arrayStride: (2 + 3) * 4, // 等于一个顶点放在 vertexBuff 中的所有 attribute 的数据长度。position attribute 为 vec2f 类型,每个 position 在 buffer 中的长度位2 * 4字节,color 为 3 * 4字节
attributes: [
{
//position attribute
shaderLocation: 0, //每个 attribute 点一个唯一索引值,0-15。shaderLocation 与该 attribute 在顶点着色器中定义的该 attribute 变量的@location 必须保持一致
offset: 0, //表示该 attribute 在 buffer 中的偏移字节数
format: 'float32x2' //attribute 的数据类型
},
{
//color attribute
shaderLocation: 1,
offset: 2 * 4, //vertexBuffer 中 color 位于 position 到后面,所以访问 color 时,需要偏移一个 position 的长度
format: 'float32x3'
}
]
}
]
},
fragment: {
module,
entryPoint: 'fs',
targets: [{format: presentationFormat}]
}
})
上面这个例子中,我们创建了两个顶点 attribute,即二维向量 position 和三维向量 color,并将这两个 attribute 都存放在一个 vertex buffer 中。在 pipeline 对象中通过 arrayStride、shaderLocation、offset 和 format 等参数告诉 GPU 如何获取到各个顶点对应的 position 和 color 并将这两项数据传给顶点着色器。效果为:
index buffer
webgpu 默认的绘制模式是 'triangle-list',绘制面时,webgpu 每执行三次 draw 就会将相邻的三个顶点连接成一个三角形。由于组成面的三角形会与共享边的另一个三角形共用边上的两个顶点,就会导致 vertex buffer 中存在很多重复的顶点数据,造成存储空间的浪费。index buffer 就是为了解决这个问题。
index buffer 中存放的是顶点在 vertex buffer 中的索引,当通过 drawIndexed 方法绘制时,webgpu 会根据 index buffer 中的索引值去 vertex buffer 中取对应的顶点数据。虽然 indexbuffer 中也会存在重复的索引,但数据量比 vertex buffer 要小得多。
javascript
//生成一个内径为 innerRadius,外径为 outerRadius,由 numSubDivisions 个梯形组成的圆环
const genPositionAndIndex = (
outerRadius: number,
innerRadius: number,
numSubDivisions: number
) => {
const position: number[] = []
const index: number[] = []
const angleStride = (Math.PI * 2) / numSubDivisions
let angle = 0
for (let i = 0; i < numSubDivisions; ++i) {
position.push(
Math.sin(angle) * outerRadius,//插入外径上顶点的点坐标
Math.cos(angle) * outerRadius,
angle / (2 * Math.PI), //插入外径上顶点的角度
Math.sin(angle) * innerRadius, //插入内径上顶点的点坐标
Math.cos(angle) * innerRadius,
angle / (2 * Math.PI) //插入内径上顶点的角度
)
const ni = i === numSubDivisions - 1 ? 0 : i + 1
//将组成梯形的两个三角形上的六个顶点的索引值存在index数组里
index.push(i * 2, i * 2 + 1, ni * 2, i * 2 + 1, ni * 2, ni * 2 + 1)
angle += angleStride
}
return {
vertexData: new Float32Array(position),
indexData: new Uint32Array(index),
numVertices: index.length //需要调用绘制函数的次数
}
}
const {vertexData, indexData, numVertices} = genPositionAndIndex(
0.45,
0.3,
16 * 1024
)
//定义 vertext buffer,该 buffer 存放了position 和 angle 两个 attribute
const vertexBuffer = device.createBuffer({
label: 'vertex buffer vertices',
size: vertexData.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
})
device.queue.writeBuffer(vertexBuffer, 0, vertexData)
//创建 index buffer
const indexBuffer = device.createBuffer({
label: 'index buffer',
size: indexData.byteLength,
usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
})
//将 index 数组写入 indexBuffer 中
device.queue.writeBuffer(indexBuffer, 0, indexData)
const pipeline = device.createRenderPipeline({
label: 'our hardcoded red triangle pipeline',
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [
{
//描述第一个 buffer layout,此处并不创建实际的 buffer
arrayStride: (2 + 1) * 4, // 等于一个顶点放在 vertexBuff 中的所有 attribute 的数据长度。position attribute 为 vec2f 类型,每个 position 在 buffer 中的长度位2 * 4字节, angle 为 1 * 4字节
attributes: [
{
//position attribute
shaderLocation: 0, //每个 attribute 点一个唯一索引值,0-15。shaderLocation 与该 attribute 在顶点着色器中定义的该 attribute 变量的@location 必须保持一致
offset: 0, //表示该 attribute 在 buffer 中的偏移字节数
format: 'float32x2' //attribute 的数据类型
},
{
//angle attribute
shaderLocation: 1, //每个 attribute 点一个唯一索引值,0-15。shaderLocation 与该 attribute 在顶点着色器中定义的该 attribute 变量的@location 必须保持一致
offset: 2 * 4, //表示该 attribute 在 buffer 中的偏移字节数
format: 'float32' //angle attribute 的数据类型
}
]
}
]
},
fragment: {
module,
entryPoint: 'fs',
targets: [{format: presentationFormat}]
}
})
function render() {
if (!device) return
//将 uniform 的值从 js 的 typedArray 读入到 GPUBuffer 中
device.queue.writeBuffer(uniformBuffer, 0, uniformValue)
// Get the current texture from the canvas context and
// set it as the texture to render to.
renderPassDescriptor.colorAttachments[0].view = context
.getCurrentTexture()
.createView()
// make a command encoder to start encoding commands
const encoder = device.createCommandEncoder({label: 'our encoder'})
// make a render pass encoder to encode render specific commands
const pass = encoder.beginRenderPass(renderPassDescriptor)
pass.setPipeline(pipeline)
pass.setVertexBuffer(0, vertexBuffer)
pass.setIndexBuffer(indexBuffer, 'uint32')
pass.setBindGroup(0, bindGroup)
pass.drawIndexed(numVertices) //通过 drawIndexed 方法按照 index buffer 提供的顺序绘制三角形
pass.end()
const commandBuffer = encoder.finish()
device.queue.submit([commandBuffer])
}
rust
struct VSOutput { //定义顶点着色器输出,color 作为 inter stage 变量,将顶点颜色通过 location(0) 传给片元着色器
@builtin(position) position: vec4f,
@location(0) opacity: f32
}
struct OurStruct {
scale: f32,
color: vec3f
}
//顶点 attribute 变量结构体,此处用于定义顶点着色器的输入参数
struct Vertex {
@location(0) position: vec2f, //position attribute,此处的 location(0)和 VSOutput.color 的 location(0) 是不一样的。一个是顶点着色器 attribute 的通道,一个是顶点着色器到片元着色器的 inter-stage 变量通道
@location(1) angle: f32,
}
@group(0) @binding(0) var<uniform> ourStruct: OurStruct;
@vertex fn vs(vert: Vertex) -> VSOutput {
var output: VSOutput;
output.position = vec4f(vert.position * vec2f(ourStruct.scale), 0, 1);
output.opacity = vert.angle; //将角度信息作为透明度传给片元着色器
return output;
}
@fragment fn fs(@location(0) opacity: f32) -> @location(0) vec4f {
return vec4f(ourStruct.color, opacity);
}