Vulkan_布料仿真模擬(Mass Spring Systems_質點彈簧模型)

布料仿真的常用方法就是將布料表達爲三維網格,然後通過彈力+外力進行布料仿真。它在遊戲中有着廣泛的應用,如衣袖的擺動,旗幟的飄動;此外,也可以用於模擬試衣等。
在這裏插入圖片描述
(僅重力與風力作用下布料圖片)

一、彈簧質點系統(Mass Spring Systems)

一個模擬變形物體最簡單額方法就是將其表示爲彈簧質點系統(Mass Spring Systems),因爲方法簡單,這個框架對於學習物理模擬和時間積分的各種技術都比較理想。一個彈簧質點包含了一系列由多個彈簧連接起來的質點,這樣的系統的物理屬性非常直接,模擬程序也很容易編寫。

1.1 Mass Spring Systems

點彈簧模型,即將物體的質量離散到一個個質點,質點之間再用彈簧連接來表現物體彈力、阻尼力等物理特性的模型。爲了不斷提升仿真度,研究者在這一簡單模型的基礎上做了許多改進,單單是連接質點的彈簧就有三大類。
在這裏插入圖片描述
第一種是結構性彈簧,這種彈簧用於連接橫向和縱向的質點,起到固定模型結構的作用。第二種是扭曲彈簧,也叫剪切彈簧,連接對角線上的相鄰質點,起到防止模型扭曲變形的作用。第三種是拉伸性彈簧,也叫彎曲彈簧連接橫向和縱向相隔着一個質點的兩個質點,保證模型形變時的邊緣圓滑(比如布料摺疊)。

1.2 物理模型(阻尼力和彈力)

假設有N個質點,質量爲mi,位置爲xi,速度爲vi , 1<i<N.

這些質點由一組彈簧S連接,彈簧參數爲(i, j, lo, ks, kd). i,j 爲連接的彈簧質點,lo爲彈簧完全放鬆時的長度,ks爲彈簧彈性係數,kd爲阻尼係數,由胡科定律知彈簧施加在兩頂點上的力可以表示爲:
在這裏插入圖片描述
由受力守恆知 fi+fj = 0. fi和fj的大小和彈簧伸長成正比關係。
對於阻尼的計算:
在這裏插入圖片描述
大小和速度成正比,並且符合力守恆,則對於一個質點,其受力方程爲:
在這裏插入圖片描述

1.3 模擬算法

在模擬算法中,牛頓第二定律是關鍵,f = ma。在已知質量和外力的情況下,通過 a=f/m 可以得到加速度,將二階常微分方程寫成兩個一階方程:
在這裏插入圖片描述
可以得到解析解:
在這裏插入圖片描述
初始狀態爲 v(to) = vo, x(to)=xo.

積分將時間t內所有的變化加和,模擬的過程就是從 to 開始不斷地計算 x(t) 和 v(t),然後更新質點的位置。

最簡單的數值解法就是利用有限的差分近似微分:
在這裏插入圖片描述

其中delta t 是爲時間步長,t爲幀號,帶入之前的公式得:
在這裏插入圖片描述
這個就是顯式的歐拉解法,下一時刻的狀態完全由當前狀態決定。

整個過程的僞代碼如下:

 // 初始化數據
 forall particles i
         initialize xi , vi and mi
 endfor
 // 循環模擬
 loop
        forall particles i
               fi ← fg + fcoll +f(xi , vi , x j , v j )
        endfor
        forall particles i
             vi ← vi + ∆t fi /mi
             xi ← xi + ∆t vi
         endfor
         display the system every nth time
 endloop

fg表示重力,fcoll表示碰撞外力。

顯式積分是最簡單的積分方法,不過有一個嚴重的問題 ——它只能在步長很小的時候穩定,這就意味着在兩個可視幀中間需要進行多個模擬步驟,不穩定的主要原因是歐拉方法僅僅依賴於當前狀態,它假設在整個步長中,外力是個常量。

1.4 布料仿真

在質點彈簧模型中,整個布料由網格點表達,而點與點之間的連線,我們稱之爲彈簧。
在這裏插入圖片描述
布料一個點的位置是由它周圍的12個點控制的。其中8個是相鄰點,另外4個是隔點的鄰接點。
在這裏插入圖片描述
對於彈簧而言,存在三種彈簧,一種是結構彈簧,一種是剪切彈簧,還有一種是彎曲彈簧。它們分別與上圖12根彈簧對應。

其中,結構彈簧模擬的是橫向的力,爲了阻止布料在橫向產生較大的拉壓變形。

剪切彈簧同理,是爲了阻止布料在斜向產生較大的拉壓變形。

而彎曲彈簧連接了兩個相隔的點,它主要作用是在布料發生彎曲形變的時候,阻止它的過度彎曲,導致布料的角一下子坍塌。這個係數有時候也可以不考慮。
在這裏插入圖片描述
在具體的代碼實現中,我們初始化點的位置、速度(初始化爲0),質量(設爲統一的值),以及是否固定(固定後將不受外力,即重力)。

二、vulkan實現

本部分使用了基於GPU計算着色器計算和整合彈簧力來實現布料仿真模型,同時也實現一個布料與固定場景對象的基本碰撞。

2.1 場景數據結構體

首先爲場景自定義計算管線、圖形管線及布料相關結構體:

	//場景
	uint32_t sceneSetup = 1; //0布料碰撞場景,1純布料模擬
	bool specializedComputeQueue = false; //後續介紹其用處
	//圖形管線部分的資源
	struct {
		VkDescriptorSetLayout descriptorSetLayout;
		VkDescriptorSet descriptorSet;
		VkPipelineLayout pipelineLayout;
		struct Pipelines {
			VkPipeline cloth;
			VkPipeline sphere;
		} pipelines;
		vks::Buffer indices;
		vks::Buffer uniformBuffer;
		struct graphicsUBO {
			glm::mat4 projection;
			glm::mat4 view;
			glm::vec4 lightPos = glm::vec4(-1.0f, 3.0f, -1.0f, 1.0f);
		} ubo;
	} graphics;

	//計算管線部分的資源
	struct {
		struct StorageBuffers {
			vks::Buffer input;
			vks::Buffer output;
		} storageBuffers;
		struct Semaphores {
			VkSemaphore ready{ 0L };
			VkSemaphore complete{ 0L };
		} semaphores;
		vks::Buffer uniformBuffer;
		VkQueue queue;
		VkCommandPool commandPool;
		std::array<VkCommandBuffer,2> commandBuffers;
		VkDescriptorSetLayout descriptorSetLayout;
		std::array<VkDescriptorSet,2> descriptorSets;
		VkPipelineLayout pipelineLayout;
		VkPipeline pipeline;
		struct computeUBO {
			float deltaT = 0.0f;                                     //時間步長
			float particleMass = 0.1f;                               //粒子大小
			float springStiffness = 8000.0f;                         //彈簧剛度
			float damping = 0.25f;                                   //衰減
			float restDistH;                                         //水平彈簧固定長度
			float restDistV;                                         //豎直彈簧固定長度
			float restDistD;				                         //斜向彈簧固定長度
			float sphereRadius = 0.5f;                               //球體半徑
			glm::vec4 spherePos = glm::vec4(0.0f, 0.0f, 0.0f, 0.0f); //球體位置
			glm::vec4 gravity = glm::vec4(0.0f, 9.8f, 0.0f, 0.0f);   //重力
			glm::ivec2 particleCount;                                //粒子數
		} ubo;
	} compute;

	// SSBO布料網格粒子聲明
	struct Particle {
		glm::vec4 pos;
		glm::vec4 vel;
		glm::vec4 uv;
		glm::vec4 normal;
		float pinned;
		glm::vec3 _pad0;
	};
	
	//衣服網格屬性
	struct Cloth {
		glm::uvec2 gridsize = glm::uvec2(50, 50);
		glm::vec2 size = glm::vec2(2.0f, 2.0f);
	} cloth;

2.2 存儲緩衝區數據創建

定義好場景數據後,我們需要先創建一個prepareStorageBuffers函數來填充計算着色器存儲緩衝區:

	//設置並填充包含粒子的計算着色器存儲緩衝區
	void prepareStorageBuffers()
	{
		std::vector<Particle> particleBuffer(cloth.gridsize.x *  cloth.gridsize.y);

		float dx =  cloth.size.x / (cloth.gridsize.x - 1);
		float dy =  cloth.size.y / (cloth.gridsize.y - 1);
		float du = 1.0f / (cloth.gridsize.x - 1);
		float dv = 1.0f / (cloth.gridsize.y - 1);
		switch (sceneSetup) {
			case 0 :
			{
				//  水平調整.布落在球體上
				glm::mat4 transM = glm::translate(glm::mat4(1.0f), glm::vec3(- cloth.size.x / 2.0f, -2.0f, - cloth.size.y / 2.0f));
				for (uint32_t i = 0; i <  cloth.gridsize.y; i++) {
					for (uint32_t j = 0; j <  cloth.gridsize.x; j++) {
						particleBuffer[i + j * cloth.gridsize.y].pos = transM * glm::vec4(dx * j, 0.0f, dy * i, 1.0f);
						particleBuffer[i + j * cloth.gridsize.y].vel = glm::vec4(0.0f);
						particleBuffer[i + j * cloth.gridsize.y].uv = glm::vec4(1.0f - du * i, dv * j, 0.0f, 0.0f);
					}
				}
				break;
			}
			case 1:
			{
				// 豎直.固定布
				glm::mat4 transM = glm::translate(glm::mat4(1.0f), glm::vec3(- cloth.size.x / 2.0f, - cloth.size.y / 2.0f, 0.0f));
				for (uint32_t i = 0; i <  cloth.gridsize.y; i++) {
					for (uint32_t j = 0; j <  cloth.gridsize.x; j++) {
						particleBuffer[i + j * cloth.gridsize.y].pos = transM * glm::vec4(dx * j, dy * i, 0.0f, 1.0f);
						particleBuffer[i + j * cloth.gridsize.y].vel = glm::vec4(0.0f);
						particleBuffer[i + j * cloth.gridsize.y].uv = glm::vec4(du * j, dv * i, 0.0f, 0.0f);
						// 固定粒子
						//particleBuffer[i + j * cloth.gridsize.y].pinned = (i == 0) && ((j == 0) || (j ==  cloth.gridsize.x / 4) || (j ==  cloth.gridsize.x -  cloth.gridsize.x / 4) || (j ==  cloth.gridsize.x - 1)); //四點
						//particleBuffer[i + j * cloth.gridsize.y].pinned = (i == 0) && ((j == 0) || (j ==  cloth.gridsize.x / 2) || (j ==  cloth.gridsize.x - 1));  //三點
						particleBuffer[i + j * cloth.gridsize.y].pinned = (i == 0) && ((j == 0) || (j ==  cloth.gridsize.x - 1));  //兩點
						// 移動球體 消去球體碰撞影響
						compute.ubo.spherePos.z = -100.0f;
					}
				}
				break;
			}
		}

		VkDeviceSize storageBufferSize = particleBuffer.size() * sizeof(Particle);
		
		// 存儲數據
		// 上傳後主機上的SSBO不會被改變,所以複製到設備本地內存
		vks::Buffer stagingBuffer;

		vulkanDevice->createBuffer(
			VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
			VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
			&stagingBuffer,
			storageBufferSize,
			particleBuffer.data());

		vulkanDevice->createBuffer(
			VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
			VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
			&compute.storageBuffers.input,
			storageBufferSize);

		vulkanDevice->createBuffer(
			VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
			VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
			&compute.storageBuffers.output,
			storageBufferSize);

		// 從暫存緩衝區複製
		VkCommandBuffer copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
		VkBufferCopy copyRegion = {};
		copyRegion.size = storageBufferSize;
		vkCmdCopyBuffer(copyCmd, stagingBuffer.buffer, compute.storageBuffers.input.buffer, 1, &copyRegion);
		vkCmdCopyBuffer(copyCmd, stagingBuffer.buffer, compute.storageBuffers.output.buffer, 1, &copyRegion);

		//爲圖形隊列添加一個初始釋放屏障,以便當計算命令緩衝區第一次執行時它不會缺少與它的“獲得”相對應的“釋放”
		addGraphicsToComputeBarriers(copyCmd);
		vulkanDevice->flushCommandBuffer(copyCmd, queue, true);

		stagingBuffer.destroy();

		// Indices  索引數據
		std::vector<uint32_t> indices;
		for (uint32_t y = 0; y <  cloth.gridsize.y - 1; y++) {
			for (uint32_t x = 0; x <  cloth.gridsize.x; x++) {
				indices.push_back((y + 1) *  cloth.gridsize.x + x);
				indices.push_back((y)*  cloth.gridsize.x + x);
			}
			// 原語重啓(由特殊值0xFFFFFFFF填充)
			indices.push_back(0xFFFFFFFF);
		}
		uint32_t indexBufferSize = static_cast<uint32_t>(indices.size()) * sizeof(uint32_t);
		indexCount = static_cast<uint32_t>(indices.size());

		vulkanDevice->createBuffer(
			VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
			VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
			&stagingBuffer,
			indexBufferSize,
			indices.data());

		vulkanDevice->createBuffer(
			VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT,
			VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
			&graphics.indices,
			indexBufferSize);

		// 從暫存緩衝區複製
		copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
		copyRegion = {};
		copyRegion.size = indexBufferSize;
		vkCmdCopyBuffer(copyCmd, stagingBuffer.buffer, graphics.indices.buffer, 1, &copyRegion);
		vulkanDevice->flushCommandBuffer(copyCmd, queue, true);

		stagingBuffer.destroy();
	}

在此函數中,我們可以看到我們調用了一個addGraphicsToComputeBarriers方法,爲圖形隊列添加一個初始釋放屏障,其具體實現爲:

	//爲圖形隊列添加屏障
	void addGraphicsToComputeBarriers(VkCommandBuffer commandBuffer) 
	{
		if (specializedComputeQueue) {
			VkBufferMemoryBarrier bufferBarrier = vks::initializers::bufferMemoryBarrier();
			bufferBarrier.srcAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;
			bufferBarrier.dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
			bufferBarrier.srcQueueFamilyIndex = vulkanDevice->queueFamilyIndices.graphics;
			bufferBarrier.dstQueueFamilyIndex = vulkanDevice->queueFamilyIndices.compute;
			bufferBarrier.size = VK_WHOLE_SIZE;

			std::vector<VkBufferMemoryBarrier> bufferBarriers;
			bufferBarrier.buffer = compute.storageBuffers.input.buffer;
			bufferBarriers.push_back(bufferBarrier);
			bufferBarrier.buffer = compute.storageBuffers.output.buffer;
			bufferBarriers.push_back(bufferBarrier);
			vkCmdPipelineBarrier(commandBuffer,
				VK_PIPELINE_STAGE_VERTEX_INPUT_BIT,
				VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
				VK_FLAGS_NONE,
				0, nullptr,
				static_cast<uint32_t>(bufferBarriers.size()), bufferBarriers.data(),
				0, nullptr);
		}
	} 

其中,specializedComputeQueue爲全局函數。

		//確保對於不同的圖形和計算隊列族以及相同的隊列族,代碼都能正常工作
#ifdef DEBUG_FORCE_SHARED_GRAPHICS_COMPUTE_QUEUE
		vulkanDevice->queueFamilyIndices.compute = vulkanDevice->queueFamilyIndices.graphics;
#endif
		//檢查計算隊列族與圖形隊列族是否不同
		specializedComputeQueue = vulkanDevice->queueFamilyIndices.graphics != vulkanDevice->queueFamilyIndices.compute;

與此同時,我們還需要設置其餘兩個管線屏障,供不同管線使用時處理資源問題,後續會一一用到並詳述:

	//計算着色器分配做業時計算命令添加屏障
	void addComputeToComputeBarriers(VkCommandBuffer commandBuffer) 
	{
		VkBufferMemoryBarrier bufferBarrier = vks::initializers::bufferMemoryBarrier();
		bufferBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
		bufferBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
		bufferBarrier.srcQueueFamilyIndex = vulkanDevice->queueFamilyIndices.compute;
		bufferBarrier.dstQueueFamilyIndex = vulkanDevice->queueFamilyIndices.compute;
		bufferBarrier.size = VK_WHOLE_SIZE;
		std::vector<VkBufferMemoryBarrier> bufferBarriers;
		bufferBarrier.buffer = compute.storageBuffers.input.buffer;
		bufferBarriers.push_back(bufferBarrier);
		bufferBarrier.buffer = compute.storageBuffers.output.buffer;
		bufferBarriers.push_back(bufferBarrier);
		vkCmdPipelineBarrier(
			commandBuffer,
			VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
			VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
			VK_FLAGS_NONE,
			0, nullptr,
			static_cast<uint32_t>(bufferBarriers.size()), bufferBarriers.data(),
			0, nullptr);
	} 
	//存儲緩衝區釋放回圖形隊列時使用
	void addComputeToGraphicsBarriers(VkCommandBuffer commandBuffer)
	{
		if (specializedComputeQueue) {
			VkBufferMemoryBarrier bufferBarrier = vks::initializers::bufferMemoryBarrier();
			bufferBarrier.srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT;
			bufferBarrier.dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT;
			bufferBarrier.srcQueueFamilyIndex = vulkanDevice->queueFamilyIndices.compute;
			bufferBarrier.dstQueueFamilyIndex = vulkanDevice->queueFamilyIndices.graphics;
			bufferBarrier.size = VK_WHOLE_SIZE;
			std::vector<VkBufferMemoryBarrier> bufferBarriers;
			bufferBarrier.buffer = compute.storageBuffers.input.buffer;
			bufferBarriers.push_back(bufferBarrier);
			bufferBarrier.buffer = compute.storageBuffers.output.buffer;
			bufferBarriers.push_back(bufferBarrier);
			vkCmdPipelineBarrier(
				commandBuffer,
				VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
				VK_PIPELINE_STAGE_VERTEX_INPUT_BIT,
				VK_FLAGS_NONE,
				0, nullptr,
				static_cast<uint32_t>(bufferBarriers.size()), bufferBarriers.data(),
				0, nullptr);
		}
	} 

2.3 UniformBuffers創建

在創建好存儲緩衝區數據後,我們需要準備和初始化 UniformBuffer供着色器調配使用:

	// 準備和初始化包含着色器的Uniform緩衝區
	void prepareUniformBuffers()
	{
		// 計算着色均勻緩衝塊
		vulkanDevice->createBuffer(
			VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
			VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
			&compute.uniformBuffer,
			sizeof(compute.ubo));
		VK_CHECK_RESULT(compute.uniformBuffer.map());
	
		// Initial values 初始值
		float dx = cloth.size.x / (cloth.gridsize.x - 1);
		float dy = cloth.size.y / (cloth.gridsize.y - 1);
		//彈簧系統長度定義
		compute.ubo.restDistH = dx;
		compute.ubo.restDistV = dy;
		compute.ubo.restDistD = sqrtf(dx * dx + dy * dy);
		compute.ubo.particleCount = cloth.gridsize;

		updateComputeUBO();

		// 頂點着色均勻緩衝塊
		vulkanDevice->createBuffer(
			VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT,
			VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT,
			&graphics.uniformBuffer,
			sizeof(graphics.ubo));
		VK_CHECK_RESULT(graphics.uniformBuffer.map());

		updateGraphicsUBO();
	}
	//更新計算着色器中Uniform數據
	void updateComputeUBO()
	{
		if (!paused) {
			compute.ubo.deltaT = 0.000005f;
			//compute.ubo.deltaT = frameTimer * 0.0075f;
			//模擬添加風力
			if (simulateWind) {
				std::default_random_engine rndEngine(benchmark.active ? 0 : (unsigned)time(nullptr));
				std::uniform_real_distribution<float> rd(1.0f, 6.0f);
				compute.ubo.gravity.x = cos(glm::radians(-timer * 360.0f)) * (rd(rndEngine) - rd(rndEngine));
				compute.ubo.gravity.z = sin(glm::radians(timer * 360.0f)) * (rd(rndEngine) - rd(rndEngine));
			}
			else {
				compute.ubo.gravity.x = 1.0f;
				compute.ubo.gravity.z = 1.0f;
			}
		}
		else {
			compute.ubo.deltaT = 0.0f;
		}
		memcpy(compute.uniformBuffer.mapped, &compute.ubo, sizeof(compute.ubo));
	}
	//更新圖形着色器中Uniform數據
	void updateGraphicsUBO()
	{
		graphics.ubo.projection = camera.matrices.perspective;
		graphics.ubo.view = camera.matrices.view;
		memcpy(graphics.uniformBuffer.mapped, &graphics.ubo, sizeof(graphics.ubo));
	}

2.4 圖形管線及shader

接下來就是常規創建描述符,描述符池,描述符集及圖形管線等常規操作,其中的布料及圓球管線中僅有簡單馮氏光照模型,具體的就不在詳述,balabala…

2.5 質點彈簧系統處理

重點來了,我們需要單獨創建一個計算管線來處理質點彈簧,舉一個簡單明瞭的例子,在初始時刻,一個布料的質點狀態如下圖:
在這裏插入圖片描述
經過計算管線處理相鄰質點作用力後,所有質點皆會在其力作用下變形如下圖:
在這裏插入圖片描述
我們首先需要創建一個單獨的計算着色器來處理彈簧系統的計算:

	void prepareCompute()
	{
		// 創建一個具有計算能力的設備隊列
		vkGetDeviceQueue(device, vulkanDevice->queueFamilyIndices.compute, 0, &compute.queue);

		// 創建計算管道
		std::vector<VkDescriptorSetLayoutBinding> setLayoutBindings = {
			vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_COMPUTE_BIT, 0),
			vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_COMPUTE_BIT, 1),
			vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_COMPUTE_BIT, 2),
		};

		VkDescriptorSetLayoutCreateInfo descriptorLayout =
			vks::initializers::descriptorSetLayoutCreateInfo(setLayoutBindings);

		VK_CHECK_RESULT(vkCreateDescriptorSetLayout(device,	&descriptorLayout, nullptr,	&compute.descriptorSetLayout));

		VkPipelineLayoutCreateInfo pipelineLayoutCreateInfo =
			vks::initializers::pipelineLayoutCreateInfo(&compute.descriptorSetLayout, 1);

		// 用於傳遞一些參數的壓入常量
		VkPushConstantRange pushConstantRange =
			vks::initializers::pushConstantRange(VK_SHADER_STAGE_COMPUTE_BIT, sizeof(uint32_t), 0);
		pipelineLayoutCreateInfo.pushConstantRangeCount = 1;
		pipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange;

		VK_CHECK_RESULT(vkCreatePipelineLayout(device, &pipelineLayoutCreateInfo, nullptr,	&compute.pipelineLayout));

		VkDescriptorSetAllocateInfo allocInfo =
			vks::initializers::descriptorSetAllocateInfo(descriptorPool, &compute.descriptorSetLayout, 1);

		// 創建切換輸入和輸出緩衝區的兩個描述符集
		VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &compute.descriptorSets[0]));
		VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &compute.descriptorSets[1]));

		std::vector<VkWriteDescriptorSet> computeWriteDescriptorSets = {
			vks::initializers::writeDescriptorSet(compute.descriptorSets[0], VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, &compute.storageBuffers.input.descriptor),
			vks::initializers::writeDescriptorSet(compute.descriptorSets[0], VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, &compute.storageBuffers.output.descriptor),
			vks::initializers::writeDescriptorSet(compute.descriptorSets[0], VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2, &compute.uniformBuffer.descriptor),

			vks::initializers::writeDescriptorSet(compute.descriptorSets[1], VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 0, &compute.storageBuffers.output.descriptor),
			vks::initializers::writeDescriptorSet(compute.descriptorSets[1], VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, &compute.storageBuffers.input.descriptor),
			vks::initializers::writeDescriptorSet(compute.descriptorSets[1], VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 2, &compute.uniformBuffer.descriptor)
		};

		vkUpdateDescriptorSets(device, static_cast<uint32_t>(computeWriteDescriptorSets.size()), computeWriteDescriptorSets.data(), 0, NULL);

		// 創建管道
		VkComputePipelineCreateInfo computePipelineCreateInfo = vks::initializers::computePipelineCreateInfo(compute.pipelineLayout, 0);
		computePipelineCreateInfo.stage = loadShader(getAssetPath() + "shaders/computecloth/cloth.comp.spv", VK_SHADER_STAGE_COMPUTE_BIT);
		VK_CHECK_RESULT(vkCreateComputePipelines(device, pipelineCache, 1, &computePipelineCreateInfo, nullptr, &compute.pipeline));

		// 作爲計算隊列家族的單獨命令池可能與圖形不同
		VkCommandPoolCreateInfo cmdPoolInfo = {};
		cmdPoolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
		cmdPoolInfo.queueFamilyIndex = vulkanDevice->queueFamilyIndices.compute;
		cmdPoolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
		VK_CHECK_RESULT(vkCreateCommandPool(device, &cmdPoolInfo, nullptr, &compute.commandPool));

		// 爲計算操作創建一個命令緩衝區
		VkCommandBufferAllocateInfo cmdBufAllocateInfo =
			vks::initializers::commandBufferAllocateInfo(compute.commandPool, VK_COMMAND_BUFFER_LEVEL_PRIMARY, 2);	

		VK_CHECK_RESULT(vkAllocateCommandBuffers(device, &cmdBufAllocateInfo, &compute.commandBuffers[0]));

		// 圖形/計算同步的信號量
		VkSemaphoreCreateInfo semaphoreCreateInfo = vks::initializers::semaphoreCreateInfo();
		VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &compute.semaphores.ready));
		VK_CHECK_RESULT(vkCreateSemaphore(device, &semaphoreCreateInfo, nullptr, &compute.semaphores.complete));

		// 構建包含計算分派命令的單個命令緩衝區
		buildComputeCommandBuffer();
	}

其中,主要的計算着色器部分下一部分詳述,此外我們需要單獨創建命令緩衝來對質點彈簧進行計算:

	void buildComputeCommandBuffer()
	{
		VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo();
		cmdBufInfo.flags = VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;

		for (uint32_t i = 0; i < 2; i++) {

			VK_CHECK_RESULT(vkBeginCommandBuffer(compute.commandBuffers[i], &cmdBufInfo));

			// 從圖形隊列中獲取存儲緩衝區
			addGraphicsToComputeBarriers(compute.commandBuffers[i]);

			vkCmdBindPipeline(compute.commandBuffers[i], VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipeline);

			uint32_t calculateNormals = 0;
			vkCmdPushConstants(compute.commandBuffers[i], compute.pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(uint32_t), &calculateNormals);

			// 分配計算作業
			const uint32_t iterations = 32;
			for (uint32_t j = 0; j < iterations; j++) {
				readSet = 1 - readSet;
				vkCmdBindDescriptorSets(compute.commandBuffers[i], VK_PIPELINE_BIND_POINT_COMPUTE, compute.pipelineLayout, 0, 1, &compute.descriptorSets[readSet], 0, 0);

				if (j == iterations - 1) {
					calculateNormals = 1;
					vkCmdPushConstants(compute.commandBuffers[i], compute.pipelineLayout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(uint32_t), &calculateNormals);
				}

				vkCmdDispatch(compute.commandBuffers[i], cloth.gridsize.x / 10, cloth.gridsize.y / 10, 1);

				//不要在循環的最後一次迭代中添加障礙,因爲我們將顯式地釋放圖形隊列
				if (j != iterations - 1) {
					addComputeToComputeBarriers(compute.commandBuffers[i]);
				}

			}

			//將存儲緩衝區釋放回圖形隊列
			addComputeToGraphicsBarriers(compute.commandBuffers[i]);
			vkEndCommandBuffer(compute.commandBuffers[i]);
		}
	}

2.6 計算着色器處理彈簧系統

接下來我們主要來看在2.5中使用的計算着色器cloth.comp:

#version 450
struct Particle {
	vec4 pos;
	vec4 vel;
	vec4 uv;
	vec4 normal;
	float pinned;
};

layout(std430, binding = 0) buffer ParticleIn {
	Particle particleIn[ ];
};

layout(std430, binding = 1) buffer ParticleOut {
	Particle particleOut[ ];
};

layout (local_size_x = 10, local_size_y = 10) in;

layout (binding = 2) uniform UBO 
{
	float deltaT;                //時間步長
	float particleMass;          //粒子大小 
	float springStiffness;		 //彈簧剛度
	float damping;				 //衰減
	float restDistH;			 //水平彈簧固定長度
	float restDistV;			 //豎直彈簧固定長度
	float restDistD;			 //斜向彈簧固定長度
	float sphereRadius;			 //球體半徑
	vec4 spherePos;				 //球體位置
	vec4 gravity;				 //重力
	ivec2 particleCount;		 //粒子數
} params;

layout (push_constant) uniform PushConsts {
	uint calculateNormals;
} pushConsts;

//求出p1點對p0點的作用力
vec3 springForce(vec3 p0, vec3 p1, float restDist) 
{
	vec3 dist = p0 - p1;
	return normalize(dist) * params.springStiffness * (length(dist) - restDist);
}

void main() 
{
	uvec3 id = gl_GlobalInvocationID; 

	uint index = id.y * params.particleCount.x + id.x;
	if (index > params.particleCount.x * params.particleCount.y) 
		return;

	// 固定
	if (particleIn[index].pinned == 1.0) {
		particleOut[index].pos = particleOut[index].pos;
		particleOut[index].vel = vec4(0.0);
		return;
	}

	// 初始化重力
	vec3 force = params.gravity.xyz * params.particleMass;

	vec3 pos = particleIn[index].pos.xyz;
	vec3 vel = particleIn[index].vel.xyz;

	// 來自鄰近粒子的彈簧力
	// 左
	if (id.x > 0) {
		force += springForce(particleIn[index-1].pos.xyz, pos, params.restDistH);
	} 
	// 右
	if (id.x < params.particleCount.x - 1) {
		force += springForce(particleIn[index + 1].pos.xyz, pos, params.restDistH);
	}
	// 上
	if (id.y < params.particleCount.y - 1) {
		force += springForce(particleIn[index + params.particleCount.x].pos.xyz, pos, params.restDistV);
	} 
	// 下
	if (id.y > 0) {
		force += springForce(particleIn[index - params.particleCount.x].pos.xyz, pos, params.restDistV);
	} 
	// 左
	if ((id.x > 0) && (id.y < params.particleCount.y - 1)) {
		force += springForce(particleIn[index + params.particleCount.x - 1].pos.xyz, pos, params.restDistD);
	}
	// 左下
	if ((id.x > 0) && (id.y > 0)) {
		force += springForce(particleIn[index - params.particleCount.x - 1].pos.xyz, pos, params.restDistD);
	}
	// 右上
	if ((id.x < params.particleCount.x - 1) && (id.y < params.particleCount.y - 1)) {
		force += springForce(particleIn[index + params.particleCount.x + 1].pos.xyz, pos, params.restDistD);
	}
	// 右下
	if ((id.x < params.particleCount.x - 1) && (id.y > 0)) {
		force += springForce(particleIn[index - params.particleCount.x + 1].pos.xyz, pos, params.restDistD);
	}

	force += (-params.damping * vel);

	// 整合
	vec3 f = force * (1.0 / params.particleMass);
	particleOut[index].pos = vec4(pos + vel * params.deltaT + 0.5 * f * params.deltaT * params.deltaT, 1.0);
	particleOut[index].vel = vec4(vel + f * params.deltaT, 0.0);

	// 球體碰撞(適用於場景二)
	vec3 sphereDist = particleOut[index].pos.xyz - params.spherePos.xyz;
	if (length(sphereDist) < params.sphereRadius + 0.01) {
		//如果粒子在球體內部,將其推到外部半徑
		particleOut[index].pos.xyz = params.spherePos.xyz + normalize(sphereDist) * (params.sphereRadius + 0.01);		
		// 取消速度
		particleOut[index].vel = vec4(0.0);
	}

	// 法線處理
	if (pushConsts.calculateNormals == 1) {
		vec3 normal = vec3(0.0);
		vec3 a, b, c;
		if (id.y > 0) {
			if (id.x > 0) {
				a = particleIn[index - 1].pos.xyz - pos;
				b = particleIn[index - params.particleCount.x - 1].pos.xyz - pos;
				c = particleIn[index - params.particleCount.x].pos.xyz - pos;
				normal += cross(a,b) + cross(b,c);
			}
			if (id.x < params.particleCount.x - 1) {
				a = particleIn[index - params.particleCount.x].pos.xyz - pos;
				b = particleIn[index - params.particleCount.x + 1].pos.xyz - pos;
				c = particleIn[index + 1].pos.xyz - pos;
				normal += cross(a,b) + cross(b,c);
			}
		}
		if (id.y < params.particleCount.y - 1) {
			if (id.x > 0) {
				a = particleIn[index + params.particleCount.x].pos.xyz - pos;
				b = particleIn[index + params.particleCount.x - 1].pos.xyz - pos;
				c = particleIn[index - 1].pos.xyz - pos;
				normal += cross(a,b) + cross(b,c);
			}
			if (id.x < params.particleCount.x - 1) {
				a = particleIn[index + 1].pos.xyz - pos;
				b = particleIn[index + params.particleCount.x + 1].pos.xyz - pos;
				c = particleIn[index + params.particleCount.x].pos.xyz - pos;
				normal += cross(a,b) + cross(b,c);
			}
		}
		particleOut[index].normal = vec4(normalize(normal), 0.0f);
	}
}

我們可以看到,根據第一部分的單個質點彈簧作用力,我們對一個質點系統進行了其所有臨近作用點的計算處理,最後重新處理法線以供採樣,其中我們還處理了一個圓球碰撞(在純布料模型的時候,我們將圓球原理布料,上述代碼中也可找出對應處理)。

2.7 渲染場景

在計算着色器實時計算布料仿真的同時,我們需要執行最後的繪製渲染,在其中我們需要使用更新頂點緩衝區繪製布料網格數據。

	void buildCommandBuffers()
	{
		VkCommandBufferBeginInfo cmdBufInfo = vks::initializers::commandBufferBeginInfo();

		VkClearValue clearValues[2];
		clearValues[0].color = { { 0.1f, 0.1f, 0.3f, 0.5f } };;
		clearValues[1].depthStencil = { 1.0f, 0 };

		VkRenderPassBeginInfo renderPassBeginInfo = vks::initializers::renderPassBeginInfo();
		renderPassBeginInfo.renderPass = renderPass;
		renderPassBeginInfo.renderArea.offset.x = 0;
		renderPassBeginInfo.renderArea.offset.y = 0;
		renderPassBeginInfo.renderArea.extent.width = width;
		renderPassBeginInfo.renderArea.extent.height = height;
		renderPassBeginInfo.clearValueCount = 2;
		renderPassBeginInfo.pClearValues = clearValues;

		for (int32_t i = 0; i < drawCmdBuffers.size(); ++i)
		{
			// 設定目標幀緩衝器
			renderPassBeginInfo.framebuffer = frameBuffers[i];

			VK_CHECK_RESULT(vkBeginCommandBuffer(drawCmdBuffers[i], &cmdBufInfo));

			// 從計算隊列中獲取存儲緩衝區
			addComputeToGraphicsBarriers(drawCmdBuffers[i]);

			// 使用更新頂點緩衝區繪製粒子系統
			vkCmdBeginRenderPass(drawCmdBuffers[i], &renderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);

			VkViewport viewport = vks::initializers::viewport((float)width, (float)height, 0.0f, 1.0f);
			vkCmdSetViewport(drawCmdBuffers[i], 0, 1, &viewport);

			VkRect2D scissor = vks::initializers::rect2D(width, height, 0, 0);
			vkCmdSetScissor(drawCmdBuffers[i], 0, 1, &scissor);

			VkDeviceSize offsets[1] = { 0 };

			// 渲染球體
			if (sceneSetup == 0) {
				vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipelines.sphere);
				vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipelineLayout, 0, 1, &graphics.descriptorSet, 0, NULL);
				vkCmdBindIndexBuffer(drawCmdBuffers[i], modelSphere.indices.buffer, 0, VK_INDEX_TYPE_UINT32);
				vkCmdBindVertexBuffers(drawCmdBuffers[i], 0, 1, &modelSphere.vertices.buffer, offsets);
				vkCmdDrawIndexed(drawCmdBuffers[i], modelSphere.indexCount, 1, 0, 0, 0);
			}

			// 渲染布料
			vkCmdBindPipeline(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipelines.cloth);
			vkCmdBindDescriptorSets(drawCmdBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, graphics.pipelineLayout, 0, 1, &graphics.descriptorSet, 0, NULL);
			vkCmdBindIndexBuffer(drawCmdBuffers[i], graphics.indices.buffer, 0, VK_INDEX_TYPE_UINT32);
			vkCmdBindVertexBuffers(drawCmdBuffers[i], 0, 1, &compute.storageBuffers.output.buffer, offsets);
			vkCmdDrawIndexed(drawCmdBuffers[i], indexCount, 1, 0, 0, 0);

			drawUI(drawCmdBuffers[i]);

			vkCmdEndRenderPass(drawCmdBuffers[i]);

			// 將存儲緩衝區釋放到計算隊列
			addGraphicsToComputeBarriers(drawCmdBuffers[i]);

			VK_CHECK_RESULT(vkEndCommandBuffer(drawCmdBuffers[i]));
		}

	}

運行程序,可見下圖效果(僅重力作用):
在這裏插入圖片描述
或者開啓風力模擬:
在這裏插入圖片描述
你也可以切換成球體碰撞(sceneSetup = 0),運行,如下:
在這裏插入圖片描述

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