UE4 自定义Shader 和 RHI

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UE4 自定义Shader 和 RHI相关的知识,希望对你有一定的参考价值。

参考技术A 使用 UE4 自定义 Shader 和 RHI 绘制简单三角形,参考上一节创建 RenderTarget 来显示三角形。

使用UE4创建自定义shader需要注释掉( Engine / Config / ConsoleVariables.ini )
r.ShaderDevelopmentMode = 1

Plugins\TPViewport\Shaders\Private\SimpleShader.usf

在插件中手动创建 SimpleRender 模块。
\Plugins\TPViewport\Source\SimpleRender\SimpleRenderer.Build.cs

Plugins\TPViewport\Source\SimpleRender\Private\SimpleRenderer.h

Plugins\TPViewport\Source\SimpleRender\Private\SimpleRenderer.cpp
模块启动时,设置shader的映射路径。

Plugins\TPViewport\Source\SimpleRender\Public\SimpleShader.h
分别创建顶点和像素shader的映射类。

Plugins\TPViewport\Source\SimpleRender\Private\SimpleShader.cpp

Plugins\TPViewport\Source\TPViewport\Public\RenderTestViewportClient.cpp
引入模块 SimpleRender 后,就可以将三角形渲染到 RenderTarget 。

UE4 自定义Shader 和 RHI

UE4 RHI与Render模块简解

  UE4中的RHI指的是Render hardware interface,作用像Ogre里的RenderSystem,针对Dx11,Dx12,Opengl等等平台抽象出相同的接口,我们能方便能使用相同接口对应不同渲染平台.

  和以前一样,先简单介绍一些类与文件的作用,我们有个抽象的了解.

  RHI.h :主要定义一些硬件平台的公共变量.

  一是 硬件支持项,如是否支持PF_FloatRGBA格式渲染目标,手机平台是否支持FrameBuffer拾取,支持体纹理,支持硬件合并渲染等等.

  二是 硬件变量,如最大Cube纹理数,阴影贴图长宽最大值等等.

  三是 常见渲染定义,如FSamplerStateInitializerRHI纹理采样,FRasterizerStateInitializerRHI栅栏化(填充格式,正方向定义,MSAA),FDepthStencilStateInitializerRHI逐片断处理中的模板与深度,FBlendStateInitializerRHI逐片断处理中的混合.FRHIDrawIndirectParameters/FRHIDrawIndexedIndirectParameters DrawCall中相关参数.

  DynamicRHI.h :包含FDynamicRHI接口定义,渲染所需求所有接口,创建buffer,创建纹理,设置着色器参数,UAV等,简单来说,对应opengl,dx提供的渲染API,其DynamicRHI.cpp文件会根据平台(Windows,apple,android等等)来选择加载合适的渲染平台(如Opengl,Dx,Vulkan等),在RHI模块的private文件夹下,可能看到各个系统会如何选择相应的渲染平台.  

  FRenderResource:定义接口如InitDynamicRHI /ReleaseDynamicRHI /InitRHI /ReleaseRHI /InitResource /ReleaseResource /UpdateRHI等渲染资源选择实现函数.

  RHICommandList相关文件是我们讲RHI主要需要讲的,在这我们先来分析出现的各个类.

  FRHICommandBase: 主要定义一个函数指针,一个执行方法调用函数指针指向的函数。

  函数指针:二个参数(FRHICommandListBase,FRHICommandBase)CallExecuteAndDestruct:传入自己FRHICommandBase到时函数指针指向的方向.

  FRHICommand: FRHICommandBase的一个模板子类,模板需要定义Execute方法,其方法只需要FRHICommandListBase,其会退化上面CallExecuteAndDestruct的FRHICommandBase参数,默认为自己.

  FRHICommand的模板具体化,对应SetRasterizerState/SetDepthStencilState/SetShaderParameter等等,几乎所有渲染API都有对应的FRHICommand的模板具体化实现.

  FRHICommandListBase: 相应FRHICommandBase的链表实现,以及定义一些上下文如IRHICommandContext ,IRHIComputeContext ,并且有相关和RHI线程交互的API,RHI本身相应的FRHICommandBase与List都是存放在渲染线程中,RHI线程可以用于在渲染线程中同步执行异步的复杂操作,如压入很多FRHICommandBase到渲染线程中执行,有些操作可以放入RHI线程中与渲染线程一起执行,在某段FRHICommandBase前,调用WaitForTasks等同步渲染线程与RHI线程,大家可以这么理解,RHI线程对于渲染线程就相当于渲染线程与游戏线程的关系,大家可以看我上篇UE4里的 渲染线程 ,看到如何在渲染线程里压入RHI线程,如何用WaitForTasks与渲染线程同步等.

  FRHICommandList: 简单来说,所有用于渲染API几乎都有二种方法,一种是插入FRHICommandListBase链表,一种是直接调用相应渲染平台对应FDynamicRHI的实现,在这说下,我看了下OpenGLDrv相应的FDynamicRHI实现,相应API如SetShaderParameter, SetDepthStencilState等等,并没有直接调用相应的OpenGL的API,而是把相关改动放入一个FOpenGLRHIState的结构中保存起来,等到DrawCall(如RHIDrawPrimitiveIndirect等)相关命令调用后,才把各个改动对应opengl的API调用起来,如上的glProgramUniform等.

  FRHIAsyncComputeCommandList: 多GPU的FRHICommandList实现。

  FRHICommandListImmediate: 直接调用相应渲染平台对应FDynamicRHI的实现,对比FRHICommandList,主要是创建资源这一块的FDynamicRHI封装,可以看到它的一些函数都是以Create开头的。

  FRHICommandListExecutor: 简单来说,管理FRHICommandListBase的几个子类单例实现,方便查找到如上的FRHICommandListImmediate 与FRHIAsyncComputeCommandListImmediate 单例实现,一般我们看到渲染代码里常见的如FRHICommandList/RHICmdList就是指的是FRHICommandListExecutor::GetImmediateCommandList().

  在这,关于RHI的就先简单了解下,RHI主要调用都在渲染线程中,不过也可以使用FRHICommandListBase链表与RHI线程来实现一些同步异步操作。其中渲染模块中FRHICommandList/RHICmdList一般是FRHICommandListExecutor::GetImmediateCommandList(),这个是直接调用相关FDynamicRHI实现,一般并不与RHI线程交互。

  介绍RHI模块后,我们来看下渲染模块的相关实现,在说下渲染模块的实现前,简单说下,UE4中大量用到C++ 的模版,除开自动生成各个分支代码,还有二点,一是代替部分接口类,减少如虚函数表的性能,二是减少一些分支判断,还是提高性能。但是会造成阅读代码比C#等语言验证,主要在于有些模板你都不知道是那些类可以用等,还好,UE4里一般这种模板使用类都有相同的前缀或是后缀,我们可以记一些相同的前缀或后缀转化成自己认为的接口实现。

  我们先看一段代码,是OpenGLDrv实现的FDynamicRHI子类FOpenGLDynamicRHI的RHIDrawPrimitiveIndirect,简接绘制多组图元集。 

void FOpenGLDynamicRHI::RHIDrawPrimitiveIndirect(uint32 PrimitiveType,FVertexBufferRHIParamRef ArgumentBufferRHI,uint32 ArgumentOffset)
{
    if (FOpenGL::SupportsDrawIndirect())
    {
        VERIFY_GL_SCOPE();

        check(ArgumentBufferRHI);
    GPUProfilingData.RegisterGPUWork(0);

        FOpenGLContextState& ContextState = GetContextStateForCurrentContext();
        BindPendingFramebuffer(ContextState);
        SetPendingBlendStateForActiveRenderTargets(ContextState);
        UpdateViewportInOpenGLContext(ContextState);
        UpdateScissorRectInOpenGLContext(ContextState);
        UpdateRasterizerStateInOpenGLContext(ContextState);
        UpdateDepthStencilStateInOpenGLContext(ContextState);
        BindPendingShaderState(ContextState);
        SetupTexturesForDraw(ContextState);
        CommitNonComputeShaderConstants();
        CachedBindElementArrayBuffer(ContextState,0);

        // Zero-stride buffer emulation won\'t work here, need to use VAB with proper zero strides
        SetupVertexArrays(ContextState, 0, PendingState.Streams, NUM_OPENGL_VERTEX_STREAMS, 1);

        GLenum DrawMode = GL_TRIANGLES;
        GLsizei NumElements = 0;
        GLint PatchSize = 0;
        FindPrimitiveType(PrimitiveType, ContextState.bUsingTessellation, 0, DrawMode, NumElements, PatchSize);

        if (FOpenGL::SupportsTessellation() && DrawMode == GL_PATCHES )
        {
            FOpenGL::PatchParameteri(GL_PATCH_VERTICES, PatchSize);
        } 

        FOpenGLVertexBuffer* ArgumentBuffer = ResourceCast(ArgumentBufferRHI);


        glBindBuffer( GL_DRAW_INDIRECT_BUFFER, ArgumentBuffer->Resource);
        {
            CONDITIONAL_SCOPE_CYCLE_COUNTER(STAT_OpenGLShaderFirstDrawTime, PendingState.BoundShaderState->RequiresDriverInstantiation());
            FOpenGL::DrawArraysIndirect( DrawMode, INDEX_TO_VOID(ArgumentOffset));
        }
        glBindBuffer( GL_DRAW_INDIRECT_BUFFER, 0);
        
        FShaderCache::LogDraw(0);
    }
    else
    {
        UE_LOG(LogRHI, Fatal,TEXT("OpenGL RHI does not yet support indirect draw calls."));
    }

}
FOpenGLDynamicRHI::RHIDrawPrimitiveIndirect

  前面说过,FOpenGLDynamicRHI是在DrawCall时,才把各个改动对应opengl的API调用起来,所以在这,我们可以看到一个渲染的完整过程,当然大家使用过Opengl或是DX直接写过程序也是一样,首先设定渲染目标,混合,设定viewport,设定栅栏化,设定逐片断处理(深度,模板),绑定Shader程序,设定shader纹理,设置shader参数,绑定VAO,设定VAO,DrawCall,嗯,就是这么个过程,无论UE4如何包装,每次DrawCall就是如上顺序处理。

  先说一下在渲染模块里比较常见的类:

  后缀Parameters: 二个主要方法,一是Bind,简单来说,对应一个或多个参数Parameter与Shader代码里参数绑定,对应opengl里的API就是如glGetUniformLocation。二是Set,简单来说,上面绑定后,我们就可以传入参数的值到GPU里,对应opengl里的API就是如glUniform等等。

  模板类里的模板如果是后缀ParametersType,一般主要是指各个后缀为Parameters的类。

  如下一些类写了些自己了解,后查找资料时发现UE4官方文档里 着色器开发 有说,比我说的清楚。

  FVertexFactory: 用来表示顶点数据格式,顶点分布结构,顶点元素Buffer,DeclarationElementList数组,相关opengl的API如glVertexAttribPointer.从opengl3+来说,一般虽然可能有多个buffer,但是应该是在一个glgenbuffer中对应不同的区段而已。

  方法Set: 只是告诉对应opengl里各个buffer的起点与终点,相应的如OffsetInstanceStreams/SetPositionStream都是类似。

  FVertexFactoryType:表示网格类型,如 Local/Particle(三种sprite/beamtrail,mesh)/Landscape/GPUSkin等,

  FMeshBatch: 一般来说,是一组相同顶点格式,相同材质的模型,一般可以使用GPU的实例渲染,减少DrawCall.

  FShaderType: Global/Material/MeshMaterial (vertex/hull/demain/geomerty/pixel 一种)

  FGlobalShader: 全局shader,简单来说,不和mesh与Material关联,一般用于后处理,固定画个方块啥的,如处理特效这种。

  方法SetParameters: 设定Shader里的FViewUniformShaderParameters /FFrameUniformShaderParameters /FBuiltinSamplersParameters 参数。

  FMaterialShader: 特定于过程的着色器,它们需要访问材质的某些属性,因此必须针对每个材质进行编译,但不需要访问任何网格属性。如FLightFunctionVS,FLightFunctionPS等。  

  对比FGlobalShader,增加一个重载的SetParameters,包含材质对Shader的设置。

  FMeshMaterialShader: 着色器是特定于过程的着色器,它们依赖于材质的属性和网格类型,因此必须针对每个材质/FVertexFactory组合进行编译。例如,TBasePassVS / TBasePassPS 需要对前向渲染过程中的所有材质输入进行评估。

  对比FMaterialShader,增加一个方法SetMesh,添加FMeshBatchElement,FVertexFactory对shader的设置,对应VertexFactory的Parameters针对Mesh填充不同的顶点信息。 如GPUSkin,填充骨骼信息到相应的shader参数中, 如MeshParticle,填充动画加速度 ,时间等。以及填充模型本身的FPrimitiveUniformShaderParameters等共有信息,如FPrimitiveUniformShaderParameters:localToworld ,worldTolocal ,objectBounds, LOD,FadeTimeScaleBias等。

  如下这些类表示渲染主要思路,预先一些相同的渲染方式,可以先缓存起来。

  FMeshDrawingPolicy: 整合渲染模型过程,从绑定Shader到调用DrawCall,各个子类对应不同的独立着色器程序。

  1 初始化,根据需要生成或绑定各个对应的Shader.

  2 SetSharedState,设定和Mesh无关的Shader变量。

  3 SetMeshRenderState,设定和Mesh相关的Shader变量。

  4 DrawMesh 调用DrawCall.

  模板类里的模板如果是DrawingPolicyType,一般主要是指FMeshDrawingPolicy的各个子类。

  FUniformLightMapPolicy: 封装和光照有关渲染的Shader参数设置。  

  方法SetMesh:绑定相应光照计算上Shader的参数,如使用GI预计算产生的间接光照图信息,直接光照图信息,天空图AO等。

  TUniformLightMapPolicy: FUniformLightMapPolicy的模版子类,模版为ELightMapPolicyType,表示各种和光照有关,模版预生成多份代码对应不同光照计算表示是否缓存,Shader预编译指令。

enum ELightMapPolicyType
{
    LMP_NO_LIGHTMAP,
    LMP_CACHED_VOLUME_INDIRECT_LIGHTING,
    LMP_CACHED_POINT_INDIRECT_LIGHTING,
    LMP_SIMPLE_DYNAMIC_LIGHTING,
    LMP_LQ_LIGHTMAP,
    LMP_HQ_LIGHTMAP,
    LMP_DISTANCE_FIELD_SHADOWS_AND_HQ_LIGHTMAP,
    // Forward shading specific
    LMP_DISTANCE_FIELD_SHADOWS_AND_LQ_LIGHTMAP,
    LMP_SIMPLE_DIRECTIONAL_LIGHT_AND_SH_INDIRECT,
    LMP_SIMPLE_DIRECTIONAL_LIGHT_AND_SH_DIRECTIONAL_INDIRECT,
    LMP_SIMPLE_DIRECTIONAL_LIGHT_AND_SH_DIRECTIONAL_CSM_INDIRECT,
    LMP_MOVABLE_DIRECTIONAL_LIGHT,
    LMP_MOVABLE_DIRECTIONAL_LIGHT_CSM,
    LMP_MOVABLE_DIRECTIONAL_LIGHT_WITH_LIGHTMAP,
    LMP_MOVABLE_DIRECTIONAL_LIGHT_CSM_WITH_LIGHTMAP,
    // LightMapDensity
    LMP_DUMMY
};
ELightMapPolicyType

  TLightMapPolicy: Shader对应预编译指令,是否缓存,模板为ELightmapQuality,有二个值,分别是LQ_LIGHTMAP,HQ_LIGHTMAP。

  模板类里的模板如果是LightMapPolicyType,一般主要是指TUniformLightMapPolicy/TLightMapPolicy的各个子类。

  如上一些基本比较重要的类就到此,在这我们重点说下FMeshDrawingPolicy这个类,从上面各个类的说明来看,可以看到把所有渲染基本类组合在一起,他的子类简单说几个,BasePassRendering ,CapsuleShadowing ,DepthRendering ,ForwardBasePassRendering ,VelocityRendering等等,还有别的带Rendering的渲染,如DistortionRendering ,DeferredShading ,DecalRendering,ShadowRendering等等虽然和FMeshDrawingPolicy不同,但是过程其实真差不了多少。 在每个Rendering中,都有对应的VS,PS,HS等,这些根据需要分别从上面所说的FGlobalShader /FMaterialShader /FMeshMaterialShader继承,简单来说,后处理特效针对渲染目标的一般从FGlobalShader继承,只针对Material不和具体Mesh有关的用FMaterialShader,最后针对模型渲染的从FMeshMaterialShader继承。

  按BasePassRendering说下,只简单渲染emissive color与light map,对应的FMeshDrawingPolicy子类为TBasePassDrawingPolicy ,如上所说,针对Mesh产生的都继承与FMeshMaterialShader生成的VS,PS等,因为光照有影响,我们看到相应的Shader都对应模版LightMapPolicyType,用于生成正确的Shader对应预编译指令,如有无光照,光照质量,静态或动态,阴影类型等。下面还定义一些与BasePassRendering相关的parameters,如天空盒相关参数,如上TBasePassDrawingPolicy在构造函数中得到或是生成上面的VS,PS,然后在SetSharedState时针对VS,PS设定参数,然后调用SetMeshRenderState针对每个FMeshBatch设定和Mesh有关的参数,然后提交DrawCall.

  每个DrawingPolicy中,对应VS,PS等对应文件可以通过宏IMPLEMENT_SHADER_TYPE查看。

  本文本来还准备更详细讲述一个基本的Rendering的过程,但是新项目时间紧,只是暂停查看,后面会仔细介绍一个完整流程,从阴影渲染,前向或是后向渲染选择一部分来详细介绍,包含大部分参数的含义与作用,不过大家熟悉如上的渲染线程再加个RHI与渲染模块如上这些基本类,应该就能把UE4的源码都联系起来了。

以上是关于UE4 自定义Shader 和 RHI的主要内容,如果未能解决你的问题,请参考以下文章

如何在shader中使用自定义函数

Unity Shader 学习笔记Shader变体Shader属性定义技巧自定义材质面板

UE4 RHI

ue4 新渲染管线整理

17 UE4蓝图:蓝图通信、自定义事件和类型转换

UE4 创建一个自定义NewLevel