即日起在codingBlog上分享您的技术经验即可获得积分,积分可兑换现金哦。

运用Vulkan封装一个2D小引擎

编程语言 ZhangDi2017 9℃ 0评论

花了半个月填了下毕设开的坑『基于Vulkan的2D游戏引擎的设计与实现』,最终实现了一个简单的2D小引擎,算是体验了Vulkan的开发流程。

主循环非常常规,三段式:

void GameEngine::Run()
{
 while (!glfwWindowShouldClose(this->window))
 {
  glfwPollEvents();
  this->ProcessInput();
  this->UpdateLogic();
  this->DrawFrame();
 }

 glfwDestroyWindow(this->window);
 glfwTerminate();
}

用到了5个单例Manager,VulkanRenderer、InputManager、AudioManager、GUIManager和TimerManager。

Vulkan初始化过程如下:

void InitVulkan()
{
 createInstance();  // 创建VkApplication && VkInstance
 setUpDebugCallback();  // 设置Vulkan Debug回调信息
 createSurface();  // 创建窗口相关的VkSurface
 pickPhysicalDevice();  // 选择计算机物理显卡,创建VkPhysicsDevice
 createLogicalDevice();  // 创建逻辑设备VkDevice
 createSwapChain();  // 创建交换链SwapChain并获取Images
 createImageViews();  // 为每一个SwapChain Images创建ImageView
 createRenderPass();  // 创建RenderPass,配置颜色和深度的信息
 creatDescriptorSetLayout(); // 创建描述符信息,为Shader的属性提供信息
 createGriphicsPipeline(); // 创建渲染管线,一旦创建几乎不能动态更改
 createCommandPool();  // 创建命令池
 createDepthResources();  // 创建深度纹理,用于DepthTest
 createFragmentBuffer();  // 创建帧缓冲,Vulkan默认采用三重缓冲
 createTextureSampler();  // 创建可以重复使用的TextureSampler
 createIndexBuffer();  // 创建可以重复使用的IBO
 createDescriptorPool();  // 创建描述符池,描述符用于绑定Shader的属性
 createSemaphores();  // 创建绘制和呈现所需的信号量
}

要绘制的对象用一个std::vector来保存,每次增加删除Sprite时按绘制顺序排序,比如先绘制不透明图片,再绘制透明图片。

因为要绘制半透明物体,渲染关系的相关配置如下:

// 深度缓冲和模板缓存的配置
VkPipelineDepthStencilStateCreateInfo depthStencil = {};
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencil.depthTestEnable = VK_TRUE;
depthStencil.depthWriteEnable = VK_FALSE; // 绘制半透明物体禁用深度写入
depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;
depthStencil.depthBoundsTestEnable = VK_FALSE;
depthStencil.stencilTestEnable = VK_FALSE;

// 颜色混合配置,包括RGB混合和Alpha值的混合,可以采用不同的配置
// 开启混合时:rgb = src.rgb * src.a + dst.rgb * (1 - src.a)
//             a   = src.a  
// 关闭混合时:rgb = src.rgb
//             a   = src.a
VkPipelineColorBlendAttachmentState state = {};
state.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT;
state.blendEnable = VK_TRUE;
state.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA;
state.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA;
state.colorBlendOp = VK_BLEND_OP_ADD;
state.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE;
state.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO;
state.alphaBlendOp = VK_BLEND_OP_ADD;

每次新增一个Sprite时,首先根据窗口大小调整Sprite的状态,随后为其创建对应的Vulkan对象:

Sprite* AddSprite(int x, int y, int width, int height, const char* fileName)
{
 Sprite *sprite = new Sprite{ device, glm::vec2(width, height), glm::vec2(windowWidth, windowHeight),glm::vec2(x, y), fileName };
 sprite->SetUp();
 createTextureImage(sprite); //为Sprite创建相关的Vulkan对象,Image、ImageView,绑定VertexBuffer与UniformBuffer
 createTextureImageView(sprite);
 createVertexBuffer(sprite);
 createUniformBuffer(sprite);
 createDescriptorSet(sprite); // 绑定描述符信息
 spriteList.push_back(sprite);
 recreateCommandBuffer = true; // 重建Vulkan的CommandBuffer
 return sprite;
}

每帧绘制时,发现Sprite状态改变则重建Sprite对应的对象信息,如果Sprite隐藏显示或新增删除,则重建CommandBuffer:

void UpdateSprites()
{
 for each (auto sprite in spriteList)
 {
  if (sprite->shouldUpdateTransform) // 重新将Sprite的MVP矩阵信息拷贝到Vulkan的UniformBuffer中
                        UpdateTransform(*sprite);
  if (sprite->shouldUpdateVertex) // 重新将Sprite的顶点信息(主要是颜色)拷贝到Vulkan的VertexBuffer中
   UpdateVertex(*sprite);
  if (sprite->shouldRecreateCommandBuffer)
  {
   sprite->shouldRecreateCommandBuffer = false;
   recreateCommandBuffer = true;
  }
 }
 if (recreateCommandBuffer)
 {
  vkDeviceWaitIdle(device);
  createCommandBuffer();
  recreateCommandBuffer = false;
 }
}

图片的移动、旋转、缩放主要由修改UBO来实现:

void UpdateTranform()
{
 ubo.model = glm::translate(glm::mat4(), glm::vec3(position.x / windowSize.x * 2, position.y / windowSize.x * 2, 0));
 ubo.model = glm::rotate(ubo.model, glm::radians(this->angle), glm::vec3(0, 0, 1.0f));
 ubo.model = glm::scale(ubo.model, glm::vec3(scale, 1));

 shouldUpdateTransform = false;
}

图片的颜色叠加和镜像翻转主要是修改顶点信息来实现:

void UpdateVertex()
{
 vertices[0].color = color; // 刷新顶点颜色信息
 vertices[1].color = color;
 vertices[2].color = color;
 vertices[3].color = color;

 if (shouldFlip)   // 如果镜像则交换左右两边UV,同时也可以用来做序列帧动画(序列帧在同一张图片里)
 {
  glm::vec2 uvTemp;
  uvTemp = vertices[0].texCoord;
  vertices[0].texCoord = vertices[1].texCoord;
  vertices[1].texCoord = uvTemp;
  uvTemp = vertices[2].texCoord;
  vertices[2].texCoord = vertices[3].texCoord;
  vertices[3].texCoord = uvTemp;
  shouldFlip = false;
 }
 shouldUpdateVertex = false;
}

在UpdateVertex或者Uniform之后,还需要将数据Copy至Vulkan对应的Buffer中去,在创建Sprite对应的数据时进行了绑定,所以直接用memcpy复制即可。

重建CommandBuffer的核心部分如下,按顺序绘制Active状态的物体:

vkCmdBeginRenderPass(commandBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
 VkDeviceSize offsets[] = { 0 };
 for (size_t j = 0; j < spriteList.size(); j++)
 {
  if (!spriteList[j]->GetActive()) // 非Active跳过
   continue;
  vkCmdBindVertexBuffers(commandBuffers[i], 0, 1, &spriteList[j]->vertexBuffer, offsets);
  vkCmdBindIndexBuffer(commandBuffers[i], indexBuffer, 0, VK_INDEX_TYPE_UINT32);
  vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &spriteList[j]->descriptorSet, 0, nullptr);
  vkCmdDrawIndexed(commandBuffers[i], indices.size(), 1, 0, 0, 0);
 }
vkCmdEndRenderPass(commandBuffers[i]);

渲染效果如下,包含旋转、缩放、平移、镜像和颜色叠加的效果:



剩下还可以扩展的功能非常多,包括Sprite节点树(父子物体关系),基于AABB树的碰撞检测等等。

其余的模块基本输入模块对GLFW封装了一层,音效模块用了FMOD,GUI模块在渲染和输入模块的基础上封装而来,每帧检查鼠标位置,处理GUI控件的状态,触发对应的回调事件即可。


转载请注明:CodingBlog » 运用Vulkan封装一个2D小引擎

喜欢 (0)or分享 (0)
发表我的评论
取消评论

*

表情