gpt4 book ai didi

Wgpu图文详解(05)纹理与绑定组

转载 作者:撒哈拉 更新时间:2025-01-15 22:25:18 55 4
gpt4 key购买 nike

前言

什么是纹理?

纹理是图形渲染中用于增强几何图形视觉效果的一种资源。它是一个二维或三维的数据数组,通常包含颜色信息,但也可以包含其他类型的数据,如法线、高度、环境光遮蔽等。纹理的主要目的是为几何图形的表面提供详细的视觉效果,使其看起来更加真实和复杂。而我们常见的图片是一个二维的像素数组,每个像素包含颜色信息(通常是 RGB 或 RGBA)。图片可以是任何内容,如照片、图标、绘画等。图片的主要用途是显示和存储视觉信息,可以用于各种应用,如网页、文档、多媒体等.

纹理和图片的差异

用途:

  • 纹理:主要用于图形渲染,增强几何图形的视觉效果。
  • 图片:主要用于显示和存储视觉内容,应用范围更广泛。

数据结构:

  • 纹理:可以是二维或三维的数据数组,包含多种类型的数据(如颜色、法线、高度等)。
  • 图片:通常是二维的像素数组,主要包含颜色信息。

处理方式:

  • 纹理:通过图形渲染管线进行处理,涉及纹理坐标、采样器、片段着色器等。
  • 图片:通过图像处理软件或库进行处理,直接操作像素数据。

存储和传输:

  • 纹理:通常存储在 GPU 内存中,优化为渲染操作。
  • 图片:可以存储在文件系统中,格式多样(如 JPEG、PNG、BMP 等),用于各种应用。

纹理与绑定组实践

在理解纹理的基本概念以后,我们基于第四章的项目内容初始化本章代码目录。要得到纹理数据,我们首先需要加载一张图片到内存中。因此,我们先放置一张样例图片到项目目录下的assets目录中(一般将资源文件放到assets目录中);同时,引入image 包,该包提供了读写图片数据的能力,以方便我们后续加载图片;最后,我们增加一个img_utils.rs的源代码文件,该文件中封装了一个名为RgbaImg的结构体来抽象图片数据。初始准备完成后,现在的项目结构如下:

010

初始化内容可checkout该 commit 。

接下来,我们会在合适的位置通过RgbaImg实例来构造出纹理对象,并将其按照之前的思路存储到WgpuCtx实例中.

构造纹理Texture

首先,我们需要在WgpuCtx结构体中增加三个字段:

  1. texture: wgpu::Texture:存储纹理Texture实例。
  2. texture_image: RgbaImg:存储图片的原始数据(宽、高、像素二进制数据)。
  3. texture_size: wgpu::Extent3d:纹理的尺寸定义。

020

texture: wgpu::Texture是一个纹理实例,主要是保存了GPU在渲染纹理过程中的一些上下文信息(譬如纹理的宽高深度等); 。

texture_image: RgbaImg主要是存储我们待会儿加载示例图片后,得到的图片的信息(宽高二进制数据等); 。

texture_size: wgpu::Extent3d主要是存储了纹理的宽高登信息.

在WgpuCtx结构体中定义好字段以后,我们需要完成纹理和图片的构造过程。这个过程其实和之前创建顶点缓冲区的思路类似,就是在WgpuCtx的new_async方法的合适位置调用相关的API进行构造:

030

首先,我们调用Rgba的new方法,传入项目下的示例图片路径,构造出RgbaImg对象,该对象存储了图片的宽高以及rgba格式的二进制数据(注意,这里的路径需要传递一个绝对路径,请读者自己将图片放置到合适位置,并在这里填写正确的绝对路径,否则运行时会找不到).

然后,我们通过图片的宽高信息来直接构造了一个Extend3d对象.

最后,我们调用wgpu::Device的create_textureAPI来创建一个纹理对象,该方法参数TextureDescriptor的具体配置如下:

040

在这里,我们梳理一遍create_texture参数字段(label字段不在赘述):

  • size: wgpu::Extend3d:用于表达纹理的基本尺寸结构(宽、高以及深度)。在这里,我们将前面获取到的图片的宽高作为了Extend3d的宽高;对于Extend3d的depth_or_array_layers字段,理论下,纹理是以3维形式存储的,但是在本例中我们主要是为了渲染一个2D图片纹理,所以在这里我们传入1.

  • mip_level_count与sample_count我们暂时先不讲,因为涉及到的内容稍微有点复杂,不是一两句能讲清楚的,等到后面实践过程中再来回顾该字段,这里目前就设置为1即可.

  • dimension: wgpu::TextureDimension:表明纹理的维度,我们传入表示2D的枚举.

  • format: wgpu::TextureFormat:表明我们使用的纹理的原始数据格式。在前面我们加载图片数据后,将其转换为了u8rgba,即四个字节(u8)每个字节分别对应Red、Green、Blue、Alpha通道的数据,同时,大部分图像数据使用sRGB来存储,所以这里我们选择了对应的枚举:Rgba8UnormSrgb。关于图像格式的细节,不在本文讨论,感兴趣的读者自行拓展学习:sRGB色彩空间.

  • usage: wgpu::TextureUsages:该字段用来表示我们的纹理数据将会被怎样使用,在这里我们设置两个:TEXTURE_BINDING与COPY_DST。前者表示我们要在着色器中使用这个纹理,即在稍后的过程中我们会在着色器中通过一定的方式访问到这个纹理;后者表示我们能将数据复制到这个纹理上,即我们后续会有将数据写入到纹理上的操作.

  • view_formats也是一个比较复杂的字段,我们暂时先不关心它,传入&[]即可.

细心的读者发现了,当我们创建wgpu::Texture的时候,会首先创建该wgpu::Extent3d实例,并通过该Extent3d再来创建Texture,那么理论上,Texture实例是包含了Extend3d的,那么为什么这里需要将再额外Extend3d保存下来呢?原因在于在接下来的流程中,我们还会消费Extend3d,但是Texture没有相关的API来访问内部的Extend3d数据.

填充数据到纹理

在完成对纹理实例的创建并保存到WgpuCtx后,我们需要调用Queue队列的write_texture API 来将我们的图片数据通过队列的方式写入到纹理中。具体做法则是在我们先前在WgpuCtx中定义的draw方法中,在最后调用队列的submit方法前调用write_textureAPI:

注意,这一步我们并不是将纹理数据写入到渲染流程中,而是通过队列提供的API,将图片像素数据写入到纹理中,即完成纹理和实际二进制数据的关联.

050

对于write_textureAPI来说,它的参数现阶段有四个,我们依次讲解:

第一个参数我们需要传递一个ImageCopyTexture对象,该对象引用了我们刚刚创建的纹理对象(self.texture),ImageCopyTexture并不是一个纹理对象,而是一个用来描述纹理操作的配置对象,它表达这样一个意图:期望将接下来传入的图像数据以拷贝的方式填充到纹理self.texture中。至于其中的mip_level、origin以及aspect字段这里我们先暂时不介绍,读者按照默认即可.

第二个参数传递的是我们保存的图像像素数据(self.texture.bytes)。这里就是我们实际要填充到纹理中的图像数据.

第三个参数我们需要传入一个ImageDataLayout对象,如果读者对先前的文章比较熟悉,那么会记得在Wgpu中,常规数据需要结合XxxLayout布局才能描述一个原始的二进制数据的形态究竟是什么(比如之前的顶点缓冲区布局)。在这里也是同样一个思路,当我们传入的图片数据在没有任何其他上下文定义的情况下,GPU内部是无法理解这个二进制数据是什么结构,因此才会有ImageDataLayout。回到本例的具体配置中,ImageDataLayout的offset字段为0,代表了我们传入的二进制数据从0开始就是图片数据;bytes_per_row字段表示了图片的每一行有多少个字节,由于我们的图片数据每一个像素由rgba四个属性组成,每个属性占用1个字节,即一个像素总共占用4个字节,对于一行来说,我们有 图片宽度n 个像素,因此这里每一行的字节数就是4 x 图片宽度n;至于rows_per_image,就不难理解了,总共有 图片高度m 行.

060

最后一个参数就是我们先前存储的Extend3d对象,这里就不再赘述意义了.

因此,调用了该方法后,我们就将真实的图片数据填充到纹理对象中。接下来我们就可以直接使用了吗?答案是否定的,纹理在准备完成以后,我们无法很直接的使用它。接下来我会详细说明这一块的内容.

创建采样器

纹理采样器Sampler是一个渲染管线中的组件,它负责根据顶点坐标等信息,从纹理图像中提取出具体的像素颜色值,用于对渲染的物体表面进行纹理映射。下图是一个不太严谨但易于理解的图示来解释采样器的核心工作:

070

左边是一个纹理图片数据,而右边则是最终要渲染的目标平面。采样器就类似于取色器一样的东西,当右边渲染平面上某一处像素位置要渲染具体的颜色的时候,采样器会通过一定的规则去左边的纹理数据中取到对应的颜色.

请务必注意,采样器基本的工作流程是依据右边的渲染目标,再从左边的纹理数据中找到对应的颜色数据,而不是从左侧的纹理数据开始,往右边渲染目标填充。这二者是有一定的区别的.

理解采样器的基本工作流程后,让我们来创建一个采样器。采样器尽管听起来像是一个工具,但在Wgpu中也算做一种资源,我们保持和之前的方式一样,首先在WgpuCtx结构体中增加一个名为sampler的字段,类型为wgpu::Sampler,用来保存我们创建的采样器;其次,在new_async的方法中合适位置调用device的create_samplerAPI,我们就可以创建一个采样器:

080

我们可以看到关于SamplerDescriptor中的三个address_mode_*字段。对于UVW坐标来说:

U和V:通常对应于二维纹理图像的水平和垂直方向的坐标。U坐标类似于二维图像中的X坐标,表示纹理图像的水平位置;V坐标类似于二维图像中的Y坐标,表示纹理图像的垂直位置。它们的取值范围一般在0到1之间,其中(0,0)通常表示纹理图像的左下角,(1,1)表示纹理图像的右上角.

W:在一些情况下,会使用到W坐标。W坐标主要用于三维纹理映射,即纹理图像本身是三维的,或者在进行一些特殊的纹理映射操作时需要额外的一个维度来控制纹理的采样。例如,在体积渲染中,纹理图像可以是一个三维的数据体,W坐标就表示在这个三维数据体中的深度方向的位置.

对于address_mode_*字段的枚举值,它的作用是指定了当采样器采样的坐标超出了纹理本身的边界时,应该如何处理。同样的,我们可以用下面不严谨的图示来描述具体的场景: