使用Vulkan封裝一個2D小引擎

花了半個月填了下畢設開的坑『基於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>來保存,每次增加刪除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控件的狀態,觸發對應的回調事件即可。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章