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

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());
cpp 复制代码
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

这里过后执行

cpp 复制代码
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:   无条件通过        → 仍然显示
cpp 复制代码
GraphicsPSOInit.BoundShaderState.SetMeshShader(
				MeshShader.GetMeshShader());

GraphicsPSOInit.BoundShaderState.PixelShaderRHI =
				PixelShader.GetPixelShader();

这两段代码是在告诉图形管线:

这次绘制的 Mesh Shader 阶段和 Pixel Shader 阶段分别使用哪一份已编译 Shader。

cpp 复制代码
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里面有两行代码:

cpp 复制代码
// 这次输出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);

和上面类似

cpp 复制代码
PrimitiveType
      ↓
设置并激活完整 PSO
      ↓
绑定 MainMS 的参数
      ↓
绑定 MainPS 的参数
      ↓
DispatchMeshShader() 真正开始绘制
复制代码
DispatchMeshShader(1, 1, 1);

这里是表示有多少组工作组:1 * 1 * 1 = 1个

Numthreads1, 1, 1是表示工作组的线程数量

因此:

复制代码
每组线程数 = 1×1×1 = 1
工作组数量 = 1×1×1 = 1
总线程调用 = 1×1 = 1

XSJHelloMeshShader.usf

cpp 复制代码
#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
相关推荐
梦帮科技20 小时前
UE5 GAS 实战:用 Gameplay Ability System 搭建「赛博修真」境界与技能体系
c++·人工智能·python·ue5·c#
远离UE41 天前
UE5 各类型灯光学习
学习·ue5
四代水门20 天前
UE5实现客户端与服务器时间同步
ue5
智海深蓝22 天前
海上平行战场:态势模拟三维可视化平台
3d·ue5
_守一23 天前
UE5 ListView记录
ue5
成都渲染101云渲染666623 天前
CR15新功能介绍以及CR15云渲染流程
ue5·图形渲染·blender·maya·corona
一锅炖出任易仙24 天前
创梦汤锅学习日记day30
学习·ai·ue5·游戏引擎
OSwich24 天前
【UE5学习笔记】UMG中控件命名规范
笔记·学习·ue5
一锅炖出任易仙25 天前
创梦汤锅学习日记day29
学习·ai·ue5·游戏引擎