WebGPU学习(八):学习“texturedCube”示例

简介: WebGPU学习(八):学习“texturedCube”示例

学习texturedCube.ts

最终渲染结果:

该示例绘制了有一个纹理的立方体。

与“rotatingCube”示例相比,该示例增加了下面的步骤:

  • 传输顶点的uv数据
  • 增加了sampler和sampled-texture类型的uniform数据

下面,我们打开texturedCube.ts文件,依次分析增加的步骤:

传递顶点的uv数据

  • shader加入uv attribute

代码如下:

  const vertexShaderGLSL = `#version 450
  ...
  layout(location = 0) in vec4 position;
  layout(location = 1) in vec2 uv;
  layout(location = 0) out vec2 fragUV;
  layout(location = 1) out vec4 fragPosition;
  void main() {
    fragPosition = 0.5 * (position + vec4(1.0));
    ...
    fragUV = uv;
  }
  `;
  
  const fragmentShaderGLSL = `#version 450
  layout(set = 0, binding = 1) uniform sampler mySampler;
  layout(set = 0, binding = 2) uniform texture2D myTexture;
  layout(location = 0) in vec2 fragUV;
  layout(location = 1) in vec4 fragPosition;
  layout(location = 0) out vec4 outColor;
  void main() {
    outColor =  texture(sampler2D(myTexture, mySampler), fragUV) * fragPosition;
  }
  `;

vertex shader传入了uv attribute数据,并将其传递给fragUV,从而传到fragment shader,作为纹理采样坐标

另外,这里可以顺便说明下:fragPosition用来实现与position相关的颜色渐变效果

  • uv数据包含在verticesBuffer的cubeVertexArray中

cubeVertexArray的代码如下:

cube.ts:
export const cubeUVOffset = 4 * 8;
export const cubeVertexArray = new Float32Array([
    // float4 position, float4 color, float2 uv,
    1, -1, 1, 1,   1, 0, 1, 1,  1, 1,
    -1, -1, 1, 1,  0, 0, 1, 1,  0, 1,
    -1, -1, -1, 1, 0, 0, 0, 1,  0, 0,
    1, -1, -1, 1,  1, 0, 0, 1,  1, 0,
    1, -1, 1, 1,   1, 0, 1, 1,  1, 1,
    -1, -1, -1, 1, 0, 0, 0, 1,  0, 0,
 
    ...
]);

创建和设置verticesBuffer的相关代码如下:

texturedCube.ts:
  const verticesBuffer = device.createBuffer({
    size: cubeVertexArray.byteLength,
    usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
  });
  verticesBuffer.setSubData(0, cubeVertexArray);
  
  ...
  
  return function frame() {
    ...
    passEncoder.setVertexBuffer(0, verticesBuffer);
    ...
  } 
  • 创建render pipeline时指定uv attribute的相关数据

代码如下:

  const pipeline = device.createRenderPipeline({
    ...
    vertexState: {
      vertexBuffers: [{
        ...
        attributes: [
        ...
        {
          // uv
          shaderLocation: 1,
          offset: cubeUVOffset,
          format: "float2"
        }]
      }],
    },
    ...
  });    

增加了sampler和sampled-texture类型的uniform数据

WebGPU相对于WebGL1,提出了sampler,可以对它设置filter、wrap等参数,从而实现了texture和sampler自由组合,同一个texture能够以不同filter、wrap来采样

  • fragment shader传入这两个uniform数据,用于纹理采样

代码如下:

  const fragmentShaderGLSL = `#version 450
  layout(set = 0, binding = 1) uniform sampler mySampler;
  layout(set = 0, binding = 2) uniform texture2D myTexture;
  layout(location = 0) in vec2 fragUV;
  layout(location = 1) in vec4 fragPosition;
  layout(location = 0) out vec4 outColor;
  void main() {
    outColor =  texture(sampler2D(myTexture, mySampler), fragUV) * fragPosition;
  }
  `;
  • 创建bind group layout时指定它们在shader中的binding位置等参数

代码如下:

  const bindGroupLayout = device.createBindGroupLayout({
    bindings: [
    ...
    {
      // Sampler
      binding: 1,
      visibility: GPUShaderStage.FRAGMENT,
      type: "sampler"
    }, {
      // Texture view
      binding: 2,
      visibility: GPUShaderStage.FRAGMENT,
      type: "sampled-texture"
    }]
  });
  • 拷贝图片到texture,返回texture

代码如下,后面会进一步研究:

  const cubeTexture = await createTextureFromImage(device, 'assets/img/Di-3d.png', GPUTextureUsage.SAMPLED);
  • 创建sampler,指定filter

代码如下:

  const sampler = device.createSampler({
    magFilter: "linear",
    minFilter: "linear",
  });

我们看一下相关定义:

GPUSampler createSampler(optional GPUSamplerDescriptor descriptor = {});
 
...
 
dictionary GPUSamplerDescriptor : GPUObjectDescriptorBase {
    GPUAddressMode addressModeU = "clamp-to-edge";
    GPUAddressMode addressModeV = "clamp-to-edge";
    GPUAddressMode addressModeW = "clamp-to-edge";
    GPUFilterMode magFilter = "nearest";
    GPUFilterMode minFilter = "nearest";
    GPUFilterMode mipmapFilter = "nearest";
    float lodMinClamp = 0;
    float lodMaxClamp = 0xffffffff;
    GPUCompareFunction compare = "never";
};

GPUSamplerDescriptor的addressMode指定了texture在u、v、w方向的wrap mode(u、v方向的wrap相当于WebGL1的wrapS、wrapT)(w方向是给3d texture用的)

mipmapFilter与mipmap有关,lodXXX与texture lod有关,compare与软阴影的Percentage Closer Filtering技术有关,我们不讨论它们

  • 创建uniform bind group时传入sampler和texture的view
  const uniformBindGroup = device.createBindGroup({
    layout: bindGroupLayout,
    bindings: [
    ...
    {
      binding: 1,
      resource: sampler,
    }, {
      binding: 2,
      resource: cubeTexture.createView(),
    }],
  });

参考资料

Sampler Object

详细分析“拷贝图片到texture”步骤

相关代码如下:

  const cubeTexture = await createTextureFromImage(device, 'assets/img/Di-3d.png', GPUTextureUsage.SAMPLED);

该步骤可以分解为两步:

1.解码图片

2.拷贝解码后的类型为HTMLImageElement的图片到GPU的texture中

下面依次分析:

解码图片

打开helper.ts文件,查看createTextureFromImage对应代码:

  const img = document.createElement('img');
  img.src = src;
  await img.decode();

这里使用decode api来解码图片,也可以使用img.onload来实现:

  const img = document.createElement('img');
  img.src = src;
  img.onload = (img) => {
    ...
  };

根据Pre-Loading and Pre-Decoding Images with Javascript for Better Performance的说法,图片的加载过程有两个步骤:

1.从服务器加载图片

2.解码图片

第1步都是在其它线程上并行执行;

如果用onload,则浏览器会在主线程上同步执行第2步,会阻塞主线程;

如果用decode api,则浏览器会在其它线程上并行执行第2步,不会阻塞主线程。

chrome和firefox浏览器都支持decode api,因此加载图片应该优先使用该API:

参考资料

Pre-Loading and Pre-Decoding Images with Javascript for Better Performance

Chrome 图片解码与 Image.decode API

拷贝图片

WebGL1直接使用texImage2D将图片上传到GPU texture中,而WebGPU能让我们更加灵活地控制上传过程。

WebGPU有两种方法上传:

  • 创建图片对应的imageBitmap,将其拷贝到GPU texture中

该方法要用到copyImageBitmapToTexture函数。虽然WebGPU规范已经定义了该函数,但目前Chrome Canary不支持它,所以暂时不能用该方法上传。

参考资料

Proposal for copyImageBitmapToTexture

ImageBitmapToTexture design

  • 将图片绘制到canvas中,通过getImageData获得数据->将其设置到buffer中->把buffer数据拷贝到GPU texture中

我们来看下createTextureFromImage对应代码:

  const imageCanvas = document.createElement('canvas');
  imageCanvas.width = img.width;
  imageCanvas.height = img.height;
 
  const imageCanvasContext = imageCanvas.getContext('2d');
  
  //flipY
  imageCanvasContext.translate(0, img.height);
  imageCanvasContext.scale(1, -1);
  
  imageCanvasContext.drawImage(img, 0, 0, img.width, img.height);
  const imageData = imageCanvasContext.getImageData(0, 0, img.width, img.height);

这里创建canvas->绘制图片->获得图片数据。

(注:在绘制图片时将图片在Y方向反转了)

接着看代码:

  let data = null;
 
  const rowPitch = Math.ceil(img.width * 4 / 256) * 256;
  if (rowPitch == img.width * 4) {
    data = imageData.data;
  } else {
    data = new Uint8Array(rowPitch * img.height);
    for (let y = 0; y < img.height; ++y) {
      for (let x = 0; x < img.width; ++x) {
        let i = x * 4 + y * rowPitch;
        data[i] = imageData.data[i];
        data[i + 1] = imageData.data[i + 1];
        data[i + 2] = imageData.data[i + 2];
        data[i + 3] = imageData.data[i + 3];
      }
    }
  }
 
  const texture = device.createTexture({
    size: {
      width: img.width,
      height: img.height,
      depth: 1,
    },
    format: "rgba8unorm",
    usage: GPUTextureUsage.COPY_DST | usage,
  });
 
  const textureDataBuffer = device.createBuffer({
    size: data.byteLength,
    usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC,
  });
 
  textureDataBuffer.setSubData(0, data);

rowPitch需要为256的倍数(也就是说,图片的宽度需要为64px的倍数),这是因为Dx12对此做了限制(参考Copies investigation):

RowPitch must be aligned to D3D12_TEXTURE_DATA_PITCH_ALIGNMENT.

Offset must be aligned to D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT, which is 512.

另外,关于纹理尺寸,可以参考WebGPU-6

第一个问题是关于纹理尺寸的,回答是WebGPU没有对尺寸有特别明确的要求。sample code中最多不能比4kor8k大就行。这个也不是太难理解,OpenGL对纹理和FBO的尺寸总是有上限的。

根据我的测试,buffer(代码中的textureDataBuffer)中的图片数据需要为未压缩的图片数据(它的类型为Uint8Array,length=img.width * img.height * 4(因为每个像素有r、g、b、a这4个值)),否则会报错(在我的测试中,“通过canvas->toDataURL得到图片的base64->将其转为Uint8Array,得到压缩后的图片数据->将其设置到buffer中”会报错)

继续看代码:

  const commandEncoder = device.createCommandEncoder({});
  commandEncoder.copyBufferToTexture({
    buffer: textureDataBuffer,
    rowPitch: rowPitch,
    imageHeight: 0,
  }, {
    texture: texture,
  }, {
    width: img.width,
    height: img.height,
    depth: 1,
  });
 
  device.defaultQueue.submit([commandEncoder.finish()]);
 
  return texture;

这里提交了copyBufferToTexture这个command到GPU,并返回texture

(注:这个command此时并没有执行,会由GPU决定什么时候执行)

WebGPU支持buffer与buffer、buffer与texture、texture与texture之间互相拷贝。

参考资料

3 channel formats

Copies investigation (+ proposals)

参考资料

WebGPU规范

webgpu-samplers Github Repo

WebGPU-6

相关实践学习
在云上部署ChatGLM2-6B大模型(GPU版)
ChatGLM2-6B是由智谱AI及清华KEG实验室于2023年6月发布的中英双语对话开源大模型。通过本实验,可以学习如何配置AIGC开发环境,如何部署ChatGLM2-6B大模型。
相关文章
|
算法 JavaScript 大数据
高德地图 错误码说明 对照表
序号  infocode info返回值 状态描述 问题排查策略 1 10000 OK 请求正常 请求正常 2 10001 INVALID_USER_KEY key不正确或过期 开发者发起请求时,传入的key不正确或者过期  3 10002 SERVICE_NOT_AVAILABLE 没有权限使用相应的服务或者请求接口的路径拼写错误 1.开发者没有权限使用相应的服务,例如:开发者申请了WEB定位功能的key,却使用该key访问逆地理编码功能时,就会返回该错误。反之亦然。2.开发者请求接口的路径拼写错误。例如:正确的https://restapi.amap.com/v3/ip在程序中被拼装写了h
2770 0
cesium添加实体不被地形遮挡的参数设置
disableDepthTestDistance:指定从相机到禁用深度测试的距离,关于深度测试我们将在后面的文章中介绍到,由于深度测试的存在,我们的对象很多时候会被地形挡住,如下:
2656 0
cesium添加实体不被地形遮挡的参数设置
深入掌握ant-design的form异步校验(一)
本文适合对ant-design的表单校验感兴趣的小伙伴阅读~
|
6月前
|
前端开发 JavaScript
【Javascript系列】Terser除了压缩代码之外,还有优化代码的功能
Terser 是一款广泛应用于前端开发的 JavaScript 解析器和压缩工具,常被视为 Uglify-es 的替代品。它不仅能高效压缩代码体积,还能优化代码逻辑,提升可靠性。例如,在调试中发现,Terser 压缩后的代码对删除功能确认框逻辑进行了优化。常用参数包括 `compress`(启用压缩)、`mangle`(变量名混淆)和 `output`(输出配置)。更多高级用法可参考官方文档。
393 11
|
12月前
|
JavaScript 开发工具
vite如何打包vue3插件为JSSDK
【9月更文挑战第10天】以下是使用 Vite 打包 Vue 3 插件为 JS SDK 的步骤:首先通过 `npm init vite-plugin-sdk --template vue` 创建 Vue 3 项目并进入项目目录 `cd vite-plugin-sdk`。接着,在 `src` 目录下创建插件文件(如 `myPlugin.js`),并在 `main.js` 中引入和使用该插件。然后,修改 `vite.config.js` 文件以配置打包选项。最后,运行 `npm run build` 进行打包,生成的 `my-plugin-sdk.js` 即为 JS SDK,可在其他项目中引入使用。
519 6
|
10月前
|
数据采集 数据可视化 数据处理
如何使用Python实现一个交易策略。主要步骤包括:导入所需库(如`pandas`、`numpy`、`matplotlib`)
本文介绍了如何使用Python实现一个交易策略。主要步骤包括:导入所需库(如`pandas`、`numpy`、`matplotlib`),加载历史数据,计算均线和其他技术指标,实现交易逻辑,记录和可视化交易结果。示例代码展示了如何根据均线交叉和价格条件进行开仓、止损和止盈操作。实际应用时需注意数据质量、交易成本和风险管理。
432 5
|
Java 开发工具 Android开发
鸿蒙HarmonyOS 与 Android 的NDK有什么区别?
鸿蒙(HarmonyOS)和Android的NDK(Native Development Kit)是两个不同的概念,它们在设计理念、架构、开发方式和目标平台等方面存在着一些显著的不同。
838 0
|
存储 分布式计算 大数据
【Flume的大数据之旅】探索Flume如何成为大数据分析的得力助手,从日志收集到实时处理一网打尽!
【8月更文挑战第24天】Apache Flume是一款高效可靠的数据收集系统,专为Hadoop环境设计。它能在数据产生端与分析/存储端间搭建桥梁,适用于日志收集、数据集成、实时处理及数据备份等多种场景。通过监控不同来源的日志文件并将数据标准化后传输至Hadoop等平台,Flume支持了性能监控、数据分析等多种需求。此外,它还能与Apache Storm或Flink等实时处理框架集成,实现数据的即时分析。下面展示了一个简单的Flume配置示例,说明如何将日志数据导入HDFS进行存储。总之,Flume凭借其灵活性和强大的集成能力,在大数据处理流程中占据了重要地位。
256 3
|
SQL 存储 数据处理
SQL中的运算符:数据操作的核心工具
【8月更文挑战第31天】
902 0
|
机器学习/深度学习 算法 数据可视化
YOLO系列算法全家桶——YOLOv1-YOLOv9详细介绍 !!(二)
YOLO系列算法全家桶——YOLOv1-YOLOv9详细介绍 !!(二)
1984 3