实例化
本次我们使用实例化功能从单个顶点缓冲区渲染具有可变参数和纹理的同一网格的许多实例(索引分层纹理)。使用辅助顶点缓冲区传递实例数据。
假设你有一个绘制了很多模型的场景,而大部分的模型包含的是同一组顶点数据,只不过进行的是不同的世界空间变换。想象一个充满草的场景:每根草都是一个包含几个三角形的小模型。你可能会需要绘制很多根草,最终在每帧中你可能会需要渲染上千或者上万根草。因为每一根草仅仅是由几个三角形构成,渲染几乎是瞬间完成的,但上千个渲染函数调用却会极大地影响性能。
如果我们需要渲染大量物体时,代码看起来会像这样:
for(unsigned int i = 0; i < INSTANCE_COUNT; i++)
{
DoSomePreparations(); //绑定描述符,绑定渲染管线等
vkCmdDraw(drawCmdBuffers[i], 4, 1, 0, 0);;
}
如果像这样绘制模型的大量实例(Instance),你很快就会因为绘制调用过多而达到性能瓶颈。与绘制顶点本身相比,使用vkCmdDraw或vkCmdDrawIndexed函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为Vulkan在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)。所以,即便渲染顶点非常快,大量相同数据调用命令GPU去渲染却未必很快。
因此,我们使用vkCmdDraw或vkCmdDrawIndexed中的firstIndex和instanceCount参数来控制实例化,通过实例化,一个几何对象的很多副本可以直接发送到图形管线,每一个副本都可以称为一个实例。
案例:小行星带
想象这样一个场景,在宇宙中有一个大的行星,它位于小行星带的中央。这样的小行星带可能包含成千上万的岩块,在很不错的显卡上也很难完成这样的渲染。实例化渲染正是适用于这样的场景,因为所有的小行星都可以使用一个模型来表示。每个小行星可以再使用不同的变换矩阵来进行少许的变化。
为了得到想要的效果,我们将会为每个小行星生成一个变换矩阵,用作它们的模型矩阵。变换矩阵首先将小行星位移到小行星带中的某处,我们还会加一个小的随机偏移值到这个偏移量上,让这个圆环看起来更自然一点。接下来,我们应用一个随机的缩放,并且以一个旋转向量为轴进行一个随机的旋转。最终的变换矩阵不仅能将小行星变换到行星的周围,而且会让它看起来更自然,与其它小行星不同。最终的结果是一个布满小行星的圆环,其中每一个小行星都与众不同。
1.数据准备
首先,我们需要定义每个实例对象所需的数据结构体:
#define INSTANCE_COUNT 4096 //实例化个数
struct InstanceData {
glm::vec3 pos;//位置
glm::vec3 rot;//旋转角度
float scale;//实例对象缩放比例
uint32_t texIndex;//纹理索引
};
之后我们创建一个prepareInstanceData函数来准备所需创建的实例化数据:
std::vector<InstanceData> instanceData;
instanceData.resize(INSTANCE_COUNT);
std::default_random_engine rndGenerator(benchmark.active ? 0 : (unsigned)time(nullptr));
std::uniform_real_distribution<float> uniformDist(0.0, 1.0);
std::uniform_int_distribution<uint32_t> rndTextureIndex(0, textures.rocks.layerCount);
// 在两个圆环上创建位置信息
for (auto i = 0; i < INSTANCE_COUNT / 2; i++) {
glm::vec2 ring0 { 5.0f, 10.0f };
glm::vec2 ring1 { 15.0f, 19.0f };
float rho, theta;
// 内环数据
rho = sqrt((pow(ring0[1], 2.0f) - pow(ring0[0], 2.0f)) * uniformDist(rndGenerator) + pow(ring0[0], 2.0f));
theta = 2.0 * M_PI * uniformDist(rndGenerator);
instanceData[i].pos = glm::vec3(rho*cos(theta), uniformDist(rndGenerator) * 0.5f - 0.25f, rho*sin(theta));
instanceData[i].rot = glm::vec3(M_PI * uniformDist(rndGenerator), M_PI * uniformDist(rndGenerator), M_PI * uniformDist(rndGenerator));
instanceData[i].scale = 1.5f + uniformDist(rndGenerator) - uniformDist(rndGenerator);
instanceData[i].texIndex = rndTextureIndex(rndGenerator);
instanceData[i].scale *= 0.75f;
// 外环数据
rho = sqrt((pow(ring1[1], 2.0f) - pow(ring1[0], 2.0f)) * uniformDist(rndGenerator) + pow(ring1[0], 2.0f));
theta = 2.0 * M_PI * uniformDist(rndGenerator);
instanceData[i + INSTANCE_COUNT / 2].pos = glm::vec3(rho*cos(theta), uniformDist(rndGenerator) * 0.5f - 0.25f, rho*sin(theta));
instanceData[i + INSTANCE_COUNT / 2].rot = glm::vec3(M_PI * uniformDist(rndGenerator), M_PI * uniformDist(rndGenerator), M_PI * uniformDist(rndGenerator));
instanceData[i + INSTANCE_COUNT / 2].scale = 1.5f + uniformDist(rndGenerator) - uniformDist(rndGenerator);
instanceData[i + INSTANCE_COUNT / 2].texIndex = rndTextureIndex(rndGenerator);
instanceData[i + INSTANCE_COUNT / 2].scale *= 0.75f;
}
...//缓存数据创建
2.管线处理
最后在创建绘制命令缓冲的时候绑定实例化的数据:
// 实例化创建
for (int32_t i = 0; i < drawCmdBuffers.size(); ++i)
{
...
//绑定管线及描述符
vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets.instancedRocks, 0, NULL);
vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelines.instancedRocks);
// 绑定位置 0 : 每个实例的缓存
vkCmdBindVertexBuffers(drawCmdBuffers[i], 0, 1, &models.rock.vertices.buffer, offsets);
// 绑定位置 1 : 实例化数据的缓存
vkCmdBindVertexBuffers(drawCmdBuffers[i], 1, 1, &instanceBuffer.buffer, offsets);
//绑定索引缓冲
vkCmdBindIndexBuffer(drawCmdBuffers[i], models.rock.indices.buffer, 0, VK_INDEX_TYPE_UINT32);
// 渲染绘制实例对象
vkCmdDrawIndexed(drawCmdBuffers[i], models.rock.indexCount, INSTANCE_COUNT, 0, 0, 0);
...
}
此处在创建渲染管线中顶点属性绑定的时候,注意:每个顶点和每个实例属性的着色器声明是相同的,不同的是输入只存储在绑定中:
std::vector<VkVertexInputAttributeDescription> attributeDescriptions = {
// 顶点属性
// 顶点着色器获取的每个顶点数据
vks::initializers::vertexInputAttributeDescription(VERTEX_BUFFER_BIND_ID, 0, VK_FORMAT_R32G32B32_SFLOAT, 0), // Location 0: Position
vks::initializers::vertexInputAttributeDescription(VERTEX_BUFFER_BIND_ID, 1, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Location 1: Normal
vks::initializers::vertexInputAttributeDescription(VERTEX_BUFFER_BIND_ID, 2, VK_FORMAT_R32G32_SFLOAT, sizeof(float) * 6), // Location 2: Texture coordinates
vks::initializers::vertexInputAttributeDescription(VERTEX_BUFFER_BIND_ID, 3, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 8), // Location 3: Color
// 实例化数据属性,这些是为每个呈现的实例获取的
vks::initializers::vertexInputAttributeDescription(INSTANCE_BUFFER_BIND_ID, 4, VK_FORMAT_R32G32B32_SFLOAT, 0), // Location 4: Position
vks::initializers::vertexInputAttributeDescription(INSTANCE_BUFFER_BIND_ID, 5, VK_FORMAT_R32G32B32_SFLOAT, sizeof(float) * 3), // Location 5: Rotation
vks::initializers::vertexInputAttributeDescription(INSTANCE_BUFFER_BIND_ID, 6, VK_FORMAT_R32_SFLOAT,sizeof(float) * 6), // Location 6: Scale
vks::initializers::vertexInputAttributeDescription(INSTANCE_BUFFER_BIND_ID, 7, VK_FORMAT_R32_SINT, sizeof(float) * 7), // Location 7: Texture array layer index
};
接下来我们重点看一下顶点着色器,针对每一个实例化的构件,都会执行这个顶点着色器:
#version 450
// Vertex attributes
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in vec2 inUV;
layout (location = 3) in vec3 inColor;
// Instanced attributes
layout (location = 4) in vec3 instancePos;
layout (location = 5) in vec3 instanceRot;
layout (location = 6) in float instanceScale;
layout (location = 7) in int instanceTexIndex;
layout (binding = 0) uniform UBO
{
mat4 projection;
mat4 modelview;
vec4 lightPos;
float locSpeed;
float globSpeed;
} ubo;
layout (location = 0) out vec3 outNormal;
layout (location = 1) out vec3 outColor;
layout (location = 2) out vec3 outUV;
layout (location = 3) out vec3 outViewVec;
layout (location = 4) out vec3 outLightVec;
void main()
{
outColor = inColor;
outUV = vec3(inUV, instanceTexIndex);
vec4 locPos = vec4(inPos.xyz, 1.0);
vec4 pos = vec4((locPos.xyz * instanceScale) + instancePos, 1.0);
gl_Position = ubo.projection * ubo.modelview * pos;
outNormal = mat3(ubo.modelview) * inNormal;
pos = ubo.modelview * vec4(inPos.xyz + instancePos, 1.0);
vec3 lPos = mat3(ubo.modelview) * ubo.lightPos.xyz;
outLightVec = lPos - pos.xyz;
outViewVec = -pos.xyz;
}
至于片元着色器就很简单了,里面仅有一个光照处理:
#version 450
layout (binding = 1) uniform sampler2DArray samplerArray;
layout (location = 0) in vec3 inNormal;
layout (location = 1) in vec3 inColor;
layout (location = 2) in vec3 inUV;
layout (location = 3) in vec3 inViewVec;
layout (location = 4) in vec3 inLightVec;
layout (location = 0) out vec4 outFragColor;
void main()
{
vec4 color = texture(samplerArray, inUV) * vec4(inColor, 1.0);
vec3 N = normalize(inNormal);
vec3 L = normalize(inLightVec);
vec3 V = normalize(inViewVec);
vec3 R = reflect(-L, N);
vec3 diffuse = max(dot(N, L), 0.1) * inColor;
vec3 specular = (dot(N,L) > 0.0) ? pow(max(dot(R, V), 0.0), 16.0) * vec3(0.75) * color.r : vec3(0.0);
outFragColor = vec4(diffuse * color.rgb + specular, 1.0);
}
编译着色器,运行可见:
你也可以将实例化个数调整到十万来看一下,在GPU强大的计算能力下,几乎运行时间不会过于冗长:
可以看到,在合适的环境下,实例化渲染能够大大增加显卡的渲染能力。正是出于这个原因,实例化渲染通常会用于渲染草、植被、粒子,以及上面这样的场景,基本上只要场景中有很多重复的形状,都能够使用实例化渲染来提高性能。