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); TShaderMapRef<FXSJHelloMeshShaderMS> MeshShader( View.ShaderMap); TShaderMapRef<FXSJHelloMeshShaderPS> 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.AllocParameters<FXSJHelloMeshShaderPassParameters>(); 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输出的索引每一项都代表一个三角形:
查找并激活 PSO
SetGraphicsPipelineState( 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 的DebugColor:
OutVertices[i].Color = 顶点颜色 * DebugColor;Pixel Shader:输出颜色
float4 MainPS(FXSJMeshVertex Input) : SV_Target0 { return Input.Color * DebugColor; }Input.Color:光栅化器插值后的颜色。SV_Target0:输出到RenderTargets[0],也就是SceneColorTexture。
