UE5 简单 Mesh Shader 制作流程

发布时间:2026/7/5 13:36:09
UE5 简单 Mesh Shader 制作流程 XSJHelloMeshShaderPass.h#pragma once #include RenderGraphResources.h class FRDGBuilder; class FViewInfo; void AddXSJHelloMeshShaderPass( FRDGBuilder GraphBuilder, const FViewInfo View, FRDGTextureRef SceneColorTexture);这里按照之前的文章没什么好讲的只是声明函数XSJHelloMeshShaderPass.cpp#include XSJHelloMeshShaderPass.h #include DataDrivenShaderPlatformInfo.h #include GlobalShader.h #include PipelineStateCache.h #include RenderGraphBuilder.h #include RenderGraphUtils.h #include RHIStaticStates.h #include SceneRendering.h #include ShaderParameterUtils.h class FXSJHelloMeshShaderMS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FXSJHelloMeshShaderMS); SHADER_USE_PARAMETER_STRUCT(FXSJHelloMeshShaderMS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER(FVector4f, DebugColor) END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation( const FGlobalShaderPermutationParameters Parameters) { return RHISupportsMeshShadersTier0(Parameters.Platform); } }; IMPLEMENT_GLOBAL_SHADER( FXSJHelloMeshShaderMS, /Engine/Private/XSJ/XSJHelloMeshShader.usf, MainMS, SF_Mesh); class FXSJHelloMeshShaderPS : public FGlobalShader { DECLARE_GLOBAL_SHADER(FXSJHelloMeshShaderPS); SHADER_USE_PARAMETER_STRUCT(FXSJHelloMeshShaderPS, FGlobalShader); BEGIN_SHADER_PARAMETER_STRUCT(FParameters, ) SHADER_PARAMETER(FVector4f, DebugColor) RENDER_TARGET_BINDING_SLOTS() END_SHADER_PARAMETER_STRUCT() public: static bool ShouldCompilePermutation( const FGlobalShaderPermutationParameters Parameters) { return RHISupportsMeshShadersTier0(Parameters.Platform); } }; IMPLEMENT_GLOBAL_SHADER( FXSJHelloMeshShaderPS, /Engine/Private/XSJ/XSJHelloMeshShader.usf, MainPS, SF_Pixel); BEGIN_SHADER_PARAMETER_STRUCT(FXSJHelloMeshShaderPassParameters, ) SHADER_PARAMETER_STRUCT_INCLUDE( FXSJHelloMeshShaderMS::FParameters, MS) SHADER_PARAMETER_STRUCT_INCLUDE( FXSJHelloMeshShaderPS::FParameters, PS) END_SHADER_PARAMETER_STRUCT() void AddXSJHelloMeshShaderPass( FRDGBuilder GraphBuilder, const FViewInfo View, FRDGTextureRef SceneColorTexture) { if (!GRHISupportsMeshShadersTier0) { return; } if (!SceneColorTexture) { return; } FXSJHelloMeshShaderPassParameters* PassParameters GraphBuilder.AllocParameters FXSJHelloMeshShaderPassParameters(); PassParameters-MS.DebugColor FVector4f(1.0f, 0.1f, 0.0f, 1.0f); PassParameters-PS.DebugColor FVector4f(1.0f, 1.0f, 1.0f, 0.85f); PassParameters-PS.RenderTargets[0] FRenderTargetBinding( SceneColorTexture, ERenderTargetLoadAction::ELoad); TShaderMapRefFXSJHelloMeshShaderMS MeshShader( View.ShaderMap); TShaderMapRefFXSJHelloMeshShaderPS PixelShader( View.ShaderMap); const FIntRect ViewRect View.ViewRect; GraphBuilder.AddPass( RDG_EVENT_NAME(XSJ.HelloMeshShader), PassParameters, ERDGPassFlags::Raster, [ PassParameters, MeshShader, PixelShader, ViewRect ](FRDGAsyncTask, FRHICommandList RHICmdList) { FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit); GraphicsPSOInit.BlendState TStaticBlendState CW_RGBA, BO_Add, BF_SourceAlpha, BF_InverseSourceAlpha, BO_Add, BF_One, BF_InverseSourceAlpha::GetRHI(); GraphicsPSOInit.RasterizerState TStaticRasterizerState FM_Solid, CM_None::GetRHI(); GraphicsPSOInit.DepthStencilState TStaticDepthStencilState false, CF_Always::GetRHI(); GraphicsPSOInit.BoundShaderState.SetMeshShader( MeshShader.GetMeshShader()); GraphicsPSOInit.BoundShaderState.PixelShaderRHI PixelShader.GetPixelShader(); GraphicsPSOInit.PrimitiveType PT_TriangleList; SetGraphicsPipelineState( RHICmdList, GraphicsPSOInit, 0); SetShaderParameters( RHICmdList, MeshShader, MeshShader.GetMeshShader(), PassParameters-MS); SetShaderParameters( RHICmdList, PixelShader, PixelShader.GetPixelShader(), PassParameters-PS); RHICmdList.SetViewport( ViewRect.Min.X, ViewRect.Min.Y, 0.0f, ViewRect.Max.X, ViewRect.Max.Y, 1.0f); RHICmdList.DispatchMeshShader(1, 1, 1); }); }FXSJHelloMeshShaderMS这个PS只是定义了一个颜色并没有其他信息IMPLEMENT_GLOBAL_SHADER(FMyVS, ..., MainVS, SF_Vertex); IMPLEMENT_GLOBAL_SHADER(FMyPS, ..., MainPS, SF_Pixel); IMPLEMENT_GLOBAL_SHADER(FMyCS, ..., MainCS, SF_Compute); IMPLEMENT_GLOBAL_SHADER(FMyMS, ..., MainMS, SF_Mesh);它们 C 都可以继承public FGlobalShader这里IMPLEMENT_GLOBAL_SHADER(XXX, SF_Meh)就说明它是MeshShader了这三个东西MainMS, SF_Mesh MainPS, SF_Pixel同一个.usf文件只是源码容器UE 会把它编译成两个完全不同的 shader bytecode。你现在是这样IMPLEMENT_GLOBAL_SHADER( FXSJHelloMeshShaderMS, /Engine/Private/XSJ/XSJHelloMeshShader.usf, MainMS, SF_Mesh);这表示从 XSJHelloMeshShader.usf 里找 MainMS() 把它编译成 Mesh Shader然后这个IMPLEMENT_GLOBAL_SHADER( FXSJHelloMeshShaderPS, /Engine/Private/XSJ/XSJHelloMeshShader.usf, MainPS, SF_Pixel);表示从同一个 usf 里找 MainPS() 把它编译成 Pixel Shader数量 通常一进一出 可决定输出多少顶点 三角形数量 由索引和 Draw Call 决定 可决定输出多少三角形 连接关系 由 Index Buffer 决定 Shader 直接写出三角形索引 几何剔除 难以整体剔除 可直接输出 0 个图元 执行模型 各顶点相对独立 线程组协作、共享数据 管线角色 只负责顶点变换 替代 VS 和传统图元组装等阶段Meshlet 是把一个大网格Mesh预先切分得到的“小型三角形簇”。一个完整 Mesh ├─ Meshlet 0几十个顶点、几十个三角形 ├─ Meshlet 1几十个顶点、几十个三角形 ├─ Meshlet 2几十个顶点、几十个三角形 └─ ...每个 Meshlet 通常包含一小组顶点一组局部三角形索引包围球或包围盒用于背面剔除的法线锥等辅助数据BEGIN_SHADER_PARAMETER_STRUCT(FXSJHelloMeshShaderPassParameters, ) SHADER_PARAMETER_STRUCT_INCLUDE( FXSJHelloMeshShaderMS::FParameters, MS) SHADER_PARAMETER_STRUCT_INCLUDE( FXSJHelloMeshShaderPS::FParameters, PS) END_SHADER_PARAMETER_STRUCT()等价概念大致是struct FXSJHelloMeshShaderPassParameters { FXSJHelloMeshShaderMS::FParameters MS; FXSJHelloMeshShaderPS::FParameters PS; };因此可以这样赋值auto* PassParameters GraphBuilder.AllocParametersFXSJHelloMeshShaderPassParameters(); PassParameters-MS.SomeBuffer MeshBuffer; PassParameters-MS.SomeValue 123; PassParameters-PS.SomeTexture Texture; PassParameters-PS.SomeSampler Sampler;if (!GRHISupportsMeshShadersTier0) { return; }拆开看GRHISupportsMeshShadersTier0全局布尔变量表示当前运行环境是否支持基础 Mesh Shader 功能。创建图形管线状态FGraphicsPipelineStateInitializer GraphicsPSOInit; RHICmdList.ApplyCachedRenderTargets(GraphicsPSOInit);GraphicsPSOInit描述这次绘制使用的完整图形管线状态。ApplyCachedRenderTargets()把 RDG 已经绑定好的 Render Target 格式等信息写入 PSO。这里对应之前设置的PassParameters-PS.RenderTargets[0] ...ExecutePassPrologue(RHICmdListPass, Pass); Pass-Execute(RHICmdListPass); // 这里才执行你的 Lambda ExecutePassEpilogue(RHICmdListPass, Pass);位置RenderGraphBuilder.cpp其中Pass-Execute()就是你传给GraphBuilder.AddPass()的 Lambda。1. RDG 在 Prologue 中启动 Render Pass因为你指定了ERDGPassFlags::RasterRDG 会在ExecutePassPrologue()中执行RHICmdList.BeginRenderPass( Pass-GetParameters().GetRenderPassInfo(), Pass-GetName());GraphicsPSOInit.BlendState TStaticBlendState CW_RGBA, BO_Add, BF_SourceAlpha, BF_InverseSourceAlpha, BO_Add, BF_One, BF_InverseSourceAlpha::GetRHI(); GraphicsPSOInit.RasterizerState TStaticRasterizerState FM_Solid, CM_None::GetRHI(); GraphicsPSOInit.DepthStencilState TStaticDepthStencilState false, CF_Always::GetRHI();这三段分别设置图形管线的颜色混合方式三角形光栅化方式深度/模板测试方式最终效果可以概括为以实心、双面、忽略深度的方式绘制并通过 Alpha 与原画面混合。参数含义CW_RGBA 写入 R、G、B、A 四个通道 BO_Add 颜色使用加法混合 BF_SourceAlpha 源颜色乘源 Alpha BF_InverseSourceAlpha 目标颜色乘 (1 - 源 Alpha) BO_Add Alpha 使用加法混合 BF_One 源 Alpha 乘 1 BF_InverseSourceAlpha 目标 Alpha 乘 (1 - 源 Alpha)颜色公式最终RGB Shader输出RGB × Shader输出Alpha 原RenderTarget RGB × (1 - Shader输出Alpha)Alpha 公式最终Alpha Shader输出Alpha 原RenderTarget Alpha × (1 - Shader输出Alpha)它通过RenderTargets[0]的绑定知道不是通过.usf猜出来的。关键代码是PassParameters-PS.RenderTargets[0] FRenderTargetBinding( SceneColorTexture, ERenderTargetLoadAction::ELoad);这里明确告诉 RDG渲染目标槽位 0 → SceneColorTexture这里过后执行GraphicsPSOInit.BlendState TStaticBlendState CW_RGBA, BO_Add, BF_SourceAlpha, BF_InverseSourceAlpha, BO_Add, BF_One, BF_InverseSourceAlpha::GetRHI();这里面的原RenderTargetRGB就是你绑定的RenderTarget的RGB这里RenderTarget绑定的是场景颜色图RasterizerState光栅化方式TStaticRasterizerState FM_Solid, CM_None ::GetRHI();FM_Solid实心填充三角形而不是只画线框。CM_None不剔除任何一面。因此无论三角形朝向相机还是背向相机都会进行光栅化。FM_Solid填满整个三角形 CM_None 正面、背面都绘制DepthStencilState深度状态TStaticDepthStencilState false, CF_Always ::GetRHI();false不写入深度缓冲。CF_Always深度比较永远通过。未指定的模板参数保持默认值模板测试关闭这个让我们画的三角形永远显示在屏幕前方它让三角形在“这个 Pass 执行时”不受场景深度遮挡因此看起来像叠加在当前画面最前面。TStaticDepthStencilState false, // 不写深度 CF_Always // 无论深度值是多少都通过 假设场景中已有一个物体的深度是0.2你的三角形深度是0.8正常深度测试会认为三角形在后面普通深度测试0.8 被 0.2 挡住 → 不显示 CF_Always 无条件通过 → 仍然显示GraphicsPSOInit.BoundShaderState.SetMeshShader( MeshShader.GetMeshShader()); GraphicsPSOInit.BoundShaderState.PixelShaderRHI PixelShader.GetPixelShader();这两段代码是在告诉图形管线这次绘制的 Mesh Shader 阶段和 Pixel Shader 阶段分别使用哪一份已编译 Shader。GraphicsPSOInit.PrimitiveType PT_TriangleList; SetGraphicsPipelineState( RHICmdList, GraphicsPSOInit, 0); SetShaderParameters( RHICmdList, MeshShader, MeshShader.GetMeshShader(), PassParameters-MS); SetShaderParameters( RHICmdList, PixelShader, PixelShader.GetPixelShader(), PassParameters-PS);GraphicsPSOInit.PrimitiveType PT_TriangleList;表示这条管线输出的是独立三角形三角形0顶点 0、1、2 三角形1顶点 3、4、5Mesh Shader 管线DispatchMeshShader() ↓ MS 自己读取/生成顶点 MS 自己输出三角形索引 ↓ 光栅化这一步在我们的XSJHelloMeshShader.usf里面有两行代码// 这次输出3个顶点、1个三角形 SetMeshOutputCounts(3, 1);它在自己将顶点梳理成三角形[outputtopology(triangle)]这是 Mesh Shader 入口函数的一个 HLSL 属性意思是这个 Mesh Shader 输出的图元类型是三角形。它告诉编译器和光栅化器MainMS输出的索引每一项都代表一个三角形查找并激活 PSOSetGraphicsPipelineState( RHICmdList, GraphicsPSOInit, 0);GraphicsPSOInit此时已经包含Render Target 格式 混合状态 光栅化状态 深度状态 Mesh Shader Pixel Shader 图元类型UE 会根据这些配置从 PSO 缓存中查找管线不存在时则创建然后设置到RHICmdList。我提前把PSO设置好了我在gpu里面拿着PSO的数据判断如何执行速度就很快如果没有PSO数据我GPU每次需要什么数据需要CPU再上传给你你GPU就需要等待浪费线程利用率等CPU把数据度过来又继续执行绑定 Mesh Shader 参数SetShaderParameters( RHICmdList, MeshShader, MeshShader.GetMeshShader(), PassParameters-MS);参数分别表示RHICmdList 向哪个命令列表设置 MeshShader UE Shader 对象及其参数元数据 MeshShader.GetMeshShader() 底层 RHI Mesh Shader PassParameters-MS 要绑定的实际参数值绑定 Pixel Shader 参数SetShaderParameters( RHICmdList, PixelShader, PixelShader.GetPixelShader(), PassParameters-PS);和上面类似PrimitiveType ↓ 设置并激活完整 PSO ↓ 绑定 MainMS 的参数 ↓ 绑定 MainPS 的参数 ↓ DispatchMeshShader() 真正开始绘制DispatchMeshShader(1, 1, 1);这里是表示有多少组工作组1 * 1 * 1 1个Numthreads[1, 1, 1]是表示工作组的线程数量因此每组线程数 1×1×1 1 工作组数量 1×1×1 1 总线程调用 1×1 1XSJHelloMeshShader.usf#include /Engine/Private/Common.ush float4 DebugColor; struct FXSJMeshVertex { float4 Position : SV_Position; float4 Color : COLOR0; }; [outputtopology(triangle)] [numthreads(1, 1, 1)] void MainMS( out vertices FXSJMeshVertex OutVertices[3], out indices uint3 OutTriangles[1]) { SetMeshOutputCounts(3, 1); OutVertices[0].Position float4(-0.55f, -0.45f, 0.5f, 1.0f); OutVertices[0].Color float4(1.0f, 0.0f, 0.0f, 1.0f) * DebugColor; OutVertices[1].Position float4(0.0f, 0.55f, 0.5f, 1.0f); OutVertices[1].Color float4(0.0f, 1.0f, 0.0f, 1.0f) * DebugColor; OutVertices[2].Position float4(0.55f, -0.45f, 0.5f, 1.0f); OutVertices[2].Color float4(0.0f, 0.2f, 1.0f, 1.0f) * DebugColor; OutTriangles[0] uint3(0, 1, 2); } float4 MainPS(FXSJMeshVertex Input) : SV_Target0 { return Input.Color * DebugColor; }Mesh Shader生成三角形[outputtopology(triangle)] [numthreads(1, 1, 1)] void MainMS(...)含义输出拓扑是三角形。每个工作组只有1×1×1个线程。C 中DispatchMeshShader(1,1,1)启动一个工作组因此这里只执行一次MainMS。SetMeshOutputCounts(3, 1);声明本工作组输出3 个顶点 1 个三角形三个顶点的位置(-0.55, -0.45, 0.5, 1) // 左下 ( 0.00, 0.55, 0.5, 1) // 上方 ( 0.55, -0.45, 0.5, 1) // 右下每个顶点设置了不同颜色顶点0红色 顶点1绿色 顶点2蓝色但随后会乘以 MS 的DebugColorOutVertices[i].Color 顶点颜色 * DebugColor;Pixel Shader输出颜色float4 MainPS(FXSJMeshVertex Input) : SV_Target0 { return Input.Color * DebugColor; }Input.Color光栅化器插值后的颜色。SV_Target0输出到RenderTargets[0]也就是SceneColorTexture。