玩具项目上传了Github: vulkanTest
vulkan比之前的图形api更注重预处理,既要做什么事必须预先录制好command buffer,
在draw的时候只是submit这个command buffer,
所以换texture不能像openGL那样在draw之前bind texture,
但是在command buffer里面可以在每个draw index之前Bind DescriptorSets
这就要把DescriptorSet里面的sampler给拆出来,然后根据每个物体的texture不同,换上不同的DescriptorSet。
先在原来的程序基础上把sampler拆出来看看。
思路是这样:
①拆DescriptorSetLayout,因为是不同的set了,所以binding可以从0开始
// 这部分不变
VkDescriptorSetLayoutBinding samplerLayoutBinding = {};
samplerLayoutBinding.binding = 0;
samplerLayoutBinding.descriptorCount = 1;
samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
samplerLayoutBinding.pImmutableSamplers = nullptr;
samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
std::array<VkDescriptorSetLayoutBinding, 1> textureBindings = { samplerLayoutBinding };
layoutInfo.bindingCount = static_cast<uint32_t>(textureBindings.size());
layoutInfo.pBindings = textureBindings.data();
// 新增一个texture的layout
if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &textureDescriptorSetLayout) != VK_SUCCESS)
{
throw std::runtime_error("failed to create descriptor set layout!");
}
② 拆pool, 这里要注意,pool size和maxSets是今后总共要从这个pool里出多少个descriptor set的数量,所以应该是这个关卡要用到的所有贴图的数量的数字,我这里暂时用一个hard coded数字代替。
void VulkanAPI::createTextureDescriptorPool()
{
std::array<VkDescriptorPoolSize, 1> texturePoolSizes = {};
texturePoolSizes[0].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
texturePoolSizes[0].descriptorCount = TEXTURE_COUNT;
VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>(texturePoolSizes.size());;
poolInfo.pPoolSizes = texturePoolSizes.data();
poolInfo.maxSets = TEXTURE_COUNT;
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &textureDescriptorPool) != VK_SUCCESS)
{
throw std::runtime_error("failed to create descriptor pool!");
}
}
texture不用每帧都变,所以不用和swapchain的挂钩,单独开一个函数就好了。
③ 从新的pool里出新的DescriptorSet,然后绑定图片
要注意的是因为拆了2个DescriptorSet,所以在shader里要标注好set=0, set=1
layout(set = 0, binding = 0) uniform UniformBufferObject{
mat4 view;
mat4 proj;
} ubo;
layout(set = 1, binding = 0) uniform sampler2D texSampler;
这里的set 的序号由之前做pipeline时候填的VkDescriptorSetLayout顺序决定
VkDescriptorSetLayout descriptorSetLayouts[2] = { descriptorSetLayout, textureDescriptorSetLayout };
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 2;
pipelineLayoutInfo.pSetLayouts = descriptorSetLayouts;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = push_constant_ranges;
command buffer里的vkCmdBindDescriptorSets的顺序也不能错
VkDescriptorSet sets[2] = { descriptorSets[i] , textureDescriptorSet };
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 2, sets, 1, &dynamicOffset);
实验下来结果和原来一样,既代表这个思路是行得通的。
和游戏引擎结合,在vulkan里开一个struct,里面存了一个image的所有相关信息,因为sampler暂时只和mipLevel有关,所以就单独存了一个hash表,用miplevel的数字去索引得到。
struct TextureInfo
{
VkImage textureImage;
uint32_t mipLevels;
VkDeviceMemory textureImageMemory;
VkImageView textureImageView;
VkSampler* textureSampler;
VkDescriptorSet descriptorSet;
};
std::vector<TextureInfo> textureInfos;
std::unordered_map<uint32_t, VkSampler> textureSamplers;
在Resource Manager里面存了一个hash表,如果之前没有读取过的图片地址,就到vulkan里面上传一张图片到GPU,返回一个uint32的数字索引,下次要画什么texture就到textureInfos里面去找。
uint32_t VulkanAPI::UploadTexture(std::string path)
{
TextureInfo newTexture = {};
int texWidth, texHeight, texChannels;
stbi_uc* pixels = stbi_load(path.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
if (!pixels) {
throw std::runtime_error("failed to load texture image!");
}
VkDeviceSize imageSize = texWidth * texHeight * 4;
newTexture.mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;
VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
stagingBuffer, stagingBufferMemory);
void* data;
vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
memcpy(data, pixels, static_cast<size_t>(imageSize));
vkUnmapMemory(device, stagingBufferMemory);
stbi_image_free(pixels);
createImage(texWidth, texHeight, newTexture.mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, newTexture.textureImage, newTexture.textureImageMemory);
transitionImageLayout(newTexture.textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, newTexture.mipLevels);
copyBufferToImage(stagingBuffer, newTexture.textureImage, static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));
vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingBufferMemory, nullptr);
generateMipmaps(newTexture.textureImage, VK_FORMAT_R8G8B8A8_UNORM, texWidth, texHeight, newTexture.mipLevels);
newTexture.textureImageView = createImageView(newTexture.textureImage, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, newTexture.mipLevels);
newTexture.textureSampler = createTextureSampler(newTexture.mipLevels);
// texture descriptorSet
VkDescriptorSetAllocateInfo textureAllocInfo = {};
textureAllocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
textureAllocInfo.descriptorPool = textureDescriptorPool;
textureAllocInfo.descriptorSetCount = 1;
textureAllocInfo.pSetLayouts = &textureDescriptorSetLayout;
if (vkAllocateDescriptorSets(device, &textureAllocInfo, &newTexture.descriptorSet) != VK_SUCCESS)
{
throw std::runtime_error("failed to allocate descriptor sets");
}
VkDescriptorImageInfo imageInfo = {};
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
imageInfo.imageView = newTexture.textureImageView;
imageInfo.sampler = *newTexture.textureSampler;
VkWriteDescriptorSet texturedescriptorWrites = {};
texturedescriptorWrites.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
texturedescriptorWrites.dstSet = newTexture.descriptorSet;
texturedescriptorWrites.dstBinding = 0;
texturedescriptorWrites.dstArrayElement = 0;
texturedescriptorWrites.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
texturedescriptorWrites.descriptorCount = 1;
texturedescriptorWrites.pImageInfo = &imageInfo;
texturedescriptorWrites.pTexelBufferView = nullptr;
vkUpdateDescriptorSets(device, 1, &texturedescriptorWrites, 0, nullptr);
textureInfos.push_back(newTexture);
return uint32_t(textureInfos.size()-1);
}
transform经过我的优化后变成这样:
1.Resource Manager读取这个关卡要用的所有mesh数据
1.Vulkan初始化的时候拿到mesh的数量,给每个mesh单独开一个model的数组,当然这些model还是在同一个uniformbuffer上的
3.每个有mesh的game object会拿到一个meshData的指针,用这个指针去找到这个mesh的model的空余位置,在把那个mat4的指针交给transform里面,这样transform的更新直接就修改那个model的数据,每次draw之前,把所有的model按照mesh种类的大的offset用多少复制上去多少
uint32_t index = 0;
for (auto &dynamicUBO : uboDynamic)
{
void* data = reinterpret_cast<size_t*>(dynamicUniformData[currentImage]) + normalUBOAlignment / sizeof(size_t) + index* (LONG_SIZE * dynamicAlignment / sizeof(size_t));
memcpy(data, dynamicUBO.second.model, dynamicUBO.second.inversePtr.size() * dynamicAlignment);
++index;
}
vertex index的Buffer也根据mesh的种类对应申请
最后的drawCall长这样
uint32_t k = 0;
for (auto &dubo : uboDynamic) {
VkDeviceSize offsets[] = { 0 };
vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, &vertexIndexBuffers[k], offsets);
vkCmdBindVertexBuffers(commandBuffers[i], 1, 1, &instanceBuffer, offsets);
vkCmdBindIndexBuffer(commandBuffers[i], vertexIndexBuffers[k], sizeof((*dubo.first).vertices[0])*(*dubo.first).vertices.size(), VK_INDEX_TYPE_UINT32);
for (uint32_t j = 0; j < dubo.second.inversePtr.size(); ++j)
{
uint32_t dynamicOffset = k* LONG_SIZE * static_cast<uint32_t>(dynamicAlignment) + j * static_cast<uint32_t>(dynamicAlignment);
VkDescriptorSet sets[2] = { descriptorSets[i] , textureInfos[dubo.second.inversePtr[j]->textureID].descriptorSet };
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 2, sets, 1, &dynamicOffset);
vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>((*dubo.first).indices.size()), INSTANCE_COUNT, 0, 0, 0);
}
++k;
}
增删model指针其实很简单,为了保持结构紧凑,增就拿最后一个的后一个,删就把最后一个和删除的那个交换,然后总数量-1
struct DynamicUBO {
glm::mat4 *model = nullptr;
static size_t DU_Alignment;
std::vector<Transform*> inversePtr;
glm::mat4* GetAvailableModel(Transform* pTr)
{
glm::mat4* m = (glm::mat4*)(((uint64_t)model + (inversePtr.size() * DU_Alignment)));
inversePtr.push_back(pTr);
return m;
}
void DeleteModel(glm::mat4* m)
{
glm::mat4* lastModel = (glm::mat4*)(((uint64_t)model + ((inversePtr .size()-1) * DU_Alignment)));
size_t index = ((uint64_t)m - (uint64_t)model) / DU_Alignment;
if (m != lastModel) {
*m = *lastModel;
inversePtr.back()->transMatrix = m;
inversePtr[index] = inversePtr.back();
}
inversePtr.resize(inversePtr.size() - 1);
}
};
来个效果图