米老鼠和蓝精鼠v 发表于 2020-12-21 09:57

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

前言及学习建议

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

初始化

插件项目与模块设置

*.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::SM);
    }

    //添加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有关,具体请参考:
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)(
      (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 的文章:


创建一个Actor蓝图,将其放入场景。在Input选项卡的Auto Receive Input选项中选择Player 0。创建一个RenderTarget与Material,并将RenderTarget拖入Material,连接BaseColor节点。最后将这个材质赋予场景中任意一个可见的模型。 在事件图表中右键输入anykey,创建一个你指定按钮的按钮事件,调用之前写的蓝图函数,并且填入所需形参(填入第二步创建的RenderTarget与各个变量)。最后播放关卡,通过指定按键测试效果。
页: [1]
查看完整版本: (虚幻4Shader篇)开始编写最简单的Shader