找回密码
 立即注册
查看: 650|回复: 0

(虚幻4Shader篇)开始编写最简单的Shader

[复制链接]
发表于 2020-12-21 09:57 | 显示全部楼层 |阅读模式
前言及学习建议

本人最近在学习UnrealEngine的GlobalShader,在这个过程中阅读了 @YivanLee 的Shader系列文章,大大提高了学习速度。但这些代码大多基于4.19,其中部分代码将会被废弃,所以我撰写这篇在此分享4.22版本的GlobalShader相关经验。
本文意在理顺思路,教会读者如何搭建基础的Shader测试环境,顺便总结学习心得,所以本文不会将所有代码贴出,详细代码请参考github。
大部分内容解释还请参看了 @YivanLee 的文章
我认为重复造轮子没有意义。具体代码可以参考我的github,读者可以按照commit一步一步学习代码:

初始化

插件项目与模块设置

*.uplugin文件中把LoadingPhase改成:
  1. "Modules": [
  2.         {
  3.             "Name": "Foo",
  4.             "Type": "Developer",
  5.             "LoadingPhase": "PostConfigInit"
  6.         }
  7.     ]
复制代码
修改插件的模块文件*.Build.cs,在PublicDependencyModuleNames.AddRange中添加RHI、Engine、RenderCore、CoreUObject。在PrivateDependencyModuleNames.AddRange中删除Slate、SlateCore、Engine、CoreUObject,添加"Projects"。
添加h与cpp文件

在插件目录中新建以下目录结构(部分文件在插件创建时就已创建):
  1. Source
  2.     |——与插件名相同的文件夹
  3.         |——Classes
  4.             |——SimplePixelShader.h(该文件用于声明结构体与测试Shader的蓝图库)
  5.         |——Private
  6.             |——与插件名相同的模块cpp文件
  7.             |——SimplePixelShader.cpp(用于实现GlobalShader、蓝图库代码)
  8.         |——Public
  9.             |——与插件名相同的模块h文件
复制代码
这里我创建了SimplePixelShader.cpp与SimplePixelShader.h文件用于之后的GlobalShader实现。在之后的内容中我也将通过这两个文件名进行说明。但读者在实践中可以使用不一样的文件名。
创建usf文件

在插件目录中新建以下目录结构:Shaders-Private。之后可以开始编写usf。
重新生成解决方案

在Unreal项目文件上右键点击“Generate Visual Studio File”,生成新的解决方案,并且编译项目。(刷新解决方案)
代码编写

设置虚拟路径

在插件的模块cpp文件(与插件同名的cpp文件)的StartupModule()中,添加虚拟路径:
  1. FString PluginShaderDir = FPaths::Combine(IPluginManager::Get().FindPlugin(TEXT("BRPlugins"))->GetBaseDir(), TEXT("Shaders"));
  2. AddShaderSourceDirectoryMapping(TEXT("/Plugin/BRPlugins"), PluginShaderDir);
复制代码
这里的BRPlugins是我所写的插件名。所写的代码需要与usf所在路径及Shader实现宏中的虚拟路径对应。PluginShaderDir变量为真实路径,AddShaderSourceDirectoryMapping如字面意思,设定一个虚拟路径代表真实路径。
最后在Shader实现宏中使用:
  1. IMPLEMENT_SHADER_TYPE(, FSimplePixelShaderPS, TEXT("/Plugin/BRPlugins/Private/SimplePixelShader.usf"), TEXT("MainPS"), SF_Pixel)
复制代码
声明并且向Ue4注册GlobalShader

继承FGlobalShader,实现所需函数:
  1. class FSimplePixelShader : public FGlobalShader
  2. {
  3. public:
  4.     //确定Shader功能支持情况
  5.     static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
  6.     {
  7.         return IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM);
  8.     }
  9.     //添加Usf中的宏
  10.     static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
  11.     {
  12.         FGlobalShader::ModifyCompilationEnvironment(Parameters, OutEnvironment);
  13.         OutEnvironment.SetDefine(TEXT("TEST_MICRO"), 1);  
  14.     }
  15.     FSimplePixelShader(){}
  16.     //构造函数,用于绑定Shader中的变量
  17.     FSimplePixelShader(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
  18.         : FGlobalShader(Initializer)
  19.     {
  20.          SimpleColorVal.Bind(Initializer.ParameterMap, TEXT("SimpleColor"));
  21.          TextureVal.Bind(Initializer.ParameterMap, TEXT("TextureVal"));
  22.          TextureSampler.Bind(Initializer.ParameterMap, TEXT("TextureSampler"));
  23.     }
  24.     //自己定义的Shader变量设置函数,形参和函数名可以自己随意设置
  25.     template<typename TShaderRHIParamRef>
  26.     void SetParameters(FRHICommandListImmediate& RHICmdList,const TShaderRHIParamRef ShaderRHI, const FLinearColor &MyColor,const FTextureRHIParamRef& TextureRHI)
  27.     {
  28.         SetShaderValue(RHICmdList, ShaderRHI, SimpleColorVal, MyColor);
  29.         SetTextureParameter(RHICmdList, ShaderRHI, TextureVal, TextureSampler,TStaticSamplerState<SF_Trilinear,AM_Clamp,AM_Clamp,AM_Clamp>::GetRHI(),TextureRHI);
  30.     }
  31.     //序列化虚函数
  32.     virtual bool Serialize(FArchive& Ar) override
  33.     {
  34.         bool bShaderHasOutdatedParameters = FGlobalShader::Serialize(Ar);
  35.         Ar << SimpleColorVal<< TextureVal<< TextureSampler;
  36.         return bShaderHasOutdatedParameters;
  37.     }
  38. private:
  39.     FShaderParameter SimpleColorVal;
  40.     FShaderResourceParameter TextureVal;
  41.     FShaderResourceParameter TextureSampler;
  42. };
  43. class FSimplePixelShaderVS : public FSimplePixelShader
  44. {
  45.     //声明Shader宏
  46.     DECLARE_SHADER_TYPE(FSimplePixelShaderVS, Global);
  47. public:
  48.     FSimplePixelShaderVS(){}
  49.     FSimplePixelShaderVS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
  50.         : FSimplePixelShader(Initializer)
  51.     {
  52.     }
  53. };
  54. class FSimplePixelShaderPS : public FSimplePixelShader
  55. {
  56.     //声明Shader宏
  57.     DECLARE_SHADER_TYPE(FSimplePixelShaderPS, Global);
  58. public:
  59.     FSimplePixelShaderPS(){}
  60.     FSimplePixelShaderPS(const ShaderMetaType::CompiledShaderInitializerType& Initializer)
  61.         : FSimplePixelShader(Initializer)
  62.     {
  63.     }
  64. };
  65. IMPLEMENT_SHADER_TYPE(, FSimplePixelShaderVS, TEXT("/Plugin/BRPlugins/Private/SimplePixelShader.usf"), TEXT("MainVS"), SF_Vertex)
  66. 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有关,具体请参考:
BeginRenderPass与EndRenderPass代替了原本的SetRenderTarget与CopyToResolveTarget函数。 另外因为YivanLee的文章中所使用的DrawPrimitive函数已被标记为会被废弃的函数,所以最后我使用DrawIndexedPrimitive函数进行绘制。
编写蓝图函数库函数

为了能够在蓝图中调用渲染函数,这里我们需要声明一个BlueprinntFunctionLibrary并编写一个函数。
这里的代码只做示例,具体的请参考我的github。
  1. void USimplePixelShaderBlueprintLibrary::DrawTestShaderRenderTarget(const UObject* WorldContextObject, UTextureRenderTarget2D* OutputRenderTarget, FLinearColor MyColor, UTexture* MyTexture, FSimpleUniformStruct UniformStruct)
  2. {  
  3.     check(IsInGameThread());  
  4.     if (!OutputRenderTarget)  
  5.     {  
  6.         return;  
  7.     }  
  8.     //取得各种所需变量
  9.     FTextureRenderTargetResource* TextureRenderTargetResource = OutputRenderTarget->GameThread_GetRenderTargetResource();  
  10.     FTextureRHIParamRef TextureRHI = MyTexture->TextureReference.TextureReferenceRHI;
  11.     const UWorld* World = WorldContextObject->GetWorld();
  12.     ERHIFeatureLevel::Type FeatureLevel = World->Scene->GetFeatureLevel();  
  13.     //往渲染队列中添加新的渲染任务
  14.     ENQUEUE_RENDER_COMMAND(CaptureCommand)(  
  15.         [TextureRenderTargetResource, FeatureLevel, MyColor,TextureRHI, UniformStruct](FRHICommandListImmediate& RHICmdList)
  16.         {  
  17.             DrawTestShaderRenderTarget_RenderThread(RHICmdList,TextureRenderTargetResource, FeatureLevel, MyColor,TextureRHI, UniformStruct);
  18.         }  
  19.     );  
  20. }
复制代码
与 @YivanLee 文章中所写的函数相比,我对形参进行了修改。从AActor 改成了const UObject WorldContextObject,相应在函数内改成
  1. const UWorld* World=WorldContextObject->GetWorld();
复制代码
这样就不需要再外部指定Actor来获取World了。
编写USF与重新编译usf

以下是一个最简单的usf代码:
  1. #include "/Engine/Public/Platform.ush"
  2. float4 SimpleColor;
  3. void MainVS(
  4. in float4 InPosition : ATTRIBUTE0,
  5. out float4 OutPosition : SV_POSITION
  6. )
  7. {
  8.     OutPosition = InPosition;
  9. }
  10. void MainPS(
  11.     out float4 OutColor : SV_Target0
  12.     )
  13. {
  14.     OutColor = SimpleColor;
  15. }
复制代码
Ue4支持usf热编译,以下摘自官方文档
在运行非cook版本的游戏或者编辑器时,可以实时修改 .usf 文件,并用热键 Ctrl+Shift+. (period)或者在控制台输入 recompileshaders changed,便能重新读取并构建shader,以做到快速开发迭代!
测试结果

这里我提供一种测试方法,详细过程可以参考了 @YivanLee 的文章:


    创建一个Actor蓝图,将其放入场景。在Input选项卡的Auto Receive Input选项中选择Player 0。创建一个RenderTarget与Material,并将RenderTarget拖入Material,连接BaseColor节点。最后将这个材质赋予场景中任意一个可见的模型。 在事件图表中右键输入anykey,创建一个你指定按钮的按钮事件,调用之前写的蓝图函数,并且填入所需形参(填入第二步创建的RenderTarget与各个变量)。最后播放关卡,通过指定按键测试效果。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-24 19:53 , Processed in 0.089450 second(s), 25 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表