布料仿真的常用方法就是將布料表達爲三維網格,然後通過彈力+外力進行布料仿真。它在遊戲中有着廣泛的應用,如衣袖的擺動,旗幟的飄動;此外,也可以用於模擬試衣等。
(僅重力與風力作用下布料圖片)
一、彈簧質點系統(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, ©Region);
vkCmdCopyBuffer(copyCmd, stagingBuffer.buffer, compute.storageBuffers.output.buffer, 1, ©Region);
//爲圖形隊列添加一個初始釋放屏障,以便當計算命令緩衝區第一次執行時它不會缺少與它的“獲得”相對應的“釋放”
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, ©Region);
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),運行,如下: