布料仿真的常用方法就是将布料表达为三维网格,然后通过弹力+外力进行布料仿真。它在游戏中有着广泛的应用,如衣袖的摆动,旗帜的飘动;此外,也可以用于模拟试衣等。
(仅重力与风力作用下布料图片)
一、弹簧质点系统(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),运行,如下: