注意:WEBGPU还处在起草阶段并未正式发布,规范可能随时会有变化!
- 两者的相同点:
这篇文章的意图是面向已经掌握WebGL,并打算将WebGL迁移到WebGPU的开发人员。
如果你计划将 WebGL 迁移到 WebGPU,这两者有很多概念都是相同的,WebGL有顶点着色器(vertex shader)和片元着色器(fragment shader),WebGPU也有,并且多了计算着色器(computer shader)。WebGL的着色语言是GLSL,WebGPU的着色语言是WGSL,虽然两者是不同的语言,但两者的概念几乎一样。
这两个API都有顶点属性, 这是一种从缓冲区中提取数据关联到着色器中的顶点属性的方法。两者都有 uniforms(全局变量), varings(一般由vertex shader 向 fragment shader中传递的插值数据接口), samplers(采样器), 渲染到纹理,定义深度\模板缓存如何工作的方法等等。
- 讲了两者之间的相同点,再说说WebGL 与 WebGPU的不同点:
(1)这两者最大的不同是 WebGL基于全局状态的,而WebGPU不是。
WebGL的工作模式是通过调用图形 API来改变OpenGL 全局渲染状态(也就是OpenGL 上下文)来绘制物体, 例如gl.bindBuffer,gl.enable , gl.blendFunc这些函数的调用都是在不断地改变全局渲染状态。但WebGPU的渲染状态是记录在渲染管线中(render pipeline, 当然也会有计算管线 compute pipeline ),pipline 一旦创建完毕就不能修改了, 如果你想改变渲染状态就需要创建一个新的pipeline并设置相关的状态(vertex state, fragment state, raster state 等等),这样做有个好处, 驱动层能够根据明确的渲染状态做更多地优化工作,在硬件层面WebGPU渲染状态改变只是通过内存copy的方式来完成。 而OpenGL 不断地通过调用图形API来改变全局渲染状态,这会导致图形驱动不能很好地做一些预测工作,对渲染指令进行一些优化导致渲染性能降低。
(2)第二个最大的不同是 WebGPU 与 WebGL相比是更低级的图形API。
在WebGL 中很多东西都是用名字关联并在应用端做检索的,比如声明在GLSL着色器代码中声明一个uniform变量, 在应用端我们可以通以下函数找到改uniform的位置索引,从而根据位置索引调用其它图形API对该变量的值做一些修改:
loc = gl.getUniformLocation(program, 'nameOfUniform');
另一个例子是 varyings, 它是用来从顶点着色器向片元着色器中传递的插值变量(高版本的OpenGL已经废弃了varying, 改用 in out 接口块的形式)。我们可以在vertex shader中声明一个 vec2 v_texCoord 或者 out vec2 v_texCoord变量 , 在fragment shader中也声明 一个相同类型和名字的变量, 因为名字一致两者就对应上了。
WebGPU 中所有的东西都是通过索引或者字节偏移量来关联的。在WebGL2中你可以创建一个uniform接口块并设置一个名字,然后你就可以在应用端查询uniform块在shader中的位置及块中元素的偏移量。在WebGPU 中块关系是靠字节偏移量和索引值来(location)确定的, 并没有相关的函数使你能够在应用端获取uniform块的任何信息,例如:
function likeWebGL(inputs) {
const {position, texcoords, normal, color} = inputs;
...
}
function likeWebGPU(inputs) {
const [position, texcoords, normal, color] = inputs;
...
}
上面的代码所示,WebGL 中对象用name连接,我们可以用以下形式对结构体的成员变量进行赋值:
const inputs = {};
inputs.normal = normal;
inputs.color = color;
inputs.position = position;
likeWebGL(inputs);
// or
likeWebGL({color, position, normal});
而WebGPU中数据初始化用索引的方式:
const inputs = [];
inputs[0] = position;
inputs[2] = normal;
inputs[3] = color;
likeWebGPU(inputs);
这里,我们用数组的形式进行参数传递,因为我们了解每个输入数据项的索引(indices), 我们知道 postion 的数组索引是0, normal 索引 2 , 等等。应用端传递数据到 WGSL中,这些索引位置关系要完全由你来维护。
【这一部分作者举的例子我觉得有点不恰当,WGSL中通过结构体定义输入输出,也可以用成员变量的形式访问,WebGPU中通过索引进行关联,更多的是利用@location(?)这些索引匹配,当然内存对齐需要自己处理。OpenGL高版本接口块的对齐也是需要开发者自己根据std130或std140 这些规范去对齐。】
(3)其它的不同点:
- Canvas
WebGL 自己管理 canvas, 当你需要创建canvas时,通过配置反走样 antialias, 绘制缓冲区perserveDrawingBuffer, 深度模板缓冲stencil depth 等等这些属性来定制canvas, canvas 创建完毕后,你唯一所做的事就是设置 canvas 的宽度和高度了。
WebGPU 中canvas 相关的初始化工作都要由你来完成,例如 绘制缓冲区是否需要有alpha融混, 创建深度缓存纹理时深度缓存纹理需不需要带stencil buffer,如果绘制区域进行缩放, 你要删除旧的颜色、深度纹理, 根据绘制区的大小创建新的颜色、深度纹理,所有的配置也要再做一次。
虽然看着WebGPU 使用麻烦,但现代图形API 绘制与具体绘制上下文(canvas)解耦了,所以你用相同的设备(device)可以绘制到多个canvas 上。
- WebGPU 不能生成 mipmap 数据
WebGL中可以通过调用 gl.generateMipmap函数自动生成 mipmap 数据。WebGPU中没有相关的API支持自动生成mipmap纹理数据,还是一切得靠自己(自力更生)。(现代图形API的目标是驱动层轻量化,把与图形绘制不相关的功能从核心中移除,只专注绘制,简化驱动的开发, 所谓薄驱动,意义就是专人干专事,别搞一堆乱七八糟的烦它)。
- WebGPU 需要采样器
WebGL1没有采样器的概念,它也是通过设置纹理全局采样状态的方式控制纹理采样。到了WebGL2,有了采样器对象,但也是可选的。WebGPU 中的采样器是必需的。
Vulkan 有 combined image sampler , 也就是在 shader 中通过一个采样函数访问采样对象就可以完成采样处理, 如果不是组合采样器,你就需要分别在shader 中指定采样器和纹理。
下面是组合采样器的采样方式:
layout(binding = 1) uniform sampler2D texSampler;
void main() {
outColor = texture(texSampler, fragTexCoord);
}
WGSL非组合采样器的方式:
@group(0) @binding(1) var mySampler: sampler;
@group(0) @binding(2) var myTexture: texture_2d<f32>;
@stage(fragment)
fn main(@location(0) fragUV: vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(myTexture, mySampler, fragUV);
}
- 下面我们介绍WebGL 到 WebGPU 迁移要点
- 首先是着色器:
下面是带纹理、光照的绘制三角形着色器代码,分别用GLSL和 WGSL实现的:
GLSL
const vSrc = `
uniform mat4 u_worldViewProjection;
uniform mat4 u_worldInverseTranspose;
attribute vec4 a_position;
attribute vec3 a_normal;
attribute vec2 a_texcoord;
varying vec2 v_texCoord;
varying vec3 v_normal;
void main() {
gl_Position = u_worldViewProjection * a_position;
v_texCoord = a_texcoord;
v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
};
const fSrc = `
precision highp float;
varying vec2 v_texCoord;
varying vec3 v_normal;
uniform sampler2D u_diffuse;
uniform vec3 u_lightDirection;
void main() {
vec4 diffuseColor = texture2D(u_diffuse, v_texCoord);
vec3 a_normal = normalize(v_normal);
float l = dot(a_normal, u_lightDirection) * 0.5 + 0.5;
gl_FragColor = vec4(diffuseColor.rgb * l, diffuseColor.a);
};
WGSL
const shaderSrc = `
struct VSUniforms {
worldViewProjection: mat4x4<f32>,
worldInverseTranspose: mat4x4<f32>,
};
@group(0) binding(0) var<uniform> vsUniforms: VSUniforms;
struct MyVSInput {
@location(0) position: vec4<f32>,
@location(1) normal: vec3<f32>,
@location(2) texcoord: vec2<f32>,
};
struct MyVSOutput {
@builtin(position) position: vec4<f32>,
@location(0) normal: vec3<f32>,
@location(1) texcoord: vec2<f32>,
};
@vertex
fn myVSMain(v: MyVSInput) -> MyVSOutput {
var vsOut: MyVSOutput;
vsOut.position = vsUniforms.worldViewProjection * v.position;
vsOut.normal = (vsUniforms.worldInverseTranspose * vec4<f32>(v.normal, 0.0)).xyz;
vsOut.texcoord = v.texcoord;
return vsOut;
};
struct FSUniforms {
lightDirection: vec3<f32>,
};
@group(0) binding(1) var<uniform> fsUniforms: FSUniforms;
@group(0) binding(2) var diffuseSampler: sampler;
@group(0) binding(3) var diffuseTexture: texture_2d<f32>;
@fragment
fn myFSMain(v: MyVSOutput) -> @location(0) vec4<f32> {
var diffuseColor = textureSample(diffuseTexture, diffuseSampler, v.texcoord);
var a_normal = normalize(v.normal);
var l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5;
return vec4<f32>(diffuseColor.rgb * l, diffuseColor.a);
};
两者在语法上有很大的不同,但主要函数的实现还是比较类似的。 GLSL 中的 vec4 在 WGSL中变成了 vec4<f32>, GLSL 中的mat4 在 WGSL中变成了 mat4x4<f32>, WGSL 要求变量要给出具体的类型, 比如 f32, i32, u32, bool 等等。 GLSL 语法上跟 C/C++ 比较像, WGSL 与 Rust 比较像。 另一个不同点 是 类型声明, GLSL在左边, WGSL在右边:
例如:
GLSL
// declare a variable of type vec4
vec4 v;
// declare a function of type mat4 that takes a vec3 parameter
mat4 someFunction(vec3 p) { ... }
// declare a struct
struct Foo { vec4: field; }
WGSL
// declare a variable of type vec4<f32>
var v: vec4<f32>;
// declare a function of type mat4x4<f32> that takes a vec3<f32> parameter
fn someFunction(p: vec3<f32>) => mat4x4<f32> { ... }
// declare a struct
struct Foo { field: vec4<f32>; }
在WGSL中如果不指定变量的类型,将会从右侧的表达式中推导出变量的类型。GLSL总是要求有变量类型。
vec4 color = texture(someTexture, someTextureCoord);
上面的代码必须指定 color 是 vec4 类型;
在WGSL中你可以这样写:
var color: vec4<f32> = textureSample(someTexture, someSampler, someTextureCoord);
or
var color = textureSample(someTexture, someSampler, someTextureCoord);
color 最终都是 vec4<f32>类型。
另一个不同点是 @??? 部分, 它是用来定义顶点数据(vertex buffer)或资源数据(uniform)来源于何处。例如 vertex shader 和 fragment shader 中 uniform 资源接口使用@group(?) binding(?) 来定义数据来源,你要自己确保它们的定义不冲突。
在WebGPU中,你可以将多个 shader 代码定义在一起,比如上面vertex shader 和 fragment shader 就写在了一起。但在WebGL中你就只能分别写着色器。注意,在WebGPU中,顶点属性(attribute)被声明为vertex shader 入口函数的参数,而在GLSL中,它们被声明为函数外的全局变量, 比如 attribute vec3 position 的形式,不像GLSL,如果你不为attribute设置一个位置,编译器将默认分配一个,在WGSL中我们必须提供位置(location)属性 。
shader中传递的插值变量 varyings 在 GLSL中也被定义为全局变量的形式,但在WGSL 中被定义为结构体中成员,用 location属性指定输入输出位置。 在vertex shader 中定义为结构体并作为入口函数的返回值返回, 在 fragment shader 中将该结构体作为输入。示例中 vertex shader 的输出和fragment shader 的输入都用了相同的结构体,但这不是必须的,WGSL着色器中所有的一切都是靠locations 去匹配,而不是名字,例如:
struct MyFSInput {
@location(0) the_normal: vec3<f32>,
@location(1) the_texcoord: vec2<f32>,
};
@stage(fragment)
fn myFSMain(v: MyFSInput) -> @location(0) vec4<f32>
{
var diffuseColor = textureSample(diffuseTexture, diffuseSampler, v.the_texcoord);
var a_normal = normalize(v.the_normal);
var l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5;
return vec4<f32>(diffuseColor.rgb * l, diffuseColor.a);
}
// 下面的形式依然能够工作:
@stage(fragment)
fn myFSMain(
@location(1) uv: vec2<f32>,
@location(0) nrm: vec3<f32>,
) -> @location(0) vec4<f32>
{
var diffuseColor = textureSample(diffuseTexture, diffuseSampler, uv);
var a_normal = normalize(nrm);
var l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5;
return vec4<f32>(diffuseColor.rgb * l, diffuseColor.a);
}
再次强调一次, WGSL 中shader间传递的参数是使用位置匹配的 而不是名字。
再一个不同点是 GLSL vertex shader通过内置变量 gl_Position设置新值, 在WGSL 中通过在结构体中声明 @builtin(position) 对内置变量进行设置。在GLSL fragment shader 中,如果我们使用MRT机制, fragment shader 中, gl_FragData[0] 表示数据输出到第一个帧缓存附件, gl_FragData[1] 表示数据输出到第二个帧缓存附件......,WebGPU中通过在结构体中声明输出变量作为返回值的形式渲染到不同的 帧缓存 attachment 附件上,区分不同的输出还是用@location 属性进行定位。OpenGL新语法也是通过布局输出到不同的帧缓存附件。
#version 300 es
precision mediump float;
in vec2 v_texCoord;
//分别对应 4 个绑定的纹理对象,将渲染结果保存到 4 个纹理中
layout(location = 0) out vec4 outColor0;
layout(location = 1) out vec4 outColor1;
layout(location = 2) out vec4 outColor2;
layout(location = 3) out vec4 outColor3;
uniform sampler2D s_Texture;
void main()
{
vec4 outputColor = texture(s_Texture, v_texCoord);
outColor0 = outputColor;
outColor1 = vec4(outputColor.r, 0.0, 0.0, 1.0);
outColor2 = vec4(0.0, outputColor.g, 0.0, 1.0);
outColor3 = vec4(0.0, 0.0, outputColor.b, 1.0);
}
- 我们再说一下WebGL 与 WebGPU 初始化、资源创建等一些不同点:
(1)WebGPU 与 WebGL 初始化
WebGL
function main() {
const gl = document.querySelector('canvas').getContext('webgl');
if (!gl) {
fail('need webgl');
return;
}
}
main();
WebGPU
async function main() {
const gpu = navigator.gpu;
if (!gpu) {
fail('this browser does not support webgpu');
return;
}
const adapter = await gpu.requestAdapter();
if (!adapter) {
fail('this browser appears to support WebGPU but it\'s disabled');
return;
}
const device = await adapter.requestDevice();
...
}
main();
WebGPU 中通过 navigator 对象先获取硬件设备 navigator.gpu, 然后通过异步形式从 gpu 获得adapter, 最后再通过 获取 抽象设备 device,并且这些都是异步的。 WebGL获取canvas后完事了。
(2) 资源的创建
a )创建缓冲区
WebGL
function createBuffer(gl, data, type = gl.ARRAY_BUFFER) {
const buf = gl.createBuffer();
gl.bindBuffer(type, buf);
gl.bufferData(type, data, gl.STATIC_DRAW);
return buf;
}
const positions = new Float32Array([1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1]);
const normals = new Float32Array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]);
const texcoords = new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]);
const indices = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);
const positionBuffer = createBuffer(gl, positions);
const normalBuffer = createBuffer(gl, normals);
const texcoordBuffer = createBuffer(gl, texcoords);
const indicesBuffer = createBuffer(gl, indices, gl.ELEMENT_ARRAY_BUFFER);
WebGPU
function createBuffer(device, data, usage) {
const buffer = device.createBuffer({
size: data.byteLength,
usage,
mappedAtCreation: true,
});
const dst = new data.constructor(buffer.getMappedRange());
dst.set(data);
buffer.unmap();
return buffer;
}
const positions = new Float32Array([1, 1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, 1, 1, 1, 1, 1, 1, 1, -1, -1, 1, -1, -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1, -1, 1, -1, 1, 1, -1, 1, -1, -1, -1, -1, -1]);
const normals = new Float32Array([1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, -1, 0, 0, -1, 0, 0, -1]);
const texcoords = new Float32Array([1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1]);
const indices = new Uint16Array([0, 1, 2, 0, 2, 3, 4, 5, 6, 4, 6, 7, 8, 9, 10, 8, 10, 11, 12, 13, 14, 12, 14, 15, 16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23]);
const positionBuffer = createBuffer(device, positions, GPUBufferUsage.VERTEX);
const normalBuffer = createBuffer(device, normals, GPUBufferUsage.VERTEX);
const texcoordBuffer = createBuffer(device, texcoords, GPUBufferUsage.VERTEX);
const indicesBuffer = createBuffer(device, indices, GPUBufferUsage.INDEX);
b)创建纹理
WebGL
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(
gl.TEXTURE_2D,
0, // level
gl.RGBA,
2, // width
2, // height
0,
gl.RGBA,
gl.UNSIGNED_BYTE,
new Uint8Array([
255, 255, 128, 255,
128, 255, 255, 255,
255, 128, 255, 255,
255, 128, 128, 255,
]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
WebGPU
const tex = device.createTexture({
size: [2, 2, 1],
format: 'rgba8unorm',
usage:
GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST,
});
device.queue.writeTexture(
{ texture: tex },
new Uint8Array([
255, 255, 128, 255,
128, 255, 255, 255,
255, 128, 255, 255,
255, 128, 128, 255,
]),
{ bytesPerRow: 8, rowsPerImage: 2 },
{ width: 2, height: 2 },
);
const sampler = device.createSampler({
magFilter: 'nearest',
minFilter: 'nearest',
});
两者之间最大的不同是 WebGPU需要分别创建纹理和采样器, 而在 WebGL中采样器的创建是可选的。
c)编译着色器
WebGL
function createShader(gl, type, source) {
const sh = gl.createShader(type);
gl.shaderSource(sh, source);
gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
throw new Error(gl.getShaderInfoLog(sh));
}
return sh;
}
const vs = createShader(gl, gl.VERTEX_SHADER, vSrc);
const fs = createShader(gl, gl.FRAGMENT_SHADER, fSrc);
WebGPU
const shaderModule = device.createShaderModule({code: shaderSrc});
在WebGL 中,如果shader 没编译成功,可以通过调用gl.getShaderParameter() 函数检查 COMPILE_STATUS 错误,然后调用gl.getShaderInfoLog()函数打印错误信息。在WebGPU里,很多WebGPU实现会自行在console里。 你也可以自己检查错误并打印出错误信息。
- 着色器程序连接 与 设置渲染管线
WebGL
function createProgram(gl, vs, fs) {
const prg = gl.createProgram();
gl.attachShader(prg, vs);
gl.attachShader(prg, fs);
gl.linkProgram(prg);
if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
throw new Error(gl.getProgramInfoLog(prg));
}
return prg;
}
const program = createProgram(gl, vs, fs);
...
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(normalLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.vertexAttribPointer(texcoordLoc, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texcoordLoc);
....
gl.enable(gl.DEPTH_TEST);
gl.enable(gl.CULL_FACE);
WebGPU
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: { // 顶点属性
module: shaderModule,
entryPoint: 'myVSMain',
buffers: [
// position
{
arrayStride: 3 * 4, // 3 floats, 4 bytes each
attributes: [
{shaderLocation: 0, offset: 0, format: 'float32x3'},
],
},
// normals
{
arrayStride: 3 * 4, // 3 floats, 4 bytes each
attributes: [
{shaderLocation: 1, offset: 0, format: 'float32x3'},
],
},
// texcoords
{
arrayStride: 2 * 4, // 2 floats, 4 bytes each
attributes: [
{shaderLocation: 2, offset: 0, format: 'float32x2',},
],
},
],
},
fragment: {
module: shaderModule,
entryPoint: 'myFSMain',
targets: [
{format: presentationFormat},
],
},
primitive: {
topology: 'triangle-list',
cullMode: 'back',
},
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus',
},
...(canvasInfo.sampleCount > 1 && {
multisample: {
count: canvasInfo.sampleCount,
},
}),
});
WebGPU着色器的链接发生在创建渲染管线的过程中,创建渲染管线是一个比较慢的过程,一般我们需要通过缓存并复用渲染管线来提升效率。我们为vertex shader 和 fragment shader指定了入口函数的名字。
在WebGL中,我们先使用gl.bindBuffer函数绑定缓存对象,然后我们调用gl.vertexAttribPointer 设置顶点格式和属性,告诉shader 如何从缓冲区提取数据。在WebGPU中,我们只是在创建渲染管线时指定如何从缓冲区中提取数据, 具体用哪个顶点缓存后面再指定。就好比先指定顶点属性格式模板,只要符合这种顶点数据格式的buffer 都能在后期通过bind来进行关联。
上面的例子中,我们看到 buffers 是一个GPUVertexBufferLayout数组, GPUVetexBufferLayout 定义了顶点属性的数据格式 format,内存中的跨度stride, 内存中的偏移量offset 及 在 shader 中 与之相关联的 attribute 索引 shaderLocation。
在 WebGPU中,我们设置渲染管线(还另外一种计算管线)的顶点状态,光栅化状态, 图元拓扑,深度\模板缓存的状态等等。只要有一种状态发生改变,我们就需要重新创建渲染管线来绘制新图元。
- Uniform 的使用
WebGL
const u_lightDirectionLoc = gl.getUniformLocation(program, 'u_lightDirection');
const u_diffuseLoc = gl.getUniformLocation(program, 'u_diffuse');
const u_worldInverseTransposeLoc = gl.getUniformLocation(program, 'u_worldInverseTranspose');
const u_worldViewProjectionLoc = gl.getUniformLocation(program, 'u_worldViewProjection');
WebGPU
const vUniformBufferSize = 2 * 16 * 4; // 2 mat4s * 16 floats per mat * 4 bytes // per float
const fUniformBufferSize = 3 * 4; // 1 vec3 * 3 floats per vec3 * 4 bytes per float
const vsUniformBuffer = device.createBuffer({
size: vUniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const fsUniformBuffer = device.createBuffer({
size: fUniformBufferSize,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const vsUniformValues = new Float32Array(2 * 16); // 2 mat4s
const worldViewProjection = vsUniformValues.subarray(0, 16);
const worldInverseTranspose = vsUniformValues.subarray(16, 32);
const fsUniformValues = new Float32Array(3); // 1 vec3
const lightDirection = fsUniformValues.subarray(0, 3);
在WebGL中,我们通过API 查找unifom 缓冲对象在shader 中的位置(location)。在WebGPU中,我们创建uniform buffer来并保存该对象的句柄。注意vUniformBufferSize和fUniformBufferSize是手工计算的。类似地,在类型化数组中创建视图时,偏移量和大小也是手动计算的。与WebGL不同,WebGPU没有提供相关的API来查询这些偏移量和大小的API。
【注意, WebGL2.0 也有类似的处理过程,WebGL2.0 已经支持将多个Uniform 对象打组成块(Uniform Block ),很明显这将减少修改uniform 的 API调用,但我们需要考虑Uniform Block中各个分量的对齐情况,目前使用uniform block 已成为主流。】
- 准备绘制
WebGL 绘制前的工作非常直接, 激活纹理单元并绑定纹理,调用glUseProgram设置相应的着色器, 绑定当前图元对应的顶点缓冲区与索引缓冲区。但WebGPU 就不这么简单了,要创建bindGroup对象关联资源,使用资源时绑定相应的bindGroup。
// happens at render time
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
WebGPU
// can happen at init time
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: vsUniformBuffer } },
{ binding: 1, resource: { buffer: fsUniformBuffer } },
{ binding: 2, resource: sampler },
{ binding: 3, resource: tex.createView() },
],
});
BindGroup资源设置一定要匹配 shader 中 声明的资源格式。
WebGL
gl.clearColor(0.5, 0.5, 0.5, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
WebGPU
const renderPassDescriptor = {
colorAttachments: [
{
// view: undefined, // Assigned later
// resolveTarget: undefined, // Assigned Later
clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
},
],
depthStencilAttachment: {
// view: undefined, // Assigned later
depthClearValue: 1,
depthLoadOp: 'clear',
depthStoreOp: 'store',
},
};
- 设置Uniform
WebGL
gl.uniform3fv(u_lightDirectionLoc, v3.normalize([1, 8, -10]));
gl.uniform1i(u_diffuseLoc, 0);
gl.uniformMatrix4fv(u_worldInverseTransposeLoc, false, m4.transpose(m4.inverse(world)));
gl.uniformMatrix4fv(u_worldViewProjectionLoc, false, m4.multiply(viewProjection, world));
WebGPU
m4.transpose(m4.inverse(world), worldInverseTranspose);
m4.multiply(viewProjection, world, worldViewProjection);
v3.normalize([1, 8, -10], lightDirection);
device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues);
device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues);
在 WebGL中,通过调用类似gl.uniform??? 这样的API更改 uniform 的值。在WebGPU中,直接修改缓冲区关联的数据并上传到GPU。 WebGPU与Vulkan 相比,简化了数据上传同步等一系列操作。
- 绘制缩放
WebGL会自己管理缩放导致的帧缓冲区重建过程,WebGPU 中你需要显式地重建绘图关联的颜色和深度纹理、纹理视图。
WebGL
function resizeCanvasToDisplaySize(canvas) {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = width !== canvas.width || height !== canvas.height;
if (needResize) {
canvas.width = width;
canvas.height = height;
}
return needResize;
}
WebGPU
// At init time
const canvas = document.querySelector('canvas');
const context = canvas.getContext('webgpu');
const presentationFormat = gpu.getPreferredFormat(adapter);
context.configure({
device,
format: presentationFormat,
});
const canvasInfo = {
canvas,
presentationFormat,
// these are filled out in resizeToDisplaySize
renderTarget: undefined,
renderTargetView: undefined,
depthTexture: undefined,
depthTextureView: undefined,
sampleCount: 4, // can be 1 or 4
};
// --- At render time ---
function resizeToDisplaySize(device, canvasInfo) {
const {
canvas,
context,
renderTarget,
presentationFormat,
depthTexture,
sampleCount,
} = canvasInfo;
const width = Math.min(device.limits.maxTextureDimension2D, canvas.clientWidth);
const height = Math.min(device.limits.maxTextureDimension2D, canvas.clientHeight);
const needResize = !canvasInfo.renderTarget ||
width !== canvas.width ||
height !== canvas.height;
if (needResize) {
if (renderTarget) {
renderTarget.destroy();
}
if (depthTexture) {
depthTexture.destroy();
}
canvas.width = width;
canvas.height = height;
if (sampleCount > 1) {
const newRenderTarget = device.createTexture({
size: [canvas.width, canvas.height],
format: presentationFormat,
sampleCount,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
canvasInfo.renderTarget = newRenderTarget;
canvasInfo.renderTargetView = newRenderTarget.createView();
}
const newDepthTexture = device.createTexture({
size: [canvas.width, canvas.height,
format: 'depth24plus',
sampleCount,
usage: GPUTextureUsage.RENDER_ATTACHMENT,
});
canvasInfo.depthTexture = newDepthTexture;
canvasInfo.depthTextureView = newDepthTexture.createView();
}
return needResize;
}
在上面的代码中, 多重采样样本数 sampleCount ,如果值为4, 等效于打开了多重采样功能, 如果值为 1 就相当于关闭了多重采样。
WebGL会尽量不耗尽内存,这意味着如果你要求一个16000x16000的canvas,WebGL可能会给你一个4096x4096的画布。你可以通过查看gl.drawingBufferWidth和gl.drawingBufferHeight找到你实际返回的canvas大小。
WebGL这样做的原因是(1)在多个显示器上拉伸画布可能会使大小超过GPU可以处理的大小(2)系统可能内存不足,而不是崩溃,WebGL将返回一个较小的绘图缓冲区。
在WebGPU中,检查这两种情况取决于你。我们正在检查上面的情况(1)。对于情况(2),我们必须自己检查内存是否不足,就像WebGPU中的其他事情一样,这样做是异步的。
device.pushErrorScope('out-of-memory');
context.configure({...});
if (sampleCount > 1) {
const newRenderTarget = device.createTexture({...});
...
}
const newDepthTexture = device.createTexture({...});
...
device.popErrorScope().then(error => {
if (error) {
// we're out of memory, try a smaller size?
}
});
- 绘制
WebGL
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
...
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(positionLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(normalLoc);
gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
gl.vertexAttribPointer(texcoordLoc, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(texcoordLoc);
...
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_SHORT, 0);
WebGPU
if (canvasInfo.sampleCount === 1) {
const colorTexture = context.getCurrentTexture();
renderPassDescriptor.colorAttachments[0].view = colorTexture.createView();
} else {
renderPassDescriptor.colorAttachments[0].view = canvasInfo.renderTargetView;
renderPassDescriptor.colorAttachments[0].resolveTarget = context.getCurrentTexture().createView();
}
renderPassDescriptor.depthStencilAttachment.view = canvasInfo.depthTextureView;
const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.setVertexBuffer(0, positionBuffer);
passEncoder.setVertexBuffer(1, normalBuffer);
passEncoder.setVertexBuffer(2, texcoordBuffer);
passEncoder.setIndexBuffer(indicesBuffer, 'uint16');
passEncoder.drawIndexed(indices.length);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
注意,代码里重复设置了WebGL顶点属性, 这可能发生在初始化阶段或渲染阶段。在WebGPU中,我们设置了如何在初始化时只是设置了如何从缓冲区提取数据,并没有关联到实际缓冲区。
在WebGPU中,当绘制区域大小改变时,我们需要重新关联到canvas的 textureView 上,也就是通过beginRenderPass渲染传递描述进行更新,然后我们需要创建一个命令编码器并开始渲染。
在渲染通道内,我们设置了渲染管线,它有点像gl.useProgram的等价物。然后我们设置绑定组,它提供我们的采样器、纹理和我们的两个uniform 缓冲区。我们将顶点缓冲区设置为与前面的声明匹配起来。最后,我们设置一个索引缓冲区并调用drawIndexed,这相当于调用gl.drawElements。
回到WebGL,我们需要调用gl.viewport。在WebGPU中,渲染通道编码器默认为一个与附件大小匹配的视口,所以除非我们想要一个不匹配的视口,否则我们不必单独设置一个视口。
在WebGL中,我们调用gl.clear来清除画布。而在WebGPU中,我们之前在向beginRenderPass传递描述对象时就已经设置好了。
另一件需要注意的重要事情是,我们正在向被称为device.queue的东西发出指令。注意,当我们需要更新GPU数据的时候,我们调用了device.queue.writeBuffer ,然后当我们创建命令编码器并使用device.queue.submit提交它,这会在GPU端异步执行数据传递。
- 其它一些不同点:
Z clip space is 0 to 1
在WebGL中,规范化设备坐标系z 的范围是 -1 到 1 , WebGPU 中 z 值的范围是从 0 到 1。这比较直观有意义。
Y axis is down in framebuffer, viewport coordinates
虽然 WebGL与 WebGPU 在规范化设备坐标系(NDC)中, Y 轴的朝向相同, 但framebuffer, viewport 空间中 Y 轴的朝向却相反。点(0, 0) 在WebGL中是左下角,但在WebGPU中是左上角。
WGSL uses @builtin(???) for GLSL’s gl_XXX variables.
内建全局变量的表达不一样 , WebGL 与 WebGPU 的对应关系:
gl_FragCoord is @builtin(position) myVarOrField: vec4<f32> and unlike WebGL it’s in normalized coordinates (-1 to +1).
gl_VertexID is @builtin(vertex_index) myVarOrField: u32
gl_InstanceID is @builtin(instance_index) myVarOrField: u32
gl_Position is @builtin(position) vec4<f32> which may be the return value of a vertex shader or a field in a structure returned by the vertex shader
There is no gl_PointCoord equivalent because points are only 1 pixel in WebGPU
WGSL only supports lines and points 1 pixel wide
WebGPU中的点、线图元都是1像素宽... 现代图形API貌似都不支持大于一个像素的线宽。