当前位置: 首页 > news >正文

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::Raster

RDG 会在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();

这三段分别设置图形管线的:

  1. 颜色混合方式
  2. 三角形光栅化方式
  3. 深度/模板测试方式

最终效果可以概括为:

以实心、双面、忽略深度的方式绘制,并通过 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、5

Mesh 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 = 1

XSJHelloMeshShader.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
http://www.jsqmd.com/news/1128526/

相关文章:

  • 5个实战技巧:深度掌握N_m3u8DL-RE的高级应用
  • 【嵌入式C语言】06.数组和指针的关系
  • 下服务器端开发流程及相关工具介绍(C++)
  • AI简历工具怎么选?2026年7款主流产品横评:鹅来面/AI简历姬/职徒/知页/Zety/Teal深度对比
  • Redis分布式锁进阶第三十八篇
  • Unlock Music:3分钟本地解密QQ音乐、网易云音乐的完整指南
  • 基于WSEN-ISDS和TM4C129的三轴运动追踪系统设计
  • Obsidian 同步到底怎么选?2026 年主流方案实测对比
  • 2026年指纹浏览器封号率实测对比,哪款防封能力真正经得起考验?
  • CTMS 执行传输报错:Exception during start of deployment for deploy type ‘SLP_CTS‘
  • 光刻胶 配套化学品|纯技术专家线晋升 CTO完整路径、薪资、晋升核心卡点
  • 软考:高级软件架构师学习笔记------了解软考
  • 来可云城际订票系统|车队订单驱动式车辆调度模块功能详解
  • C++图形化打字模拟,单字依次输入(极简可行)
  • YOLO11目标检测入门:猜拳识别实战指南
  • 【SpringBoot篇】SpringBoot WebFlux响应式大文件流式上传下载实战(Flux<DataBuffer>低内存原理、源码解析、落地方案)
  • 医用修护敷料选购指南:资质、成分与剂型的硬核拆解
  • 基于 Java Swing + MySQL C/S 即时通讯聊天系统完整开发记录
  • TensorRT量化模型部署实战:从QAT到INT8推理的工程陷阱
  • 【Java项目-企悦抽】02-AI赋能产品需求规格说明书
  • 吃透SQL查询优化:真实线上案例+Explain深度解析
  • 企业级Java电商系统选型路线图:从零到上线全流程拆解
  • 小学期十八周
  • 第十八周小学期
  • 前端工程化-02:一个完整的vue工程结构模板
  • lsm6dsv16x
  • 开源商城源码下载后能商用吗?这3款Apache-2.0协议商城放心用
  • 卫星被云挡住后,AI还能知道洪水淹到哪里吗?
  • 15-DifusionMOT:一种基于扩散算法的多目标跟踪器
  • STM32与LV3296条形码模块的硬件协同与优化方案