虚幻引擎之WireframeLit模式
文章目录
1 前言
“WireframeLit”是什么?这显然是笔者胡乱取的名字。
- Wireframe,即线框模式;
- Lit,即光照模式;
所以,WireframeLit模式,表示的是在光照模式下,同时显示线框的一个编辑器模式下的观察方式。
这源于美术的一个需求,想要模拟和3DsMax软件中类似的效果,如下所示。
其实,虚幻是可以同时显示线框和光照的,只是不再同一个View中。
通过点击场景编辑器右上角的“重叠方块”,可以将场景编辑器分成四个View,那么就可以设置一个显示线框模式,一个显示光照模式。
然而,美术肯定不会放过你的,这样明显不同于3DsMax。(求放过!
2 模式的实现
2.1 实现方案
很明显,这是一个比较简单的需求,实现的方案就是只需将Mesh绘制两次即可。
一次为光照模式,另一次为线框模式。
那么为何还需要写呢?无他,就是记录下一些实现过程中的细节。
大概梳理一下需要实现哪些东西:
- 添加一个ViewMode选项UI;
- 渲染逻辑的修改,实现绘制两遍,一遍是正常材质的光照模式,另一遍是线框模式。
注,其实从之前的文章虚幻引擎之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绘制过程的一点理解。