B-6 2024/9/26
MeshShader实现 Vulkan+obj模型
- 经过多次尝试终于实现了meshshader
- 环境:vulkan、fastobj(阅读obj模型)、meshoptimizer(网格分组)
- 放两张截图吧。龙有261w个顶点,87w个三角形。
meshshader流程说明
-
获取模型的网格数据,也就是顶点和索引。这和传统管线一样。
-
进行网格分组,把这么一大坨顶点和索引表示的网格分成一片片的meshlet。这个meshlet的划分可以借助第三方库meshoptimizer:https://github.com/zeux/meshoptimizer。
-
meshlet是什么样的数据结构呢?在传统管线中,我们要传输的是顶点和索引。在meshshader中我们依旧要传输顶点的数组。但是原本的索引数组就被分成了meshlet。meshlet_vertices指的是,顶点分组索引。meshlet_triangles是meshlet中三角形的索引。
-
比如说原本有150个顶点,300个三角形,我们规定一个meshlet里面最多64个点和126个三角形。于是乎,150个点就被分成了三组。如果meshlet_vertices的前64位是0~63,meshlet[0].meshlet_triangles里triangle_offset到triangle_offset+3*triangle_count的数字最大不超过63。如果meshlet_vertices的64位到128位是64 - 128.meshlet[1].meshlet_triangles里triangle_offset到triangle_offset*3也不超过63。当然meshlet和meshlet之间可能出现顶点公用的情况。相当于meshlet_vertices记录的是某个meshlet用到的顶点指向顶点数组的索引。而meshlet不知道,它只看见了meshlet_vertices,它觉得自己只有这么多顶点,也就是不超过64个顶点,所以索引不会大于64。因此可以用uint8来表示meshlet_triangles。然后把数组合并起来也就有了meshlet的结构,需要指定两个数组的起始位置和大小。meshlet中的offest和count相当于就是告诉读者,meshlet是从这开始到这结束。
-
利用vulkan将meshlet的相关数据放入shader中进行并行的渲染。
-
关于并行:我的2080ti在x,y,z方向上各有1024*1024*64个核心。这个通过是vulkan的物理设备特性队列获得的。一般来说我们设置32个核心为一个工作组,一个工作组处理一个meshlet。也就是我们可以设置1024*1024*64/32个工作组。shader中有两个内置变量gl_WorkGroupID、gl_LocalInvocationID。前一个指工作组的索引,也就是0~1024*1024*64/32。后面一个gl_LocalInvocationID,是工作组中核心的索引也就是0到31。meshshader有两个扩展,一个是vulkan的ext扩展,一个是英伟达的nv扩展。利用英伟达的扩展进行drawcall的时候,无需在外面分配工作组的数量。但是ext需要自己配置x,y,z。英伟达的扩展meshlet的数量不能超过1024*64个,要是超过了,那就分多次提交。
-
PFN_vkCmdDrawMeshTasksNV vkCmdDrawMeshTasksNV = (PFN_vkCmdDrawMeshTasksNV)vkGetDeviceProcAddr(pcbDevice.device(), "vkCmdDrawMeshTasksNV");//我用英伟达的扩展,直接提交meshlet的数量就能自动分配vkCmdDrawMeshTasksNV(commandBuffer, meshlets.meshletssize(), 0);
-
-
shader中代码如下:
#version 450//启用meshshader
#extension GL_NV_mesh_shader: require
//启用uint8_t和int8_t
#extension GL_EXT_shader_8bit_storage: require
#extension GL_EXT_shader_16bit_storage: require//设置工作组中线程的大小,一个工作组有32个线程
//将max_vertices的数量依次放入这些工作组中,要是有64个vertices,那就需要循环两次去执行
//有些meshlet中的vertices数量没有到64.那么最后几个点就重复执行一下
const uint myShaderGroupSize = 32;//计算在myShaderGroupSize大小的一个工作组中,处理64个顶点需要几次
//这里需要两次,因为顶点数量最大是64个,工作组大小是32个,所以需要两次
const uint VertexLoops = (64 + myShaderGroupSize - 1) / myShaderGroupSize;//计算需要循环几次才能把索引都写入
//124个三角形也就是说有3*124个索引
//4 * myShaderGroupSize意思是一次可以写入四个索引数字
const uint TriangleLoops = (3 * 124 + 4 * myShaderGroupSize - 1) / (myShaderGroupSize * 4);//设置工作组中线程的大小,一个工作组有32个线程
layout(local_size_x = myShaderGroupSize) in;//输入的UBO矩阵
struct PointLight {vec4 position; // ignore wvec4 color; // w is intensity
};
layout(set = 0, binding = 0) uniform GlobalUbo {mat4 projection;mat4 view;mat4 invView;vec4 ambientLightColor; // w is intensityPointLight pointLights[10];int numLights;
} ubo;struct Meshlet {uint vertex_offset;uint triangle_offset;uint vertex_count;uint triangle_count;};
//读取从CPU那里发送过来的数据
layout(set=1,binding = 0) readonly buffer Meshlets { Meshlet meshlets[]; };
layout(set=1,binding = 1) readonly buffer MeshletVertices { uint meshlet_vertices[]; };
layout(set=1,binding = 2) readonly buffer MeshletTriangles { uint meshlet_triangles[]; };struct Vertex {vec4 position;vec4 noraml;
};
layout(set=1,binding = 3) readonly buffer _Vertexes { Vertex vertexes[]; };layout(push_constant) uniform Push {mat4 modelMatrix;mat4 normalMatrix;
} push;//规定输出的图元是三角形,当然也可以是线段什么的
//规定顶点最大64个,三角形最多124个,这个是在CPU上会提前预设好的限制,目的是为了匹配GPU的限制
//就是一个meshlet中最多只有64个顶点和124个三角形。
layout(triangles, max_vertices = 64, max_primitives = 124) out;
layout(location = 0) out vec3 fragColor[];//不同的网格颜色用以达到不同网格分组的效果
vec3 meshletcolors[10] = {vec3(1,0,0), vec3(0,1,0),vec3(0,0,1),vec3(1,1,0),vec3(1,0,1),vec3(0,1,1),vec3(1,0.5,0),vec3(0.5,1,0),vec3(0,0.5,1),vec3(1,1,1)};void main() {//获取当前工作组的ID//工作组的大小在vulkan中设置,vkCmdDispatch就可以设置大小//由于Group和thread都是三维的,这里只用一维的x就可以了,也只设置x就可以啦uint groupIndex = gl_WorkGroupID.x;//获取工作组下的当前的进程的iduint groupThreadIndex = gl_LocalInvocationID.x;//每个工作组处理一个meshletMeshlet meshlet= meshlets[groupIndex]; for (uint i = 0; i < VertexLoops; ++i){//获取当前计算64个顶点中的那个点的索引uint localVertexIndex = groupThreadIndex + i * myShaderGroupSize;//要是顶点数量没有到64个,比如只有40个,那么也需要两次循环才能处理完//当只有40个顶点时,第二次循环运行到后面的时候,顶点索引会溢出,那么就需要对其进行处理,规定最大的索引不超过这个meshlet的最大顶点数localVertexIndex = min(localVertexIndex, meshlet.vertex_count - 1);//获取顶点数组的索引位置uint vertexIndex=meshlet_vertices[meshlet.vertex_offset+localVertexIndex];//在顶点数组中获取顶点Vertex thisvertex = vertexes[vertexIndex];//输出顶点位置vec4 positionWorld = push.modelMatrix * thisvertex.position;gl_MeshVerticesNV[localVertexIndex].gl_Position = ubo.projection * ubo.view * positionWorld;//获取一个颜色,并进行输出fragColor[localVertexIndex]= meshletcolors[groupIndex%10];}//要除个四,因为索引写入的逻辑是这样的:小索引的长度是8bit,而一个整型有32bit。因此把四个数字看成一个数字进行写入//按照这样的方法,原本的索引数组的offset要缩小四倍。//因为四个数字被一起写入了嘛,相当于原本比如长度9的8字节索引,分为:4 4 1,只需要三次写入。第0次,第1次,第2次uint packedTriangleOffset = meshlet.triangle_offset / 4;//这个是对索引的大小进行一个限制,以防一循环和上面的顶点一样溢出了uint packedTrianglesMax = (3 * meshlet.triangle_count - 1) / 4;for (uint i = 0; i < TriangleLoops; ++i){//根据工作组和工作组中的线程获取当前需要写入的索引uint localTriangleIndex = groupThreadIndex + i * myShaderGroupSize;//需要写入的索引不能超过这个localTriangleIndex = min(localTriangleIndex, packedTrianglesMax);//这个函数就是写入索引的//前一个参数是,从4 * localTriangleIndex的位置开始写入//比如比如长度9的8字节索引,分为:4 4 1,三次写入。第0次从0位置写入,第1次从4位置写入,第2次从8位置写入//注意的是,CPU里面写入的是uint8类型索引数组,而这里直接读取的是uint类型的数组。实际上这里直接把四个数组一起读取了。writePackedPrimitiveIndices4x8NV(4 * localTriangleIndex, meshlet_triangles[packedTriangleOffset + localTriangleIndex]);}//设置一下三角形的个数//这个很重要,因为读取索引的时候,因为压缩的问题,最后一次读取很可能会多读几位。所以要说明一下三角形的数量if (groupThreadIndex == 0) {gl_PrimitiveCountNV = meshlet.triangle_count;}}