您好,欢迎访问代理记账网站
  • 价格透明
  • 信息保密
  • 进度掌控
  • 售后无忧

虚幻引擎之SecondPass支持

虚幻引擎之SecondPass支持

文章目录

  • 虚幻引擎之SecondPass支持
    • 1 前言
    • 2 一个Mesh多个材质 与 一个材质多Pass
    • 3 网格渲染管线(Mesh Drawing Pipeline)
    • 4 StaticMesh的SecondPass
    • 5 SkeletalMesh的SecondPass
    • 6 SecondPass的应用
    • 7 小结
    • 参考文献

1 前言

当前,虚幻引擎提供了材质编辑器供用户实现各种渲染效果。

但某些渲染效果需要不止一个Pass才能完成。如:对卡通风格的角色进行表面的光照着色,并进行描边。

注:当然,描边的功能也可以通过后处理的方式来实现。这里仅是举例说明。

看似在Unity3D通过写多Pass的着色器可以轻松实现的功能,UE却不支持。

因而,需要对引擎源码进行一定的修改从而实现支持SecondPass的功能。

2 一个Mesh多个材质 与 一个材质多Pass

在进行具体实现的介绍之前,让我们先了解下以下两个概念:

  • 一个Mesh多个材质;
  • 一个材质多个Pass;

二者是两个不同的概念。

一个Mesh多个材质,指的是:对于一个Mesh划分为多个部分,各个部分的材质不尽相同。

例如,一辆车,车身使用Index0的材质,轮胎使用Index1的材质。

一个材质多个Pass,指的是:对于一个Mesh,使用相同或不同的材质对这个Mesh绘制多遍。

例如,前面提到的卡通渲染,第一遍,使用对表面进行光照计算的材质进行渲染,第二遍,我们使用描边材质渲染出卡通角色的轮廓。

3 网格渲染管线(Mesh Drawing Pipeline)

在介绍SecondPass的实现细节之前,首先,需要了解虚幻引擎是如何收集和渲染Mesh的。

Mesh Drawing Pipeline提供了详细的介绍。

涉及到的核心数据结构有:

  • FPrimitiveSceneProxy;
  • FMeshBatch;
  • FMeshDrawCommand;
  • FMeshProcessor;

其中,FPrimitiveSceneProxy为图元在渲染线程中的表示,它是游戏线程UPrimitiveComponent的代理。在一次渲染的过程中,渲染器会收集收集FPrimitiveSceneProxy,将其转换成为FMeshBatch。再通过FMeshProcessor将FMeshBatach转换为FMeshDrawCommand。最后,通过RHICommandList调用进行绘制。

上述提供了虚幻对Mesh进行渲染的大概逻辑。

具体的逻辑还有优化:如对于静态的Mesh,当其渲染状态不改变的情况下,它所生成的FMeshDrawCommand(渲染指令)是会被Cache(缓存起来),无需再次进行收集和转换等过程,直接可以进行渲染绘制。

经过上述的了解,回到当前文章讨论的问题,即 如何支持SecondPass ?

可以想到,最终的渲染数据来自于FPrimitiveSceneProxy转换得到的FMeshBatch。则需要在渲染收集图元的过程中,添加两个FMeshBatch即可。其中一次为FirstPass的材质,第二次为SecondPass的材质。

那么,概括一下接下来应该如何实现:

  1. 添加一个SecondPass材质的接口,用于编辑设置所需渲染的材质;
  2. 将材质资源传递到对应的FPrimitiveSceneProxy,在渲染器收集FMeshBatch时,生成SecondPass的FMeshBatch;

4 StaticMesh的SecondPass

核心功能所涉及修改的文件如下:

StaticMesh.h/cpp
StaticMeshComponent.h/cpp
StaticMeshResources.h
StaticMeshRender.cpp

StaticMesh.h

UStaticMesh 类中添加一个用于设置 SecondPass 材质的数组:

// UE源码位置,在该行代码后添加
TArray<FStaticMaterial> StaticMaterials;
// 笔者添加
/** List of SecondPass materials applied to this mesh. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = StaticMesh)
TArray<class UMaterialInterface*> SecondPassMaterials;

StaticMesh.cpp

UStaticMesh::PostLoad 函数中添加:

// 笔者添加
if (SecondPassMaterials.Num() != StaticMaterials.Num())
{
	SecondPassMaterials.SetNumZeroed(StaticMaterials.Num());
}
// UE源码位置,在该行代码前添加
if(BodySetup == NULL)
{
//...
}

通过上述两个文件的修改,即完成了 SecondPass材质的设置接口的添加。

StaticMeshComponent.h

在 UStaticMeshComponent 类中,添加以下代码:

// 用于覆写一个UStaticMeshComponent所有SubMesh的所有SecondPass材质的接口
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "StaticMesh", meta = (DisplayName = "Second Pass Material"))
    class UMaterialInterface* SecondPassMaterial = nullptr;
// 获得对应 材质索引 的SecondPass材质接口
UMaterialInterface* GetSecondPassMaterial(int32 MaterialIndex) const;

StaticMeshComponent.cpp

实现GetSecondPassMaterial接口

UMaterialInterface* UStaticMeshComponent::GetSecondPassMaterial(int32 MaterialIndex) const
{
	if (StaticMesh && StaticMesh->SecondPassMaterials.IsValidIndex(MaterialIndex) && StaticMesh->SecondPassMaterials[MaterialIndex])
	{
		return StaticMesh->SecondPassMaterials[MaterialIndex];
	}
	return SecondPassMaterial;
}

UStaticMeshComponent::GetUsedMaterials函数添加:

  • 将有效的材质添加到OutMaterials数组中;
if( GetStaticMesh() && GetStaticMesh()->RenderData )
{
	//... 省略部分源码
    // 笔者添加
    // Second Pass Materials
    for (int32 MatIdx = 0; MatIdx < StaticMesh->SecondPassMaterials.Num(); ++MatIdx)
    {
        if (StaticMesh->SecondPassMaterials.IsValidIndex(MatIdx) && StaticMesh->SecondPassMaterials[MatIdx])
        {
            OutMaterials.Add(StaticMesh->SecondPassMaterials[MatIdx]);
        }
    }	
}
// 笔者添加
if (SecondPassMaterial)
{
    OutMaterials.Add(SecondPassMaterial);
}

StaticMeshResources.h

FSectionInfo类添加一个SecondPass材质,并对构造函数进行修改:

/** Information about an element of a LOD. */
struct FSectionInfo
{
	/** Default constructor. */
	FSectionInfo()
	: Material(NULL)
#if WITH_EDITOR
	, bSelected(false)
	, HitProxy(NULL)
#endif
	, FirstPreCulledIndex(0)
	, NumPreCulledTriangles(-1)
    // 笔者添加
	, SecondPassMaterial(NULL)
	)
	{}

	/** The material with which to render this section. */
	UMaterialInterface* Material;
   	// 笔者添加
	UMaterialInterface* SecondPassMaterial;
    
	//... 省略部分源码
};

StaticMeshRender.cpp

FStaticMeshSceneProxy::FLODInfo::FLODInfo函数中添加

// Gather the materials applied to the LOD.
Sections.Empty(MeshRenderData->LODResources[LODIndex].Sections.Num());
for(int32 SectionIndex = 0;SectionIndex < LODModel.Sections.Num();SectionIndex++)
{
	const FStaticMeshSection& Section = LODModel.Sections[SectionIndex];
	FSectionInfo SectionInfo;
	// Determine the material applied to this element of the LOD.
	SectionInfo.Material = InComponent->GetMaterial(Section.MaterialIndex);
	// 笔者添加
    UMaterialInterface* SecondPassMaterial = SectionInfo.SecondPassMaterial;
    //... 
}

FStaticMeshSceneProxy::DrawStaticElements函数中添加代码,将SecondPass材质转换为FMeshBatch。

DrawStaticElements的核心功能就是将材质和顶点数据等转换为FMeshBatch用于后续的渲染。

//... 省略部分源码
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
    FMeshBatch BaseMeshBatch;
	
    if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, BaseMeshBatch))
    {
        if (NumRuntimeVirtualTextureTypes > 0)
        {
            // Runtime virtual texture mesh elements.
            FMeshBatch MeshBatch(BaseMeshBatch);
            SetupMeshBatchForRuntimeVirtualTexture(MeshBatch);
            for (ERuntimeVirtualTextureMaterialType MaterialType : RuntimeVirtualTextureMaterialTypes)
            {
                MeshBatch.RuntimeVirtualTextureMaterialType = (uint32)MaterialType;
                PDI->DrawMesh(MeshBatch, FLT_MAX);
            }
        }
        {
            PDI->DrawMesh(BaseMeshBatch, FLT_MAX);
        }
    	
        // 笔者添加,GetSecondMeshElement用于获得SecondPass材质的信息
        if (GetSecondMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, BaseMeshBatch))
        {
            FMeshBatch MeshBatch(BaseMeshBatch);
            PDI->DrawMesh(MeshBatch, FLT_MAX);
        }

        //... 省略部分源码
    }
}

FStaticMeshSceneProxy::GetSecondMeshElement 函数为笔者模仿GetMeshElement实现的。

几乎所有的代码都一致,仅是替换了材质:

bool FStaticMeshSceneProxy::GetSecondMeshElement(
	int32 LODIndex,
	int32 BatchIndex,
	int32 SectionIndex,
	uint8 InDepthPriorityGroup,
	bool bUseSelectionOutline,
	bool bAllowPreCulledIndices,
	FMeshBatch& OutMeshBatch
) const
{
	const ERHIFeatureLevel::Type FeatureLevel = GetScene().GetFeatureLevel();
	const FStaticMeshLODResources& LOD = RenderData->LODResources[LODIndex];
	const FStaticMeshVertexFactories& VFs = RenderData->LODVertexFactories[LODIndex];
	const FStaticMeshSection& Section = LOD.Sections[SectionIndex];
	const FLODInfo& ProxyLODInfo = LODs[LODIndex];

	// Get SecondPass Material 核心代码
	UMaterialInterface* MaterialInterface = nullptr;
	MaterialInterface = ProxyLODInfo.Sections[SectionIndex].SecondPassMaterial;
	if (MaterialInterface == nullptr)
	{
		return false;
	}
	FMaterialRenderProxy* MaterialRenderProxy = MaterialInterface->GetRenderProxy();
	if (MaterialRenderProxy == nullptr)
	{
		return false;
	}
	const FMaterial* Material = MaterialRenderProxy->GetMaterial(FeatureLevel);
	//... 省略部分源码
}

至此,通过上述代码的修改,即可实现静态Mesh的SecondPass设置。

5 SkeletalMesh的SecondPass

和上面一致,先给出核心功能所涉及修改的文件:

SkeletalMesh.h/cpp
SkeletalMeshTypes.h
SkinnedMeshComponent.h/cpp

SkeletalMesh.h

USkeletalMesh 类中添加一个用于设置 SecondPass 材质的数组:

// UE源码位置,在该行代码后添加
TArray<FSkeletalMaterial> Materials;
// 笔者添加
UPROPERTY(EditAnywhere, Category = SkeletalMesh)
TArray<UMaterialInterface*> SecondPassMaterials;

USkinnedMeshComponent.h

在USkinnedMeshComponent类中,添加以下代码:

UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Mesh", meta = (DisplayName = "SecondPass Material "))
	UMaterialInterface* SecondPassMaterial = nullptr;
UMaterialInterface* GetSecondPassMaterial(int32 MaterialIndex) const;

USkinnedMeshComponent.cpp

实现GetSecondPassMaterial接口

UMaterialInterface* USkinnedMeshComponent::GetSecondPassMaterial(int32 MaterialIndex) const
{
	if (SkeletalMesh && SkeletalMesh->SecondPassMaterials.IsValidIndex(MaterialIndex) && SkeletalMesh->SecondPassMaterials[MaterialIndex])
	{
		return SkeletalMesh->SecondPassMaterials[MaterialIndex];
	}
	return SecondPassMaterial;
}

USkinnedMeshComponent::GetUsedMaterials函数添加:

if( SkeletalMesh )
{
    //... 省略部分源碼
    // 筆者添加 SecondPass Materials
    for (int32 MatIdx = 0; MatIdx < SkeletalMesh->SecondPassMaterials.Num(); ++MatIdx)
    {
        if (SkeletalMesh->SecondPassMaterials.IsValidIndex(MatIdx) && SkeletalMesh->SecondPassMaterials[MatIdx])
        {
            OutMaterials.Add(SkeletalMesh->SecondPassMaterials[MatIdx]);
        }
    }
}
// 筆者添加
if (SecondPassMaterial)
{
    OutMaterials.Add(SecondPassMaterial);
}

SkeletalMeshTypes.h

FSectionElementInfo类添加一个SecondPass材质,并对构造函数进行修改:

/** info for section element in an LOD */
struct FSectionElementInfo
{
    FSectionElementInfo(UMaterialInterface* InMaterial, bool bInEnableShadowCasting, int32 InUseMaterialIndex, UMaterialInterface* InOutlineMaterial = nullptr)
    : Material( InMaterial )
    // 笔者添加
    , SecondPassMaterial(InOutlineMaterial)
    , bEnableShadowCasting( bInEnableShadowCasting )
    , UseMaterialIndex( InUseMaterialIndex )
    #if WITH_EDITOR
    , HitProxy(NULL)
    #endif
    {}

    UMaterialInterface* Material;
    // 笔者添加
    UMaterialInterface* SecondPassMaterial;
	
};

SkeletalMesh.cpp

修改FSkeletalMeshSceneProxy构造函数:

// 笔者添加
UMaterialInterface* SecondPassMaterial = Component->GetSecondPassMaterial(UseMaterialIndex);
LODSection.SectionElements.Add(
    FSectionElementInfo(
        Material,
        bSectionCastsShadow,
        UseMaterialIndex,
        SecondPassMaterial /* 笔者添加 */
        ));
MaterialsInUse_GameThread.Add(Material);

FSkeletalMeshSceneProxy::GetMeshElementsConditionallySelectable 添加以下代码:

// UE源码位置,在该行代码后添加
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, SectionElementInfo, bInSelectable, Collector);

// 笔者添加
if (SectionElementInfo.SecondPassMaterial)
{
	FSectionElementInfo SecondPassInfo(SectionElementInfo.SecondPassMaterial, false,SectionElementInfo.UseMaterialIndex);
	GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, OutlinePassInfo, bInSelectable, Collector);
}
  • GetMeshElementsConditionallySelectable被GetDynamicMeshElements调用,调用GetDynamicElementsSection收集FMeshBatch。

至此,通过上述代码的修改,即可实现动态Mesh的SecondPass设置。

6 SecondPass的应用

本文一直提到了SecondPass在卡通渲染中的描边功能的实现。那么,接下来就一起在UE中实现一个最简单的用于描边的材质吧。

模型的勾边使用Inverted Hull算法。

其基本思想就是:使用正面剔除,进行法线外扩,从而在模型外围实现勾边。

正面剔除

Inverted Hull算法需要使用正面剔除FrontFaceCulling。

由于虚幻原生不支持设置剔除面,可以结合TwoSidedSign材质节点Masked混合模式实现。

TwoSidedSign当三角形是正面的时候返回1,反面返回-1。乘以-1之后就反过来了。则有:三角形的正面值为-1,反面为1。设置给Opacity mask。从而实现剔除正面。
在这里插入图片描述
原理如下:

  • 由于描边材质是Mask类型,当是正面的时候Opacity为-1,小于默认的Opacity Clip值(0.333),会被clip掉;正面的不会。
    在这里插入图片描述

顶点沿法线方向外扩

材质的World Position Offset用于接收顶点的偏移量。

在这里插入图片描述

描边颜色

将材质着色模型设置为Unlit模式,将EmissiveColor设置为描边颜色。

综上即可几点,即可实现一个最简单的描边材质,完整的材质如下:

在这里插入图片描述
效果优化

上述的方法,描边线的粗细会随着距离远近二变化。

为了确保描边线的粗细不会随远近变化,需要在NDC坐标系下外扩固定的距离

但在由于材质蓝图里面无法直接影响NDC坐标,能控制的是World Position Offset,需要想其他办法:

  • Clip Space在转成NDC之前需要除以w,这个值是View Space的Z值,需要消除这个动态的Z值影响。需要将外扩的距离乘以Z

这样操作后接入World Position Offset可以保证在NDC的外扩值是固定的。

7 小结

上文描述的修改,提供了一个在UE中实现SecondPass材质渲染的核心功能细节。

此处给出一个效果示例:
在这里插入图片描述
不过,引擎的功能是要提供给美术、程序员使用的,还需要对相应的UI面板进行一定的自定义优化,以提供更加友好的使用环境。这个部分的实现就留给各位读者。

参考文献

  • 基于插件的StaticMesh多Pass绘制方案
  • 基于插件的SkeletalMesh多Pass绘制方案
  • 使用C++解决UE4的SkeletalMesh多Pass渲染问题
  • UE4描边功能
  • Mesh Drawing Pipeline

分享:

低价透明

统一报价,无隐形消费

金牌服务

一对一专属顾问7*24小时金牌服务

信息保密

个人信息安全有保障

售后无忧

服务出问题客服经理全程跟进