大家好,本文学习Chrome->webgpu-samplers->rotatingCube示例。

上一篇博文:

WebGPU学习(五): 现代图形API技术要点和WebGPU支持情况调研

下一篇博文:

WebGPU学习(七):学习“twoCubes”和“instancedCube”示例

学习rotatingCube.ts

我们已经学习了“绘制三角形”的示例,与它相比,本示例增加了以下的内容:

  • 增加一个uniform buffer object(简称为ubo),用于传输“model矩阵 乘以 view矩阵 乘以 projection矩阵”的结果矩阵(简称为mvp矩阵),并在每帧被更新
  • 设置顶点
  • 开启面剔除
  • 开启深度测试

下面,我们打开rotatingCube.ts文件,依次来看下新增内容:

增加一个uniform buffer object

介绍

在WebGL 1中,我们通过uniform1i,uniform4fv等函数传递每个gameObject对应的uniform变量(如diffuseMap, diffuse color, model matrix等)到shader中。

其中很多相同的值是不需要被传递的,举例如下:

如果gameObject1和gameObject3使用同一个shader1,它们的diffuse color相同,那么只需要传递其中的一个diffuse color,而在WebGL 1中我们一般把这两个diffuse color都传递了,造成了重复的开销。

WebGPU使用uniform buffer object来传递uniform变量。uniform buffer是一个全局的buffer,我们只需要设置一次值,然后在每次draw之前,设置使用的数据范围(通过offset, size来设置),从而复用相同的数据。如果uniform值有变化,则只需要修改uniform buffer对应的数据。

在WebGPU中,我们可以把所有gameObject的model矩阵设为一个ubo,所有相机的view和projection矩阵设为一个ubo,每一种material(如phong material,pbr material等)的数据(如diffuse color,specular color等)设为一个ubo,每一种light(如direction light、point light等)的数据(如light color、light position等)设为一个ubo,这样可以有效减少uniform变量的传输开销。

另外,我们需要注意ubo的内存布局:

默认的布局为std140,我们可以粗略地理解为,它约定了每一列都有4个元素。

我们来举例说明:

下面的ubo对应的uniform block,定义布局为std140:

  1. layout (std140) uniform ExampleBlock
  2. {
  3. float value;
  4. vec3 vector;
  5. mat4 matrix;
  6. float values[3];
  7. bool boolean;
  8. int integer;
  9. };

它在内存中的实际布局为:

  1. layout (std140) uniform ExampleBlock
  2. {
  3. // base alignment // aligned offset
  4. float value; // 4 // 0
  5. vec3 vector; // 16 // 16 (must be multiple of 16 so 4->16)
  6. mat4 matrix; // 16 // 32 (column 0)
  7. // 16 // 48 (column 1)
  8. // 16 // 64 (column 2)
  9. // 16 // 80 (column 3)
  10. float values[3]; // 16 // 96 (values[0])
  11. // 16 // 112 (values[1])
  12. // 16 // 128 (values[2])
  13. bool boolean; // 4 // 144
  14. int integer; // 4 // 148
  15. };

也就是说,这个ubo的第一个元素为value,第2-4个元素为0(为了对齐);

第5-7个元素为vector的x、y、z的值,第8个元素为0;

第9-24个元素为matrix的值(列优先);

第25-27个元素为values数组的值,第28个元素为0;

第29个元素为boolean转为float的值,第30-32个元素为0;

第33个元素为integer转为float的值,第34-36个元素为0。

分析本示例对应的代码

  • 在vertex shader中定义uniform block

代码如下:

  1. const vertexShaderGLSL = `#version 450
  2. layout(set = 0, binding = 0) uniform Uniforms {
  3. mat4 modelViewProjectionMatrix;
  4. } uniforms;
  5. ...
  6. void main() {
  7. gl_Position = uniforms.modelViewProjectionMatrix * position;
  8. fragColor = color;
  9. }
  10. `;

布局为默认的std140,指定了set和binding,包含一个mvp矩阵

其中set和binding用来对应相应的数据,会在后面说明

  • 创建uniformsBindGroupLayout

代码如下:

  1. const uniformsBindGroupLayout = device.createBindGroupLayout({
  2. bindings: [{
  3. binding: 0,
  4. visibility: 1,
  5. type: "uniform-buffer"
  6. }]
  7. });

binding对应vertex shader中uniform block的binding,意思是bindings数组的第一个元素的对应binding为0的uniform block

visibility为GPUShaderStage.VERTEX(等于1),指定type为“uniform-buffer”

  • 创建uniform buffer

代码如下:

  1. const uniformBufferSize = 4 * 16; // BYTES_PER_ELEMENT(4) * matrix length(4 * 4 = 16)
  2. const uniformBuffer = device.createBuffer({
  3. size: uniformBufferSize,
  4. usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
  5. });
  • 创建uniform bind group

代码如下:

  1. const uniformBindGroup = device.createBindGroup({
  2. layout: uniformsBindGroupLayout,
  3. bindings: [{
  4. binding: 0,
  5. resource: {
  6. buffer: uniformBuffer,
  7. },
  8. }],
  9. });

binding对应vertex shader中uniform block的binding,意思是bindings数组的第一个元素的对应binding为0的uniform block

  • 每一帧更新uniform buffer的mvp矩阵数据

代码如下:

  1. //因为是固定相机,所以只需要计算一次projection矩阵
  2. const aspect = Math.abs(canvas.width / canvas.height);
  3. let projectionMatrix = mat4.create();
  4. mat4.perspective(projectionMatrix, (2 * Math.PI) / 5, aspect, 1, 100.0);
  5. ...
  6. //计算mvp矩阵
  7. function getTransformationMatrix() {
  8. let viewMatrix = mat4.create();
  9. mat4.translate(viewMatrix, viewMatrix, vec3.fromValues(0, 0, -5));
  10. let now = Date.now() / 1000;
  11. mat4.rotate(viewMatrix, viewMatrix, 1, vec3.fromValues(Math.sin(now), Math.cos(now), 0));
  12. let modelViewProjectionMatrix = mat4.create();
  13. mat4.multiply(modelViewProjectionMatrix, projectionMatrix, viewMatrix);
  14. return modelViewProjectionMatrix;
  15. }
  16. ...
  17. return function frame() {
  18. //使用setSubData更新uniform buffer,后面分析
  19. uniformBuffer.setSubData(0, getTransformationMatrix());
  20. ...
  21. }
  • draw之前设置bind group

代码如下:

  1. return function frame() {
  2. ...
  3. //“0”对应vertex shader中uniform block的“set = 0”
  4. passEncoder.setBindGroup(0, uniformBindGroup);
  5. passEncoder.draw(36, 1, 0, 0);
  6. ...
  7. }

详细分析“更新uniform buffer”

本示例使用setSubData来更新uniform buffer:

  1. return function frame() {
  2. uniformBuffer.setSubData(0, getTransformationMatrix());
  3. ...
  4. }

我们在WebGPU学习(五): 现代图形API技术要点和WebGPU支持情况调研->Approaching zero driver overhead->persistent map buffer中,提到了WebGPU目前有两种方法实现“CPU把数据传输到GPU“,即更新GPUBuffer的值:

1.调用GPUBuffer->setSubData方法

2.使用persistent map buffer技术

这里使用了第1种方法。

我们看下如何在本示例中使用第2种方法:

  1. function setBufferDataByPersistentMapBuffer(device, commandEncoder, uniformBufferSize, uniformBuffer, mvpMatricesData) {
  2. const [srcBuffer, arrayBuffer] = device.createBufferMapped({
  3. size: uniformBufferSize,
  4. usage: GPUBufferUsage.COPY_SRC
  5. });
  6. new Float32Array(arrayBuffer).set(mvpMatricesData);
  7. srcBuffer.unmap();
  8. commandEncoder.copyBufferToBuffer(srcBuffer, 0, uniformBuffer, 0, uniformBufferSize);
  9. const commandBuffer = commandEncoder.finish();
  10. const queue = device.defaultQueue;
  11. queue.submit([commandBuffer]);
  12. srcBuffer.destroy();
  13. }
  14. return function frame() {
  15. //uniformBuffer.setSubData(0, getTransformationMatrix());
  16. ...
  17. const commandEncoder = device.createCommandEncoder({});
  18. setBufferDataByPersistentMapBuffer(device, commandEncoder, uniformBufferSize, uniformBuffer, getTransformationMatrix());
  19. ...
  20. }

为了验证性能,我做了benchmark测试,创建一个包含160000个mat4的ubo,使用这2种方法来更新uniform buffer,比较它们的js profile:

使用setSubData(调用setBufferDataBySetSubData函数):

setSubData占91.54%

使用persistent map buffer(调用setBufferDataByPersistentMapBuffer函数):

createBufferMapped和setBufferDataByPersistentMapBuffer占72.72+18.06=90.78%

可以看到两个的性能差不多。但考虑到persistent map buffer从实现原理上要更快(cpu和gpu共用一个buffer,不需要copy),因此应该优先使用该方法。

另外,WebGPU社区现在还在讨论如何优化更新buffer数据(如有人提出增加GPUUploadBuffer pass),因此我们还需要继续关注该方面的进展。

参考资料

Advanced-GLSL->Uniform buffer objects

设置顶点

  • 传输顶点的position和color数据到vertex shader的attribute(在glsl 4.5中用“in”表示attribute)中

代码如下:

  1. const vertexShaderGLSL = `#version 450
  2. ...
  3. layout(location = 0) in vec4 position;
  4. layout(location = 1) in vec4 color;
  5. layout(location = 0) out vec4 fragColor;
  6. void main() {
  7. gl_Position = uniforms.modelViewProjectionMatrix * position;
  8. fragColor = color;
  9. }
  10. const fragmentShaderGLSL = `#version 450
  11. layout(location = 0) in vec4 fragColor;
  12. layout(location = 0) out vec4 outColor;
  13. void main() {
  14. outColor = fragColor;
  15. }
  16. `;

在vertex shader中设置color为fragColor(在glsl 4.5中用“out”表示WebGL 1的varying变量),然后在fragment shader中接收fragColor,将其设置为outColor,从而将fragment的color设置为对应顶点的color

  • 创建vertices buffer,设置立方体的顶点数据

代码如下:

  1. cube.ts:
  2. //每个顶点包含position,color,uv数据
  3. //本示例没用到uv数据
  4. export const cubeVertexArray = new Float32Array([
  5. // float4 position, float4 color, float2 uv,
  6. 1, -1, 1, 1, 1, 0, 1, 1, 1, 1,
  7. -1, -1, 1, 1, 0, 0, 1, 1, 0, 1,
  8. -1, -1, -1, 1, 0, 0, 0, 1, 0, 0,
  9. 1, -1, -1, 1, 1, 0, 0, 1, 1, 0,
  10. 1, -1, 1, 1, 1, 0, 1, 1, 1, 1,
  11. -1, -1, -1, 1, 0, 0, 0, 1, 0, 0,
  12. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  13. 1, -1, 1, 1, 1, 0, 1, 1, 0, 1,
  14. 1, -1, -1, 1, 1, 0, 0, 1, 0, 0,
  15. 1, 1, -1, 1, 1, 1, 0, 1, 1, 0,
  16. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  17. 1, -1, -1, 1, 1, 0, 0, 1, 0, 0,
  18. -1, 1, 1, 1, 0, 1, 1, 1, 1, 1,
  19. 1, 1, 1, 1, 1, 1, 1, 1, 0, 1,
  20. 1, 1, -1, 1, 1, 1, 0, 1, 0, 0,
  21. -1, 1, -1, 1, 0, 1, 0, 1, 1, 0,
  22. -1, 1, 1, 1, 0, 1, 1, 1, 1, 1,
  23. 1, 1, -1, 1, 1, 1, 0, 1, 0, 0,
  24. -1, -1, 1, 1, 0, 0, 1, 1, 1, 1,
  25. -1, 1, 1, 1, 0, 1, 1, 1, 0, 1,
  26. -1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
  27. -1, -1, -1, 1, 0, 0, 0, 1, 1, 0,
  28. -1, -1, 1, 1, 0, 0, 1, 1, 1, 1,
  29. -1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
  30. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  31. -1, 1, 1, 1, 0, 1, 1, 1, 0, 1,
  32. -1, -1, 1, 1, 0, 0, 1, 1, 0, 0,
  33. -1, -1, 1, 1, 0, 0, 1, 1, 0, 0,
  34. 1, -1, 1, 1, 1, 0, 1, 1, 1, 0,
  35. 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
  36. 1, -1, -1, 1, 1, 0, 0, 1, 1, 1,
  37. -1, -1, -1, 1, 0, 0, 0, 1, 0, 1,
  38. -1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
  39. 1, 1, -1, 1, 1, 1, 0, 1, 1, 0,
  40. 1, -1, -1, 1, 1, 0, 0, 1, 1, 1,
  41. -1, 1, -1, 1, 0, 1, 0, 1, 0, 0,
  42. ]);
  1. rotatingCube.ts:
  2. const verticesBuffer = device.createBuffer({
  3. size: cubeVertexArray.byteLength,
  4. usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
  5. });
  6. verticesBuffer.setSubData(0, cubeVertexArray);

因为只需要设置一次顶点数据,所以这里可以使用setSubData来设置GPUBuffer的数据,对性能影响不大

  • 创建render pipeline时,指定vertex shader的attribute

代码如下:

  1. cube.ts:
  2. export const cubeVertexSize = 4 * 10; // Byte size of one cube vertex.
  3. export const cubePositionOffset = 0;
  4. export const cubeColorOffset = 4 * 4; // Byte offset of cube vertex color attribute.
  1. rotatingCube.ts:
  2. const pipeline = device.createRenderPipeline({
  3. ...
  4. vertexState: {
  5. vertexBuffers: [{
  6. arrayStride: cubeVertexSize,
  7. attributes: [{
  8. // position
  9. shaderLocation: 0,
  10. offset: cubePositionOffset,
  11. format: "float4"
  12. }, {
  13. // color
  14. shaderLocation: 1,
  15. offset: cubeColorOffset,
  16. format: "float4"
  17. }]
  18. }],
  19. },
  20. ...
  21. });
  • render pass->draw指定顶点个数为36

代码如下:

  1. return function frame() {
  2. ...
  3. const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
  4. ...
  5. passEncoder.draw(36, 1, 0, 0);
  6. passEncoder.endPass();
  7. ...
  8. }

开启面剔除

相关代码为:

  1. const pipeline = device.createRenderPipeline({
  2. ...
  3. rasterizationState: {
  4. cullMode: 'back',
  5. },
  6. ...
  7. });

相关的定义为:

  1. enum GPUFrontFace {
  2. "ccw",
  3. "cw"
  4. };
  5. enum GPUCullMode {
  6. "none",
  7. "front",
  8. "back"
  9. };
  10. ...
  11. dictionary GPURasterizationStateDescriptor {
  12. GPUFrontFace frontFace = "ccw";
  13. GPUCullMode cullMode = "none";
  14. ...
  15. };

其中ccw表示逆时针,cw表示顺时针;frontFace用来设置哪个方向是“front”(正面);cullMode用来设置将哪一面剔除掉。

因为本示例没有设置frontFace,因此frontFace为默认的ccw,即将顶点连接的逆时针方向设置为正面;

又因为本示例设置了cullMode为back,那么反面的顶点(即顺时针连接的顶点)会被剔除掉。

参考资料

[WebGL入门]六,顶点和多边形

Investigation: Rasterization State

开启深度测试

现在分析相关代码,忽略与模版测试相关的代码:

  • 创建render pipeline时,设置depthStencilState

代码如下:

  1. const pipeline = device.createRenderPipeline({
  2. ...
  3. depthStencilState: {
  4. //开启深度测试
  5. depthWriteEnabled: true,
  6. //设置比较函数为less,后面会说明
  7. depthCompare: "less",
  8. //设置depth为24bit
  9. format: "depth24plus-stencil8",
  10. },
  11. ...
  12. });
  • 创建depth texture(注意它的size->depth为1),将它的view设置为render pass -> depthStencilAttachment -> attachment

代码如下:

  1. const depthTexture = device.createTexture({
  2. size: {
  3. width: canvas.width,
  4. height: canvas.height,
  5. depth: 1
  6. },
  7. format: "depth24plus-stencil8",
  8. usage: GPUTextureUsage.OUTPUT_ATTACHMENT
  9. });
  10. const renderPassDescriptor: GPURenderPassDescriptor = {
  11. ...
  12. depthStencilAttachment: {
  13. attachment: depthTexture.createView(),
  14. depthLoadValue: 1.0,
  15. depthStoreOp: "store",
  16. ...
  17. }
  18. };

其中,depthStencilAttachment的定义为:

  1. dictionary GPURenderPassDepthStencilAttachmentDescriptor {
  2. required GPUTextureView attachment;
  3. required (GPULoadOp or float) depthLoadValue;
  4. required GPUStoreOp depthStoreOp;
  5. ...
  6. };

depthLoadValue和depthStoreOp与WebGPU学习(二): 学习“绘制一个三角形”示例->分析render pass->colorAttachment的loadOp和StoreOp类似,我们来看下相关的代码:


  1. const pipeline = device.createRenderPipeline({
  2. ...
  3. depthStencilState: {
  4. ...
  5. depthCompare: "less",
  6. ...
  7. },
  8. ...
  9. });
  10. ...
  11. const renderPassDescriptor: GPURenderPassDescriptor = {
  12. ...
  13. depthStencilAttachment: {
  14. ...
  15. depthLoadValue: 1.0,
  16. depthStoreOp: "store",
  17. ...
  18. }
  19. };

在深度测试时,gpu会将fragment的z值(范围为[0.0-1.0])与这里设置的depthLoadValue值(这里为1.0)比较。其中使用depthCompare定义的函数(这里为less,意思是所有z值大于等于1.0的fragment会被剔除)进行比较。

参考资料

Depth testing

最终渲染结果

参考资料

WebGPU规范

webgpu-samplers Github Repo

WebGPU-5

WebGPU学习(六):学习“rotatingCube”示例的更多相关文章

  1. WebGPU学习(七):学习“twoCubes”和“instancedCube”示例

    大家好,本文学习Chrome->webgpu-samplers->twoCubes和instancedCube示例. 这两个示例都与"rotatingCube"示例差不 ...

  2. C#多线程学习(六) 互斥对象

    如何控制好多个线程相互之间的联系,不产生冲突和重复,这需要用到互斥对象,即:System.Threading 命名空间中的 Mutex 类. 我们可以把Mutex看作一个出租车,乘客看作线程.乘客首先 ...

  3. Hbase深入学习(六) Java操作HBase

    Hbase深入学习(六) ―― Java操作HBase 本文讲述如何用hbase shell命令和hbase java api对hbase服务器进行操作. 先看以下读取一行记录hbase是如何进行工作 ...

  4. TweenMax动画库学习(六)

    目录            TweenMax动画库学习(一)            TweenMax动画库学习(二)            TweenMax动画库学习(三)            Tw ...

  5. Deep Learning(深度学习)学习笔记整理系列之(六)

    Deep Learning(深度学习)学习笔记整理系列 zouxy09@qq.com http://blog.csdn.net/zouxy09 作者:Zouxy version 1.0 2013-04 ...

  6. SVG 学习<六> SVG的transform

    目录 SVG 学习<一>基础图形及线段 SVG 学习<二>进阶 SVG世界,视野,视窗 stroke属性 svg分组 SVG 学习<三>渐变 SVG 学习<四 ...

  7. Angular 快速学习笔记(1) -- 官方示例要点

    创建组件 ng generate component heroes {{ hero.name }} {{}}语法绑定数据 管道pipe 格式化数据 <h2>{{ hero.name | u ...

  8. SQL 数据库 学习 007 通过一个示例简单介绍什么是字段、属性、列、元组、记录、表、主键、外键 (上)

    SQL 数据库 学习 007 通过一个示例简单介绍什么是字段.属性.列.元组.记录.表.主键.外键 (上) 我们来介绍一下:数据库是如何存储数据的. 数据库是如何存储数据的 来看一个小例子 scott ...

  9. Unity学习(六)5.x依赖打包

    http://blog.sina.com.cn/s/blog_89d90b7c0102w2ox.html unity5已经封装好了接口,所以依赖打包并没有那么神秘和复杂了. 打包: 1.定义好资源的a ...

随机推荐

  1. AE10.0在Visual Studio 2012下安装没有模板(转)

    转自百度经验: VS2012中丢失ArcGIS模板的解决方法 由于ArcGIS10.0(for .NET)默认是用VS2010作为开发工具的,所以在先安装VS2012后装ArcGIS10.0 桌面版及 ...

  2. 第二章 Unity Shader基础

    [TOC] 1. Unity Shader 的基础: ShaderLab 学习和编写着色器的过程一直是一个学习曲线很陡峭的过程,通常情况下为了自定义渲染效果往往要和很多文件和设置打交道,这些设置很容易 ...

  3. Spring Boot 自动装配(一)

    目录 目录 前言 1.起源 2.Spring 模式注解 2.1.装配方式 2.2.派生性 3.Spring @Enable 模块驱动 3.1.Spring框架中@Enable实现方式 3.2.自定义@ ...

  4. UML类图绘制

    UML图简介 含义:UML-Unified Modeling Language 统一建模语言,又称标准建模语言.是用来对软件密集系统进行可视化建模的一种语言 主要模型: 功能模型:从用户的角度展示系统 ...

  5. zz:NETCONF协议详解

    随着SDN的大热,一个诞生了十年之久的协议焕发了第二春,它就是NETCONF协议.如果你在两年前去搜索NETCONF协议,基本得到的信息都是"这个协议是一个网管协议,主要目的是弥补SNMP协 ...

  6. 挑战10个最难的Java面试题(附答案)【上】

    欢迎添加华为云小助手微信(微信号:HWCloud002 或 HWCloud003),验证通过后,输入关键字"加群",加入华为云线上技术讨论群:输入关键字"最新活动&quo ...

  7. 曹工杂谈:Spring boot应用,自己动手用Netty替换底层Tomcat容器

    前言 问:标题说的什么意思? 答:简单说,一个spring boot应用(我这里,版本升到2.1.7.Release了,没什么问题),默认使用了tomcat作为底层容器来接收和处理连接. 我这里,在依 ...

  8. Android 内存泄漏原因

    Android 手机给应用分配的堆内存通常是8 M 左右, 如果内存处理不当很容易造成 OOM (OutOfMemoryError),OOM 主要由于一下这些原因引起的: 1.  数据库 Cursor ...

  9. ajax异步请求的三种常见方式

    首先先介绍下ajax,ajax(ASynchronous JavaScript And XML)为异步的javascript和xml.所谓的异步和同步是指: 同步:客户端必须等待服务器的响应,在等待期 ...

  10. iOS设计模式之:建造者模式Builder Pattern,用于改进初始化参数

    转自:http://www.cnblogs.com/wengzilin/p/4365855.html 本文主要讨论一下iOS中的Builder Pattern.与网上很多版本不同,本文不去长篇大论地解 ...