赞
踩
注意: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的工作模式是通过调用图形 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来改变全局渲染状态,这会导致图形驱动不能很好地做一些预测工作,对渲染指令进行一些优化导致渲染性能降低。
在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 这些规范去对齐。】
WebGL 自己管理 canvas, 当你需要创建canvas时,通过配置反走样 antialias, 绘制缓冲区perserveDrawingBuffer, 深度模板缓冲stencil depth 等等这些属性来定制canvas, canvas 创建完毕后,你唯一所做的事就是设置 canvas 的宽度和高度了。
WebGPU 中canvas 相关的初始化工作都要由你来完成,例如 绘制缓冲区是否需要有alpha融混, 创建深度缓存纹理时深度缓存纹理需不需要带stencil buffer,如果绘制区域进行缩放, 你要删除旧的颜色、深度纹理, 根据绘制区的大小创建新的颜色、深度纹理,所有的配置也要再做一次。
虽然看着WebGPU 使用麻烦,但现代图形API 绘制与具体绘制上下文(canvas)解耦了,所以你用相同的设备(device)可以绘制到多个canvas 上。
WebGL中可以通过调用 gl.generateMipmap函数自动生成 mipmap 数据。WebGPU中没有相关的API支持自动生成mipmap纹理数据,还是一切得靠自己(自力更生)。(现代图形API的目标是驱动层轻量化,把与图形绘制不相关的功能从核心中移除,只专注绘制,简化驱动的开发, 所谓薄驱动,意义就是专人干专事,别搞一堆乱七八糟的烦它)。
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);
- }
下面是带纹理、光照的绘制三角形着色器代码,分别用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
- 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后完事了。
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);
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中采样器的创建是可选的。
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中,我们设置渲染管线(还另外一种计算管线)的顶点状态,光栅化状态, 图元拓扑,深度\模板缓存的状态等等。只要有一种状态发生改变,我们就需要重新创建渲染管线来绘制新图元。
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',
- },
- };
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端异步执行数据传递。
在WebGL中,规范化设备坐标系z 的范围是 -1 到 1 , WebGPU 中 z 值的范围是从 0 到 1。这比较直观有意义。
虽然 WebGL与 WebGPU 在规范化设备坐标系(NDC)中, Y 轴的朝向相同, 但framebuffer, viewport 空间中 Y 轴的朝向却相反。点(0, 0) 在WebGL中是左下角,但在WebGPU中是左上角。
内建全局变量的表达不一样 , 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
WebGPU中的点、线图元都是1像素宽... 现代图形API貌似都不支持大于一个像素的线宽。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。