前言及学习建议
本人最近在学习UnrealEngine的GlobalShader,在这个过程中阅读了@YivanLee的Shader系列文章,大大提高了学习速度。但这些代码大多基于4.19,其中部分代码将会被废弃,所以我撰写这篇在此分享4.22版本的GlobalShader相关经验。
本文意在理顺思路,教会读者如何搭建基础的Shader测试环境,顺便总结学习心得,所以本文不会将所有代码贴出,详细代码请参考github。
大部分内容解释还请参看了@YivanLee的文章,我认为重复造轮子没有意义。具体代码可以参考我的github,读者可以按照commit一步一步学习代码:
https://github.com/blueroseslol/BRPlugins
初始化
插件项目与模块设置
*.uplugin文件中把LoadingPhase改成:
"Modules": [
{
"Name": "Foo",
"Type": "Developer",
"LoadingPhase": "PostConfigInit"
}
]
修改插件的模块文件*.Build.cs,在PublicDependencyModuleNames.AddRange中添加RHI、Engine、RenderCore、CoreUObject。在PrivateDependencyModuleNames.AddRange中删除Slate、SlateCore、Engine、CoreUObject,添加”Projects”。
添加h与cpp文件
在插件目录中新建以下目录结构(部分文件在插件创建时就已创建):
Source
|——与插件名相同的文件夹
|——Classes
|——SimplePixelShader.h(该文件用于声明结构体与测试Shader的蓝图库)
|——Private
|——与插件名相同的模块cpp文件
|——SimplePixelShader.cpp(用于实现GlobalShader、蓝图库代码)
|——Public
|——与插件名相同的模块h文件
这里我创建了SimplePixelShader.cpp与SimplePixelShader.h文件用于之后的GlobalShader实现。在之后的内容中我也将通过这两个文件名进行说明。但读者在实践中可以使用不一样的文件名。
创建usf文件
在插件目录中新建以下目录结构:Shaders-Private。之后可以开始编写usf。
重新生成解决方案
在Unreal项目文件上右键点击“Generate Visual Studio File”,生成新的解决方案,并且编译项目。(刷新解决方案)
代码编写
设置虚拟路径
在插件的模块cpp文件(与插件同名的cpp文件)的StartupModule()中,添加虚拟路径:
FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("BRPlugins"))->GetBaseDir(), TEXT("Shaders"));
AddShaderSourceDirectoryMapping(TEXT("/Plugin/BRPlugins"), PluginShaderDir);
这里的BRPlugins是我所写的插件名。所写的代码需要与usf所在路径及Shader实现宏中的虚拟路径对应。PluginShaderDir变量为真实路径,AddShaderSourceDirectoryMapping如字面意思,设定一个虚拟路径代表真实路径。
最后在Shader实现宏中使用:
IMPLEMENT_SHADER_TYPE(, FSimplePixelShaderPS, TEXT("/Plugin/BRPlugins/Private/SimplePixelShader.usf"), TEXT("MainPS"), SF_Pixel)
声明并且向Ue4注册GlobalShader
继承FGlobalShader,实现所需函数:
class FSimplePixelShader : public FGlobalShader
{
public:
//确定Shader功能支持情况
static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM4);
}
//添加Usf中的宏
static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
OutEnvironment.SetDefine(TEXT("TEST_MICRO"), 1);
}
FSimplePixelShader(){}
//构造函数,用于绑定Shader中的变量
FSimplePixelShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FGlobalShader(Initializer)
{
SimpleColorVal.Bind(Initializer.ParameterMap, TEXT("SimpleColor"));
TextureVal.Bind(Initializer.ParameterMap, TEXT("TextureVal"));
TextureSampler.Bind(Initializer.ParameterMap, TEXT("TextureSampler"));
}
//自己定义的Shader变量设置函数,形参和函数名可以自己随意设置
template<typename TShaderRHIParamRef>
void SetParameters(FRHICommandListImmediate& RHICmdList,const TShaderRHIParamRef ShaderRHI, const FLinearColor &MyColor,const FTextureRHIParamRef& TextureRHI)
{
SetShaderValue(RHICmdList, ShaderRHI, SimpleColorVal, MyColor);
SetTextureParameter(RHICmdList, ShaderRHI, TextureVal, TextureSampler,TStaticSamplerState<SF_Trilinear,AM_Clamp,AM_Clamp,AM_Clamp>::GetRHI(),TextureRHI);
}
//序列化虚函数
virtual bool Serialize(FArchive& Ar) override
{
bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
Ar << SimpleColorVal<< TextureVal<< TextureSampler;
return bShaderHasOutdatedParameters;
}
private:
FShaderParameter SimpleColorVal;
FShaderResourceParameter TextureVal;
FShaderResourceParameter TextureSampler;
};
class FSimplePixelShaderVS : public FSimplePixelShader
{
//声明Shader宏
DECLARE_SHADER_TYPE(FSimplePixelShaderVS, Global);
public:
FSimplePixelShaderVS(){}
FSimplePixelShaderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FSimplePixelShader(Initializer)
{
}
};
class FSimplePixelShaderPS : public FSimplePixelShader
{
//声明Shader宏
DECLARE_SHADER_TYPE(FSimplePixelShaderPS, Global);
public:
FSimplePixelShaderPS(){}
FSimplePixelShaderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
: FSimplePixelShader(Initializer)
{
}
};
IMPLEMENT_SHADER_TYPE(, FSimplePixelShaderVS, TEXT("/Plugin/BRPlugins/Private/SimplePixelShader.usf"), TEXT("MainVS"), SF_Vertex)
IMPLEMENT_SHADER_TYPE(, FSimplePixelShaderPS, TEXT("/Plugin/BRPlugins/Private/SimplePixelShader.usf"), TEXT("MainPS"), SF_Pixel)
这里的代码只做示例,具体的请参考我的github。
如此一来就声明并向Ue4注册了Pixel与Vertex类型的GlobalShader。其实这里的PixelShader与VertexShader可以直接继承GlobalShader直接编写,不一定要像我这样写。
这里大家可以通过搜索SF_Pixel)或者SF_Vertex),通过寻找EPIC官方写的代码来进行进一步的学习。这样想要绑定什么类型的变量都可以在源代码中找到答案。
这里推荐:
- Engine\Source\Runtime\UtilityShaders\Public\OneColorShader.h
- Engine\Source\Editor\UnrealEd\Private\Texture2DPreview.cpp
- Engine\Plugins\Compositing\LensDistortion\Source\LensDistortion\Private\LensDistortionRendering.cpp
编写渲染线程的渲染函数
Ue4中的渲染函数基本都是带有_RenderThread后缀的,所以我们可以通过搜索有_RenderThread寻找对应的代码。
具体的代码请参考我的github,这里只说大致流程。其大致流程如下:
- 通过FRHIRenderPassInfo设置渲染层信息。
- 调用RHICmdList.BeginRenderPass函数开始渲染层。
- 取得相关变量。例如:各种ShaderMap、顶点格式。
- 使用上一步取得的变量,设置显卡管线状态。
- 设置视口与Shader变量。
- 使用Shader绘制。
- 调用RHICmdList.EndRenderPass函数结束渲染层。
大致步骤与4.19相同,较大的不同之处在于步骤1、2、6、7,FRHIRenderPassInfo据说与新的MeshDrawPipline有关,具体请参考:https://zhuanlan.zhihu.com/p/61464613
BeginRenderPass与EndRenderPass代替了原本的SetRenderTarget与CopyToResolveTarget函数。
另外因为YivanLee的文章中所使用的DrawPrimitive函数已被标记为会被废弃的函数,所以最后我使用DrawIndexedPrimitive函数进行绘制。
编写蓝图函数库函数
为了能够在蓝图中调用渲染函数,这里我们需要声明一个BlueprinntFunctionLibrary并编写一个函数。
这里的代码只做示例,具体的请参考我的github。
void USimplePixelShaderBlueprintLibrary::DrawTestShaderRenderTarget(const UObject* WorldContextObject, UTextureRenderTarget2D* OutputRenderTarget, FLinearColor MyColor, UTexture* MyTexture, FSimpleUniformStruct UniformStruct)
{
check(IsInGameThread());
if (!OutputRenderTarget)
{
return;
}
//取得各种所需变量
FTextureRenderTargetResource* TextureRenderTargetResource = OutputRenderTarget->GameThread_GetRenderTargetResource();
FTextureRHIParamRef TextureRHI = MyTexture->TextureReference.TextureReferenceRHI;
const UWorld* World = WorldContextObject->GetWorld();
ERHIFeatureLevel::Type FeatureLevel = World->Scene->GetFeatureLevel();
//往渲染队列中添加新的渲染任务
ENQUEUE_RENDER_COMMAND(CaptureCommand)(
[TextureRenderTargetResource, FeatureLevel, MyColor,TextureRHI, UniformStruct](FRHICommandListImmediate& RHICmdList)
{
DrawTestShaderRenderTarget_RenderThread(RHICmdList,TextureRenderTargetResource, FeatureLevel, MyColor,TextureRHI, UniformStruct);
}
);
}
与@YivanLee文章中所写的函数相比,我对形参进行了修改。从AActor 改成了const UObject WorldContextObject,相应在函数内改成
const UWorld* World=WorldContextObject->GetWorld();
这样就不需要再外部指定Actor来获取World了。
编写USF与重新编译usf
以下是一个最简单的usf代码:
#include "/Engine/Public/Platform.ush"
float4 SimpleColor;
void MainVS(
in float4 InPosition : ATTRIBUTE0,
out float4 OutPosition : SV_POSITION
)
{
OutPosition = InPosition;
}
void MainPS(
out float4 OutColor : SV_Target0
)
{
OutColor = SimpleColor;
}
Ue4支持usf热编译,以下摘自官方文档
在运行非cook版本的游戏或者编辑器时,可以实时修改 .usf 文件,并用热键 Ctrl+Shift+. (period)或者在控制台输入 recompileshaders changed,便能重新读取并构建shader,以做到快速开发迭代!
测试结果
这里我提供一种测试方法,详细过程可以参考了@YivanLee的文章https://zhuanlan.zhihu.com/p/36635394。
- 创建一个Actor蓝图,将其放入场景。在Input选项卡的Auto Receive Input选项中选择Player 0。
- 创建一个RenderTarget与Material,并将RenderTarget拖入Material,连接BaseColor节点。最后将这个材质赋予场景中任意一个可见的模型。
- 在事件图表中右键输入anykey,创建一个你指定按钮的按钮事件,调用之前写的蓝图函数,并且填入所需形参(填入第二步创建的RenderTarget与各个变量)。
- 最后播放关卡,通过指定按键测试效果。