Wgpu图文详解(04)顶点索引及其缓冲区
前言
在上一节中我们重点介绍了图形学工程中的缓冲区Buffer的概念,同时通过大量的图解和代码实例来讲解如何构建一个顶点缓冲区,通过与着色器代码的配合,最终实现了一个渐变效果三角形渲染。相信读者还记得我们曾经编写过这样的代码:
在上述代码中,我们用了三个Vertex结构体数据分别描述三个顶点在几何空间中的位置以及颜色。当然,在实际应用场景中,我们几乎不可能只是渲染一个三角形,我们不可避免的会渲染一些复杂的图形。值得提到的是,无论是2D图形还是3D图形,在图形学中通常都会通过一定的算法将其拆解为n个独立的三角面:
因此,为了能够表达更加复杂的图形,我们会增加三角形的数量。例如,在本文中,我们尝试渲染如下一个六边形:
为了实现上述的效果,我们可以将这个六边形分解为如下的4个三角面:
其中,由a、b、c、d、e、f六个点构成一个六边形;abc构成三角面A,acd构成三角面B,ade构成三角面C,aef构成三角面D。
按照之前的思路,我们可以在顶点数据列表中,增加顶点:
只是修改VRETEXT_LIST的内容,在前一篇文章的基础上,我们就可以渲染一个期望的六边形:
上述内容代码已经作为一次提交推送到
wgpu_winit_example
仓库中,对应提交记录:Commit 4e60c76。
顶点索引及其缓冲区
尽管此时我们已经完成了一个六边形的渲染,但同时我们会发现,对于相邻的三角面会共享一些顶点,例如三角面A和B都共享了顶点a、c:
通过梳理不难发现,除了顶点b、f外,另外的4个顶点:a、c、d、e,都不止一次被共享使用。假设存在一个场景会有成千上万个三角面,试想一下,如果能够复用顶点数据,那将会节省大量的内存空间。因此,有没有一种方式能够高效的复用顶点呢?答案是肯定的——使用顶点索引来表达三角面。
对于上面的六边形,首先,我们只需要将每一个顶点定义出来:
// a, b, c, d, e, f
pub const VERTEX_LIST: &[Vertex] = &[
// a
Vertex { position: [0.0, 0.5, 0.0], color: [0.0, 0.2, 1.0] },
// b
Vertex { position: [-0.5, 0.3, 0.0], color: [0.0, 0.2, 1.0] },
// c
Vertex { position: [-0.5, -0.3, 0.0], color: [0.0, 0.2, 1.0] },
// d
Vertex { position: [0.0, -0.5, 0.0], color: [0.0, 0.2, 1.0] },
// e
Vertex { position: [0.5, -0.3, 0.0], color: [0.0, 0.2, 1.0] },
// f
Vertex { position: [0.5, 0.3, 0.0], color: [0.0, 0.2, 1.0] },
];
然后,在通过顶点索引表达:
const INDEX_LIST: &[u16] = &[
0, 1, 2, // abc
0, 2, 3, // acd
0, 3, 4, // ade
0, 4, 5, // aef
];
INDEX_LIST中每一个整数就对应VERTEX_LIST数组中对应的索引位置
此时,我们的项目代码中的改动就只有vertex.rs文件中的改动:
讲一个题外话,此时你可以尝试运行基于上述改动的代码,会发现得到如下的一个效果:
如果读者对之前的内容掌握了,其实也不难理解呈现这个效果的原因。按照之前对于wgpu的图元配置( wgpu::PrimitiveTopology::TriangleList
,请读者自行复习相关概念、代码),会让wgpu按照每三个顶点作为一个三角面来渲染,同时我们将顶点改为了6个,正好构成两个三角面(abc、def)。
回到正题,我们创建了一个顶点索引数据,其目的就是告诉gpu:“在接下来请将传入的6个顶点数据,依次按照顶点索引数组的配置,渲染4个三角形“。那么我们应该如何把顶点索引数据传递给gpu(渲染管线),并让gpu能够理解并消费顶点索引数据呢?其实过程和前面我们将顶点数据创建、消费是很类似的。
首先,顶点索引数据既然是要交给gpu,那么依然离不开缓冲区这一重要概念,不过与顶点缓冲区的区别在于我们要为缓冲区指明其类型为顶点索引缓冲区:
对于上述代码。首先,我们参考之前方式,将顶点索引数组数据通过工具库bytemuck转换为字节数据。值得注意的是,这里的VERTEX_IDNEX_LIST的类型是&[u16]
,即每一个元素是u16
基本类型,因此不需要像之前顶点Vertex结构体那样为其实现bytemuck::Zeroable
和bytemuck::Pod
trait,就可以转为字节数据。
然后,我们同样通过device的create_buffer_init
API来创建一个缓冲区实例,并传入字节数据,不过这里的usage
字段我们需要传入枚举wgpu::BufferUsage::INDEX
来表明创建的缓冲区是用来存放顶点索引数据,而不是其他类型的数据。
最后,我们按照之前模式一样,创建并存放顶点缓冲区数据:同样在WgpuCtx结构体中增加vertex_index_buffer
字段来存放我们创建的顶点索引缓冲区数据。
至此,我们就完成了一个顶点索引缓冲区的创建工作了。那么接下来就是在渲染时消费到这个缓冲区数据。具体代码如下所示:
在原来WgpuCtx::draw
方法代码基础上,我们增加了上述两行代码:
// 消费存放的 vertex_index_buffer
rpass.set_index_buffer(self.vertex_index_buffer.slice(..), wgpu::IndexFormat::Uint16); // 1.
// 调用draw_indexed,传入对应数量的顶点数量
rpass.draw_indexed(0..VERTEX_INDEX_LIST.len() as u32, 0, 0..1);
首先,调用渲染通道RenderPass的set_index_buffer
API,传入我们存放的顶点索引缓冲实例的切片以及对应的数据类型格式(因为我们的每一个顶点的数据是u16
,因此这里用枚举wgpu::IndexFormat::Uint16
);
其次,调用draw_indexed
API来传入具体顶点索引数组的长度,以及要从数组中哪个位置开始作为第一个索引(这里我们填入0),最后一个参数我们默认填入0..1
表明只有一个实例。
完成上述代码以后,我们再次运行项目,会发现得到了我们希望的效果:
写在最后
实际上,无论是上一篇的顶点缓冲区还是本文的索引缓冲区。我们的核心思路都是创建一些能够表达信息的数据,再基于这些数据创建缓冲区实例。缓冲区通过类型来区分其作用。因此,后续的内容中如果出现了其他类型缓冲区,我相信读者能够理解对应的思路。
读者如果看过《learn wgpu》的内容,会发现在《learn wgpu》中是将顶点缓冲区、顶点索引等内容放到了一篇中,但是笔者在编写的时候考虑到在上一篇(《Wgpu图文详解(03)缓冲区Buffer》)内容比较多,将顶点索引这块的内容再放进去会增加读者的阅读负担,因此将顶点索引的部分单独拆到了这一章中。同时,也是希望读者能够将本文作为对上一篇文章缓冲区内容的巩固。
本章的代码仓库在这里:
后续文章的相关代码也会在该仓库中添加,所以感兴趣的读者可以点个star,谢谢你们的支持!