当前位置:   article > 正文

从WEBGL 迁移到 WEBGPU_webgl webgpu

webgl webgpu

注意: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块的任何信息,例如:

  1. function likeWebGL(inputs) {
  2. const {position, texcoords, normal, color} = inputs;
  3. ...
  4. }
  5. function likeWebGPU(inputs) {
  6. const [position, texcoords, normal, color] = inputs;
  7. ...
  8. }

上面的代码所示,WebGL 中对象用name连接,我们可以用以下形式对结构体的成员变量进行赋值:

  1. const inputs = {};
  2. inputs.normal = normal;
  3. inputs.color = color;
  4. inputs.position = position;
  5. likeWebGL(inputs);
  6. // or
  7. likeWebGL({color, position, normal});

而WebGPU中数据初始化用索引的方式:

  1. const inputs = [];
  2. inputs[0] = position;
  3. inputs[2] = normal;
  4. inputs[3] = color;
  5. 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 中指定采样器和纹理。

下面是组合采样器的采样方式:

  1. layout(binding = 1) uniform sampler2D texSampler;
  2. void main() {
  3. outColor = texture(texSampler, fragTexCoord);
  4. }

WGSL非组合采样器的方式:

  1. @group(0) @binding(1) var mySampler: sampler;
  2. @group(0) @binding(2) var myTexture: texture_2d<f32>;
  3. @stage(fragment)
  4. fn main(@location(0) fragUV: vec2<f32>) -> @location(0) vec4<f32> {
  5. return textureSample(myTexture, mySampler, fragUV);
  6. }
    • 下面我们介绍WebGL 到 WebGPU 迁移要点

    • 首先是着色器:

下面是带纹理、光照的绘制三角形着色器代码,分别用GLSL和 WGSL实现的:

GLSL

  1. const vSrc = `
  2. uniform mat4 u_worldViewProjection;
  3. uniform mat4 u_worldInverseTranspose;
  4. attribute vec4 a_position;
  5. attribute vec3 a_normal;
  6. attribute vec2 a_texcoord;
  7. varying vec2 v_texCoord;
  8. varying vec3 v_normal;
  9. void main() {
  10. gl_Position = u_worldViewProjection * a_position;
  11. v_texCoord = a_texcoord;
  12. v_normal = (u_worldInverseTranspose * vec4(a_normal, 0)).xyz;
  13. };
  14. const fSrc = `
  15. precision highp float;
  16. varying vec2 v_texCoord;
  17. varying vec3 v_normal;
  18. uniform sampler2D u_diffuse;
  19. uniform vec3 u_lightDirection;
  20. void main() {
  21. vec4 diffuseColor = texture2D(u_diffuse, v_texCoord);
  22. vec3 a_normal = normalize(v_normal);
  23. float l = dot(a_normal, u_lightDirection) * 0.5 + 0.5;
  24. gl_FragColor = vec4(diffuseColor.rgb * l, diffuseColor.a);
  25. };

WGSL

  1. const shaderSrc = `
  2. struct VSUniforms {
  3. worldViewProjection: mat4x4<f32>,
  4. worldInverseTranspose: mat4x4<f32>,
  5. };
  6. @group(0) binding(0) var<uniform> vsUniforms: VSUniforms;
  7. struct MyVSInput {
  8. @location(0) position: vec4<f32>,
  9. @location(1) normal: vec3<f32>,
  10. @location(2) texcoord: vec2<f32>,
  11. };
  12. struct MyVSOutput {
  13. @builtin(position) position: vec4<f32>,
  14. @location(0) normal: vec3<f32>,
  15. @location(1) texcoord: vec2<f32>,
  16. };
  17. @vertex
  18. fn myVSMain(v: MyVSInput) -> MyVSOutput {
  19. var vsOut: MyVSOutput;
  20. vsOut.position = vsUniforms.worldViewProjection * v.position;
  21. vsOut.normal = (vsUniforms.worldInverseTranspose * vec4<f32>(v.normal, 0.0)).xyz;
  22. vsOut.texcoord = v.texcoord;
  23. return vsOut;
  24. };
  25. struct FSUniforms {
  26. lightDirection: vec3<f32>,
  27. };
  28. @group(0) binding(1) var<uniform> fsUniforms: FSUniforms;
  29. @group(0) binding(2) var diffuseSampler: sampler;
  30. @group(0) binding(3) var diffuseTexture: texture_2d<f32>;
  31. @fragment
  32. fn myFSMain(v: MyVSOutput) -> @location(0) vec4<f32> {
  33. var diffuseColor = textureSample(diffuseTexture, diffuseSampler, v.texcoord);
  34. var a_normal = normalize(v.normal);
  35. var l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5;
  36. return vec4<f32>(diffuseColor.rgb * l, diffuseColor.a);
  37. };

两者在语法上有很大的不同,但主要函数的实现还是比较类似的。 GLSL 中的 vec4 在 WGSL中变成了 vec4<f32>, GLSL 中的mat4 在 WGSL中变成了 mat4x4<f32>, WGSL 要求变量要给出具体的类型, 比如 f32, i32, u32, bool 等等。 GLSL 语法上跟 C/C++ 比较像, WGSL 与 Rust 比较像。 另一个不同点 是 类型声明, GLSL在左边, WGSL在右边:

例如:

GLSL

  1. // declare a variable of type vec4
  2. vec4 v;
  3. // declare a function of type mat4 that takes a vec3 parameter
  4. mat4 someFunction(vec3 p) { ... }
  5. // declare a struct
  6. struct Foo { vec4: field; }

WGSL

  1. // declare a variable of type vec4<f32>
  2. var v: vec4<f32>;
  3. // declare a function of type mat4x4<f32> that takes a vec3<f32> parameter
  4. fn someFunction(p: vec3<f32>) => mat4x4<f32> { ... }
  5. // declare a struct
  6. struct Foo { field: vec4<f32>; }

在WGSL中如果不指定变量的类型,将会从右侧的表达式中推导出变量的类型。GLSL总是要求有变量类型。

vec4 color = texture(someTexture, someTextureCoord);

上面的代码必须指定 color 是 vec4 类型;

在WGSL中你可以这样写:

  1. var color: vec4<f32> = textureSample(someTexture, someSampler, someTextureCoord);
  2. or
  3. 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 去匹配,而不是名字,例如:

  1. struct MyFSInput {
  2. @location(0) the_normal: vec3<f32>,
  3. @location(1) the_texcoord: vec2<f32>,
  4. };
  5. @stage(fragment)
  6. fn myFSMain(v: MyFSInput) -> @location(0) vec4<f32>
  7. {
  8. var diffuseColor = textureSample(diffuseTexture, diffuseSampler, v.the_texcoord);
  9. var a_normal = normalize(v.the_normal);
  10. var l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5;
  11. return vec4<f32>(diffuseColor.rgb * l, diffuseColor.a);
  12. }
  13. // 下面的形式依然能够工作:
  14. @stage(fragment)
  15. fn myFSMain(
  16. @location(1) uv: vec2<f32>,
  17. @location(0) nrm: vec3<f32>,
  18. ) -> @location(0) vec4<f32>
  19. {
  20. var diffuseColor = textureSample(diffuseTexture, diffuseSampler, uv);
  21. var a_normal = normalize(nrm);
  22. var l = dot(a_normal, fsUniforms.lightDirection) * 0.5 + 0.5;
  23. return vec4<f32>(diffuseColor.rgb * l, diffuseColor.a);
  24. }

再次强调一次, 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新语法也是通过布局输出到不同的帧缓存附件。

  1. #version 300 es
  2. precision mediump float;
  3. in vec2 v_texCoord;
  4. //分别对应 4 个绑定的纹理对象,将渲染结果保存到 4 个纹理中
  5. layout(location = 0) out vec4 outColor0;
  6. layout(location = 1) out vec4 outColor1;
  7. layout(location = 2) out vec4 outColor2;
  8. layout(location = 3) out vec4 outColor3;
  9. uniform sampler2D s_Texture;
  10. void main()
  11. {
  12. vec4 outputColor = texture(s_Texture, v_texCoord);
  13. outColor0 = outputColor;
  14. outColor1 = vec4(outputColor.r, 0.0, 0.0, 1.0);
  15. outColor2 = vec4(0.0, outputColor.g, 0.0, 1.0);
  16. outColor3 = vec4(0.0, 0.0, outputColor.b, 1.0);
  17. }
    • 我们再说一下WebGL 与 WebGPU 初始化、资源创建等一些不同点:

(1)WebGPU 与 WebGL 初始化

WebGL

  1. function main() {
  2. const gl = document.querySelector('canvas').getContext('webgl');
  3. if (!gl) {
  4. fail('need webgl');
  5. return;
  6. }
  7. }
  8. main();

WebGPU

  1. async function main() {
  2. const gpu = navigator.gpu;
  3. if (!gpu) {
  4. fail('this browser does not support webgpu');
  5. return;
  6. }
  7. const adapter = await gpu.requestAdapter();
  8. if (!adapter) {
  9. fail('this browser appears to support WebGPU but it\'s disabled');
  10. return;
  11. }
  12. const device = await adapter.requestDevice();
  13. ...
  14. }
  15. main();

WebGPU 中通过 navigator 对象先获取硬件设备 navigator.gpu, 然后通过异步形式从 gpu 获得adapter, 最后再通过 获取 抽象设备 device,并且这些都是异步的。 WebGL获取canvas后完事了。

(2) 资源的创建

a )创建缓冲区

WebGL

  1. function createBuffer(gl, data, type = gl.ARRAY_BUFFER) {
  2. const buf = gl.createBuffer();
  3. gl.bindBuffer(type, buf);
  4. gl.bufferData(type, data, gl.STATIC_DRAW);
  5. return buf;
  6. }
  7. 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]);
  8. 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]);
  9. 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]);
  10. 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]);
  11. const positionBuffer = createBuffer(gl, positions);
  12. const normalBuffer = createBuffer(gl, normals);
  13. const texcoordBuffer = createBuffer(gl, texcoords);
  14. const indicesBuffer = createBuffer(gl, indices, gl.ELEMENT_ARRAY_BUFFER);

WebGPU

  1. function createBuffer(device, data, usage) {
  2. const buffer = device.createBuffer({
  3. size: data.byteLength,
  4. usage,
  5. mappedAtCreation: true,
  6. });
  7. const dst = new data.constructor(buffer.getMappedRange());
  8. dst.set(data);
  9. buffer.unmap();
  10. return buffer;
  11. }
  12. 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]);
  13. 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]);
  14. 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]);
  15. 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]);
  16. const positionBuffer = createBuffer(device, positions, GPUBufferUsage.VERTEX);
  17. const normalBuffer = createBuffer(device, normals, GPUBufferUsage.VERTEX);
  18. const texcoordBuffer = createBuffer(device, texcoords, GPUBufferUsage.VERTEX);
  19. const indicesBuffer = createBuffer(device, indices, GPUBufferUsage.INDEX);
b)创建纹理

WebGL

  1. const tex = gl.createTexture();
  2. gl.bindTexture(gl.TEXTURE_2D, tex);
  3. gl.texImage2D(
  4. gl.TEXTURE_2D,
  5. 0, // level
  6. gl.RGBA,
  7. 2, // width
  8. 2, // height
  9. 0,
  10. gl.RGBA,
  11. gl.UNSIGNED_BYTE,
  12. new Uint8Array([
  13. 255, 255, 128, 255,
  14. 128, 255, 255, 255,
  15. 255, 128, 255, 255,
  16. 255, 128, 128, 255,
  17. ]));
  18. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  19. gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

WebGPU

  1. const tex = device.createTexture({
  2. size: [2, 2, 1],
  3. format: 'rgba8unorm',
  4. usage:
  5. GPUTextureUsage.TEXTURE_BINDING |
  6. GPUTextureUsage.COPY_DST,
  7. });
  8. device.queue.writeTexture(
  9. { texture: tex },
  10. new Uint8Array([
  11. 255, 255, 128, 255,
  12. 128, 255, 255, 255,
  13. 255, 128, 255, 255,
  14. 255, 128, 128, 255,
  15. ]),
  16. { bytesPerRow: 8, rowsPerImage: 2 },
  17. { width: 2, height: 2 },
  18. );
  19. const sampler = device.createSampler({
  20. magFilter: 'nearest',
  21. minFilter: 'nearest',
  22. });

两者之间最大的不同是 WebGPU需要分别创建纹理和采样器, 而在 WebGL中采样器的创建是可选的。

c)编译着色器

WebGL

  1. function createShader(gl, type, source) {
  2. const sh = gl.createShader(type);
  3. gl.shaderSource(sh, source);
  4. gl.compileShader(sh);
  5. if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
  6. throw new Error(gl.getShaderInfoLog(sh));
  7. }
  8. return sh;
  9. }
  10. const vs = createShader(gl, gl.VERTEX_SHADER, vSrc);
  11. 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

  1. function createProgram(gl, vs, fs) {
  2. const prg = gl.createProgram();
  3. gl.attachShader(prg, vs);
  4. gl.attachShader(prg, fs);
  5. gl.linkProgram(prg);
  6. if (!gl.getProgramParameter(prg, gl.LINK_STATUS)) {
  7. throw new Error(gl.getProgramInfoLog(prg));
  8. }
  9. return prg;
  10. }
  11. const program = createProgram(gl, vs, fs);
  12. ...
  13. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  14. gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
  15. gl.enableVertexAttribArray(positionLoc);
  16. gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  17. gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);
  18. gl.enableVertexAttribArray(normalLoc);
  19. gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
  20. gl.vertexAttribPointer(texcoordLoc, 2, gl.FLOAT, false, 0, 0);
  21. gl.enableVertexAttribArray(texcoordLoc);
  22. ....
  23. gl.enable(gl.DEPTH_TEST);
  24. gl.enable(gl.CULL_FACE);

WebGPU

  1. const pipeline = device.createRenderPipeline({
  2. layout: 'auto',
  3. vertex: { // 顶点属性
  4. module: shaderModule,
  5. entryPoint: 'myVSMain',
  6. buffers: [
  7. // position
  8. {
  9. arrayStride: 3 * 4, // 3 floats, 4 bytes each
  10. attributes: [
  11. {shaderLocation: 0, offset: 0, format: 'float32x3'},
  12. ],
  13. },
  14. // normals
  15. {
  16. arrayStride: 3 * 4, // 3 floats, 4 bytes each
  17. attributes: [
  18. {shaderLocation: 1, offset: 0, format: 'float32x3'},
  19. ],
  20. },
  21. // texcoords
  22. {
  23. arrayStride: 2 * 4, // 2 floats, 4 bytes each
  24. attributes: [
  25. {shaderLocation: 2, offset: 0, format: 'float32x2',},
  26. ],
  27. },
  28. ],
  29. },
  30. fragment: {
  31. module: shaderModule,
  32. entryPoint: 'myFSMain',
  33. targets: [
  34. {format: presentationFormat},
  35. ],
  36. },
  37. primitive: {
  38. topology: 'triangle-list',
  39. cullMode: 'back',
  40. },
  41. depthStencil: {
  42. depthWriteEnabled: true,
  43. depthCompare: 'less',
  44. format: 'depth24plus',
  45. },
  46. ...(canvasInfo.sampleCount > 1 && {
  47. multisample: {
  48. count: canvasInfo.sampleCount,
  49. },
  50. }),
  51. });

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

  1. const u_lightDirectionLoc = gl.getUniformLocation(program, 'u_lightDirection');
  2. const u_diffuseLoc = gl.getUniformLocation(program, 'u_diffuse');
  3. const u_worldInverseTransposeLoc = gl.getUniformLocation(program, 'u_worldInverseTranspose');
  4. const u_worldViewProjectionLoc = gl.getUniformLocation(program, 'u_worldViewProjection');

WebGPU

  1. const vUniformBufferSize = 2 * 16 * 4; // 2 mat4s * 16 floats per mat * 4 bytes // per float
  2. const fUniformBufferSize = 3 * 4; // 1 vec3 * 3 floats per vec3 * 4 bytes per float
  3. const vsUniformBuffer = device.createBuffer({
  4. size: vUniformBufferSize,
  5. usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  6. });
  7. const fsUniformBuffer = device.createBuffer({
  8. size: fUniformBufferSize,
  9. usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  10. });
  11. const vsUniformValues = new Float32Array(2 * 16); // 2 mat4s
  12. const worldViewProjection = vsUniformValues.subarray(0, 16);
  13. const worldInverseTranspose = vsUniformValues.subarray(16, 32);
  14. const fsUniformValues = new Float32Array(3); // 1 vec3
  15. 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。

  1. // happens at render time
  2. gl.activeTexture(gl.TEXTURE0);
  3. gl.bindTexture(gl.TEXTURE_2D, tex);
  4. WebGPU
  5. // can happen at init time
  6. const bindGroup = device.createBindGroup({
  7. layout: pipeline.getBindGroupLayout(0),
  8. entries: [
  9. { binding: 0, resource: { buffer: vsUniformBuffer } },
  10. { binding: 1, resource: { buffer: fsUniformBuffer } },
  11. { binding: 2, resource: sampler },
  12. { binding: 3, resource: tex.createView() },
  13. ],
  14. });

BindGroup资源设置一定要匹配 shader 中 声明的资源格式。

WebGL

  1. gl.clearColor(0.5, 0.5, 0.5, 1.0);
  2. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

WebGPU

  1. const renderPassDescriptor = {
  2. colorAttachments: [
  3. {
  4. // view: undefined, // Assigned later
  5. // resolveTarget: undefined, // Assigned Later
  6. clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 },
  7. loadOp: 'clear',
  8. storeOp: 'store',
  9. },
  10. ],
  11. depthStencilAttachment: {
  12. // view: undefined, // Assigned later
  13. depthClearValue: 1,
  14. depthLoadOp: 'clear',
  15. depthStoreOp: 'store',
  16. },
  17. };
    • 设置Uniform

WebGL

  1. gl.uniform3fv(u_lightDirectionLoc, v3.normalize([1, 8, -10]));
  2. gl.uniform1i(u_diffuseLoc, 0);
  3. gl.uniformMatrix4fv(u_worldInverseTransposeLoc, false, m4.transpose(m4.inverse(world)));
  4. gl.uniformMatrix4fv(u_worldViewProjectionLoc, false, m4.multiply(viewProjection, world));

WebGPU

  1. m4.transpose(m4.inverse(world), worldInverseTranspose);
  2. m4.multiply(viewProjection, world, worldViewProjection);
  3. v3.normalize([1, 8, -10], lightDirection);
  4. device.queue.writeBuffer(vsUniformBuffer, 0, vsUniformValues);
  5. device.queue.writeBuffer(fsUniformBuffer, 0, fsUniformValues);

在 WebGL中,通过调用类似gl.uniform??? 这样的API更改 uniform 的值。在WebGPU中,直接修改缓冲区关联的数据并上传到GPU。 WebGPU与Vulkan 相比,简化了数据上传同步等一系列操作。

    • 绘制缩放

WebGL会自己管理缩放导致的帧缓冲区重建过程,WebGPU 中你需要显式地重建绘图关联的颜色和深度纹理、纹理视图。

WebGL

  1. function resizeCanvasToDisplaySize(canvas) {
  2. const width = canvas.clientWidth;
  3. const height = canvas.clientHeight;
  4. const needResize = width !== canvas.width || height !== canvas.height;
  5. if (needResize) {
  6. canvas.width = width;
  7. canvas.height = height;
  8. }
  9. return needResize;
  10. }

WebGPU

  1. // At init time
  2. const canvas = document.querySelector('canvas');
  3. const context = canvas.getContext('webgpu');
  4. const presentationFormat = gpu.getPreferredFormat(adapter);
  5. context.configure({
  6. device,
  7. format: presentationFormat,
  8. });
  9. const canvasInfo = {
  10. canvas,
  11. presentationFormat,
  12. // these are filled out in resizeToDisplaySize
  13. renderTarget: undefined,
  14. renderTargetView: undefined,
  15. depthTexture: undefined,
  16. depthTextureView: undefined,
  17. sampleCount: 4, // can be 1 or 4
  18. };
  19. // --- At render time ---
  20. function resizeToDisplaySize(device, canvasInfo) {
  21. const {
  22. canvas,
  23. context,
  24. renderTarget,
  25. presentationFormat,
  26. depthTexture,
  27. sampleCount,
  28. } = canvasInfo;
  29. const width = Math.min(device.limits.maxTextureDimension2D, canvas.clientWidth);
  30. const height = Math.min(device.limits.maxTextureDimension2D, canvas.clientHeight);
  31. const needResize = !canvasInfo.renderTarget ||
  32. width !== canvas.width ||
  33. height !== canvas.height;
  34. if (needResize) {
  35. if (renderTarget) {
  36. renderTarget.destroy();
  37. }
  38. if (depthTexture) {
  39. depthTexture.destroy();
  40. }
  41. canvas.width = width;
  42. canvas.height = height;
  43. if (sampleCount > 1) {
  44. const newRenderTarget = device.createTexture({
  45. size: [canvas.width, canvas.height],
  46. format: presentationFormat,
  47. sampleCount,
  48. usage: GPUTextureUsage.RENDER_ATTACHMENT,
  49. });
  50. canvasInfo.renderTarget = newRenderTarget;
  51. canvasInfo.renderTargetView = newRenderTarget.createView();
  52. }
  53. const newDepthTexture = device.createTexture({
  54. size: [canvas.width, canvas.height,
  55. format: 'depth24plus',
  56. sampleCount,
  57. usage: GPUTextureUsage.RENDER_ATTACHMENT,
  58. });
  59. canvasInfo.depthTexture = newDepthTexture;
  60. canvasInfo.depthTextureView = newDepthTexture.createView();
  61. }
  62. return needResize;
  63. }

在上面的代码中, 多重采样样本数 sampleCount ,如果值为4, 等效于打开了多重采样功能, 如果值为 1 就相当于关闭了多重采样。

WebGL会尽量不耗尽内存,这意味着如果你要求一个16000x16000的canvas,WebGL可能会给你一个4096x4096的画布。你可以通过查看gl.drawingBufferWidth和gl.drawingBufferHeight找到你实际返回的canvas大小。

WebGL这样做的原因是(1)在多个显示器上拉伸画布可能会使大小超过GPU可以处理的大小(2)系统可能内存不足,而不是崩溃,WebGL将返回一个较小的绘图缓冲区。

在WebGPU中,检查这两种情况取决于你。我们正在检查上面的情况(1)。对于情况(2),我们必须自己检查内存是否不足,就像WebGPU中的其他事情一样,这样做是异步的。

  1. device.pushErrorScope('out-of-memory');
  2. context.configure({...});
  3. if (sampleCount > 1) {
  4. const newRenderTarget = device.createTexture({...});
  5. ...
  6. }
  7. const newDepthTexture = device.createTexture({...});
  8. ...
  9. device.popErrorScope().then(error => {
  10. if (error) {
  11. // we're out of memory, try a smaller size?
  12. }
  13. });
    • 绘制

WebGL

  1. gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
  2. gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  3. ...
  4. gl.activeTexture(gl.TEXTURE0);
  5. gl.bindTexture(gl.TEXTURE_2D, tex);
  6. gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  7. gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);
  8. gl.enableVertexAttribArray(positionLoc);
  9. gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
  10. gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0);
  11. gl.enableVertexAttribArray(normalLoc);
  12. gl.bindBuffer(gl.ARRAY_BUFFER, texcoordBuffer);
  13. gl.vertexAttribPointer(texcoordLoc, 2, gl.FLOAT, false, 0, 0);
  14. gl.enableVertexAttribArray(texcoordLoc);
  15. ...
  16. gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indicesBuffer);
  17. gl.drawElements(gl.TRIANGLES, 6 * 6, gl.UNSIGNED_SHORT, 0);

WebGPU

  1. if (canvasInfo.sampleCount === 1) {
  2. const colorTexture = context.getCurrentTexture();
  3. renderPassDescriptor.colorAttachments[0].view = colorTexture.createView();
  4. } else {
  5. renderPassDescriptor.colorAttachments[0].view = canvasInfo.renderTargetView;
  6. renderPassDescriptor.colorAttachments[0].resolveTarget = context.getCurrentTexture().createView();
  7. }
  8. renderPassDescriptor.depthStencilAttachment.view = canvasInfo.depthTextureView;
  9. const commandEncoder = device.createCommandEncoder();
  10. const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
  11. passEncoder.setPipeline(pipeline);
  12. passEncoder.setBindGroup(0, bindGroup);
  13. passEncoder.setVertexBuffer(0, positionBuffer);
  14. passEncoder.setVertexBuffer(1, normalBuffer);
  15. passEncoder.setVertexBuffer(2, texcoordBuffer);
  16. passEncoder.setIndexBuffer(indicesBuffer, 'uint16');
  17. passEncoder.drawIndexed(indices.length);
  18. passEncoder.end();
  19. 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貌似都不支持大于一个像素的线宽。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/727323
推荐阅读
相关标签
  

闽ICP备14008679号