391 lines
16 KiB
Markdown
391 lines
16 KiB
Markdown
|
---
|
|||
|
title: 基于插件的SkeletalMesh多Pass绘制方案
|
|||
|
date: 2022-11-04 10:25:07
|
|||
|
excerpt:
|
|||
|
tags:
|
|||
|
rating: ⭐
|
|||
|
---
|
|||
|
>本文仅供抛转引玉,为大家提供一个可行思路。因为本人目前仅仅是个Ue4业余爱好者,手头没有含有Lod的骨骼模型做测试,所以请勿在未测试的情况下直接把本插件用于生产。
|
|||
|
|
|||
|
## 前言
|
|||
|
在阅读了 @白昼行姜暗夜摸王 的系列文章,我感觉通过改引擎的方式来实现模型外描边着实不太方便。在仔细阅读代码后,发现文章中修改代码的相关函数,不是虚函数就是包含在虚函数中。于是乎我就感觉可以通过插件的方式来实现多pass的绘制方案,便开始尝试。
|
|||
|
|
|||
|
另外感谢@大钊 @YivanLee 在我尝试过程中给予的帮助。
|
|||
|
## 思路说明
|
|||
|
>在重写函数的过程中,因为引擎部分类是定义在cpp中,以及部分函数因为没有ENGINE_API或是Private导致无法无法使用。有一个lod更新的逻辑我实在没办法绕过,所以就删除了。所以这个插件对于有lod的模型可能会有问题,解决思路我会说。
|
|||
|
|
|||
|
### 创建自定义SkeletalMeshComponent类
|
|||
|
1. 继承USkeletalMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。
|
|||
|
2. 添加用于多pass渲染的UMaterialInterface指针。(NeedSecondPass变量其实可以不用,因为既然你要使用这个类,那肯定要启用这个功能)
|
|||
|
```c++
|
|||
|
#pragma once
|
|||
|
#include "CoreMinimal.h"
|
|||
|
#include "Engine/Classes/Components/SkeletalMeshComponent.h"
|
|||
|
#include "StrokeSkeletalMeshComponent.generated.h"
|
|||
|
|
|||
|
UCLASS(ClassGroup=(Rendering, Common), hidecategories=Object, editinlinenew, meta=(BlueprintSpawnableComponent))
|
|||
|
class UStrokeSkeletalMeshComponent : public USkeletalMeshComponent
|
|||
|
{
|
|||
|
GENERATED_UCLASS_BODY()
|
|||
|
|
|||
|
//~ Begin UPrimitiveComponent Interface
|
|||
|
virtual void GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false) const override;
|
|||
|
|
|||
|
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
|
|||
|
//~ End UPrimitiveComponent Interface
|
|||
|
|
|||
|
//virtual UMaterialInterface* GetMaterial(int32 MaterialIndex) const override;
|
|||
|
public:
|
|||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
|
|||
|
UMaterialInterface* SecondPassMaterial = nullptr;
|
|||
|
|
|||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
|
|||
|
bool NeedSecondPass=false;
|
|||
|
};
|
|||
|
```
|
|||
|
3. 实现GetUsedMaterials与CreateSceneProxy函数,并包含相应头文件。
|
|||
|
### 头文件
|
|||
|
```c++
|
|||
|
#include "StrokeSkeletalMeshComponent.h"
|
|||
|
#include "StrokeSkeletalMeshSceneProxy.h"
|
|||
|
#include "Engine/Public/Rendering/SkeletalMeshRenderData.h"
|
|||
|
#include "Engine/Public/SkeletalRenderPublic.h"
|
|||
|
```
|
|||
|
### GetUsedMaterials
|
|||
|
自定义的SkeletalMeshComponent只运行OutMaterials.Add(SecondPassMaterial);会导致引擎找不到Material的ShaderMap。在尝试之处,我追踪了Skeletal的MeshDraw绘制流程,发现竟然是VertexFactory指针为空造成的。所以我判断应该是自定义的Component没有把Material进行注册造成的问题。
|
|||
|
|
|||
|
之后我尝试了手动将MaterialInterface的指针添加到SkeletalMesh的Lod数组中的material。虽然解决了问题,但是遇到重复修改会导致组件栏里多出一堆材质asset显示,虽然之后通过判断slotname进行数组管理解决了问题,但是结果还是不完美。
|
|||
|
|
|||
|
不过之后在继续查看了源代码后,我发现只要调用UpdateUVChannelData就可以解决问题。
|
|||
|
```c++
|
|||
|
void UStrokeSkeletalMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials /*= false*/) const
|
|||
|
{
|
|||
|
if (SkeletalMesh)
|
|||
|
{
|
|||
|
// The max number of materials used is the max of the materials on the skeletal mesh and the materials on the mesh component
|
|||
|
const int32 NumMaterials = FMath::Max(SkeletalMesh->Materials.Num(), OverrideMaterials.Num());
|
|||
|
for (int32 MatIdx = 0; MatIdx < NumMaterials; ++MatIdx)
|
|||
|
{
|
|||
|
// GetMaterial will determine the correct material to use for this index.
|
|||
|
|
|||
|
UMaterialInterface* MaterialInterface = GetMaterial(MatIdx);
|
|||
|
OutMaterials.Add(MaterialInterface);
|
|||
|
}
|
|||
|
//这里是我添加的代码
|
|||
|
if (NeedSecondPass)
|
|||
|
{
|
|||
|
OutMaterials.Add(SecondPassMaterial);
|
|||
|
SkeletalMesh->UpdateUVChannelData(false);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (bGetDebugMaterials)
|
|||
|
{
|
|||
|
#if WITH_EDITOR
|
|||
|
//这段代码无法绕过,而且因为不太重要,就直接注释掉了
|
|||
|
//if (UPhysicsAsset* PhysicsAssetForDebug = GetPhysicsAsset())
|
|||
|
//{
|
|||
|
// PhysicsAssetForDebug->GetUsedMaterials(OutMaterials);
|
|||
|
//}
|
|||
|
#endif
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
### CreateSceneProxy
|
|||
|
CreateSceneProxy比较简单,只需要创建自己的自定义Proxy就可以了
|
|||
|
```c++
|
|||
|
FPrimitiveSceneProxy* UStrokeSkeletalMeshComponent::CreateSceneProxy()
|
|||
|
{
|
|||
|
ERHIFeatureLevel::Type SceneFeatureLevel = GetWorld()->FeatureLevel;
|
|||
|
|
|||
|
//定义自定义Proxy类指针
|
|||
|
FStrokeSkeletalMeshSceneProxy* Result = nullptr;
|
|||
|
//FSkeletalMeshSceneProxy* Result = nullptr;
|
|||
|
FSkeletalMeshRenderData* SkelMeshRenderData = GetSkeletalMeshRenderData();
|
|||
|
|
|||
|
// Only create a scene proxy for rendering if properly initialized
|
|||
|
if (SkelMeshRenderData &&
|
|||
|
SkelMeshRenderData->LODRenderData.IsValidIndex(PredictedLODLevel) &&
|
|||
|
!bHideSkin &&
|
|||
|
MeshObject)
|
|||
|
{
|
|||
|
// Only create a scene proxy if the bone count being used is supported, or if we don't have a skeleton (this is the case with destructibles)
|
|||
|
int32 MaxBonesPerChunk = SkelMeshRenderData->GetMaxBonesPerSection();
|
|||
|
int32 MaxSupportedNumBones = MeshObject->IsCPUSkinned() ? MAX_int32 : GetFeatureLevelMaxNumberOfBones(SceneFeatureLevel);
|
|||
|
if (MaxBonesPerChunk <= MaxSupportedNumBones)
|
|||
|
{
|
|||
|
//使用new创建指针对象
|
|||
|
Result = ::new FStrokeSkeletalMeshSceneProxy(this, SkelMeshRenderData);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
|
|||
|
SendRenderDebugPhysics(Result);
|
|||
|
#endif
|
|||
|
return Result;
|
|||
|
}
|
|||
|
```
|
|||
|
### 创建自定义SkeletalMeshSceneProxy类
|
|||
|
1. 继承FSkeletalMeshSceneProxy,重写GetDynamicMeshElements函数。
|
|||
|
2. 定义USkinnedMeshComponent指针,用于获取自定义SkeletalMeshComponent的变量。(之后使用需要使用强制转换)
|
|||
|
```c++
|
|||
|
#pragma once
|
|||
|
#include "Engine/Public/SkeletalMeshTypes.h"
|
|||
|
|
|||
|
class FStrokeSkeletalMeshSceneProxy : public FSkeletalMeshSceneProxy
|
|||
|
{
|
|||
|
public:
|
|||
|
//在UStrokeSkeletalMeshComponent中调用这个构造函数初始化
|
|||
|
FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData);
|
|||
|
|
|||
|
virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override;
|
|||
|
private:
|
|||
|
UPROPERTY()
|
|||
|
const USkinnedMeshComponent* ComponentPtr;
|
|||
|
};
|
|||
|
```
|
|||
|
```c++
|
|||
|
FStrokeSkeletalMeshSceneProxy::FStrokeSkeletalMeshSceneProxy(const USkinnedMeshComponent* Component, FSkeletalMeshRenderData* InSkelMeshRenderData):
|
|||
|
FSkeletalMeshSceneProxy(Component,InSkelMeshRenderData),ComponentPtr(Component)
|
|||
|
{}
|
|||
|
```
|
|||
|
3. 实现GetDynamicMeshElements函数,并包含头文件。
|
|||
|
### 头文件
|
|||
|
```c++
|
|||
|
#include "StrokeSkeletalMeshSceneProxy.h"
|
|||
|
#include "Engine/Public/SkeletalMeshTypes.h"
|
|||
|
#include "Engine/Public/TessellationRendering.h"
|
|||
|
#include "Engine/Public/SkeletalRenderPublic.h"
|
|||
|
```
|
|||
|
### GetDynamicMeshElements
|
|||
|
```c++
|
|||
|
void FStrokeSkeletalMeshSceneProxy::GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const
|
|||
|
{
|
|||
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FStrokeSkeletalMeshSceneProxy_GetMeshElements);
|
|||
|
|
|||
|
if (!MeshObject)
|
|||
|
{
|
|||
|
return;
|
|||
|
}
|
|||
|
MeshObject->PreGDMECallback(ViewFamily.Scene->GetGPUSkinCache(), ViewFamily.FrameNumber);
|
|||
|
|
|||
|
//这句代码无法绕过,所以只能注释掉了
|
|||
|
/*
|
|||
|
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
|
|||
|
{
|
|||
|
if (VisibilityMap & (1 << ViewIndex))
|
|||
|
{
|
|||
|
const FSceneView* View = Views[ViewIndex];
|
|||
|
MeshObject->UpdateMinDesiredLODLevel(View, GetBounds(), ViewFamily.FrameNumber);
|
|||
|
}
|
|||
|
}
|
|||
|
*/
|
|||
|
const FEngineShowFlags& EngineShowFlags = ViewFamily.EngineShowFlags;
|
|||
|
|
|||
|
const int32 LODIndex = MeshObject->GetLOD();
|
|||
|
check(LODIndex < SkeletalMeshRenderData->LODRenderData.Num());
|
|||
|
const FSkeletalMeshLODRenderData& LODData = SkeletalMeshRenderData->LODRenderData[LODIndex];
|
|||
|
|
|||
|
if (LODSections.Num() > 0)
|
|||
|
{
|
|||
|
const FLODSectionElements& LODSection = LODSections[LODIndex];
|
|||
|
|
|||
|
check(LODSection.SectionElements.Num() == LODData.RenderSections.Num());
|
|||
|
|
|||
|
for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
|
|||
|
{
|
|||
|
const FSkelMeshRenderSection& Section = Iter.GetSection();
|
|||
|
const int32 SectionIndex = Iter.GetSectionElementIndex();
|
|||
|
const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();
|
|||
|
|
|||
|
bool bSectionSelected = false;
|
|||
|
|
|||
|
#if WITH_EDITORONLY_DATA
|
|||
|
// TODO: This is not threadsafe! A render command should be used to propagate SelectedEditorSection to the scene proxy.
|
|||
|
if (MeshObject->SelectedEditorMaterial != INDEX_NONE)
|
|||
|
{
|
|||
|
bSectionSelected = (MeshObject->SelectedEditorMaterial == SectionElementInfo.UseMaterialIndex);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
bSectionSelected = (MeshObject->SelectedEditorSection == SectionIndex);
|
|||
|
}
|
|||
|
|
|||
|
#endif
|
|||
|
//下面函数的代码
|
|||
|
// If hidden skip the draw
|
|||
|
check(MeshObject->LODInfo.IsValidIndex(LODIndex));
|
|||
|
bool bHide= MeshObject->LODInfo[LODIndex].HiddenMaterials.IsValidIndex(SectionElementInfo.UseMaterialIndex) && MeshObject->LODInfo[LODIndex].HiddenMaterials[SectionElementInfo.UseMaterialIndex];
|
|||
|
|
|||
|
if (bHide || Section.bDisabled)
|
|||
|
{
|
|||
|
continue;
|
|||
|
}
|
|||
|
//这个函数因为没有导出,但是比较简单所以上面就直接把代码复制出来了
|
|||
|
/* error LNK2019
|
|||
|
if (MeshObject->IsMaterialHidden(LODIndex, SectionElementInfo.UseMaterialIndex) || Section.bDisabled)
|
|||
|
{
|
|||
|
continue;
|
|||
|
}
|
|||
|
*/
|
|||
|
const UStrokeSkeletalMeshComponent* StrokeSkeletalMeshComponent = dynamic_cast<const UStrokeSkeletalMeshComponent *>(ComponentPtr);
|
|||
|
if (SectionElementInfo.Material== StrokeSkeletalMeshComponent->SecondPassMaterial)
|
|||
|
{
|
|||
|
continue;
|
|||
|
}
|
|||
|
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, SectionElementInfo, true, Collector);
|
|||
|
|
|||
|
//进行第二个pass绘制
|
|||
|
//关键点在于修改FSectionElementInfo中的Material,并且再次调用绘制函数GetDynamicElementsSection
|
|||
|
if (StrokeSkeletalMeshComponent->NeedSecondPass) {
|
|||
|
FSectionElementInfo Info = FSectionElementInfo(SectionElementInfo);
|
|||
|
if (StrokeSkeletalMeshComponent->SecondPassMaterial == nullptr) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
Info.Material = StrokeSkeletalMeshComponent->SecondPassMaterial;
|
|||
|
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, Info, true, Collector);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
|
|||
|
for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
|
|||
|
{
|
|||
|
if (VisibilityMap & (1 << ViewIndex))
|
|||
|
{
|
|||
|
if (PhysicsAssetForDebug)
|
|||
|
{
|
|||
|
DebugDrawPhysicsAsset(ViewIndex, Collector, ViewFamily.EngineShowFlags);
|
|||
|
}
|
|||
|
|
|||
|
if (EngineShowFlags.MassProperties && DebugMassData.Num() > 0)
|
|||
|
{
|
|||
|
FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);
|
|||
|
if (MeshObject->GetComponentSpaceTransforms())
|
|||
|
{
|
|||
|
const TArray<FTransform>& ComponentSpaceTransforms = *MeshObject->GetComponentSpaceTransforms();
|
|||
|
|
|||
|
for (const FDebugMassData& DebugMass : DebugMassData)
|
|||
|
{
|
|||
|
if (ComponentSpaceTransforms.IsValidIndex(DebugMass.BoneIndex))
|
|||
|
{
|
|||
|
const FTransform BoneToWorld = ComponentSpaceTransforms[DebugMass.BoneIndex] * FTransform(GetLocalToWorld());
|
|||
|
DebugMass.DrawDebugMass(PDI, BoneToWorld);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if (ViewFamily.EngineShowFlags.SkeletalMeshes)
|
|||
|
{
|
|||
|
RenderBounds(Collector.GetPDI(ViewIndex), ViewFamily.EngineShowFlags, GetBounds(), IsSelected());
|
|||
|
}
|
|||
|
|
|||
|
if (ViewFamily.EngineShowFlags.Bones)
|
|||
|
{
|
|||
|
DebugDrawSkeleton(ViewIndex, Collector, ViewFamily.EngineShowFlags);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
#endif
|
|||
|
}
|
|||
|
```
|
|||
|
>UpdateMinDesiredLODLevel函数处的代码被我注释掉了,大致的解决方法为:调用父类的GetDynamicMeshElements函数,再运行一次GetDynamicElementsSection绘制自己的pass。
|
|||
|
|
|||
|
下面的代码我没测试过,仅为说明意思,很多地方是父类运行过的逻辑,可以删除。
|
|||
|
```c++
|
|||
|
QUICK_SCOPE_CYCLE_COUNTER(STAT_FStrokeSkeletalMeshSceneProxy_GetMeshElements);
|
|||
|
//运行父类函数
|
|||
|
FStrokeSkeletalMeshSceneProxy::GetDynamicMeshElements(Views,ViewFamily,VisibilityMap,Collector);
|
|||
|
|
|||
|
const FEngineShowFlags& EngineShowFlags = ViewFamily.EngineShowFlags;
|
|||
|
|
|||
|
const int32 LODIndex = MeshObject->GetLOD();
|
|||
|
check(LODIndex < SkeletalMeshRenderData->LODRenderData.Num());
|
|||
|
const FSkeletalMeshLODRenderData& LODData = SkeletalMeshRenderData->LODRenderData[LODIndex];
|
|||
|
|
|||
|
if (LODSections.Num() > 0)
|
|||
|
{
|
|||
|
const FLODSectionElements& LODSection = LODSections[LODIndex];
|
|||
|
|
|||
|
check(LODSection.SectionElements.Num() == LODData.RenderSections.Num());
|
|||
|
|
|||
|
for (FSkeletalMeshSectionIter Iter(LODIndex, *MeshObject, LODData, LODSection); Iter; ++Iter)
|
|||
|
{
|
|||
|
const FSkelMeshRenderSection& Section = Iter.GetSection();
|
|||
|
const int32 SectionIndex = Iter.GetSectionElementIndex();
|
|||
|
const FSectionElementInfo& SectionElementInfo = Iter.GetSectionElementInfo();
|
|||
|
|
|||
|
bool bSectionSelected = false;
|
|||
|
|
|||
|
#if WITH_EDITORONLY_DATA
|
|||
|
// TODO: This is not threadsafe! A render command should be used to propagate SelectedEditorSection to the scene proxy.
|
|||
|
if (MeshObject->SelectedEditorMaterial != INDEX_NONE)
|
|||
|
{
|
|||
|
bSectionSelected = (MeshObject->SelectedEditorMaterial == SectionElementInfo.UseMaterialIndex);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
bSectionSelected = (MeshObject->SelectedEditorSection == SectionIndex);
|
|||
|
}
|
|||
|
|
|||
|
#endif
|
|||
|
//下面函数的代码
|
|||
|
// If hidden skip the draw
|
|||
|
check(MeshObject->LODInfo.IsValidIndex(LODIndex));
|
|||
|
bool bHide= MeshObject->LODInfo[LODIndex].HiddenMaterials.IsValidIndex(SectionElementInfo.UseMaterialIndex) && MeshObject->LODInfo[LODIndex].HiddenMaterials[SectionElementInfo.UseMaterialIndex];
|
|||
|
|
|||
|
if (bHide || Section.bDisabled)
|
|||
|
{
|
|||
|
continue;
|
|||
|
}
|
|||
|
const UStrokeSkeletalMeshComponent* StrokeSkeletalMeshComponent = dynamic_cast<const UStrokeSkeletalMeshComponent *>(ComponentPtr);
|
|||
|
if (SectionElementInfo.Material== StrokeSkeletalMeshComponent->SecondPassMaterial)
|
|||
|
{
|
|||
|
continue;
|
|||
|
}
|
|||
|
|
|||
|
//进行第二个pass绘制
|
|||
|
//关键点在于修改FSectionElementInfo中的Material,并且再次调用绘制函数GetDynamicElementsSection
|
|||
|
if (StrokeSkeletalMeshComponent->NeedSecondPass) {
|
|||
|
FSectionElementInfo Info = FSectionElementInfo(SectionElementInfo);
|
|||
|
if (StrokeSkeletalMeshComponent->SecondPassMaterial == nullptr) {
|
|||
|
continue;
|
|||
|
}
|
|||
|
Info.Material = StrokeSkeletalMeshComponent->SecondPassMaterial;
|
|||
|
GetDynamicElementsSection(Views, ViewFamily, VisibilityMap, LODData, LODIndex, SectionIndex, bSectionSelected, Info, true, Collector);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
```
|
|||
|
4. 解决FSkeletalMeshSectionIter未定义的报错问题。(原因在开头说了)
|
|||
|
|
|||
|
在Engine\Source\Runtime\Engine\Private\SkeletalMesh.cpp中找到FSkeletalMeshSectionIter类,并将所有代码复制到你定义的SkeletalMeshSceneProxy所在的cpp文件中。
|
|||
|
|
|||
|
完成以上步骤你就得到一个2个pass的SkeletalMeshComponent。
|
|||
|
## 关于绘制轮廓的问题
|
|||
|
@白昼行姜暗夜摸王 的文章采用了使用了绘制背面的方法来得到轮廓,但是很可惜,因为以下原因,我无法在不修改源码的情况下实现这个方法:
|
|||
|
1. SkeletalMeshSceneProxy的MeshBatch设置逻辑在绘制函数中。
|
|||
|
2. 因为绘制函数有很多核心函数因为没倒出或是private,所以无法重写。
|
|||
|
3. 判断剔除的IsLocalToWorldDeterminantNegative()函数不是虚函数。
|
|||
|
4. bIsLocalToWorldDeterminantNegative是父类的private变量,无法修改。
|
|||
|
|
|||
|
当然可以使用一些c++黑科技来突破(3、4),不过我暂时没有那种技术。
|
|||
|
|
|||
|
## 曲线救国方法
|
|||
|
绘制背面本质上还是一种深度偏移,所以我们可以采用深度偏移的方法来解决。
|
|||
|
|
|||
|
材质我没有仔细调,所以在离模型较远的地方会显得有点“脏”,只需要在Mask与深度偏移添加模型距离的判断来,进行参数调整就完美。
|
|||
|
|
|||
|
## 插件地址及使用方法
|
|||
|
### 插件地址
|
|||
|
https://github.com/blueroseslol/BRPlugins
|
|||
|
|
|||
|
### 使用方法
|
|||
|
1. 创建一个Actor蓝图。
|
|||
|
2. 创建你定义的SkeletalMeshComponent。
|
|||
|
3. 赋予轮廓材质,并勾选NeedSecondPass。
|
|||
|
|
|||
|
## 延伸思考
|
|||
|
我觉得可以通过在自定义的Component类中定义一个TArray<MaterialInterface*>,用于存储各个Pass的材质,之后再自己定义的SceneProxy中进行for循环绘制,不就实现类似unity的多pass材质系统了。
|
|||
|
|
|||
|
至少StaticMesh因为MeshBatch的设置过程,并没有在绘制函数中,所以我们可以任意修改MeshBatch设置,而可以完美实现上述猜想。下一篇文章我将介绍StaticMesh的实现方法。
|