BlueRose
文章97
标签28
分类7
(虚幻4Shader篇)开始编写最简单的Shader

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

前言及学习建议

本人最近在学习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官方写的代码来进行进一步的学习。这样想要绑定什么类型的变量都可以在源代码中找到答案。
这里推荐:

  1. Engine\Source\Runtime\UtilityShaders\Public\OneColorShader.h
  2. Engine\Source\Editor\UnrealEd\Private\Texture2DPreview.cpp
  3. Engine\Plugins\Compositing\LensDistortion\Source\LensDistortion\Private\LensDistortionRendering.cpp

编写渲染线程的渲染函数

Ue4中的渲染函数基本都是带有_RenderThread后缀的,所以我们可以通过搜索有_RenderThread寻找对应的代码。

具体的代码请参考我的github,这里只说大致流程。其大致流程如下:

  1. 通过FRHIRenderPassInfo设置渲染层信息。
  2. 调用RHICmdList.BeginRenderPass函数开始渲染层。
  3. 取得相关变量。例如:各种ShaderMap、顶点格式。
  4. 使用上一步取得的变量,设置显卡管线状态。
  5. 设置视口与Shader变量。
  6. 使用Shader绘制。
  7. 调用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。

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