Ch7-3 初识实例化绘制

本节的main.cpp对应示例代码中的:Ch7-3.hpp

什么是实例化绘制?
先前在Ch7-1 初识顶点缓冲区创建过的顶点缓冲区中包含了三角形三个顶点的数据:

vertex vertices[] = {
    { {  .0f, -.5f }, { 1, 0, 0, 1 } },
    { { -.5f,  .5f }, { 0, 1, 0, 1 } },
    { {  .5f,  .5f }, { 0, 0, 1, 1 } }
};

那么显然,用如下顶点数据,每三个顶点绘制一个三角形,就会在不同位置绘制出三个同样的三角形:

vertex vertices[] = {
    { {  .0f, -.5f }, { 1, 0, 0, 1 } },
    { { -.5f,  .5f }, { 0, 1, 0, 1 } },
    { {  .5f,  .5f }, { 0, 0, 1, 1 } },
    //左移0.5
    { {  .0f - .5f, -.5f }, { 1, 0, 0, 1 } },
    { { -.5f - .5f,  .5f }, { 0, 1, 0, 1 } },
    { {  .5f - .5f,  .5f }, { 0, 0, 1, 1 } },
    //右移0.5
    { {  .0f + .5f, -.5f }, { 1, 0, 0, 1 } },
    { { -.5f + .5f,  .5f }, { 0, 1, 0, 1 } },
    { {  .5f + .5f,  .5f }, { 0, 0, 1, 1 } }
};

但上面这堆数据太冗余了不是吗?将决定三角形形状和颜色的顶点数据,和各个三角形的位置偏移分开写则为:

vertex vertices[] = {
    { {  .0f, -.5f }, { 1, 0, 0, 1 } },
    { { -.5f,  .5f }, { 0, 1, 0, 1 } },
    { {  .5f,  .5f }, { 0, 0, 1, 1 } }
};
glm::vec2 offsets[] = {
    { {  .0f, .0f } },
    { { -.5f, .0f } },
    { {  .5f, .0f } }
};

若称每个基于vertices绘制的三角形为一个实例(instance),每个三角形的序号为实例索引(instance index),单个三角形中每个顶点的序号为顶点索引(vertex index),则绘制3个三角形实例的全部9个顶点时,vertex[顶点索引].positions + offsets[实例索引]即为其中单个顶点的坐标。

这种将多个图形共有的数据,和每个图形独有的数据,分别按不同频率输入到着色器,然后将数据加以组合以绘制多个图形实例的方式,叫做实例化绘制(instanced rendering)。

通过实例化绘制多个三角形的流程

在之前Ch7-1.hpp用顶点缓冲区绘制三角形代码的基础上,使用顶点和索引缓冲区绘制长方形需要经历以下步骤:
1.创建逐顶点、逐实例输入的顶点缓冲区
2.指定顶点属性
3.书写着色器
4.在命令缓冲区中绑定顶点缓冲区并绘制

创建顶点缓冲区并指定顶点属性

先前已经写过了verticesoffsets,将它们扔进各自的顶点缓冲区:

vertex vertices[] = {
    { {  .0f, -.5f }, { 1, 0, 0, 1 } },
    { { -.5f,  .5f }, { 0, 1, 0, 1 } },
    { {  .5f,  .5f }, { 0, 0, 1, 1 } }
};
glm::vec2 offsets[] = {
    { {  .0f, .0f } },
    { { -.5f, .0f } },
    { {  .5f, .0f } }
};
vertexBuffer vertexBuffer_perVertex(sizeof vertices);
vertexBuffer_perVertex.TransferData(vertices);
vertexBuffer vertexBuffer_perInstance(sizeof offsets);
vertexBuffer_perInstance.TransferData(offsets);

指定顶点属性:

//照抄Ch7-1
//数据来自0号顶点缓冲区,输入频率是逐顶点输入
pipelineCiPack.vertexInputBindings.emplace_back(0, sizeof(vertex), VK_VERTEX_INPUT_RATE_VERTEX);
//location为0,数据来自0号顶点缓冲区,vec2对应VK_FORMAT_R32G32_SFLOAT
pipelineCiPack.vertexInputAttributes.emplace_back(0, 0, VK_FORMAT_R32G32_SFLOAT, offsetof(vertex, position));
//location为1,数据来自0号顶点缓冲区,vec4对应VK_FORMAT_R32G32B32A32_SFLOAT
pipelineCiPack.vertexInputAttributes.emplace_back(1, 0, VK_FORMAT_R32G32B32A32_SFLOAT, offsetof(vertex, color));

//数据来自1号顶点缓冲区,输入频率是逐实例输入
pipelineCiPack.vertexInputBindings.emplace_back(1, sizeof(glm::vec2), VK_VERTEX_INPUT_RATE_INSTANCE);
//location为2,数据来自1号顶点缓冲区,vec2对应VK_FORMAT_R32G32_SFLOAT
pipelineCiPack.vertexInputAttributes.emplace_back(2, 1, VK_FORMAT_R32G32_SFLOAT, 0);

InstancedRendering.vert.shader

新建InstancedRendering.vert.shader,基于VertexBuffer.vert.shader略作修改即可:

#version 460
#pragma shader_stage(vertex)

layout(location = 0) in vec2 i_Position;
layout(location = 1) in vec4 i_Color;
layout(location = 2) in vec2 i_InstancePosition;
layout(location = 0) out vec4 o_Color;

void main() {
    gl_Position = vec4(i_Position + i_InstancePosition, 0, 1);//共通顶点数据中的位置坐标 + 各个实例的位置偏移
    o_Color = i_Color;
}

别忘了更改CreatePipeline(...):

void CreatePipeline() {
    static shaderModule vert("shader/InstancedRendering.vert.spv");
    //省略后续代码
}

绑定顶点缓冲区并绘制

将两个顶点缓冲区分别绑定到各自的binding:

VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffer_perVertex.Address(), &offset);
vkCmdBindVertexBuffers(commandBuffer, 1, 1, vertexBuffer_perInstance.Address(), &offset);

也可以直接用一条命令绑定两个缓冲区::

VkBuffer buffers[2] = { vertexBuffer_perVertex, vertexBuffer_perInstance };
VkDeviceSize offsets[2] = {};
vkCmdBindVertexBuffers(commandBuffer, 0, 2, buffers, offsets);

3个顶点构成一个三角形,绘制3个实例,首个顶点和首个实例的索引都是0:

vkCmdDraw(commandBuffer, 3, 3, 0, 0);

运行程序,你应该会看到以下图像:

_images/ch7-3-1.png