This commit is contained in:
2023-06-29 11:55:02 +08:00
commit 36e95249b1
1236 changed files with 464197 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
## UE插件
1. 像素化插件
- http://www.volumesoffun.com/blog/
- https://github.com/volumesoffun/cubiquity-for-unreal-engine
2. MotionBuilder Live Link:https://github.com/ue4plugins/MobuLiveLink
3. WeiXinSDKhttps://github.com/tkzcfc/WeiXinSDK
4. SpoutUE4:https://github.com/ZimaXXX/SpoutUE4
5. A fish flock AI:https://github.com/no5ix/flock-ai-ue4-plugin
7. Simple Windows:https://github.com/Jordonbc/UE4_SimpleWindows
8. Dark Souls Camera:https://github.com/donanroherty/UE4_DarkSoulsCamera
9. VOX4UPlugin:https://github.com/mik14a/VOX4UPlugin
10. AircraftHud:https://github.com/unoctanium/AircraftHud
11. ue4_mesh_deformation_toolkit:https://github.com/normalvector/ue4_mesh_deformation_toolkit
12. RuntimeMeshLoader:https://github.com/GameInstitute/RuntimeMeshLoader
13. PsReplayKit:https://github.com/PushkinStudio/PsReplayKit
14. 3dRudder SDK:https://github.com/3DRudder/3DRudderSDK_Unreal_Engine
16. protobuf:https://github.com/marshal-it/Protobuffer_UE4
17. Boost / PCL Unreal Third-Party Plugin:https://github.com/hausfrau87/Boost_PCL_UnrealThirdPartyPlugin
18. UE4-Binary-Builderhttps://github.com/ryanjon2040/UE4-Binary-Builder
19. UMGConvertSlate:https://github.com/sxh910924/UMGConvertSlate
20. TestPak:https://github.com/ArcEcho/TestPak
21. Painting-ue4:https://github.com/coding2233/Painting-ue4
22. Unreal XunFei Speech:https://github.com/303snowing/UnrealXunFeiSpeech
23. LocalSimulation:https://github.com/peterlnewton/LocalSimulation
24. Hybrid iOS Native and Unreal Engine:https://github.com/detroit-labs/UE-iOS-Hybrid
25. state machine:https://github.com/lhurt51/UE4-CppStateMachinePlugin
26. Hotline Miami Template:https://github.com/CarstenZarbock/HotlineMiamiTemplate
27. UE4-Physics-Missile:https://github.com/MitchKeenan0/UE4-Physics-Missile
28. MMTContent:https://github.com/BoredEngineer/MMT_Content
29. Spout-UE4:https://github.com/AleDel/Spout-UE4
30. Cover Generator:https://github.com/Deams51/CoverGenerator-UE4
31. GameSavePlugin:https://github.com/marshal-it/GameSavePlugin
32. Input Buffering plug-in:https://github.com/Isatin/UE4InputBuffer
33. BuoyancySystem:https://github.com/Jay2645/BuoyancySystem
34. UE4_Boost:https://github.com/NullP0inter/UE4_Boost
35. UE4-DynamicSubAnimInstance:https://github.com/crimsonstrife/UE4-DynamicSubAnimInstance
36. UE4-MySQLPlugin:https://github.com/anhanjinj/UE4-MySQLPlugin
37. AlipayOpenapiCpp:https://github.com/ArthasModern/AlipayOpenapiCpp
38. UE4AndroidCam:https://github.com/kevinjosue2326/UE4AndroidCam
39. ExtraWindow:https://github.com/crashangelbr/ExtraWindow
40. ExtraWindowCam:https://github.com/chaosgrid/ExtraCamWindow
41. KiruroboMocapPlugin:https://github.com/kirurobo/KiruroboMocapPlugin
42. MySQLConnectorUE4Plugin:https://github.com/KhArtNJava/MySQLConnectorUE4Plugin
43. VRUMGPlugin:https://github.com/mitchemmc/VRUMGPlugin
44. Simple Data Integration Plugin:https://github.com/HeliosOrg/SimpleDataIntegrationPlugin
45. MineSweeper:https://github.com/fengkan/MineSweeper
46. Runtime Mesh Component:https://github.com/Koderz/RuntimeMeshComponent
47. Climbing-Movement-Component:https://github.com/Deema35/Climbing-Movement-Component
48. socketio-client-ue4:https://github.com/getnamo/socketio-client-ue4
49. SanwuUEUtilites:https://github.com/sanwu/jjboomsky
50. Creature:https://github.com/kestrelm/Creature_UE4
51. ActionRPGGame:https://github.com/iniside/ActionRPGGame
52. FliteTTSPlugin:https://github.com/ideoservo/FliteTTSPlugin
53. Sphinx:https://github.com/shanecolb/sphinx-ue4
55. DoN AI Navigation Plugin:https://github.com/VSZue/DonAINavigation
56. Procedural mesh examples:https://github.com/SiggiG/ProceduralMeshes
57. Chartboost:https://github.com/getsetgames/Chartboost
58. MMT_Plugin:https://github.com/BoredEngineer/MMT_Plugin
59. UnrealMessageBusDemo:https://github.com/BhaaLseN/UnrealMessageBusDemo
60. RakNet plugin for Unreal Engine 4:https://github.com/yujen/RN4UE4
61. UE4Downloader:https://github.com/chaiyuntian/UE4Downloader
62. UE4ActorSaveLoad:https://github.com/shinaka/UE4ActorSaveLoad
63. DataAccess:https://github.com/afuzzyllama/DataAccessUE4

View File

@@ -0,0 +1,177 @@
---
title: 使用Python开发Maya插件学习笔记
date: 2022-11-04 10:20:38
excerpt:
tags:
rating: ⭐
---
# 前言
前段时间研究使用Maya重定向动画但一个一个手动操作还是有点心烦所以我花了2天时间学习了Pyhton并写了这个插件本人有c++、qt、JavaScript经验所以学的快在这个过程也积累了一些心得在此分享给大家。另外祝大家劳动节快乐。
以下是我写的插件一个通过HumanIK批量重定向动画的工具
https://github.com/blueroseslol/DccTool
可以帮助动画公司将biped骨骼动画批量重定向到Ue4或者其他骨骼上。里面有很多HumanIK控制代码以及文件导入与导出代码可以参考。如果有什么问题欢迎交流。
# 前期准备
开发环境搭建推荐看这篇文章 https://www.jianshu.com/p/813b2cc71ca2
本人是通过《Maya Python游戏与影视编程指南》一书来学习实用Python开发Maya插件书中也介绍了Python的语法。使得没有Python基础的人也可以很好的学习。同时他也介绍了Maya插件开发的命令模式与API模式。通俗的说就是用Maya的内置命令与实用Maya的API。Maya API更加适合专业插件开发者使用。举个书中例子API中的基础对象MObject是一个指针对象。所以使用c#或者c++会更加适合API模式的开发吧。
不过这本书的很多翻译都感觉怪怪,这是它位移的缺点。
我使用Vscode进行开发除了必须的Python打开一个Py文件就会提示安装插件外我还使用**MayaPy**与**MayaCode**。为了能让MayaPy将代码发送到Maya中执行还需要再Maya中执行一段开启端口的命令
**Mel**
```
commandPort -name "localhost:7001" -sourceType "mel" -echoOutput;
```
**Python**
```
import maya.cmds as cmds
cmds.commandPort(name=":7001", sourceType="mel",echoOutput=True)
```
端口与MayaPy中设置的端口有关。如果你不想每次启动Maya都手动执行命令那么可以新建一个脚本文件并将代码填入。之后放到指定目录中
```
Windows: <drive>:\Documents and Settings\<你的windows用户名>\My Documents\maya\<你maya的版本号>\scripts
其实就是我的文档下面maya文件夹
MacOSX: ~/Library/Preferences/Autodesk/maya/<你maya的版本号>/scripts.
Linux: ~/maya/<你maya的版本号>/scripts.
```
**Maya Python路径设置及代码自动补全**<br>
VS Code中按Ctrl+Shift+P输入Settings打开settings.json配置文件在大括号里加入下面代码
```
//python.pythonPath是指定Python命令路径请根据你maya的安装路径来做修改
"python.pythonPath": "C:/Program Files/Autodesk/Maya2019/bin/mayapy.exe",
//python.autoComplete.extraPaths是代码自动补全路径同样根据你自己的maya安装路径来写
"python.autoComplete.extraPaths": "C:/Program Files/Autodesk/Maya2019/devkit/other/pymel/extras/completion/py"
```
注意:settings.json文件中每一项设置用","隔开,最后一项设置后面没有",",如果报错,检查一下是不是这里出现了问题。
# 编码篇
初次学习可以参考YivanLee的文章
https://zhuanlan.zhihu.com/p/76957745
我认为首先你需要了解Maya中的物体都是节点式的当然我个人认为Maya的节点更加偏向于组件而非Houdini那样的流程节点。
## 文档与搜索技巧
请使用谷歌进行搜索不推荐bing以及baidu包括搜索API这样可以节约大量时间。
**官方文档**http://help.autodesk.com/view/MAYAUL/2018/ENU/
>Pyside2是python版本的Qt库。你只要看一下它的模块数目就能明白它的强大。另外百度的时候请搜索PyQt5虽然Pyside才是官方正版。
**Pyside2**https://doc.qt.io/qtforpython/modules.html
>PyMel与maya.cmds不同在于它返回的不是字符串而是一个PyNode对象。它可以直接修改节点属性值无需调用getAttr与setAttr。更加适合于习惯了OOP语言人士使用。同时PyMel可以简化GUI的构建。但你看了文档就会明白这个玩意就是个残废。
**Pymel文档**http://help.autodesk.com/cloudhelp/2018/JPN/Maya-Tech-Docs/PyMel/modules.html
**FBX Mel命令**https://knowledge.autodesk.com/zh-hans/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2019/CHS/Maya-DataExchange/files/GUID-335F2172-DD4D-4D79-B969-55238C08F2EF-htm.html
# pyside2与GUI
实用Python构建Maya插件UI有3种方式
1. 调用Maya内置命令创建。
2. 直接调用Pyside2函数创建。
本人使用第三种使用Qt的界面设计师工具创建。虽然本质上就是第二种方法但效率高。需要注意的是maya目录下的designer.exe是不能直接使用的。需要将安装目录下的qt-plugins中的所有文件都复制到bin所在目录中。之后的步骤就是用Qt的界面设计师工具设计界面了。
但本人电脑上有Qt所有直接就用自己这个版本了。推荐还是用Maya目录下的版本自己去下个5.6版本的也是可以的)原因后面会说。
Qt的界面设计师工具可以输出*.ui文件但Python是无法直接使用的,(虽然可以通过loadUI载入但只能调用Mel命令无法关联python函数)所以之后需要安装Pyside2目的是为了使用 pyside-uic.exe工具它可以将 *.ui文件转化为python代码。
Pyside2对应python3.x所以你需要下载3.0的版本并安装。安装完之后打开CMD切换到安装目录下的script文件夹。执行
```
pip install PySide2
```
时候再执行
```
//请注意文件路径,推荐奖*.ui文件复制到script文件夹中
pyside-uic -o output.py input.ui
```
可能是我用的Qt版本与Maya的不同最后生成出来的python存在一些小错误按钮上的setText函数中会多出一个莫名其妙的函数。还有一个问题我倒最后也没搞懂Pyside2不是对应python3.x与Qt5么那为什么Maya使用python2.7却可以调用Pyside2呢是因为预编译了对应的库么
# 学习建议
我个人建议如果你想深入地使用pyside2开发插件强烈推荐先去学习一到两个月的Qt。之后再来学习pyside2你就会非常的顺利。尤其需要了解的是Qt的信号与槽机制、GUI绘制与线程、Qt事件传递机制。
# 实用代码
## 防止窗口重复创建
```
def main():
global win
try:
win.close() # 为了不让窗口出现多个因为第一次运行还没初始化所以要try在这里尝试先关闭再重新新建一个窗口
except:
pass
//MainWindow为窗口类
win = MainWindow()
win.show()
```
## 信号槽与解决生命周期问题
```
//其中SIGNAL需要先导入
from PySide2.QtCore import SIGNAL, QObject
class MainWindow(QWidget, Ui_Form):
def slotBtnClicked(self):
//为了防止消息框易一出现就被回收,需要给它设置父对象
msgBox = QMessageBox(self)
msgBox.setText(u"The document has been modified.")
msgBox.setInformativeText(u"Do you want to save your changes?")
msgBox.setStandardButtons(QMessageBox.Save)
msgBox.setDefaultButton(QMessageBox.Save)
msgBox.show()
def __init__(self, parent=None):
self.pushButton_stop.clicked.connect(self.slotBtnClicked)
QObject.connect(self.pushButton_targetSkin,SIGNAL('clicked()'), self.slotBtnClicked)
```
## python HumanIK
相关的控制代码可以在 安装目录\scripts\others下搜索hik找到主要在hikCharacterControlsUI.mel与hikGlobalUtils.mel文件中也可以参考我插件中的代码。
## FBX导出命令
导入文件可以只用cmds.file命令但是导出就不太好用了比如需要烘焙动画什么的所以需要调用以下Mel命令。
```
# FBX Exporter options. Set as required.
# You can find a reference guide here: http://download.autodesk.com/us/fbx/20112/Maya/_index.html
# Just add/change what you need.
# Geometry
mm.eval("FBXExportSmoothingGroups -v true")
mm.eval("FBXExportHardEdges -v false")
mm.eval("FBXExportTangents -v false")
mm.eval("FBXExportSmoothMesh -v true")
mm.eval("FBXExportInstances -v false")
mm.eval("FBXExportReferencedContainersContent -v false")
# Animation
mm.eval("FBXExportBakeComplexAnimation -v true")
mm.eval("FBXExportBakeComplexStart -v "+str(exportStartFrame[x]))
mm.eval("FBXExportBakeComplexEnd -v "+str(exportEndFrame[x]))
mm.eval("FBXExportBakeComplexStep -v 1")
# mm.eval("FBXExportBakeResampleAll -v true")
mm.eval("FBXExportUseSceneName -v false")
mm.eval("FBXExportQuaternion -v euler")
mm.eval("FBXExportShapes -v true")
mm.eval("FBXExportSkins -v true")
# Constraints
mm.eval("FBXExportConstraints -v false")
# Cameras
mm.eval("FBXExportCameras -v false")
# Lights
mm.eval("FBXExportLights -v false")
# Embed Media
mm.eval("FBXExportEmbeddedTextures -v false")
# Connections
mm.eval("FBXExportInputConnections -v false")
# Axis Conversion
mm.eval("FBXExportUpAxis y")
# Export!
mm.eval("FBXExport -f "+exportNames[x]+".fbx -s")
```

View File

@@ -0,0 +1,391 @@
---
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的实现方法。

View File

@@ -0,0 +1,373 @@
---
title: 基于插件的StaticMesh多Pass绘制方案
date: 2022-11-04 10:26:59
excerpt:
tags:
rating: ⭐
---
## 前言
StaticMesh与SkeletalMesh的实现方法比较类似不过两者的绘制方式有很大的不同。不过庆幸的是Static的MeshBatch设置在绘制函数的外面。
## 思路说明
### 创建自定义StaticMeshComponent
1. 继承UStaticMeshComponent,重写GetUsedMaterials与CreateSceneProxy函数。
2. 添加用于多pass渲染的UMaterialInterface指针。NeedSecondPass变量其实可以不用因为既然你要使用这个类那肯定要启用这个功能
```c++
#pragma once
#include "CoreMinimal.h"
#include "Engine/Classes/Components/StaticMeshComponent.h"
#include "StrokeStaticMeshComponent.generated.h"
UCLASS(ClassGroup=(Rendering, Common), hidecategories=Object, editinlinenew, meta=(BlueprintSpawnableComponent))
class UStrokeStaticMeshComponent : public UStaticMeshComponent
{
GENERATED_UCLASS_BODY()
//~ Begin UPrimitiveComponent Interface
virtual void GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials = false) const override;
virtual FPrimitiveSceneProxy* CreateSceneProxy() override;
//~ End UPrimitiveComponent Interface
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
UMaterialInterface* SecondPassMaterial = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MultiplePass")
bool NeedSecondPass=false;
};
```
3. 实现GetUsedMaterials与CreateSceneProxy函数并包含相应头文件。
### 头文件
```
#include "StrokeStaticMeshSceneProxy.h"
```
### GetUsedMaterials
```c++
void UStrokeStaticMeshComponent::GetUsedMaterials(TArray<UMaterialInterface*>& OutMaterials, bool bGetDebugMaterials /*= false*/) const
{
if (GetStaticMesh() && GetStaticMesh()->RenderData)
{
TMap<int32, UMaterialInterface*> MapOfMaterials;
for (int32 LODIndex = 0; LODIndex < GetStaticMesh()->RenderData->LODResources.Num(); LODIndex++)
{
FStaticMeshLODResources& LODResources = GetStaticMesh()->RenderData->LODResources[LODIndex];
int32 MaterialNum = 0;
for (int32 SectionIndex = 0; SectionIndex < LODResources.Sections.Num(); SectionIndex++)
{
// Get the material for each element at the current lod index
int32 MaterialIndex = LODResources.Sections[SectionIndex].MaterialIndex;
if (!MapOfMaterials.Contains(MaterialIndex))
{
MapOfMaterials.Add(MaterialIndex, GetMaterial(MaterialIndex));
MaterialNum++;
}
}
//这里是我添加的代码
if (NeedSecondPass)
{
bool NeedAddMaterial = true;
for (int i = 0; i < MapOfMaterials.Num(); ++i)
{
if (MapOfMaterials[i]== SecondPassMaterial)
{
NeedAddMaterial = false;
}
}
if (NeedAddMaterial)
{
MapOfMaterials.Add(MaterialNum, SecondPassMaterial);
}
}
}
if (MapOfMaterials.Num() > 0)
{
//We need to output the material in the correct order (follow the material index)
//So we sort the map with the material index
MapOfMaterials.KeySort([](int32 A, int32 B) {
return A < B; // sort keys in order
});
//Preadd all the material item in the array
OutMaterials.AddZeroed(MapOfMaterials.Num());
//Set the value in the correct order
int32 MaterialIndex = 0;
for (auto Kvp : MapOfMaterials)
{
OutMaterials[MaterialIndex++] = Kvp.Value;
}
}
}
}
```
### CreateSceneProxy
```c++
FPrimitiveSceneProxy* UStrokeStaticMeshComponent::CreateSceneProxy()
{
if (GetStaticMesh() == nullptr || GetStaticMesh()->RenderData == nullptr)
{
return nullptr;
}
const TIndirectArray<FStaticMeshLODResources>& LODResources = GetStaticMesh()->RenderData->LODResources;
if (LODResources.Num() == 0 || LODResources[FMath::Clamp<int32>(GetStaticMesh()->MinLOD.Default, 0, LODResources.Num() - 1)].VertexBuffers.StaticMeshVertexBuffer.GetNumVertices() == 0)
{
return nullptr;
}
//与SkeletalMeshComponent中写的类似直接通过new来创建
FPrimitiveSceneProxy* Proxy = ::new FStrokeStaticMeshSceneProxy(this, false);
#if STATICMESH_ENABLE_DEBUG_RENDERING
SendRenderDebugPhysics(Proxy);
#endif
return Proxy;
}
```
### 创建自定义StaticMeshSceneProxy
**这里与SkeletalMeshSceneProxy不同GetDynamicMeshElements没有起到绘制作用真正起到作用的是DrawStaticElements。**
1. 继承FStaticMeshSceneProxy,重写DrawStaticElements函数。
2. 定义UStaticMeshComponent指针用于获取自定义StaticMeshComponent的变量。之后使用需要使用强制转换
```c++
#pragma once
#include "Engine\Public\StaticMeshResources.h"
class FStrokeStaticMeshSceneProxy : public FStaticMeshSceneProxy
{
public:
//在UStrokeStaticMeshComponent中调用这个构造函数初始化
FStrokeStaticMeshSceneProxy(UStaticMeshComponent* Component, bool bForceLODsShareStaticLighting);
virtual void DrawStaticElements(FStaticPrimitiveDrawInterface* PDI) override;
private:
UPROPERTY()
const UStaticMeshComponent* ComponentPtr;
};
```
3. 复制DrawStaticElements中所需函数的代码。
将Engine\Source\Runtime\Engine\Private\StaticMeshRender.cpp中的AllowShadowOnlyMesh、UseLightPropagationVolumeRT2函数代码复制到你定义的StaticMeshSceneProxy类所在的cpp文件中。
4. 实现DrawStaticElements函数并包含头文件。
与SkeletalMesh的绘制函数不同它有3处需要调用绘制函数的地方。其中有一处因为不存在lod所以代码有些不同所以请不要在未测试的情况下使用本插件进行生产项目。
不过一个好消息是我们可以在绘制前设置MeshBatch从而完美结局描边问题。
```c++
void FStrokeStaticMeshSceneProxy::DrawStaticElements(FStaticPrimitiveDrawInterface* PDI)
{
checkSlow(IsInParallelRenderingThread());
if (!HasViewDependentDPG())
{
// Determine the DPG the primitive should be drawn in.
uint8 PrimitiveDPG = GetStaticDepthPriorityGroup();
int32 NumLODs = RenderData->LODResources.Num();
//Never use the dynamic path in this path, because only unselected elements will use DrawStaticElements
bool bIsMeshElementSelected = false;
const auto FeatureLevel = GetScene().GetFeatureLevel();
//check if a LOD is being forced
if (ForcedLodModel > 0)
{
int32 LODIndex = FMath::Clamp(ForcedLodModel, ClampedMinLOD + 1, NumLODs) - 1;
const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
// Draw the static mesh elements.
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
#if WITH_EDITOR
if (GIsEditor)
{
const FLODInfo::FSectionInfo& Section = LODs[LODIndex].Sections[SectionIndex];
bIsMeshElementSelected = Section.bSelected;
PDI->SetHitProxy(Section.HitProxy);
}
#endif // WITH_EDITOR
const int32 NumBatches = GetNumMeshBatches();
PDI->ReserveMemoryForMeshes(NumBatches);
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
FMeshBatch MeshBatch;
if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, MeshBatch))
{
PDI->DrawMesh(MeshBatch, FLT_MAX);
//以下是我添加的代码
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[SectionIndex].Material;
const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
{
continue;
}
if (StrokeStaticMeshComponent->NeedSecondPass) {
if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
//设置反转剔除选项
MeshBatch.ReverseCulling = true;
PDI->DrawMesh(MeshBatch, FLT_MAX);
}
}
}
}
}
else //no LOD is being forced, submit them all with appropriate cull distances
{
for (int32 LODIndex = ClampedMinLOD; LODIndex < NumLODs; LODIndex++)
{
const FStaticMeshLODResources& LODModel = RenderData->LODResources[LODIndex];
float ScreenSize = GetScreenSize(LODIndex);
bool bUseUnifiedMeshForShadow = false;
bool bUseUnifiedMeshForDepth = false;
if (GUseShadowIndexBuffer && LODModel.bHasDepthOnlyIndices)
{
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
// The shadow-only mesh can be used only if all elements cast shadows and use opaque materials with no vertex modification.
// In some cases (e.g. LPV) we don't want the optimization
bool bSafeToUseUnifiedMesh = AllowShadowOnlyMesh(FeatureLevel);
bool bAnySectionUsesDitheredLODTransition = false;
bool bAllSectionsUseDitheredLODTransition = true;
bool bIsMovable = IsMovable();
bool bAllSectionsCastShadow = bCastShadow;
for (int32 SectionIndex = 0; bSafeToUseUnifiedMesh && SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
const FMaterial* Material = ProxyLODInfo.Sections[SectionIndex].Material->GetRenderProxy()->GetMaterial(FeatureLevel);
// no support for stateless dithered LOD transitions for movable meshes
bAnySectionUsesDitheredLODTransition = bAnySectionUsesDitheredLODTransition || (!bIsMovable && Material->IsDitheredLODTransition());
bAllSectionsUseDitheredLODTransition = bAllSectionsUseDitheredLODTransition && (!bIsMovable && Material->IsDitheredLODTransition());
const FStaticMeshSection& Section = LODModel.Sections[SectionIndex];
bSafeToUseUnifiedMesh =
!(bAnySectionUsesDitheredLODTransition && !bAllSectionsUseDitheredLODTransition) // can't use a single section if they are not homogeneous
&& Material->WritesEveryPixel()
&& !Material->IsTwoSided()
&& !IsTranslucentBlendMode(Material->GetBlendMode())
&& !Material->MaterialModifiesMeshPosition_RenderThread()
&& Material->GetMaterialDomain() == MD_Surface;
bAllSectionsCastShadow &= Section.bCastShadow;
}
if (bSafeToUseUnifiedMesh)
{
bUseUnifiedMeshForShadow = bAllSectionsCastShadow;
// Depth pass is only used for deferred renderer. The other conditions are meant to match the logic in FDepthPassMeshProcessor::AddMeshBatch.
bUseUnifiedMeshForDepth = ShouldUseAsOccluder() && GetScene().GetShadingPath() == EShadingPath::Deferred && !IsMovable();
if (bUseUnifiedMeshForShadow || bUseUnifiedMeshForDepth)
{
const int32 NumBatches = GetNumMeshBatches();
PDI->ReserveMemoryForMeshes(NumBatches);
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
FMeshBatch MeshBatch;
if (GetShadowMeshElement(LODIndex, BatchIndex, PrimitiveDPG, MeshBatch, bAllSectionsUseDitheredLODTransition))
{
bUseUnifiedMeshForShadow = bAllSectionsCastShadow;
MeshBatch.CastShadow = bUseUnifiedMeshForShadow;
MeshBatch.bUseForDepthPass = bUseUnifiedMeshForDepth;
MeshBatch.bUseAsOccluder = bUseUnifiedMeshForDepth;
MeshBatch.bUseForMaterial = false;
PDI->DrawMesh(MeshBatch, ScreenSize);
//以下是我添加的代码
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[0].Material;
const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
{
continue;
}
if (StrokeStaticMeshComponent->NeedSecondPass) {
if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
//通过MeshBatch设置Material与反转剔除选项
MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
//设置反转剔除选项
MeshBatch.ReverseCulling = true;
PDI->DrawMesh(MeshBatch, FLT_MAX);
}
}
}
}
}
}
// Draw the static mesh elements.
for (int32 SectionIndex = 0; SectionIndex < LODModel.Sections.Num(); SectionIndex++)
{
#if WITH_EDITOR
if (GIsEditor)
{
const FLODInfo::FSectionInfo& Section = LODs[LODIndex].Sections[SectionIndex];
bIsMeshElementSelected = Section.bSelected;
PDI->SetHitProxy(Section.HitProxy);
}
#endif // WITH_EDITOR
const int32 NumBatches = GetNumMeshBatches();
PDI->ReserveMemoryForMeshes(NumBatches);
for (int32 BatchIndex = 0; BatchIndex < NumBatches; BatchIndex++)
{
FMeshBatch MeshBatch;
if (GetMeshElement(LODIndex, BatchIndex, SectionIndex, PrimitiveDPG, bIsMeshElementSelected, true, MeshBatch))
{
// If we have submitted an optimized shadow-only mesh, remaining mesh elements must not cast shadows.
MeshBatch.CastShadow &= !bUseUnifiedMeshForShadow;
MeshBatch.bUseAsOccluder &= !bUseUnifiedMeshForDepth;
MeshBatch.bUseForDepthPass &= !bUseUnifiedMeshForDepth;
PDI->DrawMesh(MeshBatch, ScreenSize);
//以下是我添加的代码
const FLODInfo& ProxyLODInfo = LODs[LODIndex];
UMaterialInterface* MaterialInterface = ProxyLODInfo.Sections[SectionIndex].Material;
const UStrokeStaticMeshComponent* StrokeStaticMeshComponent = dynamic_cast<const UStrokeStaticMeshComponent *>(ComponentPtr);
if (MaterialInterface == StrokeStaticMeshComponent->SecondPassMaterial)
{
continue;
}
if (StrokeStaticMeshComponent->NeedSecondPass) {
if (StrokeStaticMeshComponent->SecondPassMaterial == nullptr) {
continue;
}
MeshBatch.MaterialRenderProxy = StrokeStaticMeshComponent->SecondPassMaterial->GetRenderProxy();
//
MeshBatch.ReverseCulling = true;
PDI->DrawMesh(MeshBatch, FLT_MAX);
}
}
}
}
}
}
}
}
```
## 插件地址及使用方法
### 插件地址
https://github.com/blueroseslol/BRPlugins
### 使用方法
1. 创建一个Actor蓝图。
2. 创建你定义的StaticMeshComponent。
3. 赋予轮廓材质并勾选NeedSecondPass。
使用这个材质的效果:

View File

@@ -0,0 +1,84 @@
---
title: 虚幻商城发布插件指南
date: 2022-11-04 10:17:18
excerpt: 摘要
tags:
rating: ⭐
---
## 前言
前段时间本人尝试发布自己写的插件到Market Place因为每次审核会需要1~3天时间不等所以在这里分享一下我的经验
## 添加插件描述
- CreatedByURL一般填写作者介绍的网址github仓库与主页、blog、小蓝鸟都可以的。
- Description对插件的简单描述。
- DocsURL文档网址这里我链接到自己的blog里的文章。也可以使用Github建的主页。
- MarketplaceURL在点击Add Product后就会生成一个产品ID之后将ID替换下面的URL即可。比如我的产品地址为https://publish.unrealengine.com/v3/edit-product/657c763ab2e244edbcca955eb81d8377
- SupportURL支持网址一般会使用discord。
额外需要注意的就是模块白名单(黑名单),这个需要与发布页面的**Supported Target Platforms**对应。EPIC会在审查时进行构建测试所以最好是对应平台编译测试通过添加此平台。
以下是我的插件描述仅供参考:
```
"CreatedByURL": "http://www.blueroses.top/",
"Description": "This plugin uses UnrealEngine's MeshDraw framework to achieve a multi-Pass effect looks like Unity's, with no post-processing required to achieve strokes and cutoff.",
"DocsURL": "http://www.blueroses.top/2022/05/02/multidraw-document/",
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/657c763ab2e244edbcca955eb81d8377",
"SupportURL": "https://discord.gg/y4fWqYsf4U",
"Modules": [
    {
        "Name": "MultiDraw",
        "Type": "Runtime",
        "LoadingPhase": "Default",
        "WhitelistPlatforms": [ "Win64" ]
    }
]
```
## Code
给所有的代码文件都加上版权声明模块cs、h、cpp文件都需要例如我这里填写了
>// Copyright 2022 BlueRose, Inc. All Rights Reserved.
有一个技巧在项目创建时可以在Project-Description-Legal-CopyrightNotice里填写之后创建新类都会自动加上版权信息。
## Content
所有会被引用的二进制Asset都必须放置到插件的Content里最后还需要使用Fix Up Redirectors In Folder对Redirector进行清理。
如果你有使用UE的ContentExample项目中的DemoRoom或者其他EPIC项目的资源那就需要注意排查将带有UnrealEngine、EPIC、UE4等Logo标志、符号的Texture与Materials都删除不然系统检查会无法通过。
案例关卡的可以放在插件的Content里但建议还是将其分离前期可能会修改2~3次再加上还需要构建其他版本的插件如果将案例关卡内容放在插件里就会占用可观的网盘空间。
## 打包与构建测试
使用构建Bat来快速构建多个版本的插件压缩包文件。
>"D:\UnrealEngine\UE_5.0\Engine\Build\BatchFiles\RunUAT.bat" BuildPlugin -Plugin="D:\UnrealEngine\Project\MarketPlaceProject\Plugins\MultiDraw\MultiDraw.uplugin" -Package="D:\UnrealEngine\Project\Plugins\MultiDraw\" -Rocket -VS2019
为了能看到Log我们可以使用
>cmd /k "xxxx.bat"  
打包插件完成之后上传之前把Binaries与Intermediate删除压缩成zip格式上传到google或者onedriver之类的就可以了。
## 截图
画廊一般会放置截图而且这些图片必须为1080p分辨率所以本人这里使用了UE内置的截图功能
https://docs.unrealengine.com/4.27/zh-CN/WorkingWithMedia/CapturingMedia/TakingScreenshots/
>HighResShot filename=PATH (XxY OR Multiplier) CaptureX CaptureY CaptureW CaptureH bMaskUsingCustomDepth bDumpBufferVisualizationTargets bCaptureHDR bDateTimeAsFilename
- XxY:指定截图的尺寸宽度x高度
- 乘数Multiplier:根据指定的值放大截图尺寸。
- CaptureX CaptureY CaptureW CaptureH:用整数定义视口中要用于截图的区域。
- bMaskUsingCustomDepth:用一个布尔值0或1来控制是否要用自定义深度Custom Depth缓冲作为捕获的遮罩。详情请参见自定义深度遮罩。
- bDumpBufferVisualizationTargets:用一个布尔值0或1来控制是否要将GBuffer中的每一个通道都捕获为一张图片并将其导出。
- bCaptureHDR:用一个布尔值0或1来控制是否要使用.EXR文件格式来捕获HDR图片。
- bDateTimeAsFilename:用一个布尔值0或1来控制是否要在生成的文件名中加入时间戳。
因为展示需要1080p所以我们只需要使用这个命令即可。
>HighResShot 1920x1080
## 视频推荐
我推荐查看以下这3个视频
Unreal Engine How To Sell Plugins #1: "Create Plugin"
https://www.youtube.com/watch?v=v3_HkAU_rls
Unreal Engine How To Sell Plugins #3: "PackagePlugin"
https://www.youtube.com/watch?v=SKArLiShb9c
Unreal Engine How To Sell Plugins #4: "Submitting Plugin for Approval"
https://www.youtube.com/watch?v=-6QYlGVRIkI
## 本人插件
![](https://cdn.jsdelivr.net/gh/blueroseslol/ImageBag@latest/ImageBag/Images/MultiDraw_MarketPlace.png)
想制作backface outline、Fur没在插件中实现等需要多Pass的功能的效果可以考虑使用我的插件
https://www.unrealengine.com/marketplace/en-US/product/multidraw