Choose your operating system:
Windows
macOS
Linux
在创建允许玩家自定义角色并切换不同部位的系统时,例如不同的头部或身体类型、衣着或其他选项,你需要以模块化方式构造角色。作为取代完整角色骨架网格体的替代方法,可以将骨架网格体分解成多个分段,例如躯干、腿和头,然后将它们导入到引擎中;接下来你可以使用本页所述的一些方法来组装这些部件并添加动画。这样不仅提高了生成不同角色的灵活性,也可以提高性能。
主姿势组件
主姿势组件 是一个可以通过蓝图调用的函数,你可以用它将一个 有皮肤网格体组件对象 (或多个有皮肤网格体组件对象)设置为另一个打算用作主对象的有皮肤网格体组件对象的子代。例如,你可以将躯干定义为主姿势组件,为躯干分配动画,然后添加脚、腿、手和头作为子代,这些部位将跟随为躯干分配的动画而动。
在后台,子代不使用任何骨骼变换缓冲,即使对子代设置了动画也不会运行任何动画,在渲染时,子代仅使用躯干的骨骼变换缓冲,这样就会形成一个超轻量级的连接系统。唯一必须运行动画的组件是躯干,所有连接的组件都将使用躯干的骨骼变换。在以下示例设置中,我们为躯干分配了动画。我们使用蓝图中的构造脚本将骨架网格体设置为 主骨骼组件 ,而我们模块化角色其他方面则作为子代。
在上图中,我们在蓝图内部使用
构造脚本(Construction Script)
将躯干骨架网格体设置为
主骨骼组件(Master Bone Component)
,并将其他模块化角色部位设置为子代。
设置主姿势组件 功能还有一个布尔型参数,名为 强制更新(Force Update) 。如果强制更新(Force Update)被禁用,则在所有运行时信息与输入主组件相同时则跳过更新。如果启用强制更新,将强制更新运行时信息。这只适用于注册期间,因为注册是可以序列化的,并且需要刷新所有运行时数据。
角色的每个方面都是一个骨架网格体,我们可以换成另一个 骨架网格体组件 。 在下图中,我们关闭了躯干和脚的显示(我们可以将它们更换为具有相同骨架层级的不同的骨架网格体)。
但有一个需要考虑的问题,就是在使用 主姿势组件 时,虽然会降低游戏线程成本,但不会降低渲染成本。你仍需要单独渲染相同数量的组件,需要牢记的是每个组件的分段越多,就会需要越多的绘制调用。
还有一个限制,即主骨骼的任何子代都必须是具有完全匹配结构的子集,不能有任何多余的关节或省略任何关节。由于多余关节没有骨骼缓冲数据,因此将使用引用姿势进行渲染。此外,还无法对任何子代运行任何其他动画或物理效果。
从网格体复制姿势
从网格体复制姿势
是一个可以在子代的
动画蓝图
上使用的
动画图形
节点,允许你从任意
骨架网格体组件
复制动画姿势。从网格体复制姿势不仅复制匹配的骨骼,还会复制使用引用姿势的所有内容。但是,你可以如下图所示,在任何复制的变换上播放动画。
在使用 从网格体复制姿势 时,你需要确保作为复制来源的骨架网格体组件已经有了tick事件,否则将复制最后一帧动画(例如,从身体进行复制且头是子代)。为确保身体有tick事件,可以将头与身体连接起来,这样就可以确保父代先发生tick事件,而后轮到子代。
你还可以用代码设置此关系。如果将其设置为先决条件,便可确保它们先发生tick事件,而后轮到当前组件。请参阅 Tick依赖关系 页面以了解更多信息。
使用"从网格体复制姿势"时要考虑的一些要素包括,它比主姿势组件的开销更大,因为它会在每个子代上运行动画。此外,如果想要在子代上使用物理效果,则需要改为使用 刚体 或 AnimDynamics 骨架控制节点。
在动画编辑器中预览动画时,可以指定更多将自动使用"从网格体复制姿势"的网格体。你还可以创建自定义 预览网格体集合 ,用来构建一起制作动画的相关骨架网格体集合(例如,一个角色的组成部分)。下面我们将说明如何为预览更改和分配不同的骨架网格体,以便为角色切换不同的头部。
骨架网格体合并
你可以在运行时使用代码 [`FSkeletalMeshMerge`]() 将多个骨架网个体合并和一个骨架网格体。虽然创建骨架网个体的初始开销很高,但因为只需要一个骨架网格体,而不是多个,所以渲染开销会更低。例如,如果你的角色只有3个组件(头部、身体和腿),同时屏幕上会出现50个角色,这就需要 50次绘制调用 。如果不使用骨架网格体合并,每个组件都需要单独绘制调用,这样每个角色都需要绘制调用三次,总共就需要 150次绘制调用 .
在使用
FSkeletalMeshMerge
时,你的主"身体"必须包含所有动画,因为合并后的网格体仅使用设置的骨架,而不包含你需要添加动画的所有关节。如果你的某些身体部位有额外的关节,还必须在身体上包含所有动画。其他要考虑的事情包括,只能在合并的网格体上运行一个动画,而且不支持将变形目标传输到合并的网格体。但是,如果查看
FSkeletalMeshMerge::GenerateLODModel
,当你有了骨架网格体后,就可以通过计算基本网格体和任意变形之间的
FMorphTargetDelta
来创建变形目标。
此外,在使用
FSkeletalMeshMerge
时,你很可能需要从头开始以一种特定方式构建内容。你需要使用一个常用材质,并决定纹理图集(例如,靴子放这个区域,手套放这个区域等),这样就可以对纹理进行裁剪和拼接,创建新纹理并将整个角色渲染为一个分段。
网格体合并示例
在以下示例中,我们使用网格体合并代码在运行时组装多个骨架网格体。
在上图中,我们需要在运行时连接多个骨架网格体以形成一个骨架网格体。在该示例中,我们创建一个可以通过蓝图调用的函数,名为 网格体合并(Mesh Merge) ,它将让我们能够定义想要合并起来的网格体。需要做的第一件事是根据 蓝图函数库 创建C++类,以便从任意蓝图调用该函数,我们将其命名为 MeshMergeFunctionLibrary 。
下面我们提供了相同的代码块,供你在你的 标头 和 源代码 文件中使用:
.h示例代码
//在项目设置的"描述"页面中填写版权声明。
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "UObject/NoExportTypes.h"
#include "MeshMergeFunctionLibrary.generated.h"
/**
* 等效于FSkeleMeshMergeSectionMapping的蓝图
* 用于将单个源骨架网格体的所有分段映射到
* 合并后骨架网格体中的最后一个分段条目的信息。
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkelMeshMergeSectionMapping_BP
{
GENERATED_BODY()
/**合并后骨架网格体的最终分段条目的索引*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh Merge Params")
TArray < int32 > SectionIDs;
};
/**
* 用于包含一个网格体的一组UV变换。
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkelMeshMergeUVTransform
{
GENERATED_BODY()
/** 在给定网格体上应如何变换UV的列表,其中索引表示具体的UV通道。*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh Merge Params")
TArray < FTransform > UVTransforms;
};
/**
* 等效于FSkelMeshMergeUVTransforms的蓝图
* 用于映射所有分段的有关如何变换UV的信息
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkelMeshMergeUVTransformMapping
{
GENERATED_BODY()
/** 对于每个网格体上的每个UV通道,描述应如何变换UVS。*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Mesh Merge Params")
TArray < FSkelMeshMergeUVTransform > UVTransformsPerMesh;
};
/**
* 包含用于执行骨架网格体合并的所有参数的结构。
*/
USTRUCT(BlueprintType)
struct PROJECTNAME_API FSkeletalMeshMergeParams
{
GENERATED_BODY()
FSkeletalMeshMergeParams()
{
StripTopLODS = 0;
bNeedsCpuAccess = false;
bSkeletonBefore = false;
Skeleton = nullptr;
}
// 一个可选数组,用于将源网格体的分段映射到合并后的分段条目
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray < FSkelMeshMergeSectionMapping_BP > MeshSectionMappings;
// 一个可选数组,用于变换每个网格体中的UV
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray < FSkelMeshMergeUVTransformMapping > UVTransformsPerMesh;
// 要合并的骨架网格体的列表。
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TArray < USkeletalMesh* > MeshesToMerge;
// 要从输入网格体移除的高LOD的数量
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 StripTopLODS;
// CPU是否会因任何原因(例如,产生粒子效果)而需要访问所产生的网格体。
UPROPERTY(EditAnywhere, BlueprintReadWrite)
uint32 bNeedsCpuAccess :1;
// 先更新骨架再合并。否则,合并后更新。
// 还必须提供骨架。
UPROPERTY(EditAnywhere, BlueprintReadWrite)
uint32 bSkeletonBefore :1;
// 将用于合并后网格体的骨架。
// 如果生成的骨架正常,则留空。
UPROPERTY(EditAnywhere, BlueprintReadOnly)
class USkeleton* Skeleton;
};
/**
*
*/
UCLASS()
class PROJECTNAME_API UMeshMergeFunctionLibrary : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
/**
* 将指定的网格体合并为一个网格体。
* @return合并的网格体(如果合并失败将失效)。
*/
UFUNCTION(BlueprintCallable, Category = "Mesh Merge", meta = (UnsafeDuringActorConstruction = "true"))
static class USkeletalMesh* MergeMeshes(const FSkeletalMeshMergeParams& Params);
};
在标头中,需要将所有 PROJECTNAME_API 引用更改为引用实际项目名称。例如,如果项目名为"MyProject",需要在所有出现的引用中改为使用 MYPROJECT_API ,这样代码才能起作用。
.cpp示例代码
//在项目设置的"描述"页面中填写版权声明。
#include "MeshMergeFunctionLibrary.h"
#include "SkeletalMeshMerge.h"
#include "Engine/SkeletalMeshSocket.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/Skeleton.h"
static void ToMergeParams(const TArray<FSkelMeshMergeSectionMapping_BP>& InSectionMappings, TArray<FSkelMeshMergeSectionMapping>& OutSectionMappings)
{
if (InSectionMappings.Num() > 0)
{
OutSectionMappings.AddUninitialized(InSectionMappings.Num());
for (int32 i = 0; i < InSectionMappings.Num(); ++i)
{
OutSectionMappings[i].SectionIDs = InSectionMappings[i].SectionIDs;
}
}
};
static void ToMergeParams(const TArray<FSkelMeshMergeUVTransformMapping>& InUVTransformsPerMesh, TArray<FSkelMeshMergeUVTransforms>& OutUVTransformsPerMesh)
{
if (InUVTransformsPerMesh.Num() > 0)
{
OutUVTransformsPerMesh.Empty();
OutUVTransformsPerMesh.AddUninitialized(InUVTransformsPerMesh.Num());
for (int32 i = 0; i < InUVTransformsPerMesh.Num(); ++i)
{
TArray<TArray<FTransform>>& OutUVTransforms = OutUVTransformsPerMesh[i].UVTransformsPerMesh;
const TArray<FSkelMeshMergeUVTransform>& InUVTransforms = InUVTransformsPerMesh[i].UVTransformsPerMesh;
if (InUVTransforms.Num() > 0)
{
OutUVTransforms.Empty();
OutUVTransforms.AddUninitialized(InUVTransforms.Num());
for (int32 j = 0; j < InUVTransforms.Num(); j++)
{
OutUVTransforms[i] = InUVTransforms[i].UVTransforms;
}
}
}
}
};
USkeletalMesh* UMeshMergeFunctionLibrary::MergeMeshes(const FSkeletalMeshMergeParams& Params)
{
TArray<USkeletalMesh*> MeshesToMergeCopy = Params.MeshesToMerge;
MeshesToMergeCopy.RemoveAll([](USkeletalMesh* InMesh)
{
return InMesh == nullptr;
});
if (MeshesToMergeCopy.Num() <= 1)
{
UE_LOG(LogTemp, Warning, TEXT("Must provide multiple valid Skeletal Meshes in order to perform a merge."));
return nullptr;
}
EMeshBufferAccess BufferAccess = Params.bNeedsCpuAccess ?
EMeshBufferAccess::ForceCPUAndGPU :
EMeshBufferAccess::Default;
TArray<FSkelMeshMergeSectionMapping> SectionMappings;
TArray<FSkelMeshMergeUVTransforms> UvTransforms;
ToMergeParams(Params.MeshSectionMappings, SectionMappings);
ToMergeParams(Params.UVTransformsPerMesh, UvTransforms);
bool bRunDuplicateCheck = false;
USkeletalMesh* BaseMesh = NewObject<USkeletalMesh>();
if (Params.Skeleton && Params.bSkeletonBefore)
{
BaseMesh->Skeleton = Params.Skeleton;
bRunDuplicateCheck = true;
for (USkeletalMeshSocket* Socket : BaseMesh->GetMeshOnlySocketList())
{
if (Socket)
{
UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocket: %s"), *(Socket->SocketName.ToString()));
}
}
for (USkeletalMeshSocket* Socket : BaseMesh->Skeleton->Sockets)
{
if (Socket)
{
UE_LOG(LogTemp, Warning, TEXT("SkelSocket: %s"), *(Socket->SocketName.ToString()));
}
}
}
FSkeletalMeshMerge Merger(BaseMesh, MeshesToMergeCopy, SectionMappings, Params.StripTopLODS, BufferAccess, UvTransforms.GetData());
if (!Merger.DoMerge())
{
UE_LOG(LogTemp, Warning, TEXT("Merge failed!"));
return nullptr;
}
if (Params.Skeleton && !Params.bSkeletonBefore)
{
BaseMesh->Skeleton = Params.Skeleton;
}
if (bRunDuplicateCheck)
{
TArray<FName> SkelMeshSockets;
TArray<FName> SkelSockets;
for (USkeletalMeshSocket* Socket : BaseMesh->GetMeshOnlySocketList())
{
if (Socket)
{
SkelMeshSockets.Add(Socket->GetFName());
UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocket: %s"), *(Socket->SocketName.ToString()));
}
}
for (USkeletalMeshSocket* Socket : BaseMesh->Skeleton->Sockets)
{
if (Socket)
{
SkelSockets.Add(Socket->GetFName());
UE_LOG(LogTemp, Warning, TEXT("SkelSocket: %s"), *(Socket->SocketName.ToString()));
}
}
TSet<FName> UniqueSkelMeshSockets;
TSet<FName> UniqueSkelSockets;
UniqueSkelMeshSockets.Append(SkelMeshSockets);
UniqueSkelSockets.Append(SkelSockets);
int32 Total = SkelSockets.Num() + SkelMeshSockets.Num();
int32 UniqueTotal = UniqueSkelMeshSockets.Num() + UniqueSkelSockets.Num();
UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocketCount: %d | SkelSocketCount: %d | Combined: %d"), SkelMeshSockets.Num(), SkelSockets.Num(), Total);
UE_LOG(LogTemp, Warning, TEXT("SkelMeshSocketCount: %d | SkelSocketCount: %d | Combined: %d"), UniqueSkelMeshSockets.Num(), UniqueSkelSockets.Num(), UniqueTotal);
UE_LOG(LogTemp, Warning, TEXT("Found Duplicates: %s"), *((Total != UniqueTotal) ? FString("True") : FString("False")));
}
return BaseMesh;
}
当你在编辑器中编译完代码后,就会创建具有 骨架网格体组件 和 骨架网格体参数 类型的公开变量的ActorBlueprint。该变量将为你提供多个属性,不仅可以定义要合并的骨架网格体,还可以定义如何合并这些网格体和其他选项。
以下是你可以用来定义如何合并骨架网格体的选项:
属性 |
说明 |
---|---|
网格体分段映射(Mesh Section Mappings) |
这是一个可选数组,用于将源网格体的分段映射到合并后的分段条目。 |
每个网格体的UV变换(UVTransforms Per Mesh) |
这是一个可选数组,用于变换每个网格体中的UV。 |
要合并的网格体(Meshes to Merge) |
这些是将要合并起来的骨架网格体。 |
分割顶级LOD(Strip Top LOD) |
要从输入网格体中移除的顶级LOD数量。 |
需要CPU访问(Needs Cpu Access) |
CPU是否会因任何原因(例如,产生粒子效果)而需要访问所产生的网格体。 |
骨架先于(Skeleton Before) |
是在合并之前还是之后更新骨架(还必须提供骨架)。 |
骨架(Skeleton) |
这是将用于合并后网格体的骨架。如果生成的骨架正常,则留空。 |
在
事件图形(Event Graph)
内部,在
事件开始播放(Event Begin Play)
时,使用以下节点网络。
单击查看大图。
你可以使用新的蓝图函数
合并网格体
,通过网格体合并参数传递来返回骨架网格体对象引用。然后可以使用添加到蓝图的
骨架网格体组件
作为目标,以设置要使用的新骨架网格体,并将它指向合并网格体函数调用的返回值。在以上示例中,我们还可以为骨架网格体分配闲置动画,以在所有网格体合并完成后播放。
在将网格体合并蓝图添加到关卡后,在
细节(Details)
面板内部,可以定义
网格体合并参数(Mesh Merge Parameters)
,包括要使用的
要使用的网格体(Meshes to Use)
和
骨架(Skeleton)
资源。
在运行时,网格体合并函数将根据定义的网格体执行并组装骨架网格体。
比较图表
无论你是使用
主姿势组件
、
从网格体复制姿势
还是
骨架网格体合并
,每种方法都存在一些优势和劣势。下表概括了各自关联的设置和性能成本,以及支持(或不支持)的其他功能。
主姿势 |
复制姿势 |
网格体合并 |
|
---|---|---|---|
设置成本 |
最小 |
中等 |
高 |
游戏线程成本 |
最小 |
高 |
中等 |
渲染线程成本 |
高 |
高 |
低 |
物理 |
否 |
AnimDynamics或刚体 |
是 |
变形目标 |
是 |
是 |
否 |