B-6 meshshader实现,利用vulkan和obj模型

B-6 2024/9/26

MeshShader实现 Vulkan+obj模型

  • 经过多次尝试终于实现了meshshader
  • 环境:vulkan、fastobj(阅读obj模型)、meshoptimizer(网格分组)
  • 放两张截图吧。龙有261w个顶点,87w个三角形。

在这里插入图片描述

meshshader流程说明

  1. 获取模型的网格数据,也就是顶点和索引。这和传统管线一样。

  2. 进行网格分组,把这么一大坨顶点和索引表示的网格分成一片片的meshlet。这个meshlet的划分可以借助第三方库meshoptimizer:https://github.com/zeux/meshoptimizer。

  3. meshlet是什么样的数据结构呢?在传统管线中,我们要传输的是顶点和索引。在meshshader中我们依旧要传输顶点的数组。但是原本的索引数组就被分成了meshlet。meshlet_vertices指的是,顶点分组索引。meshlet_triangles是meshlet中三角形的索引。

  4. 比如说原本有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是从这开始到这结束。

  5. 利用vulkan将meshlet的相关数据放入shader中进行并行的渲染。

  6. 关于并行:我的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);
      
  7. 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 w
  vec4 color; // w is intensity
};
layout(set = 0, binding = 0) uniform GlobalUbo {
  mat4 projection;
  mat4 view;
  mat4 invView;
  vec4 ambientLightColor; // w is intensity
  PointLight 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;
	//获取工作组下的当前的进程的id
	uint groupThreadIndex = gl_LocalInvocationID.x;
	
	
	//每个工作组处理一个meshlet
	Meshlet 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;
  	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值