UE4 Slate源码分析


1.Slate控件类型:

在Slate中,控件分为三种类型:

SLeafWidget不带子槽的控件,如TextBlock;

SPanel子槽数量为动态的控件,如SHorizontalBox,SVerticalBox;

SCompoundWidget子槽数量固定的控件如SDockTab,Border,Button等。

2.创建控件
/**
 * Slate widgets are constructed through SNew and SAssignNew.
 * e.g.
 *      
 *     TSharedRef<SButton> MyButton = SNew(SButton);
 *        or
 *     TSharedPtr<SButton> MyButton;
 *     SAssignNew( MyButton, SButton );
 *
 * Using SNew and SAssignNew ensures that widgets are populated
 */

SNew返回TSharedRef; SAssignNew 返回TSharedPtr.

在虚幻4引擎中,TSharedRef可以直接转换为TSharedPtr,但是这个转化不是双向的,TSharedPtr转化为TSharedRef需要用到AsShared() 函数。

3.总体的渲染流程
img

第一块是控件绘制,在主线程中,给每个控件分配LayerId,并从控件抽象出FSlateDrawElement。

第二块是绘制指令生成渲染指令,也是在主线程中,把FSlateDrawElement包装成FSlateRenderBatch,并根据控件的信息生成VertexBuffer。

第三块是合批并执行渲染,在渲染线程,把之前生成的FSlateRenderBatch按照LayerId从小到大排序,并尝试合批。最后把UI渲染到BackBuffer。

下面从widget源码分析整个渲染的流程

SWidget总共1800行左右,是Slate控件的基类,其中最主要的方法声明如下:

class SLATECORE_API SWidget    : public FSlateControlledConstruction,
    public TSharedFromThis<SWidget>     // Enables 'this->AsShared()'
{
public:
/**
     * Called to tell a widget to paint itself (and it's children).
     *
     * The widget should respond by populating the OutDrawElements array with FDrawElements
     * that represent it and any of its children.
     *
     * @param Args              All the arguments necessary to paint this widget (@todo umg: move all params into this struct)
     * @param AllottedGeometry  The FGeometry that describes an area in which the widget should appear.
     * @param MyCullingRect    The clipping rectangle allocated for this widget and its children.
     * @param OutDrawElements   A list of FDrawElements to populate with the output.
     * @param LayerId           The Layer onto which this widget should be rendered.
     * @param InColorAndOpacity Color and Opacity to be applied to all the descendants of the widget being painted
     * @param bParentEnabled    True if the parent of this widget is enabled.
     * @return The maximum layer ID attained by this widget or any of its children.
     */
    int32 Paint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const;

    /**
     * Ticks this widget with Geometry.  Override in derived classes, but always call the parent implementation.
     *
     * @param  AllottedGeometry The space allotted for this widget
     * @param  InCurrentTime  Current absolute real time
     * @param  InDeltaTime  Real time passed since last tick
     */
    virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime);

private:
        /**
     * The widget should respond by populating the OutDrawElements array with FDrawElements
     * that represent it and any of its children. Called by the non-virtual OnPaint to enforce pre/post conditions
     * during OnPaint.
     *
     * @param Args              All the arguments necessary to paint this widget (@todo umg: move all params into this struct)
     * @param AllottedGeometry  The FGeometry that describes an area in which the widget should appear.
     * @param MyCullingRect     The rectangle representing the bounds currently being used to completely cull widgets.  Unless          IsChildWidgetCulled(...) returns true, you should paint the widget. 
     * @param OutDrawElements   A list of FDrawElements to populate with the output.
     * @param LayerId           The Layer onto which this widget should be rendered.
     * @param InColorAndOpacity Color and Opacity to be applied to all the descendants of the widget being painted
     * @param bParentEnabled    True if the parent of this widget is enabled.
     * @return The maximum layer ID attained by this widget or any of its children.
     */
    virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const = 0;

    /**
     * Compute the Geometry of all the children and add populate the ArrangedChildren list with their values.
     * Each type of Layout panel should arrange children based on desired behavior.
     *
     * @param AllottedGeometry    The geometry allotted for this widget by its parent.
     * @param ArrangedChildren    The array to which to add the WidgetGeometries that represent the arranged children.
     */
    virtual void OnArrangeChildren(const FGeometry& AllottedGeometry, FArrangedChildren& ArrangedChildren) const = 0;

}

public中的Paint方法由SWidget实现,OnPaint方法需要继承SWidget的控件自己实现,OnArrangeChildren方法由带槽的控件实现。

每个控件在OnPaint中最终会调用FSlateDrawElement中的以下方法,并且返回layerId:

image-20220421154936427

Unreal的控件绘制是从FSlateApplication::DrawWindows开始,从SWindow::Paint开始派发,LayerId初始为0。

整个控件的绘制过程,实际上是一个递归调用,所以Unreal的UI绘制是一个深度优先的遍历。递归过程如下:

  1. SWindow调用SWidget::Paint。
  2. 执行SWidget::Paint,并调用纯虚函数OnPaint,分发给各控件实现的OnPaint。
  3. 执行各控件的OnPaint,完成绘制,如果包含子控件则调用子控件的Paint,回到第二步。
  4. 直到所有控件绘制完。

SWidget中的Paint方法会调用OnPaint方法:

// Paint the geometry of this widget.
int32 NewLayerId = OnPaint(UpdatedArgs, AllottedGeometry, CullingBounds, OutDrawElements, LayerId, ContentWidgetStyle, bParentEnabled);

FSlateDrawElement类在DrawElements.h中定义,绘制类型如下:

enum class EElementType : uint8
{
    ET_Box,
    ET_DebugQuad,
    ET_Text,
    ET_ShapedText,
    ET_Spline,
    ET_Line,
    ET_Gradient,
    ET_Viewport,
    ET_Border,
    ET_Custom,
    ET_CustomVerts,
    ET_PostProcessPass,
    /** Total number of draw commands */
    ET_Count,
};

SImage绘制的是ET_Box类型。

以MakeText为例:

void FSlateDrawElement::MakeText( FSlateWindowElementList& ElementList, uint32 InLayer, const FPaintGeometry& PaintGeometry, const FString& InText, const FSlateFontInfo& InFontInfo, ESlateDrawEffect InDrawEffects, const FLinearColor& InTint )
{
    SCOPE_CYCLE_COUNTER(STAT_SlateDrawElementMakeTime)
    PaintGeometry.CommitTransformsIfUsingLegacyConstructor();
    ...

  FSlateDrawElement& Element = ElementList.AddUninitialized();
    FSlateTextPayload& DataPayload = ElementList.CreatePayload<FSlateTextPayload>(Element);
    DataPayload.SetTint(InTint);
    DataPayload.SetText(InText, InFontInfo);
    Element.Init(ElementList, EElementType::ET_Text, InLayer, PaintGeometry, InDrawEffects);
}

同其它的MakeXXX方法一样,最终会创建一个FSlateDrawElement,调用Element.Init方法:

void FSlateDrawElement::Init(FSlateWindowElementList& ElementList, EElementType InElementType, uint32 InLayer, const FPaintGeometry& PaintGeometry, ESlateDrawEffect InDrawEffects)
{
    RenderTransform = PaintGeometry.GetAccumulatedRenderTransform();
    Position = PaintGeometry.DrawPosition;
    Scale = PaintGeometry.DrawScale;
    LocalSize = PaintGeometry.GetLocalSize();
    ClipStateHandle.SetPreCachedClipIndex(ElementList.GetClippingIndex());

    LayerId = InLayer;

    ElementType = InElementType;
    DrawEffects = InDrawEffects;

    // Calculate the layout to render transform as this is needed by several calculations downstream.
    const FSlateLayoutTransform InverseLayoutTransform(Inverse(FSlateLayoutTransform(Scale, Position)));

    // This is a workaround because we want to keep track of the various Scenes 
    // in use throughout the UI. We keep a synchronized set with the render thread on the SlateRenderer and 
    // use indices to synchronize between them.
    FSlateRenderer* Renderer = FSlateApplicationBase::Get().GetRenderer();
    checkSlow(Renderer);
    SceneIndex = Renderer->GetCurrentSceneIndex();

    BatchFlags = ESlateBatchDrawFlag::None;
    BatchFlags |= static_cast<ESlateBatchDrawFlag>(static_cast<uint32>(InDrawEffects) & static_cast<uint32>(ESlateDrawEffect::NoBlending | ESlateDrawEffect::PreMultipliedAlpha | ESlateDrawEffect::NoGamma | ESlateDrawEffect::InvertAlpha));
     ...
    if ((InDrawEffects & ESlateDrawEffect::ReverseGamma) != ESlateDrawEffect::None)
    {
        BatchFlags |= ESlateBatchDrawFlag::ReverseGamma;
    }
}

这里只是将绘制的相关的数据放在FSlateDrawElement中,比如:变换、位置、缩放等,实际上没有进行渲染调用。

SlateApplication.h

/**
     * Draws slate windows, optionally only drawing the passed in window
     */
void FSlateApplication::PrivateDrawWindows( TSharedPtr<SWindow> DrawOnlyThisWindow )
{
    check(Renderer.IsValid());

    // Grab a scope lock around access to the resource proxy map, just to ensure we never cross over
    // with the loading thread.
    FScopeLock ScopeLock(Renderer->GetResourceCriticalSection());

    FMemMark Mark(FMemStack::Get());

    FWidgetPath WidgetsToVisualizeUnderCursor;

#if SLATE_HAS_WIDGET_REFLECTOR
    // Is user expecting visual feedback from the Widget Reflector?
    if (WidgetReflectorPtr.IsValid() && WidgetReflectorPtr.Pin()->IsVisualizingLayoutUnderCursor())
    {
        WidgetsToVisualizeUnderCursor = GetCursorUser()->GetLastWidgetsUnderCursor().ToWidgetPath();
    }
#endif

    // Prepass the window
    DrawPrepass( DrawOnlyThisWindow );

    FDrawWindowArgs DrawWindowArgs( Renderer->GetDrawBuffer(), WidgetsToVisualizeUnderCursor);

    {
        SCOPE_CYCLE_COUNTER( STAT_SlateDrawWindowTime );

        TSharedPtr<SWindow> ActiveModalWindow = GetActiveModalWindow(); 

        if (ActiveModalWindow.IsValid())
        {
            DrawWindowAndChildren( ActiveModalWindow.ToSharedRef(), DrawWindowArgs );

            for( TArray< TSharedRef<SWindow> >::TConstIterator CurrentWindowIt( SlateWindows ); CurrentWindowIt; ++CurrentWindowIt )
            {
                const TSharedRef<SWindow>& CurrentWindow = *CurrentWindowIt;
                if ( CurrentWindow->GetType() == EWindowType::ToolTip )
                {
                    DrawWindowAndChildren(CurrentWindow, DrawWindowArgs);
                }
            }

            TArray< TSharedRef<SWindow> > NotificationWindows;
            FSlateNotificationManager::Get().GetWindows(NotificationWindows);
            for( auto CurrentWindowIt( NotificationWindows.CreateIterator() ); CurrentWindowIt; ++CurrentWindowIt )
            {
                DrawWindowAndChildren(*CurrentWindowIt, DrawWindowArgs);
            }   
        }
        else if( DrawOnlyThisWindow.IsValid() )
        {
            DrawWindowAndChildren( DrawOnlyThisWindow.ToSharedRef(), DrawWindowArgs );
        }
        else
        {
            // Draw all windows
            // Use of an old-style iterator is intentional here, as SlateWindows 
            // array may be mutated by user logic in draw calls. The iterator 
            // prevents us from reading off the end and only keeps an index 
            // internally:
            for( TArray< TSharedRef<SWindow> >::TConstIterator CurrentWindowIt( SlateWindows ); CurrentWindowIt; ++CurrentWindowIt )
            {
                TSharedRef<SWindow> CurrentWindow = *CurrentWindowIt;
                // Only draw visible windows or in off-screen rendering mode
                if (bRenderOffScreen || CurrentWindow->IsVisible() )
                {
                    DrawWindowAndChildren( CurrentWindow, DrawWindowArgs );
                }
            }
        }
    }

    // This is potentially dangerous on the movie playback thread that slate sometimes runs on
    if(!IsInSlateThread())
    {
        // Some windows may have been destroyed/removed.
        // Do not attempt to draw any windows that have been removed.
        TArray<SWindow*> AllWindows = GatherAllDescendants(SlateWindows);
        DrawWindowArgs.OutDrawBuffer.RemoveUnusedWindowElement(AllWindows);
    }


    Renderer->DrawWindows( DrawWindowArgs.OutDrawBuffer );
}

?在slate 独立窗口中应该绘制的是ActiveModalWindow:

DrawWindowAndChildren( ActiveModalWindow.ToSharedRef(), DrawWindowArgs );

最终会调用FSlateRenderer的DrawWindows方法,FSlateRenderer没有实现该方法,实际位置为FSlateRHIRenderer的DrawWindows:

/**
* Creates necessary resources to render a window and sends draw commands to the rendering thread
*
* @param WindowDrawBuffer    The buffer containing elements to draw
*/
void FSlateRHIRenderer::DrawWindows_Private(FSlateDrawBuffer& WindowDrawBuffer)
{
    ...
    // Iterate through each element list and set up an RHI window for it if needed
    const TArray<TSharedRef<FSlateWindowElementList>>& WindowElementLists = WindowDrawBuffer.GetWindowElementLists();
    for (int32 ListIndex = 0; ListIndex < WindowElementLists.Num(); ++ListIndex)
    {
        FSlateWindowElementList& ElementList = *WindowElementLists[ListIndex];

        SWindow* Window = ElementList.GetRenderWindow();

        if (Window)
        {
                // Add all elements for this window to the element batcher
                ElementBatcher->AddElements(ElementList);
        }
        ...
    }

WindowDrawBuffer是从FSlateApplication的FDrawWindowArgs传递的:

FDrawWindowArgs DrawWindowArgs( Renderer->GetDrawBuffer(), WidgetsToVisualizeUnderCursor);

其中第一个参数就是要绘制的数据,我们看下Renderer是如何获取到DrawBuffer的,Renderer是在FSlateApplication中创建和初始化的

bool FSlateApplication::InitializeRenderer( TSharedRef<FSlateRenderer> InRenderer, bool bQuietMode )
{
   Renderer = InRenderer;
   bool bResult = Renderer->Initialize();
   if (!bResult && !bQuietMode)
   {
      FPlatformMisc::MessageBoxExt(EAppMsgType::Ok, *NSLOCTEXT("SlateD3DRenderer", "ProblemWithGraphicsCard", "There is a problem with your graphics card. Please ensure your card meets the minimum system requirements and that you have the latest drivers installed.").ToString(), *NSLOCTEXT("SlateD3DRenderer", "UnsupportedVideoCardErrorTitle", "Unsupported Graphics Card").ToString());
   }
   return bResult;
}
image-20220422100914717

在FSlateOpenGLRenderer中GetDrawBuffer方法:

/** Returns a draw buffer that can be used by Slate windows to draw window elements */
FSlateDrawBuffer& FSlateOpenGLRenderer::GetDrawBuffer()
{
    // Clear out the buffer each time its accessed
    DrawBuffer.ClearBuffer();
    return DrawBuffer;
}

可以看到获取的DrawBuffer是空的,接着分析DrawWindowArgs的调用,在上面代码中调用了5次DrawWindowAndChildren:

void FSlateApplication::DrawWindowAndChildren( const TSharedRef<SWindow>& WindowToDraw, FDrawWindowArgs& DrawWindowArgs )
{
    // On Mac, where child windows can be on screen even if their parent is hidden or minimized, we want to always draw child windows.
    // On other platforms we set bDrawChildWindows to true only if we draw the current window.
    bool bDrawChildWindows = PLATFORM_MAC;

    // Only draw visible windows or in off-screen rendering mode
    if (bRenderOffScreen || (WindowToDraw->IsVisible() && (!WindowToDraw->IsWindowMinimized() || FApp::UseVRFocus())) )
    {
        ...

        // Draw Prep
        FSlateWindowElementList& WindowElementList = DrawWindowArgs.OutDrawBuffer.AddWindowElementList(WindowToDraw);

        ...
    }
}

查看FSlateDrawBuffer的AddWindowElementList方法:

FSlateWindowElementList& FSlateDrawBuffer::AddWindowElementList(TSharedRef<SWindow> ForWindow)
{
    for ( int32 WindowIndex = 0; WindowIndex < WindowElementListsPool.Num(); ++WindowIndex )
    {
        TSharedRef<FSlateWindowElementList> ExistingElementList = WindowElementListsPool[WindowIndex];

        if (ExistingElementList->GetPaintWindow() == &ForWindow.Get())
        {
            WindowElementLists.Add(ExistingElementList); 
            WindowElementListsPool.RemoveAtSwap(WindowIndex);

            ExistingElementList->ResetElementList();

            return *ExistingElementList;
        }
    }

    TSharedRef<FSlateWindowElementList> WindowElements = MakeShared<FSlateWindowElementList>(ForWindow);
    WindowElements->ResetElementList();
    WindowElementLists.Add(WindowElements);

    return *WindowElements;
}

最终返回了FSlateWindowElementList:

FSlateWindowElementList存储Slate的窗口中的所有图元信息。在SWindow执行Draw的时候,会向SlateDrawBuffer申请一个SlateWindowElementList,一个SWindow对应着一个List。在SWindow的子控件执行Paint或者OnPaint的时候,会传入这个FSlateWindowElementList引用,然后控件会将包含的图元信息向List的SlateDrawLayer中写入。

FSlateWindowElementList具有一个DrawLayer的Stack,在图元收集阶段使用。

image-20220422173232609

在AddElements方法中,数据来源是:

const TArrayView<FSlateCachedElementData* const> CachedElementDataList = WindowElementList.GetCachedElementDataList();

最终定位到DrawElements.cpp文件中:

void FSlateWindowElementList::PushCachedElementData(FSlateCachedElementData& CachedElementData)
{
    check(&CachedElementData); 
    const int32 Index = CachedElementDataList.AddUnique(&CachedElementData);
    CachedElementDataListStack.Push(Index);
}

查看PushCachedElementData的调用,在SWindow的PaintWindow中,SWindow继承自FSlateInvalidationRoot,最终调用的是FSlateInvalidationRoot的GetCachedElements方法,返回FSlateCachedElementData数据:

OutDrawElements.PushCachedElementData(GetCachedElements());

FSlateCachedElementData的数据结构:

struct FSlateCachedElementData
{
    friend class FSlateElementBatcher;
    friend struct FSlateCachedElementsHandle;

    void Empty();

    FSlateCachedElementsHandle AddCache(const SWidget* Widget);

    FSlateDrawElement& AddCachedElement(FSlateCachedElementsHandle& CacheHandle, const FSlateClippingManager& ParentClipManager, const SWidget* Widget);

    FSlateRenderBatch& AddCachedRenderBatch(FSlateRenderBatch&& NewBatch, int32& OutIndex);
    void RemoveCachedRenderBatches(const TArray<int32>& CachedRenderBatchIndices);

    FSlateCachedClipState& FindOrAddCachedClipState(const FSlateClippingState* RefClipState);
    void CleanupUnusedClipStates();

    const TSparseArray<FSlateRenderBatch>& GetCachedBatches() const { return CachedBatches; }

    void AddReferencedObjects(FReferenceCollector& Collector);

private:
    /** Removes a cache node completely from the cache */
    void RemoveList(FSlateCachedElementsHandle& CacheHandle);

private:

    /** List of cached batches to submit for drawing */
    TSparseArray<FSlateRenderBatch> CachedBatches;

    TArray<TSharedPtr<FSlateCachedElementList>> CachedElementLists;

    TArray<FSlateCachedElementList*, TInlineAllocator<50>> ListsWithNewData;

    TArray<FSlateCachedClipState> CachedClipStates;
};

从接口定义就可以发现渲染的数据是通过AddCachedElement方法传入的:

FSlateDrawElement& FSlateWindowElementList::AddCachedElement()
{
    FSlateCachedElementData* CurrentCachedElementData = GetCurrentCachedElementData();
    check(CurrentCachedElementData);

    FWidgetDrawElementState& CurrentWidgetState = WidgetDrawStack.Top();
    check(!CurrentWidgetState.bIsVolatile);

    if (!CurrentWidgetState.CacheHandle.IsValid())
    {
        CurrentWidgetState.CacheHandle = CurrentCachedElementData->AddCache(CurrentWidgetState.Widget);
    }

    return CurrentCachedElementData->AddCachedElement(CurrentWidgetState.CacheHandle, GetClippingManager(), CurrentWidgetState.Widget);
}

该方法返回了FSlateDrawElement引用。

添加widget到WidgetDrawStack中:

void FSlateWindowElementList::PushPaintingWidget(const SWidget& CurrentWidget, int32 StartingLayerId, FSlateCachedElementsHandle& CurrentCacheHandle)
{
   FSlateCachedElementData* CurrentCachedElementData = GetCurrentCachedElementData();
   if (CurrentCachedElementData)
   {
      // When a widget is pushed reset its draw elements.  They are being recached or possibly going away
      if (CurrentCacheHandle.IsValid())
      {
#if WITH_SLATE_DEBUGGING
         check(CurrentCacheHandle.IsOwnedByWidget(&CurrentWidget));
#endif
         CurrentCacheHandle.ClearCachedElements();
      }

      WidgetDrawStack.Emplace(CurrentCacheHandle, CurrentWidget.IsVolatileIndirectly() || CurrentWidget.IsVolatile(), &CurrentWidget);
   }
}

PushPaintingWidget是在SWidget中Paint方法调用的:

int32 SWidget::Paint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
    ...
    OutDrawElements.PushPaintingWidget(*this, LayerId, PersistentState.CachedElementHandle);
    ...
}

在DrawWindows_Private中的数据填充分析完成了,接下来看下拿到数据后是如何渲染的:

void FSlateRHIRenderer::DrawWindows_Private(FSlateDrawBuffer& WindowDrawBuffer)
{
    ...
// Tell the rendering thread to draw the windows
            {
               FSlateDrawWindowCommandParams Params;

               Params.Renderer = this;
               Params.WindowElementList = &ElementList;
               Params.Window = Window;
#if WANTS_DRAW_MESH_EVENTS
               Params.WindowTitle = Window->GetTitle().ToString();
#endif
               Params.bLockToVsync = bLockToVsync;
#if ALPHA_BLENDED_WINDOWS
               Params.bClear = Window->GetTransparencySupport() == EWindowTransparency::PerPixel;
#else
               Params.bClear = false;
#endif 
               Params.WorldTimeSeconds = FApp::GetCurrentTime() - GStartTime;
               Params.DeltaTimeSeconds = FApp::GetDeltaTime();
               Params.RealTimeSeconds = FPlatformTime::Seconds() - GStartTime;

               // Skip the actual draw if we're in a headless execution environment
               bool bLocalTakingAScreenShot = bTakingAScreenShot;
               if (GIsClient && !IsRunningCommandlet() && !GUsingNullRHI)
               {
                  ENQUEUE_RENDER_COMMAND(SlateDrawWindowsCommand)(
                     [Params, ViewInfo](FRHICommandListImmediate& RHICmdList)
                     {
                        Params.Renderer->DrawWindow_RenderThread(RHICmdList, *ViewInfo, *Params.WindowElementList, Params);
                     }
                  );
               }

               SlateWindowRendered.Broadcast(*Window, &ViewInfo->ViewportRHI);

               if (bLocalTakingAScreenShot)
               {
                  // immediately flush the RHI command list
                  FlushRenderingCommands();
               }
            }
    ...
}

ENQUEUE_RENDER_COMMAND定义如下:

#define ENQUEUE_RENDER_COMMAND(Type) \
   struct Type##Name \
   {  \
      static const char* CStr() { return #Type; } \
      static const TCHAR* TStr() { return TEXT(#Type); } \
   }; \
   EnqueueUniqueRenderCommand<Type##Name>

EnqueueUniqueRenderCommand:

template<typename TSTR, typename LAMBDA>
FORCEINLINE_DEBUGGABLE void EnqueueUniqueRenderCommand(LAMBDA&& Lambda)
{
    QUICK_SCOPE_CYCLE_COUNTER(STAT_EnqueueUniqueRenderCommand);
    typedef TEnqueueUniqueRenderCommandType<TSTR, LAMBDA> EURCType;

#if 0 // UE_SERVER && UE_BUILD_DEBUG
    UE_LOG(LogRHI, Warning, TEXT("Render command '%s' is being executed on a dedicated server."), TSTR::TStr())
#endif
    if (IsInRenderingThread())
    {
        FRHICommandListImmediate& RHICmdList = GetImmediateCommandList_ForRenderCommand();
        Lambda(RHICmdList);
    }
    else
    {
        if (ShouldExecuteOnRenderThread())
        {
            CheckNotBlockedOnRenderThread();
            TGraphTask<EURCType>::CreateTask().ConstructAndDispatchWhenReady(Forward<LAMBDA>(Lambda));
        }
        else
        {
            EURCType TempCommand(Forward<LAMBDA>(Lambda));
            FScopeCycleCounter EURCMacro_Scope(TempCommand.GetStatId());
            TempCommand.DoTask(ENamedThreads::GameThread, FGraphEventRef());
        }
    }
}

最终通过lambda表达式执行DrawWindow_RenderThread方法,该方法总共有500多行代码。

/** Draws windows from a FSlateDrawBuffer on the render thread */
void FSlateRHIRenderer::DrawWindow_RenderThread(FRHICommandListImmediate& RHICmdList, FViewportInfo& ViewportInfo, FSlateWindowElementList& WindowElementList, const struct FSlateDrawWindowCommandParams& DrawCommandParams)
{
    ...
    RenderingPolicy->DrawElements
                            (
                                RHICmdList,
                                BackBufferTarget,
                                BackBuffer,
                                PostProcessBuffer,
                                ViewportInfo.bRequiresStencilTest ? ViewportInfo.DepthStencil : EmptyTarget,
                                BatchData.GetFirstRenderBatchIndex(),
                                BatchData.GetRenderBatches(),
                                RenderParams
                            );
    ...
}

简单的看下去是调用shader进行渲染的

查看UMG中UImage的实现:

TSharedRef<SWidget> UImage::RebuildWidget()
{
    MyImage = SNew(SImage)
            .FlipForRightToLeftFlowDirection(bFlipForRightToLeftFlowDirection);

    return MyImage.ToSharedRef();
}

UMG就是对Slate的一个简单封装。

于是现在一切流程清晰后,可以得到如下结论:

【1】如果想新建一个自己的UI类型,直接派生一个SWidget子类然后重载OnPaint,然后再在UMG层建一个与之对应的简单封装即可。

【2】如果想要在UI上做一些花样,比如动态UI,3D UI可以在渲染层的vertexshader或者pixleshader上做文章,可以自己给UI写一些特殊shader。

【3】想要优化UI,也是在渲染层做优化,不过unreal的UI已经自带batch,剔除等优化操作了。

参考资料:

https://zhuanlan.zhihu.com/p/45682313
https://blog.uwa4d.com/archives/USparkle_UESlate.html
https://zhuanlan.zhihu.com/p/56127773
typedef TArray<FSlateVertex, FSlateStatTrackingMemoryAllocator<FRenderingBufferStatTracker>> FSlateVertexArray;
typedef TArray<SlateIndex, FSlateStatTrackingMemoryAllocator<FRenderingBufferStatTracker>> FSlateIndexArray;
typedef TArray<FSlateDrawElement, FSlateStatTrackingMemoryAllocator<FDrawElementStatTracker>> FSlateDrawElementArray;

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注