虚幻引擎之WireframeLit模式

虚幻引擎之WireframeLit模式

1 前言

“WireframeLit”是什么?这显然是笔者胡乱取的名字。

  • Wireframe,即线框模式;
  • Lit,即光照模式;

所以,WireframeLit模式,表示的是在光照模式下,同时显示线框的一个编辑器模式下的观察方式。

这源于美术的一个需求,想要模拟和3DsMax软件中类似的效果,如下所示。
在这里插入图片描述
其实,虚幻是可以同时显示线框和光照的,只是不再同一个View中。

通过点击场景编辑器右上角的“重叠方块”,可以将场景编辑器分成四个View,那么就可以设置一个显示线框模式,一个显示光照模式。

在这里插入图片描述

然而,美术肯定不会放过你的,这样明显不同于3DsMax。(求放过!

2 模式的实现

2.1 实现方案

很明显,这是一个比较简单的需求,实现的方案就是只需将Mesh绘制两次即可。

一次为光照模式,另一次为线框模式。

那么为何还需要写呢?无他,就是记录下一些实现过程中的细节。

大概梳理一下需要实现哪些东西:

  1. 添加一个ViewMode选项UI;
  2. 渲染逻辑的修改,实现绘制两遍,一遍是正常材质的光照模式,另一遍是线框模式。

注,其实从之前的文章虚幻引擎之SecondPass支持已经可以看到基本的实现思路。考虑静态Mesh和动态Mesh分别处理。

2.2 实现细节

2.2.1 ViewMode的UI

所涉及修改的文件如下:

EngineBaseTypes.h
ShowFlagsValues.inl
ShowFlags.cpp
ViewModeNames.cpp
EditorViewportCommands.h/cpp
SEditorViewport.cpp
SSCSEditorViewport.cpp
SEditorViewportViewMenu.cpp

增加ShowFlag枚举

EngineBaseTypes.h

在EViewModeIndex中增加枚举量:

UENUM()
enum EViewModeIndex
{
	//... 省略部分源码
	
	// 笔者添加
	VMI_WireframeLit = 29 UMETA(DisplayName = "Wireframe Lit"),
	// 虚幻源码,在这部分前添加
	VMI_Max UMETA(Hidden),
	VMI_Unknown = 255 UMETA(DisplayName = "Unknown"),
};

ShowFlagsValues.inl

// 找到VMI_Wireframe源码这个位置,在下面添加
/** needed for VMI_Wireframe and VMI_BrushWireframe */
SHOWFLAG_FIXED_IN_SHIPPING(0, Wireframe, SFG_Hidden, NSLOCTEXT("UnrealEd", "WireframeSF", "Wireframe"))
// 笔者添加 VMI_WireframeLit
SHOWFLAG_FIXED_IN_SHIPPING(0, WireframeLit, SFG_Hidden, NSLOCTEXT("UnrealEd", "WireframeLitSF", "WireframeLit"))
  • ShowFlagsValues.inl 会被ShowFlags.h中的struct FEngineShowFlags类使用。生成一个成员属性。

添加这个Flag标记的思路如下:

  • 若开启WireframeLit,就可以通过这个标记来开启光照模式下,再加上线框的渲染。

ShowFlags.cpp

FindViewMode函数中添加:

// 笔者添加,返回对应的枚举
else if (EngineShowFlags.WireframeLit)
{
	return VMI_WireframeLit;
}
// 虚幻源码,在这前添加
return EngineShowFlags.Lighting ? VMI_Lit : VMI_Unlit;

EngineShowFlagOverride函数中添加:

// 笔者添加,开启光照
if (ViewModeIndex == VMI_WireframeLit)
{
	EngineShowFlags.SetLighting(true);
}

ApplyViewMode函数中添加:

switch(ViewModeIndex)
{
	// 省略部分源码
	// WireframeLit开启后处理
	case VMI_WireframeLit:
		bPostProcessing = true;
		break;
}

//...省略部分源码

// 在WireframeLit模式下,设置WireframeLit为真,用于后续是否渲染的判断
EngineShowFlags.SetWireframeLit(ViewModeIndex == VMI_WireframeLit);

ViewModeNames.cpp

在FillViewModeDisplayNames函數中添加:

// 筆者添加
else if (ViewModeIndex == VMI_WireframeLit)
{
	ViewModeDisplayNames.Emplace(LOCTEXT("UViewModeUtils_VMI_WireframeLit", "Wireframe Lit"));
}
// 虛幻源碼,在該位置前添加
// VMI_Max
else if (ViewModeIndex == VMI_Max)
{
	ViewModeDisplayNames.Emplace(LOCTEXT("UViewModeUtils_VMI_Max", "Max EViewModeIndex value"));
}

EditorViewportCommands.h

在FEditorViewportCommands类中添加:

/** Changes the viewport to wireframe */
TSharedPtr< FUICommandInfo > WireframeMode;

// 笔者添加
TSharedPtr< FUICommandInfo > WireframeLitMode;

EditorViewportCommands.cpp

在FEditorViewportCommands::RegisterCommands函数中添加:

UI_COMMAND( WireframeLitMode, "Wireframe Lit View Mode", "Renders the scene with normal lighting", EUserInterfaceActionType::RadioButton, FInputChord());

SEditorViewport.cpp

在SEditorViewport::BindCommands函数中添加:

// 虛幻源碼位置
MAP_VIEWMODE_ACTION( Commands.WireframeMode, VMI_BrushWireframe );
// 筆者添加
MAP_VIEWMODE_ACTION(Commands.WireframeLitMode, VMI_WireframeLit);

SEditorViewportViewMenu.cpp

在SEditorViewportViewMenu::FillViewMenu函数中添加:

FToolMenuSection& Section = Menu->AddSection("ViewMode", LOCTEXT("ViewModeHeader", "View Mode"));
{
    Section.AddMenuEntry(BaseViewportActions.LitMode, UViewModeUtils::GetViewModeDisplayName(VMI_Lit));
    Section.AddMenuEntry(BaseViewportActions.UnlitMode, UViewModeUtils::GetViewModeDisplayName(VMI_Unlit));
    Section.AddMenuEntry(BaseViewportActions.WireframeMode, UViewModeUtils::GetViewModeDisplayName(VMI_BrushWireframe));
    // 笔者添加,这里的图标使用了和线框模式一样的。
    Section.AddMenuEntry(BaseViewportActions.WireframeLitMode, UViewModeUtils::GetViewModeDisplayName(VMI_WireframeLit),
    TAttribute<FText>(), FSlateIcon(FEditorStyle::GetStyleSetName(),"EditorViewport.WireframeMode"));
    //... 省略后续代码
}

基本的UI这块代码如上述所示,大概的思路都是模仿Wireframe的,搜索其源码位置进行抄写。

2.2.2 动态Mesh处理

当用户切换成为WireframeLit模式,WireframeLit标记则为真。

当前的想法在光照模式下添加线框的渲染Batch。

对于动态Mesh的渲染,如之前文章提到的,在FSkeletalMeshSceneProxy::GetDynamicElementsSection函数中进行修改。

SkeletalMesh.cpp

// 笔者修改,去除const修饰符
bool bIsWireframe = ViewFamily.EngineShowFlags.Wireframe;
#if WITH_EDITOR
	// 若为WireframeLit模式,将bWireframe设置为真,添加一个Batch进行渲染。
	const bool bIsWireframeLit = ViewFamily.EngineShowFlags.WireframeLit;
	if (bIsWireframeLit)
	{
		// add wireframe pass
		for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
		{
			if (VisibilityMap & (1 << ViewIndex))
			{
				const FSceneView* View = Views[ViewIndex];

				FMeshBatch& Mesh = Collector.AllocateMesh();
				CreateBaseMeshBatch(View, LODData, LODIndex, SectionIndex, SectionElementInfo, Mesh);
				if (!Mesh.VertexFactory)
				{
					// hide this part
					continue;
				}
				Mesh.bWireframe = bIsWireframeLit;
				Mesh.Type = PT_TriangleList;
				Mesh.bSelectable = bInSelectable;

				Mesh.ReverseCulling = IsLocalToWorldDeterminantNegative();
				Mesh.CastShadow = SectionElementInfo.bEnableShadowCasting;
				Mesh.bCanApplyViewModeOverrides = true;
				Mesh.bUseWireframeSelectionColoring = bIsSelected;
				Mesh.ExtraSortKeyPriority = ExtraSortKeyPriority;
				Collector.AddMesh(ViewIndex, Mesh);
			}
		}
         // 接下来代码,不渲染线框而是渲染光照
		bIsWireframe = false;
	}
#endif // #if WITH_EDITOR

// 虚幻源码部分,正常渲染光照材质。
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
    //...
}
2.2.3 静态Mesh处理

静态Mesh稍微麻烦一点,通过之前的文章可以知道,若渲染状态不变的情况下,静态Mesh的渲染命令是会被Cache起来的。

那么,UE是在切换到线框模式的情况下,渲染静态网格的线框呢?

答案就是:

  • FStaticMeshSceneProxy::GetDynamicMeshElements

在线框模式下,静态模型会通过GetDynamicMeshElements将线框材质进行收集,从而实现线框渲染。

那么,笔者添加的WireframeLit模式也需要像Wireframe一样触发这个静态模型线框渲染的流程。

PrimitiveDrawingUtils.cpp

  • 通过IsRichView函数,判断是否会进行动态绘制:
// Flags which make the view rich when present.
if( ViewFamily.UseDebugViewPS()	||
   ViewFamily.EngineShowFlags.LightComplexity ||
   ViewFamily.EngineShowFlags.StationaryLightOverlap ||
   ViewFamily.EngineShowFlags.BSPSplit ||
   ViewFamily.EngineShowFlags.LightMapDensity ||
   ViewFamily.EngineShowFlags.PropertyColoration ||
   ViewFamily.EngineShowFlags.MeshEdges ||
   ViewFamily.EngineShowFlags.LightInfluences ||
   ViewFamily.EngineShowFlags.Wireframe ||
   // 笔者添加
   #if WITH_EDITOR
   ViewFamily.EngineShowFlags.WireframeLit ||
   #endif
   ViewFamily.EngineShowFlags.LevelColoration ||
   ViewFamily.EngineShowFlags.LODColoration ||
   ViewFamily.EngineShowFlags.HLODColoration ||
   ViewFamily.EngineShowFlags.MassProperties )
{
    return true;
}

StaticMeshRender.cpp

在 FStaticMeshSceneProxy::GetDynamicMeshElements 函数中添加以下代码:

#if WITH_EDITOR
// 若是WireframeLit模式就进行一下处理
const bool bIsbIsWireframeLitView = EngineShowFlags.WireframeLit;
if (bIsbIsWireframeLitView)
{
    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        const FSceneView* View = Views[ViewIndex];
        if (IsShown(View) && (VisibilityMap & (1 << ViewIndex)))
        {
            FFrozenSceneViewMatricesGuard FrozenMatricesGuard(*const_cast<FSceneView*>(Views[ViewIndex]));

            FLODMask LODMask = GetLODMask(View);

            for (int32 LODIndex = 0; LODIndex < RenderData->LODResources.Num(); LODIndex++)
            {
                if (LODMask.ContainsLOD(LODIndex) && LODIndex >= ClampedMinLOD)
                {
                    const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
                    const FLODInfo& ProxyLODInfo = LODs[LODIndex];

                    for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
                    {
                        // wireframe
                        FLinearColor ViewWireframeColor(bLevelColorationEnabled ? GetLevelColor() : GetWireframeColor());
                        if (bPropertyColorationEnabled)
                        {
                            ViewWireframeColor = GetPropertyColor();
                        }

                        auto WireframeMaterialInstance = new FColoredMaterialRenderProxy(
                            GEngine->WireframeMaterial->GetRenderProxy(),
                            GetSelectionColor(ViewWireframeColor, !(GIsEditor && EngineShowFlags.Selection) || bProxyIsSelected, IsHovered(), false)
                        );

                        Collector.RegisterOneFrameMaterialProxy(WireframeMaterialInstance);

                        const int32 NumBatches = GetNumMeshBatches();

                        for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
                        {
                            if (LODModel.Sections.Num() > 0)
                            {
                                FMeshBatch& Mesh = Collector.AllocateMesh();
                                if (GetWireframeMeshElement(LODIndex, BatchIndex, WireframeMaterialInstance, SDPG_World, true, Mesh))
                                {
                                    // We implemented our own wireframe
                                    Mesh.bCanApplyViewModeOverrides = false;
                                    Collector.AddMesh(ViewIndex, Mesh);
                                    INC_DWORD_STAT_BY(STAT_StaticMeshTriangles, Mesh.GetNumPrimitives());
                                }
                            }
                        }

                        // Material 渲染材质
                        for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
                        {
                            bool bSectionIsSelected = false;
                            FMeshBatch& MeshElement = Collector.AllocateMesh();
                            if (GIsEditor)
                            {
                                const FLODInfo::FSectionInfo& Section = LODs[LODIndex].Sections[SectionIndex];

                                bSectionIsSelected = Section.bSelected || (bIsWireframeView && bProxyIsSelected);
                                MeshElement.BatchHitProxyId = Section.HitProxy ? Section.HitProxy->Id : FHitProxyId();
                            }

                            if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, SDPG_World, bSectionIsSelected, true, MeshElement))
                            {
                                MeshElement.bCanApplyViewModeOverrides = false;
                                MeshElement.bDitheredLODTransition = true;
                                MeshElement.bUseWireframeSelectionColoring = bSectionIsSelected;
                                Collector.AddMesh(ViewIndex, MeshElement);
                                INC_DWORD_STAT_BY(STAT_StaticMeshTriangles, MeshElement.GetNumPrimitives());
                            }
                        }
                    }
                }
            }
        }
    }
    // 直接返回不处理下面的逻辑
    return;
}
#endif

// 虚幻源码部分,略过
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
{
    //...
}

3 实现效果

下图为当前模式的效果,可以看到静态网格和动态网格都已经支持。

在这里插入图片描述

4 小结

仅仅是个小需求,记录下实现过程的细节。

同时,加深了对虚幻Mesh绘制过程的一点理解。

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值